183 lines
No EOL
8.4 KiB
Python
183 lines
No EOL
8.4 KiB
Python
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() |