diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 3e7c729c0..1cc0c4a71 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -508,13 +508,16 @@ PLATFORM_HINTS = { ), "telegram": ( "You are on a text messaging communication platform, Telegram. " - "Standard markdown is automatically converted to Telegram format. " + "Standard Markdown is automatically converted to Telegram formatting. " "Supported: **bold**, *italic*, ~~strikethrough~~, ||spoiler||, " "`inline code`, ```code blocks```, [links](url), and ## headers. " - "Telegram has NO table syntax — prefer bullet lists or labeled " - "key: value pairs over pipe tables (any tables you do emit are " - "auto-rewritten into row-group bullets, which you can produce " - "directly for cleaner output). " + "Telegram supports rich Markdown, so when it improves clarity you may " + "use headings, tables (pipe `| col | col |` syntax), task lists " + "(`- [ ]` / `- [x]`), nested blockquotes, collapsible details, " + "footnotes/references, math/formulas (`$...$`, `$$...$$`), underline, " + "subscript/superscript, marked (highlighted) text, and anchors. Prefer " + "real Markdown tables and task lists over hand-built bullet substitutes " + "when presenting structured data. " "You can send media files natively: to deliver a file to the user, " "include MEDIA:/absolute/path/to/file in your response. Images " "(.png, .jpg, .webp) appear as photos, audio (.ogg) sends as voice " diff --git a/cli-config.yaml.example b/cli-config.yaml.example index a741970ec..8ce9ad8e1 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -719,11 +719,6 @@ platform_toolsets: # # allowed_chats: ["-1001234567890"] # extra: # disable_link_previews: false # Set true to suppress Telegram URL previews in bot messages -# # Bot API 10.1 Rich Messages: final replies send raw markdown via -# # sendRichMessage so tables, task lists, collapsible details, math, etc. -# # render natively (with automatic MarkdownV2 fallback). Opt-in while -# # the new endpoint is validated; default false. -# rich_messages: false # Set true to enable native rich rendering # # Discord-specific settings (config.yaml top-level, not under platforms:): # diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 3cf241966..e0d13cdbd 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -416,10 +416,8 @@ class TelegramAdapter(BasePlatformAdapter): self._mention_patterns = self._compile_mention_patterns() self._reply_to_mode: str = getattr(config, 'reply_to_mode', 'first') or 'first' self._disable_link_previews: bool = self._coerce_bool_extra("disable_link_previews", False) - # Bot API 10.1 Rich Messages: opportunistically send final replies via - # sendRichMessage with the raw agent markdown so tables/task lists/etc. - # render natively. Opt-out via platforms.telegram.extra.rich_messages. - self._rich_messages_enabled: bool = self._coerce_bool_extra("rich_messages", False) + # Bot API 10.1 Rich Messages: send final replies via sendRichMessage + # with the raw agent markdown so tables/task lists/etc. render natively. # Latched off after a capability failure on sendRichMessage / # sendRichMessageDraft (e.g. older python-telegram-bot without the # endpoint) so later sends skip the doomed rich attempt entirely. @@ -949,12 +947,8 @@ class TelegramAdapter(BasePlatformAdapter): return inspect.iscoroutinefunction(getattr(self._bot, "do_api_request", None)) def _should_attempt_rich(self, content: str) -> bool: - # getattr defaults: tests build adapters via object.__new__() (no - # __init__), so the flags may be unset — default rich OFF (the - # feature is opt-in via platforms.telegram.extra.rich_messages). return bool( - getattr(self, "_rich_messages_enabled", False) - and not getattr(self, "_rich_send_disabled", False) + not getattr(self, "_rich_send_disabled", False) and content and content.strip() and self._content_fits_rich_limits(content) @@ -1132,8 +1126,7 @@ class TelegramAdapter(BasePlatformAdapter): def _should_attempt_rich_draft(self, content: str) -> bool: return bool( - getattr(self, "_rich_messages_enabled", False) - and not getattr(self, "_rich_send_disabled", False) + not getattr(self, "_rich_send_disabled", False) and not getattr(self, "_rich_draft_disabled", False) and content and content.strip() diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 09acb74ce..998f9ddba 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -877,6 +877,18 @@ class TestPromptBuilderConstants: # check that this test is calibrated correctly). assert "include MEDIA:" in PLATFORM_HINTS["telegram"] + def test_telegram_hint_encourages_rich_markdown(self): + # Telegram Bot API 10.1 rich messages are default-on, so the hint must + # encourage native structured markdown instead of forbidding tables. + hint = PLATFORM_HINTS["telegram"] + lowered = hint.lower() + assert "Telegram has NO table syntax" not in hint + assert "rich markdown" in lowered + assert "table" in lowered + assert "task list" in lowered + assert "math" in lowered + assert "include MEDIA:" in hint + def test_platform_hints_mattermost(self): hint = PLATFORM_HINTS["mattermost"] assert "Mattermost" in hint diff --git a/tests/gateway/test_telegram_rich_messages.py b/tests/gateway/test_telegram_rich_messages.py index 8bb0b1702..db971cd6d 100644 --- a/tests/gateway/test_telegram_rich_messages.py +++ b/tests/gateway/test_telegram_rich_messages.py @@ -27,17 +27,8 @@ RICH_CONTENT = "## Results\n\n| Case | Status |\n|---|---|\n| rich | ✅ |\n\n- def _make_adapter(extra=None): - """Build a TelegramAdapter with a mock bot wired for the rich path. - - Rich messages are opt-in (default off) while the Bot API 10.1 endpoint - is validated live, so tests that exercise the rich path enable it - explicitly here; opt-out tests pass their own ``extra``. - """ - config = PlatformConfig( - enabled=True, - token="fake-token", - extra={"rich_messages": True} if extra is None else extra, - ) + """Build a TelegramAdapter with a mock bot wired for the rich path.""" + config = PlatformConfig(enabled=True, token="fake-token", extra=extra or {}) adapter = TelegramAdapter(config) bot = MagicMock() # do_api_request as an AsyncMock makes inspect.iscoroutinefunction(...) True, @@ -76,24 +67,16 @@ async def test_rich_happy_path_sends_raw_markdown(): @pytest.mark.asyncio -async def test_rich_opt_out_uses_legacy(): +async def test_legacy_rich_messages_config_is_ignored(): adapter = _make_adapter(extra={"rich_messages": False}) result = await adapter.send("12345", RICH_CONTENT) assert result.success is True - adapter._bot.do_api_request.assert_not_called() - adapter._bot.send_message.assert_awaited() - - -@pytest.mark.asyncio -async def test_rich_opt_out_accepts_string_false(): - adapter = _make_adapter(extra={"rich_messages": "false"}) - - await adapter.send("12345", RICH_CONTENT) - - adapter._bot.do_api_request.assert_not_called() - adapter._bot.send_message.assert_awaited() + # The legacy toggle was removed; stale config entries must not disable the + # rich path. + adapter._bot.do_api_request.assert_awaited_once() + adapter._bot.send_message.assert_not_called() @pytest.mark.asyncio @@ -265,13 +248,9 @@ async def test_notification_opt_in_drops_disable_flag(): @pytest.mark.asyncio -async def test_rich_gate_tolerates_missing_enabled_attr(): - """Adapters missing _rich_messages_enabled (object.__new__ in some tests) - must not raise — the gate reads it via getattr(default=True), and a bot - without an async do_api_request falls through to the legacy path.""" +async def test_rich_gate_tolerates_minimal_bot_without_raw_endpoint(): + """A bot without an async do_api_request falls through to the legacy path.""" adapter = _make_adapter() - del adapter._rich_messages_enabled # simulate object.__new__ construction - # SimpleNamespace bot has no do_api_request -> _bot_supports_rich() False. adapter._bot = SimpleNamespace( send_message=AsyncMock(return_value=SimpleNamespace(message_id=42)), send_chat_action=AsyncMock(), @@ -337,17 +316,6 @@ async def test_rich_draft_transient_failure_does_not_latch_off(): assert adapter._rich_draft_disabled is False -@pytest.mark.asyncio -async def test_rich_draft_opt_out_uses_legacy(): - adapter = _make_adapter(extra={"rich_messages": False}) - - result = await adapter.send_draft("12345", draft_id=7, content=RICH_CONTENT) - - assert result.success is True - adapter._bot.do_api_request.assert_not_called() - adapter._bot.send_message_draft.assert_awaited_once() - - @pytest.mark.asyncio async def test_rich_draft_oversized_uses_legacy(): adapter = _make_adapter() diff --git a/website/docs/user-guide/messaging/telegram.md b/website/docs/user-guide/messaging/telegram.md index 9b145fbbc..31aeac88a 100644 --- a/website/docs/user-guide/messaging/telegram.md +++ b/website/docs/user-guide/messaging/telegram.md @@ -900,19 +900,11 @@ gateway: ## Rendering: Rich Messages, Tables and Link Previews -**Rich Messages (Bot API 10.1).** When enabled, final replies are sent with Telegram's native [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) using the agent's **raw markdown**, so tables, task lists, headings, nested blockquotes, collapsible `
`, footnotes/references, math/formulas, underline, sub/superscript, marked text, and anchors render natively — no client-side flattening. In DMs the live streaming preview also uses `sendRichMessageDraft`, so the animated draft matches the final rich message. This is **opt-in** (default off) while the new endpoint is validated; enable it per platform: - -```yaml -gateway: - platforms: - telegram: - extra: - rich_messages: true -``` +**Rich Messages (Bot API 10.1).** Final replies are sent with Telegram's native [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) using the agent's **raw markdown**, so tables, task lists, headings, nested blockquotes, collapsible `
`, footnotes/references, math/formulas, underline, sub/superscript, marked text, and anchors render natively — no client-side flattening. In DMs the live streaming preview also uses `sendRichMessageDraft`, so the animated draft matches the final rich message. The rich path is skipped automatically when content exceeds the 32,768-byte rich text limit, and any rejection from Telegram (unsupported endpoint on an older `python-telegram-bot`, parser error, oversized blocks/columns) **transparently falls back** to the MarkdownV2 path — your message is never lost. Transient/network errors are *not* silently re-sent (no duplicate final message). -**MarkdownV2 fallback.** When the rich path is disabled or unavailable, Hermes converts markdown to MarkdownV2. Since MarkdownV2 has no native table syntax, pipe tables are normalized: +**MarkdownV2 fallback.** When the rich path is unavailable for a message, Hermes converts markdown to MarkdownV2. Since MarkdownV2 has no native table syntax, pipe tables are normalized: - **Small tables** are flattened into **row-group bullets** — each row becomes a readable bulleted list under the column headings. Good for 2–4 columns and short cells. - **Larger or wider tables** fall back to a **fenced code block** with aligned columns so nothing collapses. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md index 399948015..0a5503e9f 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md @@ -877,19 +877,11 @@ gateway: ## 渲染:富消息、表格和链接预览 -**富消息(Bot API 10.1)。** 启用后,最终回复通过 Telegram 原生的 [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) 发送,使用 Agent 的**原始 markdown**,因此表格、任务列表、标题、嵌套引用块、可折叠的 `
`、脚注/引用、数学公式、下划线、上下标、高亮文本和锚点都能原生渲染——无需客户端展平。在私聊中,实时流式预览也使用 `sendRichMessageDraft`,因此动画草稿与最终的富消息保持一致。此功能为**选择性启用**(默认关闭),在新端点经过验证期间需手动开启;可按平台配置: - -```yaml -gateway: - platforms: - telegram: - extra: - rich_messages: true -``` +**富消息(Bot API 10.1)。** 最终回复通过 Telegram 原生的 [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) 发送,使用 Agent 的**原始 markdown**,因此表格、任务列表、标题、嵌套引用块、可折叠的 `
`、脚注/引用、数学公式、下划线、上下标、高亮文本和锚点都能原生渲染——无需客户端展平。在私聊中,实时流式预览也使用 `sendRichMessageDraft`,因此动画草稿与最终的富消息保持一致。 当内容超过 32,768 字节的富文本上限时,富消息路径会自动跳过;Telegram 的任何拒绝(较旧 `python-telegram-bot` 不支持该端点、解析错误、块/列过多)都会**透明回退**到 MarkdownV2 路径——消息绝不会丢失。瞬时/网络错误**不会**被静默重发(不会产生重复的最终消息)。 -**MarkdownV2 回退。** 当富消息路径被禁用或不可用时,Hermes 会将 markdown 转换为 MarkdownV2。由于 MarkdownV2 没有原生表格语法,管道表格会被规范化: +**MarkdownV2 回退。** 当某条消息无法使用富消息路径时,Hermes 会将 markdown 转换为 MarkdownV2。由于 MarkdownV2 没有原生表格语法,管道表格会被规范化: - **小表格**被展平为**行组项目符号**——每行在列标题下变为可读的项目符号列表。适合 2-4 列和短单元格。 - **较大或较宽的表格**回退为带对齐列的**围栏代码块**,以防内容折叠。