# 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: logger.info(f"Tentative d'extraction du texte du CV à partir de : {cv_document.filepath}") # AJOUTEZ CETTE # --- AJOUTEZ CES LIGNES DE DEBUG --- logger.info(f"Texte extrait (début) du CV: '{cv_text_to_analyze[:100]}...'") # Affiche les 100 premiers caractères logger.info(f"Longueur du texte extrait du CV (avant strip): {len(cv_text_to_analyze)}") logger.info(f"Longueur du texte extrait du CV (après strip): {len(cv_text_to_analyze.strip())}") # --- FIN DES LIGNES DE DEBUG --- 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))