mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
perf(tools): filtrage signal/bruit des sorties d'outils (façon rtk)
Les résultats d'outils dévoraient le contexte du modèle : sortie shell tronquée aveuglément à 4000 c. (bruit gardé, erreur parfois coupée), gros fichiers lus en entier. - run_shell : succès = 12 dernières lignes ; échec = lignes d'erreur + fin de sortie, répétitions dédupliquées (« ligne ×N ») - read_file : fenêtres de 200 lignes avec marche à suivre (start_line=N pour la suite, grep_search pour cibler) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
bddab1c619
commit
f87ac32f88
2 files changed
+163
-8
No files matched your search
+94
-8
@@ -41,15 +41,45 @@ def _safe_path(rel: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ── Implémentations ──────────────────────────────────────────────────────
|
# ── Implémentations ──────────────────────────────────────────────────────
|
||||||
def read_file(path: str) -> dict:
|
# Fenêtrage de lecture : un gros fichier entier engloutit le contexte du
|
||||||
|
# modèle. Au-delà du seuil, on renvoie une fenêtre + la marche à suivre.
|
||||||
|
_READ_WINDOW_LINES = 200
|
||||||
|
_READ_MAX_CHARS = 12_000
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path: str, start_line: int = 1) -> dict:
|
||||||
target = _safe_path(path)
|
target = _safe_path(path)
|
||||||
if not os.path.isfile(target):
|
if not os.path.isfile(target):
|
||||||
raise ToolError(f"fichier introuvable : {path}")
|
raise ToolError(f"fichier introuvable : {path}")
|
||||||
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
||||||
content = f.read()
|
lines = f.read().splitlines()
|
||||||
size = os.path.getsize(target)
|
total = len(lines)
|
||||||
summary = "fichier vide (0 octet)" if size == 0 else f"{len(content.splitlines())} lignes lues"
|
if total == 0:
|
||||||
return {"ok": True, "content": content, "summary": summary}
|
return {"ok": True, "content": "", "summary": "fichier vide (0 octet)"}
|
||||||
|
|
||||||
|
start = max(1, int(start_line or 1))
|
||||||
|
window = lines[start - 1 : start - 1 + _READ_WINDOW_LINES]
|
||||||
|
content = "\n".join(window)
|
||||||
|
truncated_by_chars = False
|
||||||
|
if len(content) > _READ_MAX_CHARS:
|
||||||
|
content = content[:_READ_MAX_CHARS]
|
||||||
|
truncated_by_chars = True
|
||||||
|
|
||||||
|
end = start + len(window) - 1
|
||||||
|
if start == 1 and end >= total and not truncated_by_chars:
|
||||||
|
return {"ok": True, "content": content, "summary": f"{total} lignes lues"}
|
||||||
|
|
||||||
|
# Fenêtre partielle : le modèle sait où il en est et comment continuer.
|
||||||
|
note = (
|
||||||
|
f"[fichier {path} : {total} lignes — fenêtre {start}-{end}. "
|
||||||
|
f"Pour la suite : read_file(path, start_line={end + 1}). "
|
||||||
|
"Pour cibler un passage précis : grep_search puis edit_file.]"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"content": content + "\n" + note,
|
||||||
|
"summary": f"lignes {start}-{end} sur {total}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_html(target: str) -> list[str]:
|
def check_html(target: str) -> list[str]:
|
||||||
@@ -306,6 +336,53 @@ def _search_duckduckgo(query: str, n: int) -> list[dict]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Lignes porteuses de signal dans une sortie de commande en échec.
|
||||||
|
_ERROR_LINE = re.compile(
|
||||||
|
r"error|erreur|fail|except|traceback|fatal|warn|undefined|cannot|"
|
||||||
|
r"not found|introuvable|refus|denied|invalid|missing|panic",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_lines(lines: list[str]) -> list[str]:
|
||||||
|
"""Compacte les répétitions consécutives (« ligne ×N »)."""
|
||||||
|
out: list[str] = []
|
||||||
|
for line in lines:
|
||||||
|
if out:
|
||||||
|
base, _, count = out[-1].partition(" ×")
|
||||||
|
if base == line:
|
||||||
|
n = int(count) if count.isdigit() else 1
|
||||||
|
out[-1] = f"{line} ×{n + 1}"
|
||||||
|
continue
|
||||||
|
out.append(line)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_output(output: str, exit_code: int) -> str:
|
||||||
|
"""Filtre la sortie shell façon rtk : le signal, pas le déroulé.
|
||||||
|
|
||||||
|
- succès : dernières lignes seulement (le détail n'apporte rien) ;
|
||||||
|
- échec : lignes d'erreur + fin de sortie, dédupliquées.
|
||||||
|
Tronquer bêtement à N caractères gardait le bruit et coupait l'erreur.
|
||||||
|
"""
|
||||||
|
lines = [l.rstrip() for l in output.splitlines() if l.strip()]
|
||||||
|
lines = _dedupe_lines(lines)
|
||||||
|
if exit_code == 0:
|
||||||
|
kept = lines[-12:]
|
||||||
|
text = "\n".join(kept)
|
||||||
|
if len(lines) > 12:
|
||||||
|
text = f"[…{len(lines) - 12} lignes omises]\n" + text
|
||||||
|
return text[:1200]
|
||||||
|
|
||||||
|
error_lines = [l for l in lines if _ERROR_LINE.search(l)]
|
||||||
|
tail = lines[-10:]
|
||||||
|
kept = error_lines[:20] + [l for l in tail if l not in error_lines[:20]]
|
||||||
|
text = "\n".join(kept)
|
||||||
|
if len(lines) > len(kept):
|
||||||
|
text = f"[…sortie filtrée : {len(kept)}/{len(lines)} lignes]\n" + text
|
||||||
|
return text[:2500]
|
||||||
|
|
||||||
|
|
||||||
def run_shell(command: str, timeout: int = 60) -> dict:
|
def run_shell(command: str, timeout: int = 60) -> dict:
|
||||||
"""Exécute une commande shell dans le workspace (outil sensible).
|
"""Exécute une commande shell dans le workspace (outil sensible).
|
||||||
|
|
||||||
@@ -328,7 +405,8 @@ def run_shell(command: str, timeout: int = 60) -> dict:
|
|||||||
raise ToolError(f"délai dépassé ({timeout}s)") from exc
|
raise ToolError(f"délai dépassé ({timeout}s)") from exc
|
||||||
|
|
||||||
out = (proc.stdout or "") + (proc.stderr or "")
|
out = (proc.stdout or "") + (proc.stderr or "")
|
||||||
out = out[:4000] # borne la taille renvoyée au modèle
|
# Filtrage signal/bruit (façon rtk) plutôt que troncature aveugle.
|
||||||
|
out = _compact_output(out, proc.returncode)
|
||||||
status = "ok" if proc.returncode == 0 else "error"
|
status = "ok" if proc.returncode == 0 else "error"
|
||||||
return {
|
return {
|
||||||
"ok": proc.returncode == 0,
|
"ok": proc.returncode == 0,
|
||||||
@@ -401,11 +479,19 @@ TOOL_DEFINITIONS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "read_file",
|
"name": "read_file",
|
||||||
"description": "Lire le contenu d'un fichier du workspace.",
|
"description": (
|
||||||
|
"Lire le contenu d'un fichier du workspace. Les gros fichiers "
|
||||||
|
"sont renvoyés par fenêtres de 200 lignes : utilise start_line "
|
||||||
|
"pour lire la suite, ou grep_search pour cibler un passage."
|
||||||
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {"type": "string", "description": "Chemin relatif au workspace"}
|
"path": {"type": "string", "description": "Chemin relatif au workspace"},
|
||||||
|
"start_line": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Première ligne de la fenêtre (défaut 1)",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["path"],
|
"required": ["path"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
_WS = os.path.abspath(settings.workspace_dir)
|
||||||
|
os.makedirs(_WS, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── read_file fenêtré ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _write(name: str, content: str) -> str:
|
||||||
|
with open(os.path.join(_WS, name), "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_petit_entier():
|
||||||
|
rel = _write("petit.txt", "a\nb\nc\n")
|
||||||
|
result = tools.read_file(rel)
|
||||||
|
assert result["content"] == "a\nb\nc"
|
||||||
|
assert "3 lignes" in result["summary"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_gros_fenetre():
|
||||||
|
rel = _write("gros.txt", "\n".join(f"ligne {i}" for i in range(1, 501)))
|
||||||
|
result = tools.read_file(rel)
|
||||||
|
assert "ligne 200" in result["content"]
|
||||||
|
assert "ligne 201" not in result["content"].replace("start_line=201", "")
|
||||||
|
assert "start_line=201" in result["content"] # marche à suivre
|
||||||
|
assert "1-200 sur 500" in result["summary"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_fenetre_suivante():
|
||||||
|
rel = _write("gros2.txt", "\n".join(f"ligne {i}" for i in range(1, 501)))
|
||||||
|
result = tools.read_file(rel, start_line=201)
|
||||||
|
assert "ligne 201" in result["content"]
|
||||||
|
assert "201-400 sur 500" in result["summary"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── sortie shell filtrée ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_compact_succes_garde_la_fin():
|
||||||
|
out = "\n".join(f"étape {i}" for i in range(1, 101))
|
||||||
|
compact = tools._compact_output(out, 0)
|
||||||
|
assert "étape 100" in compact
|
||||||
|
assert "étape 1\n" not in compact
|
||||||
|
assert "lignes omises" in compact
|
||||||
|
|
||||||
|
|
||||||
|
def test_compact_echec_garde_les_erreurs():
|
||||||
|
out = "\n".join(
|
||||||
|
["compilation démarrée"]
|
||||||
|
+ [f"module {i} ok" for i in range(50)]
|
||||||
|
+ ["ERROR: variable x undefined", "build failed"]
|
||||||
|
)
|
||||||
|
compact = tools._compact_output(out, 1)
|
||||||
|
assert "ERROR: variable x undefined" in compact
|
||||||
|
assert "build failed" in compact
|
||||||
|
assert "module 3 ok" not in compact
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_repetitions():
|
||||||
|
lines = ["warn: deprecated"] * 5 + ["fin"]
|
||||||
|
assert tools._dedupe_lines(lines) == ["warn: deprecated ×5", "fin"]
|
||||||
Reference in new issue
Block a user