QuentinL52 commited on
Commit
2e32ddd
·
verified ·
1 Parent(s): 2c35e00

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +178 -146
main.py CHANGED
@@ -1,48 +1,73 @@
1
  import tempfile
2
  import requests
3
- from fastapi import FastAPI, UploadFile, File, HTTPException, Body
 
 
4
  from fastapi.concurrency import run_in_threadpool
 
5
  from pydantic import BaseModel, Field
6
  from typing import List, Dict, Any, Optional
7
- from datetime import datetime
8
- import uvicorn
9
- import os
10
- import logging
11
 
 
12
  logging.basicConfig(level=logging.INFO)
13
  logger = logging.getLogger(__name__)
14
 
15
- from src.cv_parsing_agents import CvParserAgent
16
- from src.interview_simulator.entretient_version_prod import InterviewProcessor
17
- from src.scoring_engine import ContextualScoringEngine
18
- from src.rag_handler import RAGHandler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
 
20
  app = FastAPI(
21
- title="API d'IA pour la RH",
22
- description="Une API pour le parsing de CV et la simulation d'entretiens avec analyse asynchrone.",
23
- version="1.3.0"
 
 
24
  )
25
 
26
- # Configuration de l'API Celery externe
27
- CELERY_API_URL = os.getenv("CELERY_API_URL", "https://celery-7as1.onrender.com")
 
 
 
 
 
 
28
 
29
- # Initialisation des services au démarrage
30
- try:
31
- logger.info("Initialisation du RAG Handler...")
32
- rag_handler = RAGHandler()
33
- if rag_handler.vector_store:
34
- logger.info(f"Vector store chargé avec {rag_handler.vector_store.index.ntotal} vecteurs.")
35
- else:
36
- logger.warning("Le RAG Handler n'a pas pu être initialisé (pas de documents ?). Le feedback contextuel sera désactivé.")
37
- except Exception as e:
38
- logger.error(f"Erreur critique lors de l'initialisation du RAG Handler: {e}", exc_info=True)
39
- rag_handler = None
40
 
 
41
  class InterviewRequest(BaseModel):
42
- user_id: str = Field(..., example="google_user_12345")
43
  job_offer_id: str = Field(..., example="job_offer_abcde")
44
- cv_document: Dict[str, Any] = Field(..., example={"candidat": {"nom": "John Doe", "compétences": {"hard_skills": ["Python", "FastAPI"]}}})
45
- job_offer: Dict[str, Any] = Field(..., example={"poste": "Développeur Python", "description": "Recherche développeur expérimenté..."})
46
  messages: List[Dict[str, Any]]
47
  conversation_history: List[Dict[str, Any]]
48
 
@@ -58,181 +83,188 @@ class TaskResponse(BaseModel):
58
  message: Optional[str] = None
59
 
60
  class HealthCheck(BaseModel):
61
- status: str = Field(default="ok", example="ok")
62
  celery_api_status: Optional[str] = None
 
 
63
 
64
- @app.get("/", tags=["Status"], summary="Vérification de l'état de l'API")
65
- async def read_root() -> HealthCheck:
66
- """Vérifie que l'API est en cours d'exécution et teste la connexion à l'API Celery."""
 
 
 
67
  celery_status = "unknown"
68
  try:
69
  response = requests.get(f"{CELERY_API_URL}/", timeout=5)
70
- if response.status_code == 200:
71
- celery_status = "connected"
72
- else:
73
- celery_status = "error"
74
- except Exception as e:
75
- logger.warning(f"Impossible de se connecter à l'API Celery: {e}")
76
  celery_status = "disconnected"
77
 
78
- return HealthCheck(status="ok", celery_api_status=celery_status)
 
 
 
 
 
 
 
 
 
 
79
 
80
- # --- Endpoint du parser de CV ---
81
- @app.post("/parse-cv/", tags=["CV Parsing"], summary="Analyser un CV au format PDF avec scoring contextuel")
82
- async def parse_cv_endpoint(file: UploadFile = File(...)):
 
 
 
 
 
 
 
 
 
 
 
83
  if file.content_type != "application/pdf":
84
- raise HTTPException(status_code=400, detail="Le fichier doit être au format PDF.")
85
- tmp_path = None
 
86
  try:
 
87
  contents = await file.read()
88
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
89
  tmp.write(contents)
90
- tmp.flush()
91
  tmp_path = tmp.name
92
 
