Files
appetito/server/app/main.py
T

111 lines
3.6 KiB
Python
Raw Normal View History

2026-06-07 00:08:07 +02:00
import json
import logging
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from openai import AsyncOpenAI
from .config import settings
from .models import RecipeRequest, RecipeResponse
from .prompt import SYSTEM_PROMPT
logger = logging.getLogger("uvicorn")
_client: AsyncOpenAI | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global _client
_client = AsyncOpenAI(
api_key=settings.nvidia_api_key,
base_url=settings.nvidia_base_url,
timeout=float(settings.nvidia_timeout_seconds),
)
yield
await _client.close()
app = FastAPI(title="Recipe AI Server", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def _check_password(x_app_password: str = Header(...)):
if x_app_password != settings.app_password:
raise HTTPException(status_code=401, detail="Password non valida")
@app.get("/health")
async def health():
return {"status": "ok", "provider": "nvidia", "model": settings.nvidia_model}
@app.post("/auth/test", dependencies=[Depends(_check_password)])
async def auth_test():
return {"status": "ok"}
@app.post("/recipes/generate", response_model=RecipeResponse, dependencies=[Depends(_check_password)])
async def generate_recipes(request: RecipeRequest):
user_message = _build_user_message(request)
for attempt in range(3):
try:
stream = await _client.chat.completions.create(
model=settings.nvidia_model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=settings.nvidia_temperature,
top_p=settings.nvidia_top_p,
max_tokens=settings.nvidia_max_tokens,
extra_body={
"chat_template_kwargs": {"enable_thinking": True},
"reasoning_budget": settings.nvidia_reasoning_budget,
},
stream=True,
)
content = ""
async for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
if delta.content is not None:
content += delta.content
data = json.loads(content.strip())
return RecipeResponse(**data)
except json.JSONDecodeError:
if attempt == 2:
raise HTTPException(status_code=502, detail="Il modello non ha prodotto JSON valido dopo 3 tentativi")
logger.warning("Tentativo %d: JSON non valido, retry...", attempt + 1)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
def _build_user_message(request: RecipeRequest) -> str:
ingredients_str = "\n".join(
f"- {ing.name}: {ing.quantity} {ing.unit}" for ing in request.ingredients
)
p = request.preferences
return (
f"Ingredienti disponibili:\n{ingredients_str}\n\n"
f"Preferenze:\n"
f"- Porzioni: {p.servings}\n"
f"- Tempo massimo: {p.maxTimeMinutes} minuti\n"
f"- Difficoltà: {p.difficulty}\n"
f"- Dieta: {p.diet}\n"
f"- Allergie: {', '.join(p.allergies) or 'nessuna'}\n"
f"- Alimenti da evitare: {', '.join(p.avoid) or 'nessuno'}\n"
f"- Strumenti disponibili: {', '.join(p.tools) or 'standard'}\n\n"
"Genera 3 ricette in JSON."
)