feat(telegram): support quick-command-only menus

This commit is contained in:
stevehq26-bot 2026-05-18 14:38:37 +01:00 committed by Teknium
parent e80d3084e5
commit b1acf80e17
7 changed files with 144 additions and 3 deletions

View file

@ -1070,7 +1070,7 @@ def load_gateway_config() -> GatewayConfig:
if isinstance(group_allowed_chats, list):
group_allowed_chats = ",".join(str(v) for v in group_allowed_chats)
os.environ["TELEGRAM_GROUP_ALLOWED_CHATS"] = str(group_allowed_chats)
for _telegram_extra_key in ("guest_mode", "disable_link_previews"):
for _telegram_extra_key in ("guest_mode", "disable_link_previews", "command_menu"):
if _telegram_extra_key in telegram_cfg:
plat_data = platforms_data.setdefault(Platform.TELEGRAM.value, {})
if not isinstance(plat_data, dict):

View file

@ -1507,11 +1507,28 @@ class TelegramAdapter(BasePlatformAdapter):
BotCommandScopeDefault,
BotCommandScopeChat,
)
from hermes_cli.commands import telegram_menu_commands
from hermes_cli.commands import (
telegram_menu_commands,
telegram_quick_menu_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)
if self.config.extra.get("command_menu") == "quick_commands_only":
# Fetch quick_commands via the gateway runner reference if
# available; otherwise fall back to PlatformConfig.extra.
_qc = self.config.extra.get("quick_commands")
if not isinstance(_qc, dict) or not _qc:
_runner_ref = getattr(self, "_runner_ref", None)
_runner = _runner_ref() if callable(_runner_ref) else None
_gw_cfg = getattr(_runner, "config", None) if _runner else None
_qc = getattr(_gw_cfg, "quick_commands", {}) or {}
menu_commands, hidden_count = telegram_quick_menu_commands(
_qc,
max_commands=MAX_COMMANDS_PER_SCOPE,
)
else:
menu_commands, hidden_count = telegram_menu_commands(max_commands=MAX_COMMANDS_PER_SCOPE)
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

View file

@ -740,6 +740,42 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
return all_commands[:max_commands], hidden_count
def telegram_quick_menu_commands(
quick_commands: Mapping[str, Any] | None,
max_commands: int = 100,
) -> tuple[list[tuple[str, str]], int]:
"""Return Telegram BotCommands for profile-defined quick commands only.
Specialist Telegram bots often use ``quick_commands`` as their whole
user-facing interface. This helper lets a profile opt into a focused
Telegram slash menu without exposing every generic Hermes command.
``show_in_telegram_menu: false`` hides a quick command from the native
menu while leaving gateway dispatch unchanged.
"""
if not isinstance(quick_commands, Mapping):
return [], 0
menu: list[tuple[str, str]] = []
seen: set[str] = set()
for raw_name, raw_config in quick_commands.items():
if not isinstance(raw_name, str) or not isinstance(raw_config, Mapping):
continue
if raw_config.get("show_in_telegram_menu") is False:
continue
name = _sanitize_telegram_name(raw_name)
if not name or name in seen:
continue
desc = str(raw_config.get("description") or f"Run /{raw_name}")
if len(desc) > 40:
desc = desc[:37] + "..."
menu.append((name, desc))
seen.add(name)
hidden_count = max(0, len(menu) - max_commands)
return menu[:max_commands], hidden_count
def discord_skill_commands(
max_slots: int,
reserved_names: set[str],

View file

@ -270,6 +270,25 @@ class TestLoadGatewayConfig:
assert config.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
def test_bridges_telegram_command_menu_from_config_yaml(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
config_path = hermes_home / "config.yaml"
config_path.write_text(
"telegram:\n"
" command_menu: quick_commands_only\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
config = load_gateway_config()
assert (
config.platforms[Platform.TELEGRAM].extra["command_menu"]
== "quick_commands_only"
)
def test_bridges_group_sessions_per_user_from_config_yaml(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()

View file

@ -26,6 +26,7 @@ from hermes_cli.commands import (
slack_subcommand_map,
telegram_bot_commands,
telegram_menu_commands,
telegram_quick_menu_commands,
)
@ -1153,6 +1154,47 @@ class TestTelegramMenuCommands:
# No empty string in menu names
assert "" not in menu_names
def test_quick_menu_commands_include_profile_quick_commands_only(self):
menu, hidden = telegram_quick_menu_commands(
{
"agent-health": {
"type": "exec",
"command": "echo ok",
"description": "Show agent health",
},
"hidden": {
"type": "exec",
"command": "echo hidden",
"description": "Hidden command",
"show_in_telegram_menu": False,
},
}
)
assert hidden == 0
assert menu == [("agent_health", "Show agent health")]
def test_quick_menu_commands_sanitize_dedupe_and_trim_descriptions(self):
menu, hidden = telegram_quick_menu_commands(
{
"agent-health": {
"description": "A" * 80,
},
"agent_health": {
"description": "Duplicate after sanitization",
},
"+++": {
"description": "Sanitizes to empty",
},
},
max_commands=1,
)
assert hidden == 0
assert len(menu) == 1
assert menu[0][0] == "agent_health"
assert menu[0][1] == ("A" * 37) + "..."
# ---------------------------------------------------------------------------
# Backward-compat aliases

View file

@ -126,6 +126,11 @@ quick_commands:
Then type `/status`, `/deploy`, or `/inbox` in the CLI or a messaging platform. Quick commands are resolved at dispatch time and may not appear in every built-in autocomplete/help table.
For specialist Telegram bots, set `telegram.command_menu: quick_commands_only`
to make Telegram's native slash menu show only profile-defined quick commands.
Set `show_in_telegram_menu: false` on a quick command to keep it callable but
hide it from the Telegram menu.
String-only prompt shortcuts are not supported as quick commands. Put longer reusable prompts in a skill, or use `type: alias` to point at an existing slash command.
### Custom model aliases

View file

@ -1417,6 +1417,28 @@ Usage: type `/status`, `/disk`, `/update`, `/gpu`, or `/restart` in the CLI or a
- **Type** — supported types are `exec` and `alias`; other types show an error
- **Works everywhere** — CLI, Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant
Telegram profiles can opt into a focused BotCommand menu that shows only
profile-defined quick commands:
```yaml
telegram:
command_menu: quick_commands_only
quick_commands:
health:
type: exec
command: scripts/health.sh
description: Show service health
internal-debug:
type: exec
command: scripts/debug.sh
description: Internal debug helper
show_in_telegram_menu: false
```
`show_in_telegram_menu: false` hides a quick command from Telegram's native
slash menu while leaving the command callable.
String-only prompt shortcuts are not valid quick commands. For reusable prompt workflows, create a skill or alias to an existing slash command.
## Human Delay