Terminal / tools /registry_macro.py
Baida—-
deploy: full backend sync for Node A [Kernel Restore]
8b87770 verified
Raw
History Blame Contribute Delete
12.6 kB
"""registry_macro.py — Macro tool compositi: bulk-write, fetch, webhook, notion, python_analyze.
Estratto da registry.py per ridurre il file principale.
Funzioni esportate:
_write_and_check, _bulk_write_files, _fetch_and_extract,
_read_multiple_files, _trigger_webhook, _notion_rw, _jina_fetch, _python_analyze
"""
from __future__ import annotations
import httpx
import asyncio
import subprocess
import tempfile
import os
import sys
import logging
_logger = logging.getLogger("tools.registry")
# ─── P24-F1: Macro tools — workflow compositi ─────────────────────────────────
async def _write_and_check(
path: str,
content: str,
language: str = "auto",
run_lint: bool = True,
) -> dict:
"""Macro: write_file → lint_code — scrive e verifica in un unico step."""
write_res = await _write_file(path=path, content=content)
lint_res: "dict | None" = None
if run_lint:
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
if ext in ("py", "js", "ts", "tsx", "jsx", "json", "css", "html"):
lint_res = await _lint_code_tool(content=content, language=language, path=path)
has_errors = bool(lint_res and (lint_res.get("errors") or lint_res.get("error")))
return {
"ok": True,
"path": path,
"written": True,
"bytes_written": len(content.encode("utf-8")),
"lint": lint_res,
"has_errors": has_errors,
"message": (
f"Scritto {path} ({len(content)} chars)"
+ (" — lint OK" if lint_res and not has_errors else " — errori lint" if has_errors else "")
),
}
async def _bulk_write_files(
files: dict,
stop_on_error: bool = False,
run_lint: bool = False,
) -> dict:
"""Macro: write_file x N in parallelo — scrive più file in un solo step."""
if not isinstance(files, dict) or not files:
return {"ok": False, "error": "files deve essere un dict {path: content} non vuoto"}
async def _single(p: str, c: str) -> "tuple[str, bool, str]": # type: ignore[type-arg]
try:
await _write_file(path=p, content=str(c))
if run_lint:
ext = p.rsplit(".", 1)[-1].lower() if "." in p else ""
if ext in ("py", "js", "ts", "tsx", "jsx", "json"):
lr = await _lint_code_tool(content=str(c), path=p)
if lr.get("errors"):
return p, False, f"lint errors: {str(lr['errors'])[:200]}"
return p, True, ""
except Exception as exc: # noqa: BLE001
return p, False, str(exc)[:200]
results = await asyncio.gather(*[_single(p, c) for p, c in files.items()])
written = [p for p, ok, _ in results if ok]
failed = [{"path": p, "error": e} for p, ok, e in results if not ok]
return {
"ok": len(failed) == 0,
"written": written,
"failed": failed,
"total": len(files),
"written_count": len(written),
"message": f"Scritti {len(written)}/{len(files)} file" + (f" — {len(failed)} errori" if failed else ""),
}
async def _fetch_and_extract(url: str, extract: str = "all") -> dict:
"""Macro: read_page → estrazione strutturata (title, testo, word_count, links)."""
import re as _re
page = await _read_page(url=url, query="")
raw_text = page.get("text") or page.get("content") or ""
title = page.get("title", "")
paras = [p.strip() for p in raw_text.split("\n") if len(p.strip()) > 60]
result: dict = {
"ok": page.get("ok", True),
"url": url,
"title": title,
"word_count": len(raw_text.split()),
"char_count": len(raw_text),
}
if page.get("error"):
result["error"] = page["error"]
if extract in ("all", "text", "summary"):
result["paragraphs"] = paras[:8]
result["excerpt"] = raw_text[:2500]
if extract in ("all", "links"):
result["links"] = list(dict.fromkeys(_re.findall(r'https?://[^\s"<>]{10,}', raw_text)))[:30]
return result
async def _read_multiple_files(paths: list, max_chars_per_file: int = 4000) -> dict:
"""Macro: read_file x N in parallelo — legge più file e aggrega i contenuti."""
if not paths:
return {"ok": False, "error": "paths non può essere vuoto"}
async def _single(path: str) -> "tuple[str, str | None, str | None]": # type: ignore[type-arg]
try:
res = await _read_file(path=path)
content = (res.get("content") or res.get("text") or "")[:max_chars_per_file]
return path, content, None
except Exception as exc: # noqa: BLE001
return path, None, str(exc)[:200]
results = await asyncio.gather(*[_single(p) for p in paths])
files_out = {p: c for p, c, e in results if c is not None}
errors_out = {p: e for p, c, e in results if e is not None}
return {
"ok": len(errors_out) == 0,
"files": files_out,
"errors": errors_out,
"read_count": len(files_out),
"total": len(paths),
}
async def _trigger_webhook(
url: str,
payload: "dict | str | None" = None,
method: str = "POST",
headers: "dict | None" = None,
timeout: float = 10.0,
) -> dict:
"""Wrapper — delega all'implementazione in tools/trigger_webhook.py."""
try:
from tools.trigger_webhook import trigger_webhook as _tw
return await _tw(url=url, payload=payload, method=method, headers=headers, timeout=timeout)
except ImportError:
import httpx as _hx
body = None
_hdrs: dict = {"User-Agent": "agente-ai/1.0"}
if headers:
_hdrs.update(headers)
if payload is not None:
import json as _j
body = _j.dumps(payload).encode() if isinstance(payload, dict) else str(payload).encode()
_hdrs.setdefault("Content-Type", "application/json")
async with _hx.AsyncClient(timeout=min(float(timeout), 10.0), follow_redirects=True) as _c:
_m = method.upper()
if _m == "GET":
r = await _c.get(url, headers=_hdrs)
elif _m == "PUT":
r = await _c.put(url, content=body, headers=_hdrs)
else:
r = await _c.post(url, content=body, headers=_hdrs)
return {"ok": r.is_success, "status": r.status_code, "body": r.text[:2000], "error": None}
# ─── P24-F2: notion_rw wrapper ────────────────────────────────────────────────
async def _notion_rw(
action: str,
query: str = "",
page_id: str = "",
parent_id: str = "",
title: str = "",
content: str = "",
max_results: int = 5,
) -> dict:
"""Wrapper — delega all'implementazione in tools/notion_tool.py."""
from tools.notion_tool import notion_rw as _nrw # noqa: PLC0415
return await _nrw(
action=action, query=query, page_id=page_id,
parent_id=parent_id, title=title, content=content,
max_results=max_results,
)
# ─── P24-F3: jina_fetch wrapper ────────────────────────────────────────────────
async def _jina_fetch(
url: str,
query: str = "",
target_selector: str = "",
remove_selector: str = "",
max_length: int = 12000,
) -> dict:
"""Wrapper — delega a tools/jina_reader.py (Jina Reader per SPA e siti JS)."""
from tools.jina_reader import jina_fetch as _jr # noqa: PLC0415
return await _jr(
url=url, query=query,
target_selector=target_selector,
remove_selector=remove_selector,
max_length=max_length,
)
async def _python_analyze(code: str = "", content: str = "", filename: str = "<string>") -> dict:
"""P30-B1: Analisi statica Python in-process — zero exec_engine, zero deps esterne.
Usa ast.parse() + visitor per:
- Syntax check con posizione esatta (riga, colonna, testo)
- Metriche strutturali: funzioni/classi/imports/nesting/righe
- Suggerimenti actionable (funzioni troppo lunghe, nesting alto, ecc.)
Zero I/O, zero network. Tipicamente <5ms.
GAP-1: accetta sia 'code' che 'content' — alias per compatibilità frontend.
Il frontend (toolDefsCode.ts) invia 'content', il backend usava solo 'code'.
"""
# GAP-1: alias — frontend può inviare 'content' invece di 'code'
code = code or content
import ast as _ast
result: dict = {"syntax_ok": False, "errors": [], "complexity": {}, "suggestions": [], "summary": ""}
if not code or not code.strip():
result["errors"] = [{"type": "EmptyCode", "message": "Nessun codice fornito (parametro 'code' o 'content' richiesto)", "line": 0}]
result["summary"] = "Codice vuoto"
return result
# 1. Syntax check
try:
tree = _ast.parse(code, filename=filename)
result["syntax_ok"] = True
except SyntaxError as _e:
result["errors"] = [{
"type": "SyntaxError", "message": str(_e.msg or _e),
"line": _e.lineno or 0, "col": _e.offset or 0,
"text": (_e.text or "").rstrip(),
}]
result["summary"] = f"SyntaxError alla riga {_e.lineno}: {_e.msg}"
return result
except IndentationError as _e:
result["errors"] = [{
"type": "IndentationError", "message": str(_e.msg or _e), "line": _e.lineno or 0,
}]
result["summary"] = f"IndentationError alla riga {_e.lineno}"
return result
# 2. AST visitor per metriche
class _V(_ast.NodeVisitor):
def __init__(self):
self.fns: list[dict] = []
self.classes: list[dict] = []
self.imports: list[str] = []
self.max_nesting = 0
self._depth = 0
def _ent(self): self._depth += 1; self.max_nesting = max(self.max_nesting, self._depth)
def _ex(self): self._depth -= 1
def visit_FunctionDef(self, n):
_ln = (n.end_lineno or n.lineno) - n.lineno + 1
self.fns.append({"name": n.name, "line": n.lineno, "lines": _ln})
self._ent(); self.generic_visit(n); self._ex()
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, n):
self.classes.append({"name": n.name, "line": n.lineno})
self._ent(); self.generic_visit(n); self._ex()
def visit_For(self, n): self._ent(); self.generic_visit(n); self._ex()
def visit_While(self, n): self._ent(); self.generic_visit(n); self._ex()
def visit_If(self, n): self._ent(); self.generic_visit(n); self._ex()
def visit_With(self, n): self._ent(); self.generic_visit(n); self._ex()
def visit_Try(self, n): self._ent(); self.generic_visit(n); self._ex()
def visit_Import(self, n):
for _a in n.names: self.imports.append(_a.name)
def visit_ImportFrom(self, n):
if n.module: self.imports.append(n.module)
_v = _V(); _v.visit(tree)
_tot = len(code.splitlines())
result["complexity"] = {
"total_lines": _tot,
"functions": len(_v.fns),
"classes": len(_v.classes),
"imports": _v.imports[:20],
"max_nesting": _v.max_nesting,
}
# 3. Suggerimenti actionable
_sug: list[str] = []
for _f in _v.fns:
if _f["lines"] > 50:
_sug.append(f"Funzione '{_f['name']}' (riga {_f['line']}) ha {_f['lines']} righe — valuta di dividerla")
if _v.max_nesting >= 5:
_sug.append(f"Nesting max {_v.max_nesting} livelli — rischio complessità ciclomatica alta")
if not _v.fns and _tot > 30:
_sug.append("Nessuna funzione definita su codice lungo — struttura in funzioni")
if "import *" in code:
_sug.append("Evita 'import *' — importa esplicitamente solo ciò che serve")
_dup_imports = {_i for _i in _v.imports if _v.imports.count(_i) > 1}
if _dup_imports:
_sug.append(f"Import duplicati rilevati: {', '.join(sorted(_dup_imports))}")
result["suggestions"] = _sug[:5]
# 4. Summary
_parts = [f"OK — {_tot} righe"]
if _v.fns: _parts.append(f"{len(_v.fns)} funzioni")
if _v.classes:_parts.append(f"{len(_v.classes)} classi")
if _v.imports:_parts.append(f"{len(_v.imports)} import")
if _v.max_nesting: _parts.append(f"nesting max {_v.max_nesting}")
result["summary"] = "Sintassi " + ", ".join(_parts)
return result