Custom executors for the Hermes agent Telegram inline query surface.
Type @botname <query> in any Telegram chat to trigger inline tools without the bot being a member of the group.
The inline query pipeline has three layers:
TelegramInlineRouter(gateway/platforms/telegram_inline_router.pyin hermes-agent) — loads~/.hermes/inline_tools.yaml, matches the query to a tool, and dispatches to an executor.- Executor — a Python class that subclasses
InlineExecutor, placed in~/.hermes/inline_executors/*.py. Loaded automatically on gateway start. - Executors own their full result lifecycle. Return
List[InlineQueryResult]— ready-to-send Telegram objects. For slow operations, return a stub immediately.
- Copy executor files to
~/.hermes/inline_executors/ - Add the corresponding tool entry to
~/.hermes/inline_tools.yaml(seeinline_tools.yaml.example) - Restart the gateway
Search the agent's outbound message history and send a past response into any chat.
Trigger: @botname #<search term>
Example: @botname #release notes → finds the most recent message containing "release notes" and offers it as an inline result to tap and send.
Scope: searches the 5 most recent sessions by default. Configurable via session_window in inline_tools.yaml:
- id: repost
executor: inline_repost
session_window: 3 # search last 3 sessions instead of 5Auth: enforces TELEGRAM_ALLOWED_USERS independently (the adapter-level inline handler has no allowlist gate — see Security note).
Requires: hermes-agent with the pluggable inline executor API (PR #50884 or later — InlineExecutor ABC + register_executor()).
Query routing uses a two-stage model implemented in inline_classifier.py. On each inline query the classifier first checks whether any enabled tool declares a prefix: field that matches the query start — if so, only those tools are dispatched (O(1), no model inference). For queries that don't match any prefix, the classifier encodes the query with fastembed (BAAI/bge-small-en-v1.5) and computes cosine similarity against each tool's description: embedding, dispatching only tools above a configurable threshold. Both stages merge results in FILO order (last-registered tool wins on tie). Tool embeddings are built once at plugin init and rebuilt whenever inline_tools.yaml mtime changes.
Copy executor_template.py, implement execute(), and add a register() function:
from gateway.platforms.telegram_inline_router import InlineExecutor
from telegram import InlineQueryResultArticle, InputTextMessageContent
class MyExecutor(InlineExecutor):
def __init__(self, tool_config, bot):
self._config = tool_config
async def execute(self, user_id, query):
return [
InlineQueryResultArticle(
id="result",
title="Result title",
description="Short description",
input_message_content=InputTextMessageContent(message_text="your result here"),
)
]
def register(router):
router.register_executor("my_executor", lambda cfg, bot: MyExecutor(cfg, bot))Add an entry to inline_tools.yaml:
- id: my_tool
type: direct
executor: my_executor
description: "What this tool does, used by the classifier"
prefix: "!"
match:
- pattern: "^!"
type: prefix
priority: 0
timeout_sec: 10
enabled: truetype |
Behaviour |
|---|---|
prefix |
re.match(pattern, query) — anchored left |
url |
re.search(pattern, query) — URL anywhere |
search |
catch-all, matches everything |
Lower priority wins when multiple tools could match.
Executors return List[InlineQueryResult] — Telegram objects passed directly to iq.answer().
| Telegram type | Common constructor args |
|---|---|
InlineQueryResultArticle |
id, title, description, input_message_content |
InlineQueryResultCachedAudio |
id, audio_file_id, caption |
Auth is enforced at the adapter level. Executors accessing sensitive data may add a second check as defense-in-depth.
The inline handler has a ~10 s empirical deadline; the adapter enforces 6.5 s via INLINE_RESPONSE_DEADLINE. Executors that need longer should return a stub result immediately and manage their own background state.