sync: 154 file da Baida98/AI@a284c7fa (2026-08-15 11:54 UTC) [deploy-all]

#35
by Baida07 - opened
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
- async def _tg_reply(chat_id: str | int, text: str, token: str | None = None,
48
- keyboard: dict | None = None) -> None:
49
- """Invia risposta al chat_id con HTML + opzionale inline keyboard."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  bot_token = token or _get_bot_token()
51
  if not bot_token:
52
- return
53
- payload: dict = {
54
- "chat_id": chat_id,
55
- "text": text,
56
- "parse_mode": "HTML",
57
- "link_preview_options": {"is_disabled": True},
58
- }
59
- if keyboard:
60
- payload["reply_markup"] = keyboard
 
 
 
 
61
  try:
62
- import httpx
63
- timeout = httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0)
64
- gateway_url = os.getenv("TELEGRAM_REPLY_PROXY_URL", "").strip()
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("tg_reply rejected: status=%s detail=%s", response.status_code, detail)
 
 
84
  except Exception as exc:
85
  detail = str(exc) or repr(exc)
86
- _logger.warning("tg_reply error: %s: %s", type(exc).__name__, detail)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
 
89
  async def _tg_answer_callback(callback_query_id: str, text: str = "", token: str | None = None) -> None:
90
- """Risponde a un callback_query (obbligatorio per chiudere il loading sui buttons)."""
91
- bot_token = token or _get_bot_token()
92
- if not bot_token:
93
  return
94
- try:
95
- import httpx
96
- async with httpx.AsyncClient(timeout=5.0) as c:
97
- await c.post(
98
- f"https://api.telegram.org/bot{bot_token}/answerCallbackQuery",
99
- json={"callback_query_id": callback_query_id, "text": text, "show_alert": False},
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 ritorna il message_id (per editMessageText streaming)."""
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
- try:
120
- import httpx
121
- async with httpx.AsyncClient(timeout=8.0) as c:
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 esistente β€” streaming live via editMessageText.
136
- Ritorna True se successo. Rate-limit: max 20 edit/min per chat Telegram."""
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
- try:
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 foto/chart via sendPhoto Telegram.
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
- m = _re_p.search(r"[?&]c=([^&]+)", photo_url)
186
- if m:
187
- cfg_dict = _j_p.loads(_ul_p.unquote(m.group(1)))
188
- async with _hx_p.AsyncClient(timeout=20.0) as c:
189
- qr = await c.post(
190
  "https://quickchart.io/chart",
191
  json={"chart": cfg_dict, "width": 720, "height": 420,
192
  "backgroundColor": "white", "format": "png"},
193
  )
194
- if qr.status_code == 200 and qr.headers.get("content-type", "").startswith("image/"):
195
- png_bytes = qr.content
196
- _logger.debug("tg_photo: quickchart POST ok, %d bytes", len(png_bytes))
197
  except Exception as exc:
198
- _logger.debug("tg_photo: quickchart POST fallback: %s", exc)
199
 
200
  try:
201
- import httpx as _hx_s
202
- async with _hx_s.AsyncClient(timeout=15.0) as c:
203
  if png_bytes:
204
- import json as _j_s
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"] = _j_s.dumps(keyboard)
210
- files = {"photo": ("chart.png", png_bytes, "image/png")}
211
- await c.post(f"https://api.telegram.org/bot{bot_token}/sendPhoto",
212
- data=data, files=files)
 
 
213
  else:
214
- payload: dict = {"chat_id": chat_id, "photo": photo_url, "parse_mode": "HTML"}
215
  if caption_safe:
216
  payload["caption"] = caption_safe
217
  if keyboard:
218
  payload["reply_markup"] = keyboard
219
- await c.post(f"https://api.telegram.org/bot{bot_token}/sendPhoto", json=payload)
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
- """Invia sendChatAction β€” mostra '⌨️ digitando…' prima di operazioni pesanti.
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
- """Aggiunge reazione emoji a un messaggio (Bot API 7.1+, Feb 2024).
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
- try:
257
- async with httpx.AsyncClient(timeout=3.0) as c:
258
- await c.post(
259
- f"https://api.telegram.org/bot{bot_token}/setMessageReaction",
260
- json={
261
- "chat_id": chat_id,
262
- "message_id": int(message_id),
263
- "reaction": [{"type": "emoji", "emoji": emoji}],
264
- "is_big": False,
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
- rw_url = os.getenv("RAILWAY_URL", "https://baida-a-terminal.hf.space").rstrip("/")
1415
- await _tg_reply(chat_id, "⏳ <b>Score</b> β€” carico report + metriche runtime…")
 
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
- _tr = await _c.get(f"{rw_url}/api/telemetry")
 
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>⚑ Orchestration Nodes:</b>\n<code>"
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 (Railway idle)</i>\n"
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
- try:
1922
- import httpx as _hx
1923
- async with _hx.AsyncClient(timeout=5.0) as c:
1924
- await c.post(f"https://api.telegram.org/bot{bot_token}/answerInlineQuery",
1925
- json={"inline_query_id":iq_id,"results":results,"cache_time":30,"is_personal":True})
1926
- except Exception as exc:
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,