Files
appetito/server/app/main.py
T
davide 78751f2c6d config: remove hardcoded defaults, add nvidia_enable_thinking setting
All LLM parameters must now be explicitly set in .env — no silent
fallbacks in code. Added nvidia_enable_thinking bool and wired it into
the chat completion extra_body instead of the previous hardcoded True.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:16:26 +02:00

111 lines
3.6 KiB
Python

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": settings.nvidia_enable_thinking},
"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."
)