add server: FastAPI + Docker
Python 3.11 + FastAPI con tre endpoint (/health, /auth/test, /recipes/generate). Autenticazione via header X-App-Password, validazione input Pydantic, chiamata in streaming a NVIDIA NIM (nemotron-3-ultra-550b-a55b) con thinking. Dockerizzato con docker-compose; configurazione tramite .env. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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."
|
||||
)
|
||||
Reference in New Issue
Block a user