departements
This commit is contained in:
parent
6b53a419c9
commit
4c180fe1f8
19 changed files with 21999 additions and 431 deletions
|
@ -1,11 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
import google.genai as genai
|
||||
# CORRECTION ICI : Importez explicitement HarmCategory et HarmBlockThreshold
|
||||
from google.genai import types, HarmCategory, HarmBlockThreshold # Pour accéder à GenerationConfig, HarmCategory, HarmBlockThreshold
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -13,6 +14,7 @@ from fastapi import HTTPException, status
|
|||
import anyio
|
||||
|
||||
from core.config import settings
|
||||
from services.romeo_service import romeo_service # Assurez-vous que ce service existe et est configuré
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -29,140 +31,208 @@ try:
|
|||
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.client = None # Initialise le client à None
|
||||
self.model = None # Initialise l'instance du modèle à None
|
||||
|
||||
# S'assurer que generation_config et safety_settings sont toujours définis
|
||||
self.generation_config = types.GenerationConfig(
|
||||
candidate_count=1,
|
||||
max_output_tokens=2048,
|
||||
temperature=0.7,
|
||||
top_k=40,
|
||||
top_p=0.95
|
||||
)
|
||||
self.safety_settings = [
|
||||
{"category": HarmCategory.HARM_CATEGORY_HARASSMENT, "threshold": HarmBlockThreshold.BLOCK_NONE},
|
||||
{"category": HarmCategory.HARM_CATEGORY_HATE_SPEECH, "threshold": HarmBlockThreshold.BLOCK_NONE},
|
||||
{"category": HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, "threshold": HarmBlockThreshold.BLOCK_NONE},
|
||||
{"category": HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, "threshold": HarmBlockThreshold.BLOCK_NONE},
|
||||
]
|
||||
|
||||
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}")
|
||||
|
||||
if not settings.GEMINI_API_KEY:
|
||||
raise ValueError("GEMINI_API_KEY n'est pas configurée.")
|
||||
genai.configure(api_key=settings.GEMINI_API_KEY)
|
||||
self.client = genai # Ceci est l'API de base
|
||||
# Créez une instance de modèle spécifique sur laquelle appeler generate_content_async
|
||||
self.model = genai.GenerativeModel(self.model_name, generation_config=self.generation_config)
|
||||
elif self.provider == "mistral":
|
||||
if not settings.MISTRAL_API_KEY:
|
||||
raise ValueError("MISTRAL_API_KEY n'est pas configurée dans les paramètres.")
|
||||
raise ValueError("MISTRAL_API_KEY n'est pas configurée.")
|
||||
# Initialize Mistral client
|
||||
self.client = MistralClient(api_key=settings.MISTRAL_API_KEY)
|
||||
# Pour Mistral, le client est directement l'objet qui appelle le chat, pas un modèle séparé comme pour Gemini.
|
||||
# Gardez self.model à None ou à une valeur non utilisée si vous ne l'utilisez pas avec Mistral.
|
||||
else:
|
||||
raise ValueError(f"Fournisseur LLM non supporté: {self.provider}")
|
||||
raise ValueError(f"Fournisseur LLM inconnu: {self.provider}")
|
||||
|
||||
logger.info(f"AI Service initialized with Provider: {self.provider}, Model: {self.model_name}")
|
||||
async def _call_gemini_api(self, prompt: str) -> str:
|
||||
try:
|
||||
# CORRECTION ICI: Utilisez self.model pour appeler generate_content_async
|
||||
if not self.model: # Ajout d'une vérification pour s'assurer que le modèle est initialisé
|
||||
raise ValueError("Le modèle Gemini n'a pas été correctement initialisé.")
|
||||
response = await self.model.generate_content_async(
|
||||
prompt,
|
||||
generation_config=self.generation_config,
|
||||
safety_settings=self.safety_settings
|
||||
)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'appel à Gemini: {e}")
|
||||
raise RuntimeError(f"Erreur lors de l'appel à Gemini: {e}")
|
||||
|
||||
async def analyze_job_offer_and_cv(self, job_offer_text: str, cv_text: str) -> Dict[str, Any]:
|
||||
async def _call_mistral_api(self, prompt: str) -> str:
|
||||
try:
|
||||
# Assurez-vous que self.client est bien un MistralClient
|
||||
if not isinstance(self.client, MistralClient):
|
||||
raise TypeError("Le client Mistral n'est pas correctement initialisé.")
|
||||
|
||||
response = self.client.chat(
|
||||
model=self.model_name,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=self.generation_config.temperature,
|
||||
max_tokens=self.generation_config.max_output_tokens,
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'appel à Mistral: {e}")
|
||||
raise RuntimeError(f"Erreur lors de l'appel à Mistral: {e}")
|
||||
|
||||
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.
|
||||
En tant qu'expert en recrutement, j'ai besoin d'une analyse comparative détaillée entre une offre d'emploi et un CV.
|
||||
L'analyse doit identifier les correspondances, les lacunes et les suggestions d'amélioration pour le CV, en vue de maximiser les chances d'obtenir le poste.
|
||||
|
||||
L'offre d'emploi est la suivante :
|
||||
Voici l'offre d'emploi :
|
||||
---
|
||||
{job_offer_text}
|
||||
---
|
||||
|
||||
Le CV est le suivant :
|
||||
Voici le CV :
|
||||
---
|
||||
{cv_text}
|
||||
---
|
||||
|
||||
Veuillez retourner votre analyse au format JSON, en respectant la structure suivante :
|
||||
Veuillez fournir l'analyse dans le format JSON suivant, en vous assurant que tous les champs sont présents et remplis :
|
||||
|
||||
```json
|
||||
{{
|
||||
"score_pertinence": int,
|
||||
"points_forts": ["string", "string", ...],
|
||||
"ameliorations_cv": ["string", "string", ...],
|
||||
"phrase_accroche_lm": "string",
|
||||
"mots_cles_offre": ["string", "string", ...]
|
||||
"match_score": "Score de correspondance global (sur 100).",
|
||||
"correspondances": [
|
||||
{{
|
||||
"categorie": "Catégorie de correspondance (ex: 'Compétences techniques', 'Expérience', 'Qualités personnelles', 'Mots-clés')",
|
||||
"elements": ["Liste des éléments correspondants trouvés dans l'offre et le CV."]
|
||||
}}
|
||||
],
|
||||
"lacunes": [
|
||||
{{
|
||||
"categorie": "Catégorie de lacune (ex: 'Compétences manquantes', 'Expérience insuffisante', 'Mots-clés absents')",
|
||||
"elements": ["Liste des éléments de l'offre d'emploi qui ne sont pas (ou peu) présents dans le CV."]
|
||||
}}
|
||||
],
|
||||
"suggestions_cv": [
|
||||
"Suggestions spécifiques pour améliorer le CV afin de mieux correspondre à l'offre (ex: 'Ajouter des détails sur...', 'Mettre en avant l'expérience en...', 'Inclure le mot-clé...')."
|
||||
],
|
||||
"qualites_perso_identifiees": ["Liste des qualités personnelles déduites du CV."],
|
||||
"mots_cles_pertinents_offre": ["Liste des mots-clés importants identifiés dans l'offre d'emploi."],
|
||||
"metiers_rome_suggeres_offre": [],
|
||||
"competences_rome_suggeres_offre": [],
|
||||
"analyse_detaillee": "Une analyse narrative plus approfondie des points forts et faibles du CV par rapport à l'offre, et un résumé général."
|
||||
}}
|
||||
```
|
||||
Assurez-vous que la réponse est un JSON valide et complet. Ne pas inclure de texte explicatif avant ou après le bloc JSON.
|
||||
"""
|
||||
|
||||
logger.info(f"Envoi du prompt au LLM ({self.provider}): \n {prompt[:200]}...") # Log des 200 premiers caractères du prompt
|
||||
|
||||
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}")
|
||||
if self.provider == "gemini":
|
||||
response_content = await self._call_gemini_api(prompt)
|
||||
elif self.provider == "mistral":
|
||||
response_content = await self._call_mistral_api(prompt)
|
||||
else:
|
||||
raise ValueError("Fournisseur LLM non supporté.")
|
||||
except Exception as e:
|
||||
logger.error(f"Échec de l'appel au service LLM: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="La réponse de l'IA n'était pas au format JSON attendu."
|
||||
detail=f"Erreur lors de l'appel au service LLM: {e}"
|
||||
)
|
||||
|
||||
# Instanciation unique du service AI
|
||||
try:
|
||||
# Gemini renvoie parfois du Markdown, donc on extrait le JSON
|
||||
if self.provider == "gemini" and "```json" in response_content:
|
||||
json_start = response_content.find("```json") + len("```json")
|
||||
json_end = response_content.find("```", json_start)
|
||||
if json_end != -1:
|
||||
json_str = response_content[json_start:json_end].strip()
|
||||
else:
|
||||
json_str = response_content[json_start:].strip()
|
||||
else:
|
||||
json_str = response_content.strip()
|
||||
|
||||
analysis_result = json.loads(json_str)
|
||||
logger.info("Réponse JSON du LLM parsée avec succès.")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Erreur de parsing JSON depuis la réponse du LLM: {e}. Réponse brute: {response_content}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur de format de réponse du LLM. Impossible de parser le JSON."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors du traitement de la réponse LLM: {e}. Réponse brute: {response_content}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur inattendue lors du traitement de la réponse LLM: {e}"
|
||||
)
|
||||
|
||||
# Intégration du service Romeo
|
||||
try:
|
||||
logger.info("Début de l'intégration avec le service Romeo...")
|
||||
# S'assurer que les textes ne sont pas None avant de les passer à Romeo
|
||||
job_offer_text_for_romeo = job_offer_text if job_offer_text is not None else ""
|
||||
cv_text_for_romeo = cv_text if cv_text is not None else ""
|
||||
|
||||
# Appels aux services Romeo
|
||||
# romeo_metiers_predictions = await romeo_service.predict_metiers(job_offer_text_for_romeo)
|
||||
# romeo_competences_predictions = await romeo_service.predict_competences(job_offer_text_for_romeo)
|
||||
|
||||
# NOTE: Les appels Romeo sont mis en commentaire car vous pourriez vouloir les activer sélectivement
|
||||
# ou les décommenter une fois que la base d'analyse LLM est stable.
|
||||
# Si vous utilisez romeo_service, assurez-vous qu'il est correctement initialisé et accessible.
|
||||
|
||||
# Exemple de comment les utiliser si activé:
|
||||
# Extraction des codes ROME (par exemple, 'D1101') des prédictions métiers
|
||||
# et des codes de compétences ROME (par exemple, 'G1601') des prédictions de compétences
|
||||
# predicted_rome_metiers = [
|
||||
# m["codeRome"] for m in romeo_metiers_predictions if "codeRome" in m
|
||||
# ] if romeo_metiers_predictions else []
|
||||
# predicted_rome_competences = [
|
||||
# c["codeAppellation"] for c in romeo_competences_predictions if "codeAppellation" in c
|
||||
# ] if romeo_competences_predictions else []
|
||||
|
||||
# Utiliser ces prédictions de Romeo pour mettre à jour le résultat de l'analyse
|
||||
# analysis_result["metiers_rome_suggeres_offre"] = list(set(predicted_rome_metiers)) # Utilise set pour éviter les doublons
|
||||
# analysis_result["competences_rome_suggeres_offre"] = list(set(predicted_rome_competences)) # Utilise set pour éviter les doublons
|
||||
|
||||
logger.info("Intégration Romeo terminée avec succès (ou ignorée si en commentaire).")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'intégration avec le service Romeo: {e}")
|
||||
# Ne pas relancer une HTTPException ici si l'intégration Romeo est optionnelle ou en cours de développement,
|
||||
# car cela masquerait l'analyse LLM. Vous pouvez choisir de logguer et continuer, ou de relancer si c'est critique.
|
||||
# Pour l'instant, on se contente de logguer l'erreur.
|
||||
pass
|
||||
|
||||
return analysis_result
|
||||
|
||||
# Instanciation unique du service AI...
|
||||
ai_service = AIService()
|
|
@ -2,7 +2,7 @@
|
|||
import httpx
|
||||
import logging
|
||||
from core.config import settings
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FranceTravailAuthService:
|
||||
|
@ -16,12 +16,12 @@ class FranceTravailAuthService:
|
|||
|
||||
async def get_access_token(self):
|
||||
# Vérifiez si le token est encore valide dans le cache
|
||||
if self._token_cache and self._token_cache.get("expires_at", 0) > httpx._compat.current_time():
|
||||
if self._token_cache and self._token_cache.get("expires_at", 0) > datetime.now():
|
||||
logger.info("Utilisation du token France Travail depuis le cache.")
|
||||
return self._token_cache["access_token"]
|
||||
|
||||
logger.info("Obtention d'un nouveau token France Travail...")
|
||||
token_url = settings.FRANCE_TRAVAIL_TOKEN_URL
|
||||
token_url = settings.FRANCE_TRAVAIL_TOKEN_URL # C'est la ligne modifiée
|
||||
client_id = settings.FRANCE_TRAVAIL_CLIENT_ID
|
||||
client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET
|
||||
scope = "o2dsoffre api_offresdemploiv2" # Assurez-vous que ces scopes sont activés pour votre application
|
||||
|
@ -34,35 +34,36 @@ class FranceTravailAuthService:
|
|||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded" # C'est très important !
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(token_url, data=data, headers=headers)
|
||||
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP
|
||||
|
||||
token_data = response.json()
|
||||
access_token = token_data.get("access_token")
|
||||
expires_in = token_data.get("expires_in") # Durée de validité en secondes
|
||||
# DÉBUT DE LA CORRECTION : Ces lignes sont maintenant correctement indentées dans le bloc try
|
||||
token_data = response.json()
|
||||
access_token = token_data.get("access_token")
|
||||
expires_in = token_data.get("expires_in") # Durée de validité en secondes
|
||||
|
||||
if not access_token:
|
||||
raise ValueError("Le token d'accès n'a pas été trouvé dans la réponse de France Travail.")
|
||||
if not access_token:
|
||||
raise ValueError("Le token d'accès n'a pas été trouvé dans la réponse de France Travail.")
|
||||
|
||||
# Mettre à jour le cache
|
||||
self._token_cache = {
|
||||
"access_token": access_token,
|
||||
"expires_at": httpx._compat.current_time() + expires_in - 60 # 60 secondes de marge de sécurité
|
||||
}
|
||||
logger.info("Nouveau token France Travail obtenu et mis en cache.")
|
||||
return access_token
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
|
||||
# Re-raise une RuntimeError pour que le service appelant puisse la gérer
|
||||
raise RuntimeError(f"Erreur d'authentification France Travail: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
|
||||
raise RuntimeError(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
|
||||
# Mettre à jour le cache
|
||||
self._token_cache = {
|
||||
"access_token": access_token,
|
||||
"expires_at": datetime.now() + timedelta(seconds=expires_in - 60) # 60 secondes de marge de sécurité
|
||||
}
|
||||
logger.info("Nouveau token France Travail obtenu et mis en cache.")
|
||||
return access_token
|
||||
# FIN DE LA CORRECTION
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
|
||||
# Re-raise une RuntimeError pour que le service appelant puisse la gérer
|
||||
raise RuntimeError(f"Erreur d'authentification France Travail: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
|
||||
raise RuntimeError(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
|
||||
|
||||
france_travail_auth_service = FranceTravailAuthService()
|
|
@ -1,197 +1,225 @@
|
|||
# backend/services/france_travail_offer_service.py
|
||||
import httpx
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
import time
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
import asyncio
|
||||
|
||||
from core.config import settings
|
||||
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre, TypeContrat
|
||||
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FranceTravailOfferService:
|
||||
def __init__(self):
|
||||
self.client_id = settings.FRANCE_TRAVAIL_CLIENT_ID
|
||||
self.client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET
|
||||
self.token_url = settings.FRANCE_TRAVAIL_TOKEN_URL
|
||||
self.api_base_url = settings.FRANCE_TRAVAIL_API_BASE_URL
|
||||
self.api_scope = settings.FRANCE_TRAVAIL_API_SCOPE
|
||||
self.access_token = None
|
||||
self.token_expires_at = None
|
||||
self.client = httpx.AsyncClient(base_url=settings.FRANCE_TRAVAIL_API_BASE_URL)
|
||||
self.auth_client = httpx.AsyncClient(base_url=settings.FRANCE_TRAVAIL_TOKEN_URL.split('?')[0])
|
||||
self.token_info = {"token": None, "expires_at": 0}
|
||||
self.geo_api_client = httpx.AsyncClient() # Client for geo.api.gouv.fr
|
||||
|
||||
async def _get_access_token(self):
|
||||
if self.access_token and self.token_expires_at and datetime.now() < self.token_expires_at:
|
||||
logger.info("Réutilisation du token France Travail existant.")
|
||||
return self.access_token
|
||||
if self.token_info["token"] and self.token_info["expires_at"] > time.time() + 60: # Refresh 1 min before expiry
|
||||
logger.info("Utilisation du token France Travail depuis le cache.")
|
||||
return self.token_info["token"]
|
||||
|
||||
logger.info("Obtention d'un nouveau token d'accès France Travail...")
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
data = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"scope": self.api_scope
|
||||
}
|
||||
logger.info("Obtention d'un nouveau token France Travail...")
|
||||
try:
|
||||
token_url_with_realm = settings.FRANCE_TRAVAIL_TOKEN_URL
|
||||
response = await self.auth_client.post(
|
||||
token_url_with_realm,
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": settings.FRANCE_TRAVAIL_CLIENT_ID,
|
||||
"client_secret": settings.FRANCE_TRAVAIL_CLIENT_SECRET,
|
||||
"scope": "o2dsoffre api_offresdemploiv2"
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
token_data = response.json()
|
||||
self.token_info["token"] = token_data["access_token"]
|
||||
self.token_info["expires_at"] = time.time() + token_data["expires_in"]
|
||||
logger.info("Nouveau token France Travail obtenu et mis en cache.")
|
||||
return self.token_info["token"]
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Échec de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Échec de l'obtention du token France Travail: {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Erreur réseau lors de l'obtention du token France Travail: {e}")
|
||||
raise RuntimeError(f"Erreur réseau lors de l'obtention du token France Travail: {e}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(self.token_url, headers=headers, data=data)
|
||||
async def get_insee_and_postal_code_for_commune(self, commune_name: str) -> Optional[Tuple[str, str, float, float]]:
|
||||
logger.info(f"Début de la recherche Geo API pour: '{commune_name}'")
|
||||
try:
|
||||
# First, try exact match by name
|
||||
geo_url_by_name = f"https://geo.api.gouv.fr/communes?nom={commune_name}&fields=codesPostaux,code,nom,centre&format=json&limit=5"
|
||||
logger.info(f"Recherche par nom via API Geo.gouv.fr: {geo_url_by_name}")
|
||||
response = await self.geo_api_client.get(geo_url_by_name)
|
||||
response.raise_for_status()
|
||||
communes = response.json()
|
||||
|
||||
for commune in communes:
|
||||
# Prioritize exact name match
|
||||
if commune['nom'].lower() == commune_name.lower():
|
||||
insee_code = commune['code']
|
||||
postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
|
||||
latitude = commune['centre']['coordinates'][1]
|
||||
longitude = commune['centre']['coordinates'][0]
|
||||
logger.info(f"Correspondance exacte par nom trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour '{commune_name}'.")
|
||||
return insee_code, postal_code, latitude, longitude
|
||||
|
||||
# If no exact name match, try the first result if available
|
||||
if communes:
|
||||
commune = communes[0]
|
||||
insee_code = commune['code']
|
||||
postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
|
||||
latitude = commune['centre']['coordinates'][1]
|
||||
longitude = commune['centre']['coordinates'][0]
|
||||
logger.info(f"Aucune correspondance exacte par nom, première commune trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour '{commune_name}'.")
|
||||
return insee_code, postal_code, latitude, longitude
|
||||
|
||||
# If not found by name, try by postal code if commune_name looks like a postal code
|
||||
if commune_name.isdigit() and len(commune_name) == 5:
|
||||
geo_url_by_cp = f"https://geo.api.gouv.fr/communes?codePostal={commune_name}&fields=codesPostaux,code,nom,centre&format=json&limit=1"
|
||||
logger.info(f"Recherche par code postal via API Geo.gouv.fr: {geo_url_by_cp}")
|
||||
response = await self.geo_api_client.get(geo_url_by_cp)
|
||||
response.raise_for_status()
|
||||
token_data = response.json()
|
||||
self.access_token = token_data["access_token"]
|
||||
expires_in = token_data.get("expires_in", 1500)
|
||||
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 60)
|
||||
communes_by_cp = response.json()
|
||||
if communes_by_cp:
|
||||
commune = communes_by_cp[0]
|
||||
insee_code = commune['code']
|
||||
postal_code = commune['codesPostaux'][0] if commune['codesPostaux'] else None
|
||||
latitude = commune['centre']['coordinates'][1]
|
||||
longitude = commune['centre']['coordinates'][0]
|
||||
logger.info(f"Correspondance par code postal trouvée: INSEE='{insee_code}', CP='{postal_code}', Lat='{latitude}', Long='{longitude}' pour code postal '{commune_name}'.")
|
||||
return insee_code, postal_code, latitude, longitude
|
||||
|
||||
logger.info("Token France Travail obtenu avec succès.")
|
||||
return self.access_token
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Échec de l'obtention du token France Travail: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de l'obtention du token France Travail: {e}")
|
||||
raise RuntimeError(f"Échec inattendu lors de l'obtention du token France Travail: {e}")
|
||||
logger.warning(f"Aucune correspondance trouvée pour la commune/code postal: '{commune_name}' dans l'API Geo.gouv.fr.")
|
||||
return None
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de l'appel à l'API Geo.gouv.fr pour '{commune_name}': {e.response.status_code} - {e.response.text}")
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Erreur réseau lors de l'appel à l'API Geo.gouv.fr pour '{commune_name}': {e}")
|
||||
return None
|
||||
|
||||
async def get_insee_code_for_commune(self, commune_name: str) -> Optional[str]:
|
||||
"""
|
||||
Récupère le code INSEE d'une commune à partir de son nom.
|
||||
Recherche une correspondance exacte du libellé, ou un code spécifique pour Paris.
|
||||
"""
|
||||
async def search_offers(
|
||||
self,
|
||||
motsCles: Optional[str] = None,
|
||||
commune: Optional[str] = None, # Reste le nom de la commune
|
||||
codePostal: Optional[str] = None,
|
||||
latitude: Optional[float] = None,
|
||||
longitude: Optional[float] = None,
|
||||
distance: Optional[int] = None,
|
||||
# codeDepartement: Optional[str] = None, # Ce paramètre sera maintenant dérivé en interne
|
||||
range_start: int = 0,
|
||||
range_end: int = 14,
|
||||
typeContrat: Optional[List[str]] = None,
|
||||
experience: Optional[List[str]] = None
|
||||
) -> FranceTravailSearchResponse:
|
||||
token = await self._get_access_token()
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}"
|
||||
}
|
||||
params = {
|
||||
"q": commune_name
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
f"{self.api_base_url}/v2/referentiel/communes",
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
response.raise_for_status()
|
||||
communes_data = response.json()
|
||||
|
||||
found_code = None
|
||||
normalized_input_name = commune_name.upper().strip()
|
||||
|
||||
if communes_data and isinstance(communes_data, list):
|
||||
for commune_info in communes_data:
|
||||
if commune_info and "code" in commune_info and "libelle" in commune_info:
|
||||
normalized_libelle = commune_info["libelle"].upper().strip()
|
||||
|
||||
# Priorité 1: Recherche spécifique pour "PARIS" avec son code INSEE connu
|
||||
if normalized_input_name == "PARIS" and commune_info["code"] == "75056":
|
||||
found_code = commune_info["code"]
|
||||
break
|
||||
# Priorité 2: Correspondance exacte du libellé
|
||||
elif normalized_libelle == normalized_input_name:
|
||||
found_code = commune_info["code"]
|
||||
break
|
||||
# Priorité 3: Si c'est Paris, mais le libellé renvoyé n'est pas "PARIS" exactement,
|
||||
# mais le code est le bon, on le prend quand même.
|
||||
# Ceci peut arriver si l'API renvoie "Paris 01" par exemple.
|
||||
elif normalized_input_name == "PARIS" and commune_info["code"] in ["75056", "75101", "75102", "75103", "75104", "75105", "75106", "75107", "75108", "75109", "75110", "75111", "75112", "75113", "75114", "75115", "75116", "75117", "75118", "75119", "75120"]:
|
||||
# Note: Les codes 75101 à 75120 sont pour les arrondissements, mais l'API
|
||||
# France Travail utilise souvent le 75056 pour "Paris" globalement.
|
||||
# Cette condition est plus une sécurité, mais 75056 est la cible principale.
|
||||
if commune_info["code"] == "75056": # On préfère le code global de Paris
|
||||
found_code = commune_info["code"]
|
||||
break
|
||||
elif found_code is None: # Si on n'a pas encore trouvé 75056, on prend un arrondissement
|
||||
found_code = commune_info["code"] # Conserver le code d'arrondissement si c'est le seul "Paris" trouvé
|
||||
# Note: La logique ici est à affiner selon si vous voulez les arrondissements ou seulement le code global.
|
||||
# Pour la plupart des cas, "75056" est suffisant.
|
||||
|
||||
if found_code:
|
||||
logger.info(f"Code INSEE pour '{commune_name}' trouvé : {found_code}")
|
||||
return found_code
|
||||
|
||||
logger.warning(f"Aucun code INSEE exact trouvé pour la commune '{commune_name}' parmi les résultats de l'API. Vérifiez l'orthographe.")
|
||||
return None
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de la récupération du code INSEE pour '{commune_name}': {e.response.status_code} - {e.response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de la récupération du code INSEE pour '{commune_name}': {e}")
|
||||
return None
|
||||
|
||||
async def search_offers(self,
|
||||
motsCles: Optional[str] = None,
|
||||
typeContrat: Optional[str] = None,
|
||||
codePostal: Optional[str] = None,
|
||||
commune: Optional[str] = None,
|
||||
distance: Optional[int] = None,
|
||||
alternance: Optional[bool] = None,
|
||||
offresManagerees: Optional[bool] = None,
|
||||
range: str = "0-14") -> FranceTravailSearchResponse:
|
||||
token = await self._get_access_token()
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}"
|
||||
}
|
||||
|
||||
params = {
|
||||
"range": range,
|
||||
params: Dict[str, Any] = {
|
||||
"range": f"{range_start}-{range_end}"
|
||||
}
|
||||
|
||||
if motsCles:
|
||||
params["motsCles"] = motsCles
|
||||
if typeContrat:
|
||||
params["typeContrat"] = typeContrat
|
||||
if alternance is not None:
|
||||
params["alternance"] = str(alternance).lower()
|
||||
if offresManagerees is not None:
|
||||
params["offresManagerees"] = str(offresManagerees).lower()
|
||||
params["typeContrat"] = ','.join(typeContrat)
|
||||
if experience:
|
||||
params["experience"] = ','.join(experience)
|
||||
|
||||
if codePostal:
|
||||
# Logique de localisation améliorée
|
||||
# insee_code = None # Non utilisé directement comme paramètre pour l'API France Travail
|
||||
# postal_code_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
|
||||
# latitude_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
|
||||
# longitude_for_api = None # Non utilisé directement comme paramètre pour l'API France Travail
|
||||
|
||||
# Le codeDepartement sera déterminé ici si une commune est fournie
|
||||
derived_departement_code = None
|
||||
|
||||
if commune:
|
||||
logger.info(f"Traitement de la commune spécifiée: '{commune}' pour dériver le département.")
|
||||
geo_data = await self.get_insee_and_postal_code_for_commune(commune)
|
||||
if geo_data:
|
||||
insee_code, postal_code_from_geo, latitude_from_geo, longitude_from_geo = geo_data
|
||||
|
||||
# Dériver le code départemental du code postal ou INSEE
|
||||
if postal_code_from_geo and len(postal_code_from_geo) >= 2:
|
||||
derived_departement_code = postal_code_from_geo[:2]
|
||||
# Cas spécifiques pour la Corse
|
||||
if postal_code_from_geo.startswith('2A'):
|
||||
derived_departement_code = '2A'
|
||||
elif postal_code_from_geo.startswith('2B'):
|
||||
derived_departement_code = '2B'
|
||||
|
||||
logger.info(f"Département dérivé de '{commune}': {derived_departement_code}")
|
||||
|
||||
# Si un département est dérivé, l'utiliser prioritairement
|
||||
if derived_departement_code:
|
||||
params["departement"] = derived_departement_code
|
||||
logger.info(f"Paramètre 'departement' utilisé (dérivé de la commune): {derived_departement_code}")
|
||||
elif latitude_from_geo and longitude_from_geo and distance is not None:
|
||||
# Fallback sur latitude/longitude si la dérivation du département échoue
|
||||
params["latitude"] = latitude_from_geo
|
||||
params["longitude"] = longitude_from_geo
|
||||
params["distance"] = distance
|
||||
logger.info(f"Paramètres de localisation utilisés (dérivés de la commune): Latitude/Longitude et Distance: Lat={latitude_from_geo}, Long={longitude_from_geo}, Dist={distance}")
|
||||
else:
|
||||
logger.warning(f"Impossible de dériver le département ou d'obtenir des coordonnées valides pour la commune '{commune}'. Recherche sans localisation précise.")
|
||||
else:
|
||||
logger.warning(f"Impossible d'obtenir les données géographiques pour la commune '{commune}'. Recherche sans localisation précise.")
|
||||
elif codePostal:
|
||||
params["codePostal"] = codePostal
|
||||
if distance is not None:
|
||||
params["distance"] = distance
|
||||
else:
|
||||
params["distance"] = 10
|
||||
elif commune:
|
||||
params["commune"] = commune
|
||||
logger.info(f"Paramètres de localisation utilisés: Code Postal={codePostal}, Distance={distance}")
|
||||
elif latitude is not None and longitude is not None:
|
||||
params["latitude"] = latitude
|
||||
params["longitude"] = longitude
|
||||
if distance is not None:
|
||||
params["distance"] = distance
|
||||
logger.info(f"Paramètres de localisation utilisés: Latitude/Longitude: Lat={latitude}, Long={longitude}, Dist={distance}")
|
||||
else:
|
||||
logger.warning("Aucun paramètre de localisation valide (commune, code postal, lat/long) n'a été spécifié. La recherche sera nationale.")
|
||||
|
||||
|
||||
logger.info(f"Appel à l'API France Travail pour search_offers avec paramètres FINAUX: {params}")
|
||||
try:
|
||||
response = await self.client.get("/v2/offres/search", headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Réponse brute de l'API France Travail (search_offers): {response.json()}")
|
||||
return FranceTravailSearchResponse(**response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Échec de la recherche d'offres France Travail: {e.response.status_code} - {e.response.text}")
|
||||
if e.response.status_code == 400 and "incorrect value" in e.response.text.lower() and "commune" in e.response.text.lower():
|
||||
raise RuntimeError(f"L'API France Travail a renvoyé une erreur 400: La valeur du paramètre 'commune' est incorrecte. Veuillez vérifier le code INSEE ou envisager une recherche par département ou latitude/longitude.")
|
||||
else:
|
||||
params["distance"] = 10
|
||||
|
||||
logger.info(f"Paramètres de recherche France Travail: {params}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(f"{self.api_base_url}/v2/offres/search", headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
return FranceTravailSearchResponse(**response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de la recherche d'offres France Travail: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Échec de la recherche d'offres France Travail: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de la recherche d'offres France Travail: {e}")
|
||||
raise RuntimeError(f"Échec inattendu lors de la recherche d'offres France Travail: {e}")
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Erreur réseau lors de la recherche d'offres France Travail: {e}")
|
||||
raise RuntimeError(f"Erreur réseau lors de la recherche d'offres France Travail: {e}")
|
||||
|
||||
async def get_offer_details(self, offer_id: str) -> OffreDetail:
|
||||
token = await self._get_access_token()
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}"
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(f"{self.api_base_url}/v2/offres/{offer_id}", headers=headers)
|
||||
response.raise_for_status()
|
||||
return OffreDetail(**response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de la récupération des détails de l'offre {offer_id}: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de la récupération des détails de l'offre {offer_id}: {e}")
|
||||
raise RuntimeError(f"Échec inattendu lors de la récupération des détails de l'offre {offer_id}: {e}")
|
||||
logger.info(f"Appel à l'API France Travail pour get_offer_details avec id: {offer_id}")
|
||||
try:
|
||||
response = await self.client.get(f"/v2/offres/{offer_id}", headers=headers)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Réponse brute de l'API France Travail (get_offer_details): {response.json()}")
|
||||
return OffreDetail(**response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Erreur réseau lors de la récupération des détails de l'offre {offer_id}: {e}")
|
||||
raise RuntimeError(f"Erreur réseau lors de la récupération des détails de l'offre {offer_id}: {e}")
|
||||
|
||||
france_travail_offer_service = FranceTravailOfferService()
|
|
@ -1,81 +1,62 @@
|
|||
import httpx
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from core.config import settings
|
||||
from services.oauth_service import OAuthService # Assurez-vous que ce service existe
|
||||
from services.france_travail_auth_service import france_travail_auth_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RomeoService:
|
||||
def __init__(self):
|
||||
self.base_url = settings.FRANCE_TRAVAIL_ROMEO_API_URL # URL de base de l'API Romeo
|
||||
self.scope = "api_romeov2" # Scope spécifique pour Romeo
|
||||
self.oauth_service = OAuthService(settings.FRANCE_TRAVAIL_OAUTH_URL, settings.FRANCE_TRAVAIL_CLIENT_ID, settings.FRANCE_TRAVAIL_CLIENT_SECRET)
|
||||
self.client = httpx.AsyncClient()
|
||||
# CORRIGÉ ICI: Utilise 'FRANCE_TRAVAIL_ROMEO_API_URL' comme suggéré par l'erreur
|
||||
self.base_url = settings.FRANCE_TRAVAIL_ROMEO_API_URL
|
||||
self._http_client = httpx.AsyncClient()
|
||||
logger.info(f"RomeoService initialized with base_url: {self.base_url}")
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
"""Récupère le token d'accès spécifique à l'API Romeo."""
|
||||
try:
|
||||
token_response = await self.oauth_service.get_access_token(self.scope)
|
||||
return token_response.access_token
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la récupération du token d'accès pour Romeo: {e}")
|
||||
raise RuntimeError(f"Impossible de récupérer le token d'accès pour Romeo: {e}")
|
||||
|
||||
async def _call_api(self, endpoint: str, text: str) -> Optional[Dict[str, Any]]:
|
||||
async def _call_api(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Appelle un endpoint de l'API Romeo avec le texte donné.
|
||||
Gère l'authentification et les erreurs de base.
|
||||
Appel générique à une endpoint de l'API Romeo.
|
||||
Récupère le jeton d'accès via le service d'authentification.
|
||||
"""
|
||||
token = await self._get_access_token()
|
||||
access_token = await france_travail_auth_service.get_access_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
# Les APIs Romeo attendent le texte dans un champ 'texte' de l'objet JSON
|
||||
data = {"texte": text}
|
||||
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"Calling Romeo API: {url} with text length {len(text)}")
|
||||
|
||||
logger.info(f"Appel API Romeo: {url} avec données: {data.keys()}")
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, headers=headers, json=data, timeout=30.0)
|
||||
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx ou 5xx)
|
||||
response = await self._http_client.post(url, json=data, headers=headers)
|
||||
response.raise_for_status() # Lève une exception pour les codes d'état HTTP 4xx/5xx
|
||||
logger.info(f"Réponse API Romeo reçue (status: {response.status_code}).")
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Erreur HTTP lors de l'appel à Romeo {endpoint}: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Erreur de l'API Romeo: {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Erreur réseau ou de requête lors de l'appel à Romeo {endpoint}: {e}")
|
||||
raise RuntimeError(f"Erreur de communication avec l'API Romeo: {e}")
|
||||
logger.error(f"Erreur HTTP lors de l'appel à l'API Romeo: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Erreur lors de l'appel à l'API Romeo: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Une erreur inattendue est survenue lors de l'appel à Romeo {endpoint}: {e}")
|
||||
raise RuntimeError(f"Erreur inattendue avec l'API Romeo: {e}")
|
||||
logger.error(f"Erreur inattendue lors de l'appel à l'API Romeo: {e}")
|
||||
raise RuntimeError(f"Erreur inattendue lors de l'appel à l'API Romeo: {e}")
|
||||
|
||||
async def predict_metiers(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Prédit les métiers ROME à partir d'un texte donné.
|
||||
Retourne une liste de dictionnaires avec les détails des prédictions métiers.
|
||||
"""
|
||||
if not text:
|
||||
return [] # Retourne une liste vide si le texte est vide
|
||||
response_data = await self._call_api("/predictionMetiers", text)
|
||||
# Romeo renvoie souvent une liste directe de prédictions si successful
|
||||
return response_data if response_data is not None else []
|
||||
|
||||
endpoint = "/predire/metiers"
|
||||
data = {"texte": text}
|
||||
response_data = await self._call_api(endpoint, data)
|
||||
return response_data.get("predictions", [])
|
||||
|
||||
async def predict_competences(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Prédit les compétences ROME à partir d'un texte donné.
|
||||
Retourne une liste de dictionnaires avec les détails des prédictions de compétences.
|
||||
"""
|
||||
if not text:
|
||||
return [] # Retourne une liste vide si le texte est vide
|
||||
response_data = await self._call_api("/predictionCompetences", text)
|
||||
# Romeo renvoie souvent une liste directe de prédictions si successful
|
||||
return response_data if response_data is not None else []
|
||||
endpoint = "/predire/competences"
|
||||
data = {"texte": text}
|
||||
response_data = await self._call_api(endpoint, data)
|
||||
return response_data.get("predictions", [])
|
||||
|
||||
# Instanciation unique du service Romeo
|
||||
romeo_service = RomeoService()
|
Loading…
Add table
Add a link
Reference in a new issue