93
- logger.info(f"Début du parsing du CV temporaire : {tmp_path}")
94
  cv_agent = CvParserAgent(pdf_path=tmp_path)
95
  parsed_data = await run_in_threadpool(cv_agent.process)
96
- if not parsed_data:
97
- raise HTTPException(status_code=500, detail="Échec du parsing du CV.")
98
- logger.info("Parsing du CV réussi. Lancement du scoring contextuel.")
99
- scoring_engine = ContextualScoringEngine(parsed_data)
100
- scored_skills_data = await run_in_threadpool(scoring_engine.calculate_scores)
101
- if parsed_data.get("candidat"):
102
- parsed_data["candidat"].update(scored_skills_data)
103
- else:
104
- parsed_data.update(scored_skills_data)
105
-
106
- logger.info("Scoring terminé. Retour de la réponse complète.")
 
 
 
107
  return parsed_data
108
-
109
  except Exception as e:
110
- logger.error(f"Erreur lors du parsing ou du scoring du CV : {e}", exc_info=True)
111
- raise HTTPException(status_code=500, detail=f"Erreur interne du serveur : {e}")
 
 
 
112
  finally:
113
  if tmp_path and os.path.exists(tmp_path):
114
  try:
115
  os.remove(tmp_path)
116
- logger.info(f"Fichier temporaire supprimé : {tmp_path}")
117
- except Exception as cleanup_error:
118
- logger.warning(f"Erreur lors de la suppression du fichier temporaire : {cleanup_error}")
119
 
120
- # --- Endpoint de simulation d'entretien ---
121
- @app.post("/simulate-interview/", tags=["Simulation d'Entretien"], summary="Gérer une conversation d'entretien")
122
- async def simulate_interview_endpoint(request: InterviewRequest):
 
 
 
 
 
 
 
123
  try:
124
  processor = InterviewProcessor(
125
  cv_document=request.cv_document,
126
  job_offer=request.job_offer,
127
  conversation_history=request.conversation_history
128
  )
129
- ai_response_object = await run_in_threadpool(processor.run, messages=request.messages)
130
 
131
- # On retourne juste la réponse de l'assistant pour le chat
132
- return {"response": ai_response_object["messages"][-1].content}
133
-
134
  except Exception as e:
135
- logger.error(f"Erreur interne dans /simulate-interview/: {e}", exc_info=True)
136
- raise HTTPException(status_code=500, detail=f"Erreur interne du serveur : {e}")
137
 
138
- # --- Endpoints pour l'analyse asynchrone via API Celery externe ---
139
- @app.post("/trigger-analysis/", tags=["Analyse Asynchrone"], response_model=TaskResponse, status_code=202)
140
  async def trigger_analysis(request: AnalysisRequest):
141
- """
142
- Déclenche l'analyse de l'entretien en tâche de fond via l'API Celery externe.
143
- Retourne immédiatement un ID de tâche.
144
- """
145
  try:
146
- logger.info(f"Déclenchement d'analyse via API Celery pour candidat: {request.candidate_id}")
147
-
148
- # Appel à l'API Celery externe
149
- celery_response = requests.post(
150
  f"{CELERY_API_URL}/trigger-analysis",
151
- json={
152
- "conversation_history": request.conversation_history,
153
- "job_description_text": request.job_description_text,
154
- "candidate_id": request.candidate_id
155
- },
156
  headers={"Content-Type": "application/json"},
157
  timeout=30
158
  )
159
 
160
- if celery_response.status_code == 202:
161
- celery_data = celery_response.json()
162
  return TaskResponse(
163
- task_id=celery_data["task_id"],
164
- status=celery_data["status"],
165
- result=celery_data.get("result"),
166
- message="Analyse démarrée avec succès"
167
  )
168
  else:
169
- logger.error(f"Erreur API Celery: {celery_response.status_code} - {celery_response.text}")
170
- raise HTTPException(
171
- status_code=503,
172
- detail=f"Service d'analyse indisponible: {celery_response.status_code}"
173
- )
174
 
175
- except requests.exceptions.RequestException as e:
176
- logger.error(f"Erreur de connexion à l'API Celery: {e}")
177
- raise HTTPException(
178
- status_code=503,
179
- detail="Service d'analyse temporairement indisponible"
180
- )
181
  except Exception as e:
182
- logger.error(f"Erreur inattendue lors du déclenchement de l'analyse: {e}")
183
  raise HTTPException(status_code=500, detail=str(e))
