mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +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.
|
||||
if awaiting_confirmation:
|
||||
# Laisse le modèle conclure son tour (message d'attente).
|
||||
|
||||
@@ -31,7 +31,7 @@ DEFAULT_SYSTEM_PROMPT = (
|
||||
# par défaut, conformément à la maquette.
|
||||
AVAILABLE_TOOLS = [
|
||||
"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"}
|
||||
DEFAULT_TOOL_STATE = {
|
||||
@@ -40,6 +40,7 @@ DEFAULT_TOOL_STATE = {
|
||||
"edit_file": True,
|
||||
"list_dir": True,
|
||||
"grep_search": True,
|
||||
"run_check": True,
|
||||
"code_task": True,
|
||||
"web_search": False,
|
||||
"run_shell": False,
|
||||
|
||||
@@ -37,7 +37,7 @@ class ChatRequest(BaseModel):
|
||||
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -10,8 +10,10 @@ toute tentative de sortie (../, chemin absolu hors workspace) est rejetée.
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
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 ────────────────────────────
|
||||
TOOL_IMPL = {
|
||||
"read_file": read_file,
|
||||
@@ -337,6 +384,7 @@ TOOL_IMPL = {
|
||||
"grep_search": grep_search,
|
||||
"web_search": web_search,
|
||||
"run_shell": run_shell,
|
||||
"run_check": run_check,
|
||||
}
|
||||
|
||||
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