feat(telegram): restore rich messages opt-out

Salvages PR #45840's client-compatibility opt-out while keeping rich messages enabled by default via telegram.extra.rich_messages: true.
This commit is contained in:
Justin Sunseri 2026-06-13 21:35:54 -07:00 committed by Teknium
parent 8d5d36d793
commit 12682d96b9
7 changed files with 114 additions and 14 deletions

View file

@ -719,6 +719,7 @@ platform_toolsets:
# # allowed_chats: ["-1001234567890"]
# extra:
# disable_link_previews: false # Set true to suppress Telegram URL previews in bot messages
# rich_messages: true # Set false to force legacy MarkdownV2 for clients that cannot render Bot API rich messages
#
# Discord-specific settings (config.yaml top-level, not under platforms:):
#

View file

@ -421,6 +421,9 @@ class TelegramAdapter(BasePlatformAdapter):
self._disable_link_previews: bool = self._coerce_bool_extra("disable_link_previews", False)
# Bot API 10.1 Rich Messages: send final replies via sendRichMessage
# with the raw agent markdown so tables/task lists/etc. render natively.
# Enabled by default; users can opt out for clients that accept but do
# not render rich messages via platforms.telegram.extra.rich_messages.
self._rich_messages_enabled: bool = self._coerce_bool_extra("rich_messages", True)
# 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.
@ -954,7 +957,8 @@ class TelegramAdapter(BasePlatformAdapter):
self, content: str, metadata: Optional[Dict[str, Any]] = None
) -> bool:
return bool(
not getattr(self, "_rich_send_disabled", False)
getattr(self, "_rich_messages_enabled", True)
and not getattr(self, "_rich_send_disabled", False)
and not (metadata or {}).get("expect_edits")
and content
and content.strip()
@ -995,7 +999,8 @@ class TelegramAdapter(BasePlatformAdapter):
streams split exactly as before.
"""
if (
not getattr(self, "_rich_send_disabled", False)
getattr(self, "_rich_messages_enabled", True)
and not getattr(self, "_rich_send_disabled", False)
and self._bot_supports_rich()
):
return self.RICH_MESSAGE_MAX_CHARS
@ -1184,7 +1189,8 @@ class TelegramAdapter(BasePlatformAdapter):
def _should_attempt_rich_draft(self, content: str) -> bool:
return bool(
not getattr(self, "_rich_send_disabled", False)
getattr(self, "_rich_messages_enabled", True)
and not getattr(self, "_rich_send_disabled", False)
and not getattr(self, "_rich_draft_disabled", False)
and content
and content.strip()

View file

@ -1982,6 +1982,9 @@ DEFAULT_CONFIG = {
"reactions": False, # Add 👀/✅/❌ reactions to messages during processing
"channel_prompts": {}, # Per-chat/topic ephemeral system prompts (topics inherit from parent group)
"allowed_chats": "", # If set, bot ONLY responds in these group/supergroup chat IDs (whitelist)
"extra": {
"rich_messages": True, # Bot API 10.1 rich messages; set false to force legacy MarkdownV2
},
},
# Mattermost platform settings (gateway mode)

View file

@ -813,6 +813,37 @@ class TestLoadGatewayConfig:
assert config.platforms[Platform.TELEGRAM].extra["disable_link_previews"] is True
def test_loads_telegram_rich_messages_from_gateway_platform_extra(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
config_path = hermes_home / "config.yaml"
config_path.write_text(
"gateway:\n"
" platforms:\n"
" telegram:\n"
" extra:\n"
" rich_messages: false\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
config = load_gateway_config()
assert config.platforms[Platform.TELEGRAM].extra["rich_messages"] is False
def test_load_config_default_includes_telegram_rich_messages(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
from hermes_cli.config import load_config
config = load_config()
assert config["telegram"]["extra"]["rich_messages"] is True
def test_bridges_telegram_extra_base_url_from_config_yaml(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()

View file

@ -106,16 +106,42 @@ async def test_rich_happy_path_sends_raw_markdown():
@pytest.mark.asyncio
async def test_legacy_rich_messages_config_is_ignored():
async def test_rich_messages_opt_out_uses_legacy_send_path():
adapter = _make_adapter(extra={"rich_messages": False})
result = await adapter.send("12345", RICH_CONTENT)
assert result.success is True
# 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()
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_not_called()
bot.send_message.assert_awaited()
@pytest.mark.asyncio
async def test_rich_messages_opt_out_accepts_string_false():
adapter = _make_adapter(extra={"rich_messages": "false"})
result = await adapter.send("12345", RICH_CONTENT)
assert result.success is True
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_not_called()
bot.send_message.assert_awaited()
@pytest.mark.asyncio
async def test_rich_messages_default_is_enabled():
adapter = _make_adapter()
result = await adapter.send("12345", RICH_CONTENT)
assert result.success is True
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_awaited_once()
bot.send_message.assert_not_called()
@pytest.mark.asyncio
@ -441,9 +467,9 @@ def test_prefers_fresh_final_streaming_when_rich_enabled():
assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is True
def test_prefers_fresh_final_streaming_ignores_legacy_toggle():
def test_prefers_fresh_final_streaming_honors_rich_opt_out():
adapter = _make_adapter(extra={"rich_messages": False})
assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is True
assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is False
# ----------------------------------------------------------------------
@ -456,12 +482,25 @@ def test_streaming_overflow_limit_is_rich_cap_when_enabled():
assert adapter.streaming_overflow_limit() == TelegramAdapter.RICH_MESSAGE_MAX_CHARS
def test_streaming_overflow_limit_ignores_legacy_toggle():
def test_streaming_overflow_limit_none_when_rich_opted_out():
adapter = _make_adapter(extra={"rich_messages": False})
assert adapter.streaming_overflow_limit() == TelegramAdapter.RICH_MESSAGE_MAX_CHARS
assert adapter.streaming_overflow_limit() is None
def test_streaming_overflow_limit_none_when_rich_latched_off():
adapter = _make_adapter()
adapter._rich_send_disabled = True
assert adapter.streaming_overflow_limit() is None
@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
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_not_called()
bot.send_message_draft.assert_awaited_once()

View file

@ -909,7 +909,17 @@ The rich path is skipped automatically when content exceeds the 32,768-byte rich
- **Small tables** are flattened into **row-group bullets** — each row becomes a readable bulleted list under the column headings. Good for 24 columns and short cells.
- **Larger or wider tables** fall back to a **fenced code block** with aligned columns so nothing collapses.
There's nothing to configure for the fallback — the adapter picks the right rendering per message. If you want the legacy "always code-block" behavior, disable table normalization by setting `telegram.pretty_tables: false` in `config.yaml` (default: `true`).
There's nothing to configure for the API fallback — the adapter picks the right rendering per message. If a Telegram client accepts but cannot render rich messages (for example, a watch client that shows them as opaque media blocks), opt out and force the MarkdownV2 path:
```yaml
gateway:
platforms:
telegram:
extra:
rich_messages: false
```
Rich messages are enabled by default (`rich_messages: true`). This setting is for client-rendering compatibility; Hermes already falls back automatically when Telegram rejects the rich API call. If you only want the legacy "always code-block" table behavior while keeping rich messages enabled, disable table normalization by setting `telegram.pretty_tables: false` in `config.yaml` (default: `true`).
**Link previews.** Telegram auto-generates link previews for URLs in bot messages. If you'd rather suppress those (long `/tools` output, agent reply that mentions ten links, etc.):

View file

@ -886,7 +886,17 @@ gateway:
- **小表格**被展平为**行组项目符号**——每行在列标题下变为可读的项目符号列表。适合 2-4 列和短单元格。
- **较大或较宽的表格**回退为带对齐列的**围栏代码块**,以防内容折叠。
回退无需配置——适配器会为每条消息选择正确的渲染方式。如果你想要旧版"始终使用代码块"行为,可在 `config.yaml` 中设置 `telegram.pretty_tables: false` 禁用表格规范化(默认:`true`)。
API 回退无需配置——适配器会为每条消息选择正确的渲染方式。如果某个 Telegram 客户端能接收但不能渲染富消息(例如手表客户端把它们显示为不透明媒体块),可以选择退出并强制使用 MarkdownV2 路径:
```yaml
gateway:
platforms:
telegram:
extra:
rich_messages: false
```
富消息默认启用(`rich_messages: true`)。这个设置用于客户端渲染兼容性;当 Telegram 拒绝富消息 API 调用时Hermes 已经会自动回退。如果你只是想在保持富消息启用的同时恢复旧版「始终使用代码块」表格行为,可在 `config.yaml` 中设置 `telegram.pretty_tables: false` 禁用表格规范化(默认:`true`)。
**链接预览。** Telegram 会为机器人消息中的 URL 自动生成链接预览。如果你希望抑制这些预览(长 `/tools` 输出、提及十个链接的 Agent 回复等):