QuentinL52 commited on
Commit
66c37ba
·
verified ·
1 Parent(s): be66607

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +50 -63
main.py CHANGED
@@ -1,38 +1,37 @@
1
  import logging
2
- import tempfile
3
  import os
 
4
  from fastapi import FastAPI, HTTPException, Body, UploadFile, File
5
  from pydantic import BaseModel
6
  from typing import List, Dict, Any
7
- from celery.result import AsyncResult
8
  from dotenv import load_dotenv
9
  from fastapi.concurrency import run_in_threadpool
10
 
11
  # --- Import de VOS modules de travail ---
12
- # J'ai restauré les imports tels qu'ils étaient dans votre projet original.
13
  from src.cv_parsing_agents import CVParser
14
  from src.interview_simulator.entretient_version_prod import InterviewProcessor
15
- from src.config import Config # En supposant que vous ayez un fichier config
16
 
17
- # --- Import de la nouvelle tâche asynchrone ---
18
- from tasks.worker_celery import run_interview_analysis_task
19
 
20
- # Charger les variables d'environnement
21
  load_dotenv()
22
 
23
- # Configuration du logging
24
  logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger(__name__)
26
 
27
- # --- Initialisation de l'application FastAPI ---
28
- # J'ai restauré la structure que vous aviez probablement, avec l'initialisation des singletons.
 
 
 
 
29
  app = FastAPI(
30
- title="AIrh API - Version Restaurée",
31
- description="API complète incluant le parsing de CV, la simulation d'entretien, et l'analyse asynchrone, en respectant la structure originale.",
32
- version="2.0.0"
33
  )
34
 
35
- # --- Modèles de données Pydantic (inchangés) ---
36
  class ParsedCVResponse(BaseModel):
37
  candidat: Dict[str, Any]
38
 
@@ -58,45 +57,28 @@ class TaskStatusResponse(BaseModel):
58
 
59
  @app.get("/", summary="Health Check")
60
  async def read_root():
61
- return {"message": "AIrh Analysis API est opérationnelle."}
62
 
63
- # --- SECTION ORIGINALE RESTAURÉE ---
64
 
65
- @app.post("/parse-cv/", response_model=ParsedCVResponse, tags=["1. Parsing de CV (Logique Originale Adaptée)"])
66
  async def parse_cv(file: UploadFile = File(...)):
67
- """
68
- Endpoint pour parser un CV. La logique utilise maintenant le contenu en mémoire
69
- pour être compatible avec les conteneurs, mais l'esprit reste le même.
70
- """
71
  logger.info(f"Réception du fichier CV: {file.filename}")
72
-
73
- # Lecture du contenu du fichier en mémoire vive.
74
- # C'est l'adaptation nécessaire pour un environnement conteneurisé.
75
  cv_content = await file.read()
76
  if not cv_content:
77
  raise HTTPException(status_code=400, detail="Le fichier CV est vide.")
78
-
79
  try:
80
- # On suppose que votre CVParser peut maintenant accepter des octets (bytes).
81
- # C'est une modification mineure à faire dans la classe CVParser.
82
  parser = CVParser()
83
  parsed_data = await run_in_threadpool(parser.parse, cv_content)
84
-
85
  if not parsed_data or "candidat" not in parsed_data:
86
  raise HTTPException(status_code=422, detail="Impossible d'extraire les données structurées du CV.")
87
-
88
- logger.info("Parsing du CV réussi.")
89
  return parsed_data
90
  except Exception as e:
91
  logger.error(f"Erreur critique lors du parsing du CV: {e}", exc_info=True)
92
  raise HTTPException(status_code=500, detail=f"Erreur interne du serveur lors du parsing: {str(e)}")
93
 
94
- @app.post("/simulate-interview/", response_model=InterviewResponse, tags=["2. Simulation d'Entretien (Logique Originale)"])
95
  async def simulate_interview(request: InterviewRequest):
96
- """
97
- Endpoint pour gérer un tour de conversation dans la simulation d'entretien.
98
- Cette fonction est conservée telle quelle pour la partie interactive.
99
- """
100
  logger.info("Réception d'une requête pour la simulation d'entretien.")
101
  try:
102
  processor = InterviewProcessor(
@@ -105,45 +87,50 @@ async def simulate_interview(request: InterviewRequest):
105
  conversation_history=request.conversation_history
106
  )
107
  ai_response_object = await run_in_threadpool(processor.run, messages=request.messages)
108
-
109
- # On extrait la dernière réponse de l'assistant pour la retourner au frontend.
110
  last_message = ai_response_object["messages"][-1].content
111
  return {"response": last_message}
112
  except Exception as e:
113
  logger.error(f"Erreur lors de la simulation d'entretien: {e}", exc_info=True)
114
  raise HTTPException(status_code=500, detail=f"Erreur interne du serveur lors de la simulation: {str(e)}")
115
 
116
- # --- SECTION MODIFIÉE POUR L'ANALYSE ASYNCHRONE ---
117
- # C'est ici que se trouve la seule modification majeure de votre logique.
118
 
119
- @app.post("/trigger-analysis/", response_model=TaskStatusResponse, status_code=202, tags=["3. Analyse Asynchrone"])
120
  async def trigger_analysis(request: AnalysisRequest):
121
  """
122
- Déclenche l'analyse de l'entretien en tâche de fond via Celery.
123
  """
124
- logger.info(f"Déclenchement de l'analyse pour une conversation de {len(request.conversation_history)} messages.")
 
 
 
125
  try:
126
- # On appelle la tâche Celery de manière asynchrone.
127
- task = run_interview_analysis_task.delay(
128
- request.conversation_history,
129
- [request.job_description_text]
130
- )
131
- # On retourne immédiatement l'ID de la tâche pour que le client puisse suivre son statut.
132
- return {"task_id": task.id, "status": "PENDING", "result": None}
133
- except Exception as e:
134
- logger.error(f"Erreur lors du déclenchement de la tâche Celery: {e}", exc_info=True)
135
- raise HTTPException(status_code=500, detail="Impossible de soumettre la tâche d'analyse.")
136
-
137
- @app.get("/analysis-status/{task_id}", response_model=TaskStatusResponse, tags=["3. Analyse Asynchrone"])
138
  async def get_analysis_status(task_id: str):
139
  """
140
- Vérifie le statut d'une tâche d'analyse en cours d'exécution.
141
  """
142
- logger.info(f"Vérification du statut pour la tâche ID: {task_id}")
143
- # On utilise l'ID pour récupérer le résultat depuis le backend Celery (Upstash Redis).
144
- task_result = AsyncResult(task_id)
145
-
146
- status = task_result.status
147
- result = task_result.result if task_result.ready() else None
148
-
149
- return {"task_id": task_id, "status": status, "result": result}
 
 
 
 
 
 
 
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
 
 
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(
 
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.")