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