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/.py\n" - + "---\n" - + "\n" - + BT3 + "\n\n" - "Regole:\n" - "1. Scrivi SOLO il blocco con FILE: e contenuto completo (non un diff)\n" - "2. Per piu' file includi un blocco per file\n" - "3. Se non riesci a determinare il file, scrivi FILE: UNKNOWN e spiega" - ) - context = f"Backend: {railway_url} Repo: {os.getenv('GITHUB_REPO','Baida98/AI')}" - - _buf: list[str] = [] - _last: list[float] = [0.0] - - async def _on_step(event: dict) -> None: - if event.get("action") == "text_chunk": - tok = event.get("token", "") - if tok: - _buf.append(tok) - now = time.monotonic() - if now - _last[0] > 2.0: - preview = "".join(_buf).strip()[-300:] - await _edit( - "🔧 AutoFix — Step 2/4 AI analizza…\n\n" - "" + 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__'. TTL = 5 minuti senza heartbeat. - Non modifica nulla — pura lettura. Non interferisce con il lavoro agente. - """ - import httpx as _hx_c, time as _tc, json as _jc - SB_URL = os.getenv("SUPABASE_URL", "https://zwdoplodbdsxfrddoxmo.supabase.co").rstrip("/") - SB_KEY = os.getenv("SUPABASE_KEY", "") - if not SB_KEY: - await _tg_reply(chat_id, "⚠️ SUPABASE_KEY non configurata su Railway.") - return - await _tg_reply(chat_id, "🔗 Agent Coord — lettura sessioni…") - try: - async with _hx_c.AsyncClient(timeout=8.0) as _c: - r = await _c.get( - f"{SB_URL}/rest/v1/agent_tasks", - params={"goal": "ilike.*__session__*", "select": "goal,context,updated_at"}, - headers={ - "apikey": SB_KEY, - "Authorization": f"Bearer {SB_KEY}", - "Content-Type": "application/json", - }, - ) - if not r.is_success: - await _tg_reply(chat_id, f"❌ Supabase {r.status_code}: {r.text[:200]}") - return - rows = r.json() or [] - except Exception as exc: - await _tg_reply(chat_id, - "❌ " + 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 in qualsiasi chat. - Abilita tramite @BotFather → /setinline → ARJagent_ap_bot. - """ - bot_token = token or _get_bot_token() - if not bot_token: return - iq_id = iq.get("id","") - query = (iq.get("query") or "").strip() - q60 = query[:60] - if not query: - results = [ - {"type":"article","id":"help","title":"🤖 Comandi Agente AI", - "description":"Lista tutti i comandi", - "input_message_content":{"message_text": - "🤖 Agente AI\n/do <goal> — task streaming\n/ask <q> — risposta veloce\n" - "/autofix — fix errori auto\n/status — stato\n\nagente-ai.pages.dev","parse_mode":"HTML"}}, - {"type":"article","id":"do","title":"🚀 Nuovo Task AI", - "description":"/do — lancia task complesso", - "input_message_content":{"message_text":"/do "}}, - {"type":"article","id":"ask","title":"🧠 Domanda AI", - "description":"/ask ", - "input_message_content":{"message_text":"/ask "}}, - ] - else: - results = [ - {"type":"article","id":"ask","title":f"🧠 Chiedi: {q60}", - "description":f"/ask {q60}", - "input_message_content":{"message_text":f"/ask {query}"}}, - {"type":"article","id":"do","title":f"🚀 Task: {q60}", - "description":f"/do {q60}", - "input_message_content":{"message_text":f"/do {query}"}}, - {"type":"article","id":"fix","title":f"🔧 AutoFix: {q60}", - "description":f"/autofix {q60}", - "input_message_content":{"message_text":f"/autofix {query}"}}, - ] - try: - import httpx as _hx - async with _hx.AsyncClient(timeout=5.0) as c: - await c.post(f"https://api.telegram.org/bot{bot_token}/answerInlineQuery", - json={"inline_query_id":iq_id,"results":results,"cache_time":30,"is_personal":True}) - except Exception as exc: - _logger.debug("inline answer error: %s", exc) - - -async def _handle_callback(callback_query: dict, token: str) -> None: - """Gestisce i callback_data dei bottoni inline (notify_task_done, menu).""" - cq_id = callback_query.get("id", "") - chat_id = callback_query.get("message", {}).get("chat", {}).get("id", 0) - data = (callback_query.get("data") or "").strip() - - # Risponde subito per chiudere il loading Telegram - await _tg_answer_callback(cq_id, token=token) - - if not chat_id or not data: - return - - # ── task_sum: — riepilogo task da Supabase ────────────────────────── - if data.startswith("task_sum:"): - tid = data[9:].strip() - 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") - .eq("task_id", tid) - .limit(1) - .execute() - ) - rows = res.data or [] - if rows: - r = rows[0] - st = r.get("status", "?") - gol = html.escape((r.get("goal") or "")[:200]) - em = {"SUCCESS":"✅","ERROR":"❌","RUNNING":"⚙️","QUEUED":"⏳"}.get(st,"•") - ca = r.get("created_at", 0) - age = _fmt_elapsed(ca) if isinstance(ca, int) and ca > 1_000_000 else "?" - await _tg_reply( - chat_id, - "📋 Riepilogo task\n\n" - "ID: " + 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: — quick wins (non persisti — riepilogo generico) ───── - elif data.startswith("task_wins:"): - tid = data[10:].strip() - await _tg_reply( - chat_id, - "💡 Quick 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")