mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Fix: remonter les erreurs Ollama au lieu de les avaler (agent muet)
Ollama signale certains échecs en cours de génération (OOM, contexte trop
grand, débordement sur CPU) via une ligne JSON {"error": ...} avec un statut
HTTP 200. La boucle de streaming ne lisait que message.content / tool_calls /
done : ces lignes étaient ignorées, le flux se terminait avec un contenu vide,
et l'UI n'affichait rien — l'agent semblait "ne pas répondre".
- ollama_client: OllamaError + détection des chunks {"error": ...} (chat & pull),
corps de réponse remonté sur erreur HTTP, timeout de connexion court
(échec rapide si Ollama injoignable) avec lecture sans limite, et parsing
JSON tolérant aux lignes partielles.
- agent: capture d'OllamaError -> événement "error" affiché à l'utilisateur.
- docker-compose: config GPU NVIDIA pour le service Ollama embarqué (sinon CPU).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
52f67e9344
commit
d47f7476ed
3 files changed
+78
-9
No files matched your search
@@ -18,7 +18,7 @@ from typing import AsyncIterator
|
||||
|
||||
import httpx
|
||||
|
||||
from .ollama_client import ollama
|
||||
from .ollama_client import OllamaError, ollama
|
||||
from .tools import TOOL_DEFINITIONS, ToolError, run_tool
|
||||
|
||||
MAX_ITERATIONS = 6
|
||||
@@ -156,6 +156,11 @@ async def run_agent(
|
||||
if final_chunk.strip():
|
||||
text_parts.append(final_chunk.strip())
|
||||
break
|
||||
except OllamaError as exc:
|
||||
# Échec signalé par Ollama (HTTP ou ligne d'erreur dans le flux) :
|
||||
# souvent un débordement mémoire / contexte trop grand. On l'expose.
|
||||
yield {"type": "error", "message": f"Ollama : {exc}"}
|
||||
return
|
||||
except (httpx.HTTPError, OSError) as exc:
|
||||
yield {"type": "error", "message": str(exc)}
|
||||
return
|
||||
|
||||
@@ -13,6 +13,38 @@ import httpx
|
||||
from .config import settings
|
||||
|
||||
|
||||
class OllamaError(RuntimeError):
|
||||
"""Erreur renvoyée par Ollama (statut HTTP ≥ 400 ou champ ``error`` dans le flux).
|
||||
|
||||
Ollama signale certains échecs *au milieu* d'un flux streaming (HTTP 200)
|
||||
via une ligne JSON ``{"error": "..."}`` — typiquement un débordement mémoire
|
||||
ou un contexte trop grand. On lève alors cette exception pour que l'appelant
|
||||
la remonte à l'utilisateur au lieu de l'avaler silencieusement.
|
||||
"""
|
||||
|
||||
|
||||
# Connexion rapide à échouer si Ollama est injoignable, mais lecture sans limite :
|
||||
# une génération longue (ou un chargement de modèle sur CPU) ne doit pas couper.
|
||||
_STREAM_TIMEOUT = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=10.0)
|
||||
|
||||
|
||||
async def _raise_for_stream_status(resp: httpx.Response) -> None:
|
||||
"""Lève une ``OllamaError`` détaillée si la réponse streaming est en erreur.
|
||||
|
||||
Sur une réponse en flux, ``raise_for_status`` n'inclut pas le corps ; on le
|
||||
lit explicitement pour exposer le message d'Ollama (modèle absent, etc.).
|
||||
"""
|
||||
if resp.status_code < 400:
|
||||
return
|
||||
body = await resp.aread()
|
||||
detail = body.decode(errors="replace").strip()
|
||||
try:
|
||||
detail = json.loads(detail).get("error", detail)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
raise OllamaError(f"Ollama a renvoyé {resp.status_code} : {detail[:500]}")
|
||||
|
||||
|
||||
class OllamaClient:
|
||||
"""Enveloppe asynchrone autour de l'API REST d'Ollama."""
|
||||
|
||||
@@ -49,14 +81,23 @@ class OllamaClient:
|
||||
|
||||
async def pull_model(self, name: str) -> AsyncIterator[dict]:
|
||||
"""Télécharge un modèle en streamant la progression (/api/pull)."""
|
||||
async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=_STREAM_TIMEOUT, follow_redirects=True
|
||||
) as client:
|
||||
async with client.stream(
|
||||
"POST", f"{self.host}/api/pull", json={"name": name}
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
await _raise_for_stream_status(resp)
|
||||
async for line in resp.aiter_lines():
|
||||
if line.strip():
|
||||
yield json.loads(line)
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(chunk, dict) and chunk.get("error"):
|
||||
raise OllamaError(str(chunk["error"]))
|
||||
yield chunk
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
@@ -74,14 +115,27 @@ class OllamaClient:
|
||||
if options:
|
||||
payload["options"] = options
|
||||
|
||||
async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=_STREAM_TIMEOUT, follow_redirects=True
|
||||
) as client:
|
||||
async with client.stream(
|
||||
"POST", f"{self.host}/api/chat", json=payload
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
await _raise_for_stream_status(resp)
|
||||
async for line in resp.aiter_lines():
|
||||
if line.strip():
|
||||
yield json.loads(line)
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
# Ligne partielle / non-JSON : on l'ignore plutôt que de
|
||||
# faire planter tout le flux.
|
||||
continue
|
||||
# Échec en cours de génération (OOM, contexte trop grand…) :
|
||||
# Ollama l'émet dans le flux avec HTTP 200. On le remonte.
|
||||
if isinstance(chunk, dict) and chunk.get("error"):
|
||||
raise OllamaError(str(chunk["error"]))
|
||||
yield chunk
|
||||
|
||||
|
||||
ollama = OllamaClient()
|
||||
@@ -34,6 +34,16 @@ services:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
# Accès GPU NVIDIA : sans ceci, l'Ollama embarqué tourne en CPU (très lent).
|
||||
# Nécessite le NVIDIA Container Toolkit installé sur l'hôte. Si tu n'as pas
|
||||
# de GPU NVIDIA, commente tout le bloc `deploy:` ci-dessous.
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: ["gpu"]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
Reference in new issue
Block a user