QuentinL52 commited on
Commit
c42e6f5
·
verified ·
1 Parent(s): 1cb3d71

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +193 -91
main.py CHANGED
@@ -1,85 +1,125 @@
1
- import logging
2
- import os
3
- import requests # Ajout de la librairie pour les requêtes HTTP
4
- from fastapi import FastAPI, HTTPException, Body, UploadFile, File
5
- from pydantic import BaseModel
6
- from typing import List, Dict, Any
7
- from dotenv import load_dotenv
8
  from fastapi.concurrency import run_in_threadpool
9
-
10
- # --- Import de VOS modules de travail ---
11
- from src.cv_parsing_agents import CVParser
12
- from src.interview_simulator.entretient_version_prod import InterviewProcessor
13
- from src.config import Config
14
-
15
- # --- Celery n'est plus importé ici ---
16
-
17
- load_dotenv()
18
 
19
  logging.basicConfig(level=logging.INFO)
20
  logger = logging.getLogger(__name__)
21
 
22
- # --- Récupération de l'URL du nouveau service ---
23
- # Vous devrez ajouter cette variable à votre environnement sur la plateforme où vous déploierez cette API.
24
- CELERY_SERVICE_URL = os.environ.get("CELERY_SERVICE_URL")
25
- if not CELERY_SERVICE_URL:
26
- logger.warning("La variable d'environnement CELERY_SERVICE_URL n'est pas définie. Les analyses asynchrones échoueront.")
27
 
28
  app = FastAPI(
29
- title="AIrh Main API",
30
- description="API principale gérant le parsing de CV et la simulation interactive.",
31
- version="2.1.0"
32
  )
33
 
34
- # --- Modèles de données Pydantic ---
35
- class ParsedCVResponse(BaseModel):
36
- candidat: Dict[str, Any]
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  class InterviewRequest(BaseModel):
39
- cv_document: Dict[str, Any]
40
- job_offer: Dict[str, Any]
 
 
41
  messages: List[Dict[str, Any]]
42
  conversation_history: List[Dict[str, Any]]
43
 
44
- class InterviewResponse(BaseModel):
45
- response: str
46
-
47
  class AnalysisRequest(BaseModel):
48
  conversation_history: List[Dict[str, Any]]
49
  job_description_text: str
50
-
51
- class TaskStatusResponse(BaseModel):
 
52
  task_id: str
53
  status: str
54
  result: Any = None
 
55
 
56
- # --- Endpoints de l'API ---
57
-
58
- @app.get("/", summary="Health Check")
59
- async def read_root():
60
- return {"message": "AIrh Main API est opérationnelle."}
61
 
62
- # --- SECTION ORIGINALE (INCHANGÉE) ---
63
-
64
- @app.post("/parse-cv/", response_model=ParsedCVResponse, tags=["1. Parsing de CV"])
65
- async def parse_cv(file: UploadFile = File(...)):
66
- logger.info(f"Réception du fichier CV: {file.filename}")
67
- cv_content = await file.read()
68
- if not cv_content:
69
- raise HTTPException(status_code=400, detail="Le fichier CV est vide.")
70
  try:
71
- parser = CVParser()
72
- parsed_data = await run_in_threadpool(parser.parse, cv_content)
73
- if not parsed_data or "candidat" not in parsed_data:
74
- raise HTTPException(status_code=422, detail="Impossible d'extraire les données structurées du CV.")
75
- return parsed_data
76
  except Exception as e:
77
- logger.error(f"Erreur critique lors du parsing du CV: {e}", exc_info=True)
78
- raise HTTPException(status_code=500, detail=f"Erreur interne du serveur lors du parsing: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- @app.post("/simulate-interview/", response_model=InterviewResponse, tags=["2. Simulation d'Entretien"])
81
- async def simulate_interview(request: InterviewRequest):
82
- logger.info("Réception d'une requête pour la simulation d'entretien.")
 
 
 
 
 
 
 
 
 
 
 
83
  try:
84
  processor = InterviewProcessor(
85
  cv_document=request.cv_document,
@@ -87,50 +127,112 @@ async def simulate_interview(request: InterviewRequest):
87
  conversation_history=request.conversation_history
88
  )
89
  ai_response_object = await run_in_threadpool(processor.run, messages=request.messages)
90
- last_message = ai_response_object["messages"][-1].content
91
- return {"response": last_message}
92
- except Exception as e:
93
- logger.error(f"Erreur lors de la simulation d'entretien: {e}", exc_info=True)
94
- raise HTTPException(status_code=500, detail=f"Erreur interne du serveur lors de la simulation: {str(e)}")
95
 
96
- # --- SECTION MODIFIÉE POUR APPELER LE SERVICE EXTERNE ---
 
 
97
 
98
- @app.post("/trigger-analysis/", response_model=TaskStatusResponse, status_code=202, tags=["3. Analyse Asynchrone (Externe)"])
 
99
  async def trigger_analysis(request: AnalysisRequest):
100
  """
101
- Déclenche l'analyse en appelant le service Celery externe.
 
102
  """
103
- logger.info("Redirection de la demande d'analyse vers le service Celery externe.")
104
- if not CELERY_SERVICE_URL:
105
- raise HTTPException(status_code=503, detail="Le service d'analyse est actuellement indisponible.")
106
-
107
  try:
108
- # On fait une requête POST à notre service Celery sur Render
109
- api_url = f"{CELERY_SERVICE_URL}/trigger-analysis"
110
- logger.info(f"Appel de l'API externe : {api_url}")
111
- response = requests.post(api_url, json=request.dict())
112
- response.raise_for_status() # Lève une exception si le statut n'est pas 2xx
113
- return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  except requests.exceptions.RequestException as e:
115
- logger.error(f"Erreur de communication avec le service Celery: {e}")
116
- raise HTTPException(status_code=502, detail="Erreur de communication avec le service d'analyse.")
 
 
 
 
 
 
117
 
118
- @app.get("/analysis-status/{task_id}", response_model=TaskStatusResponse, tags=["3. Analyse Asynchrone (Externe)"])
119
  async def get_analysis_status(task_id: str):
120
  """
121
- Vérifie le statut d'une tâche en interrogeant le service Celery externe.
 
122
  """
123
- logger.info(f"Vérification du statut de la tâche externe: {task_id}")
124
- if not CELERY_SERVICE_URL:
125
- raise HTTPException(status_code=503, detail="Le service d'analyse est actuellement indisponible.")
126
-
127
  try:
128
- # On fait une requête GET à notre service Celery sur Render
129
- api_url = f"{CELERY_SERVICE_URL}/analysis-status/{task_id}"
130
- logger.info(f"Appel de l'API externe : {api_url}")
131
- response = requests.get(api_url)
132
- response.raise_for_status()
133
- return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  except requests.exceptions.RequestException as e:
135
- logger.error(f"Erreur de communication avec le service Celery: {e}")
136
- raise HTTPException(status_code=502, detail="Erreur de communication avec le service d'analyse.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
 
 
49
  class AnalysisRequest(BaseModel):
50
  conversation_history: List[Dict[str, Any]]
51
  job_description_text: str
52
+ candidate_id: Optional[str] = None
53
+
54
+ class TaskResponse(BaseModel):
55
  task_id: str
56
  status: str
57
  result: Any = None
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,
 
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)