mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 05:28:15 +02:00
Phase 7 : outils web_search et run_shell avec validation
Backend : - tools.py : web_search (DuckDuckGo HTML sans clé, ou SearxNG via SEARX_URL) et run_shell (confiné au workspace, sortie bornée, timeout) - agent.py : run_shell sensible -> émet tool_confirm et ne s'exécute pas tant que l'utilisateur n'a pas validé (confirm_shell) - agent_config.py : web_search/run_shell désactivés par défaut, flag confirm_shell - routes/shell.py : POST /api/shell/run exécute une commande validée - routes/chat.py : relaie l'événement tool_confirm, passe confirm_shell Frontend : - streamChat : événement tool_confirm - store : pendingShell + approveShell/rejectShell (réinjecte le résultat à l'agent) - ChatPanel : carte de validation de commande (Approuver / Refuser) - SettingsView : toggles web_search/run_shell, marqueur sensible, switch confirm_shell - ToolCard : glyphes web_search/run_shell, statut 'à valider', aperçu d'argument Tests : run_shell confiné, gate de confirmation (commande dangereuse non exécutée), route shell, parser DuckDuckGo. 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
2db6beea5c
commit
de2befe31e
12 files changed
+398
-26
No files matched your search
@@ -9,7 +9,12 @@ toute tentative de sortie (../, chemin absolu hors workspace) est rejetée.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import settings
|
||||
|
||||
@@ -71,11 +76,114 @@ def list_dir(path: str = ".") -> dict:
|
||||
}
|
||||
|
||||
|
||||
def web_search(query: str, max_results: int = 5) -> dict:
|
||||
"""Recherche web (DuckDuckGo HTML, sans clé d'API).
|
||||
|
||||
Optionnellement, si SEARX_URL est défini, interroge une instance SearxNG.
|
||||
Renvoie une liste de résultats {title, url, snippet}.
|
||||
"""
|
||||
query = (query or "").strip()
|
||||
if not query:
|
||||
raise ToolError("requête de recherche vide")
|
||||
|
||||
searx = os.environ.get("SEARX_URL")
|
||||
try:
|
||||
if searx:
|
||||
results = _search_searx(searx, query, max_results)
|
||||
else:
|
||||
results = _search_duckduckgo(query, max_results)
|
||||
except httpx.HTTPError as exc:
|
||||
raise ToolError(f"recherche web indisponible : {exc}") from exc
|
||||
|
||||
summary = f"{len(results)} résultat(s)" if results else "aucun résultat"
|
||||
return {"ok": True, "results": results, "summary": summary}
|
||||
|
||||
|
||||
def _search_searx(base: str, query: str, n: int) -> list[dict]:
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
r = client.get(
|
||||
base.rstrip("/") + "/search",
|
||||
params={"q": query, "format": "json"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json().get("results", [])[:n]
|
||||
return [
|
||||
{"title": d.get("title", ""), "url": d.get("url", ""),
|
||||
"snippet": d.get("content", "")}
|
||||
for d in data
|
||||
]
|
||||
|
||||
|
||||
_DDG_RESULT = re.compile(
|
||||
r'<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)</a>'
|
||||
r'.*?class="result__snippet"[^>]*>(.*?)</a>',
|
||||
re.DOTALL,
|
||||
)
|
||||
_TAGS = re.compile(r"<[^>]+>")
|
||||
|
||||
|
||||
def _clean(text: str) -> str:
|
||||
return html.unescape(_TAGS.sub("", text)).strip()
|
||||
|
||||
|
||||
def _search_duckduckgo(query: str, n: int) -> list[dict]:
|
||||
with httpx.Client(timeout=10.0, follow_redirects=True) as client:
|
||||
r = client.post(
|
||||
"https://html.duckduckgo.com/html/",
|
||||
data={"q": query},
|
||||
headers={"User-Agent": "Mozilla/5.0 (Loki agent)"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
results = []
|
||||
for url, title, snippet in _DDG_RESULT.findall(r.text)[:n]:
|
||||
results.append({
|
||||
"title": _clean(title),
|
||||
"url": html.unescape(url),
|
||||
"snippet": _clean(snippet),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def run_shell(command: str, timeout: int = 60) -> dict:
|
||||
"""Exécute une commande shell dans le workspace (outil sensible).
|
||||
|
||||
L'exécution effective n'a lieu qu'après validation utilisateur (gérée par
|
||||
la boucle agentique / la route /api/shell). Confinée au workspace.
|
||||
"""
|
||||
command = (command or "").strip()
|
||||
if not command:
|
||||
raise ToolError("commande vide")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=_workspace_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ToolError(f"délai dépassé ({timeout}s)") from exc
|
||||
|
||||
out = (proc.stdout or "") + (proc.stderr or "")
|
||||
out = out[:4000] # borne la taille renvoyée au modèle
|
||||
status = "ok" if proc.returncode == 0 else "error"
|
||||
return {
|
||||
"ok": proc.returncode == 0,
|
||||
"exit_code": proc.returncode,
|
||||
"output": out,
|
||||
"summary": f"code {proc.returncode}",
|
||||
"_status": status,
|
||||
}
|
||||
|
||||
|
||||
# ── Registre & définitions exposées au modèle ────────────────────────────
|
||||
TOOL_IMPL = {
|
||||
"read_file": read_file,
|
||||
"write_file": write_file,
|
||||
"list_dir": list_dir,
|
||||
"web_search": web_search,
|
||||
"run_shell": run_shell,
|
||||
}
|
||||
|
||||
TOOL_DEFINITIONS = [
|
||||
@@ -121,6 +229,37 @@ TOOL_DEFINITIONS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Rechercher sur le web et renvoyer les meilleurs résultats.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Termes de recherche"}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_shell",
|
||||
"description": (
|
||||
"Exécuter une commande shell dans le workspace. Outil sensible :"
|
||||
" l'utilisateur doit valider la commande avant exécution."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "Commande à exécuter"}
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user