238 lines
No EOL
12 KiB
Python
238 lines
No EOL
12 KiB
Python
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() |