Python: Add Telegram channel for agent-framework-hosting#6698
Python: Add Telegram channel for agent-framework-hosting#6698eavanvalkenburg wants to merge 6 commits into
Conversation
- Add agent-framework-hosting-telegram package with TelegramChannel supporting polling and webhook transports, streaming edits with Telegram Bot API rate limiting, per-chat serial workers, and multi-modal inbound/outbound (text, photo, document, voice) - Add local_telegram sample demonstrating multi-channel hosting with a TelegramChannel alongside ResponsesChannel, using per-chat FileHistoryProvider and a run_hook for Telegram persona temperature - Fix test layout: move tests to tests/hosting_telegram/ (no __init__.py) - Remove old [tool.mypy] section and mypy poe task; source type-checking is handled by pyright via shared_tasks - Update uv.lock, pyproject.toml workspace sources, and PACKAGE_STATUS.md Fixes microsoft#6588 Refs microsoft#6265 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 60%
✓ Correctness
The streaming edit worker has a deadlock bug: when the agent streams more than 4096 characters,
accumulatedwill never equallast_sent(which is truncated to 4096), causing the while-loop termination condition to remain False forever. Afterworker_doneis set and the final truncated snapshot has already been sent, the worker hitsawait wake.wait()with no one to wake it, hangingawait edit_taskin the finally block indefinitely.
✓ Security Reliability
The primary security concern is the use of Python's
!=operator for webhook secret token comparison instead ofhmac.compare_digest(), which is vulnerable to timing side-channel attacks. An attacker who discovers the webhook URL could exploit response-time differences to brute-force the secret token character by character. Additionally, per-chat worker tasks and queues grow without bound over the lifetime of the process since they are never evicted after a chat goes idle, which is a reliability concern for long-running deployments with many unique chats.
✓ Test Coverage
The test suite covers parsing helpers, webhook auth, per-chat ordering, ack-before-run, and shutdown draining well. However, the streaming path (
_stream_to_chat) — which is ~140 lines of complex async coordination (edit worker, rate-limiting, typing keepalive, parse_mode stripping/restoration, final edit fallback) and the PR rationale's explicit reviewer focus — has zero test coverage. The callback_query handler and photo-output path are also untested.
✓ Failure Modes
The edit_worker in the streaming path has a deadlock when the initial placeholder message fails to send. If message_id stays None (network error, rate limit, bot blocked), the worker loops indefinitely at
await wake.wait()after the stream finishes because the only wake-seters have already fired and the worker'scontinuepath doesn't check the termination condition. This permanently stalls the per-chat worker.
✗ Design Approach
The new Telegram channel mostly follows the host’s channel contract, but there is one design bug in the streaming path: streamed replies can silently lose outbound images. Because this package defaults Telegram requests to streaming, the current approach misses a core advertised capability for a common response shape.
Flagged Issues
- In python/packages/hosting-telegram/agent_framework_hosting_telegram/_channel.py:721-744, the streaming finalization path only edits text and only falls back to _reply_with_result when no text accumulated. Since image sending exists only in _reply_with_result at lines 746-768, any streamed response that emits text deltas and also includes final image Content will drop the image entirely.
Automated review by eavanvalkenburg's agents
|
Flagged issue In python/packages/hosting-telegram/agent_framework_hosting_telegram/_channel.py:721-744, the streaming finalization path only edits text and only falls back to _reply_with_result when no text accumulated. Since image sending exists only in _reply_with_result at lines 746-768, any streamed response that emits text deltas and also includes final image Content will drop the image entirely. Source: automated DevFlow PR review |
There was a problem hiding this comment.
Pull request overview
Adds a new Python hosting channel package (agent-framework-hosting-telegram) plus a local_telegram sample that hosts the same agent over both Telegram and the OpenAI Responses-shaped endpoint, including streaming via progressive Telegram message edits and per-chat session isolation.
Changes:
- Introduces
TelegramChannelwith webhook/polling transports, per-chat serial processing, and streaming via throttlededitMessageText. - Adds unit tests for Telegram update parsing, webhook handling, shutdown behavior, and per-chat ordering.
- Adds a
local_telegramsample (plus workspace/package status wiring) demonstrating multi-channel hosting and history persistence.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| python/samples/04-hosting/af-hosting/local_telegram/README.md | Documents how to run and use the new multi-channel Telegram + Responses sample. |
| python/samples/04-hosting/af-hosting/local_telegram/pyproject.toml | Sample dependencies and uv source wiring for running locally. |
| python/samples/04-hosting/af-hosting/local_telegram/call_server.py | Local OpenAI SDK client for exercising the sample’s /responses endpoint. |
| python/samples/04-hosting/af-hosting/local_telegram/app.py | Sample host wiring: agent + ResponsesChannel + TelegramChannel + hooks + commands. |
| python/pyproject.toml | Adds agent-framework-hosting-telegram to the Python workspace. |
| python/packages/hosting-telegram/tests/hosting_telegram/test_channel.py | Unit tests for parsing, webhook dispatch, ordering, and shutdown semantics. |
| python/packages/hosting-telegram/README.md | Package-level usage and session/isolation key explanation. |
| python/packages/hosting-telegram/pyproject.toml | New package metadata, deps, and tooling config (ruff/pyright/pytest/etc.). |
| python/packages/hosting-telegram/LICENSE | MIT license for the new package. |
| python/packages/hosting-telegram/agent_framework_hosting_telegram/_channel.py | Core TelegramChannel implementation (polling/webhook, streaming edits, media handling). |
| python/packages/hosting-telegram/agent_framework_hosting_telegram/init.py | Public exports for TelegramChannel and telegram_isolation_key. |
| python/PACKAGE_STATUS.md | Marks the new package as alpha. |
- Fix webhook secret validation to use constant-time compare_digest - Harden webhook update parsing: require integer chat IDs and guard slash-only commands - Fix streaming edge cases in TelegramChannel: - prevent edit worker deadlocks when text exceeds 4096 chars - prevent deadlock when placeholder send fails (message_id stays None) - enforce edit throttling with minimum interval sleep - honor send_typing_action=False in streaming mode - always forward final multimodal output (e.g. images), while avoiding duplicate text sends - Expand Telegram tests for slash-only command handling, non-int chat IDs, and streaming behavior (long text, final images, typing toggle) - Fix sample/docs feedback: - rename sample package to agent-framework-hosting-sample-local-telegram - switch sample uv.sources from feature branch to main - align docs/tool names with lookup_weather - fix broken links and server run instructions in README/call_server.py - align local_telegram app docstrings with reasoning hook behavior and strip model in responses_hook Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dal support - Remove stale PR reference from module docstring - Add Google-style docstring to TelegramChannel.__init__ documenting all keyword args - Fix _stream_to_chat to iterate update.contents instead of using getattr(update, 'text', None); text chunks are extracted from Content items with type='text', non-text content in updates is correctly ignored (images etc. are forwarded via the final response) - Update _FakeStreamUpdate test helper to use contents list matching the real AgentResponseUpdate API; add from_text/from_image class methods - Update _FakeResponseStream to accept _FakeStreamUpdate objects directly - Add test verifying multimodal stream updates don't corrupt text accumulator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…channel sample local_telegram is now a focused Telegram-only sample: - Removes ResponsesChannel and all responses_hook code - Removes call_server.py (no HTTP endpoint to call) - Uses a deterministic lookup_weather tool (hash-based, not random) - Single run_hook that strips model and raises reasoning effort - Drops agent-framework-hosting-responses dependency New local_multi_channel sample shows running both channels at once: - ResponsesChannel + TelegramChannel sharing a FileHistoryProvider - Cross-channel session resumption via previous_response_id - call_server.py moved here (the Responses endpoint lives here now) - Demonstrates the multi-channel coordination story Update README table to list both samples with clear descriptions. Also delete personal_assistant/.venv which was not tracked but caused pyright to crawl the entire installed venv (thousands of files), making sample pyright checks hang indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 62%
✓ Correctness
The TelegramChannel implementation is solid overall — per-chat serial queuing, webhook ack-before-run, streaming edit worker, and shutdown draining are all correctly implemented. The previously-resolved review comments (chat_id validation, slash-only command handling, typing action toggle, etc.) have all been addressed. One asymetry exists between the streaming and non-streaming paths: the streaming path truncates text to Telegram's 4096-char limit via
_TELEGRAM_MAX_TEXT_LEN, but the non-streaming_send/_reply_with_resultpath does not, causing silent message delivery failure for long agent responses.
✓ Security Reliability
The Telegram channel implementation is well-structured with good defensive practices. Previously identified security issues (chat_id integer validation, command parsing IndexError on bare
/) have been resolved. The code uses timing-safehmac.compare_digestfor secret verification, validates webhook payload structure, and wraps all update processing in_safe_process_updateto contain failures. The main remaining concern is that per-chat worker tasks are never reaped when a chat goes idle, representing an unbounded resource growth for long-running bots, though this is acceptable for an alpha-stage package.
✓ Test Coverage
The test suite covers core webhook handling, per-chat ordering, shutdown, and basic streaming. However, there are notable gaps: the
_handle_callback_querycode path (inline-button clicks) has zero test coverage, and theparse_modestreaming behavior (interim plain text, final with parse_mode, retry on 400) — a key feature called out in the PR description — is untested.
✓ Design Approach
I found one blocking issue in the streaming edit path and one sample-design issue repeated in the Telegram docs. The channel can suppress the final fallback text even when the last
editMessageTextnever succeeded, which loses replies under real Telegram API failures. Separately, the samples recommend multi-worker Hypercorn while defaulting Telegram to polling mode, which starts multiple independentgetUpdatesconsumers for the same bot token.
Automated review by eavanvalkenburg's agents
- only mark final edit as sent after a confirmed 2xx edit response - fall back to sendMessage when final edit returns a non-success status - add regression test covering failed final edit fallback behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- assert await_args is not None before reading kwargs in streaming fallback test - resolves test-typing failures across mypy/pyright/ty/zuban for hosting-telegram Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| ``` | ||
|
|
||
| For production, configure `webhook_url="https://…"` and the channel will | ||
| register the webhook on startup and receive updates over HTTPS. |
There was a problem hiding this comment.
nit: how will customers configure Telegram to talk to the host?
Motivation & Context
Adds the Telegram Bot channel for the
agent-framework-hostingstack, enabling developers to expose an agent on Telegram with streaming progressive edits, per-chat session isolation, and multi-modal support (photos, documents, voice). Also adds thelocal_telegramsample showing how to combineTelegramChannelwithResponsesChannelon the same host.This is part of the channel-by-channel split of the hosting work tracked in #6265.
Description & Review Guide
Major changes:
agent-framework-hosting-telegrampackage — newTelegramChannelsupporting:start_polling) and webhook (/telegramroute) transportseditMessageTextedits with a background rate-limit worker (default 0.4 s throttle, well under Telegram's per-chat edit ceiling)sendChatAction("typing")keepalive every 4 s so the typing bubble stays visible during long streamsparse_mode(partial Markdown mid-stream is invalid and Telegram rejects it with 400)ContentitemsContentitems (uri + image/* media_type) sent assendPhotorun_hookfor per-channel customisation (e.g. persona temperature) and access to parsed requestlocal_telegramsample — demonstrates:TelegramChannel+ResponsesChannelon the sameAgentFrameworkHostFileHistoryProviderkeyed to Telegram chat IDResponsesChannelrun_hookhonouringprevious_response_idfor cross-channel session resumptionTelegramChannelrun_hookbumping temperature for a chattier Telegram persona/newcommand to reset chat sessionImpact: New alpha package with no changes to existing packages. Adds
agent-framework-hosting-telegramto the workspace and lockfile.Reviewer focus: streaming edit worker correctness (rate-limit, wake/done event interaction), parse_mode stripping on interim edits vs restoration on final edit, per-chat serial queue design.
Related Issue
Fixes #6588
Refs #6265
Contribution Checklist