diff --git a/src/ai_typewriter/ai_client.py b/src/ai_typewriter/ai_client.py new file mode 100644 index 0000000..2783fd2 --- /dev/null +++ b/src/ai_typewriter/ai_client.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import requests + +from .config import AppConfig + + +class AIClientError(RuntimeError): + """Raised when the configured AI backend cannot return text.""" + + +def ask_ai(prompt: str, config: AppConfig) -> str: + if not prompt.strip(): + raise AIClientError("Le texte capturé est vide.") + + if config.provider == "ollama": + return _ask_ollama(prompt, config) + if config.provider == "gemini": + return _ask_gemini(prompt, config) + raise AIClientError(f"Provider non supporté: {config.provider}") + + +def _ask_ollama(prompt: str, config: AppConfig) -> str: + url = f"{config.server_url}/api/chat" + payload = { + "model": config.model, + "stream": False, + "messages": [ + {"role": "system", "content": config.system_prompt}, + {"role": "user", "content": prompt}, + ], + } + try: + response = requests.post(url, json=payload, timeout=config.request_timeout_seconds) + response.raise_for_status() + data = response.json() + except requests.RequestException as exc: + raise AIClientError(f"Erreur Ollama: {exc}") from exc + except ValueError as exc: + raise AIClientError("Réponse Ollama invalide: JSON illisible") from exc + + content = data.get("message", {}).get("content") + if not isinstance(content, str) or not content.strip(): + raise AIClientError("Réponse Ollama vide ou inattendue") + return content.strip() + + +def _ask_gemini(prompt: str, config: AppConfig) -> str: + if not config.api_key: + raise AIClientError("api_key est obligatoire pour provider='gemini'.") + + base = config.server_url or "https://generativelanguage.googleapis.com" + url = f"{base}/v1beta/models/{config.model}:generateContent" + payload = { + "systemInstruction": {"parts": [{"text": config.system_prompt}]}, + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": {"temperature": 0.2}, + } + try: + response = requests.post( + url, + params={"key": config.api_key}, + json=payload, + timeout=config.request_timeout_seconds, + ) + response.raise_for_status() + data = response.json() + except requests.RequestException as exc: + raise AIClientError(f"Erreur Gemini: {exc}") from exc + except ValueError as exc: + raise AIClientError("Réponse Gemini invalide: JSON illisible") from exc + + try: + text = data["candidates"][0]["content"]["parts"][0]["text"] + except (KeyError, IndexError, TypeError) as exc: + raise AIClientError("Réponse Gemini vide ou inattendue") from exc + if not isinstance(text, str) or not text.strip(): + raise AIClientError("Réponse Gemini vide") + return text.strip()