184
 
185
- @app.get("/analysis-status/{task_id}", tags=["Analyse Asynchrone"], response_model=TaskResponse)
186
  async def get_analysis_status(task_id: str):
187
- """
188
- Vérifie le statut de la tâche d'analyse via l'API Celery externe.
189
- Si terminée, retourne le résultat.
190
- """
191
  try:
192
- logger.info(f"Vérification du statut pour la tâche: {task_id}")
193
 
194
- # Appel à l'API Celery externe
195
- celery_response = requests.get(
196
- f"{CELERY_API_URL}/task-status/{task_id}",
197
- timeout=10
198
- )
199
-
200
- if celery_response.status_code == 200:
201
- celery_data = celery_response.json()
202
  return TaskResponse(
203
  task_id=task_id,
204
- status=celery_data["status"],
205
- result=celery_data.get("result"),
206
- message=celery_data.get("progress", "Statut récupéré")
207
  )
208
  else:
209
- logger.error(f"Erreur API Celery: {celery_response.status_code}")
210
- raise HTTPException(
211
- status_code=503,
212
- detail="Service d'analyse indisponible"
213
- )
214
 
215
- except requests.exceptions.RequestException as e:
216
- logger.error(f"Erreur de connexion à l'API Celery: {e}")
217
- raise HTTPException(
218
- status_code=503,
219
- detail="Service d'analyse temporairement indisponible"
220
- )
221
  except Exception as e:
222
- logger.error(f"Erreur lors de la vérification du statut: {e}")
223
  raise HTTPException(status_code=500, detail=str(e))
224
 
225
- @app.get("/celery-stats", tags=["Debug"], summary="Statistiques de l'API Celery")
226
- async def get_celery_stats():
227
- """Récupère les statistiques de l'API Celery externe."""
228
- try:
229
- response = requests.get(f"{CELERY_API_URL}/worker-stats", timeout=10)
230
- if response.status_code == 200:
231
- return response.json()
232
- else:
233
- return {"error": f"API Celery inaccessible: {response.status_code}"}
234
- except Exception as e:
235
- return {"error": f"Impossible de récupérer les stats: {e}"}
 
 
 
 
 
 
 
 
 
236
 
237
  if __name__ == "__main__":
238
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
1
  import tempfile
2
  import requests
3
+ import os
4
+ import logging
5
+ from fastapi import FastAPI, UploadFile, File, HTTPException
6
  from fastapi.concurrency import run_in_threadpool
7
+ from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel, Field
9
  from typing import List, Dict, Any, Optional
 
 
 
 
10
 
11
+ # Configuration du logging
12
  logging.basicConfig(level=logging.INFO)
13
  logger = logging.getLogger(__name__)
14
 
15
+ # Imports avec gestion d'erreurs robuste
16
+ try:
17
+ from src.cv_parsing_agents import CvParserAgent, create_fallback_cv_data
18
+ CV_PARSING_AVAILABLE = True
19
+ logger.info("✅ CV Parsing disponible")
20
+ except Exception as e:
21
+ logger.error(f"❌ CV Parsing indisponible: {e}")
22
+ CV_PARSING_AVAILABLE = False
23
+ CvParserAgent = None
24
+ create_fallback_cv_data = None
25
+
26
+ try:
27
+ from src.interview_simulator.entretient_version_prod import InterviewProcessor
28
+ INTERVIEW_AVAILABLE = True
29
+ logger.info("✅ Interview Simulator disponible")
30
+ except Exception as e:
31
+ logger.error(f"❌ Interview Simulator indisponible: {e}")
32
+ INTERVIEW_AVAILABLE = False
33
+ InterviewProcessor = None
34
+
35
+ try:
36
+ from src.scoring_engine import ContextualScoringEngine
37
+ SCORING_AVAILABLE = True
38
+ logger.info("✅ Scoring Engine disponible")
39
+ except Exception as e:
40
+ logger.error(f"❌ Scoring Engine indisponible: {e}")
41
+ SCORING_AVAILABLE = False
42
+ ContextualScoringEngine = None
43
 
44
+ # Application FastAPI
45
  app = FastAPI(
46
+ title="AIrh Interview Assistant",
47
+ description="API pour l'analyse de CV et la simulation d'entretiens d'embauche",
48
+ version="1.3.0",
49
+ docs_url="/docs",
50
+ redoc_url="/redoc"
51
  )
