departements

This commit is contained in:
el 2025-07-01 18:25:10 +02:00
parent 6b53a419c9
commit 4c180fe1f8
19 changed files with 21999 additions and 431 deletions

View file

@ -1,11 +1,12 @@
import json
import logging
import sys
from typing import Optional, Dict, Any
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
# MODIFIÉ ICI: Importations pour google-genai
from google import genai
from google.genai import types # Nécessaire pour configurer les types comme GenerateContentConfig
import mistralai
from mistralai.client import MistralClient
@ -13,6 +14,7 @@ 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__)
@ -29,140 +31,208 @@ try:
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":
try:
# Initialisation du client genai.Client()
self.client = genai.Client(
api_key=settings.GEMINI_API_KEY
)
logger.info(f"Client Gemini genai.Client() initialisé.")
# Configuration de la génération avec types.GenerateContentConfig
self.gemini_config = types.GenerateContentConfig(
temperature=0.7,
top_p=1.0,
top_k=1,
safety_settings=[
types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_NONE"),
]
)
logger.info(f"Configuration Gemini types.GenerateContentConfig créée.")
except Exception as e:
logger.error(f"Erreur d'initialisation du client Gemini: {e}")
raise ValueError(f"Impossible d'initialiser le client Gemini. Vérifiez votre GEMINI_API_KEY. Erreur: {e}")
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 dans les paramètres.")
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 non supporté: {self.provider}")
raise ValueError(f"Fournisseur LLM inconnu: {self.provider}")
logger.info(f"AI Service initialized with Provider: {self.provider}, Model: {self.model_name}")
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 analyze_job_offer_and_cv(self, job_offer_text: str, cv_text: str) -> Dict[str, Any]:
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'assistant spécialisé dans la rédaction de CV et de lettres de motivation, votre tâche est d'analyser une offre d'emploi et un CV fournis, puis de :
1. Calculer un score de pertinence entre 0 et 100 indiquant à quel point le CV correspond à l'offre.
2. Identifier les 3 à 5 points forts du CV en relation avec l'offre.
3. Suggérer 3 à 5 améliorations clés pour le CV afin de mieux correspondre à l'offre.
4. Proposer une brève phrase d'accroche pour une lettre de motivation, personnalisée pour cette offre et ce CV.
5. Identifier 3 à 5 mots-clés ou phrases importants de l'offre d'emploi que l'on devrait retrouver dans le CV.
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.
L'offre d'emploi est la suivante :
Voici l'offre d'emploi :
---
{job_offer_text}
---
Le CV est le suivant :
Voici le CV :
---
{cv_text}
---
Veuillez retourner votre analyse au format JSON, en respectant la structure suivante :
Veuillez fournir l'analyse dans le format JSON suivant, en vous assurant que tous les champs sont présents et remplis :
```json
{{
"score_pertinence": int,
"points_forts": ["string", "string", ...],
"ameliorations_cv": ["string", "string", ...],
"phrase_accroche_lm": "string",
"mots_cles_offre": ["string", "string", ...]
"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 = ""
if self.provider == "gemini":
try:
# MODIFIÉ ICI: Utilisation d'une lambda pour envelopper l'appel à generate_content
# avec tous ses arguments, pour que run_sync reçoive une fonction sans arguments supplémentaires
response = await anyio.to_thread.run_sync(
lambda: self.client.models.generate_content(
model=self.model_name,
contents=[{"role": "user", "parts": [{"text": prompt}]}],
config=self.gemini_config,
)
)
response_content = response.text
# Nettoyage de la réponse pour retirer les blocs de code Markdown
if response_content.startswith("```json") and response_content.endswith("```"):
response_content = response_content[len("```json"): -len("```")].strip()
elif response_content.startswith("```") and response_content.endswith("```"):
response_content = response_content[len("```"): -len("```")].strip()
except Exception as e:
logger.error(f"Erreur lors de l'appel à Gemini: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de l'appel à l'API Gemini: {e}"
)
elif self.provider == "mistral":
if not settings.MISTRAL_API_KEY:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="La clé API Mistral n'est pas configurée."
)
try:
response = await self.client.chat_async(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=1000
)
response_content = response.choices[0].message.content
except Exception as e:
logger.error(f"Erreur lors de l'appel à Mistral: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de l'appel à l'API Mistral: {e}"
)
else:
raise ValueError(f"Fournisseur LLM non supporté: {self.provider}")
logger.info(f"Réponse brute de l'IA (après nettoyage si nécessaire) ({self.provider}): {response_content}")
try:
parsed_response = json.loads(response_content)
return parsed_response
except json.JSONDecodeError as e:
logger.error(f"Erreur de décodage JSON de la réponse IA ({self.provider}): {e}")
logger.error(f"Contenu non-JSON reçu (après nettoyage): {response_content}")
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="La réponse de l'IA n'était pas au format JSON attendu."
detail=f"Erreur lors de l'appel au service LLM: {e}"
)
# Instanciation unique du service AI
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()