mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
feat(gateway): configure Telegram command menu priority
Adds a configurable Telegram BotCommand menu cap and priority list via platforms.telegram.extra.command_menu (max_commands clamped 1..100; priority_mode prepend|append|replace). Default cap stays 30; hidden commands remain invokable when typed and /commands lists the full set. Salvaged from PR #42021. Cherry-picked onto current main; the original edited gateway/platforms/telegram.py, now relocated to plugins/platforms/telegram/adapter.py.
This commit is contained in:
parent
281a439ad4
commit
dbe14ce35d
5 changed files with 275 additions and 12 deletions
|
|
@ -741,6 +741,17 @@ platform_toolsets:
|
|||
# extra:
|
||||
# disable_link_previews: false # Set true to suppress Telegram URL previews in bot messages
|
||||
# rich_messages: false # Bot API 10.1 rich messages (tables/task lists/details/math); default false for copyable legacy MarkdownV2, set true to opt in
|
||||
# command_menu:
|
||||
# # Telegram allows up to 100 BotCommands; Hermes defaults to 30 to
|
||||
# # stay under Telegram's payload-size limits while keeping common
|
||||
# # commands visible. Values are clamped to 1..100.
|
||||
# max_commands: 30
|
||||
# # prepend = user priority first, then Hermes defaults
|
||||
# # append = Hermes defaults first, then user priority
|
||||
# # replace = only the list below defines priority
|
||||
# priority_mode: prepend
|
||||
# priority:
|
||||
# - my_plugin_command
|
||||
#
|
||||
# Discord-specific settings (config.yaml top-level, not under platforms:):
|
||||
#
|
||||
|
|
|
|||
|
|
@ -534,6 +534,10 @@ def telegram_bot_commands() -> list[tuple[str, str]]:
|
|||
return result
|
||||
|
||||
|
||||
_DEFAULT_TELEGRAM_MENU_MAX_COMMANDS = 30
|
||||
_TELEGRAM_BOT_API_MAX_COMMANDS = 100
|
||||
_TELEGRAM_PRIORITY_MODES = {"prepend", "append", "replace"}
|
||||
|
||||
_TELEGRAM_MENU_PRIORITY = (
|
||||
# Most-typed everyday commands first.
|
||||
"help",
|
||||
|
|
@ -571,12 +575,92 @@ need to survive the visible menu cap ahead of lower-priority built-ins.
|
|||
"""
|
||||
|
||||
|
||||
def _nested_mapping(root: Mapping[str, Any], *path: str) -> Mapping[str, Any]:
|
||||
node: Any = root
|
||||
for key in path:
|
||||
if not isinstance(node, Mapping):
|
||||
return {}
|
||||
node = node.get(key)
|
||||
return node if isinstance(node, Mapping) else {}
|
||||
|
||||
|
||||
def _telegram_command_menu_config() -> dict[str, Any]:
|
||||
"""Return normalized Telegram command-menu config with safe defaults.
|
||||
|
||||
Canonical user-facing path:
|
||||
``platforms.telegram.extra.command_menu``.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.config import read_raw_config
|
||||
raw_cfg = read_raw_config() or {}
|
||||
except Exception:
|
||||
raw_cfg = {}
|
||||
if not isinstance(raw_cfg, Mapping):
|
||||
raw_cfg = {}
|
||||
|
||||
menu_cfg = dict(_nested_mapping(raw_cfg, "platforms", "telegram", "extra", "command_menu"))
|
||||
|
||||
max_commands = menu_cfg.get("max_commands", _DEFAULT_TELEGRAM_MENU_MAX_COMMANDS)
|
||||
try:
|
||||
max_commands = int(max_commands)
|
||||
except (TypeError, ValueError):
|
||||
max_commands = _DEFAULT_TELEGRAM_MENU_MAX_COMMANDS
|
||||
max_commands = max(1, min(_TELEGRAM_BOT_API_MAX_COMMANDS, max_commands))
|
||||
|
||||
priority_mode = str(menu_cfg.get("priority_mode") or "prepend").strip().lower()
|
||||
if priority_mode not in _TELEGRAM_PRIORITY_MODES:
|
||||
priority_mode = "prepend"
|
||||
|
||||
raw_priority = menu_cfg.get("priority")
|
||||
if isinstance(raw_priority, list):
|
||||
priority = [str(item) for item in raw_priority if str(item).strip()]
|
||||
else:
|
||||
priority = []
|
||||
|
||||
return {
|
||||
"max_commands": max_commands,
|
||||
"priority_mode": priority_mode,
|
||||
"priority": priority,
|
||||
}
|
||||
|
||||
|
||||
def telegram_menu_max_commands() -> int:
|
||||
"""Return configured Telegram BotCommand menu cap with safe bounds."""
|
||||
return int(_telegram_command_menu_config()["max_commands"])
|
||||
|
||||
|
||||
def _dedupe_sanitized_names(raw_names: list[str] | tuple[str, ...]) -> tuple[str, ...]:
|
||||
result: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw_name in raw_names:
|
||||
name = _sanitize_telegram_name(str(raw_name))
|
||||
if name and name not in seen:
|
||||
seen.add(name)
|
||||
result.append(name)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _telegram_effective_priority() -> tuple[str, ...]:
|
||||
menu_cfg = _telegram_command_menu_config()
|
||||
configured = list(_dedupe_sanitized_names(menu_cfg["priority"]))
|
||||
defaults = list(_dedupe_sanitized_names(_TELEGRAM_MENU_PRIORITY))
|
||||
|
||||
if menu_cfg["priority_mode"] == "replace":
|
||||
raw_priority = configured
|
||||
elif menu_cfg["priority_mode"] == "append":
|
||||
raw_priority = defaults + configured
|
||||
else:
|
||||
raw_priority = configured + defaults
|
||||
|
||||
return _dedupe_sanitized_names(raw_priority)
|
||||
|
||||
|
||||
def _prioritize_telegram_menu_commands(
|
||||
commands: list[tuple[str, str]],
|
||||
) -> list[tuple[str, str]]:
|
||||
priority = {
|
||||
_sanitize_telegram_name(name): index
|
||||
for index, name in enumerate(_TELEGRAM_MENU_PRIORITY)
|
||||
name: index
|
||||
for index, name in enumerate(_telegram_effective_priority())
|
||||
}
|
||||
return [
|
||||
command
|
||||
|
|
|
|||
|
|
@ -108,9 +108,6 @@ _TELEGRAM_IMAGE_EXT_TO_MIME = {
|
|||
}
|
||||
|
||||
|
||||
MAX_COMMANDS_PER_SCOPE = 30
|
||||
|
||||
|
||||
def check_telegram_requirements() -> bool:
|
||||
"""Check if Telegram dependencies are available.
|
||||
|
||||
|
|
@ -2428,11 +2425,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
BotCommandScopeAllGroupChats,
|
||||
BotCommandScopeDefault,
|
||||
)
|
||||
from hermes_cli.commands import telegram_menu_commands
|
||||
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
|
||||
# Telegram allows up to 100 commands but has an undocumented
|
||||
# payload size limit (~4KB total). Limit to 30 core commands
|
||||
# to stay well under the threshold while covering all categories.
|
||||
menu_commands, hidden_count = telegram_menu_commands(max_commands=MAX_COMMANDS_PER_SCOPE)
|
||||
# payload size limit (~4KB total). Hermes defaults to 30 to
|
||||
# stay well under the threshold while covering all categories;
|
||||
# users can tune the cap via platforms.telegram.extra.command_menu.
|
||||
max_commands = telegram_menu_max_commands()
|
||||
menu_commands, hidden_count = telegram_menu_commands(max_commands=max_commands)
|
||||
bot_commands = [BotCommand(name, desc) for name, desc in menu_commands]
|
||||
# Register for all scopes independently — Telegram picks the
|
||||
# narrowest matching scope per chat type (forum topics fall
|
||||
|
|
@ -2451,7 +2450,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
if hidden_count:
|
||||
logger.info(
|
||||
"[%s] Telegram menu: %d commands registered, %d hidden (over %d limit). Use /commands for full list.",
|
||||
self.name, len(menu_commands), hidden_count, 30,
|
||||
self.name, len(menu_commands), hidden_count, max_commands,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
|
|
@ -6184,8 +6183,8 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
if chat_id in self._forum_command_registered:
|
||||
return
|
||||
from telegram import BotCommand, BotCommandScopeChat
|
||||
from hermes_cli.commands import telegram_menu_commands
|
||||
menu_commands, _ = telegram_menu_commands(max_commands=MAX_COMMANDS_PER_SCOPE)
|
||||
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
|
||||
menu_commands, _ = telegram_menu_commands(max_commands=telegram_menu_max_commands())
|
||||
bot_commands = [BotCommand(name, desc) for name, desc in menu_commands]
|
||||
await self._bot.set_my_commands(bot_commands, scope=BotCommandScopeChat(chat_id=chat_id))
|
||||
self._forum_command_registered.add(chat_id)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from hermes_cli.commands import (
|
|||
slack_subcommand_map,
|
||||
telegram_bot_commands,
|
||||
telegram_menu_commands,
|
||||
telegram_menu_max_commands,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1154,6 +1155,149 @@ class TestTelegramMenuCommands:
|
|||
):
|
||||
assert name in names
|
||||
|
||||
def test_configured_priority_prepends_plugin_commands(self, tmp_path, monkeypatch):
|
||||
"""Configured Telegram priorities keep local/plugin commands visible."""
|
||||
from unittest.mock import patch
|
||||
import hermes_cli.plugins as plugins_mod
|
||||
|
||||
plugin_dir = tmp_path / "plugins" / "cmd-plugin"
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
"name: cmd-plugin\nversion: 0.1.0\ndescription: Test plugin\n"
|
||||
)
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"def register(ctx):\n"
|
||||
" ctx.register_command('lcm', lambda args: 'ok', description='LCM status and diagnostics')\n"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"plugins:\n"
|
||||
" enabled:\n"
|
||||
" - cmd-plugin\n"
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" priority_mode: prepend\n"
|
||||
" priority:\n"
|
||||
" - lcm\n"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
with patch.object(plugins_mod, "_plugin_manager", None):
|
||||
menu, _hidden = telegram_menu_commands(max_commands=30)
|
||||
|
||||
names = [name for name, _desc in menu]
|
||||
assert names[0] == "lcm"
|
||||
assert "help" in names[1:]
|
||||
|
||||
def test_configured_priority_append_keeps_defaults_before_user_priority(self, tmp_path, monkeypatch):
|
||||
"""append mode preserves built-in defaults ahead of configured names."""
|
||||
from unittest.mock import patch
|
||||
import hermes_cli.plugins as plugins_mod
|
||||
|
||||
plugin_dir = tmp_path / "plugins" / "cmd-plugin"
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
"name: cmd-plugin\nversion: 0.1.0\ndescription: Test plugin\n"
|
||||
)
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"def register(ctx):\n"
|
||||
" ctx.register_command('lcm', lambda args: 'ok', description='LCM status and diagnostics')\n"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"plugins:\n"
|
||||
" enabled:\n"
|
||||
" - cmd-plugin\n"
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" priority_mode: append\n"
|
||||
" priority:\n"
|
||||
" - lcm\n"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
with patch.object(plugins_mod, "_plugin_manager", None):
|
||||
menu, _hidden = telegram_menu_commands(max_commands=30)
|
||||
|
||||
names = [name for name, _desc in menu]
|
||||
assert names.index("help") < names.index("lcm")
|
||||
|
||||
def test_configured_priority_replace_ignores_builtin_priority_order(self, tmp_path, monkeypatch):
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" priority_mode: replace\n"
|
||||
" priority:\n"
|
||||
" - status\n"
|
||||
" - help\n"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
menu, _hidden = telegram_menu_commands(max_commands=5)
|
||||
names = [name for name, _desc in menu]
|
||||
|
||||
assert names[:2] == ["status", "help"]
|
||||
|
||||
def test_telegram_menu_max_commands_uses_config_with_safe_bounds(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
assert telegram_menu_max_commands() == 30
|
||||
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: 12\n"
|
||||
)
|
||||
assert telegram_menu_max_commands() == 12
|
||||
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: 250\n"
|
||||
)
|
||||
assert telegram_menu_max_commands() == 100
|
||||
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: 0\n"
|
||||
)
|
||||
assert telegram_menu_max_commands() == 1
|
||||
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"platforms:\n"
|
||||
" telegram:\n"
|
||||
" extra:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: nope\n"
|
||||
)
|
||||
assert telegram_menu_max_commands() == 30
|
||||
|
||||
def test_telegram_menu_ignores_undocumented_command_menu_paths(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"telegram:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: 12\n"
|
||||
"gateway:\n"
|
||||
" platforms:\n"
|
||||
" telegram:\n"
|
||||
" command_menu:\n"
|
||||
" max_commands: 9\n"
|
||||
)
|
||||
|
||||
assert telegram_menu_max_commands() == 30
|
||||
|
||||
def test_includes_plugin_commands_via_lazy_discovery(self, tmp_path, monkeypatch):
|
||||
"""Telegram menu generation should discover plugin slash commands on first access."""
|
||||
from unittest.mock import patch
|
||||
|
|
|
|||
|
|
@ -79,6 +79,31 @@ Notes:
|
|||
profile-text indicator.
|
||||
- Off by default, since it mutates the bot's global profile.
|
||||
|
||||
### Command menu priority and cap (Optional)
|
||||
|
||||
Hermes registers its command menu automatically when the Telegram gateway starts. The menu is built from the central slash-command registry plus eligible plugin/skill commands, then capped to a safe default of 30 commands so Telegram accepts the payload reliably.
|
||||
|
||||
If you have local or plugin commands that should stay visible in Telegram's `/` picker, prioritize them in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
command_menu:
|
||||
max_commands: 30
|
||||
priority_mode: prepend # prepend | append | replace
|
||||
priority:
|
||||
- my_plugin_command
|
||||
```
|
||||
|
||||
`priority_mode` controls how your list combines with Hermes' built-in priority list:
|
||||
|
||||
- `prepend`: put your commands first, then Hermes defaults
|
||||
- `append`: keep Hermes defaults first, then your commands
|
||||
- `replace`: use only your list for priority ordering
|
||||
|
||||
Telegram allows up to 100 BotCommands, but large command payloads can fail. Hermes defaults to 30 for reliability and clamps configured values to `1..100`; use `/commands` for the full command list.
|
||||
|
||||
## Step 3: Privacy Mode (Critical for Groups)
|
||||
|
||||
Telegram bots have a **privacy mode** that is **enabled by default**. This is the single most common source of confusion when using bots in groups.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue