mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 20:57:05 +02:00
Fix Ollama chat reliability
This commit is contained in:
1 parent
d47f7476ed
commit
e9c83e3a0d
5 files changed
+297
-78
No files matched your search
+71
-19
@@ -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
@@ -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",
|
||||
},
|
||||
)
|
||||
Reference in new issue
Block a user