import json import logging import sys from typing import Optional, Dict, Any, List import google.genai as genai # CORRECTION ICI : Importez explicitement HarmCategory et HarmBlockThreshold from google.genai import types, HarmCategory, HarmBlockThreshold # Pour accéder à GenerationConfig, HarmCategory, HarmBlockThreshold import mistralai from mistralai.client import MistralClient from fastapi import HTTPException, status import anyio from core.config import settings from services.romeo_service import romeo_service # Assurez-vous que ce service existe et est configuré logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # --- DEBUGGING PRINTS --- try: logger.info(f"Loaded mistralai package from: {mistralai.__file__}") logger.info(f"mistralai package version: {mistralai.__version__}") if hasattr(MistralClient, '__module__'): logger.info(f"MistralClient class module: {MistralClient.__module__}") client_module = sys.modules.get(MistralClient.__module__) if client_module and hasattr(client_module, '__file__'): logger.info(f"MistralClient class file: {client_module.__file__}") except Exception as e: logger.error(f"Error during mistralai debug info collection: {e}") class AIService: def __init__(self): self.provider = settings.LLM_PROVIDER self.model_name = settings.GEMINI_MODEL_NAME if self.provider == "gemini" else settings.MISTRAL_MODEL_NAME self.client = None # Initialise le client à None self.model = None # Initialise l'instance du modèle à None # S'assurer que generation_config et safety_settings sont toujours définis self.generation_config = types.GenerationConfig( candidate_count=1, max_output_tokens=2048, temperature=0.7, top_k=40, top_p=0.95 ) self.safety_settings = [ {"category": HarmCategory.HARM_CATEGORY_HARASSMENT, "threshold": HarmBlockThreshold.BLOCK_NONE}, {"category": HarmCategory.HARM_CATEGORY_HATE_SPEECH, "threshold": HarmBlockThreshold.BLOCK_NONE}, {"category": HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, "threshold": HarmBlockThreshold.BLOCK_NONE}, {"category": HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, "threshold": HarmBlockThreshold.BLOCK_NONE}, ] if self.provider == "gemini": if not settings.GEMINI_API_KEY: raise ValueError("GEMINI_API_KEY n'est pas configurée.") genai.configure(api_key=settings.GEMINI_API_KEY) self.client = genai # Ceci est l'API de base # Créez une instance de modèle spécifique sur laquelle appeler generate_content_async self.model = genai.GenerativeModel(self.model_name, generation_config=self.generation_config) elif self.provider == "mistral": if not settings.MISTRAL_API_KEY: raise ValueError("MISTRAL_API_KEY n'est pas configurée.") # Initialize Mistral client self.client = MistralClient(api_key=settings.MISTRAL_API_KEY) # Pour Mistral, le client est directement l'objet qui appelle le chat, pas un modèle séparé comme pour Gemini. # Gardez self.model à None ou à une valeur non utilisée si vous ne l'utilisez pas avec Mistral. else: raise ValueError(f"Fournisseur LLM inconnu: {self.provider}") async def _call_gemini_api(self, prompt: str) -> str: try: # CORRECTION ICI: Utilisez self.model pour appeler generate_content_async if not self.model: # Ajout d'une vérification pour s'assurer que le modèle est initialisé raise ValueError("Le modèle Gemini n'a pas été correctement initialisé.") response = await self.model.generate_content_async( prompt, generation_config=self.generation_config, safety_settings=self.safety_settings ) return response.text except Exception as e: logger.error(f"Erreur lors de l'appel à Gemini: {e}") raise RuntimeError(f"Erreur lors de l'appel à Gemini: {e}") async def _call_mistral_api(self, prompt: str) -> str: try: # Assurez-vous que self.client est bien un MistralClient if not isinstance(self.client, MistralClient): raise TypeError("Le client Mistral n'est pas correctement initialisé.") response = self.client.chat( model=self.model_name, messages=[{"role": "user", "content": prompt}], temperature=self.generation_config.temperature, max_tokens=self.generation_config.max_output_tokens, ) return response.choices[0].message.content except Exception as e: logger.error(f"Erreur lors de l'appel à Mistral: {e}") raise RuntimeError(f"Erreur lors de l'appel à Mistral: {e}") async def analyze_job_offer_and_cv( self, job_offer_text: str, cv_text: str ) -> Dict[str, Any]: prompt = f""" En tant qu'expert en recrutement, j'ai besoin d'une analyse comparative détaillée entre une offre d'emploi et un CV. L'analyse doit identifier les correspondances, les lacunes et les suggestions d'amélioration pour le CV, en vue de maximiser les chances d'obtenir le poste. Voici l'offre d'emploi : --- {job_offer_text} --- Voici le CV : --- {cv_text} --- Veuillez fournir l'analyse dans le format JSON suivant, en vous assurant que tous les champs sont présents et remplis : ```json {{ "match_score": "Score de correspondance global (sur 100).", "correspondances": [ {{ "categorie": "Catégorie de correspondance (ex: 'Compétences techniques', 'Expérience', 'Qualités personnelles', 'Mots-clés')", "elements": ["Liste des éléments correspondants trouvés dans l'offre et le CV."] }} ], "lacunes": [ {{ "categorie": "Catégorie de lacune (ex: 'Compétences manquantes', 'Expérience insuffisante', 'Mots-clés absents')", "elements": ["Liste des éléments de l'offre d'emploi qui ne sont pas (ou peu) présents dans le CV."] }} ], "suggestions_cv": [ "Suggestions spécifiques pour améliorer le CV afin de mieux correspondre à l'offre (ex: 'Ajouter des détails sur...', 'Mettre en avant l'expérience en...', 'Inclure le mot-clé...')." ], "qualites_perso_identifiees": ["Liste des qualités personnelles déduites du CV."], "mots_cles_pertinents_offre": ["Liste des mots-clés importants identifiés dans l'offre d'emploi."], "metiers_rome_suggeres_offre": [], "competences_rome_suggeres_offre": [], "analyse_detaillee": "Une analyse narrative plus approfondie des points forts et faibles du CV par rapport à l'offre, et un résumé général." }} ``` Assurez-vous que la réponse est un JSON valide et complet. Ne pas inclure de texte explicatif avant ou après le bloc JSON. """ logger.info(f"Envoi du prompt au LLM ({self.provider}): \n {prompt[:200]}...") # Log des 200 premiers caractères du prompt response_content = "" try: if self.provider == "gemini": response_content = await self._call_gemini_api(prompt) elif self.provider == "mistral": response_content = await self._call_mistral_api(prompt) else: raise ValueError("Fournisseur LLM non supporté.") except Exception as e: logger.error(f"Échec de l'appel au service LLM: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur lors de l'appel au service LLM: {e}" ) try: # Gemini renvoie parfois du Markdown, donc on extrait le JSON if self.provider == "gemini" and "```json" in response_content: json_start = response_content.find("```json") + len("```json") json_end = response_content.find("```", json_start) if json_end != -1: json_str = response_content[json_start:json_end].strip() else: json_str = response_content[json_start:].strip() else: json_str = response_content.strip() analysis_result = json.loads(json_str) logger.info("Réponse JSON du LLM parsée avec succès.") except json.JSONDecodeError as e: logger.error(f"Erreur de parsing JSON depuis la réponse du LLM: {e}. Réponse brute: {response_content}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur de format de réponse du LLM. Impossible de parser le JSON." ) except Exception as e: logger.error(f"Erreur inattendue lors du traitement de la réponse LLM: {e}. Réponse brute: {response_content}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur inattendue lors du traitement de la réponse LLM: {e}" ) # Intégration du service Romeo try: logger.info("Début de l'intégration avec le service Romeo...") # S'assurer que les textes ne sont pas None avant de les passer à Romeo job_offer_text_for_romeo = job_offer_text if job_offer_text is not None else "" cv_text_for_romeo = cv_text if cv_text is not None else "" # Appels aux services Romeo # romeo_metiers_predictions = await romeo_service.predict_metiers(job_offer_text_for_romeo) # romeo_competences_predictions = await romeo_service.predict_competences(job_offer_text_for_romeo) # NOTE: Les appels Romeo sont mis en commentaire car vous pourriez vouloir les activer sélectivement # ou les décommenter une fois que la base d'analyse LLM est stable. # Si vous utilisez romeo_service, assurez-vous qu'il est correctement initialisé et accessible. # Exemple de comment les utiliser si activé: # Extraction des codes ROME (par exemple, 'D1101') des prédictions métiers # et des codes de compétences ROME (par exemple, 'G1601') des prédictions de compétences # predicted_rome_metiers = [ # m["codeRome"] for m in romeo_metiers_predictions if "codeRome" in m # ] if romeo_metiers_predictions else [] # predicted_rome_competences = [ # c["codeAppellation"] for c in romeo_competences_predictions if "codeAppellation" in c # ] if romeo_competences_predictions else [] # Utiliser ces prédictions de Romeo pour mettre à jour le résultat de l'analyse # analysis_result["metiers_rome_suggeres_offre"] = list(set(predicted_rome_metiers)) # Utilise set pour éviter les doublons # analysis_result["competences_rome_suggeres_offre"] = list(set(predicted_rome_competences)) # Utilise set pour éviter les doublons logger.info("Intégration Romeo terminée avec succès (ou ignorée si en commentaire).") except Exception as e: logger.error(f"Erreur lors de l'intégration avec le service Romeo: {e}") # Ne pas relancer une HTTPException ici si l'intégration Romeo est optionnelle ou en cours de développement, # car cela masquerait l'analyse LLM. Vous pouvez choisir de logguer et continuer, ou de relancer si c'est critique. # Pour l'instant, on se contente de logguer l'erreur. pass return analysis_result # Instanciation unique du service AI... ai_service = AIService()