""" Client HTTP bas niveau pour l'API LLM compatible OpenAI. Ce module ne contient qu'une seule fonction publique : call_llm(). Il ne connaît pas la logique de normalisation — c'est le rôle de normalizer.py. Exceptions levées : LLMUnavailable — serveur injoignable (timeout, connexion refusée) LLMError — réponse HTTP ≥ 400 ou format de réponse inattendu """ import logging import requests from tickettracker import config logger = logging.getLogger(__name__) class LLMUnavailable(Exception): """Le serveur LLM est injoignable (réseau, timeout).""" class LLMError(Exception): """L'API LLM a retourné une erreur (HTTP ≥ 400 ou réponse malformée).""" def call_llm( messages: list[dict], *, model: str | None = None, timeout: int | None = None, ) -> str: """Appelle l'API LLM et retourne le texte brut de la réponse. Args: messages: Liste de messages au format OpenAI [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}] model: Nom du modèle (défaut : config.LLM_MODEL) timeout: Timeout en secondes (défaut : config.LLM_TIMEOUT) Returns: Le texte du premier choix de la réponse. Raises: LLMUnavailable: Si le serveur est injoignable. LLMError: Si l'API retourne une erreur ou une réponse inattendue. """ _model = model or config.LLM_MODEL _timeout = timeout if timeout is not None else config.LLM_TIMEOUT if not config.LLM_API_KEY: raise LLMError( "Clé API LLM manquante. " "Définissez la variable d'environnement TICKETTRACKER_LLM_API_KEY." ) headers = { "Authorization": f"Bearer {config.LLM_API_KEY}", "Content-Type": "application/json", } payload = { "model": _model, "messages": messages, "temperature": 0.1, # faible variabilité : on veut un format stable } logger.debug("Appel LLM %s (model=%s, timeout=%ds)", config.LLM_URL, _model, _timeout) try: response = requests.post( config.LLM_URL, json=payload, headers=headers, timeout=_timeout, ) except requests.exceptions.Timeout: raise LLMUnavailable( f"Timeout après {_timeout}s lors de l'appel au LLM ({config.LLM_URL})." ) except requests.exceptions.ConnectionError as e: raise LLMUnavailable( f"Impossible de joindre le serveur LLM ({config.LLM_URL}) : {e}" ) if not response.ok: raise LLMError( f"Erreur API LLM : HTTP {response.status_code} — {response.text[:200]}" ) try: data = response.json() return data["choices"][0]["message"]["content"] except (KeyError, IndexError, ValueError) as e: raise LLMError( f"Réponse LLM inattendue (impossible d'extraire le contenu) : {e}\n" f"Réponse brute : {response.text[:300]}" ) from e