mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
Phase 4 : boucle agentique, outils fichiers et aperçu live
Backend : - tools.py : read_file / write_file / list_dir confinés au workspace (anti-traversal) - agent.py : boucle de tool-calling itérative au-dessus d'Ollama, génère des événements (token, tool_call, tool_result, final) - routes/chat.py : pilote run_agent, relaie les événements en SSE, persiste la réponse finale + le récap des outils (meta) - routes/files.py : arborescence et contenu du workspace (confiné) - db.py : colonne meta sur messages (+ migration douce), list_messages_for_model Frontend : - ToolCard : carte d'appel d'outil dans le fil (statut en cours/terminé/échec) - streamChat : événements tool_call / tool_result - store : streamTools, fileTree, preview (ouverture auto d'un HTML écrit) - LeftPanel : arborescence réelle du workspace, clic -> aperçu - PreviewPanel : rendu HTML en iframe + onglet Code - ChatPanel : rendu des outils dans les bulles agent (streaming + persisté) Tests manuels : outils + confinement, boucle agent (mock Ollama), chat HTTP de bout en bout (SSE, persistance meta, fichiers écrits, confinement route). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
48cbcaafaf
commit
b6bf4edf97
13 files changed
+677
-80
No files matched your search
@@ -0,0 +1,113 @@
|
||||
"""Boucle agentique : tool-calling itératif au-dessus d'Ollama.
|
||||
|
||||
L'agent dialogue avec le modèle ; quand celui-ci demande un outil, on l'exécute,
|
||||
on réinjecte le résultat, et on reboucle jusqu'à une réponse finale (ou la
|
||||
limite d'itérations). La fonction est un générateur asynchrone d'événements
|
||||
relayés tels quels au client via SSE.
|
||||
|
||||
Événements émis :
|
||||
token {content} — fragment de texte de l'agent
|
||||
tool_call {name, args} — début d'exécution d'un outil
|
||||
tool_result {name, args, summary, status}
|
||||
final {content, tools} — réponse complète + récap des outils
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import AsyncIterator
|
||||
|
||||
import httpx
|
||||
|
||||
from .ollama_client import ollama
|
||||
from .tools import TOOL_DEFINITIONS, ToolError, run_tool
|
||||
|
||||
MAX_ITERATIONS = 6
|
||||
|
||||
|
||||
def _parse_args(raw) -> dict:
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
async def run_agent(
|
||||
model: str,
|
||||
convo: list[dict],
|
||||
*,
|
||||
options: dict | None = None,
|
||||
tools_enabled: bool = True,
|
||||
) -> AsyncIterator[dict]:
|
||||
tools = TOOL_DEFINITIONS if tools_enabled else None
|
||||
collected: list[dict] = []
|
||||
text_parts: list[str] = []
|
||||
|
||||
try:
|
||||
for _ in range(MAX_ITERATIONS):
|
||||
content_buf = ""
|
||||
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"):
|
||||
break
|
||||
|
||||
assistant_turn: dict = {"role": "assistant", "content": content_buf}
|
||||
if tool_calls:
|
||||
assistant_turn["tool_calls"] = tool_calls
|
||||
convo.append(assistant_turn)
|
||||
if content_buf.strip():
|
||||
text_parts.append(content_buf.strip())
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
|
||||
# Exécution des outils demandés, puis réinjection des résultats.
|
||||
for tc in tool_calls:
|
||||
fn = tc.get("function", {})
|
||||
name = fn.get("name", "")
|
||||
args = _parse_args(fn.get("arguments"))
|
||||
|
||||
yield {"type": "tool_call", "name": name, "args": args}
|
||||
|
||||
try:
|
||||
result = run_tool(name, args)
|
||||
summary = result.get("summary", "terminé")
|
||||
status = "ok"
|
||||
except ToolError as exc:
|
||||
result = {"ok": False, "error": str(exc)}
|
||||
summary = str(exc)
|
||||
status = "error"
|
||||
|
||||
record = {"name": name, "args": args, "summary": summary, "status": status}
|
||||
collected.append(record)
|
||||
yield {"type": "tool_result", **record}
|
||||
|
||||
convo.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"name": name,
|
||||
"content": json.dumps(result, ensure_ascii=False),
|
||||
}
|
||||
)
|
||||
except (httpx.HTTPError, OSError) as exc:
|
||||
yield {"type": "error", "message": str(exc)}
|
||||
return
|
||||
|
||||
yield {
|
||||
"type": "final",
|
||||
"content": "\n\n".join(text_parts).strip(),
|
||||
"tools": collected,
|
||||
}
|
||||
Reference in new issue
Block a user