feat(telegram): tool-side inline-query dispatch - #52683
Conversation
Related: supersedes the closed #50884 (and its closed predecessor #50880) — same author, same goal (Telegram inline-query mode), but this revision keeps the adapter thin and pushes inline logic to a tool-side router instead of a second |
9099af2 to
fb99231
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for keeping the Telegram adapter thin and carrying the router tests with the feature.
Problems
plugins/platforms/telegram/adapter.py:253-296lazily re-imports and rebinds the existing PTB globals, but the newInlineQueryHandleris absent from that path. After a first import without PTB, the PR leaves it asAny; the new registration in the PR'sconnect()block then fails and the broad exception silently disables inline dispatch. Please rebind it and add coverage for the lazy-install path.gateway/platforms/telegram_inline_router.py:18-19documents tool-levelprefixandhandles, butInlineToolRegistry.match()in the PR only readstool["match"]. Either implement those declared fields or remove them from the public schema.- The new gate tests build
PlatformConfig.extradirectly.PlatformConfig.from_dict()only preservesdata["extra"](gateway/config.py:500-539), so add a config-loader test and document the exact YAML nesting required forinline.enabled.
This is an automated hermes-sweeper review.
| @@ -162,6 +162,7 @@ async def _shutdown_abandoned_app(app) -> None: | |||
| Application, | |||
| CommandHandler, | |||
| CallbackQueryHandler, | |||
| InlineQueryHandler, | |||
There was a problem hiding this comment.
check_telegram_requirements() has a lazy-install recovery path that rebinds the existing handler globals, but it will not rebind this new one. Include InlineQueryHandler as _IQH in that import and assign it globally there; otherwise an adapter imported before PTB is installed leaves this as Any and the new connect-time registration is caught and silently disabled.
| prefix: "!" # shorthand: routes queries starting with "!" to this tool | ||
| handles: [mybotname] # optional list of bot usernames; omit to match all bots | ||
| match: | ||
| - pattern: "https?://example\\.com/" |
There was a problem hiding this comment.
The public schema says tool-level prefix and handles affect routing, but match() only evaluates entries under tool["match"]. Please implement those fields (including bot filtering) or remove them from this contract until they are supported.
fb99231 to
47a6b08
Compare
|
Fixed all three, pushed
|
Adds Telegram inline-query (@bot) support as a thin adapter dispatcher over a tool-side router, rather than a parallel platform/adapter. - gateway/platforms/telegram_inline_router.py: yaml-driven registry (inline_tools.yaml) + InlineExecutor ABC + TelegramInlineRouter. The router owns query->tool matching, executor lookup, the answerInlineQuery response deadline, and discovery of user-space executors from inline_executors/. The framework ships the dispatch surface only; concrete executors are user-space. - plugins/platforms/telegram/adapter.py: ~15-line dispatch - _handle_inline_query forwards to the router; registers an InlineQueryHandler in connect(); inline.enabled in config.extra gates it. No Platform.TELEGRAM_INLINE / second adapter / config.py platform registration -- inline queries are handled by the main adapter. Tests: tests/gateway/test_telegram_inline_router.py (9) -- registry matching, dispatch paths, deadline enforcement, executor discovery. Supersedes NousResearch#50884 (the parallel-platform approach). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the executor-execution tests (running a stub through dispatch and the deadline path) — this PR ships dispatch only; the execution phase (concrete executors producing results) is a later addition. Remaining tests cover registry matching, routing decisions, and executor discovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/gateway/test_telegram_inline_query.py for _handle_inline_query: disabled -> no dispatch and no answer; enabled -> dispatch + results passthrough; unconfigured -> enabled by default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ne tests Move the answerInlineQuery RESPONSE_DEADLINE back to the adapter (_handle_inline_query wraps dispatch in wait_for) instead of enforcing it inside router.dispatch. The adapter-side deadline holds even when an executor layer replaces the router's dispatch (e.g. a classifier), which an in-dispatch deadline would silently lose. Adds adapter tests: the full dispatch -> executor -> answer path with a stand-in executor, and the deadline-cancel path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t prefix/handles; document inline.enabled nesting
hermes-sweeper review of this PR raised three points:
1. check_telegram_requirements()'s lazy-install rebind path was missing
InlineQueryHandler from its `global` declaration, its re-import from
telegram.ext, and its rebind assignment. After a first import without
python-telegram-bot installed, InlineQueryHandler stayed pinned at the
module-load-failure fallback (`Any`) even once TELEGRAM_AVAILABLE
flipped True post-install. connect()'s
InlineQueryHandler(self._handle_inline_query) call then raised
(Any(...) isn't callable), caught by that call site's broad
`except Exception` and logged as "inline-query dispatch not enabled"
with no other symptom. Added InlineQueryHandler to all three places
and a regression test that exercises the actual rebind path.
2. gateway/platforms/telegram_inline_router.py's docstring documented
`prefix` (shorthand for a single type=prefix matcher) and `handles`
(per-tool bot-username allowlist) as registry fields, but
InlineToolRegistry.match() only ever read `match`. Implemented both:
`prefix` folds into the tool's effective matcher list (escaped, not
treated as a regex), and `handles` filters candidate tools by the
router's bot_username before ranking — TelegramInlineRouter.dispatch()
now threads its own bot_username into match() so the filter is
actually enforced during real dispatch, not just when calling
match() directly.
3. The gate tests built PlatformConfig.extra directly, masking that
PlatformConfig.from_dict() only ever populates .extra from the YAML
extra: block — inline is not one of the shared/bridged top-level
platform keys (allow_from, require_mention, etc.), so
`telegram: inline: {...}` written as a sibling of enabled/token is
silently dropped; it must be `telegram: extra: inline: {...}`.
Documented this exactly on _handle_inline_query's docstring and added
two load_gateway_config() tests: one proving the extra: nesting works,
one proving the flat/top-level form doesn't.
Also rebased this branch onto origin/main (660 commits behind).
Confirmed the usual 9 telegram test-order-pollution failures elsewhere in
the suite are unrelated (same ones from earlier PRs this session), and
that two of the new/existing inline tests (test_inline_end_to_end_with_
stub_executor, test_dispatch_threads_bot_username_to_registry_handles_
filter) fall victim to that same pre-existing pollution only when run
together with the full test_config.py suite — verified via git stash
that test_inline_end_to_end_with_stub_executor already failed this way
before any of these changes. All inline-specific test files pass cleanly
in isolation (20 tests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CvMiTppKDazXfakuoLgQK
check-windows-footguns.py flagged open() without encoding= in InlineToolRegistry._load() -- platform-default encoding (cp1252/mbcs on Windows) can mojibake a registry file authored on POSIX. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CvMiTppKDazXfakuoLgQK
47a6b08 to
f8090b6
Compare
…lability
check_telegram_requirements() bundled InlineQueryHandler into the same
`from telegram.ext import (...)` statement as CallbackQueryHandler,
MessageHandler, ContextTypes, and filters (both at the module-level import
and the lazy-install rebind path). Any environment whose telegram.ext
doesn't define InlineQueryHandler -- an older PTB release predating inline
mode, or (as several pre-existing test files' minimal telegram.ext doubles
turned out to be) a mock that only stubs the handlers it exercises --
raised ImportError for the WHOLE statement, leaving ParseMode, filters,
etc. pinned at their None/Any import-failure fallbacks. That broke real
Telegram sends entirely ('NoneType' object has no attribute 'MARKDOWN_V2'),
not just inline-query dispatch -- surfaced as 7 failures in
tests/gateway/test_telegram_thread_fallback.py once this branch rebased
onto current main and picked up whatever else runs before it in a shared
test process.
Fix: give InlineQueryHandler its own try/except in both import sites,
mirroring the existing LinkPreviewOptions optional-import pattern already
a few lines above each -- its absence now only disables the inline-query
bonus feature, exactly as intended.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CvMiTppKDazXfakuoLgQK
|
Rebased onto current Also fixed a real bug found while testing the rebase: Fixed by giving Also fixed a Windows footgun ( Full |
|
After some experiments, I've decided inline mode isn't conducive to interacting with an agent at this stage, and isn't useful enough to justify keeping ше on the bot: you can't get a response from the agent fast enough given the short response deadline, and the kinds of tools that could realistically run within that window aren't useful enough to be worth it. It also interferes with much more useful guest mode (PR #56476) in practice: on most clients, typing the agent's @handle switches the composer into inline-query mode, which then blocks you from actually sending the message that would trigger guest mode. It doesn't block the interaction completely because the bot can still be called in guest mode by typing @handle after the request, but in my opinion it creates enough friction to be annoying. Not worth it for what inline mode was delivering - closing this out. |
check-windows-footguns.py flagged open() without encoding= -- platform-default encoding (cp1252/mbcs on Windows) can mojibake this file's contents. Rebased onto the fixed feat/telegram-inline-toolside (NousResearch#52683) as part of today's PR-conflict drift audit -- both PRs insert a handler registration at the same point in TelegramAdapter.connect(); purely textual, kept both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CvMiTppKDazXfakuoLgQK
Summary
Adds Telegram inline-query (
@bot …) support as a thin adapter dispatcher over a tool-side router. The Telegram adapter stays a ~15-line dispatcher; all inline logic — query→tool matching, executor lookup, the response deadline, and executor discovery — lives in the router, and concrete executors are user-space.Replaces the earlier inline-mode draft #50884.
Design
gateway/platforms/telegram_inline_router.py(new) — the dispatch surface:InlineToolRegistryloadsinline_tools.yamland matches a query to a tool (url / prefix / search patterns, priority-ordered).InlineExecutorABC — the user-space contract:execute(user_id, query) -> [InlineQueryResult].TelegramInlineRouter—register_executor,discover_executors()(loads modules frominline_executors/), anddispatch()which enforces theanswerInlineQueryRESPONSE_DEADLINE(6.5 s) router-side.plugins/platforms/telegram/adapter.py— ~15 lines: register anInlineQueryHandlerinconnect(), and_handle_inline_queryforwards to the router.inline.enabled: falsein the platform'sconfig.extrasilences it without unregistering the handler. No new platform, noconfig.pyregistration.Scope: dispatch only — execution phase to follow
This PR ships the dispatch surface: query→tool matching, executor registration/discovery, and the response-deadline contract. It does not yet include an execution phase — the framework bundles no concrete executors, and
dispatch()returns empty when no user-space executor is registered for a matched tool. Concrete executors (and any framework-level execution helpers) are a deliberate follow-up; for now they live entirely in user-space (inline_executors/, each module definingregister(router)).For an illustrative set of executors written against this contract — media lookup, repost, an optional classifier layer — see hermes-telegram-inline-tools. Example only; not bundled, required, or endorsed by this PR.
Why now
This lands ahead of the rest of the guest-mode work on purpose. Inline-query code had accreted inside
plugins/platforms/telegram/adapter.py, and that bloat is the main source of the rebase conflicts across the guest-mode PR stack. Extracting inline mode into a thin dispatcher + router removes ~120 lines from the adapter, giving the guest-mode chain a clean base to rebase onto — so this is a prerequisite for landing guest mode cleanly, not just a standalone feature.Tests
tests/gateway/test_telegram_inline_router.py(7): registry matching (url / prefix+priority / disabled / missing-file), dispatch routing (no-match, unregistered executor), and executor discovery.tests/gateway/test_telegram_inline_query.py(5): the adapter'sinline.enabledgate (disabled / enabled / default), the full dispatch→executor→answer()path with a stand-in executor, and the response-deadline cancel path.The framework still bundles no executors (execution phase deferred); the end-to-end test registers a stand-in executor to exercise the wiring. Existing platform suite (162) unaffected; sources compile.
Test plan
inline.enabledgate, e2e wiring with a stand-in executor, deadline cancel)inline.enabled: false→ inline silently ignored (unit-tested)answer()(unit-tested with a stand-in executor)@bot <query>returns results in the clientUpdate: fixed the lazy-install rebind bug, implemented prefix/handles, documented inline.enabled nesting (addresses hermes-sweeper review)
Also rebased onto
origin/main(660 commits behind).Blocking:
InlineQueryHandlernever rebound after lazy install.check_telegram_requirements()'s rebind path (used when python-telegram-bot isn't installed at first import and gets lazy-installed on demand) was missingInlineQueryHandlerfrom itsglobaldeclaration, its re-import, and its rebind assignment — the other handler classes (CommandHandler,CallbackQueryHandler, etc.) were all there, this one just got missed. After a successful lazy install,InlineQueryHandlerstayed pinned at the module-load-failure fallback (Any), soconnect()'sInlineQueryHandler(self._handle_inline_query)call raised (Any(...)isn't callable) and was silently swallowed by that call site's own broadexcept Exception— logged as "inline-query dispatch not enabled" with no other symptom. Fixed all three spots and added a regression test exercising the real rebind path.prefix/handles— implemented rather than removed. Both are useful as documented (a literal-prefix shorthand and a per-tool bot-username allowlist for shared registries), so implemented them inInlineToolRegistry.match()instead of stripping them from the schema:prefixfolds into the tool's effective matcher list (escaped, not a raw regex), andhandlesfilters candidate tools by the router'sbot_usernamebefore ranking.TelegramInlineRouter.dispatch()now threads its ownbot_usernameintomatch()sohandlesis actually enforced during real dispatch, not just when callingmatch()directly by hand. 8 new tests cover both, including case-insensitive@handling forhandlesand thatprefixis treated as a literal, not a regex wildcard.inline.enablednesting — documented and tested.PlatformConfig.from_dict()only ever populates.extrafrom the YAMLextra:block —inlineisn't one of the shared/bridged top-level platform keys (allow_from,require_mention, etc. — see the shared-key loop inload_gateway_config()), sotelegram: inline: {...}written as a sibling ofenabled/tokenis silently dropped; it must betelegram: extra: inline: {...}. Documented exactly this on_handle_inline_query's docstring, with the working YAML alongside the broken form, and added twoload_gateway_config()tests: one proving theextra:nesting round-trips correctly, one proving the flat/top-level form is silently ignored (so a regression here fails loudly instead of quietly breaking someone's config).Confirmed the usual 9 telegram test-order-pollution failures elsewhere in the suite are unrelated (same ones from earlier PRs this session), and that
test_inline_end_to_end_with_stub_executor(pre-existing) plus the newtest_dispatch_threads_bot_username_to_registry_handles_filterfall victim to that same pollution only when run together with the fulltest_config.pysuite — confirmed viagit stashthat the pre-existing test already failed this way before any of these changes. All inline-specific test files (20 tests) pass cleanly in isolation.🤖 Generated with Claude Code