Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/decisions/0027-hosting-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ The app chooses which helper to call for that route and deployment. For example:

- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
response id or a `conv_*` conversation id when present;
- `telegram_session_id(update)` from `agent-framework-hosting-telegram`, which can choose the chat, user, thread, or
other Telegram-native partitioning logic for that helper;
- `telegram_session_id(update, bot_id=...)` from `agent-framework-hosting-telegram`, which uses the bot and sender for
private chats and the bot and chat for shared group sessions;
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
`a2a_session_id(request_context)` from their respective protocol packages;
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
Expand Down
52 changes: 51 additions & 1 deletion docs/specs/002-python-hosting-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ must be aligned with the helper-first model before implementation. Old vocabular
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_telegram` | Protocol-specific helpers such as `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_session_id(...)`, and command/media helpers when useful. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | Telegram Bot API helpers: update parsing, chat/session/command/media extraction, final rendering, and streaming edit rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_activity_protocol` | Protocol-specific helpers such as `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, and command/media helpers when useful. |

The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
needed, but helper functions should stay usable from plain app code and tests.
Expand Down Expand Up @@ -244,6 +245,52 @@ text deltas, and a completed event. The final completed payload is produced thro
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
metadata.

## `agent-framework-hosting-telegram`

The Telegram package provides side-effect-free helpers around Telegram Bot API
update and method payloads. It does not provide a Bot API client, polling loop,
webhook route, command registry, retry policy, or rate limiter.

### Update helpers

- `telegram_to_run(update, *, resolve_file_url=None, stream=False) -> AgentRunArgs`
- `telegram_chat_id(update) -> int | None`
- `telegram_session_id(update, *, bot_id) -> str | None`
- `telegram_command(update) -> str | None`
- `telegram_callback_query_id(update) -> str | None`
- `telegram_media_file_id(update_or_message) -> tuple[str, str] | None`

`telegram_to_run(...)` handles `message`, `edited_message`, and
`callback_query` updates. Text and captions become AF text content. When the
app supplies an async `resolve_file_url` callback, supported Telegram media
file ids can become AF URI content. The package does not call Telegram's
`getFile` method itself.

`telegram_session_id(..., bot_id=...)` includes the bot identity in every key.
Private chats return `telegram:<bot_id>:<user_id>`; other chats return
`telegram:<bot_id>:<chat_id>`, giving groups a shared session by default. This
matches Telegram's native isolation boundaries while preventing two bots from
sharing state accidentally. Apps that want per-user sessions inside a group
can construct a key that includes both chat and sender ids. The app must
authorize those Telegram identities before loading session state.

`telegram_command(...)` parses Telegram's `/name` and `/name@bot` syntax. It
does not register commands or invoke handlers.

### Response helpers

- `telegram_from_run(result, *, chat_id, parse_mode=None)`
- `telegram_from_streaming_run(stream, *, chat_id, message_id, initial_text=None, parse_mode=None)`

The helpers produce Telegram method/payload values for app-owned Bot API
calls. Final rendering supports text and image URI output and applies
Telegram's text-length boundary. Streaming rendering produces cumulative
`editMessageText` payloads for a placeholder message id supplied by the app,
omitting edits that match an optional `initial_text`, then renders the final
rich output. Image-only responses remove the placeholder with `deleteMessage`
before sending the image. The app owns the initial placeholder send, Bot API
calls, edit throttling, retries, and failure policy.

## Security responsibilities

Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
Expand Down Expand Up @@ -346,3 +393,6 @@ Implementation validation must cover:
- Responses streaming SSE rendering;
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
- sample type checking for the local Responses sample.
- Telegram update parsing, chat/session/command/media extraction, final
rendering, and streaming edit rendering;
- sample type checking for the local Telegram polling and webhook entry points.
1 change: 1 addition & 0 deletions python/PACKAGE_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Status is grouped into these buckets:
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `rc` |
| `agent-framework-hosting` | `python/packages/hosting` | `alpha` |
| `agent-framework-hosting-responses` | `python/packages/hosting-responses` | `alpha` |
| `agent-framework-hosting-telegram` | `python/packages/hosting-telegram` | `alpha` |
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` |
| `agent-framework-lab` | `python/packages/lab` | `beta` |
| `agent-framework-mem0` | `python/packages/mem0` | `beta` |
Expand Down
21 changes: 21 additions & 0 deletions python/packages/hosting-telegram/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
83 changes: 83 additions & 0 deletions python/packages/hosting-telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# agent-framework-hosting-telegram

Telegram Bot API-shaped helpers for app-owned Agent Framework hosting.

This is an alpha, helper-only package: it converts between Telegram's
`Update` JSON shape and Agent Framework run values in both directions. It
does **not** provide a bot client, a hosting/channel registry, or a
long-running service. Your app remains fully responsible for:

