Spaces:
Running
Running
sync: 154 file da Baida98/AI@a284c7fa (2026-08-15 11:54 UTC) [deploy-all]
#35
by Baida07 - opened
- api/telegram_webhook.py +127 -146
- tests/test_telegram_reply_transport.py +37 -1
api/telegram_webhook.py
CHANGED
|
@@ -44,70 +44,89 @@ def _get_bot_token() -> str:
|
|
| 44 |
return os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
|
| 45 |
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
bot_token = token or _get_bot_token()
|
| 51 |
if not bot_token:
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
try:
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
gateway_secret = os.getenv("TELEGRAM_REPLY_PROXY_SECRET", "").strip()
|
| 66 |
-
if gateway_url and gateway_secret:
|
| 67 |
-
request_url = gateway_url
|
| 68 |
-
request_headers = {"Authorization": f"Bearer {gateway_secret}"}
|
| 69 |
-
else:
|
| 70 |
-
# CompatibilitΓ per ambienti che non hanno ancora il gateway Pages.
|
| 71 |
-
request_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
| 72 |
-
request_headers = {}
|
| 73 |
-
# Il gateway isola l'egress Telegram dal runtime HF, dove la connessione
|
| 74 |
-
# diretta puΓ² scadere. trust_env evita proxy ambiente non necessari.
|
| 75 |
-
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as c:
|
| 76 |
-
response = await c.post(request_url, headers=request_headers, json=payload)
|
| 77 |
try:
|
| 78 |
data = response.json()
|
| 79 |
except ValueError:
|
| 80 |
data = {}
|
| 81 |
if response.status_code >= 400 or not data.get("ok", False):
|
| 82 |
detail = str(data.get("description") or data.get("error") or response.text[:160] or "unknown")
|
| 83 |
-
_logger.warning("
|
|
|
|
|
|
|
| 84 |
except Exception as exc:
|
| 85 |
detail = str(exc) or repr(exc)
|
| 86 |
-
_logger.warning("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
async def _tg_answer_callback(callback_query_id: str, text: str = "", token: str | None = None) -> None:
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
if not bot_token:
|
| 93 |
return
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
)
|
| 101 |
-
except Exception as exc:
|
| 102 |
-
_logger.debug("answer_callback error: %s", exc)
|
| 103 |
|
| 104 |
|
| 105 |
async def _tg_send(chat_id: str | int, text: str, token: str | None = None,
|
| 106 |
keyboard: dict | None = None) -> str | None:
|
| 107 |
-
"""Invia messaggio e
|
| 108 |
-
bot_token = token or _get_bot_token()
|
| 109 |
-
if not bot_token:
|
| 110 |
-
return None
|
| 111 |
payload: dict = {
|
| 112 |
"chat_id": chat_id,
|
| 113 |
"text": text,
|
|
@@ -116,26 +135,15 @@ async def _tg_send(chat_id: str | int, text: str, token: str | None = None,
|
|
| 116 |
}
|
| 117 |
if keyboard:
|
| 118 |
payload["reply_markup"] = keyboard
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
r = await c.post(
|
| 123 |
-
f"https://api.telegram.org/bot{bot_token}/sendMessage",
|
| 124 |
-
json=payload,
|
| 125 |
-
)
|
| 126 |
-
j = r.json()
|
| 127 |
-
return str(j.get("result", {}).get("message_id", "")) if j.get("ok") else None
|
| 128 |
-
except Exception as exc:
|
| 129 |
-
_logger.warning("tg_send error: %s", exc)
|
| 130 |
-
return None
|
| 131 |
|
| 132 |
|
| 133 |
async def _tg_edit(chat_id: str | int, message_id: str, text: str,
|
| 134 |
token: str | None = None, keyboard: dict | None = None) -> bool:
|
| 135 |
-
"""Aggiorna messaggio
|
| 136 |
-
|
| 137 |
-
bot_token = token or _get_bot_token()
|
| 138 |
-
if not bot_token or not message_id:
|
| 139 |
return False
|
| 140 |
payload: dict = {
|
| 141 |
"chat_id": chat_id,
|
|
@@ -146,17 +154,7 @@ async def _tg_edit(chat_id: str | int, message_id: str, text: str,
|
|
| 146 |
}
|
| 147 |
if keyboard:
|
| 148 |
payload["reply_markup"] = keyboard
|
| 149 |
-
|
| 150 |
-
import httpx
|
| 151 |
-
async with httpx.AsyncClient(timeout=8.0) as c:
|
| 152 |
-
r = await c.post(
|
| 153 |
-
f"https://api.telegram.org/bot{bot_token}/editMessageText",
|
| 154 |
-
json=payload,
|
| 155 |
-
)
|
| 156 |
-
return r.json().get("ok", False)
|
| 157 |
-
except Exception as exc:
|
| 158 |
-
_logger.debug("tg_edit error: %s", exc)
|
| 159 |
-
return False
|
| 160 |
|
| 161 |
|
| 162 |
async def _tg_photo(
|
|
@@ -166,78 +164,66 @@ async def _tg_photo(
|
|
| 166 |
token: str | None = None,
|
| 167 |
keyboard: dict | None = None,
|
| 168 |
) -> None:
|
| 169 |
-
"""Invia
|
| 170 |
-
|
| 171 |
-
Strategia anti URL-lungo:
|
| 172 |
-
1. POST a quickchart.io β scarica PNG bytes β multipart sendPhoto (no limite URL).
|
| 173 |
-
2. Fallback: invia URL direttamente (funziona se URL < ~2000 chars).
|
| 174 |
-
"""
|
| 175 |
bot_token = token or _get_bot_token()
|
| 176 |
if not bot_token:
|
| 177 |
return
|
| 178 |
caption_safe = (caption or "")[:1024]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
import httpx as _hx_p, json as _j_p, urllib.parse as _ul_p, re as _re_p
|
| 181 |
-
|
| 182 |
png_bytes: bytes | None = None
|
| 183 |
if "quickchart.io/chart" in photo_url:
|
| 184 |
try:
|
| 185 |
-
|
| 186 |
-
if
|
| 187 |
-
cfg_dict = _j_p.loads(_ul_p.unquote(
|
| 188 |
-
async with _hx_p.AsyncClient(timeout=20.0) as
|
| 189 |
-
|
| 190 |
"https://quickchart.io/chart",
|
| 191 |
json={"chart": cfg_dict, "width": 720, "height": 420,
|
| 192 |
"backgroundColor": "white", "format": "png"},
|
| 193 |
)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
_logger.debug("tg_photo: quickchart POST ok, %d bytes", len(png_bytes))
|
| 197 |
except Exception as exc:
|
| 198 |
-
_logger.debug("tg_photo
|
| 199 |
|
| 200 |
try:
|
| 201 |
-
|
| 202 |
-
async with _hx_s.AsyncClient(timeout=15.0) as c:
|
| 203 |
if png_bytes:
|
| 204 |
-
import json as
|
| 205 |
data: dict = {"chat_id": str(chat_id), "parse_mode": "HTML"}
|
| 206 |
if caption_safe:
|
| 207 |
data["caption"] = caption_safe
|
| 208 |
if keyboard:
|
| 209 |
-
data["reply_markup"] =
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
| 213 |
else:
|
| 214 |
-
payload
|
| 215 |
if caption_safe:
|
| 216 |
payload["caption"] = caption_safe
|
| 217 |
if keyboard:
|
| 218 |
payload["reply_markup"] = keyboard
|
| 219 |
-
await
|
| 220 |
except Exception as exc:
|
| 221 |
_logger.warning("tg_photo error: %s", exc)
|
| 222 |
|
| 223 |
|
| 224 |
async def _tg_typing(chat_id: str | int, action: str = "typing", token: str | None = None) -> None:
|
| 225 |
-
"""
|
| 226 |
-
|
| 227 |
-
Dura 5 secondi o fino al prossimo messaggio del bot.
|
| 228 |
-
Azioni: typing, upload_photo, upload_document, find_location, record_video_note.
|
| 229 |
-
"""
|
| 230 |
-
bot_token = token or _get_bot_token()
|
| 231 |
-
if not bot_token:
|
| 232 |
-
return
|
| 233 |
-
try:
|
| 234 |
-
async with httpx.AsyncClient(timeout=3.0) as c:
|
| 235 |
-
await c.post(
|
| 236 |
-
f"https://api.telegram.org/bot{bot_token}/sendChatAction",
|
| 237 |
-
json={"chat_id": chat_id, "action": action},
|
| 238 |
-
)
|
| 239 |
-
except Exception:
|
| 240 |
-
pass
|
| 241 |
|
| 242 |
|
| 243 |
async def _tg_react(
|
|
@@ -246,26 +232,19 @@ async def _tg_react(
|
|
| 246 |
emoji: str = "π",
|
| 247 |
token: str | None = None,
|
| 248 |
) -> None:
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
Emoji supportate: π π β€ π₯ π₯° π π π€ π€― π± π π€© π β
π― β‘ π π―
|
| 252 |
-
"""
|
| 253 |
-
bot_token = token or _get_bot_token()
|
| 254 |
-
if not bot_token or not message_id:
|
| 255 |
return
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
except Exception:
|
| 268 |
-
pass
|
| 269 |
|
| 270 |
|
| 271 |
def _fmt_elapsed(created_at_ms: int) -> str:
|
|
@@ -1411,8 +1390,9 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1411 |
"""π Score card dettagliata β chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
|
| 1412 |
import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
|
| 1413 |
gh_token = os.getenv("GITHUB_TOKEN", "").strip()
|
| 1414 |
-
|
| 1415 |
-
|
|
|
|
| 1416 |
|
| 1417 |
report: dict | None = None
|
| 1418 |
if gh_token:
|
|
@@ -1460,7 +1440,8 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1460 |
rt_repair: dict = {}
|
| 1461 |
try:
|
| 1462 |
async with _hx_sc.AsyncClient(timeout=5.0) as _c:
|
| 1463 |
-
|
|
|
|
| 1464 |
if _tr.status_code == 200:
|
| 1465 |
_td = _tr.json()
|
| 1466 |
rt_timing = _td.get("timing", {})
|
|
@@ -1507,7 +1488,8 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1507 |
d_dev = round(avg_ai - avg_dev); s_dev = ("+" if d_dev >= 0 else "") + str(d_dev)
|
| 1508 |
d_mns = round(avg_ai - avg_mns); s_mns = ("+" if d_mns >= 0 else "") + str(d_mns)
|
| 1509 |
d_cur = round(avg_ai - avg_cur); s_cur = ("+" if d_cur >= 0 else "") + str(d_cur)
|
| 1510 |
-
caption = f"π <b>Score</b> β {ts} UTC <code>v{ver}</code>\n"
|
|
|
|
| 1511 |
caption += f"<code>{bar_g}</code> <b>{avg_ai}%</b> {verdict}\n\n"
|
| 1512 |
caption += f"<code>{'Modello':<10} {'Score':>5} {'Ξ':>4} Wins</code>\n"
|
| 1513 |
caption += f"<code>{'Agente AI':<10} {str(avg_ai)+'%':>5} {'β':>4} β</code>\n"
|
|
@@ -1528,13 +1510,13 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1528 |
await _tg_photo(chat_id, chart_url, caption=caption[:1024], keyboard=_BENCH_ACTION_KB)
|
| 1529 |
|
| 1530 |
# ββ Messaggio 2 β dettaglio completo βββββββββββββββββββββββββ
|
| 1531 |
-
det = "π <b>Score β Dettaglio</b>\n\n"
|
| 1532 |
|
| 1533 |
# Orchestration nodes
|
| 1534 |
NODE_ICONS = {"planner":"π§ ","executor":"βοΈ","reasoner":"π¬",
|
| 1535 |
"recovery_manager":"π‘","robustness_layer":"π","memory_module":"πΎ"}
|
| 1536 |
if nodes:
|
| 1537 |
-
det += "<b>β‘
|
| 1538 |
for nk, nv in nodes.items():
|
| 1539 |
sr = str(nv.get("success_rate", "?"))
|
| 1540 |
lat = nv.get("avg_latency_s")
|
|
@@ -1562,7 +1544,7 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1562 |
det += f" {k[:20]:<20} {v}\n"
|
| 1563 |
det += "</code>\n"
|
| 1564 |
else:
|
| 1565 |
-
det += "<i>βΉοΈ Telemetria runtime non disponibile
|
| 1566 |
|
| 1567 |
# Top 3 best + Top 3 worst
|
| 1568 |
sorted_tasks = sorted([t for t in tasks if t.get("score") is not None], key=lambda t: -t["score"])
|
|
@@ -1918,13 +1900,12 @@ async def _handle_inline(iq: dict, token: str) -> None:
|
|
| 1918 |
"description":f"/autofix {q60}",
|
| 1919 |
"input_message_content":{"message_text":f"/autofix {query}"}},
|
| 1920 |
]
|
| 1921 |
-
|
| 1922 |
-
|
| 1923 |
-
|
| 1924 |
-
|
| 1925 |
-
|
| 1926 |
-
|
| 1927 |
-
_logger.debug("inline answer error: %s", exc)
|
| 1928 |
|
| 1929 |
|
| 1930 |
async def _handle_callback(callback_query: dict, token: str) -> None:
|
|
|
|
| 44 |
return os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
|
| 45 |
|
| 46 |
|
| 47 |
+
def _get_reply_gateway() -> tuple[str, str]:
|
| 48 |
+
"""Restituisce il gateway Pages autenticato, se configurato."""
|
| 49 |
+
return (
|
| 50 |
+
os.getenv("TELEGRAM_REPLY_PROXY_URL", "").strip(),
|
| 51 |
+
os.getenv("TELEGRAM_REPLY_PROXY_SECRET", "").strip(),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
async def _tg_api_call(
|
| 56 |
+
method: str,
|
| 57 |
+
payload: dict,
|
| 58 |
+
token: str | None = None,
|
| 59 |
+
*,
|
| 60 |
+
timeout: httpx.Timeout | float | None = None,
|
| 61 |
+
) -> dict:
|
| 62 |
+
"""Invia un metodo Bot API tramite il gateway Pages quando disponibile.
|
| 63 |
+
|
| 64 |
+
Hugging Face puΓ² bloccare l'egress TCP verso Telegram. Il gateway mantiene il
|
| 65 |
+
token del bot fuori dal runtime e inoltra solo metodi strettamente consentiti.
|
| 66 |
+
"""
|
| 67 |
bot_token = token or _get_bot_token()
|
| 68 |
if not bot_token:
|
| 69 |
+
_logger.warning("tg_api %s skipped: TELEGRAM_BOT_TOKEN missing", method)
|
| 70 |
+
return {}
|
| 71 |
+
|
| 72 |
+
gateway_url, gateway_secret = _get_reply_gateway()
|
| 73 |
+
if gateway_url and gateway_secret:
|
| 74 |
+
request_url = gateway_url
|
| 75 |
+
request_headers = {"Authorization": f"Bearer {gateway_secret}"}
|
| 76 |
+
request_payload = {"method": method, **payload}
|
| 77 |
+
else:
|
| 78 |
+
request_url = f"https://api.telegram.org/bot{bot_token}/{method}"
|
| 79 |
+
request_headers = {}
|
| 80 |
+
request_payload = payload
|
| 81 |
+
|
| 82 |
try:
|
| 83 |
+
client_timeout = timeout or httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0)
|
| 84 |
+
async with httpx.AsyncClient(timeout=client_timeout, trust_env=False) as client:
|
| 85 |
+
response = await client.post(request_url, headers=request_headers, json=request_payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
try:
|
| 87 |
data = response.json()
|
| 88 |
except ValueError:
|
| 89 |
data = {}
|
| 90 |
if response.status_code >= 400 or not data.get("ok", False):
|
| 91 |
detail = str(data.get("description") or data.get("error") or response.text[:160] or "unknown")
|
| 92 |
+
_logger.warning("tg_api rejected: method=%s status=%s detail=%s", method, response.status_code, detail)
|
| 93 |
+
return {}
|
| 94 |
+
return data
|
| 95 |
except Exception as exc:
|
| 96 |
detail = str(exc) or repr(exc)
|
| 97 |
+
_logger.warning("tg_api error: method=%s %s: %s", method, type(exc).__name__, detail)
|
| 98 |
+
return {}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
async def _tg_reply(chat_id: str | int, text: str, token: str | None = None,
|
| 102 |
+
keyboard: dict | None = None) -> None:
|
| 103 |
+
"""Invia una risposta con HTML e opzionale inline keyboard."""
|
| 104 |
+
payload: dict = {
|
| 105 |
+
"chat_id": chat_id,
|
| 106 |
+
"text": text,
|
| 107 |
+
"parse_mode": "HTML",
|
| 108 |
+
"link_preview_options": {"is_disabled": True},
|
| 109 |
+
}
|
| 110 |
+
if keyboard:
|
| 111 |
+
payload["reply_markup"] = keyboard
|
| 112 |
+
await _tg_api_call("sendMessage", payload, token)
|
| 113 |
|
| 114 |
|
| 115 |
async def _tg_answer_callback(callback_query_id: str, text: str = "", token: str | None = None) -> None:
|
| 116 |
+
"""Chiude il caricamento dei pulsanti inline tramite il gateway."""
|
| 117 |
+
if not callback_query_id:
|
|
|
|
| 118 |
return
|
| 119 |
+
await _tg_api_call(
|
| 120 |
+
"answerCallbackQuery",
|
| 121 |
+
{"callback_query_id": callback_query_id, "text": text, "show_alert": False},
|
| 122 |
+
token,
|
| 123 |
+
timeout=5.0,
|
| 124 |
+
)
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
async def _tg_send(chat_id: str | int, text: str, token: str | None = None,
|
| 128 |
keyboard: dict | None = None) -> str | None:
|
| 129 |
+
"""Invia un messaggio e restituisce l'identificativo per gli edit streaming."""
|
|
|
|
|
|
|
|
|
|
| 130 |
payload: dict = {
|
| 131 |
"chat_id": chat_id,
|
| 132 |
"text": text,
|
|
|
|
| 135 |
}
|
| 136 |
if keyboard:
|
| 137 |
payload["reply_markup"] = keyboard
|
| 138 |
+
data = await _tg_api_call("sendMessage", payload, token, timeout=8.0)
|
| 139 |
+
message_id = (data.get("result") or {}).get("message_id")
|
| 140 |
+
return str(message_id) if message_id is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
async def _tg_edit(chat_id: str | int, message_id: str, text: str,
|
| 144 |
token: str | None = None, keyboard: dict | None = None) -> bool:
|
| 145 |
+
"""Aggiorna un messaggio streaming attraverso il gateway."""
|
| 146 |
+
if not message_id:
|
|
|
|
|
|
|
| 147 |
return False
|
| 148 |
payload: dict = {
|
| 149 |
"chat_id": chat_id,
|
|
|
|
| 154 |
}
|
| 155 |
if keyboard:
|
| 156 |
payload["reply_markup"] = keyboard
|
| 157 |
+
return bool(await _tg_api_call("editMessageText", payload, token, timeout=8.0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
|
| 160 |
async def _tg_photo(
|
|
|
|
| 164 |
token: str | None = None,
|
| 165 |
keyboard: dict | None = None,
|
| 166 |
) -> None:
|
| 167 |
+
"""Invia grafici tramite gateway; conserva il fallback multipart per ambienti legacy."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
bot_token = token or _get_bot_token()
|
| 169 |
if not bot_token:
|
| 170 |
return
|
| 171 |
caption_safe = (caption or "")[:1024]
|
| 172 |
+
gateway_url, gateway_secret = _get_reply_gateway()
|
| 173 |
+
if gateway_url and gateway_secret:
|
| 174 |
+
payload: dict = {"chat_id": chat_id, "photo": photo_url, "parse_mode": "HTML"}
|
| 175 |
+
if caption_safe:
|
| 176 |
+
payload["caption"] = caption_safe
|
| 177 |
+
if keyboard:
|
| 178 |
+
payload["reply_markup"] = keyboard
|
| 179 |
+
await _tg_api_call("sendPhoto", payload, bot_token, timeout=20.0)
|
| 180 |
+
return
|
| 181 |
|
| 182 |
import httpx as _hx_p, json as _j_p, urllib.parse as _ul_p, re as _re_p
|
|
|
|
| 183 |
png_bytes: bytes | None = None
|
| 184 |
if "quickchart.io/chart" in photo_url:
|
| 185 |
try:
|
| 186 |
+
match = _re_p.search(r"[?&]c=([^&]+)", photo_url)
|
| 187 |
+
if match:
|
| 188 |
+
cfg_dict = _j_p.loads(_ul_p.unquote(match.group(1)))
|
| 189 |
+
async with _hx_p.AsyncClient(timeout=20.0, trust_env=False) as client:
|
| 190 |
+
response = await client.post(
|
| 191 |
"https://quickchart.io/chart",
|
| 192 |
json={"chart": cfg_dict, "width": 720, "height": 420,
|
| 193 |
"backgroundColor": "white", "format": "png"},
|
| 194 |
)
|
| 195 |
+
if response.status_code == 200 and response.headers.get("content-type", "").startswith("image/"):
|
| 196 |
+
png_bytes = response.content
|
|
|
|
| 197 |
except Exception as exc:
|
| 198 |
+
_logger.debug("tg_photo quickchart fallback: %s", exc)
|
| 199 |
|
| 200 |
try:
|
| 201 |
+
async with httpx.AsyncClient(timeout=15.0, trust_env=False) as client:
|
|
|
|
| 202 |
if png_bytes:
|
| 203 |
+
import json as _json
|
| 204 |
data: dict = {"chat_id": str(chat_id), "parse_mode": "HTML"}
|
| 205 |
if caption_safe:
|
| 206 |
data["caption"] = caption_safe
|
| 207 |
if keyboard:
|
| 208 |
+
data["reply_markup"] = _json.dumps(keyboard)
|
| 209 |
+
await client.post(
|
| 210 |
+
f"https://api.telegram.org/bot{bot_token}/sendPhoto",
|
| 211 |
+
data=data,
|
| 212 |
+
files={"photo": ("chart.png", png_bytes, "image/png")},
|
| 213 |
+
)
|
| 214 |
else:
|
| 215 |
+
payload = {"chat_id": chat_id, "photo": photo_url, "parse_mode": "HTML"}
|
| 216 |
if caption_safe:
|
| 217 |
payload["caption"] = caption_safe
|
| 218 |
if keyboard:
|
| 219 |
payload["reply_markup"] = keyboard
|
| 220 |
+
await client.post(f"https://api.telegram.org/bot{bot_token}/sendPhoto", json=payload)
|
| 221 |
except Exception as exc:
|
| 222 |
_logger.warning("tg_photo error: %s", exc)
|
| 223 |
|
| 224 |
|
| 225 |
async def _tg_typing(chat_id: str | int, action: str = "typing", token: str | None = None) -> None:
|
| 226 |
+
await _tg_api_call("sendChatAction", {"chat_id": chat_id, "action": action}, token, timeout=5.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
|
| 229 |
async def _tg_react(
|
|
|
|
| 232 |
emoji: str = "π",
|
| 233 |
token: str | None = None,
|
| 234 |
) -> None:
|
| 235 |
+
if not message_id:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
return
|
| 237 |
+
await _tg_api_call(
|
| 238 |
+
"setMessageReaction",
|
| 239 |
+
{
|
| 240 |
+
"chat_id": chat_id,
|
| 241 |
+
"message_id": int(message_id),
|
| 242 |
+
"reaction": [{"type": "emoji", "emoji": emoji}],
|
| 243 |
+
"is_big": False,
|
| 244 |
+
},
|
| 245 |
+
token,
|
| 246 |
+
timeout=5.0,
|
| 247 |
+
)
|
|
|
|
|
|
|
| 248 |
|
| 249 |
|
| 250 |
def _fmt_elapsed(created_at_ms: int) -> str:
|
|
|
|
| 1390 |
"""π Score card dettagliata β chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
|
| 1391 |
import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
|
| 1392 |
gh_token = os.getenv("GITHUB_TOKEN", "").strip()
|
| 1393 |
+
runtime_url = (os.getenv("TELEMETRY_URL") or os.getenv("BACKEND_URL") or os.getenv("RAILWAY_URL") or "https://baida07-terminal.hf.space").rstrip("/")
|
| 1394 |
+
machine_token = os.getenv("INTERNAL_TOKEN", "").strip()
|
| 1395 |
+
await _tg_reply(chat_id, "β³ <b>Score</b> β carico benchmark archiviato + telemetria runtimeβ¦")
|
| 1396 |
|
| 1397 |
report: dict | None = None
|
| 1398 |
if gh_token:
|
|
|
|
| 1440 |
rt_repair: dict = {}
|
| 1441 |
try:
|
| 1442 |
async with _hx_sc.AsyncClient(timeout=5.0) as _c:
|
| 1443 |
+
_headers = {"X-Machine-Token": machine_token} if machine_token else {}
|
| 1444 |
+
_tr = await _c.get(f"{runtime_url}/api/telemetry", headers=_headers)
|
| 1445 |
if _tr.status_code == 200:
|
| 1446 |
_td = _tr.json()
|
| 1447 |
rt_timing = _td.get("timing", {})
|
|
|
|
| 1488 |
d_dev = round(avg_ai - avg_dev); s_dev = ("+" if d_dev >= 0 else "") + str(d_dev)
|
| 1489 |
d_mns = round(avg_ai - avg_mns); s_mns = ("+" if d_mns >= 0 else "") + str(d_mns)
|
| 1490 |
d_cur = round(avg_ai - avg_cur); s_cur = ("+" if d_cur >= 0 else "") + str(d_cur)
|
| 1491 |
+
caption = f"π <b>Score snapshot</b> β {ts} UTC <code>v{ver}</code>\n"
|
| 1492 |
+
caption += "<i>Report archiviato: non Γ¨ una valutazione live del runtime.</i>\n"
|
| 1493 |
caption += f"<code>{bar_g}</code> <b>{avg_ai}%</b> {verdict}\n\n"
|
| 1494 |
caption += f"<code>{'Modello':<10} {'Score':>5} {'Ξ':>4} Wins</code>\n"
|
| 1495 |
caption += f"<code>{'Agente AI':<10} {str(avg_ai)+'%':>5} {'β':>4} β</code>\n"
|
|
|
|
| 1510 |
await _tg_photo(chat_id, chart_url, caption=caption[:1024], keyboard=_BENCH_ACTION_KB)
|
| 1511 |
|
| 1512 |
# ββ Messaggio 2 β dettaglio completo βββββββββββββββββββββββββ
|
| 1513 |
+
det = "π <b>Score β Dettaglio</b>\n<i>Benchmark archiviato del " + (ts or "timestamp non disponibile") + " UTC; non rappresenta una misura live.</i>\n\n"
|
| 1514 |
|
| 1515 |
# Orchestration nodes
|
| 1516 |
NODE_ICONS = {"planner":"π§ ","executor":"βοΈ","reasoner":"π¬",
|
| 1517 |
"recovery_manager":"π‘","robustness_layer":"π","memory_module":"πΎ"}
|
| 1518 |
if nodes:
|
| 1519 |
+
det += "<b>β‘ Proxy benchmark per nodo (non telemetria live):</b>\n<code>"
|
| 1520 |
for nk, nv in nodes.items():
|
| 1521 |
sr = str(nv.get("success_rate", "?"))
|
| 1522 |
lat = nv.get("avg_latency_s")
|
|
|
|
| 1544 |
det += f" {k[:20]:<20} {v}\n"
|
| 1545 |
det += "</code>\n"
|
| 1546 |
else:
|
| 1547 |
+
det += "<i>βΉοΈ Telemetria runtime non disponibile o non autorizzata.</i>\n"
|
| 1548 |
|
| 1549 |
# Top 3 best + Top 3 worst
|
| 1550 |
sorted_tasks = sorted([t for t in tasks if t.get("score") is not None], key=lambda t: -t["score"])
|
|
|
|
| 1900 |
"description":f"/autofix {q60}",
|
| 1901 |
"input_message_content":{"message_text":f"/autofix {query}"}},
|
| 1902 |
]
|
| 1903 |
+
await _tg_api_call(
|
| 1904 |
+
"answerInlineQuery",
|
| 1905 |
+
{"inline_query_id": iq_id, "results": results, "cache_time": 30, "is_personal": True},
|
| 1906 |
+
bot_token,
|
| 1907 |
+
timeout=5.0,
|
| 1908 |
+
)
|
|
|
|
| 1909 |
|
| 1910 |
|
| 1911 |
async def _handle_callback(callback_query: dict, token: str) -> None:
|
tests/test_telegram_reply_transport.py
CHANGED
|
@@ -4,7 +4,7 @@ from unittest.mock import patch
|
|
| 4 |
|
| 5 |
import httpx
|
| 6 |
|
| 7 |
-
from api.telegram_webhook import _tg_reply
|
| 8 |
|
| 9 |
|
| 10 |
class _Response:
|
|
@@ -70,6 +70,42 @@ class TelegramReplyTransportTests(unittest.IsolatedAsyncioTestCase):
|
|
| 70 |
self.assertEqual(args[0], "https://tma-agente.pages.dev/api/telegram/send")
|
| 71 |
self.assertEqual(kwargs["headers"], {"Authorization": "Bearer gateway-secret"})
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
async def test_reply_logs_rejected_telegram_response(self):
|
| 74 |
_Client.response = _Response(
|
| 75 |
status_code=429,
|
|
|
|
| 4 |
|
| 5 |
import httpx
|
| 6 |
|
| 7 |
+
from api.telegram_webhook import _handle_inline, _tg_answer_callback, _tg_reply, _tg_send
|
| 8 |
|
| 9 |
|
| 10 |
class _Response:
|
|
|
|
| 70 |
self.assertEqual(args[0], "https://tma-agente.pages.dev/api/telegram/send")
|
| 71 |
self.assertEqual(kwargs["headers"], {"Authorization": "Bearer gateway-secret"})
|
| 72 |
|
| 73 |
+
async def test_stream_message_uses_gateway_and_returns_message_id(self):
|
| 74 |
+
_Client.response = _Response(payload={"ok": True, "result": {"message_id": 77}})
|
| 75 |
+
with patch.dict(
|
| 76 |
+
os.environ,
|
| 77 |
+
{
|
| 78 |
+
"TELEGRAM_REPLY_PROXY_URL": "https://tma-agente.pages.dev/api/telegram/send",
|
| 79 |
+
"TELEGRAM_REPLY_PROXY_SECRET": "gateway-secret",
|
| 80 |
+
},
|
| 81 |
+
clear=False,
|
| 82 |
+
), patch("httpx.AsyncClient", _Client):
|
| 83 |
+
message_id = await _tg_send(123, "stream", token="test-token")
|
| 84 |
+
|
| 85 |
+
args, kwargs = _Client.last_post
|
| 86 |
+
self.assertEqual(message_id, "77")
|
| 87 |
+
self.assertEqual(args[0], "https://tma-agente.pages.dev/api/telegram/send")
|
| 88 |
+
self.assertEqual(kwargs["json"]["method"], "sendMessage")
|
| 89 |
+
|
| 90 |
+
async def test_callback_and_inline_answer_use_gateway(self):
|
| 91 |
+
with patch.dict(
|
| 92 |
+
os.environ,
|
| 93 |
+
{
|
| 94 |
+
"TELEGRAM_REPLY_PROXY_URL": "https://tma-agente.pages.dev/api/telegram/send",
|
| 95 |
+
"TELEGRAM_REPLY_PROXY_SECRET": "gateway-secret",
|
| 96 |
+
},
|
| 97 |
+
clear=False,
|
| 98 |
+
), patch("httpx.AsyncClient", _Client):
|
| 99 |
+
await _tg_answer_callback("callback-id", token="test-token")
|
| 100 |
+
args, kwargs = _Client.last_post
|
| 101 |
+
self.assertEqual(args[0], "https://tma-agente.pages.dev/api/telegram/send")
|
| 102 |
+
self.assertEqual(kwargs["json"]["method"], "answerCallbackQuery")
|
| 103 |
+
|
| 104 |
+
await _handle_inline({"id": "inline-id", "query": "ciao"}, "test-token")
|
| 105 |
+
|
| 106 |
+
_args, kwargs = _Client.last_post
|
| 107 |
+
self.assertEqual(kwargs["json"]["method"], "answerInlineQuery")
|
| 108 |
+
|
| 109 |
async def test_reply_logs_rejected_telegram_response(self):
|
| 110 |
_Client.response = _Response(
|
| 111 |
status_code=429,
|