import json import logging import sys from typing import Optional, Dict, Any # MODIFIÉ ICI: Supprime 'from google.genai import types' car types.GenerateContentConfig n'est plus utilisé de cette manière import google.generativeai as genai 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}") # --- Configuration globale du client Gemini (Ce bloc est maintenant supprimé car la configuration est faite via GenerativeModel) --- # Vous pouvez retirer ce bloc si vous l'aviez : # if settings.LLM_PROVIDER == "gemini" and settings.GEMINI_API_KEY: # try: # genai.configure( # api_key=settings.GEMINI_API_KEY, # client_options={"api_endpoint": "generativelanguage.googleapis.com"} # ) # logger.info("GenAI client globally configured with API endpoint.") # except Exception as e: # logger.error(f"Erreur lors de la configuration globale de GenAI: {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: # MODIFICATION CRUCIALE ICI : On initialise directement le GenerativeModel # genai.Client() et types.GenerateContentConfig ne sont plus utilisés directement ici self.model = genai.GenerativeModel( model_name=self.model_name, safety_settings=self.raw_safety_settings, # Passez safety_settings ici generation_config=self.raw_generation_config # Passez generation_config ici # La clé API est lue automatiquement depuis GEMINI_API_KEY si elle est configurée. # Ou vous pouvez la passer explicitement: api_key=settings.GEMINI_API_KEY ) logger.info(f"Modèle Gemini GenerativeModel initialisé avec modèle : {self.model_name}") except Exception as e: logger.error(f"Erreur d'initialisation du modèle Gemini: {e}") raise ValueError(f"Impossible d'initialiser le modèle Gemini. Vérifiez votre GEMINI_API_KEY et le nom du modèle. 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 : 'contents' est maintenant passé comme argument positionnel direct à generate_content response = await anyio.to_thread.run_sync( self.model.generate_content, contents, # <-- Correction pour l'erreur "unexpected keyword argument 'contents'" # Les configurations (température, safety_settings) sont déjà définies lors de l'initialisation de self.model ) 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()