ay/backend/routers/ai.py
2025-06-24 18:17:53 +02:00

213 lines
No EOL
9.9 KiB
Python

# backend/routers/ai.py (Mise à jour avec extraction de texte)
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field, model_validator
from services.ai_service import ai_service
from core.security import get_current_user
from models.user import User
from typing import Optional
# NOUVELLE IMPORTATION pour le service France Travail
from services.france_travail_offer_service import france_travail_offer_service
# NOUVELLES IMPORTATIONS pour les documents et la base de données
from crud import document as crud_document
from models.document import Document
from core.database import get_db
from sqlalchemy.orm import Session
# NOUVELLES IMPORTATIONS pour l'extraction de texte
import os
import pypdf # Pour les fichiers PDF
import docx # Pour les fichiers DOCX (pip install python-docx)
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
# Modèle de requête pour l'analyse d'offre
class AnalyzeRequest(BaseModel):
cv_id: Optional[int] = Field(None, description="ID du CV de l'utilisateur déjà stocké. Si fourni, cv_text sera ignoré.")
cv_text: Optional[str] = Field(None, description="Texte brut du CV à analyser. Utilisé si cv_id n'est pas fourni (ex: pour analyse anonyme).")
job_offer_text: Optional[str] = Field(None, description="Le texte complet de l'offre d'emploi à analyser (si pas d'offer_id).")
france_travail_offer_id: Optional[str] = Field(None, description="L'ID de l'offre France Travail à analyser (si pas de job_offer_text).")
@model_validator(mode='after')
def check_inputs_provided(self) -> 'AnalyzeRequest':
if not (self.cv_id or self.cv_text):
raise ValueError("Veuillez fournir un 'cv_id' ou un 'cv_text'.")
if not (self.job_offer_text or self.france_travail_offer_id):
raise ValueError("Au moins 'job_offer_text' ou 'france_travail_offer_id' doit être fourni pour l'offre d'emploi.")
return self
# Fonction utilitaire pour extraire le texte d'un fichier
def extract_text_from_file(filepath: str) -> str:
file_extension = os.path.splitext(filepath)[1].lower()
text_content = ""
if not os.path.exists(filepath):
raise FileNotFoundError(f"Le fichier n'existe pas : {filepath}")
if file_extension == ".pdf":
try:
with open(filepath, 'rb') as f:
reader = pypdf.PdfReader(f)
for page in reader.pages:
text_content += page.extract_text() or ""
if not text_content.strip(): # Vérifie si le texte extrait est vide ou ne contient que des espaces
logger.warning(f"Le fichier PDF {filepath} a été lu mais aucun texte significatif n'a été extrait.")
except Exception as e:
logger.error(f"Erreur lors de l'extraction du texte du PDF {filepath}: {e}")
raise ValueError(f"Impossible d'extraire le texte du fichier PDF. Erreur: {e}")
elif file_extension == ".docx":
try:
document = docx.Document(filepath)
for paragraph in document.paragraphs:
text_content += paragraph.text + "\n"
if not text_content.strip():
logger.warning(f"Le fichier DOCX {filepath} a été lu mais aucun texte significatif n'a été extrait.")
except Exception as e:
logger.error(f"Erreur lors de l'extraction du texte du DOCX {filepath}: {e}")
raise ValueError(f"Impossible d'extraire le texte du fichier DOCX. Erreur: {e}")
else: # Tente de lire comme un fichier texte
try:
with open(filepath, 'r', encoding='utf-8') as f:
text_content = f.read()
except UnicodeDecodeError:
# Si UTF-8 échoue, tente latin-1
try:
with open(filepath, 'r', encoding='latin-1') as f:
text_content = f.read()
except Exception as e:
logger.error(f"Erreur lors de la lecture du fichier texte {filepath} avec UTF-8 et Latin-1: {e}")
raise ValueError(f"Impossible de lire le fichier texte (problème d'encodage). Erreur: {e}")
except Exception as e:
logger.error(f"Erreur inattendue lors de la lecture du fichier texte {filepath}: {e}")
raise ValueError(f"Impossible de lire le fichier texte. Erreur: {e}")
return text_content
@router.post("/analyze-job-offer-and-cv", summary="Analyse la pertinence d'un CV pour une offre d'emploi", response_model=dict)
async def analyze_job_offer_and_cv_route(
request: AnalyzeRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Analyse la pertinence d'un CV par rapport à une offre d'emploi en utilisant l'IA.
Prend en entrée soit les textes bruts, soit les IDs des documents.
"""
cv_text_to_analyze: Optional[str] = request.cv_text
if request.cv_id:
cv_document: Optional[Document] = crud_document.get_document_by_id(db, request.cv_id, current_user.id)
if not cv_document:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="CV non trouvé ou non accessible par cet utilisateur.")
try:
# Utilise la nouvelle fonction d'extraction de texte
cv_text_to_analyze = extract_text_from_file(cv_document.filepath)
if not cv_text_to_analyze.strip(): # Vérifier après extraction si le contenu est vide
raise ValueError("Le fichier CV est vide ou l'extraction de texte a échoué.")
except FileNotFoundError as e:
logger.error(f"Fichier CV introuvable: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fichier CV introuvable sur le serveur: {e}")
except ValueError as e:
logger.error(f"Erreur lors de l'extraction/lecture du CV {cv_document.filepath}: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur lors de la lecture ou de l'extraction du CV: {e}")
except Exception as e:
logger.error(f"Erreur inattendue lors du traitement du CV {cv_document.filepath}: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur interne lors du traitement du CV: {e}")
# Le reste du code pour l'offre d'emploi reste inchangé
job_offer_text_to_analyze: Optional[str] = request.job_offer_text
if request.france_travail_offer_id:
try:
offer_details = await france_travail_offer_service.get_offer_details(request.france_travail_offer_id)
job_offer_text_to_analyze = offer_details.description
if not job_offer_text_to_analyze:
raise ValueError("La description de l'offre France Travail est vide.")
except RuntimeError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de la récupération de l'offre France Travail: {e}"
)
if not job_offer_text_to_analyze:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible d'obtenir le texte de l'offre d'emploi pour l'analyse."
)
if not cv_text_to_analyze:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Le texte du CV n'a pas pu être obtenu.")
try:
analysis_result = await ai_service.analyze_job_offer_and_cv(
job_offer_text=job_offer_text_to_analyze,
cv_text=cv_text_to_analyze
)
return analysis_result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# L'endpoint /score-offer-anonymous
@router.post("/score-offer-anonymous", summary="Analyse la pertinence d'un CV pour une offre d'emploi (anonyme)", response_model=dict)
async def score_offer_anonymous(
request: AnalyzeRequest,
db: Session = Depends(get_db)
):
"""
Analyse la pertinence d'un CV par rapport à une offre d'emploi sans nécessiter d'authentification.
Prend uniquement le texte de l'offre d'emploi.
"""
if not request.job_offer_text and not request.france_travail_offer_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Au moins 'job_offer_text' ou 'france_travail_offer_id' doit être fourni pour l'offre d'emploi."
)
if request.cv_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Le 'cv_id' n'est pas autorisé pour les analyses anonymes."
)
job_offer_text_to_analyze: Optional[str] = request.job_offer_text
if request.france_travail_offer_id:
try:
offer_details = await france_travail_offer_service.get_offer_details(request.france_travail_offer_id)
job_offer_text_to_analyze = offer_details.description
if not job_offer_text_to_analyze:
raise ValueError("La description de l'offre France Travail est vide.")
except RuntimeError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de la récupération de l'offre France Travail: {e}"
)
if not job_offer_text_to_analyze:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible d'obtenir le texte de l'offre d'emploi pour l'analyse."
)
if not request.cv_text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="'cv_text' est requis pour l'analyse anonyme si le CV n'est pas stocké."
)
try:
analysis_result = await ai_service.analyze_job_offer_and_cv(
job_offer_text=job_offer_text_to_analyze,
cv_text=request.cv_text
)
return analysis_result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))