Fix Ollama chat reliability

This commit is contained in:
Michael committed 2026-06-30 10:36:29 +02:00
1 parent d47f7476ed
commit e9c83e3a0d
5 files changed
+297 -78

No files matched your search

+71 -19
View File
@@ -24,6 +24,12 @@ from .tools import TOOL_DEFINITIONS, ToolError, run_tool
MAX_ITERATIONS = 6
def _tools_not_supported(exc: OllamaError) -> bool:
"""Détecte l'erreur Ollama renvoyée par un modèle sans function calling."""
message = str(exc).lower()
return "does not support tools" in message or "does not support tool" in message
def _parse_args(raw) -> dict:
if isinstance(raw, dict):
return raw
@@ -54,26 +60,62 @@ async def run_agent(
tools = None
collected: list[dict] = []
text_parts: list[str] = []
active_tools = tools
tool_fallback_used = False
try:
for _ in range(MAX_ITERATIONS):
content_buf = ""
thinking_buf = ""
thinking_status_sent = False
tool_calls: list[dict] = []
async for chunk in ollama.chat(
model, convo, tools=tools, options=options, stream=True
):
msg = chunk.get("message", {})
token = msg.get("content", "")
if token:
content_buf += token
yield {"type": "token", "content": token}
if msg.get("tool_calls"):
tool_calls.extend(msg["tool_calls"])
if chunk.get("done"):
# Un modèle peut savoir discuter sans supporter les outils. Ollama
# refuse alors la requête entière : on retente une fois en chat simple.
while True:
try:
async for chunk in ollama.chat(
model, convo, tools=active_tools, options=options, stream=True
):
msg = chunk.get("message", {})
token = msg.get("content", "")
thinking = msg.get("thinking", "")
if thinking:
thinking_buf += thinking
if not thinking_status_sent:
thinking_status_sent = True
yield {"type": "status", "message": "Réflexion…"}
if token:
content_buf += token
yield {"type": "token", "content": token}
if msg.get("tool_calls"):
tool_calls.extend(msg["tool_calls"])
if chunk.get("done"):
break
break
except OllamaError as exc:
if (
active_tools
and not tool_fallback_used
and not content_buf
and not tool_calls
and _tools_not_supported(exc)
):
active_tools = None
tool_fallback_used = True
yield {
"type": "notice",
"message": (
"Ce modèle ne supporte pas les outils ; "
"réponse en mode conversation simple."
),
}
continue
raise
assistant_turn: dict = {"role": "assistant", "content": content_buf}
if thinking_buf:
assistant_turn["thinking"] = thinking_buf
if tool_calls:
assistant_turn["tool_calls"] = tool_calls
convo.append(assistant_turn)
@@ -106,7 +148,7 @@ async def run_agent(
convo.append(
{
"role": "tool",
"name": name,
"tool_name": name,
"content": json.dumps(
{
"ok": False,
@@ -137,7 +179,7 @@ async def run_agent(
convo.append(
{
"role": "tool",
"name": name,
"tool_name": name,
"content": json.dumps(result, ensure_ascii=False),
}
)
@@ -162,11 +204,21 @@ async def run_agent(
yield {"type": "error", "message": f"Ollama : {exc}"}
return
except (httpx.HTTPError, OSError) as exc:
yield {"type": "error", "message": str(exc)}
yield {
"type": "error",
"message": f"Impossible de joindre Ollama ({ollama.host}) : {exc}",
}
return
yield {
"type": "final",
"content": "\n\n".join(text_parts).strip(),
"tools": collected,
}
final_content = "\n\n".join(text_parts).strip()
if not final_content and not collected:
yield {
"type": "error",
"message": (
"Le modèle a terminé sans renvoyer de texte. Essayez un modèle "
"de chat récent ou désactivez son mode de réflexion avancée."
),
}
return
yield {"type": "final", "content": final_content, "tools": collected}
+49 -15
View File
@@ -9,6 +9,7 @@ Flux :
from __future__ import annotations
import json
import logging
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
@@ -19,6 +20,7 @@ from ..agent import run_agent
from ..config import settings
router = APIRouter(prefix="/api", tags=["chat"])
logger = logging.getLogger(__name__)
class ChatRequest(BaseModel):
@@ -54,20 +56,38 @@ async def chat(req: ChatRequest) -> StreamingResponse:
yield _sse("start", {"model": model})
final_content = ""
tools_meta: list[dict] = []
error_message = ""
async for ev in run_agent(
model,
convo,
options=agent_config.ollama_options(cfg),
enabled_tools=agent_config.enabled_tool_names(cfg),
confirm_shell=cfg.get("confirm_shell", True),
):
etype = ev.pop("type")
if etype in ("token", "tool_call", "tool_result", "tool_confirm", "error"):
yield _sse(etype, ev)
elif etype == "final":
final_content = ev["content"]
tools_meta = ev["tools"]
try:
async for ev in run_agent(
model,
convo,
options=agent_config.ollama_options(cfg),
enabled_tools=agent_config.enabled_tool_names(cfg),
confirm_shell=cfg.get("confirm_shell", True),
):
etype = ev.pop("type")
if etype in (
"token",
"status",
"notice",
"tool_call",
"tool_result",
"tool_confirm",
):
yield _sse(etype, ev)
elif etype == "error":
error_message = ev.get("message", "Erreur Ollama inconnue")
yield _sse("error", {"message": error_message})
elif etype == "final":
final_content = ev["content"]
tools_meta = ev["tools"]
except Exception as exc:
# Après l'envoi des en-têtes SSE, une exception non gérée coupe le
# socket sans explication et laisse l'interface bloquée.
logger.exception("Échec inattendu du flux de chat")
error_message = f"Erreur interne du chat : {exc}"
yield _sse("error", {"message": error_message})
if final_content or tools_meta:
db.add_message(
@@ -77,6 +97,20 @@ async def chat(req: ChatRequest) -> StreamingResponse:
model,
meta={"tools": tools_meta} if tools_meta else None,
)
yield _sse("done", {"content": final_content, "tools": tools_meta})
yield _sse(
"done",
{
"content": final_content,
"tools": tools_meta,
"error": error_message or None,
},
)
return StreamingResponse(event_stream(), media_type="text/event-stream")
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
},
)