Synthèse optionnelle via SDK openai (client injectable, prompt système FR, max 800 car., timeout 30 s, temp 0.3). Mode dégradé strict : generate() ne lève jamais, retourne None si clé absente/timeout/erreur. Provider litellm optionnel (extra ai-litellm) réutilisant le prompt OpenAI. Factory get_synthesis_provider() selon AISettings. 23 tests sans réseau, couverture synthesis/ 93%. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
103 lines
4.0 KiB
Python
103 lines
4.0 KiB
Python
"""Fournisseur de synthèse IA via ``litellm``.
|
|
|
|
Ce module définit :class:`LiteLLMSynthesisProvider`, un fournisseur de
|
|
synthèse IA qui délègue l'appel à ``litellm.completion`` en réutilisant le
|
|
prompt système et la construction de prompt de
|
|
:class:`~pronote_sync.synthesis.openai.OpenAISynthesisProvider`. La méthode
|
|
:meth:`LiteLLMSynthesisProvider.generate` ne lève jamais d'exception : tout
|
|
échec est journalisé (message rédigé) et dégradé en retour ``None``.
|
|
|
|
Ce module nécessite l'extra ``ai-litellm`` (le paquet ``litellm``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import litellm
|
|
|
|
from pronote_sync.models.synthesis import SynthesisInput, SynthesisResult
|
|
from pronote_sync.synthesis.openai import OpenAISynthesisProvider
|
|
from pronote_sync.utils.redaction import redact_secrets
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
__all__ = ["LiteLLMSynthesisProvider"]
|
|
|
|
|
|
class LiteLLMSynthesisProvider:
|
|
"""Fournisseur de synthèse IA utilisant ``litellm``.
|
|
|
|
Réutilise le prompt système et la construction de prompt de
|
|
:class:`OpenAISynthesisProvider`. Ne lève jamais d'exception : en cas
|
|
d'échec, :meth:`generate` retourne ``None``.
|
|
"""
|
|
|
|
SYSTEM_PROMPT = OpenAISynthesisProvider.SYSTEM_PROMPT
|
|
MAX_LENGTH = OpenAISynthesisProvider.MAX_LENGTH
|
|
TIMEOUT = OpenAISynthesisProvider.TIMEOUT
|
|
TEMPERATURE = OpenAISynthesisProvider.TEMPERATURE
|
|
|
|
def __init__(
|
|
self, api_key: str, base_url: str | None = None, model: str = "gpt-4o-mini"
|
|
) -> None:
|
|
"""Initialise le fournisseur LiteLLM.
|
|
|
|
:param api_key: Clé API du fournisseur.
|
|
:param base_url: URL de base de l'API (``None`` pour l'URL par défaut).
|
|
:param model: Identifiant du modèle.
|
|
"""
|
|
self._api_key = api_key
|
|
self._base_url = base_url
|
|
self._model = model
|
|
|
|
def generate(self, input_data: SynthesisInput) -> SynthesisResult | None:
|
|
"""Génère une synthèse IA à partir des données d'entrée.
|
|
|
|
Construit le prompt via ``OpenAISynthesisProvider._build_prompt``,
|
|
appelle ``litellm.completion`` en transmettant explicitement
|
|
``api_key`` et ``base_url`` (uniquement si non ``None``) ainsi que
|
|
``timeout``, puis nettoie la réponse (troncature à
|
|
:attr:`MAX_LENGTH`, suppression des sauts de ligne en début et fin).
|
|
Ne lève jamais d'exception : toute erreur est journalisée (message
|
|
rédigé) et dégradée en retour ``None``.
|
|
|
|
:param input_data: Données de synthèse (diff agenda, messages, événements).
|
|
:return: Résultat de la synthèse, ou ``None`` en cas d'échec ou de
|
|
réponse vide.
|
|
:rtype: SynthesisResult | None
|
|
"""
|
|
try:
|
|
completion_kwargs: dict[str, Any] = {
|
|
"model": self._model,
|
|
"messages": [
|
|
{"role": "system", "content": self.SYSTEM_PROMPT},
|
|
{
|
|
"role": "user",
|
|
"content": OpenAISynthesisProvider._build_prompt(input_data),
|
|
},
|
|
],
|
|
"max_tokens": self.MAX_LENGTH,
|
|
"temperature": self.TEMPERATURE,
|
|
"timeout": self.TIMEOUT,
|
|
}
|
|
if self._api_key is not None:
|
|
completion_kwargs["api_key"] = self._api_key
|
|
if self._base_url is not None:
|
|
completion_kwargs["base_url"] = self._base_url
|
|
response = litellm.completion(**completion_kwargs)
|
|
content = response.choices[0].message.content
|
|
if not content:
|
|
return None
|
|
synthesis_text = content[: self.MAX_LENGTH].strip()
|
|
if not synthesis_text:
|
|
return None
|
|
return SynthesisResult(text=synthesis_text)
|
|
except Exception as e:
|
|
logger.error(
|
|
"Échec de la génération de la synthèse IA (litellm) : %s",
|
|
redact_secrets(str(e)),
|
|
)
|
|
return None
|