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,139 @@
|
||||
"""Outils de l'agent, exécutés côté serveur et confinés au workspace.
|
||||
|
||||
Chaque outil expose :
|
||||
- une définition JSON (format function-calling Ollama/OpenAI) ;
|
||||
- une implémentation Python qui renvoie un dict {ok, summary, ...}.
|
||||
|
||||
Toutes les opérations fichier sont strictement confinées à WORKSPACE_DIR :
|
||||
toute tentative de sortie (../, chemin absolu hors workspace) est rejetée.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from .config import settings
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Erreur d'exécution d'un outil (message destiné au modèle)."""
|
||||
|
||||
|
||||
def _workspace_root() -> str:
|
||||
root = os.path.abspath(settings.workspace_dir)
|
||||
os.makedirs(root, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
def _safe_path(rel: str) -> str:
|
||||
"""Résout un chemin relatif en restant confiné au workspace."""
|
||||
root = _workspace_root()
|
||||
target = os.path.abspath(os.path.join(root, rel or "."))
|
||||
if target != root and not target.startswith(root + os.sep):
|
||||
raise ToolError(f"chemin hors du workspace refusé : {rel}")
|
||||
return target
|
||||
|
||||
|
||||
# ── Implémentations ──────────────────────────────────────────────────────
|
||||
def read_file(path: str) -> dict:
|
||||
target = _safe_path(path)
|
||||
if not os.path.isfile(target):
|
||||
raise ToolError(f"fichier introuvable : {path}")
|
||||
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
size = os.path.getsize(target)
|
||||
summary = "fichier vide (0 octet)" if size == 0 else f"{len(content.splitlines())} lignes lues"
|
||||
return {"ok": True, "content": content, "summary": summary}
|
||||
|
||||
|
||||
def write_file(path: str, content: str) -> dict:
|
||||
target = _safe_path(path)
|
||||
os.makedirs(os.path.dirname(target) or _workspace_root(), exist_ok=True)
|
||||
existed = os.path.isfile(target)
|
||||
with open(target, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
lines = len(content.splitlines())
|
||||
verb = "modifié" if existed else "écrit"
|
||||
return {"ok": True, "summary": f"{verb} · {lines} lignes", "lines": lines}
|
||||
|
||||
|
||||
def list_dir(path: str = ".") -> dict:
|
||||
target = _safe_path(path)
|
||||
if not os.path.isdir(target):
|
||||
raise ToolError(f"répertoire introuvable : {path}")
|
||||
entries = []
|
||||
for name in sorted(os.listdir(target)):
|
||||
full = os.path.join(target, name)
|
||||
entries.append({"name": name, "type": "dir" if os.path.isdir(full) else "file"})
|
||||
return {
|
||||
"ok": True,
|
||||
"entries": entries,
|
||||
"summary": f"{len(entries)} élément(s)",
|
||||
}
|
||||
|
||||
|
||||
# ── Registre & définitions exposées au modèle ────────────────────────────
|
||||
TOOL_IMPL = {
|
||||
"read_file": read_file,
|
||||
"write_file": write_file,
|
||||
"list_dir": list_dir,
|
||||
}
|
||||
|
||||
TOOL_DEFINITIONS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Lire le contenu d'un fichier du workspace.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Chemin relatif au workspace"}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Créer ou modifier un fichier du workspace.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Chemin relatif au workspace"},
|
||||
"content": {"type": "string", "description": "Contenu complet du fichier"},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_dir",
|
||||
"description": "Lister le contenu d'un répertoire du workspace.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Répertoire (défaut : racine)"}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def run_tool(name: str, args: dict) -> dict:
|
||||
"""Exécute un outil par son nom ; lève ToolError si inconnu/invalide."""
|
||||
impl = TOOL_IMPL.get(name)
|
||||
if impl is None:
|
||||
raise ToolError(f"outil inconnu : {name}")
|
||||
try:
|
||||
return impl(**(args or {}))
|
||||
except ToolError:
|
||||
raise
|
||||
except TypeError as exc:
|
||||
raise ToolError(f"arguments invalides pour {name} : {exc}") from exc
|
||||
except OSError as exc:
|
||||
raise ToolError(f"erreur système ({name}) : {exc}") from exc
|
||||
Reference in new issue
Block a user