diff --git "a/api/telegram_webhook.py" "b/api/telegram_webhook.py"
--- "a/api/telegram_webhook.py"
+++ "b/api/telegram_webhook.py"
@@ -16,2270 +16,32 @@ DISABILITATO per impedire conflitti con il daemon.
Per passare a webhook mode: ferma il daemon, poi riabilita manualmente.
"""
from __future__ import annotations
-import asyncio, html, logging, os, time
+import asyncio, logging, os
from fastapi import APIRouter, BackgroundTasks, Request, HTTPException
-from pydantic import BaseModel
-import httpx # top-level — era lazy in 20+ funzioni
-
-_logger = logging.getLogger("api.telegram_webhook") # unico logger (duplicato rimosso)
-
-
-def _log_tg_exc(task: "asyncio.Task[None]") -> None:
- """Gap-2.6: log exceptions from fire-and-forget tasks."""
- try:
- exc = task.exception()
- if exc:
- _logger.warning("tg_webhook bg task error: %s: %s", type(exc).__name__, exc)
- except (asyncio.CancelledError, asyncio.InvalidStateError):
- pass
+# ── Import da moduli split (M2 refactor) ──────────────────────────────────────
+from .telegram_tg_client import (
+ _get_bot_token, _log_tg_exc,
+ _tg_reply, _tg_send, _tg_edit, _tg_typing, _tg_react,
+)
+from .telegram_keyboards import (
+ _MAIN_KB, _QUICK_PICK_KB, _after_task_kb, _LAST_GOAL,
+)
+from .telegram_cmd_monitoring import (
+ _cmd_help, _cmd_logs, _cmd_status, _cmd_commit_summary,
+ _cmd_check, _cmd_tasks,
+)
+from .telegram_cmd_ai import (
+ _cmd_do, _cmd_autofix, _cmd_nota, _cmd_cerca, _cmd_meteo,
+ _cmd_riepilogo, _cmd_score, _cmd_bench, _cmd_improve,
+ _cmd_git, _cmd_coord, _cmd_scan_now, _cmd_telemetry,
+)
+from .telegram_callbacks import _handle_inline, _handle_callback
+
+_logger = logging.getLogger("api.telegram_webhook")
router = APIRouter(prefix="/api/telegram", tags=["telegram"])
-# ── Helpers ───────────────────────────────────────────────────────────────────
-
-def _get_bot_token() -> str:
- return os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
-
-
-async def _tg_reply(chat_id: str | int, text: str, token: str | None = None,
- keyboard: dict | None = None) -> None:
- """Invia risposta al chat_id con HTML + opzionale inline keyboard."""
- bot_token = token or _get_bot_token()
- if not bot_token:
- return
- payload: dict = {
- "chat_id": chat_id,
- "text": text,
- "parse_mode": "HTML",
- "link_preview_options": {"is_disabled": True},
- }
- if keyboard:
- payload["reply_markup"] = keyboard
- try:
- import httpx
- async with httpx.AsyncClient(timeout=8.0) as c:
- await c.post(
- f"https://api.telegram.org/bot{bot_token}/sendMessage",
- json=payload,
- )
- except Exception as exc:
- _logger.warning("tg_reply error: %s", exc)
-
-
-async def _tg_answer_callback(callback_query_id: str, text: str = "", token: str | None = None) -> None:
- """Risponde a un callback_query (obbligatorio per chiudere il loading sui buttons)."""
- bot_token = token or _get_bot_token()
- if not bot_token:
- return
- try:
- import httpx
- async with httpx.AsyncClient(timeout=5.0) as c:
- await c.post(
- f"https://api.telegram.org/bot{bot_token}/answerCallbackQuery",
- json={"callback_query_id": callback_query_id, "text": text, "show_alert": False},
- )
- except Exception as exc:
- _logger.debug("answer_callback error: %s", exc)
-
-
-async def _tg_send(chat_id: str | int, text: str, token: str | None = None,
- keyboard: dict | None = None) -> str | None:
- """Invia messaggio e ritorna il message_id (per editMessageText streaming)."""
- bot_token = token or _get_bot_token()
- if not bot_token:
- return None
- payload: dict = {
- "chat_id": chat_id,
- "text": text,
- "parse_mode": "HTML",
- "link_preview_options": {"is_disabled": True},
- }
- if keyboard:
- payload["reply_markup"] = keyboard
- try:
- import httpx
- async with httpx.AsyncClient(timeout=8.0) as c:
- r = await c.post(
- f"https://api.telegram.org/bot{bot_token}/sendMessage",
- json=payload,
- )
- j = r.json()
- return str(j.get("result", {}).get("message_id", "")) if j.get("ok") else None
- except Exception as exc:
- _logger.warning("tg_send error: %s", exc)
- return None
-
-
-async def _tg_edit(chat_id: str | int, message_id: str, text: str,
- token: str | None = None, keyboard: dict | None = None) -> bool:
- """Aggiorna messaggio esistente — streaming live via editMessageText.
- Ritorna True se successo. Rate-limit: max 20 edit/min per chat Telegram."""
- bot_token = token or _get_bot_token()
- if not bot_token or not message_id:
- return False
- payload: dict = {
- "chat_id": chat_id,
- "message_id": int(message_id),
- "text": text[:4000],
- "parse_mode": "HTML",
- "link_preview_options": {"is_disabled": True},
- }
- if keyboard:
- payload["reply_markup"] = keyboard
- try:
- import httpx
- async with httpx.AsyncClient(timeout=8.0) as c:
- r = await c.post(
- f"https://api.telegram.org/bot{bot_token}/editMessageText",
- json=payload,
- )
- return r.json().get("ok", False)
- except Exception as exc:
- _logger.debug("tg_edit error: %s", exc)
- return False
-
-
-async def _tg_photo(
- chat_id: str | int,
- photo_url: str,
- caption: str = "",
- token: str | None = None,
- keyboard: dict | None = None,
-) -> None:
- """Invia foto/chart via sendPhoto Telegram.
-
- Strategia anti URL-lungo:
- 1. POST a quickchart.io → scarica PNG bytes → multipart sendPhoto (no limite URL).
- 2. Fallback: invia URL direttamente (funziona se URL < ~2000 chars).
- """
- bot_token = token or _get_bot_token()
- if not bot_token:
- return
- caption_safe = (caption or "")[:1024]
-
- import httpx as _hx_p, json as _j_p, urllib.parse as _ul_p, re as _re_p
-
- png_bytes: bytes | None = None
- if "quickchart.io/chart" in photo_url:
- try:
- m = _re_p.search(r"[?&]c=([^&]+)", photo_url)
- if m:
- cfg_dict = _j_p.loads(_ul_p.unquote(m.group(1)))
- async with _hx_p.AsyncClient(timeout=20.0) as c:
- qr = await c.post(
- "https://quickchart.io/chart",
- json={"chart": cfg_dict, "width": 720, "height": 420,
- "backgroundColor": "white", "format": "png"},
- )
- if qr.status_code == 200 and qr.headers.get("content-type", "").startswith("image/"):
- png_bytes = qr.content
- _logger.debug("tg_photo: quickchart POST ok, %d bytes", len(png_bytes))
- except Exception as exc:
- _logger.debug("tg_photo: quickchart POST fallback: %s", exc)
-
- try:
- import httpx as _hx_s
- async with _hx_s.AsyncClient(timeout=15.0) as c:
- if png_bytes:
- import json as _j_s
- data: dict = {"chat_id": str(chat_id), "parse_mode": "HTML"}
- if caption_safe:
- data["caption"] = caption_safe
- if keyboard:
- data["reply_markup"] = _j_s.dumps(keyboard)
- files = {"photo": ("chart.png", png_bytes, "image/png")}
- await c.post(f"https://api.telegram.org/bot{bot_token}/sendPhoto",
- data=data, files=files)
- else:
- payload: dict = {"chat_id": chat_id, "photo": photo_url, "parse_mode": "HTML"}
- if caption_safe:
- payload["caption"] = caption_safe
- if keyboard:
- payload["reply_markup"] = keyboard
- await c.post(f"https://api.telegram.org/bot{bot_token}/sendPhoto", json=payload)
- except Exception as exc:
- _logger.warning("tg_photo error: %s", exc)
-
-
-async def _tg_typing(chat_id: str | int, action: str = "typing", token: str | None = None) -> None:
- """Invia sendChatAction — mostra '⌨️ digitando…' prima di operazioni pesanti.
-
- Dura 5 secondi o fino al prossimo messaggio del bot.
- Azioni: typing, upload_photo, upload_document, find_location, record_video_note.
- """
- bot_token = token or _get_bot_token()
- if not bot_token:
- return
- try:
- async with httpx.AsyncClient(timeout=3.0) as c:
- await c.post(
- f"https://api.telegram.org/bot{bot_token}/sendChatAction",
- json={"chat_id": chat_id, "action": action},
- )
- except Exception:
- pass
-
-
-async def _tg_react(
- chat_id: str | int,
- message_id: int | str,
- emoji: str = "👍",
- token: str | None = None,
-) -> None:
- """Aggiunge reazione emoji a un messaggio (Bot API 7.1+, Feb 2024).
-
- Emoji supportate: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🎉 🤩 🏆 ✅ 💯 ⚡ 🚀 🎯
- """
- bot_token = token or _get_bot_token()
- if not bot_token or not message_id:
- return
- try:
- async with httpx.AsyncClient(timeout=3.0) as c:
- await c.post(
- f"https://api.telegram.org/bot{bot_token}/setMessageReaction",
- json={
- "chat_id": chat_id,
- "message_id": int(message_id),
- "reaction": [{"type": "emoji", "emoji": emoji}],
- "is_big": False,
- },
- )
- except Exception:
- pass
-
-
-def _fmt_elapsed(created_at_ms: int) -> str:
- """Formatta elapsed time da un timestamp ms → stringa leggibile."""
- diff = int(time.time() * 1000) - created_at_ms
- s = diff // 1000
- if s < 60:
- return f"{s}s fa"
- if s < 3600:
- return f"{s // 60}m{s % 60:02d}s fa"
- return f"{s // 3600}h{(s % 3600) // 60:02d}m fa"
-
-
-_MAIN_KB = {
- "inline_keyboard": [
- [{"text": "🚀 Nuovo Task", "callback_data": "agent"},
- {"text": "📋 Attività", "callback_data": "tgw_tasks"}],
- [{"text": "🩺 Salute", "callback_data": "tgw_health"},
- {"text": "🔧 AutoFix", "callback_data": "tgw_autofix"}],
- [{"text": "💬 Chiedi all'AI","callback_data": "tgw_ask"},
- {"text": "📈 Stato", "callback_data": "tgw_status"}],
- [{"text": "🌐 Dashboard →", "url": "https://agente-ai.pages.dev"}],
- ]
-}
-_WEBAPP_KB = {
- "inline_keyboard": [
- [{"text": "🚀 Apri Dashboard", "web_app": {"url": "https://agente-ai.pages.dev"}}],
- [{"text": "🏠 Menu", "callback_data": "tgw_help"}],
- ]
-}
-_BACK_KB = {
- "inline_keyboard": [
- [{"text": "🏠 Menu", "callback_data": "tgw_help"},
- {"text": "📊 Stato", "callback_data": "tgw_status"}],
- ]
-}
-
-# ── Quick-pick task templates (MX-QUICKPICK) ──────────────────────────────────
-_QUICK_PICK_KB = {
- "inline_keyboard": [
- [{"text": "🔍 Analizza bug", "callback_data": "qp_bug"},
- {"text": "⚡ Ottimizza DB", "callback_data": "qp_db"}],
- [{"text": "🔧 AutoFix log", "callback_data": "qp_autofix"},
- {"text": "🔀 Riassumi commit", "callback_data": "qp_commits"}],
- [{"text": "📝 Genera docs", "callback_data": "qp_docs"},
- {"text": "🧪 Genera test", "callback_data": "qp_tests"}],
- [{"text": "✍️ Scrivi obiettivo...", "callback_data": "qp_custom"}],
- [{"text": "🏠 Menu", "callback_data": "tgw_help"}],
- ]
-}
-
-# ── After-task keyboard (retry + navigazione) ─────────────────────────────────
-# Ultimo goal per chat_id — usato da 🔁 Rifai
-_LAST_GOAL: dict[int, str] = {}
-
-def _after_task_kb(chat_id: int) -> dict:
- """Keyboard mostrata dopo ogni task completato."""
- return {
- "inline_keyboard": [
- [{"text": "🔁 Rifai", "callback_data": "tgw_retry"},
- {"text": "📋 Attività", "callback_data": "tgw_tasks"}],
- [{"text": "🚀 Nuovo Task", "callback_data": "agent"},
- {"text": "🏠 Menu", "callback_data": "tgw_help"}],
- ]
- }
-
-
-
-# ── Bench cache + keyboard (GAP-TGB) ─────────────────────────────────────────
-# Salva l'ultimo run bench per chat_id → usato dai callback tgw_bench_fix/run
-_BENCH_CACHE: dict[int, dict] = {}
-
-_BENCH_ACTION_KB = {
- "inline_keyboard": [
- [{"text": "🔧 Applica Fix", "callback_data": "tgw_bench_fix"},
- {"text": "🔄 Riesegui", "callback_data": "tgw_bench_run"}],
- [{"text": "⚙️ Migliora", "callback_data": "tgw_improve"}],
- [{"text": "🏠 Menu", "callback_data": "tgw_help"}],
- ]
-}
-
-# ── ReplyKeyboardRemove — rimuove tastiera persistente da versioni precedenti ──
-_REPLY_KB_REMOVE = {"remove_keyboard": True}
-
-# ── Sub-menu inline keyboards ─────────────────────────────────────────────────
-_TASK_MENU_KB = {
- "inline_keyboard": [
- [{"text": "🤖 Nuovo Task", "callback_data": "tgw_do"},
- {"text": "🔧 AutoFix", "callback_data": "tgw_autofix"}],
- [{"text": "⚙️ Migliora AI", "callback_data": "tgw_improve"},
- {"text": "📋 Task recenti", "callback_data": "tgw_tasks"}],
- [{"text": "🧠 Briefing", "callback_data": "tgw_briefing"},
- {"text": "📝 Salva Nota", "callback_data": "tgw_nota"}],
- [{"text": "🔍 Cerca web", "callback_data": "tgw_cerca"},
- {"text": "🌤 Meteo", "callback_data": "tgw_meteo"}],
- ]
-}
-_STATUS_MENU_KB = {
- "inline_keyboard": [
- [{"text": "📊 Daemon+Task", "callback_data": "tgw_status"},
- {"text": "🔌 Provider AI", "callback_data": "tgw_providers"}],
- [{"text": "🔗 Coord sessioni", "callback_data": "tgw_coord"},
- {"text": "🔀 Git log", "callback_data": "tgw_git"}],
- [{"text": "🌐 Dashboard", "url": "https://agente-ai.pages.dev"}],
- ]
-}
-_PERF_MENU_KB = {
- "inline_keyboard": [
- [{"text": "📊 Benchmark", "callback_data": "tgw_bench"},
- {"text": "🏆 Score", "callback_data": "tgw_score"}],
- [{"text": "📡 Telemetria", "callback_data": "tgw_telemetry"},
- {"text": "⚙️ Migliora", "callback_data": "tgw_improve"}],
- [{"text": "🔧 Fix gap bench", "callback_data": "tgw_bench_fix"}],
- ]
-}
-_HEALTH_MENU_KB = {
- "inline_keyboard": [
- [{"text": "🔍 Scan completo", "callback_data": "tgw_health"},
- {"text": "📝 Log errori", "callback_data": "tgw_logs"}],
- [{"text": "📊 Status", "callback_data": "tgw_status"},
- {"text": "🔌 Provider AI", "callback_data": "tgw_providers"}],
- ]
-}
-_DEV_MENU_KB = {
- "inline_keyboard": [
- [{"text": "📸 Snapshot", "callback_data": "tgw_snap"},
- {"text": "✅ Verify", "callback_data": "tgw_verify"}],
- [{"text": "💊 Heal", "callback_data": "tgw_heal"},
- {"text": "📝 Log", "callback_data": "tgw_logs"}],
- [{"text": "🔀 Git commits", "callback_data": "tgw_git"},
- {"text": "🏓 Ping", "callback_data": "tgw_ping"}],
- ]
-}
-
-# ── Command handlers ──────────────────────────────────────────────────────────
-
-async def _cmd_help(chat_id: int) -> None:
- """Menu principale: un messaggio pulito + inline keyboard essenziale."""
- await _tg_typing(chat_id)
- welcome = (
- "🤖 Agente AI\n"
- "Assistente autonomo per lo sviluppo software\n\n"
- "📝 Come usarmi:\n"
- "Scrivi qualsiasi obiettivo — lo eseguo autonomamente:\n"
- " • analizza i bug in providers.py\n"
- " • ottimizza le query Supabase più lente\n"
- " • fai autofix degli errori nel log\n\n"
- "📌 Comandi rapidi:\n"
- " /avvia — lancia un task AI\n"
- " /stato — vedi cosa sta facendo\n"
- " /salute — controllo sistema\n"
- " /chiedi — domanda veloce all\'AI\n\n"
- "⬇️ O scegli dal menu:"
- )
- await _tg_reply(chat_id, welcome, keyboard=_MAIN_KB)
-
-
-async def _cmd_logs(chat_id: int, level: str = "WARNING") -> None:
- """Mostra ultimi log dal backend Railway filtrando per livello."""
- import httpx as _hx
- railway_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
- await _tg_reply(chat_id,
- f"📋 Log Railway — {level.upper()}\n⏳ Fetching…")
- try:
- async with _hx.AsyncClient(timeout=10.0) as c:
- r = await c.get(f"{railway_url}/api/telegram/logs",
- params={"level": level.upper(), "n": 20})
- data = r.json() if r.status_code == 200 else {}
- except Exception as exc:
- await _tg_reply(chat_id,
- "❌ Log non disponibili\n" + html.escape(str(exc)[:200]) + "\n"
- "Controlla Railway dashboard.", keyboard=_BACK_KB)
- return
- records = data.get("records", [])
- if not records:
- msg = ("✅ Nessun " + level.upper() + " nei log!\nSistema stabile."
- if level.upper() in ("WARNING","ERROR")
- else "📋 Log vuoti — nessun record disponibile")
- await _tg_reply(chat_id, msg, keyboard=_BACK_KB)
- return
- import datetime as _dt
- lines = [f"📋 Log ({data.get('count',0)} rec — {level.upper()})\n"]
- for rec in records[:15]:
- ts = _dt.datetime.fromtimestamp(rec.get("ts",0), tz=_dt.timezone.utc).strftime("%H:%M:%S")
- lvl = rec.get("level","?")
- lgr = rec.get("logger","").split(".")[-1][:18]
- msg = html.escape(str(rec.get("msg",""))[:100])
- icon = "🔴" if lvl=="ERROR" else "🟡" if lvl=="WARNING" else "⚪"
- lines.append(f"{icon} {ts} [{lgr}] {msg}")
- await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
-
-
-async def _cmd_status(chat_id: int) -> None:
- await _tg_typing(chat_id)
- try:
- from api.state import _agent_tasks, _loop_registry # noqa: F401
- total = len(_agent_tasks)
- running = sum(1 for t in _agent_tasks.values() if t.get("status") == "RUNNING")
- success = sum(1 for t in _agent_tasks.values() if t.get("status") == "SUCCESS")
- error = sum(1 for t in _agent_tasks.values() if t.get("status") == "ERROR")
- queued = sum(1 for t in _agent_tasks.values() if t.get("status") == "QUEUED")
-
- # Supabase fallback: se in-memory è vuoto (restart backend) legge dal DB
- sb_line = ""
- if total == 0:
- try:
- from api.state import _sb
- if _sb:
- res = await asyncio.to_thread(
- lambda: _sb.table("agent_tasks")
- .select("status")
- .order("created_at", desc=True)
- .limit(50)
- .execute()
- )
- rows = res.data or []
- if rows:
- db_run = sum(1 for r in rows if r.get("status") == "RUNNING")
- db_done = sum(1 for r in rows if r.get("status") == "SUCCESS")
- db_err = sum(1 for r in rows if r.get("status") == "ERROR")
- sb_line = (
- "\n📦 Supabase (ultimi 50): "
- + str(db_run) + " in corso / "
- + str(db_done) + " ok / "
- + str(db_err) + " err"
- + " (backend riavviato)"
- )
- except Exception as _exc:
- _logger.debug("[telegram_webhook] silenced %s", type(_exc).__name__) # noqa: BLE001
-
- from api.scheduler import _tasks as sched_tasks, _loop_task
- sched_ok = _loop_task is not None and not _loop_task.done()
- sched_pending = sum(1 for t in sched_tasks.values() if t.get("status") == "pending")
- sched_label = "✅ attivo" if sched_ok else "❌ fermo"
-
- ts_now = time.strftime("%Y-%m-%d %H:%M:%S")
- railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
- ry_line = ""
- try:
- import httpx as _hx
- async with _hx.AsyncClient(timeout=4.0) as c:
- rv = await c.get(f"{railway_url}/api/info")
- if rv.status_code == 200:
- rj = rv.json()
- ry_line = ("\n🚂 Railway: v" + rj.get("version","?")
- + " — " + rj.get("sprint",""))
- except Exception:
- pass
-
- # ── NEW-1: HEAD git + ultimo commit ──────────────────────────────────
- # Chiama GitHub API con GITHUB_TOKEN (Railway env) — timeout 4s, silent fail.
- # Mostra: sha corto + prima riga commit message + età ("3h fa").
- git_line = ""
- try:
- import httpx as _hx_g, datetime as _dt
- _gh_token = os.getenv("GITHUB_TOKEN", "").strip()
- _gh_repo = os.getenv("GITHUB_REPO", "Baida98/AI").strip()
- if _gh_token and _gh_repo:
- async with _hx_g.AsyncClient(timeout=4.0) as _gc:
- _gr = await _gc.get(
- f"https://api.github.com/repos/{_gh_repo}/commits/main",
- headers={"Authorization": f"Bearer {_gh_token}", "User-Agent": "agente-ai"},
- params={"per_page": 1},
- )
- if _gr.status_code == 200:
- _cj = _gr.json()
- _sha = (_cj.get("sha") or "")[:7]
- _cmsg = ((_cj.get("commit") or {}).get("message") or "").split("\n")[0][:45]
- _date = ((_cj.get("commit") or {}).get("committer") or {}).get("date", "")
- _age = ""
- if _date:
- _ts = _dt.datetime.fromisoformat(_date.replace("Z", "+00:00"))
- _secs = int((_dt.datetime.now(_dt.timezone.utc) - _ts).total_seconds())
- if _secs < 3600: _age = f"{_secs // 60}m fa"
- elif _secs < 86400: _age = f"{_secs // 3600}h fa"
- else: _age = f"{_secs // 86400}g fa"
- git_line = (
- f"\n🔀 HEAD: {html.escape(_sha)}"
- f" {html.escape(_cmsg)} ({_age})"
- )
- except Exception:
- pass
-
- # ── NEW-2: task live — goal + step corrente dal loop_registry ────────
- # Per il primo task RUNNING: mostra goal + azione corrente (dal buffer SSE)
- # + tempo trascorso. Zero overhead se non c'è task in corso.
- live_line = ""
- try:
- _running_list = [t for t in _agent_tasks.values() if t.get("status") == "RUNNING"]
- if _running_list:
- import json as _lj
- _rt = _running_list[0]
- _tid = _rt.get("id") or _rt.get("task_id") or ""
- _goal_s = html.escape((_rt.get("goal") or "")[:38])
- _elapsed = ""
- _ca = _rt.get("created_at", 0)
- if isinstance(_ca, int) and _ca > 0:
- _es = int(time.time() * 1000 - _ca) // 1000
- _elapsed = f" · {_es // 60}m{_es % 60:02d}s" if _es >= 60 else f" · {_es}s"
- # Legge ultimo evento SSE dal buffer (action/type corrente)
- _act = ""
- _ebuf = (_loop_registry.get(_tid) or {}).get("event_buffer", [])
- for _ev in reversed(_ebuf[-30:]):
- try:
- _raw = _ev[6:] if _ev.startswith("data: ") else _ev
- _ed = _lj.loads(_raw)
- _a = _ed.get("action") or _ed.get("type") or ""
- if _a and _a not in ("ping", "connected", "keepalive"):
- _act = f" → {html.escape(str(_a)[:20])}"
- break
- except Exception:
- pass
- live_line = f"\n⚙️ Live: {_goal_s}{_act}{_elapsed}"
- except Exception:
- pass
-
- # ── NEW-3: coord mini — sessioni agent-coord attive ──────────────────
- # 1 riga: chi sta lavorando, su quale sprint, su quali file.
- # Timeout aggressivo 3s — /status deve essere veloce.
- coord_line = ""
- try:
- import httpx as _hx_c2, json as _jc2, time as _tc2
- _SB_URL2 = os.getenv("SUPABASE_URL", "https://zwdoplodbdsxfrddoxmo.supabase.co").rstrip("/")
- _SB_KEY2 = os.getenv("SUPABASE_KEY", "")
- if _SB_KEY2:
- async with _hx_c2.AsyncClient(timeout=3.0) as _cc:
- _cr = await _cc.get(
- f"{_SB_URL2}/rest/v1/agent_tasks",
- params={"goal": "ilike.*__session__*", "select": "context"},
- headers={"apikey": _SB_KEY2, "Authorization": f"Bearer {_SB_KEY2}"},
- )
- if _cr.is_success:
- _now_ms = int(_tc2.time() * 1000)
- _active = []
- for _row in (_cr.json() or []):
- try:
- _ctx = _jc2.loads(_row.get("context") or "{}")
- if _now_ms - int(_ctx.get("lastHeartbeat", 0)) < 300_000:
- _active.append(_ctx)
- except Exception:
- pass
- if _active:
- _s = _active[0]
- _files = [f.split("/")[-1] for f in _s.get("claimedFiles", [])]
- _fstr = ", ".join(_files[:3]) or "—"
- _extra = f" (+{len(_active)-1})" if len(_active) > 1 else ""
- coord_line = (
- f"\n🔗 Coord: {html.escape(_s.get('sessionName','?'))}"
- f" [{html.escape(_s.get('sprint','—'))}]"
- f" · {html.escape(_fstr)}{_extra}"
- )
- else:
- coord_line = "\n🔗 Coord: nessuna sessione attiva"
- except Exception:
- pass
-
- # ── Icona salute sistema ──────────────────────────────────────────────
- if running > 0:
- _sys_icon, _sys_label = "⚙️", f"{running} task in esecuzione"
- elif error > 0 and success == 0 and total > 0:
- _sys_icon, _sys_label = "🔴", "ultimi task terminati con errore"
- elif total == 0:
- _sys_icon, _sys_label = "💤", "nessun task recente"
- else:
- _sys_icon, _sys_label = "✅", "tutto operativo"
-
- parts = [f"📊 Sistema {_sys_icon} — {_sys_label}", ""]
- if running or queued:
- parts.append(f"⚙️ In esecuzione: {running} · In coda: {queued}")
- if success or error or total:
- parts.append(f"✅ Completati: {success} · ❌ Errori: {error} · Totale: {total}")
- for _extra_line in [sb_line, ry_line, git_line, live_line, coord_line]:
- if _extra_line:
- parts.append(_extra_line)
- parts += [
- "",
- f"🗓 Scheduler: {sched_label}" + (f" · {sched_pending} in coda" if sched_pending else ""),
- "",
- f"🕐 {ts_now} UTC",
- ]
- await _tg_reply(chat_id, "\n".join(p for p in parts if p is not None),
- keyboard=_MAIN_KB)
- except Exception as exc:
- await _tg_reply(chat_id, "⚠️ Errore lettura stato: " + html.escape(str(exc)[:200]))
-
-
-async def _cmd_commit_summary(chat_id: int) -> None:
- """Riepilogo humanizzato degli ultimi commit via GitHub + AI leggera."""
- import json as _json
- GH_API = "https://api.github.com/repos/Baida98/AI/commits?per_page=8"
- SKIP_RE = re.compile(r"^(🔒|🔓|acquire push lock|release push lock)", re.I)
- try:
- import httpx
- async with httpx.AsyncClient(timeout=10) as cli:
- r = await cli.get(GH_API, headers={"Authorization": f"Bearer {_gh_token}",
- "Accept": "application/vnd.github+json"})
- commits = r.json() if r.status_code == 200 else []
- except Exception:
- commits = []
-
- # Filtra commit di lock/chore puro
- commits = [c for c in commits
- if not SKIP_RE.match((c.get("commit", {}).get("message") or "").split("\n")[0])][:6]
-
- if not commits:
- await _tg_reply(chat_id, "📭 Nessun commit recente trovato.", keyboard=_BACK_KB)
- return
-
- # Costruisci breve sintesi statica (no AI) con traduzione tipo
- _TYPE_IT = {
- "feat": "Nuova funzione", "fix": "Correzione bug", "docs": "Documentazione",
- "refactor": "Refactor", "chore": "Manutenzione", "test": "Test",
- "perf": "Performance", "ux": "Esperienza utente", "style": "Stile",
- "ci": "CI/CD", "build": "Build",
- }
- _TYPE_ICON = {
- "feat": "✨", "fix": "🔧", "docs": "📄", "refactor": "♻️",
- "chore": "🔩", "test": "🧪", "perf": "⚡", "ux": "🎨",
- "ci": "⚙️", "build": "📦",
- }
- lines = ["🔀 Ultimi commit\n"]
- for c in commits:
- raw_msg = (c.get("commit", {}).get("message") or "").split("\n")[0]
- sha = (c.get("sha") or "")[:7]
- date = (c.get("commit", {}).get("author", {}).get("date") or "")
- time_s = date[11:16] if len(date) >= 16 else "??:??"
- # Estrai tipo e corpo
- m = re.match(r"^(feat|fix|docs|refactor|chore|test|perf|ux|style|ci|build)(?:\([^)]+\))?:\s*(.+)$", raw_msg)
- if m:
- tipo, corpo = m.group(1), m.group(2)
- icon = _TYPE_ICON.get(tipo, "📌")
- tipo_it = _TYPE_IT.get(tipo, tipo)
- # Humanizza la descrizione: rimuovi jargon tecnico comune
- corpo_h = corpo.replace("_", " ").replace("-", " ")
- corpo_h = re.sub(r"(impl|add|implement|update|refactor|fix|use|remove|clean)", "", corpo_h, flags=re.I).strip()
- corpo_h = corpo_h[:60] or corpo[:60]
- lines.append(f"{icon} {tipo_it} — {html.escape(corpo_h)}\n {time_s} · {sha}")
- else:
- lines.append(f"📌 {html.escape(raw_msg[:65])}\n {time_s} · {sha}")
-
- await _tg_reply(
- chat_id,
- "\n\n".join(lines),
- keyboard={
- "inline_keyboard": [
- [{"text": "🔄 Aggiorna", "callback_data": "qp_commits"},
- {"text": "🚀 Nuovo Task", "callback_data": "agent"}],
- [{"text": "🏠 Menu", "callback_data": "tgw_help"}],
- ]
- }
- )
-
-
-async def _cmd_check(chat_id: int) -> None:
- """🔍 Health check live: Railway, HF Space A+B, versione vs GH HEAD."""
- import time as _time
- await _tg_typing(chat_id)
- await _tg_reply(chat_id, "🔍 Check infrastruttura...", keyboard=None)
-
- async def _probe(label: str, url: str, timeout: int = 7) -> str:
- t0 = _time.monotonic()
- try:
- async with httpx.AsyncClient(timeout=timeout) as cli:
- r = await cli.get(url)
- ms = round((_time.monotonic() - t0) * 1000)
- icon = "✅" if r.status_code == 200 else "⚠️"
- return f"{icon} {label} — {r.status_code} {ms}ms"
- except Exception as exc:
- ms = round((_time.monotonic() - t0) * 1000)
- return f"❌ {label} — {str(exc)[:55]} {ms}ms"
-
- probes = await asyncio.gather(
- _probe("Railway backend", "https://ai-production-4c06.up.railway.app/health"),
- _probe("HF Space A", "https://arjanit98-terminal.hf.space/api/version"),
- _probe("HF Space B", "https://baida00-ai-backend-collab.hf.space/api/version"),
- )
-
- # GH HEAD vs HF Space version
- gh_sha, hf_build = "?", "?"
- try:
- async with httpx.AsyncClient(timeout=5) as cli:
- ref_r = await cli.get(
- "https://api.github.com/repos/Baida98/AI/git/refs/heads/main",
- headers={"Authorization": f"Bearer {_gh_token}", "Accept": "application/vnd.github+json"},
- )
- if ref_r.status_code == 200:
- gh_sha = (ref_r.json().get("object", {}).get("sha") or "?")[:10]
- except Exception:
- pass
- try:
- async with httpx.AsyncClient(timeout=5) as cli:
- ver_r = await cli.get("https://arjanit98-terminal.hf.space/api/version")
- if ver_r.status_code == 200:
- d = ver_r.json()
- hf_build = f"{d.get('version','?')} ({d.get('build_date','?')})"
- except Exception:
- pass
-
- sync_icon = "✅" if gh_sha != "?" and gh_sha[:8] in hf_build else "⚠️"
- lines_out = ["🔍 Infrastructure Check"]
- lines_out.extend(probes)
- lines_out.append(f"GH HEAD: {gh_sha}")
- lines_out.append(f"HF build: {hf_build}")
- lines_out.append(f"{sync_icon} GH↔HF {'in sync' if sync_icon=='✅' else 'OUT OF SYNC — usa 🔄 Sync HF'}")
- await _tg_reply(chat_id, "\n".join(lines_out), keyboard=_BACK_KB)
-
-
-async def _cmd_tasks(chat_id: int) -> None:
- await _tg_typing(chat_id)
- STATUS_EMOJI = {
- "SUCCESS": "✅", "ERROR": "❌", "RUNNING": "⚙️",
- "QUEUED": "⏳", "CANCELLED": "🚫",
- }
- try:
- from api.state import _agent_tasks
- mem_tasks = sorted(
- _agent_tasks.values(),
- key=lambda t: t.get("created_at", 0),
- reverse=True,
- )[:8]
-
- # Supabase fallback se memoria vuota
- if not mem_tasks:
- try:
- from api.state import _sb
- if _sb:
- res = await asyncio.to_thread(
- lambda: _sb.table("agent_tasks")
- .select("task_id,goal,status,created_at")
- .order("created_at", desc=True)
- .limit(8)
- .execute()
- )
- rows = res.data or []
- if rows:
- lines = ["📋 Ultimi task (Supabase) ⚠️ backend riavviato\n"]
- for r in rows:
- em = STATUS_EMOJI.get(r.get("status", ""), "•")
- gol = html.escape((r.get("goal") or "")[:55])
- tid = html.escape(str(r.get("task_id") or "")[:8])
- ts = r.get("created_at", 0)
- age = _fmt_elapsed(ts) if isinstance(ts, int) and ts > 1_000_000 else "?"
- lines.append(em + " " + tid + " " + gol + " " + age + "")
- return await _tg_reply(chat_id, "\n".join(lines))
- except Exception as _exc:
- _logger.debug("[telegram_webhook] silenced %s", type(_exc).__name__) # noqa: BLE001
- return await _tg_reply(chat_id, "📋 Nessun task recente in memoria.")
-
- _ST_LABEL = {
- "SUCCESS": "Fatto", "ERROR": "Fallito ⚠️", "RUNNING": "In corso…",
- "QUEUED": "In coda", "CANCELLED": "Annullato",
- }
- _ST_FLAIR = {
- "SUCCESS": "✅", "ERROR": "❌", "RUNNING": "⚙️",
- "QUEUED": "⏳", "CANCELLED": "🚫",
- }
- lines = ["Cosa ha fatto l'agente:\n"]
- for t in mem_tasks:
- st = t.get("status", "?")
- em = _ST_FLAIR.get(st, "•")
- gol = html.escape((t.get("goal") or "—")[:70])
- ca = t.get("created_at", 0)
- age = _fmt_elapsed(ca) if isinstance(ca, int) and ca > 1_000_000 else "poco fa"
- lbl = _ST_LABEL.get(st, st)
- lines.append(f"{em} {gol}\n {lbl} · {age}")
- await _tg_reply(chat_id, "\n\n".join(lines), keyboard=_BACK_KB)
- except Exception as exc:
- await _tg_reply(chat_id, "⚠️ " + html.escape(str(exc)[:200]))
-
-
-async def _cmd_do(chat_id: int, goal: str) -> None:
- """Lancia task AI con streaming progressivo via editMessageText.
-
- Flusso: invia msg iniziale → salva message_id → on_step accumula token
- → edit throttled ogni 1.5s → flush finale con output + keyboard.
- Rate-limit sicuro: max ~40 edit/min totali, Telegram consente 20 edit/min/chat.
- """
- if not goal.strip():
- await _tg_reply(chat_id, "⚠️ Usa il menu per scegliere un task:", keyboard=_QUICK_PICK_KB)
- return
-
- _LAST_GOAL[chat_id] = goal # salva per retry
- await _tg_typing(chat_id)
- msg_id = await _tg_send(
- chat_id,
- "🚀 Avvio task…\n\n"
- "🎯 " + html.escape(goal[:200]) + "\n\n"
- "⏳ Sto elaborando, un momento…",
- )
-
- _buf: list[str] = []
- _last_edit: list[float] = [0.0] # list per mutabilità in closure
- _EDIT_INTERVAL = 1.5
- _MAX_LEN = 3600
-
- async def _flush(final: bool = False) -> None:
- if not msg_id:
- return
- content = "".join(_buf).strip()
- if not content:
- return
- now = time.monotonic()
- if not final and (now - _last_edit[0]) < _EDIT_INTERVAL:
- return
- prefix = "🧠 Risposta AI\nGoal: " + html.escape(goal[:80]) + "\n\n"
- suffix = "" if final else "\n⏳"
- body = html.escape(content[: _MAX_LEN - len(prefix) - len(suffix)])
- await _tg_edit(chat_id, msg_id, prefix + body + suffix)
- _last_edit[0] = time.monotonic()
-
- async def _on_step(event: dict) -> None:
- if event.get("action") == "text_chunk":
- tok = event.get("token", "")
- if tok:
- _buf.append(tok)
- await _flush(final=False)
-
- try:
- from agents.unified_loop import UnifiedAgentLoop
- from api.state import _get_ai_client, _get_mem_manager_async, _get_executor, _get_planner
- client = _get_ai_client()
- memory = await _get_mem_manager_async()
- executor = _get_executor()
- planner = _get_planner()
- try:
- from agents.critic import Critic
- from agents.response_verifier import ResponseVerifier
- critic = Critic(llm_client=client)
- verifier = ResponseVerifier()
- except Exception:
- critic = verifier = None
- loop = UnifiedAgentLoop(
- llm_client=client, critic=critic, verifier=verifier,
- memory=memory, executor=executor, planner=planner,
- )
- result = await asyncio.wait_for(
- loop.run(goal=goal, context="", max_steps=8, on_step=_on_step),
- timeout=120.0,
- )
- output = result.get("output", "") if isinstance(result, dict) else str(result)
- await _flush(final=True)
- if msg_id and output:
- _out_str = str(output)
- _out_esc = html.escape(_out_str[:3000])
- # Usa blockquote espandibile per output lunghi (Bot API 7.4+, Jul 2024)
- _out_body = (
- f"
{_out_esc}" - if len(_out_str) > 400 else _out_esc - ) - await _tg_edit( - chat_id, msg_id, - "✅ Fatto!\n\n" - "🎯 " + html.escape(goal[:100]) + "\n\n" - + _out_body, - keyboard=_after_task_kb(chat_id), - ) - await _tg_react(chat_id, msg_id, "🎉") - try: - from .telegram_notify import notify_task_done - task_id = result.get("task_id", "tgw-do") if isinstance(result, dict) else "tgw-do" - await notify_task_done(str(task_id), goal, str(output)) - except Exception: - if not msg_id: - _fb_esc = html.escape(str(output)[:800]) - _fb_body = ( - f"
{_fb_esc}" - if len(str(output)) > 400 else _fb_esc - ) - await _tg_reply( - chat_id, - "✅ Fatto!\n\n" - "🎯 " + html.escape(goal[:80]) + "\n\n" - + _fb_body, - ) - except asyncio.TimeoutError: - await _flush(final=True) - err = ("⏱ Timeout — task >120s.\nGoal: " - + html.escape(goal[:120]) + "\n\nUsa il pannello web.") - if msg_id: await _tg_edit(chat_id, msg_id, err, keyboard=_MAIN_KB) - else: await _tg_reply(chat_id, err) - except Exception as exc: - await _flush(final=True) - err = "❌ Errore\n
" + html.escape(str(exc)[:300]) + ""
- if msg_id: await _tg_edit(chat_id, msg_id, err, keyboard=_MAIN_KB)
- else: await _tg_reply(chat_id, err)
-
-
-async def _cmd_autofix(chat_id: int, hint: str = "") -> None:
- """Comando /autofix — legge errori dai log backend, genera patch via AI, pusha su GitHub.
-
- Flusso:
- 1. GET /api/telegram/logs?level=ERROR — raccoglie ultimi errori
- 2. AI loop con streaming (on_step) — analizza e genera patch
- 3. Parsa blocco ```autofix\nFILE: path\n---\ncontent``` dall'output AI
- 4. Git Data API blob->tree->commit->PATCH ref — push automatico
- 5. Riporta commit SHA al chat + link GitHub
-
- Env vars Railway: GITHUB_TOKEN (gia' presente), GITHUB_REPO, GITHUB_BRANCH.
- """
- import httpx, base64 as _b64, json as _json
- await _tg_typing(chat_id, "upload_document")
-
- msg_id = await _tg_send(
- chat_id,
- "🔧 AutoFix avviato\n\n"
- + (f"Hint: {html.escape(hint[:100])}\n\n" if hint else "")
- + "⏳ Step 1/4 — Lettura log errori…",
- )
-
- async def _edit(text: str, final: bool = False) -> None:
- if msg_id:
- await _tg_edit(chat_id, msg_id, text, keyboard=_MAIN_KB if final else None)
-
- # ── Step 1: leggi errori dal log endpoint ─────────────────────────────────────────
- railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app").rstrip("/")
- try:
- async with httpx.AsyncClient(timeout=10.0) as c:
- resp = await c.get(f"{railway_url}/api/telegram/logs",
- params={"level": "ERROR", "n": 30})
- log_data = resp.json() if resp.status_code == 200 else {}
- except Exception as e:
- log_data = {}
- _logger.warning("autofix: log fetch: %s", e)
-
- records = log_data.get("records", [])
-
- if not records and not hint:
- await _edit(
- "✅ AutoFix\n\n"
- "🟢 Nessun errore nei log recenti!\n\n"
- "Sistema stabile. Usa /autofix <descrizione bug> "
- "per fix su un errore specifico.",
- final=True,
- )
- return
-
- log_lines = "\n".join(
- f"[{r.get('level','?')}] {r.get('logger','?')}: {r.get('msg','')}"
- for r in records[:15]
- ) if records else f"(nessun errore nei log — hint: {hint})"
-
- log_preview = "\n".join(
- f"• [{r.get('level','?')}] {r.get('logger','?')}: {r.get('msg','')[:80]}"
- for r in records[:5]
- ) or f"hint: {html.escape(hint[:100])}"
-
- await _edit(
- "🔧 AutoFix\n\n"
- "📋 Errori trovati:\n" + log_preview
- + "\n\n⏳ Step 2/4 — Analisi AI in corso…",
- )
-
- # ── Step 2: AI loop genera patch ──────────────────────��──────────────────────────────
- BT3 = "```" # triple backtick — non usare literal per evitare syntax issues
- goal = (
- "Sei un senior engineer. Analizza questi errori dal log backend Python "
- "e genera un patch preciso per risolvere il problema principale.\n\n"
- "LOG ERRORI:\n" + log_lines[:2000]
- + (f"\n\nHINT UTENTE: {hint}" if hint else "")
- + "\n\n"
- "FORMATO RISPOSTA RICHIESTO (tassativo):\n"
- + BT3 + "autofix\n"
- + "FILE: backend/api/" + html.escape(preview) + "\n\n⏳"
- )
- _last[0] = now
-
- try:
- from agents.unified_loop import UnifiedAgentLoop
- from api.state import _get_ai_client, _get_mem_manager_async, _get_executor, _get_planner
- client = _get_ai_client()
- memory = await _get_mem_manager_async()
- executor = _get_executor()
- planner = _get_planner()
- try:
- from agents.critic import Critic
- from agents.response_verifier import ResponseVerifier
- critic = Critic(llm_client=client)
- verifier = ResponseVerifier()
- except Exception:
- critic = verifier = None
- loop = UnifiedAgentLoop(
- llm_client=client, critic=critic, verifier=verifier,
- memory=memory, executor=executor, planner=planner,
- )
- result = await asyncio.wait_for(
- loop.run(goal=goal, context=context, max_steps=5, on_step=_on_step),
- timeout=150.0,
- )
- ai_output = result.get("output", "") if isinstance(result, dict) else str(result)
- except asyncio.TimeoutError:
- await _edit("⏱ AutoFix timeout — AI non ha risposto in 150s.\n\n"
- "Riprova con un hint più specifico.", final=True)
- return
- except Exception as exc:
- await _edit("❌ AutoFix — errore AI\n"
- + html.escape(str(exc)[:300]) + "", final=True)
- return
-
- # ── Step 3: parsa blocco autofix dall'output AI ───────────────────────────────────
- await _edit(
- "🔧 AutoFix\n\n⏳ Step 3/4 — Parsing patch…\n\n"
- "" + html.escape(ai_output.strip()[-300:]) + ""
- )
-
- import re as _re
- BLOCK_RE = _re.compile(
- r"```autofix\s*\nFILE:\s*(\S+)\s*\n---\s*\n([\s\S]*?)```",
- _re.MULTILINE,
- )
- patches: list[tuple[str, str]] = [
- (m.group(1).strip(), m.group(2))
- for m in BLOCK_RE.finditer(ai_output)
- if m.group(1).strip() not in ("", "UNKNOWN") and m.group(2).strip()
- ]
-
- if not patches:
- await _edit(
- "⚠️ AutoFix — nessuna patch parsata\n\n"
- "Output AI:\n" + html.escape(ai_output.strip()[:600]) + "\n\n"
- "L'AI non ha prodotto un blocco autofix valido.\n"
- "Prova: /autofix descrizione precisa del bug",
- final=True,
- )
- return
-
- # ── Step 4: push su GitHub via Git Data API ─────────────────────────────────────────
- gh_token = os.getenv("GITHUB_TOKEN", "")
- gh_repo = os.getenv("GITHUB_REPO", "Baida98/AI")
- gh_branch = os.getenv("GITHUB_BRANCH", "main")
-
- if not gh_token:
- await _edit(
- "❌ AutoFix — GITHUB_TOKEN non configurato\n\n"
- "Aggiungi GITHUB_TOKEN nelle Railway env vars.",
- final=True,
- )
- return
-
- await _edit(
- "🔧 AutoFix\n\n"
- f"⏳ Step 4/4 — Push {len(patches)} file su GitHub…\n\n"
- + "\n".join(f"• {p[0]}" for p in patches[:6])
- )
-
- gh_hdr = {
- "Authorization": f"token {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "Content-Type": "application/json",
- "User-Agent": "agente-ai-autofix",
- }
- gh_base = f"https://api.github.com/repos/{gh_repo}"
-
- try:
- async with httpx.AsyncClient(timeout=20.0) as c:
- r = await c.get(f"{gh_base}/git/refs/heads/{gh_branch}", headers=gh_hdr)
- r.raise_for_status()
- h_sha = r.json()["object"]["sha"]
-
- r = await c.get(f"{gh_base}/git/commits/{h_sha}", headers=gh_hdr)
- r.raise_for_status()
- t_sha = r.json()["tree"]["sha"]
-
- tree_items = []
- for fpath, content in patches:
- b64 = _b64.b64encode(content.encode()).decode()
- r = await c.post(f"{gh_base}/git/blobs", headers=gh_hdr,
- json={"content": b64, "encoding": "base64"})
- r.raise_for_status()
- tree_items.append({"path": fpath, "mode": "100644",
- "type": "blob", "sha": r.json()["sha"]})
-
- r = await c.post(f"{gh_base}/git/trees", headers=gh_hdr,
- json={"base_tree": t_sha, "tree": tree_items})
- r.raise_for_status()
- new_t = r.json()["sha"]
-
- err_summary = ("; ".join(rc.get("msg","")[:60] for rc in records[:2])
- or hint[:60] or "autofix via /autofix command")
- commit_msg = (
- f"fix(autofix): {err_summary}\n\n"
- f"Generato da /autofix — {len(patches)} file patchati\n"
- + "Patch: " + ", ".join(p[0] for p in patches)
- )
- r = await c.post(f"{gh_base}/git/commits", headers=gh_hdr,
- json={"message": commit_msg, "tree": new_t, "parents": [h_sha]})
- r.raise_for_status()
- c_sha = r.json()["sha"]
-
- r = await c.patch(f"{gh_base}/git/refs/heads/{gh_branch}", headers=gh_hdr,
- json={"sha": c_sha, "force": False})
- r.raise_for_status()
-
- files_list = "\n".join(f"• {p[0]}" for p in patches[:6])
- await _edit(
- "✅ AutoFix completato!\n\n"
- f"Commit: {c_sha[:10]}\n"
- f"Branch: {gh_branch}\n"
- f"File patchati:\n{files_list}\n\n"
- "U0001f680 Railway deploy: avviato automaticamente\n"
- f"U0001f517 Vedi commit",
- final=True,
- )
- _logger.info("autofix: pushed %s — %d files", c_sha[:10], len(patches))
-
- except Exception as exc:
- await _edit(
- "❌ AutoFix — push GitHub fallito\n"
- + html.escape(str(exc)[:400]) + "\n\n"
- "Verifica GITHUB_TOKEN nelle Railway env vars.",
- final=True,
- )
- _logger.error("autofix: push failed: %s", exc)
-
-
-async def _cmd_nota(chat_id: int, text: str) -> None:
- """Salva nota rapida nella memoria persistente dell'agente."""
- goal = (
- f"Salva questa nota nella tua memoria persistente usando il tool remember: "
- f"«{text}». Poi confermami con '✅ Nota salvata' e ripeti il testo della nota."
- )
- await _cmd_do(chat_id, goal)
-
-
-async def _cmd_cerca(chat_id: int, query: str) -> None:
- """Ricerca web + sintesi AI in italiano."""
- goal = (
- f"Cerca su web: {query}. "
- "Usa web_search e sintetizza i risultati più rilevanti in 4-5 bullet points concisi "
- "in italiano. Per ogni punto includi la fonte (dominio) tra parentesi."
- )
- await _cmd_do(chat_id, goal)
-
-
-async def _cmd_meteo(chat_id: int, city: str) -> None:
- """Meteo per città specifica via tool get_weather."""
- goal = (
- f"Usa il tool get_weather per {city} e dimmi: temperatura attuale, "
- "condizioni meteo, umidità, previsioni per le prossime ore. Risposta concisa in italiano."
- )
- await _cmd_do(chat_id, goal)
-
-
-async def _cmd_riepilogo(chat_id: int) -> None:
- """Briefing completo: task recenti + deploy + score + prossimi passi.
-
- GAP-TGB: usa benchmark_handler.get_smart_summary per briefing strutturato
- con analisi gap da ultimo report. Fallback: agent-loop se import fallisce.
- """
- try:
- from .benchmark_handler import get_smart_summary as _gsummary
- text = await _gsummary(chat_id)
- await _tg_reply(chat_id, text, keyboard=_BENCH_ACTION_KB)
- except Exception as _exc:
- _logger.debug("benchmark_handler fallback: %s", _exc)
- goal = (
- "Dammi un briefing completo dello stato attuale come assistente personale. "
- "Struttura ESATTA con questi header ## obbligatori:\n"
- "## 📋 Task recenti (usa recall per recuperare gli ultimi 5)\n"
- "## 🚀 Deploy status (CF Pages + Railway + HF Space — dati reali via tool)\n"
- "## 📊 Score AI (ultimo benchmark disponibile)\n"
- "## ⚠️ Problemi aperti (errori, timeout, regressioni)\n"
- "## ✅ Prossimi passi (3 priorità concrete)\n"
- "Usa i tool per dati reali — mai inventare status."
- )
- await _cmd_do(chat_id, goal)
-
-
-async def _cmd_scan_now(chat_id: int) -> None:
- """Health-check via health_full() — AI + Supabase + Telegram."""
- await _tg_reply(chat_id, "🔍 Scan in corso…")
- try:
- from .providers import health_full
- h = await health_full()
- ai = h.get("ai", {})
- sb = h.get("supabase", {})
- tg = h.get("telegram", {})
- bk = h.get("backend", {})
- ai_ok = "✅" if ai.get("ok") else "❌"
- sb_ok = "✅" if sb.get("ok") else "❌"
- tg_ok = "✅" if tg.get("ok") else "❌"
- over = "✅" if h.get("ok") else "⚠️"
- sb_msg = "ok" if sb.get("ok") else str(sb.get("error", "?"))[:40]
- tg_msg = ("@" + str(tg.get("username", ""))) if tg.get("username") else str(tg.get("error", "?"))[:40]
- avail = str(ai.get("available", 0)) + "/" + str(ai.get("total", 0))
- best = str(ai.get("best", "?"))
- ntasks = str(bk.get("active_tasks", 0))
- ms_str = str(h.get("elapsed_ms", "?"))
- st_str = str(h.get("status", "?"))
- parts = [
- over + " Health Check — " + st_str,
- "",
- ai_ok + " AI — " + avail + " provider | best: " + best,
- sb_ok + " Supabase — " + sb_msg,
- tg_ok + " Telegram — " + tg_msg,
- "📦 Task attivi: " + ntasks + " 🕒 " + ms_str + "ms",
- ]
- await _tg_reply(chat_id, "\n".join(parts), keyboard=_MAIN_KB)
- except Exception as exc:
- await _tg_reply(
- chat_id,
- "❌ Scan errore\n" + html.escape(str(exc)[:300]) + "",
- )
-
-
-async def _cmd_coord(chat_id: int) -> None:
- """🔗 /coord — sessioni agent-coord attive e file claimati (sola lettura).
-
- Legge la tabella Supabase 'agent_tasks' dove le sessioni agent-coord.mjs
- persistono con goal='__session__" + html.escape(str(exc)[:200]) + "")
- return
-
- SESSION_TTL_MS = 5 * 60 * 1000 # 5 minuti senza heartbeat = morta
- now_ms = int(_tc.time() * 1000)
- active, stale = [], []
- for row in rows:
- try:
- ctx = _jc.loads(row.get("context") or "{}")
- except Exception:
- continue
- hb = int(ctx.get("lastHeartbeat", 0))
- age_ms = now_ms - hb
- entry = {
- "name": ctx.get("sessionName", "?"),
- "sprint": ctx.get("sprint", "—"),
- "files": ctx.get("claimedFiles", []),
- "age_ms": age_ms,
- }
- (active if age_ms < SESSION_TTL_MS else stale).append(entry)
-
- if not rows:
- await _tg_reply(chat_id,
- "🔗 Agent Coord\n\n"
- "ℹ️ Nessuna sessione registrata.\nTutti i file sono liberi.",
- keyboard=_BACK_KB)
- return
-
- def _age_str(ms: int) -> str:
- s = ms // 1000
- return f"{s // 60}m{s % 60:02d}s" if s >= 60 else f"{s}s"
-
- out = ["🔗 Agent Coord", ""]
- if active:
- out.append(f"✅ Attive ({len(active)})")
- for s in active:
- fnames = ", ".join(
- "" + html.escape(f.split("/")[-1]) + ""
- for f in s["files"]
- ) or "nessun file"
- out.append(
- f" {html.escape(s['name'])} "
- f"[{html.escape(s['sprint'])}] · {_age_str(s['age_ms'])} fa"
- )
- out.append(f" 📎 {fnames}")
- else:
- out.append("✅ Nessuna sessione attiva — file liberi")
- if stale:
- out.append("")
- out.append(f"⚫ Inattive >5min ({len(stale)})")
- for s in stale:
- out.append(f" {html.escape(s['name'])} · {_age_str(s['age_ms'])} fa")
-
- await _tg_reply(chat_id, "\n".join(out), keyboard=_BACK_KB)
-
-
-async def _cmd_git(chat_id: int, n: int = 5) -> None:
- """🔀 /git [n] — ultimi N commit su main con SHA, messaggio, autore, eta e link diff.
-
- Chiama GitHub API (GITHUB_TOKEN Railway). Default: ultimi 5 commit.
- Max: 10. Timeout 6s. Silent fail se GITHUB_TOKEN assente.
- """
- import httpx as _hx_git2, datetime as _dt2
- _gh_token = os.getenv("GITHUB_TOKEN", "").strip()
- _gh_repo = os.getenv("GITHUB_REPO", "Baida98/AI").strip()
- if not _gh_token:
- await _tg_reply(chat_id,
- "⚠️ GITHUB_TOKEN non configurato su Railway — /git non disponibile.")
- return
- n = max(1, min(n, 10))
- await _tg_reply(chat_id, f"🔀 Git log — ultimi {n} commit…")
- try:
- async with _hx_git2.AsyncClient(timeout=6.0) as _gc:
- _gr = await _gc.get(
- f"https://api.github.com/repos/{_gh_repo}/commits",
- headers={"Authorization": f"Bearer {_gh_token}", "User-Agent": "agente-ai"},
- params={"sha": "main", "per_page": str(n)},
- )
- if _gr.status_code != 200:
- await _tg_reply(chat_id,
- f"❌ GitHub API {_gr.status_code}: {html.escape(_gr.text[:200])}")
- return
- commits = _gr.json() or []
- except Exception as exc:
- await _tg_reply(chat_id,
- "❌ " + html.escape(str(exc)[:200]) + "")
- return
-
- if not commits:
- await _tg_reply(chat_id, "ℹ️ Nessun commit trovato.", keyboard=_BACK_KB)
- return
-
- _now_utc = _dt2.datetime.now(_dt2.timezone.utc)
-
- def _age(iso: str) -> str:
- try:
- _ts = _dt2.datetime.fromisoformat(iso.replace("Z", "+00:00"))
- _secs = int((_now_utc - _ts).total_seconds())
- if _secs < 3600: return f"{_secs // 60}m fa"
- if _secs < 86400: return f"{_secs // 3600}h fa"
- if _secs < 604800: return f"{_secs // 86400}g fa"
- return _ts.strftime("%d/%m")
- except Exception:
- return "?"
-
- lines = [f"🔀 Git log — main ({_gh_repo})\n"]
- for c in commits:
- _sha = (c.get("sha") or "")[:7]
- _commit = c.get("commit") or {}
- _msg_raw = (_commit.get("message") or "").split("\n")[0][:52]
- _msg = html.escape(_msg_raw)
- _author = html.escape((_commit.get("author") or {}).get("name", "?")[:18])
- _date = (_commit.get("committer") or {}).get("date", "")
- _when = _age(_date)
- _url = f"https://github.com/{_gh_repo}/commit/{c.get('sha','')}"
- lines.append(
- f"{html.escape(_sha)} {_msg}\n"
- f" {_author} · {_when} "
- f'diff →'
- )
-
- await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
-
-async def _cmd_telemetry(chat_id: int) -> None:
- """📡 Metriche runtime live: /api/telemetry + /debug/timing da Railway."""
- import httpx as _hx_t
- rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
- await _tg_reply(chat_id, "⏳ Telemetria — interrogo Railway…")
- try:
- async with _hx_t.AsyncClient(timeout=8.0) as _c:
- tel_r, tim_r = await asyncio.gather(
- _c.get(f"{rw_url}/api/telemetry"),
- _c.get(f"{rw_url}/debug/timing"),
- return_exceptions=True,
- )
- except Exception as e:
- await _tg_reply(chat_id, f"❌ Railway non raggiungibile\n{html.escape(str(e)[:120])}", keyboard=_BACK_KB)
- return
-
- tel_d = tel_r.json() if not isinstance(tel_r, Exception) and tel_r.status_code == 200 else {}
- tim_d = tim_r.json() if not isinstance(tim_r, Exception) and tim_r.status_code == 200 else {}
-
- msg = "📡 Telemetria Runtime\n\n"
- timing = tel_d.get("timing", {})
- if timing:
- msg += "⏱ Latenze per fase (avg / p90 / n):\n"
- for phase, v in list(timing.items())[:10]:
- avg_v = v.get("avg", 0); p90_v = v.get("p90", 0); n_v = v.get("n", 0)
- icon = "🟢" if avg_v < 3000 else "🟡" if avg_v < 10000 else "🔴"
- msg += f"{icon} {phase[:18]:<18} {avg_v:>7}ms p90:{p90_v:>7}ms ×{n_v}\n"
- msg += "\n"
- else:
- msg += "⚠️ Nessun dato timing — backend idle o prima run.\n"
- repair = tel_d.get("repair", tim_d.get("repair_stats", {}))
- if repair:
- msg += "\n🔧 Quality & Repair:\n"
- for k, v in list(repair.items())[:10]:
- msg += f" {k[:22]:<22} {v:>6}\n"
- msg += "\n"
- ts_d = tim_d.get("timing_stats", {})
- if ts_d:
- msg += "\n📊 Breakdown /debug/timing:\n"
- for label, v in list(ts_d.items())[:8]:
- avg_v = v.get("avg") or 0; icon = "🟢" if avg_v < 3000 else "🟡" if avg_v < 10000 else "🔴"
- msg += f"{icon} {label[:18]:<18} {avg_v:>8}ms ×{v.get('count',0)}\n"
- msg += ""
- if len(msg) > 4000:
- msg = msg[:4000] + "…"
- await _tg_reply(chat_id, msg, keyboard=_BACK_KB)
-
-async def _cmd_score(chat_id: int) -> None:
- """🏆 Score card dettagliata — chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
- import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
- gh_token = os.getenv("GITHUB_TOKEN", "").strip()
- rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
- await _tg_reply(chat_id, "⏳ Score — carico report + metriche runtime…")
-
- report: dict | None = None
- if gh_token:
- try:
- async with _hx_sc.AsyncClient(timeout=8.0) as _c:
- _r = await _c.get(
- "https://api.github.com/repos/Baida98/AI/contents/benchmark-report.json?ref=main",
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r.status_code == 200:
- report = _j_sc.loads(_b64_sc.b64decode(_r.json()["content"]).decode())
- except Exception as _e:
- _logger.warning("cmd_score fetch: %s", _e)
-
- if not report:
- await _tg_reply(chat_id,
- "❌ Score — benchmark-report.json non trovato.\n"
- "Avvia prima /bench per generare i dati.", keyboard=_BENCH_ACTION_KB)
- return
-
- s = report.get("summary", {})
- tasks = report.get("tasks", [])
- gaps = report.get("gapCards", [])
- nodes = report.get("orchestrationNodeMap", {})
- ts = (report.get("timestamp") or "")[:16].replace("T", " ")
- seed = report.get("seed", "?")
- ver = report.get("version", "?")
- replay = report.get("replayCli", f"node benchmark-extended.mjs --seed {seed}")
- mft = s.get("mft")
- gap_cnt = s.get("gapCount", len(gaps))
- verdict = s.get("verdict", "").replace("_", " ")
- avg_ai = s.get("avgScore", 0)
- avg_rpl = s.get("avgReplit", 57.9)
- avg_cur = s.get("avgCursor", 64.1)
- avg_dev = s.get("avgDevin", 70.1)
- avg_mns = s.get("avgManus", 71.2)
- w_rpl = s.get("wins_replit", "?")
- w_dev = s.get("wins_devin", "?")
- w_mns = s.get("wins_manus", "?")
-
- # Runtime telemetria da Railway (best-effort)
- rt_timing: dict = {}
- rt_repair: dict = {}
- try:
- async with _hx_sc.AsyncClient(timeout=5.0) as _c:
- _tr = await _c.get(f"{rw_url}/api/telemetry")
- if _tr.status_code == 200:
- _td = _tr.json()
- rt_timing = _td.get("timing", {})
- rt_repair = _td.get("repair", {})
- except Exception:
- pass
-
- # ── Chart per categoria ───────────────────────────────────────
- cat_map: dict = {}
- for t in tasks:
- cat = (t.get("cat") or "other").replace("_", " ")[:14]
- cat_map.setdefault(cat, []).append(float(t.get("score", 0)))
- labels_ = list(cat_map.keys())
- scores_ = [round(sum(v)/len(v)) for v in cat_map.values()]
- colors_ = ["#4CAF50" if sc >= avg_rpl else "#FF9800" if sc >= 50 else "#F44336" for sc in scores_]
- chart_cfg = {
- "type": "horizontalBar",
- "data": {"labels": labels_, "datasets": [
- {"label": "Agente AI", "data": scores_, "backgroundColor": colors_, "borderWidth": 1},
- {"label": f"Replit {avg_rpl}",
- "data": [avg_rpl]*len(labels_), "type": "line",
- "borderColor": "#2196F3", "borderDash": [5,3], "pointRadius": 0, "fill": False, "borderWidth": 2},
- {"label": f"Devin {avg_dev}",
- "data": [avg_dev]*len(labels_), "type": "line",
- "borderColor": "#FF9800", "borderDash": [5,3], "pointRadius": 0, "fill": False, "borderWidth": 2},
- {"label": f"Manus {avg_mns}",
- "data": [avg_mns]*len(labels_), "type": "line",
- "borderColor": "#9C27B0", "borderDash": [5,3], "pointRadius": 0, "fill": False, "borderWidth": 2},
- ]},
- "options": {
- "title": {"display": True, "text": f"Agente AI {avg_ai}% | Replit {avg_rpl}% | Devin {avg_dev}% | Manus {avg_mns}%"},
- "scales": {"xAxes": [{"ticks": {"min": 0, "max": 100, "stepSize": 20}}]},
- "legend": {"display": True, "position": "bottom"},
- "plugins": {"datalabels": {"display": False}},
- },
- }
- chart_url = ("https://quickchart.io/chart?c=" +
- _ul_sc.quote(_j_sc.dumps(chart_cfg, separators=(",",":"))) +
- "&width=760&height=440&backgroundColor=white")
-
- # ── Caption messaggio 1 (foto, max 1024) ─────────────────────
- bar_g = "█" * round(avg_ai/10) + "░" * (10 - round(avg_ai/10))
- d_rpl = round(avg_ai - avg_rpl); s_rpl = ("+" if d_rpl >= 0 else "") + str(d_rpl)
- d_dev = round(avg_ai - avg_dev); s_dev = ("+" if d_dev >= 0 else "") + str(d_dev)
- d_mns = round(avg_ai - avg_mns); s_mns = ("+" if d_mns >= 0 else "") + str(d_mns)
- d_cur = round(avg_ai - avg_cur); s_cur = ("+" if d_cur >= 0 else "") + str(d_cur)
- caption = f"🏆 Score — {ts} UTC v{ver}\n"
- caption += f"{bar_g} {avg_ai}% {verdict}\n\n"
- caption += f"{'Modello':<10} {'Score':>5} {'Δ':>4} Wins\n"
- caption += f"{'Agente AI':<10} {str(avg_ai)+'%':>5} {'─':>4} ─\n"
- caption += f"{'Replit':<10} {str(avg_rpl)+'%':>5} {s_rpl:>4} {w_rpl}/10\n"
- caption += f"{'Cursor':<10} {str(avg_cur)+'%':>5} {s_cur:>4} ─\n"
- caption += f"{'Devin':<10} {str(avg_dev)+'%':>5} {s_dev:>4} {w_dev}/10\n"
- caption += f"{'Manus':<10} {str(avg_mns)+'%':>5} {s_mns:>4} {w_mns}/10\n\n"
- for cat, vals in sorted(cat_map.items(), key=lambda x: -sum(x[1])/len(x[1])):
- sc = round(sum(vals)/len(vals))
- bar = "█" * round(sc/10) + "░" * (10 - round(sc/10))
- d = round(sc - avg_rpl)
- vs = ("+" if d >= 0 else "") + str(d)
- icon = "✅" if sc >= avg_rpl else "⚠️" if sc >= 50 else "❌"
- line = f"{cat[:12]:<12} {bar} {sc:>3}% {vs:>4} {icon}\n"
- if len(caption) + len(line) < 1010:
- caption += line
- caption += f"\n🎲 Seed {seed} MFT {mft}s Gaps: {gap_cnt}"
- await _tg_photo(chat_id, chart_url, caption=caption[:1024], keyboard=_BENCH_ACTION_KB)
-
- # ── Messaggio 2 — dettaglio completo ─────────────────────────
- det = "📊 Score — Dettaglio\n\n"
-
- # Orchestration nodes
- NODE_ICONS = {"planner":"🧠","executor":"⚙️","reasoner":"🔬",
- "recovery_manager":"🛡","robustness_layer":"🔒","memory_module":"💾"}
- if nodes:
- det += "⚡ Orchestration Nodes:\n"
- for nk, nv in nodes.items():
- sr = str(nv.get("success_rate", "?"))
- lat = nv.get("avg_latency_s")
- ntsk = nv.get("tasks", "")
- try:
- icon = "✅" if float(sr.rstrip("%")) >= 60 else "⚠️" if float(sr.rstrip("%")) >= 30 else "❌"
- except Exception:
- icon = "❓"
- lat_s = f" {lat}s" if lat is not None else ""
- tsk_s = f" ×{ntsk}" if ntsk else ""
- det += f"{NODE_ICONS.get(nk,'•')} {nk[:20]:<20} {icon} {sr:>5}{lat_s}{tsk_s}\n"
- det += "\n"
-
- # Runtime telemetria (live da Railway)
- if rt_timing:
- det += "\n⏱ Runtime Latenze (live):\n"
- for phase, v in list(rt_timing.items())[:8]:
- avg_v = v.get("avg", 0); p90_v = v.get("p90", 0); n_v = v.get("n", 0)
- icon = "🟢" if avg_v < 3000 else "🟡" if avg_v < 10000 else "🔴"
- det += f"{icon} {phase[:16]:<16} avg:{avg_v:>7}ms p90:{p90_v:>7}ms ×{n_v}\n"
- det += "\n"
- if rt_repair:
- det += "🔧 Quality counters:\n"
- for k, v in list(rt_repair.items())[:6]:
- det += f" {k[:20]:<20} {v}\n"
- det += "\n"
- else:
- det += "ℹ️ Telemetria runtime non disponibile (Railway idle)\n"
-
- # Top 3 best + Top 3 worst
- sorted_tasks = sorted([t for t in tasks if t.get("score") is not None], key=lambda t: -t["score"])
- if sorted_tasks:
- det += "\n🥇 Migliori task:\n"
- for t in sorted_tasks[:3]:
- ref_r = (t.get("ref") or {}).get("replit", avg_rpl)
- d_ = round(t["score"] - ref_r)
- ds_ = ("+" if d_ >= 0 else "") + str(d_)
- ms_ = f" {round(t['agentMs']/1000)}s" if t.get("agentMs") else ""
- det += f" {(t.get('id') or '?'):>3} {t['score']}% ({ds_} vsRpl){ms_}\n"
- det += f" {(t.get('label') or '')[:60]}\n"
- det += "\n⚠️ Task critici:\n"
- for t in sorted_tasks[-3:]:
- ref_r = (t.get("ref") or {}).get("replit", avg_rpl)
- ref_m = (t.get("ref") or {}).get("manus", avg_mns)
- d_ = round(t["score"] - ref_r)
- dm_ = round(t["score"] - ref_m)
- ds_ = ("+" if d_ >= 0 else "") + str(d_)
- dms_ = ("+" if dm_ >= 0 else "") + str(dm_)
- det += f" {(t.get('id') or '?'):>3} {t['score']}% vsRpl {ds_} vsManus {dms_}\n"
- det += f" {(t.get('label') or '')[:60]}\n"
-
- # Gap cards top 3
- if gaps:
- det += "\n🔥 Gap da correggere:\n"
- for g in gaps[:3]:
- gicon = "🔴" if g.get("gravita") == "critical" else "🟡"
- det += f"{gicon} {(g.get('modulo_coinvolto') or '').replace('_',' ')} — {(g.get('causa_radice') or '')[:55]}\n"
- det += f" Fix: {(g.get('fix_immediato') or '')[:65]}\n"
- det += f" Atteso: {(g.get('beneficio_atteso') or '')[:40]}\n"
-
- det += f"\n{replay}"
- det += f"\n🌐 Dashboard →"
-
- _TG_MAX = 4000
- if len(det) <= _TG_MAX:
- await _tg_reply(chat_id, det, keyboard=_BENCH_ACTION_KB)
- else:
- await _tg_reply(chat_id, det[:_TG_MAX])
- await _tg_reply(chat_id, det[_TG_MAX:_TG_MAX*2][:_TG_MAX], keyboard=_BENCH_ACTION_KB)
-
-
-async def _cmd_bench(chat_id: int, mode: str = "default") -> None:
- """📊 Benchmark via bench.yml (benchmark-extended.mjs) + quickchart.io.
-
- GAP-TGB: workflow_dispatch su bench.yml — usa benchmark-extended.mjs
- (20 categorie, seed canonico 1337, tutte le fix v5).
- Risultati inviati via Telegram da ab-bench.mjs --notify al completamento.
- """
- gh_token = os.getenv("GITHUB_TOKEN", "").strip()
-
- # ── Tenta fetch ultimo run completato da GitHub Actions artifact ─────────
- last_report: dict | None = None
- if gh_token:
- try:
- import httpx as _hx
- async with _hx.AsyncClient(timeout=8.0) as _c:
- _r = await _c.get(
- "https://api.github.com/repos/Baida98/AI/actions/workflows/bench.yml/runs"
- "?status=completed&per_page=1",
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r.status_code == 200:
- _runs = _r.json().get("workflow_runs", [])
- if _runs:
- last_report = {
- "run_id": _runs[0]["id"],
- "run_url": _runs[0]["html_url"],
- "conclusion":_runs[0].get("conclusion","?"),
- "updated": _runs[0].get("updated_at",""),
- }
- except Exception as _exc:
- _logger.debug("bench fetch last run: %s", _exc)
-
- # ── Trigger nuovo run via workflow_dispatch ───────────────────────────────
- run_url = "https://github.com/Baida98/AI/actions/workflows/bench.yml"
- if gh_token:
- try:
- import httpx as _hx
- async with _hx.AsyncClient(timeout=10.0) as _c:
- _r = await _c.post(
- "https://api.github.com/repos/Baida98/AI/actions/workflows/bench.yml/dispatches",
- json={"ref": "main", "inputs": {
- "mode": mode,
- "run_improve": "false",
- "force_update_baseline": "false",
- }},
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r.status_code == 204:
- _logger.info("[bench] workflow_dispatch OK (mode=%s)", mode)
- # Attendi 2s e leggi il run ID appena creato
- await asyncio.sleep(2.0)
- async with _hx.AsyncClient(timeout=8.0) as _c2:
- _r2 = await _c2.get(
- "https://api.github.com/repos/Baida98/AI/actions/workflows/"
- "bench.yml/runs?per_page=1",
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r2.status_code == 200:
- _rr = _r2.json().get("workflow_runs", [])
- if _rr:
- run_url = _rr[0]["html_url"]
- else:
- _logger.warning("[bench] workflow_dispatch status=%d", _r.status_code)
- except Exception as _exc:
- _logger.warning("[bench] workflow_dispatch error: %s", _exc)
-
- # ── Costruisci messaggio con quickchart dell'ultimo run (se disponibile) ──
- _BENCH_CACHE[chat_id] = {"mode": mode, "run_url": run_url}
-
- # ── Fetch benchmark-report.json dal repo per quickchart reale ──────────────
- bench_report: dict | None = None
- if gh_token:
- try:
- import httpx as _hx, base64 as _b64, json as _json
- async with _hx.AsyncClient(timeout=8.0) as _c:
- _br = await _c.get(
- "https://api.github.com/repos/Baida98/AI/contents/benchmark-report.json?ref=main",
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _br.status_code == 200:
- _content = _b64.b64decode(_br.json()["content"]).decode()
- bench_report = _json.loads(_content)
- except Exception as _exc:
- _logger.debug("bench fetch benchmark-report.json: %s", _exc)
-
- chart_url: str | None = None
-
- def _build_quickchart(report: dict) -> str:
- """Costruisce URL quickchart.io da benchmark-report.json."""
- import json as _j, urllib.parse as _ul
- tasks = report.get("tasks", [])
- summary = report.get("summary", {})
- avg_ai = summary.get("avgScore", 0)
- avg_rpl = summary.get("avgReplit", 57.9)
- avg_mns = summary.get("avgManus", 71.2)
- cat_map: dict[str, list[float]] = {}
- for t in tasks:
- cat = (t.get("cat") or "other").replace("_", " ")[:14]
- cat_map.setdefault(cat, []).append(t.get("score", 0))
- if not cat_map:
- return ""
- labels = list(cat_map.keys())
- scores = [round(sum(v)/len(v)) for v in cat_map.values()]
- colors = ["#4CAF50" if s >= avg_rpl else "#FF9800" if s >= 50 else "#F44336" for s in scores]
- cfg = {
- "type": "horizontalBar",
- "data": {
- "labels": labels,
- "datasets": [
- {"label": "Agente AI", "data": scores,
- "backgroundColor": colors, "borderColor": colors, "borderWidth": 1},
- {"label": f"Replit {avg_rpl}",
- "data": [avg_rpl]*len(labels),
- "type": "line", "borderColor": "#2196F3", "borderDash": [5,3],
- "pointRadius": 0, "fill": False, "borderWidth": 2},
- {"label": f"Manus {avg_mns}",
- "data": [avg_mns]*len(labels),
- "type": "line", "borderColor": "#9C27B0", "borderDash": [5,3],
- "pointRadius": 0, "fill": False, "borderWidth": 2},
- ],
- },
- "options": {
- "title": {"display": True,
- "text": f"Agente AI {avg_ai}% | Replit {avg_rpl}% | Manus {avg_mns}%"},
- "scales": {"xAxes": [{"ticks": {"min": 0, "max": 100, "stepSize": 20}}]},
- "legend": {"display": True, "position": "bottom"},
- "plugins": {"datalabels": {"display": False}},
- },
- }
- return ("https://quickchart.io/chart?c=" +
- _ul.quote(_j.dumps(cfg, separators=(",",":"))) +
- "&width=720&height=420&backgroundColor=white")
-
- if bench_report:
- chart_url = _build_quickchart(bench_report)
-
- summary = (bench_report or {}).get("summary", {})
- avg_ai = summary.get("avgScore")
- avg_rpl = summary.get("avgReplit")
- avg_mns = summary.get("avgManus")
- # ── Tabella ASCII con barre per caption Telegram ──────────────────────────
- def _text_table_bench(report: dict, rpl: float) -> str:
- tasks = report.get("tasks", [])
- cat_map: dict[str, list[float]] = {}
- for t in tasks:
- cat = (t.get("cat") or "other").replace("_", " ")[:12]
- cat_map.setdefault(cat, []).append(float(t.get("score", 0)))
- if not cat_map:
- return ""
- rows = []
- for cat, vals in sorted(cat_map.items(), key=lambda x: -sum(x[1]) / len(x[1])):
- sc = round(sum(vals) / len(vals))
- bar = "█" * round(sc / 10) + "░" * (10 - round(sc / 10))
- delta_rpl = sc - rpl
- vs = ("+" if delta_rpl >= 0 else "") + str(round(delta_rpl)) + "vsRpl"
- rows.append(f"{cat:<12} {bar} {sc:>3}% {vs}")
- return "\n".join(rows)
-
- text_table = ""
- if bench_report and avg_rpl is not None:
- text_table = _text_table_bench(bench_report, float(avg_rpl))
-
- score_line = ""
- if avg_ai is not None:
- score_line = (
- f"\n📈 Score: AI {avg_ai}%"
- + (f" | Replit {avg_rpl}%" if avg_rpl else "")
- + (f" | Manus {avg_mns}%" if avg_mns else "")
- + "\n"
- )
-
- def _build_bench_caption(header: str) -> str:
- tbl = ("\n" + text_table + "") if text_table else ""
- link = f'\n🔗 GitHub Actions'
- full = header + score_line + tbl + link
- if len(full) > 1020 and text_table:
- avail = max(0, 1020 - len(header) - len(score_line) - len(link) - 14)
- tbl = "\n" + text_table[:avail] + "…"
- full = header + score_line + tbl + link
- return full[:1024]
-
- if last_report:
- _conclusion = last_report.get("conclusion", "?")
- _em = "✅" if _conclusion == "success" else ("❌" if _conclusion == "failure" else "⚠️")
- _upd = last_report.get("updated", "")[:16].replace("T", " ")
- header = f"📊 Benchmark avviato — {_em} {_conclusion}\n🕐 {_upd} UTC"
- else:
- header = "📊 Benchmark avviato (benchmark-extended.mjs)"
-
- caption = _build_bench_caption(header)
-
- if chart_url:
- await _tg_photo(chat_id, chart_url, caption=caption, keyboard=_BENCH_ACTION_KB)
- else:
- await _tg_reply(chat_id, caption, keyboard=_BENCH_ACTION_KB)
-
-
-
-# ── _cmd_improve — ciclo miglioramento: bench → gap → patch ─────────────────
-
-async def _cmd_improve(chat_id: int) -> None:
- """⚙️ Ciclo di miglioramento: bench.yml con --improve → gap → patch automatica.
-
- Triggera bench.yml con run_improve=true (benchmark-extended.mjs --improve).
- Il ciclo: few-shot retry sui task falliti → identifica pattern di miglioramento
- → genera regole → propone patch via notify Telegram.
- """
- gh_token = os.getenv("GITHUB_TOKEN", "").strip()
-
- await _tg_reply(chat_id,
- "⚙️ Ciclo miglioramento avviato\n\n"
- "• Avvio benchmark-extended.mjs --improve\n"
- "• Identifica gap · genera regole migliorate · propone patch\n"
- "Riceverai notifica Telegram al completamento (~10-25 min).",
- keyboard=_BACK_KB)
-
- run_url = "https://github.com/Baida98/AI/actions/workflows/bench.yml"
- if not gh_token:
- await _tg_reply(chat_id,
- "⚠️ GITHUB_TOKEN non configurato — impossibile avviare workflow.",
- keyboard=_BACK_KB)
- return
-
- try:
- import httpx as _hx
- async with _hx.AsyncClient(timeout=10.0) as _c:
- _r = await _c.post(
- "https://api.github.com/repos/Baida98/AI/actions/workflows/bench.yml/dispatches",
- json={"ref": "main", "inputs": {
- "mode": "default",
- "run_improve": "true",
- "force_update_baseline": "false",
- }},
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r.status_code == 204:
- _logger.info("[improve] workflow_dispatch OK")
- await asyncio.sleep(2.0)
- async with _hx.AsyncClient(timeout=8.0) as _c2:
- _r2 = await _c2.get(
- "https://api.github.com/repos/Baida98/AI/actions/workflows/"
- "bench.yml/runs?per_page=1",
- headers={"Authorization": f"Bearer {gh_token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "AgenteAI-Bot"},
- )
- if _r2.status_code == 200:
- _rr = _r2.json().get("workflow_runs", [])
- if _rr:
- run_url = _rr[0]["html_url"]
- await _tg_reply(chat_id,
- f"✅ Ciclo miglioramento avviato\n"
- f'🔗 Segui su GitHub Actions',
- keyboard=_BENCH_ACTION_KB)
- else:
- await _tg_reply(chat_id,
- f"⚠️ GitHub API status {_r.status_code} — riprova tra poco.",
- keyboard=_BACK_KB)
- except Exception as _exc:
- _logger.warning("[improve] error: %s", _exc)
- await _tg_reply(chat_id,
- f"❌ Errore: {html.escape(str(_exc)[:150])}",
- keyboard=_BACK_KB)
-
-
-# ── callback_query handler — inline buttons da notify_task_done ───────────────
-
-async def _handle_inline(iq: dict, token: str) -> None:
- """Gestisce inline query: @ARJagent_ap_bot " + html.escape(tid) + "\n"
- "Stato: " + em + " " + st + "\n"
- "Goal: " + gol + "\n"
- "Avviato: " + age,
- token=token,
- )
- return
- await _tg_reply(chat_id, "⚠️ Task " + html.escape(tid) + " non trovato in Supabase.", token=token)
- except Exception as exc:
- await _tg_reply(chat_id, "❌ Errore: " + html.escape(str(exc)[:200]), token=token)
-
- # ── task_wins:" + html.escape(tid) + "\n\n"
- "I quick wins vengono inclusi nella notifica del task.\n"
- "Avvia /tasks per vedere i task recenti o usa il pannello web per dettagli completi.\n\n"
- "Apri Dashboard →",
- token=token,
- )
-
- # ── agent — nuovo task (quick-pick) ──────────────────────────────────────
- elif data == "agent":
- await _tg_reply(
- chat_id,
- "🚀 Avvia Task — scegli un template o scrivi il tuo obiettivo:",
- token=token, keyboard=_QUICK_PICK_KB,
- )
-
- # ── tgw_status / tgw_tasks / tgw_health / tgw_help — menu comandi ────────
- elif data == "tgw_chart":
- await _tg_reply(chat_id,
- "📈 Grafici\n\nHeatmap commit, burndown sprint e molto altro nella Dashboard:",
- token=token, keyboard=_WEBAPP_KB)
- elif data == "tgw_ask":
- await _tg_reply(chat_id,
- "🧠 Chiedi all'AI\n\n"
- "Scrivi la domanda direttamente o usa:\n"
- "/ask <domanda>\n\n"
- "Esempio: /ask come ottimizzare una query PostgreSQL?",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_bench":
- asyncio.create_task(_cmd_bench(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_autofix":
- asyncio.create_task(_cmd_autofix(chat_id,"")).add_done_callback(_log_tg_exc)
- elif data == "tgw_logs":
- asyncio.create_task(_cmd_logs(chat_id,"WARNING")).add_done_callback(_log_tg_exc)
- elif data == "tgw_bench_fix":
- # Legge gap dall'ultimo run (cache) e chiede all'agente un fix mirato
- cached = _BENCH_CACHE.get(chat_id, {})
- goal = (
- "Analizza i gap del benchmark-extended (bench.yml) più recente. "
- "Identifica le categorie con score più basso e genera patch mirate "
- "(prompt rules, retry logic, tool selection) per migliorare ogni area debole. "
- "Priorità: agentic → coding → reasoning. Fai push delle modifiche su GitHub."
- )
- asyncio.create_task(_cmd_do(chat_id, goal)).add_done_callback(_log_tg_exc)
- elif data == "tgw_bench_run":
- # Rilancia bench
- asyncio.create_task(_cmd_bench(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_score":
- asyncio.create_task(_cmd_score(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_telemetry":
- asyncio.create_task(_cmd_telemetry(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_improve":
- asyncio.create_task(_cmd_improve(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_status":
- await _cmd_status(chat_id)
- elif data == "tgw_tasks":
- await _cmd_tasks(chat_id)
- elif data == "tgw_health":
- await _cmd_scan_now(chat_id)
- elif data == "tgw_check":
- asyncio.create_task(_cmd_check(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_help":
- await _cmd_help(chat_id)
- elif data == "tgw_coord":
- asyncio.create_task(_cmd_coord(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_git":
- asyncio.create_task(_cmd_git(chat_id)).add_done_callback(_log_tg_exc)
-
- # ── Sub-menu dispatchers (Reply Keyboard → inline sub-menu) ─────────────────
- elif data in ("menu_task",):
- await _tg_reply(chat_id, "🚀 Task AI — scegli un'azione:", token=token, keyboard=_TASK_MENU_KB)
- elif data == "menu_status":
- await _cmd_status(chat_id)
- await _tg_reply(chat_id, "📊 Stato Sistema — altre opzioni:", token=token, keyboard=_STATUS_MENU_KB)
- elif data == "menu_perf":
- await _tg_reply(chat_id, "📈 Performance — scegli:", token=token, keyboard=_PERF_MENU_KB)
- elif data == "menu_health":
- asyncio.create_task(_cmd_scan_now(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "menu_dev":
- await _tg_reply(chat_id, "🛠 Dev Tools — scegli:", token=token, keyboard=_DEV_MENU_KB)
- elif data == "tgw_home":
- asyncio.create_task(_cmd_help(chat_id)).add_done_callback(_log_tg_exc)
-
- # ── Nuovi callback task/utility ────────────────────────────────────────────
- elif data == "tgw_do":
- await _tg_reply(chat_id,
- "🚀 Nuovo Task AI\n\n"
- "Scrivi il tuo obiettivo nella chat — elaboro tutto come task AI.\n\n"
- "Esempi:\n"
- " analizza bug in backend/api/state.py\n"
- " genera test per providers.py\n"
- " ottimizza le query Supabase più lente\n\n"
- "Oppure: /do <goal>",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_briefing":
- asyncio.create_task(_cmd_riepilogo(chat_id)).add_done_callback(_log_tg_exc)
- elif data == "tgw_nota":
- await _tg_reply(chat_id,
- "📝 Salva Nota\n\n"
- "Usa: /nota <testo da ricordare>\n\n"
- "Esempio: /nota domani rivedere il deploy HF Space",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_cerca":
- await _tg_reply(chat_id,
- "🔍 Cerca sul web\n\n"
- "Usa: /cerca <query>\n\n"
- "Esempio: /cerca best practices FastAPI async Python",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_meteo":
- await _tg_reply(chat_id,
- "🌤 Meteo\n\n"
- "Usa: /meteo <città>\n\n"
- "Esempio: /meteo Milano",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_providers":
- try:
- from api.providers import _heartbeat_state as _hs_pv
- providers = (_hs_pv or {}).get("providers") or []
- if providers:
- lines = ["🔌 Provider AI\n"]
- best = next((p for p in providers if p.get("ok")), None)
- for p in providers:
- ok = p.get("ok", False)
- icon = "✅" if ok else ("🔑" if "401" in str(p.get("error","")) else ("💳" if "429" in str(p.get("error","")) else "❌"))
- ms = p.get("latency_ms")
- ms_str = f"{ms}ms" if ms else "–"
- name = p.get("name","?")
- star = " ⭐" if (best and p.get("name") == best.get("name")) else ""
- lines.append(f"{icon} {html.escape(name)}{star} — {ms_str}")
- if not ok and p.get("error"):
- lines.append(f" {html.escape(str(p['error'])[:80])}")
- import time as _tpv
- last = (_hs_pv or {}).get("last_run_at",0)
- age = f"{int(_tpv.time()-last)}s fa" if last else "?"
- lines.append(f"\n⏱ Heartbeat {age}")
- await _tg_reply(chat_id, "\n".join(lines), token=token, keyboard=_HEALTH_MENU_KB)
- else:
- await _tg_reply(chat_id,
- "⚠️ Dati provider non ancora disponibili (~90s al primo heartbeat).",
- token=token, keyboard=_BACK_KB)
- except Exception as exc:
- await _tg_reply(chat_id,
- "❌ Errore: " + html.escape(str(exc)[:200]) + "",
- token=token, keyboard=_BACK_KB)
- elif data == "tgw_snap":
- try:
- from .integrity_manager import handle_snap_cmd as _hi_snap
- asyncio.create_task(_hi_snap(chat_id, _tg_reply, None)).add_done_callback(_log_tg_exc)
- except Exception:
- await _tg_reply(chat_id, "⚠️ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
- elif data == "tgw_verify":
- try:
- from .integrity_manager import handle_verify_cmd as _hi_verify
- asyncio.create_task(_hi_verify(chat_id, _tg_reply, None)).add_done_callback(_log_tg_exc)
- except Exception:
- await _tg_reply(chat_id, "⚠️ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
- elif data == "tgw_heal":
- try:
- from .integrity_manager import handle_heal_cmd as _hi_heal
- asyncio.create_task(_hi_heal(chat_id, _tg_reply, None)).add_done_callback(_log_tg_exc)
- except Exception:
- await _tg_reply(chat_id, "⚠️ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
- elif data == "tgw_ping":
- ry = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
- try:
- async with httpx.AsyncClient(timeout=8.0) as _hxc:
- r = await _hxc.get(f"{ry}/health")
- j = r.json()
- ok = "✅" if j.get("status")=="ok" else "⚠️"
- await _tg_reply(chat_id,
- f"🏓 Pong! {ok}\nv{j.get('version','?')} — Railway live",
- token=token, keyboard=_DEV_MENU_KB)
- except Exception as exc:
- await _tg_reply(chat_id,
- "❌ Railway non raggiungibile\n"+html.escape(str(exc)[:150])+"",
- token=token, keyboard=_BACK_KB)
-
- # ── Quick-pick task templates (MX-QUICKPICK) ─────────────────────────────
- elif data == "qp_bug":
- asyncio.create_task(_cmd_do(chat_id,
- "Leggi i log di Railway/HF degli ultimi 30 minuti. Identifica gli errori principali, "
- "trova la root cause reale e suggerisci un fix concreto con codice.")).add_done_callback(_log_tg_exc)
- elif data == "qp_db":
- asyncio.create_task(_cmd_do(chat_id,
- "Analizza le query Supabase più lente del progetto (agent_tasks, agent_logs, semantic_memory). "
- "Identifica query N+1, indici mancanti, e proponi le ottimizzazioni con SQL concreto.")).add_done_callback(_log_tg_exc)
- elif data == "qp_autofix":
- asyncio.create_task(_cmd_autofix(chat_id, "")).add_done_callback(_log_tg_exc)
- elif data == "qp_commits":
- asyncio.create_task(_cmd_do(chat_id,
- "Leggi gli ultimi 10 commit su Baida98/AI via GitHub API e produci un riepilogo in italiano: "
- "cosa è stato fatto, da chi, e qual è lo stato attuale del progetto.")).add_done_callback(_log_tg_exc)
- elif data == "qp_docs":
- asyncio.create_task(_cmd_do(chat_id,
- "Genera la documentazione degli endpoint API principali del backend Railway: "
- "/api/telegram, /api/agent, /health. Per ognuno: metodo HTTP, parametri, risposta attesa, esempi.")).add_done_callback(_log_tg_exc)
- elif data == "qp_tests":
- asyncio.create_task(_cmd_do(chat_id,
- "Genera test pytest per i moduli critici del backend: "
- "api/telegram_webhook.py, agents/unified_loop.py, providers. "
- "Priorità: test di integrazione per i path più usati e i casi di errore.")).add_done_callback(_log_tg_exc)
- elif data == "qp_custom":
- await _tg_reply(chat_id,
- "✍️ Scrivi il tuo obiettivo\n"
- "Scrivi liberamente nella chat — lo eseguo come task AI.\n"
- "Esempi:\n"
- " ottimizza le query Supabase lente\n"
- " analizza providers.py e suggerisci fix",
- token=token, keyboard=_BACK_KB)
- # ── tgw_retry — rilancia l'ultimo task ───────────────────────────────────
- elif data == "tgw_retry":
- last = _LAST_GOAL.get(chat_id)
- if last:
- asyncio.create_task(_cmd_do(chat_id, last)).add_done_callback(_log_tg_exc)
- else:
- await _tg_reply(chat_id,
- "⚠️ Nessun task precedente da ripetere. Avvia un nuovo task:",
- token=token, keyboard=_QUICK_PICK_KB)
-
- # ── m — menu principale ───────────────────────────────────────────────────
- elif data == "m":
- await _tg_reply(
- chat_id,
- "🤖 Menu principale\n\nScegli un'azione:",
- token=token,
- keyboard=_MAIN_KB,
- )
-
- # ── fallback — command non gestito ───────────────────────────────────────
- else:
- _logger.debug("callback_query unhandled: %s", data)
-
-
# ── Webhook endpoint (passivo — getUpdates polling attivo nel daemon) ─────────
@router.post("/webhook")