mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
feat(agent): run_check statique + vérification auto après écriture
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
31dce6036b
commit
c92e4d4a13
5 files changed
+141
-2
No files matched your search
@@ -334,6 +334,36 @@ async def run_agent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Vérification statique automatique après toute écriture de
|
||||||
|
# code : l'erreur revient au modèle dans le même tour
|
||||||
|
# (1 passe de correction max, bornée par MAX_ITERATIONS).
|
||||||
|
if (
|
||||||
|
name in ("write_file", "edit_file")
|
||||||
|
and status == "ok"
|
||||||
|
and str(args.get("path", "")).lower().endswith(
|
||||||
|
(".py", ".js", ".mjs", ".html", ".htm", ".json")
|
||||||
|
)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
from .tools import run_check
|
||||||
|
check = run_check(args["path"])
|
||||||
|
except ToolError:
|
||||||
|
check = None
|
||||||
|
if check and not check["ok"]:
|
||||||
|
record = {
|
||||||
|
"name": "run_check",
|
||||||
|
"args": {"path": args["path"]},
|
||||||
|
"summary": check["summary"],
|
||||||
|
"status": "error",
|
||||||
|
}
|
||||||
|
collected.append(record)
|
||||||
|
yield {"type": "tool_result", **record}
|
||||||
|
convo.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_name": "run_check",
|
||||||
|
"content": json.dumps(check, ensure_ascii=False),
|
||||||
|
})
|
||||||
|
|
||||||
# Une commande shell attend une validation : on interrompt la boucle.
|
# Une commande shell attend une validation : on interrompt la boucle.
|
||||||
if awaiting_confirmation:
|
if awaiting_confirmation:
|
||||||
# Laisse le modèle conclure son tour (message d'attente).
|
# Laisse le modèle conclure son tour (message d'attente).
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ DEFAULT_SYSTEM_PROMPT = (
|
|||||||
# par défaut, conformément à la maquette.
|
# par défaut, conformément à la maquette.
|
||||||
AVAILABLE_TOOLS = [
|
AVAILABLE_TOOLS = [
|
||||||
"read_file", "write_file", "edit_file", "list_dir", "grep_search",
|
"read_file", "write_file", "edit_file", "list_dir", "grep_search",
|
||||||
"code_task", "web_search", "run_shell",
|
"run_check", "code_task", "web_search", "run_shell",
|
||||||
]
|
]
|
||||||
SENSITIVE_TOOLS = {"run_shell"}
|
SENSITIVE_TOOLS = {"run_shell"}
|
||||||
DEFAULT_TOOL_STATE = {
|
DEFAULT_TOOL_STATE = {
|
||||||
@@ -40,6 +40,7 @@ DEFAULT_TOOL_STATE = {
|
|||||||
"edit_file": True,
|
"edit_file": True,
|
||||||
"list_dir": True,
|
"list_dir": True,
|
||||||
"grep_search": True,
|
"grep_search": True,
|
||||||
|
"run_check": True,
|
||||||
"code_task": True,
|
"code_task": True,
|
||||||
"web_search": False,
|
"web_search": False,
|
||||||
"run_shell": False,
|
"run_shell": False,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ChatRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# Outils autorisés en mode Plan : lecture/analyse uniquement.
|
# Outils autorisés en mode Plan : lecture/analyse uniquement.
|
||||||
_READONLY_TOOLS = {"read_file", "list_dir", "grep_search"}
|
_READONLY_TOOLS = {"read_file", "list_dir", "grep_search", "run_check"}
|
||||||
|
|
||||||
|
|
||||||
def _apply_mode(cfg: dict, mode: str) -> dict:
|
def _apply_mode(cfg: dict, mode: str) -> dict:
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ toute tentative de sortie (../, chemin absolu hors workspace) est rejetée.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -328,6 +330,51 @@ def run_shell(command: str, timeout: int = 60) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_check(path: str) -> dict:
|
||||||
|
"""Vérification STATIQUE d'un fichier de code — n'exécute jamais rien.
|
||||||
|
|
||||||
|
.py -> py_compile ; .js/.mjs -> node --check (si node présent) ;
|
||||||
|
.html -> check_html ; .json -> parse. Autres types : ok sans contrôle.
|
||||||
|
"""
|
||||||
|
target = _safe_path(path)
|
||||||
|
if not os.path.isfile(target):
|
||||||
|
raise ToolError(f"fichier introuvable : {path}")
|
||||||
|
ext = os.path.splitext(target)[1].lower()
|
||||||
|
issues: list[str] = []
|
||||||
|
|
||||||
|
if ext == ".py":
|
||||||
|
import py_compile
|
||||||
|
try:
|
||||||
|
py_compile.compile(target, doraise=True)
|
||||||
|
except py_compile.PyCompileError as exc:
|
||||||
|
issues.append(str(exc.msg)[:500])
|
||||||
|
elif ext in (".js", ".mjs"):
|
||||||
|
node = shutil.which("node")
|
||||||
|
if node:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[node, "--check", target], capture_output=True, text=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
issues.append((proc.stderr or proc.stdout)[:500])
|
||||||
|
elif ext in (".html", ".htm"):
|
||||||
|
issues.extend(check_html(target))
|
||||||
|
elif ext == ".json":
|
||||||
|
try:
|
||||||
|
with open(target, encoding="utf-8") as f:
|
||||||
|
json.load(f)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
issues.append(f"JSON invalide : {exc}")
|
||||||
|
|
||||||
|
ok = not issues
|
||||||
|
return {
|
||||||
|
"ok": ok,
|
||||||
|
"issues": issues,
|
||||||
|
"summary": "aucun problème" if ok else f"{len(issues)} problème(s)",
|
||||||
|
"_status": "ok" if ok else "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Registre & définitions exposées au modèle ────────────────────────────
|
# ── Registre & définitions exposées au modèle ────────────────────────────
|
||||||
TOOL_IMPL = {
|
TOOL_IMPL = {
|
||||||
"read_file": read_file,
|
"read_file": read_file,
|
||||||
@@ -337,6 +384,7 @@ TOOL_IMPL = {
|
|||||||
"grep_search": grep_search,
|
"grep_search": grep_search,
|
||||||
"web_search": web_search,
|
"web_search": web_search,
|
||||||
"run_shell": run_shell,
|
"run_shell": run_shell,
|
||||||
|
"run_check": run_check,
|
||||||
}
|
}
|
||||||
|
|
||||||
TOOL_DEFINITIONS = [
|
TOOL_DEFINITIONS = [
|
||||||
@@ -490,6 +538,23 @@ TOOL_DEFINITIONS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "run_check",
|
||||||
|
"description": (
|
||||||
|
"Vérifier statiquement un fichier de code du workspace "
|
||||||
|
"(syntaxe Python/JS/JSON, structure HTML). N'exécute rien."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string", "description": "Chemin relatif au workspace"}
|
||||||
|
},
|
||||||
|
"required": ["path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
os.environ.setdefault("DATA_DIR", tempfile.mkdtemp())
|
||||||
|
os.environ.setdefault("WORKSPACE_DIR", tempfile.mkdtemp())
|
||||||
|
|
||||||
|
from app import tools # noqa: E402
|
||||||
|
from app.config import settings # noqa: E402
|
||||||
|
|
||||||
|
# Le workspace effectif peut avoir été fixé par un autre fichier de test
|
||||||
|
# importé avant celui-ci : on écrit là où les outils lisent réellement.
|
||||||
|
_WS = os.path.abspath(settings.workspace_dir)
|
||||||
|
os.makedirs(_WS, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _write(name: str, content: str) -> str:
|
||||||
|
path = os.path.join(_WS, name)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_valide():
|
||||||
|
rel = _write("ok.py", "x = 1\nprint(x)\n")
|
||||||
|
assert tools.run_check(rel)["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_casse():
|
||||||
|
rel = _write("ko.py", "def broken(:\n")
|
||||||
|
result = tools.run_check(rel)
|
||||||
|
assert result["ok"] is False
|
||||||
|
assert result["issues"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_casse():
|
||||||
|
rel = _write("ko.json", "{invalid")
|
||||||
|
assert tools.run_check(rel)["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_fichier_inconnu_type():
|
||||||
|
rel = _write("notes.txt", "bonjour")
|
||||||
|
result = tools.run_check(rel)
|
||||||
|
assert result["ok"] is True # type non vérifiable = pas d'erreur
|
||||||
Reference in new issue
Block a user