This commit is contained in:
el 2025-06-24 18:17:53 +02:00
commit d7666f7b2c
44 changed files with 2246 additions and 0 deletions

View file

@ -0,0 +1,184 @@
import json
import logging
import sys
from typing import Optional, Dict, Any
from google import genai
from google.genai import types
import mistralai
from mistralai.client import MistralClient
from fastapi import HTTPException, status
import anyio # <-- NOUVELLE IMPORTATION : Pour gérer les appels synchrones dans async
from core.config import settings
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.raw_safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE"
},
]
self.raw_generation_config = {
"temperature": 0.7,
"top_p": 1,
"top_k": 1,
}
if self.provider == "gemini":
try:
self.client = genai.Client(api_key=settings.GEMINI_API_KEY)
self.gemini_config = types.GenerateContentConfig(
temperature=self.raw_generation_config["temperature"],
top_p=self.raw_generation_config["top_p"],
top_k=self.raw_generation_config["top_k"],
safety_settings=[
types.SafetySetting(category=s["category"], threshold=s["threshold"])
for s in self.raw_safety_settings
]
)
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}")
elif self.provider == "mistral":
if not settings.MISTRAL_API_KEY:
raise ValueError("MISTRAL_API_KEY n'est pas configurée dans les paramètres.")
self.client = MistralClient(api_key=settings.MISTRAL_API_KEY)
else:
raise ValueError(f"Fournisseur LLM non supporté: {self.provider}")
logger.info(f"AI Service initialized with Provider: {self.provider}, Model: {self.model_name}")
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.
L'offre d'emploi est la suivante :
---
{job_offer_text}
---
Le CV est le suivant :
---
{cv_text}
---
Veuillez retourner votre analyse au format JSON, en respectant la structure suivante :
{{
"score_pertinence": int,
"points_forts": ["string", "string", ...],
"ameliorations_cv": ["string", "string", ...],
"phrase_accroche_lm": "string",
"mots_cles_offre": ["string", "string", ...]
}}
"""
response_content = ""
if self.provider == "gemini":
try:
contents = [
{"role": "user", "parts": [{"text": prompt}]}
]
# MODIFIÉ ICI : Utilisation de anyio.to_thread.run_sync pour l'appel synchrone
response = await anyio.to_thread.run_sync(
self.client.models.generate_content,
model=self.model_name,
contents=contents,
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}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="La réponse de l'IA n'était pas au format JSON attendu."
)
# Instanciation unique du service AI
ai_service = AIService()