52
 
53
+ # Configuration CORS pour HF Spaces
54
+ app.add_middleware(
55
+ CORSMiddleware,
56
+ allow_origins=["*"],
57
+ allow_credentials=True,
58
+ allow_methods=["*"],
59
+ allow_headers=["*"],
60
+ )
61
 
62
+ # Configuration API Celery
63
+ CELERY_API_URL = os.getenv("CELERY_API_URL", "https://celery-7as1.onrender.com")
 
 
 
 
 
 
 
 
 
64
 
65
+ # Modèles Pydantic
66
  class InterviewRequest(BaseModel):
67
+ user_id: str = Field(..., example="user_12345")
68
  job_offer_id: str = Field(..., example="job_offer_abcde")
69
+ cv_document: Dict[str, Any]
70
+ job_offer: Dict[str, Any]
71
  messages: List[Dict[str, Any]]
72
  conversation_history: List[Dict[str, Any]]
73
 
 
83
  message: Optional[str] = None
84
 
85
  class HealthCheck(BaseModel):
86
+ status: str = "ok"
87
  celery_api_status: Optional[str] = None
88
+ services: Dict[str, bool] = Field(default_factory=dict)
89
+ message: str = "API AIrh fonctionnelle"
90
 
91
+ # Endpoints
92
+ @app.get("/", response_model=HealthCheck, tags=["Status"])
93
+ async def health_check():
94
+ """Health check de l'API avec test de connectivité Celery."""
95
+
96
+ # Test connexion Celery
97
  celery_status = "unknown"
98
  try:
99
  response = requests.get(f"{CELERY_API_URL}/", timeout=5)
100
+ celery_status = "connected" if response.status_code == 200 else "error"
101
+ except Exception:
 
 
 
 
102
  celery_status = "disconnected"
103
 
104
+ services = {
105
+ "cv_parsing": CV_PARSING_AVAILABLE,
106
+ "interview_simulation": INTERVIEW_AVAILABLE,
107
+ "scoring_engine": SCORING_AVAILABLE,
108
+ "celery_api": celery_status == "connected"
109
+ }
110
+
111
+ return HealthCheck(
112
+ celery_api_status=celery_status,
113
+ services=services
114
+ )
115
 
116
+ @app.post("/parse-cv/", tags=["CV Parsing"])
117
+ async def parse_cv(file: UploadFile = File(...)):
118
+ """Analyse un CV PDF et extrait les informations structurées."""
119
+
120
+ if not CV_PARSING_AVAILABLE:
121
+ # Fallback si le parsing n'est pas disponible
122
+ return create_fallback_cv_data() if create_fallback_cv_data else {
123
+ "error": "Service de parsing de CV temporairement indisponible",
124
+ "candidat": {
125
+ "informations_personnelles": {"nom": "Test User"},
126
+ "compétences": {"hard_skills": [], "soft_skills": []}
127
+ }
128
+ }
129
+
130
  if file.content_type != "application/pdf":
131
+ raise HTTPException(status_code=400, detail="Fichier PDF requis")
132
+
133
+ tmp_path = None
134
  try:
135
+ # Sauvegarder le fichier temporairement
136
  contents = await file.read()
137
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
138
  tmp.write(contents)
 
139
  tmp_path = tmp.name
140
 
141
+ # Traiter le CV
142
  cv_agent = CvParserAgent(pdf_path=tmp_path)
143
  parsed_data = await run_in_threadpool(cv_agent.process)
144
+
145
+ if not parsed_data and create_fallback_cv_data:
146
+ parsed_data = create_fallback_cv_data(tmp_path)
147
+
148
+ # Scoring si disponible
149
+ if SCORING_AVAILABLE and ContextualScoringEngine and parsed_data:
150
+ try:
151
+ scoring_engine = ContextualScoringEngine(parsed_data)
152
+ scored_data = await run_in_threadpool(scoring_engine.calculate_scores)
153
+ if parsed_data.get("candidat"):
154
+ parsed_data["candidat"].update(scored_data)
155
+ except Exception as e:
156
+ logger.warning(f"Scoring échoué: {e}")
157
+
158
  return parsed_data
159
+
160
  except Exception as e:
161
+ logger.error(f"Erreur parsing CV: {e}")
162
+ if create_fallback_cv_data:
163
+ return create_fallback_cv_data(tmp_path)
164
+ raise HTTPException(status_code=500, detail=str(e))
165
+
166
  finally:
