departements
This commit is contained in:
parent
6b53a419c9
commit
4c180fe1f8
19 changed files with 21999 additions and 431 deletions
|
@ -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()
|
Loading…
Add table
Add a link
Reference in a new issue