Spaces:
Sleeping
Sleeping
Update main.py
Browse files
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
|
| 16 |
|
| 17 |
-
# ---
|
| 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 |
-
# ---
|
| 28 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
app = FastAPI(
|
| 30 |
-
title="AIrh API
|
| 31 |
-
description="API
|
| 32 |
-
version="2.
|
| 33 |
)
|
| 34 |
|
| 35 |
-
# --- Modèles de données Pydantic
|
| 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
|
| 62 |
|
| 63 |
-
# --- SECTION ORIGINALE
|
| 64 |
|
| 65 |
-
@app.post("/parse-cv/", response_model=ParsedCVResponse, tags=["1. Parsing de CV
|
| 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
|
| 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
|
| 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
|
| 123 |
"""
|
| 124 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
| 125 |
try:
|
| 126 |
-
# On
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 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
|
| 141 |
"""
|
| 142 |
-
logger.info(f"Vérification du statut
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.")
|