167
  if tmp_path and os.path.exists(tmp_path):
168
  try:
169
  os.remove(tmp_path)
170
+ except Exception:
171
+ pass
 
172
 
173
+ @app.post("/simulate-interview/", tags=["Interview"])
174
+ async def simulate_interview(request: InterviewRequest):
175
+ """Gère une conversation d'entretien d'embauche."""
176
+
177
+ if not INTERVIEW_AVAILABLE:
178
+ raise HTTPException(
179
+ status_code=503,
180
+ detail="Service de simulation d'entretien indisponible"
181
+ )
182
+
183
  try:
184
  processor = InterviewProcessor(
185
  cv_document=request.cv_document,
186
  job_offer=request.job_offer,
187
  conversation_history=request.conversation_history
188
  )
 
189
 
190
+ result = await run_in_threadpool(processor.run, messages=request.messages)
191
+ return {"response": result["messages"][-1].content}
192
+
193
  except Exception as e:
194
+ logger.error(f"Erreur simulation entretien: {e}")
195
+ raise HTTPException(status_code=500, detail=str(e))
196
 
197
+ @app.post("/trigger-analysis/", response_model=TaskResponse, status_code=202, tags=["Analysis"])
 
198
  async def trigger_analysis(request: AnalysisRequest):
199
+ """Déclenche une analyse asynchrone via l'API Celery."""
200
+
 
 
201
  try:
202
+ response = requests.post(
 
 
 
203
  f"{CELERY_API_URL}/trigger-analysis",
204
+ json=request.dict(),
 
 
 
 
205
  headers={"Content-Type": "application/json"},
206
  timeout=30
207
  )
208
 
209
+ if response.status_code == 202:
210
+ data = response.json()
211
  return TaskResponse(
212
+ task_id=data["task_id"],
213
+ status=data["status"],
214
+ message="Analyse démarrée"
 
215
  )
216
  else:
217
+ raise HTTPException(status_code=503, detail="Service d'analyse indisponible")
 
 
 
 
218
 
219
+ except requests.RequestException:
220
+ raise HTTPException(status_code=503, detail="API Celery inaccessible")
 
 
 
 
221
  except Exception as e:
 
222
  raise HTTPException(status_code=500, detail=str(e))
223
 
224
+ @app.get("/analysis-status/{task_id}", response_model=TaskResponse, tags=["Analysis"])
225
  async def get_analysis_status(task_id: str):
226
+ """Récupère le statut d'une analyse."""
227
+
 
 
228
  try:
229
+ response = requests.get(f"{CELERY_API_URL}/task-status/{task_id}", timeout=10)
230
 
231
+ if response.status_code == 200:
232
+ data = response.json()
 
 
 
 
 
 
233
  return TaskResponse(
234
  task_id=task_id,
235
+ status=data["status"],
236
+ result=data.get("result"),
237
+ message=data.get("progress", "Statut récupéré")
238
  )
239
  else:
240
+ raise HTTPException(status_code=503, detail="Service d'analyse indisponible")
 
 
 
 
241
 
242
+ except requests.RequestException:
243
+ raise HTTPException(status_code=503, detail="API Celery inaccessible")
 
 
 
 
244
  except Exception as e:
 
245
  raise HTTPException(status_code=500, detail=str(e))
246
 
247
+ # Endpoint de debug pour HF Spaces
248
+ @app.get("/debug", tags=["Debug"])
249
+ async def debug_info():
250
+ """Informations de debug pour le déploiement."""
251
+ return {
252
+ "environment": {
253
+ "HF_HOME": os.getenv("HF_HOME"),
254
+ "CELERY_API_URL": CELERY_API_URL,
255
+ "PYTHONPATH": os.getenv("PYTHONPATH")
256
+ },
257
+ "services": {
258
+ "cv_parsing": CV_PARSING_AVAILABLE,
259
+ "interview_simulation": INTERVIEW_AVAILABLE,
260
+ "scoring_engine": SCORING_AVAILABLE
261
+ },
262
+ "cache_dirs": {
263
+ "/tmp/cache": os.path.exists("/tmp/cache"),
264
+ "/app/cache": os.path.exists("/app/cache")
265
+ }
266
+ }
267
 
268
  if __name__ == "__main__":
269
+ import uvicorn
270
+ uvicorn.run(app, host="0.0.0.0", port=7860)