import time, logging, json, traceback from typing import Optional, Dict, Any from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from model_pipeline import Predictor, FEATURE_MAP logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s" ) log = logging.getLogger("api") # ----------- input model ----------- class PredictIn(BaseModel): include_neg: bool = False Debitore_cluster: Optional[str] = None Stato_Giudizio: Optional[str] = None Cedente: Optional[str] = None # alias con spazi/punti Importo_iniziale_outstanding: Optional[float] = Field(None, alias="Importo iniziale outstanding") Decreto_sospeso: Optional[str] = Field(None, alias="Decreto sospeso") Notifica_Decreto: Optional[str] = Field(None, alias="Notifica Decreto") Opposizione_al_decreto_ingiuntivo: Optional[str] = Field(None, alias="Opposizione al decreto ingiuntivo") Ricorso_al_TAR: Optional[str] = Field(None, alias="Ricorso al TAR") Sentenza_TAR: Optional[str] = Field(None, alias="Sentenza TAR") Atto_di_Precetto: Optional[str] = Field(None, alias="Atto di Precetto") Decreto_Ingiuntivo: Optional[str] = Field(None, alias="Decreto Ingiuntivo") Sentenza_giudizio_opposizione: Optional[str] = Field(None, alias="Sentenza giudizio opposizione") giorni_da_iscrizione: Optional[int] = None giorni_da_cessione: Optional[int] = None Zona: Optional[str] = None model_config = {"populate_by_name": True, "extra": "allow"} # ----------- app ----------- app = FastAPI(title="Predizione+SHAP API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"] ) t0 = time.time() predictor: Predictor | None = None @app.on_event("startup") def _load_model(): global predictor predictor = Predictor() log.info(f"Model loaded in {predictor.load_seconds:.2f}s") @app.get("/health") def health(): return {"ok": predictor is not None, "uptime_s": time.time()-t0} @app.post("/predict") def predict(inp: PredictIn): if predictor is None: raise HTTPException(503, "Model not ready") # ricomponi payload secondo i nomi originali delle feature payload: Dict[str, Any] = {} for k in FEATURE_MAP.values(): ak = k.replace(" ", "_").replace(".", "_") payload[k] = getattr(inp, ak, None) payload["include_neg"] = inp.include_neg try: out = predictor.predict_dict(payload, include_neg=inp.include_neg) # assicura chiave 'class' (nessuna alias confusion) if "class_" in out and "class" not in out: out["class"] = out.pop("class_") log.info(json.dumps({ "event":"predict_ok", "class": out.get("class"), "stage": out.get("stage_used"), "p100": round(out.get("p100", 0.0), 4) })) return out except Exception as e: log.exception("predict_error") raise HTTPException(500, f"Prediction error: {e}") from e