"""Client Ollama partagé — appel LLM en mode JSON natif.""" from __future__ import annotations import json import logging import requests from ..config import OLLAMA_URL, OLLAMA_MODEL, OLLAMA_TIMEOUT logger = logging.getLogger(__name__) def parse_json_response(raw: str) -> dict | None: """Parse une réponse JSON d'Ollama, en gérant les blocs markdown.""" text = raw.strip() if text.startswith("```"): first_nl = text.find("\n") if first_nl != -1: text = text[first_nl + 1:] if text.rstrip().endswith("```"): text = text.rstrip()[:-3] text = text.strip() try: return json.loads(text) except json.JSONDecodeError: logger.warning("Ollama : JSON invalide : %s", raw[:200]) return None def call_ollama( prompt: str, temperature: float = 0.1, max_tokens: int = 2500, ) -> dict | None: """Appelle Ollama en mode JSON natif avec retry. Args: prompt: Le prompt à envoyer. temperature: Température de génération (défaut: 0.1). max_tokens: Nombre max de tokens (défaut: 2500). Returns: Le dict JSON parsé, ou None en cas d'erreur. """ for attempt in range(2): try: response = requests.post( f"{OLLAMA_URL}/api/generate", json={ "model": OLLAMA_MODEL, "prompt": prompt, "stream": False, "format": "json", "options": { "temperature": temperature, "num_predict": max_tokens, }, }, timeout=OLLAMA_TIMEOUT, ) response.raise_for_status() raw = response.json().get("response", "") result = parse_json_response(raw) if result is not None: return result if attempt == 0: logger.info("Ollama : retry après échec de parsing") except requests.ConnectionError: logger.warning("Ollama non disponible (connexion refusée)") return None except requests.Timeout: logger.warning("Ollama timeout après %ds", OLLAMA_TIMEOUT) return None except (requests.RequestException, json.JSONDecodeError) as e: logger.warning("Ollama erreur : %s", e) return None return None