ay/backend/services/ai_service.py

168 lines
No EOL
7.5 KiB
Python

import json
import logging
import sys
from typing import Optional, Dict, Any
# 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
from fastapi import HTTPException, status
import anyio
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
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}")
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:
# 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}")
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()