Files
TicketTracker/tickettracker/llm/client.py

100 lines
3.0 KiB
Python
Raw Normal View History

"""
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