Skip to main content

tai42-channel-telegram

License: Apache 2.0

A Telegram Channel plugin for the TAI ecosystem. It delivers an ask_user question to a Telegram chat as a sendMessage — the caller's requested recipient if it is on the operator allowlist, otherwise the operator-configured default chat — a ForceReply for typed text/select answers, a tappable URL button opening the interaction callback door for confirm/external, a web_app webview button for form — and bridges the human's typed reply back to the interactions store through its own verified webhook route. Outbound is plain HTTPS over a pooled httpx client; there is no Telegram SDK dependency (the Bot API is flat JSON-over-HTTPS).

The TAI ecosystem

TAI is an open-source runtime for MCP tools, agents, and workflows. A Channel is a registered deliverer that pushes an interaction question to a human on a specific medium and bridges the reply back into the interactions store — so ask_user can reach a person out-of-band instead of only showing the question in the Studio inbox. This package is one such channel (Telegram); siblings back the same contract with Slack or Twilio SMS/WhatsApp. The ecosystem is open-ended: any package can back the same contract, so this repo is this channel's own full doc home, and the documentation site covers the platform-level story:

Its only tai-* dependencies are tai42-contract (the Channel protocol, ChannelDelivery, ChannelDeliveryError, and the tai42_app handle) and tai42-kit[redis] (HttpxClient, RedisClient, TaiBaseSettings, and the settings cache). Beyond those it depends on httpx, starlette, and pydantic / pydantic-settings.

Install

Requires Python 3.13+. Install from PyPI into the environment that runs the server:

uv add tai42-channel-telegram

Or from source — clone this repo and add it as an editable dependency; the tai42-* dependencies resolve in-tree from the workspace.

git clone https://github.com/tai42ai/tai42   # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai42/plugins/channel-telegram

Discovery

The skeleton discovers this channel by importing its modules — the manifest's channel_modules loader imports every module under the package, and importing tai42_channel_telegram.register fires the registrations as a side-effect: the "telegram" channel name on tai42_app.channels, the public inbound route, and the setWebhook startup hook. Name the package in your manifest:

channel_modules:
  - tai42_channel_telegram

A bare import tai42_channel_telegram (library use) does NOT register anything.

Configuration

Settings are read from the CHANNEL_TELEGRAM_ environment group (see TelegramSettings). Every credential is bound to env by the operator — never a tool parameter, never visible to the LLM:

Env var Default Purpose
CHANNEL_TELEGRAM_BOT_TOKEN — Bot credential from BotFather (required)
CHANNEL_TELEGRAM_ALLOWED_RECIPIENTS [] Whitelist of chats a caller-supplied recipient may name (numeric id — negative for groups — or @username), as a comma-separated string or a JSON list. A group or channel recipient cannot receive form questions — Telegram allows the web_app button in private chats only
CHANNEL_TELEGRAM_DEFAULT_RECIPIENT — The chat questions go to when the caller names no recipient (trusted; not checked against the allowlist)
CHANNEL_TELEGRAM_WEBHOOK_SECRET — setWebhook secret_token; verified on every inbound update (required)
CHANNEL_TELEGRAM_PUBLIC_BASE_URL — This deployment's public base URL (required)
CHANNEL_TELEGRAM_REDIS_URL — Redis the correlation store lives in (required)
CHANNEL_TELEGRAM_API_BASE_URL https://api.telegram.org Bot API origin (stub servers/e2e only)
CHANNEL_TELEGRAM_HTTP_TIMEOUT_SECONDS 30 Budget per outbound HTTP call

Optional Redis connection tuning (see TelegramCorrelationSettings):

Env var Default Purpose
CHANNEL_TELEGRAM_REDIS_MAX_CONNECTIONS — Pool size cap
CHANNEL_TELEGRAM_SOCKET_TIMEOUT — Per-command socket timeout (seconds)
CHANNEL_TELEGRAM_SOCKET_CONNECT_TIMEOUT — Connect-phase timeout (seconds)
CHANNEL_TELEGRAM_RETRY_ON_TIMEOUT false Retry commands on timeout
CHANNEL_TELEGRAM_RETRY_ATTEMPTS 0 Exponential-backoff retries on connection errors

