Skip to content

Add Telegram channel adapter - #20

Merged
5 commits merged into
BerriAI:mainfrom
CatalanCabbage:telegram-channel
Oct 13, 2026
Merged

5 commits merged into
BerriAI:mainfrom
CatalanCabbage:telegram-channel

Conversation

@CatalanCabbage

@CatalanCabbage CatalanCabbage commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Coming from this LinkedIn thread.
This adds telegram support. I've been using my own private setup for Telegram comms, so I had most of the impl down, tried and tested.
Left streaming out so I could keep this as close to existing interfaces as possible.

Tested live with all msg types on my phone apart from the automated tests.
image


LLM generated summary:

AgentChat can now receive messages and send confirmed replies through Telegram bots. The adapter lives in its own agentchat.channels.telegram package and uses the existing aiohttp dependency (telegram = [] extra). It comes with a setup guide and a framework-free echo example.

  • Inbound:
    • Long polling by default. If an update arrives while the queue is full, the offset is held back so Telegram redelivers it instead of it being dropped.
    • Optional webhook mode via TELEGRAM_WEBHOOK_URL and TELEGRAM_WEBHOOK_SECRET. It checks the secret header in constant time, returns 503 when full, and exposes create_app() for host-owned servers.
  • Chats:
    • Private chats, groups and supergroups.
    • Groups only accept addressed messages: an @mention, a /command@bot, a text_mention, or a reply to the bot.
    • Forum topics become threads.
    • Inline button taps arrive as messages carrying the button label, and are answered so the client's spinner stops.
  • Capabilities:
    • reply: Markdown converted to Telegram formatting.
    • reply_rich: native reply_markup, e.g. RichReply(text=..., blocks=({"inline_keyboard": [...]},)).
    • set_status: typing indicator, kept alive while working.
    • upload_files: via sendDocument.
    • download_attachment: via getFile, bounded by max_bytes.
    • History and mirroring keep the existing unsupported behavior, because the Bot API cannot read chat history.
  • Delivery:
    • One send attempt per reply. Failures raise TelegramError(RuntimeError) with error_code, description and retry_after.
    • Rate-limit scheduling stays with the host, as docs/providers.md specifies, so there is no automatic 429 retry.
    • Tokens never appear in errors or logs.
    • Replies over 4,096 characters (counted in UTF-16 units, as Telegram does) raise ValueError before any send.

Design decisions

Formatting: Markdown becomes Telegram entities, never parse_mode.

  • Telegram doesn't render raw Markdown, so passing agent text through unchanged would show literal ** and backticks.
  • parse_mode (MarkdownV2/HTML) corrupts code silently: the underscore in get_user_data() is read as italic, and the message renders as getuserdata() with no error.
  • rendering.py is a small stdlib-only converter that emits entity offsets in UTF-16 code units:
    • It supports bold, italic, code, pre with a language, strikethrough, spoiler, links, headings, lists and blockquotes.
    • Underscores and asterisks inside a word stay literal, and __ is not a delimiter, so __init__.py survives intact.
    • Only http, https and tg links become text_link.
  • A seeded property test over 2,000 random inputs checks that every entity stays in bounds, never splits a surrogate pair, and nests the way Telegram requires.

Streaming: not included.

  • The Bot API has sendMessageDraft (Bot API 9.3, open to all bots since 9.5).
  • It only works in private chats, and the draft is an ephemeral 30-second preview that still needs a final sendMessage.
  • AgentChat has no streaming API: handlers return a single string. Adding one is a core SDK change that every channel would need to support, so it belongs in its own proposal.

Core changes

None to runtime modules. In the shared contract suite, test_providers.py gains a Telegram harness. test_optional_capability_fallbacks becomes capability-aware: each harness declares the protocols it implements, missing protocols must still fall back, and implemented ones must pass isinstance. The four existing adapters keep every assertion they had.

Validation

  • ruff check . passes. python -m pytest: 463 passed on Python 3.11, the CI version, and on 3.14. Coverage includes:
    • normalization and addressing, and the renderer property test
    • polling against a local fake Bot API server through the real HTTP client: closing during an in-flight long poll, offset holdback, fatal 409/401, backoff on 5xx/429, offset confirmation on close
    • the webhook's 401/400/503/200 responses, and listening before setWebhook
    • typing keepalive and cancellation, rich replies, upload and download limits
    • no token in errors or logs, and import isolation
  • Live test against a real bot (polling):
    • Outbound:
      • Telegram echoed every formatting entity back exactly as sent, with one exception: bold that wraps inline code, which Telegram accepts and splits around the code span.
      • Reply receipts, an inline keyboard, the typing indicator, and a sendDocument upload / getFile download / max_bytes round trip.
    • Inbound:
      • A private chat, a button tap, and a photo download.
      • In a supergroup: a mention and a reply to the bot were answered, and an unaddressed message was ignored.
      • A forum topic was routed as a thread.
  • Webhook mode was checked against local HTTP servers only, not end to end against Telegram.

Long polling by default with offset holdback, optional secret-checked
webhook mode, Markdown converted to Telegram entities, typing keepalive,
inline-keyboard rich replies, document uploads and bounded downloads.
@ghost
ghost merged commit 398a779 into BerriAI:main Oct 13, 2026
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant