feat(gateway): add pre_callback_query_dispatch plugin hook for Telegram inline keyboard callbacks - #21471
Conversation
Fires a new `pre_callback_query_dispatch` hook at the entry point of
`_handle_callback_query` so plugins can intercept inline keyboard button
clicks before any built-in prefix handling runs.
A plugin returning `{"action": "skip"}` claims the callback and
suppresses all built-in handling. Returning `{"action": "allow"}` or
`None` falls through to the existing model-picker / approval / confirm
logic unchanged.
Relates to NousResearch#21461 (pre_gateway_dispatch for forum topic events).
|
Hi @ChaseFlorell — I opened #34539 earlier today not realizing this PR already existed (my fault: I searched for While I'm here, a few additive surface ideas from my implementation that may or may not be worth folding into your simpler hook — entirely up to you and the maintainer whether any of these are in-scope.
If any of these are useful, happy to send them as small follow-up PRs once this lands. If none are — totally fine, the simpler surface is also a defensible choice. Reference (closed): #34539 with the full implementation of all five. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for adding a concrete plugin seam for Telegram inline callbacks. The interception point is still absent on current main, but this needs a rework against the current adapter and hook API.
Problems
gateway/platforms/telegram.py:1923awaitsself._fire_plugin_hook, but neitherTelegramAdapternorBasePlatformAdapterdefines that method. Current dispatch is synchronous viahermes_cli/plugins.py:2047; this path would raise on every callback query.PluginManager.invoke_hook()returns a list (hermes_cli/plugins.py:1890-1925), so the proposed single-result handling does not specify multiple-plugin precedence.- A
skipreturns withoutquery.answer(), unlike built-in callback success paths (plugins/platforms/telegram/adapter.py:5363,:5427). Async plugin callbacks are not awaited by the current dispatcher.
Suggested changes
- Port the hook to
plugins/platforms/telegram/adapter.py::_handle_callback_query(Telegram moved there in560010547) and useinvoke_hook()synchronously. - Apply the existing
pre_gateway_dispatchfirst-recognized-action model fromgateway/run.py:8905-8924, define callback acknowledgement behavior, and test the real dispatcher rather than a synthetic_fire_plugin_hookattribute. - Add the new behavior-changing hook to
website/docs/user-guide/features/hooks.md.
Automated hermes-sweeper review.
| data = query.data | ||
| query_message = getattr(query, "message", None) | ||
|
|
||
| hook_result = await self._fire_plugin_hook( |
There was a problem hiding this comment.
_fire_plugin_hook is not defined by TelegramAdapter or BasePlatformAdapter; the current plugin API is synchronous hermes_cli.plugins.invoke_hook() (hermes_cli/plugins.py:2047) and returns a list. As written, every non-empty callback query will raise AttributeError; port this to the current adapter and aggregate returned action dicts explicitly.
| message_id=str(query_message.message_id) if query_message else None, | ||
| raw_query=query, | ||
| ) | ||
| if hook_result and hook_result.get("action") == "skip": |
There was a problem hiding this comment.
Please define the acknowledgement contract for skip. This returns without query.answer(), whereas built-in successful callback paths answer the query. The plugin manager invokes callbacks synchronously (hermes_cli/plugins.py:1913-1925), so plugins cannot rely on an async callback being awaited to acknowledge it.
|
I have a concrete dynamic callback consumer that currently has to monkey-patch Its callback payload is That gives me four requirements for a useful hook contract:
A first-recognized So the feature remains useful on current main, but I agree with the existing review that it should be rebuilt around the real |
What does this PR do?
Adds a
pre_callback_query_dispatchplugin hook that fires at the entry point of_handle_callback_queryinTelegramAdapter, before any built-in prefix routing (mp:,ea:,sc:, etc.).Plugins can register for this hook to intercept inline keyboard button clicks. Returning
{"action": "skip"}claims the callback and suppresses all built-in handling. Returning{"action": "allow"}orNonefalls through to existing logic unchanged — so this is a non-breaking, purely additive change.The motivation is to allow custom callback logic (e.g. a news-feedback plugin handling
nf:yes:<id>/nf:no:<id>) to live in a plugin rather than intelegram.pydirectly. Without this hook, any customisation must be patched into core gateway files and is silently lost onhermes update.Related Issue
Fixes #21469
Extends the pattern introduced in #21461 (forum topic events →
pre_gateway_dispatch). That issue covered messages; this one covers button interactions, which cannot be represented asMessageEventobjects and therefore cannot go throughpre_gateway_dispatch.Type of Change
Changes Made
hermes_cli/plugins.py: Added"pre_callback_query_dispatch"toVALID_HOOKSwith a doc-comment describing the kwargs and supported return values.gateway/platforms/telegram.py: Added_fire_plugin_hook("pre_callback_query_dispatch", ...)call at the top of_handle_callback_query, immediately after thedatavariable is set. Early-returns if a plugin returns{"action": "skip"}.tests/gateway/test_telegram_callback_query_hook.py: 7 new tests covering: hook fires with correct kwargs,skipsuppresses built-in handling,allowandNonefall through, early-exit whenqueryorquery.datais absent, andpre_callback_query_dispatchpresent inVALID_HOOKS.How to Test
pytest tests/gateway/test_telegram_callback_query_hook.py -v— all 7 tests pass.pre_callback_query_dispatchwith a handler that returns{"action": "skip"}for a custom prefix; press an inline button with that prefix — the hook fires and built-in handling is suppressed.ea:,sc:,mp:) — hook fires but returnsNone; built-in logic proceeds normally.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A