josibra commited on
Commit
a2ff2c3
·
1 Parent(s): dc57d5e

Déploiement auto depuis GitHub Actions avec LFS

Browse files
Files changed (3) hide show
  1. Dockerfile +5 -2
  2. app/main.py +92 -3
  3. app/schemas.py +2 -3
Dockerfile CHANGED
@@ -27,10 +27,13 @@ COPY --chown=user models/ ./models/
27
  # 7. Variables de l'API avec le nouveau chemin
28
  ENV GLOBAL_THRESHOLD=0.45
29
  ENV MODEL_PATH=$HOME/app/models/pmvl_catboost_final.cbm
30
- ENV MODEL_FEATURES_PATH=$HOME/app/models/pmvl_feature_columns.txt
31
 
32
  # 8. Exposer le port 7860 (Port par défaut de Hugging Face Spaces)
33
  EXPOSE 7860
34
 
35
- # 9. Démarrage de l'API sur le port 7860
 
 
 
36
  CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
27
  # 7. Variables de l'API avec le nouveau chemin
28
  ENV GLOBAL_THRESHOLD=0.45
29
  ENV MODEL_PATH=$HOME/app/models/pmvl_catboost_final.cbm
30
+ ENV FEATURES_PATH=$HOME/app/models/pmvl_feature_columns.txt
31
 
32
  # 8. Exposer le port 7860 (Port par défaut de Hugging Face Spaces)
33
  EXPOSE 7860
34
 
35
+ # 9. Création du dossier de logs avec les permissions nécessaires
36
+ RUN mkdir -p $HOME/app/logs && chmod -R 777 $HOME/app/logs
37
+
38
+ # 10. Démarrage de l'API sur le port 7860
39
  CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
app/main.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import RedirectResponse
4
  import os
@@ -9,6 +9,11 @@ import gradio as gr
9
  from .schemas import PMVLFeatures, PredictionResponse
10
  from .model_loader import get_model, get_feature_columns
11
 
 
 
 
 
 
12
  GLOBAL_THRESHOLD_ENV = "GLOBAL_THRESHOLD"
13
  DEFAULT_THRESHOLD = 0.45
14
 
