GiusMagi commited on
Commit
0c071a5
Β·
verified Β·
1 Parent(s): 0ecb81a

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +163 -64
server.py CHANGED
@@ -2,11 +2,16 @@
2
  import numpy as np
3
  import pandas as pd
4
  import cloudpickle as cp
5
- from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import JSONResponse
8
- from pydantic import BaseModel
9
  import traceback
 
 
 
 
 
10
 
11
  app = FastAPI(
12
  title="Incassi API",
@@ -16,103 +21,197 @@ app = FastAPI(
16
  redoc_url="/redoc",
17
  )
18
 
19
- # CORS: inizia permissivo, poi restringi al dominio Lovable
20
  app.add_middleware(
21
  CORSMiddleware,
22
- allow_origins=["*"], # poi metti ["https://<tuo-dominio-lovable>"]
23
  allow_credentials=True,
24
  allow_methods=["*"],
25
  allow_headers=["*"],
26
  )
27
 
28
- # Carica l'artefatto con cloudpickle
29
- with open("incassi_model.pkl", "rb") as f:
30
- mdl = cp.load(f)
31
-
32
-
33
-
34
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
 
36
  class PredictIn(BaseModel):
37
- Debitore_cluster: str | None = None
38
- Stato_Giudizio: str | None = None
39
- Cedente: str | None = None
40
- Importo_iniziale_outstanding: float | None = None
41
- Decreto_sospeso: str | int | None = None
42
- Notifica_Decreto: str | int | None = None
43
- Opposizione_al_decreto_ingiuntivo: str | int | None = None
44
- Ricorso_al_TAR: str | int | None = None
45
- Sentenza_TAR: str | int | None = None
46
- Atto_di_Precetto: str | int | None = None
47
- Decreto_Ingiuntivo: str | int | None = None
48
- Sentenza_giudizio_opposizione: str | int | None = None
49
- giorni_da_iscrizione: int | None = None
50
- giorni_da_cessione: int | None = None
51
- Zona: str | None = None
 
52
 
53
  class CurveIn(PredictIn):
54
- iscr_start: int = 0
55
- iscr_end: int = 1500
56
- step: int = 50
57
 
58
- # --------- Mapper: API -> nomi "raw" del training ---------
59
- def _to_df(d: dict) -> pd.DataFrame:
60
- return pd.DataFrame([{
61
- "Debitore_cluster": d.get("Debitore_cluster"),
62
- "Stato_Giudizio": d.get("Stato_Giudizio"),
63
- "Cedente": d.get("Cedente"),
64
- "Importo iniziale outstanding": d.get("Importo_iniziale_outstanding"),
65
- "Decreto sospeso": d.get("Decreto_sospeso"),
66
- "Notifica Decreto": d.get("Notifica_Decreto"),
67
- "Opposizione al decreto ingiuntivo": d.get("Opposizione_al_decreto_ingiuntivo"),
68
- "Ricorso al TAR": d.get("Ricorso_al_TAR"),
69
- "Sentenza TAR": d.get("Sentenza_TAR"),
70
- "Atto di Precetto": d.get("Atto_di_Precetto"),
71
- "Decreto Ingiuntivo": d.get("Decreto_Ingiuntivo"),
72
- "Sentenza giudizio opposizione": d.get("Sentenza_giudizio_opposizione"),
73
- "giorni_da_iscrizione": d.get("giorni_da_iscrizione"),
74
- "giorni_da_cessione": d.get("giorni_da_cessione"),
75
- "Zona": d.get("Zona"),
76
- }])
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # --------- Endpoints ---------
79
  @app.get("/")
80
  def root():
81
- return {"ok": True, "service": "incassi-api"}
 
 
 
 
82
 
83
  @app.get("/version")
84
  def version():
85
  return {
86
  "model_version": getattr(mdl, "model_version", "unknown"),
87
  "p100_threshold": float(getattr(mdl, "p100_thr", 0.5)),
 
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
90
  @app.post("/predict")
91
  def predict(inp: PredictIn):
 
92
  try:
93
- df = _to_df(inp.dict())
 
 
 
 
 
 
 
 
94
  p100, prob_ord, yhat, final_class, _ = mdl.predict(df)
95
- return {
 
96
  "p100": float(p100),
97
  "prob_ord": np.asarray(prob_ord, dtype=float).tolist(),
98
  "yhat": float(yhat),
99
  "final_class": str(final_class),
100
  }
 
 
 
 
 
 
101
  except Exception as e:
102
- print("PREDICT ERROR:\n", traceback.format_exc())
103
- return JSONResponse(status_code=500, content={"error": str(e)})
 
104
 
105
  @app.post("/curve_iscrizione")
106
  def curve_iscrizione(inp: CurveIn):
 
107
  try:
108
- base = inp.dict()
109
- out = mdl.curve_iscrizione(base, inp.iscr_start, inp.iscr_end, inp.step)
110
- # JSON-safe
111
- out["x"] = [int(v) for v in out.get("x", [])]
112
- out["p100"] = [float(v) for v in out.get("p100", [])]
113
- for k, arr in list(out.get("classes", {}).items()):
114
- out["classes"][k] = [float(v) for v in arr]
115
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  except Exception as e:
117
- print("CURVE ERROR:\n", traceback.format_exc())
118
- return JSONResponse(status_code=500, content={"error": str(e)})
 
 
 
 
 
 
 
 
 
2
  import numpy as np
3
  import pandas as pd
4
  import cloudpickle as cp
5
+ from fastapi import FastAPI, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import JSONResponse
8
+ from pydantic import BaseModel, Field
9
  import traceback
10
+ import logging
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
 
16
  app = FastAPI(
17
  title="Incassi API",
 
21
  redoc_url="/redoc",
22
  )
23
 
24
+ # CORS
25
  app.add_middleware(
26
  CORSMiddleware,
27
+ allow_origins=["*"],
28
  allow_credentials=True,
29
  allow_methods=["*"],
30
  allow_headers=["*"],
31
  )
32
 
33
+ # Carica il modello con debug dettagliato
34
+ try:
35
+ logger.info("πŸ”§ Caricamento modello in corso...")
36
+ with open("incassi_model.pkl", "rb") as f:
37
+ mdl = cp.load(f)
38
+ logger.info(f"βœ… Modello caricato - Versione: {getattr(mdl, 'model_version', 'unknown')}")
39
+
40
+ # Test rapido del modello
41
+ test_df = pd.DataFrame([{
42
+ "Debitore_cluster": "A",
43
+ "Importo iniziale outstanding": 10000.0,
44
+ "giorni_da_iscrizione": 100,
45
+ "giorni_da_cessione": 50
46
+ }])
47
+ test_result = mdl.predict(test_df)
48
+ logger.info(f"🎯 Test modello riuscito - yhat: {test_result[2]:.2f}")
49
+
50
+ except Exception as e:
51
+ logger.error(f"❌ ERRORE CRITICO nel caricamento modello: {e}")
52
+ traceback.print_exc()
53
+ raise e
54
 
55
+ # ===== PYDANTIC MODELS CON VALIDAZIONE MIGLIORATA =====
56
  class PredictIn(BaseModel):
57
+ # Usa Field con alias per gestire nomi diversi
58
+ Debitore_cluster: str | None = Field(default=None, description="Cluster del debitore")
59
+ Stato_Giudizio: str | None = Field(default=None, description="Stato del giudizio")
60
+ Cedente: str | None = Field(default=None, description="Cedente")
61
+ Importo_iniziale_outstanding: float | None = Field(default=None, ge=0.0, description="Importo iniziale outstanding")
62
+ Decreto_sospeso: str | int | None = Field(default=None, description="Decreto sospeso")
63
+ Notifica_Decreto: str | int | None = Field(default=None, description="Notifica decreto")
64
+ Opposizione_al_decreto_ingiuntivo: str | int | None = Field(default=None, description="Opposizione al decreto ingiuntivo")
65
+ Ricorso_al_TAR: str | int | None = Field(default=None, description="Ricorso al TAR")
66
+ Sentenza_TAR: str | int | None = Field(default=None, description="Sentenza TAR")
67
+ Atto_di_Precetto: str | int | None = Field(default=None, description="Atto di precetto")
68
+ Decreto_Ingiuntivo: str | int | None = Field(default=None, description="Decreto ingiuntivo")
69
+ Sentenza_giudizio_opposizione: str | int | None = Field(default=None, description="Sentenza giudizio opposizione")
70
+ giorni_da_iscrizione: int | None = Field(default=None, ge=0, description="Giorni da iscrizione")
71
+ giorni_da_cessione: int | None = Field(default=None, ge=0, description="Giorni da cessione")
72
+ Zona: str | None = Field(default=None, description="Zona")
73
 
74
  class CurveIn(PredictIn):
75
+ iscr_start: int = Field(default=0, ge=0, description="Giorni da iscrizione - inizio")
76
+ iscr_end: int = Field(default=1500, ge=0, description="Giorni da iscrizione - fine")
77
+ step: int = Field(default=50, ge=1, description="Step per la curva")
78
 
79
+ # ===== UTILITY FUNCTIONS =====
80
+ def _to_model_format(d: dict) -> pd.DataFrame:
81
+ """Converte input API -> formato atteso dal modello"""
82
+ logger.info(f"πŸ”„ Conversione input: {d}")
83
+
84
+ try:
85
+ # Mapping esplicito API -> modello
86
+ row = {
87
+ "Debitore_cluster": d.get("Debitore_cluster"),
88
+ "Stato_Giudizio": d.get("Stato_Giudizio"),
89
+ "Cedente": d.get("Cedente"),
90
+ "Importo iniziale outstanding": d.get("Importo_iniziale_outstanding"), # ⚠️ Nota l'underscore -> spazio
91
+ "Decreto sospeso": d.get("Decreto_sospeso"),
92
+ "Notifica Decreto": d.get("Notifica_Decreto"),
93
+ "Opposizione al decreto ingiuntivo": d.get("Opposizione_al_decreto_ingiuntivo"),
94
+ "Ricorso al TAR": d.get("Ricorso_al_TAR"),
95
+ "Sentenza TAR": d.get("Sentenza_TAR"),
96
+ "Atto di Precetto": d.get("Atto_di_Precetto"),
97
+ "Decreto Ingiuntivo": d.get("Decreto_Ingiuntivo"),
98
+ "Sentenza giudizio opposizione": d.get("Sentenza_giudizio_opposizione"),
99
+ "giorni_da_iscrizione": d.get("giorni_da_iscrizione"),
100
+ "giorni_da_cessione": d.get("giorni_da_cessione"),
101
+ "Zona": d.get("Zona"),
102
+ }
103
+
104
+ df = pd.DataFrame([row])
105
+ logger.info(f"βœ… Conversione completata - Shape: {df.shape}")
106
+ return df
107
+
108
+ except Exception as e:
109
+ logger.error(f"❌ Errore nella conversione: {e}")
110
+ raise HTTPException(status_code=400, detail=f"Errore nella conversione dati: {str(e)}")
111
 
112
+ # ===== ENDPOINTS =====
113
  @app.get("/")
114
  def root():
115
+ return {
116
+ "ok": True,
117
+ "service": "incassi-api",
118
+ "model_version": getattr(mdl, "model_version", "unknown")
119
+ }
120
 
121
  @app.get("/version")
122
  def version():
123
  return {
124
  "model_version": getattr(mdl, "model_version", "unknown"),
125
  "p100_threshold": float(getattr(mdl, "p100_thr", 0.5)),
126
+ "expected_features": len(getattr(mdl, "feat_cols_full", [])),
127
  }
128
 
129
+ @app.get("/health")
130
+ def health_check():
131
+ """Health check endpoint"""
132
+ try:
133
+ # Quick model test
134
+ test_df = pd.DataFrame([{"giorni_da_iscrizione": 100}])
135
+ _ = mdl._ensure_raw_cols(test_df)
136
+ return {"status": "healthy", "model": "loaded"}
137
+ except Exception as e:
138
+ raise HTTPException(status_code=503, detail=f"Model unhealthy: {str(e)}")
139
+
140
  @app.post("/predict")
141
  def predict(inp: PredictIn):
142
+ """Predizione singola"""
143
  try:
144
+ logger.info("πŸš€ /predict chiamato")
145
+ logger.info(f"πŸ“₯ Input ricevuto: {inp.dict()}")
146
+
147
+ # Converti input -> formato modello
148
+ df = _to_model_format(inp.dict())
149
+ logger.info(f"πŸ”„ DataFrame creato: {df.shape}")
150
+
151
+ # Predizione
152
+ logger.info("πŸ”§ Esecuzione predizione...")
153
  p100, prob_ord, yhat, final_class, _ = mdl.predict(df)
154
+
155
+ result = {
156
  "p100": float(p100),
157
  "prob_ord": np.asarray(prob_ord, dtype=float).tolist(),
158
  "yhat": float(yhat),
159
  "final_class": str(final_class),
160
  }
161
+
162
+ logger.info(f"βœ… Predizione completata: yhat={result['yhat']:.2f}")
163
+ return result
164
+
165
+ except HTTPException:
166
+ raise # Re-raise HTTP exceptions
167
  except Exception as e:
168
+ logger.error(f"❌ ERRORE in /predict: {e}")
169
+ logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
170
+ raise HTTPException(status_code=500, detail=f"Errore interno: {str(e)}")
171
 
172
  @app.post("/curve_iscrizione")
173
  def curve_iscrizione(inp: CurveIn):
174
+ """Curva di crescita per giorni da iscrizione"""
175
  try:
176
+ logger.info("πŸš€ /curve_iscrizione chiamato")
177
+
178
+ # Validazione parametri
179
+ if inp.iscr_end <= inp.iscr_start:
180
+ raise HTTPException(status_code=400, detail="iscr_end deve essere > iscr_start")
181
+
182
+ if (inp.iscr_end - inp.iscr_start) / inp.step > 1000:
183
+ raise HTTPException(status_code=400, detail="Troppi punti nella curva (max 1000)")
184
+
185
+ # Converti input -> base row
186
+ base_dict = inp.dict()
187
+
188
+ # Esegui curva
189
+ logger.info(f"πŸ”§ Calcolo curva: {inp.iscr_start}-{inp.iscr_end} step {inp.step}")
190
+ out = mdl.curve_iscrizione(base_dict, inp.iscr_start, inp.iscr_end, inp.step)
191
+
192
+ # JSON-safe conversion
193
+ result = {
194
+ "x": [int(v) for v in out.get("x", [])],
195
+ "p100": [float(v) for v in out.get("p100", [])],
196
+ "classes": {}
197
+ }
198
+
199
+ for k, arr in out.get("classes", {}).items():
200
+ result["classes"][k] = [float(v) for v in arr]
201
+
202
+ logger.info(f"βœ… Curva completata: {len(result['x'])} punti")
203
+ return result
204
+
205
+ except HTTPException:
206
+ raise
207
  except Exception as e:
208
+ logger.error(f"❌ ERRORE in /curve_iscrizione: {e}")
209
+ logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
210
+ raise HTTPException(status_code=500, detail=f"Errore interno: {str(e)}")
211
+
212
+ # ===== STARTUP EVENT =====
213
+ @app.on_event("startup")
214
+ async def startup_event():
215
+ logger.info("πŸš€ API avviata con successo")
216
+ logger.info(f"πŸ“‹ Modello version: {getattr(mdl, 'model_version', 'unknown')}")
217
+ logger.info(f"🎯 Soglia p100: {getattr(mdl, 'p100_thr', 0.5):.3f}")