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