"""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 = "") -> 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