Add Telegram Bot API 10.1 rich message support#44780
Conversation
Introduce opportunistic support for Telegram Bot API 10.1 rich messages by sending raw agent Markdown via sendRichMessage and streaming previews via sendRichMessageDraft. Implements a rich-path fast‑path in gateway/platforms/telegram.py (RICH_MESSAGE_MAX_BYTES=32768, feature gate platforms.telegram.extra.rich_messages, bot capability checks, routing/thread handling, and conservative fallback rules: permanent/capability errors fall back to the legacy MarkdownV2 path, transient/network errors are surfaced without legacy-resend). Also add a latch for draft capability failures (_rich_draft_disabled) and preserve legacy chunking and draft behavior when needed. Update agent prompt hints (telegram encourages rich Markdown/tables), add CLI config example option, update English and Chinese docs to describe rich messages and fallbacks, and add/adjust tests for rich send and draft behavior.
…drafts) Cherry-pick of upstream PR NousResearch#44780 (ITheEqualizer). Final replies send raw agent markdown via sendRichMessage; DM streaming previews use sendRichMessageDraft; MarkdownV2 stays as the fallback path. Opt out with platforms.telegram.extra.rich_messages: false. Resolved a trivial conflict with main's supports_code_blocks attribute (kept both). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Verified this end-to-end — the post-cutoff API claims are real and the tests are genuine. Details below, plus one small bug and a few notes. API verification (live docs, since 10.1 shipped a day ago):
Bug (small): pool timeouts become non-retryable on the rich path. Legacy Note: Note: the "Telegram exposes no rich-edit method" comment is inaccurate. 10.1 added a Note: Nit: Also: this implements #44428 (where the blocker discussed was the PTB 22.6 pin — the raw Cherry-picked onto our rollup branch (with that conflict resolved) and everything above ran against the live venv. Nice work on the fallback taxonomy — the no-resend-on-transient rule for finals vs. the latch-off for drafts is exactly the right asymmetry. |
|
Heads-up: this went Resolution is keep-both: # Telegram message limits
MAX_MESSAGE_LENGTH = 4096
supports_code_blocks = True # Telegram MarkdownV2 renders fenced code blocks
# Bot API 10.1 Rich Messages cap the raw markdown/html text at 32,768
# UTF-8 bytes. Content above this is sent via the legacy chunking path.
RICH_MESSAGE_MAX_BYTES = 32768Verified the merged result compiles with both attributes present — the two changes don't interact ( |
Improve Telegram adapter rich-message support: switch the rich limit to a 32,768-character cap, add robust handling for missing-endpoint/capability errors (latch off rich sends), separate capability vs content BadRequest classification, and implement retry/flood-control/backoff logic for sendRichMessage. Add _rich_send_disabled flag and honor metadata.expect_edits to keep messages that will be edited on the legacy (editable) MarkdownV2 path. Carry reply parameters as reply_parameters, expose link_preview options, and ensure drafts only latch off on true capability failures (not per-frame parse errors). Propagate expect_edits from stream consumers and runner progress code so preview/status bubbles remain editable. Update tests to cover new behaviors and adjust docs (including i18n) to note the character limit, latching behavior, and flood-control semantics.
…d path Parity with the legacy send() loop (dc4de14 / NousResearch#35664): an httpx pool timeout explicitly never left the process, so the rich path now retries it in place and surfaces retryable=True instead of silently dropping the message. Flagged in the NousResearch#44780 review; the PR branch predates the legacy pool-timeout fix, so it could not mirror it.
|
Verified the follow-up commit ( Verification:
Confirmed fixed from the earlier review / #44829: Still open (1): pool timeouts remain non-retryable on the rich path. The final transient return is still Still open (2): Cherry-picked onto our rollup branch (same single keep-both conflict, plus the two-line pool-timeout mirror since that branch has |
|
Merged via PR #44829 — your commit was cherry-picked onto current main with your authorship preserved in git history (05b9c84), via rebase-merge. Excellent work: the endpoint shipped a day before you wired it, and the fallback design (permanent-vs-transient classification, no duplicate resends, routing parity) was careful and correct. Three follow-ups were added on top during salvage:
Thanks for the contribution! |
Summary
Telegram's Bot API 10.1 (June 11 2026) added Rich Messages (
sendRichMessage/sendRichMessageDraft/InputRichMessage). This wires the Telegram gateway to use them so replies render tables, task lists, headings, nested blockquotes, collapsible<details>, footnotes/references, math/formulas, underline, sub/superscript, marked text, and anchors natively, instead of flattening tables into row-group bullets.sendRichMessage.sendRichMessageDraft, so the animated draft matches the final rich message.platforms.telegram.extra.rich_messages: false.python-telegram-bot 22.6doesn't expose a typedsend_rich_message, so we call the raw endpoint viaBot.do_api_request("sendRichMessage"/"sendRichMessageDraft", api_kwargs=..., return_type=Message).Motivation
The legacy path runs every reply through MarkdownV2 (
format_message), which escapes/destroys rich syntax — pipe tables became "row-group bullets" and the prompt hint actively told agents to avoid tables. With Rich Messages, the raw markdown renders server-side, so structured output (tables, task lists) finally works on Telegram.What changed
gateway/platforms/telegram.py__init__:rich_messagesopt-out flag (default on) + a latch for draft capability failures.do_api_requestis a coroutine fn), raw-markdown payload builder, conservative fallback-error classifier, single-send routing mirror, and_try_send_rich/_try_send_rich_draft.send(): rich fast-path before the MarkdownV2 path; falls through to legacy on permanent/capability errors, returns directly on success or transient failure.send_draft(): triessendRichMessageDraft, with the existing plain-textsendMessageDraftas the fallback frame.agent/prompt_builder.py— rewrotePLATFORM_HINTS["telegram"]to encourage rich Markdown; kept theinclude MEDIA:/absolute/pathguidance. (Static stable-tier text → prompt-cache safe.)Docs/config — documented
rich_messagesincli-config.yaml.example; rewrote the rendering section inwebsite/docs/user-guide/messaging/telegram.md(+ zh-Hans i18n copy).Tests — new
tests/gateway/test_telegram_rich_messages.py(21 tests) + a prompt-hint regression intests/agent/test_prompt_builder.py.Safety / design
inspect.iscoroutinefunction(bot.do_api_request)— real bots + opt-in mocks engage rich; plain mocks fall through to legacy unchanged._thread_kwargs_for_send/_is_private_dm_topic_send: preservesmessage_thread_id,direct_messages_topic_id(dropping the pairedNonethread id), reply anchor (scalarreply_to_message_id),disable_notification, and the DM-topic fail-loud refusal (delegated to legacy as the single source).InputRichMessageblock model (RichBlock*/RichText*), thehtmlinput variant, and rich message editing.Config
On by default. To disable per platform:
Tests
Run with
-o 'addopts=':tests/gateway/test_telegram_rich_messages.py— 21 passed (16 final-send + 5 streaming-draft).test_telegram_rich_messages.py+test_stream_consumer_draft.py+tests/agent/test_prompt_builder.py→ 157 passed, 1 skipped.tests/gateway(single process): base-vs-branch failure set is identical — the only differences are the new rich tests now passing. The 13 remaining failures (simplexModuleNotFoundError, wecom, shutdown_forensics, and the approval/model_picker/slash_confirm files that inject their own faketelegrammodule) are a pre-existing cross-filesys.modulespollution artifact that reproduces onmainand is unrelated to this change.Deploy
Requires a gateway restart (
/restartorhermes gateway restart).Caveat
Validated against mocked unit tests, not a live bot. Since the endpoint shipped ~1 day ago, if the exact
rich_messagepayload shape differs in practice, the graceful fallback keeps existing sends working — rich rendering just won't engage until confirmed live.