File size: 3,048 Bytes
9373226
 
 
 
 
 
 
 
5797a99
9373226
 
 
 
 
 
 
5797a99
 
 
 
 
 
9373226
 
 
 
 
 
 
 
 
 
5797a99
 
 
9373226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5797a99
 
 
 
 
 
 
 
9373226
 
5797a99
 
 
 
9373226
 
 
 
5797a99
9373226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5797a99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, Literal

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field

from services.config import settings
from services.dataset import SubstitutionDatabase
from services.recipe_service import RecipeAdapterService
from services.semantic import WordVectorFallback


BASE_DIR = Path(__file__).resolve().parent
ROOT_INDEX = BASE_DIR / "index.html"
STATIC_DIR = BASE_DIR / "static"
STATIC_INDEX = STATIC_DIR / "index.html"

app = FastAPI(title="BiteWise API", version="2.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

if STATIC_DIR.exists():
    app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")


class AdaptRequest(BaseModel):
    recipe_text: str = Field(min_length=5)
    diet: Literal["vegan", "keto", "both"] = "vegan"


_db = None
_semantic = None
_service = None


def get_service() -> RecipeAdapterService:
    global _db, _semantic, _service
    if _service is not None:
        return _service

    _db = SubstitutionDatabase(settings.dataset_path)
    _semantic = WordVectorFallback(
        model_name=settings.semantic_model_name,
        model_path=settings.semantic_model_path,
        enable_download=settings.enable_semantic_download,
    )
    _service = RecipeAdapterService(db=_db, semantic=_semantic)
    return _service


def _get_index_file() -> Path | None:
    if ROOT_INDEX.exists():
        return ROOT_INDEX
    if STATIC_INDEX.exists():
        return STATIC_INDEX
    return None


@app.get("/")
def root():
    index_file = _get_index_file()
    if index_file is not None:
        return FileResponse(str(index_file))

    return JSONResponse(
        {
            "name": "BiteWise API",
            "status": "running",
            "hint": "Put index.html in the repo root or in static/index.html",
        }
    )


@app.get("/health")
def health():
    return {"ok": True}


@app.get("/api/meta")
def meta():
    service = get_service()
    return {
        "ner_model": settings.ner_model_name,
        "qa_model": settings.qa_model_name,
        "semantic_model": settings.semantic_model_name,
        "semantic_available": service.semantic.available,
        "semantic_mode": service.semantic._kind,
        "dataset_path": str(settings.dataset_path),
    }


@app.post("/api/adapt")
def adapt(req: AdaptRequest) -> Dict[str, Any]:
    try:
        service = get_service()
        return service.adapt(req.recipe_text, req.diet)
    except FileNotFoundError as e:
        raise HTTPException(status_code=500, detail=str(e))
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")