- **Fetching updates** -- long polling (`getUpdates`) or registering a
webhook -- and for verifying webhook authenticity (e.g. Telegram's secret
token header, or an IP allowlist).
- **The Bot API client** -- issuing the actual HTTP calls (`sendMessage`,
`sendPhoto`, `editMessageText`, `answerCallbackQuery`, `getFile`, ...) with
whatever HTTP library you prefer.
- **Rate limits and retries** -- Telegram enforces per-chat and global rate
limits; back off and retry on `429`/`5xx` yourself.
- **Command dispatch** -- `telegram_command(...)` only parses and normalizes
a leading `/command`; your app decides what each command does.
- **Sessions/storage** -- pair these helpers with
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/)'s
`AgentState` / `SessionStore` (or your own) to persist `AgentSession`s across turns.

## Helpers

- `telegram_chat_id(update)` -- the chat id an update belongs to.
- `telegram_session_id(update, bot_id=...)` -- a bot-scoped `AgentState`
session id. Private chats use `telegram:<bot_id>:<user_id>`; other chats use
`telegram:<bot_id>:<chat_id>`.
- `telegram_command(update)` -- a leading slash command, with `/name@bot args`
normalized to `/name args`. Returns `None` if there is none.
- `telegram_callback_query_id(update)` -- a callback query's id, so you can
call `answerCallbackQuery` yourself.
- `telegram_media_file_id(update_or_message)` -- the `(file_id, mime_type)`
of inbound media (largest photo size, document, voice, audio, or video).
- `telegram_to_run(update, *, resolve_file_url=None, stream=False)` -- convert
a `message`, `edited_message`, or `callback_query` update into
`Agent.run` arguments. Provide `resolve_file_url` (typically backed by
`getFile`) to turn inbound media into content; without it (or when it
returns `None`), text/caption is preserved and media is otherwise dropped.
Media-only input with no resolvable URL raises `ValueError`.
- `telegram_from_run(result, *, chat_id, parse_mode=None)` -- render a
finished run as one `TelegramOperation` (`sendPhoto` when the response has
an image, otherwise `sendMessage`, falling back to `"(no response)"`).
- `telegram_from_streaming_run(stream, *, chat_id, message_id,
initial_text=None, parse_mode=None)` -- render a streaming run as
`editMessageText` operations with the cumulative text so far, followed by any
images in the final response as `sendPhoto` operations. Pass the
app-created placeholder text as `initial_text` so an identical first edit is
omitted. Image-only responses first emit `deleteMessage` for the placeholder.

`TelegramOperation` is a minimal `TypedDict` of `{"method": str, "payload": dict}`
-- your app is responsible for actually calling the Bot API with it.

```python
from agent_framework_hosting import AgentState
from agent_framework_hosting_telegram import (
telegram_chat_id,
telegram_from_run,
telegram_session_id,
telegram_to_run,
)

state = AgentState(agent)


async def handle_update(update: dict) -> None:
chat_id = telegram_chat_id(update)
if chat_id is None:
return # Not a chat update this bot handles.

session_id = telegram_session_id(update, bot_id=bot.id)
session = await state.get_or_create_session(session_id)
run = await telegram_to_run(update, resolve_file_url=resolve_telegram_file_url)
result = await (await state.get_target()).run(run["messages"], session=session, options=run["options"])
await state.set_session(session_id, session) # type: ignore[arg-type]

operation = telegram_from_run(result, chat_id=chat_id)
await call_bot_api(operation["method"], operation["payload"]) # Your HTTP client.
```

The base execution-state helpers live in
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/).
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) Microsoft. All rights reserved.

"""Telegram Bot API-shaped helpers for app-owned Agent Framework hosting."""

import importlib.metadata

from ._parsing import (
ResolveFileUrl,
telegram_callback_query_id,
telegram_chat_id,
telegram_command,
telegram_media_file_id,
telegram_session_id,
telegram_to_run,
)
from ._rendering import (
TELEGRAM_MAX_CAPTION_LENGTH,
TELEGRAM_MAX_TEXT_LENGTH,
TelegramOperation,
telegram_from_run,
telegram_from_streaming_run,
)

try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"

__all__ = [
"TELEGRAM_MAX_CAPTION_LENGTH",
"TELEGRAM_MAX_TEXT_LENGTH",
"ResolveFileUrl",
"TelegramOperation",
"__version__",
"telegram_callback_query_id",
"telegram_chat_id",
"telegram_command",
"telegram_from_run",
"telegram_from_streaming_run",
"telegram_media_file_id",
"telegram_session_id",
"telegram_to_run",
]
Loading
Loading