@@ -26,6 +31,71 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  GROUP_KEYS = [
30
  "PMVL[ENTITE]",
31
  "PMVL[Selected Fund code]",
@@ -84,7 +154,7 @@ def run_model_prediction(features: PMVLFeatures) -> PredictionResponse:
84
  feature_columns = get_feature_columns()
85
 
86
  # 1) Récupérer les données brutes
87
- raw_dict = features.model_dump(by_alias=True)
88
  # Le modèle n'utilise pas directement la date
89
  raw_dict.pop("PMVL[Holding date]", None)
90
 
@@ -130,7 +200,26 @@ def health_check():
130
  @app.post("/predict", response_model=PredictionResponse, tags=["prédiction"])
131
  def predict_pmvl(features: PMVLFeatures):
132
  try:
133
- return run_model_prediction(features)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  except Exception as e:
135
  raise HTTPException(
136
  status_code=500,
 
1
+ from fastapi import FastAPI, HTTPException, Request
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import RedirectResponse
4
  import os
 
9
  from .schemas import PMVLFeatures, PredictionResponse
10
  from .model_loader import get_model, get_feature_columns
11
 
12
+ import json
13
+ import time
14
+ from uuid import uuid4
15
+ from pathlib import Path
16
+
17
  GLOBAL_THRESHOLD_ENV = "GLOBAL_THRESHOLD"
18
  DEFAULT_THRESHOLD = 0.45
19
 
 
31
  allow_headers=["*"],
32
  )
33
 
34
+ # On crée le dossier logs s'il n'existe pas
35
+ LOG_DIR = Path("logs")
36
+ LOG_DIR.mkdir(exist_ok=True)
37
+ PRODUCTION_LOGS_FILE = LOG_DIR / "production_logs.jsonl"
38
+
39
+ @app.middleware("http")
40
+ async def production_logging_middleware(request: Request, call_next):
41
+ """
42
+ Middleware pour logger chaque requête à l'API en production.
43
+ Idéal pour surveiller la latence, le taux d'erreur et capturer
44
+ les données pour l'analyse de Data Drift ultérieure.
45
+ """
46
+ start_time = time.time()
47
+ request_id = str(uuid4())
48
+
49
+ # On lit le body de la requête (nécessaire pour capturer les inputs)
50
+ body_bytes = b""
51
+ if request.url.path == "/predict" and request.method == "POST":
52
+ body_bytes = await request.body()
53
+
54
+ # Fonction pour reconstruire le body afin que les routes suivantes puissent le lire
55
+ async def receive():
56
+ return {"type": "http.request", "body": body_bytes}
57
+ request._receive = receive
58
+
59
+ # Exécution de la requête
60
+ response = None
61
+ error_msg = None
62
+ try:
63
+ response = await call_next(request)
64
+ status_code = response.status_code
65
+ except Exception as e:
66
+ status_code = 500
67
+ error_msg = str(e)
68
+ raise e
69
+ finally:
70
+ latency_ms = (time.time() - start_time) * 1000
71
+
72
+ # On ne loggue en détail que les appels à l'endpoint de prédiction
73
+ if request.url.path == "/predict":
74
+ # Extraction des inputs
75
+ input_data = None
76
+ if body_bytes:
77
+ try:
78
+ input_data = json.loads(body_bytes.decode("utf-8"))
79
+ except json.JSONDecodeError:
80
+ input_data = "Unparseable JSON"
81
+
82
+ # Préparation du log
83
+ log_entry = {
84
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
85
+ "request_id": request_id,
86
+ "endpoint": request.url.path,
87
+ "latency_ms": round(latency_ms, 2),
88
+ "status_code": status_code,
89
+ "input_features": input_data,
90
+ "error": error_msg
91
+ }
92
+
93
+ # Écriture dans le fichier JSONL
94
+ with open(PRODUCTION_LOGS_FILE, "a", encoding="utf-8") as f:
95
+ f.write(json.dumps(log_entry) + "\n")
96
+
97
+ return response
98
+
99
  GROUP_KEYS = [
100
  "PMVL[ENTITE]",
101
  "PMVL[Selected Fund code]",
 
154
  feature_columns = get_feature_columns()
155
 
156
  # 1) Récupérer les données brutes
157
+ raw_dict = features.model_dump(by_alias=True, mode="json")
158
  # Le modèle n'utilise pas directement la date
159
  raw_dict.pop("PMVL[Holding date]", None)
160
 
 
200
  @app.post("/predict", response_model=PredictionResponse, tags=["prédiction"])
201
  def predict_pmvl(features: PMVLFeatures):
202
  try:
203
+ # Exécution de la prédiction existante
204
+ result = run_model_prediction(features)
205
+
206
+ # --- NOUVEAU CODE POUR LE LOGGING DES OUTPUTS ---
207
+ # On ajoute une entrée spécifique pour les outputs dans un fichier séparé
208
+ # ou on l'ajoute au fichier principal. Ici, on crée un log d'inférence complet.
209
+ inference_log = {
210
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
211
+ "input_features": features.model_dump(mode="json"),
212
+ "output": {
213
+ "proba_bonne_estimation": result.proba_bonne_estimation,
214
+ "prediction": result.prediction,
215
+ "seuil_applique": result.seuil_applique,
216
+ },
217
+ }
218
+ with open(LOG_DIR / "inference_results.jsonl", "a", encoding="utf-8") as f:
219
+ f.write(json.dumps(inference_log) + "\n")
220
+ # ------------------------------------------------
221
+
222
+ return result
223
  except Exception as e:
224
  raise HTTPException(
225
  status_code=500,
app/schemas.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel, Field, validator
2
  from typing import Optional
3
  from datetime import date
4
 
@@ -123,8 +123,7 @@ class PMVLFeatures(BaseModel):
123
  description="Nom du portefeuille"
124
  )
125
 
126
- class Config:
127
- populate_by_name = True # Nouvelle syntaxe Pydantic V2
128
 
129
 
130
  class PredictionResponse(BaseModel):
 
1
+ from pydantic import BaseModel, Field, ConfigDict
2
  from typing import Optional
3
  from datetime import date
4
 
 
123
  description="Nom du portefeuille"
124
  )
125
 
126
+ model_config = ConfigDict(populate_by_name=True)
 
127
 
128
 
129
  class PredictionResponse(BaseModel):