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}")