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()