mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 19:29:16 +02:00
Certains modèles « thinking » ne renvoyaient que du raisonnement, sans reponse finale -> erreur. Nouvel interrupteur global `think` (Reglages > Generation) : - think=false envoie `think:false` a Ollama pour desactiver le raisonnement - repli automatique si le modele refuse le parametre (does not support thinking) - message d'erreur mis a jour pour pointer vers l'interrupteur Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
"""Route de conversation agentique : boucle d'outils + streaming SSE.
|
|
|
|
Flux :
|
|
1. on enregistre le message utilisateur ;
|
|
2. on lance la boucle agentique (run_agent) sur l'historique de la session ;
|
|
3. on relaie tokens et événements d'outils au client (SSE) ;
|
|
4. on enregistre la réponse finale de l'assistant + le récap des outils.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from contextlib import suppress
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from .. import agent_config, db
|
|
from ..agent import run_agent
|
|
from ..config import settings
|
|
|
|
router = APIRouter(prefix="/api", tags=["chat"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
session_id: str
|
|
content: str
|
|
model: str | None = None
|
|
|
|
|
|
def _sse(event: str, data: dict) -> str:
|
|
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
|
|
|
|
|
@router.post("/chat")
|
|
async def chat(req: ChatRequest) -> StreamingResponse:
|
|
session = db.get_session(req.session_id)
|
|
if not session:
|
|
raise HTTPException(404, "session introuvable")
|
|
|
|
model = req.model or session.get("model") or settings.default_model
|
|
cfg = agent_config.get_config(model)
|
|
|
|
# Premier message : titre la session avec un extrait.
|
|
if not db.list_messages(req.session_id):
|
|
title = req.content.strip().split("\n")[0][:60] or "Nouvelle session"
|
|
db.rename_session(req.session_id, title)
|
|
|
|
db.add_message(req.session_id, "user", req.content, None)
|
|
|
|
convo = [{"role": "system", "content": cfg["system_prompt"]}]
|
|
convo += db.list_messages_for_model(req.session_id)
|
|
|
|
async def event_stream():
|
|
yield _sse("start", {"model": model})
|
|
final_content = ""
|
|
tools_meta: list[dict] = []
|
|
stats_meta: dict | None = None
|
|
thinking_meta = ""
|
|
error_message = ""
|
|
|
|
queue: asyncio.Queue[dict | None] = asyncio.Queue()
|
|
|
|
async def produce_events() -> None:
|
|
try:
|
|
async for event 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),
|
|
think=cfg.get("think", True),
|
|
):
|
|
await queue.put(event)
|
|
except Exception as exc:
|
|
logger.exception("Échec inattendu du flux de chat")
|
|
await queue.put(
|
|
{
|
|
"type": "error",
|
|
"message": f"Erreur interne du chat : {exc}",
|
|
}
|
|
)
|
|
finally:
|
|
await queue.put(None)
|
|
|
|
producer = asyncio.create_task(produce_events())
|
|
try:
|
|
while True:
|
|
try:
|
|
ev = await asyncio.wait_for(queue.get(), timeout=15.0)
|
|
except TimeoutError:
|
|
# Empêche OpenResty/Nginx/Cloudflare de fermer le SSE pendant
|
|
# le chargement parfois long d'un modèle Ollama.
|
|
yield _sse("ping", {"status": "waiting"})
|
|
continue
|
|
|
|
if ev is None:
|
|
break
|
|
|
|
etype = ev.pop("type")
|
|
if etype in (
|
|
"token",
|
|
"thinking",
|
|
"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"]
|
|
stats_meta = ev.get("stats")
|
|
thinking_meta = ev.get("thinking", "")
|
|
finally:
|
|
if not producer.done():
|
|
producer.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await producer
|
|
|
|
if final_content or tools_meta:
|
|
meta: dict = {}
|
|
if tools_meta:
|
|
meta["tools"] = tools_meta
|
|
if stats_meta:
|
|
meta["stats"] = stats_meta
|
|
if thinking_meta:
|
|
meta["thinking"] = thinking_meta
|
|
db.add_message(
|
|
req.session_id,
|
|
"assistant",
|
|
final_content,
|
|
model,
|
|
meta=meta or None,
|
|
)
|
|
yield _sse(
|
|
"done",
|
|
{
|
|
"content": final_content,
|
|
"tools": tools_meta,
|
|
"stats": stats_meta,
|
|
"error": error_message or None,
|
|
},
|
|
)
|
|
|
|
return StreamingResponse(
|
|
event_stream(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache, no-transform",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|