Files
TicketTracker/tickettracker/llm/client.py
laurent 1e5fc97bb7 feat: migration Windows → Ubuntu, stabilisation suite de tests
- Ajout venv Python (.venv) avec pip bootstrap (python3-venv absent)
- Correction OCR Linux : marqueur TTC/TVA tolère la confusion T↔I
  (Tesseract 5.3.4 Linux lit parfois "TIc" au lieu de "TTC")
- test_leclerc.py : skipif si Tesseract absent, xfail pour test de somme
  (précision OCR variable entre plateformes, solution LLM vision prévue)
- Résultat : 77 passent, 1 xfail, 0 échec (vs 78 sur Windows)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 18:53:41 +01:00

100 lines
3.0 KiB
Python

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