How an answer travels

  1. A tool calls ask_user(question, channel="telegram", ...). The runtime persists the interaction, mints a public callback ticket, and calls this plugin's deliver with the question and its callback_url.
  2. deliver resolves the recipient chat: a caller-supplied recipient must be on CHANNEL_TELEGRAM_ALLOWED_RECIPIENTS or the delivery is refused (fail closed, nothing sent); no caller recipient means CHANNEL_TELEGRAM_DEFAULT_RECIPIENT. It then sends ONE sendMessage to that chat — the Bot API has no idempotency key, so a failed send raises ChannelDeliveryError instead of retrying (a blind retry could double-send).
    • text / select (typed reply): the message carries reply_markup: {force_reply: true}, and the sent message_id → callback_url mapping is stored in plugin-owned Redis keys (channel:telegram:corr:{message_id}, TTL = the question's remaining budget). A select question lists its options as guided text.
    • confirm / external (tap): the message carries a tappable URL button opening the callback door directly — no correlation state, no inbound involvement.
    • form (webview): the message carries a web_app button opening the schema-rendered callback page as an in-chat webview; the page POSTs the answer straight to the callback door, so like the tap forms there is no correlation state and no inbound involvement. Advertised by the supports_form_delivery capability flag. Telegram accepts a web_app button in a PRIVATE chat only, so a form question to a group or channel recipient fails the send where every other format succeeds.
  3. The human replies in Telegram. Telegram POSTs the update to this plugin's own public route POST /api/channels/telegram/inbound, registered by setWebhook at startup with a shared secret_token.
  4. The inbound door verifies X-Telegram-Bot-Api-Secret-Token against the configured secret — constant-time over sha256 digests of both sides, and FAIL CLOSED: an unset or empty secret answers 500, never "skip verification". The body is read through a streaming bounded reader (413 the moment it crosses 1 MiB). The reply is matched on message.reply_to_message.message_id and the configured recipient chats (the default recipient plus the allowlist; an entry matches the update's numeric chat id or its @username), the callback_url is looked up in the correlation store, and the answer is forwarded as the JSON object {"answer": "<typed text>"} — the callback door validates it against the question's stored answer_format and records it.
  5. The blocked ask_user call returns the recorded answer.

Telegram redelivers an update until it gets a 2xx, so the inbound status code is the retry contract:

Condition Response
Configured secret unset/empty, or no recipient chat configured 500 (fail closed, logged)
Header missing or mismatched 401 (one constant deny body)
Body over the cap / not a JSON object 413 / 400
Update out of scope (no message / chat not a configured recipient / not a reply / no text) 200 "ignored" (reason logged)
No pending correlation for the replied-to message 200 "ignored" (logged warning)
Callback door 200 200 "forwarded", mapping cleared
Callback door 404 (ticket terminally gone) 200 "stale", mapping cleared
Callback door 400 (answer rejected) 200 "rejected", mapping KEPT — the human can reply again
Callback door other status / unreachable exception → 500, Telegram's redelivery is the recovery

Setup

  1. Create a bot with @BotFather (/newbot) and put its token in CHANNEL_TELEGRAM_BOT_TOKEN.
  2. Find each target chat id (message the bot, then read https://api.telegram.org/bot<token>/getUpdates; group ids are negative). Put the fallback chat in CHANNEL_TELEGRAM_DEFAULT_RECIPIENT and any chats callers may address per ask in CHANNEL_TELEGRAM_ALLOWED_RECIPIENTS (comma-separated or a JSON list).
  3. Generate a webhook secret (1-256 chars of [A-Za-z0-9_-]) for CHANNEL_TELEGRAM_WEBHOOK_SECRET.
  4. Set CHANNEL_TELEGRAM_PUBLIC_BASE_URL to the deployment's public origin. Telegram only delivers webhooks over HTTPS (TLS ≥ 1.2) to ports 443, 80, 88, or 8443. The startup hook points the bot's webhook at {public_base_url}/api/channels/telegram/inbound and aborts startup loudly if setWebhook fails.

Development

uv venv --python 3.13
uv pip install --no-sources --group dev --editable .
uv run --no-sync pytest --cov --cov-report=term-missing
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright

The offline suite is fully self-contained (fake transport, in-memory Redis). The live suite (uv run pytest -m integration) sends real messages through the Bot API when CHANNEL_TELEGRAM_BOT_TOKEN / CHANNEL_TELEGRAM_DEFAULT_RECIPIENT are set in the ambient environment, and skips cleanly otherwise; it never calls setWebhook.

License

Apache-2.0. See LICENSE and NOTICE.

Correlation surface (2.0)

Since 2.0 inbound answers resolve through the platform's shared inbound-answer ladder: the plugin exposes its correlation store over the contract's CorrelationStore port (reserve / peek / release) plus a transport ack, and the skeleton owns the forward / retry-in-place / bridge ladder. The plugin-local pop/restore correlation helpers from 1.x are gone.

Metadata

Release files for tai42-channel-telegram 2.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tai42-channel-telegram 2.1.0
File Size Uploaded
tai42_channel_telegram-2.1.0.tar.gz 58.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tai42-channel-telegram 2.1.0
File Interpreter ABI Platform
tai42_channel_telegram-2.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 94.1 kB

Release files / tai42_channel_telegram-2.1.0.tar.gz

Download URL tai42_channel_telegram-2.1.0.tar.gz
Size 58.3 kB
Tags Source
SHA-256 checksum
How to use checksums
449922b551251658efd24c8e1b707832725c69b96afbf644693c8bbc25a065cd
BLAKE2b-256 checksum
How to use checksums
8bfea2b2ba74f37cb210c2c25f3823eddea02cfa613380bdb622205fefde3f72
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / tai42_channel_telegram-2.1.0-py3-none-any.whl

Download URL tai42_channel_telegram-2.1.0-py3-none-any.whl
Size 35.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e7a5529cace28f498bfa762f7ed882fe29ecff6176a628b344ec5883e8ccb33e
BLAKE2b-256 checksum
How to use checksums
5eb533d2479042570989992565c8c7b597aff4f1aa05b953b69dd6ff70c74fee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

6.0.6

2 release files

6.0.5

2 release files

6.0.4

2 release files

6.0.3

2 release files

6.0.2

2 release files

6.0.1

2 release files

6.0.0

2 release files

5.0.1

2 release files

5.0.0

2 release files

4.4.4

2 release files

4.4.3

2 release files

4.4.2

2 release files

4.4.1

2 release files

4.4.0

2 release files

4.3.1

2 release files

4.3.0

2 release files

4.2.2

2 release files

4.2.1

2 release files

4.2.0

2 release files

4.1.2

2 release files

4.1.1

2 release files

4.1.0

2 release files

4.0.3

2 release files

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.4.10

2 release files

3.4.9

2 release files

3.4.8

2 release files

3.4.7

2 release files

3.4.6

2 release files

3.4.5

2 release files

3.4.4

2 release files

3.4.3

2 release files

3.4.2

2 release files

3.4.1

2 release files

3.4.0

2 release files

3.3.1

2 release files

3.3.0

2 release files

3.2.1

2 release files

3.2.0

2 release files

3.1.0

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.1.1

2 release files

This release

2.1.0 This release

2 release files

2.0.0

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page