feat(telegram): config-driven callback→text routes for inline keyboards - #43949
feat(telegram): config-driven callback→text routes for inline keyboards#43949tjkang wants to merge 1 commit into
Conversation
platforms.telegram.extra.callback_text_routes maps inline-keyboard callback_data strings (exact match) to text injected into the conversation as if the tapping user had typed it. Lets deployments wire buttons sent outside the agent's own toolchain (skills / cron jobs via the raw Bot API) back into the normal conversation flow: authorization check, callback ack, best-effort prompt edit (append choice + strip keyboard), then a full-context MessageEvent through handle_message. Routes are consulted as the final step of _handle_callback_query, after every built-in prefix handler has declined the query — chain position guarantees built-in callbacks always win. Parse-time validation drops invalid entries and keys over Telegram's 64-byte callback_data limit.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused, config-driven route design. The problem remains present on current main: the active handler returns for unrecognized callback data at plugins/platforms/telegram/adapter.py:5637-5639.
Problems
- The PR modifies
gateway/platforms/telegram.py, but current main no longer tracks that adapter. Commit476d8d9ccmigrated Telegram into the plugin surface, and polling now registersplugins/platforms/telegram/adapter.py::_handle_callback_queryatplugins/platforms/telegram/adapter.py:3202-3203. The implementation and its tests therefore do not affect the runtime Telegram adapter.
Suggested changes
- Port the parser, route handler, initialization, and final callback fallback into
plugins/platforms/telegram/adapter.py; port the test import toplugins.platforms.telegram.adapteras current Telegram tests do. - Add the config example to
website/docs/user-guide/messaging/telegram.md, which already documentsplatforms.telegram.extrasettings.
Automated hermes-sweeper review.
| 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) | ||
| # Config-driven callback→text routes (extra.callback_text_routes): |
There was a problem hiding this comment.
Blocking: current main no longer contains gateway/platforms/telegram.py; Telegram now runs from plugins/platforms/telegram/adapter.py (migration 476d8d9cc). Please port this initialization and the associated handler changes to the active plugin adapter.
Independent validation + conflict diagnosisI ran this exact design in production today (same config-driven
On the merge conflictThe PR is currently conflicting with main. The cause: the Telegram adapter moved from Suggested extension: prefix routes (applicable beyond our use case)While validating, I extended the design with one small, generally useful addition: prefix route keys (keys ending in Use cases this unlocks beyond my own:
Happy to rebase this PR onto current main (with or without the prefix extension) if that is useful — just say the word. This feature would unblock a class of cron/skill-driven button workflows that currently require patching the gateway. |
What does this PR do?
Adds config-driven callback→text routes to the Telegram adapter:
platforms.telegram.extra.callback_text_routesmaps inline-keyboardcallback_datastrings (exact match) to text that is injected into the conversation as if the tapping user had typed it.Problem. Deployments that send inline-keyboard prompts outside the agent's own toolchain — e.g. a cron job or skill posting a morning check-in via the raw Bot API — have no way to route the button tap back into the normal conversation flow. The callback query arrives at
_handle_callback_query, matches no built-in prefix, and is silently dropped. The only workaround today is a stickyreply_keyboard(poor UX) or patching the gateway.Solution. A config-level escape hatch:
When an authorized user taps a button whose
callback_datamatches a route, the adapter:_is_callback_user_authorized(same gate as all built-in callbacks),MessageEvent(chat, user, thread, topic flags) via_build_message_eventand dispatches it throughhandle_message, so session routing, skills, and logging treat it exactly like a typed message.Why this approach / relation to open PRs. The send-side PRs (#42800, #42865, #29338, #38731, #28682) let the agent emit buttons and bundle their own receive paths. This PR is the orthogonal receive-side piece for buttons created outside
send_message(skills, cron, external tooling), and is config-only:callback_datakey is bounded),MessageEvent), so multi-topic and group-attribution routing keep working.Safety. Routes are consulted as the final step of
_handle_callback_query, only after every built-in prefix handler has declined the query — chain position alone guarantees config routes can never shadow built-in callbacks (mp:,gt:,ea:,update_prompt:, …), with no hand-maintained reserved-prefix list to drift. Parse-time validation drops non-string entries, empty text, and keys over Telegram's 64-bytecallback_datalimit with a warning. Injected text is attributed to the tapping user and passes the same authorization gate as typing — no new trust surface.Related Issue
N/A — feature extracted from a long-running production deployment (daily companion bot) where it has been running as a local patch.
Type of Change
Changes Made
gateway/platforms/telegram.py:_parse_callback_text_routes()(parsed once in__init__) +_handle_callback_text_route()invoked as the final step of_handle_callback_querycli-config.yaml.example: documentedextra.callback_text_routeswith exampletests/gateway/test_telegram_callback_text_routes.py: 7 tests — routed tap (full context), keyboard strip + choice echo, unknown data fall-through, unauthorized rejection, edit-failure still routes, invalid config ignored, oversized/empty entries droppedHow to Test
pytest tests/gateway/test_telegram_callback_text_routes.py -q→ 8 passedpytest tests/ -q→ full suite passesconfig.yaml, send a message with a matchinginline_keyboardbutton via the Bot API, tap it → the mapped text is appended to the prompt, the keyboard disappears, and the agent responds as if the text was typed.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — docstrings + config examplecli-config.yaml.exampleif I added/changed config keysCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A