feat(gateway): Telegram Bot API 10.1 Rich Messages support#1106
Conversation
This comment has been minimized.
This comment has been minimized.
Propose integrating sendRichMessage and sendRichMessageDraft into the TG gateway adapter for structured formatting and AI streaming UX. Refs: https://core.telegram.org/bots/api#rich-message-formatting-options
- Add sendRichMessage() for structured content (tables, code, headings) - Add sendRichMessageDraft() for future AI streaming support - Add is_complex_markdown() classifier to route complex replies - Feature-gated via TELEGRAM_RICH_MESSAGES=true env var (default: off) - Falls back to sendMessage on sendRichMessage failure When enabled, replies containing tables, fenced code blocks, headings, or content >4096 chars will use sendRichMessage with InputRichMessage markdown format. This passes agent markdown directly (GFM-compatible) without needing any conversion layer.
a0c899a to
46e171f
Compare
Address review from 擺渡法師: - Detect h1-h6 headings (not just h1-h3) - Handle headings at line start (not just after \n) - Handle indented headings (leading whitespace) - Reject #hashtag (no space after #)
Address review from 口渡法師: clamp content to Bot API limit (32768 UTF-8 chars) before sending, avoiding wasted round-trip on oversized payloads.
Users get rich messages automatically. Set TELEGRAM_RICH_MESSAGES=false to opt out.
Address 擺渡法師 review: LLM outputs commonly use alignment markers like |:---|, |---:|, | :---: | which the naive |---| check missed. Now parses the separator row properly: starts/ends with |, each cell between pipes contains only dashes (optionally wrapped in colons).
Address 口渡法師 review: some agents/tools use ~~~ instead of backtick fences. Both are valid GFM code fences.
Explain rationale for: - Classify at adapter layer (not agent) — zero prompt changes needed - Conservative heuristic — only route when legacy visibly breaks - 4096 threshold — sendMessage hard limit, prefer rich over chunking - GFM table parsing — avoid false positives on plain pipe text - Fallback strategy — one extra round-trip worst case, never lost delivery - sendRichMessageDraft — wired for Phase 2 streaming
This comment has been minimized.
This comment has been minimized.
Review + live E2E (Bot API 10.1)Reviewed the adapter and ran the send path end-to-end against the live API (real bot/chat, crafted 🔴 1 — Rich truncation counts bytes, not characters. The 32768 cap is in UTF-8 characters, but truncation uses 🔴 2 — Legacy Fix for both: chunk into limit-sized pieces (32768 rich / 4096 sendMessage), counting characters and reopening code fences across boundaries so blocks stay valid. I reused the fence-aware 🟡 3 — Sending code via 🟢 Minor: tables spanning a chunk boundary still break (no lossless API alternative at 32768/msg); ADR drift — it says Fixes for 🔴1 / 🔴2 (+ regression tests) are independent of the 🟡3 design decision. Let me know if you'd like the PR. |
…inking draft instead" This reverts commit bad8749.
When streaming_placeholder = false, the core skips sending the initial '…' message. The gateway's edit loop still works via a dummy MessageRef since the gateway adapter uses sendRichMessageDraft (no real msg_id needed). Config: [gateway] streaming_placeholder = false
🔴 1: Rich truncation now counts chars, not bytes (CJK safe)
🔴 2: Legacy sendMessage chunks at 4096 chars (no more lost replies)
🟡 3: Code blocks stay on legacy path (preserves syntax highlighting);
only tables + headings route to sendRichMessage
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ADR sync - Guard edit_message when rich_messages=false (drop silently instead of attempting editMessageText with dummy 'draft' message_id) - Incorporate thread_id in draft_id derivation to prevent forum topic collision - ADR: remove code blocks from classification, fix MarkdownV2→Markdown, update config reference from TOML to env var - docs/telegram.md: update markdown rendering section, add TELEGRAM_RICH_MESSAGES to env var table - docs/config-reference.md: add streaming + streaming_placeholder fields - config.toml.example: add streaming/streaming_placeholder examples
…ft path - edit_message with reply_to='draft': route to sendRichMessageDraft (rich) or silently drop (non-rich). Never attempt editMessageText on dummy ref. - edit_message with real message_id: perform actual editMessageText for legacy streaming placeholder updates. - Stream finalization: when placeholder is dummy 'draft' ref, send final content as new message (send_message) so it gets persisted. This ensures rich path uses sendRichMessage for the final reply, not just a draft. Addresses 擺渡法師's 🔴 #2 — streaming finalization must persist.
法師團隊 Final Review — PR #1106LGTM ✅ CI Status (commit
|
| Job | Status |
|---|---|
gateway |
✅ pass |
check |
✅ pass |
smoke-test (native-sandbox) |
✅ pass |
All Findings Resolved
| # | Original Finding | Resolution | Commit |
|---|---|---|---|
| 🔴 1 | Test assertions for code blocks (CI red) | Fixed — assert!(!...) for code block cases |
dbdd025 |
| 🔴 2 | Stream finalization never persists (draft expires) | Fixed — dummy "draft" ref detected; finalization uses send_message (persists via sendRichMessage). Real message_ids route to editMessageText. |
90f5416 |
| 🟡 3 | draft_id collision in forum topics |
Fixed — draft_id now derived from channel_id + thread_id |
32dd083 |
| 🟡 4 | ADR vs code vs test contradiction (code blocks) | Fixed — ADR updated to match code (code blocks NOT routed to rich) | 32dd083 |
| 🟡 8 | docs/telegram.md outdated |
Fixed — rich message docs added, TELEGRAM_RICH_MESSAGES in env table |
32dd083 |
| 🟡 9/10 | config-reference.md + config.toml.example missing fields |
Fixed — streaming + streaming_placeholder added |
32dd083 |
Non-blocking Follow-ups (agreed by team)
| # | Finding | Rationale for deferral |
|---|---|---|
| 🟡 5 | Permissive bool parsing for env var | Common Rust/Unix pattern; not a security issue |
| 🟡 6 | Raw markdown passthrough to rich_message | Telegram parses server-side; no XSS risk; 32k truncate + fallback |
| 🟡 7 | No integration test for rich path | Classifier unit-tested; delivery has fallback; e2e needs real token |
| 🟡 12 | Default-on backward compat | Fallback chain complete; docs clearly state opt-out; acceptable |
Reviewer Sign-off
| 法師 | Angle | Verdict |
|---|---|---|
| 擺渡 (Codex) | 架構 | LGTM ✅ |
| 普渡 (Claude) | 正確性 | ✅ (🔴 #1 confirmed fixed) |
| 口渡 (Copilot) | 安全/CI | No blocker ✅ |
| 覺渡 (Kiro) | 文件/UX | LGTM ✅ |
All critical and important findings addressed. CI green. Ready to merge. 🚀
Previous comments minimized (OUTDATED). This is the final consolidated review.
Re-review — post streaming/thinking + 2464d0bRe-reviewed the branch after the streaming/thinking work and ✅ Confirmed sound
🟡 Remaining — docs/ADR drift (highest priority, pure docs)
🟡 Remaining — code consistency / edge (non-blocking)
|
Reply to @musingfox re-reviewThanks for the thorough re-review! Here's the status of each finding: ✅ Already addressed (commits
|
| # | Finding | Fix |
|---|---|---|
| 1 | ADR vs code drift (MarkdownV2, code blocks, config gate) | ADR updated: removed code blocks from triggers, MarkdownV2→Markdown, [telegram] rich_messages→TELEGRAM_RICH_MESSAGES env var |
| 2 | docs/telegram.md:202 outdated |
Rewritten with rich message info + TELEGRAM_RICH_MESSAGES added to env table |
| 3 | Config docs missing fields | streaming + streaming_placeholder added to config-reference.md and config.toml.example |
| 5 | draft_id ignores thread_id |
Now derived from channel_id + thread_id (wrapping_add) to prevent forum collision |
🟡 Acknowledged — not fixing in this PR
| # | Finding | Rationale |
|---|---|---|
| 4 | Streaming edit path byte-based .len() |
Affects ephemeral drafts only (30s TTL). Worst case: CJK content slightly mis-truncated in preview animation. Final persisted message uses correct chars() treatment. Low blast radius — follow-up. |
| 6 | Default-on + permissive bool | Fallback chain guarantees no message loss on older clients (rich fails → sendMessage). Permissive parsing is standard Rust/Unix env var convention. Documenting opt-out is sufficient. — follow-up. |
All functional paths confirmed sound. CI green.
- Context section: MarkdownV2 → Markdown (matches actual code) - Status: Proposed → Accepted (feature is implemented)
Summary
Telegram Bot API 10.1 introduced Rich Messages — allowing bots to send highly structured text with native tables, code blocks, headings, math formulas, and stream AI-generated replies.
This PR integrates Rich Messages into the OAB gateway TG adapter. Default on — users don't need any configuration. Set
TELEGRAM_RICH_MESSAGES=falseto opt out.Classification Logic
The adapter auto-classifies each reply and routes to
sendRichMessagewhen any of these conditions are met:|---|,|:---:|)#through######(followed by space)If none match → uses traditional
sendMessagewith Markdown parse mode.Changes
sendRichMessage()for structured contentsendRichMessageDraft()wired for future AI streaming supportis_complex_markdown()classifier with robust table separator + ATX heading detectionTELEGRAM_RICH_MESSAGES=true), opt-out with=falsesendMessageon API failureArchitecture
No agent-side changes needed — existing markdown output is GFM-compatible with Rich Markdown.
References