agent-framework-hosting-telegram 1.0.0a260721__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_framework_hosting_telegram-1.0.0a260721/LICENSE +21 -0
- agent_framework_hosting_telegram-1.0.0a260721/PKG-INFO +109 -0
- agent_framework_hosting_telegram-1.0.0a260721/README.md +83 -0
- agent_framework_hosting_telegram-1.0.0a260721/agent_framework_hosting_telegram/__init__.py +43 -0
- agent_framework_hosting_telegram-1.0.0a260721/agent_framework_hosting_telegram/_parsing.py +338 -0
- agent_framework_hosting_telegram-1.0.0a260721/agent_framework_hosting_telegram/_rendering.py +171 -0
- agent_framework_hosting_telegram-1.0.0a260721/pyproject.toml +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-framework-hosting-telegram
|
|
3
|
+
Version: 1.0.0a260721
|
|
4
|
+
Summary: Telegram Bot API-shaped helpers for agent-framework-hosting.
|
|
5
|
+
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: agent-framework-core>=1.11.0,<2
|
|
20
|
+
Requires-Dist: agent-framework-hosting==1.0.0a260721
|
|
21
|
+
Project-URL: homepage, https://aka.ms/agent-framework
|
|
22
|
+
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
23
|
+
Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
|
|
24
|
+
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
25
|
+
|
|
26
|
+
# agent-framework-hosting-telegram
|
|
27
|
+
|
|
28
|
+
Telegram Bot API-shaped helpers for app-owned Agent Framework hosting.
|
|
29
|
+
|
|
30
|
+
This is an alpha, helper-only package: it converts between Telegram's
|
|
31
|
+
`Update` JSON shape and Agent Framework run values in both directions. It
|
|
32
|
+
does **not** provide a bot client, a hosting/channel registry, or a
|
|
33
|
+
long-running service. Your app remains fully responsible for:
|
|
34
|
+
|
|
35
|
+
- **Fetching updates** -- long polling (`getUpdates`) or registering a
|
|
36
|
+
webhook -- and for verifying webhook authenticity (e.g. Telegram's secret
|
|
37
|
+
token header, or an IP allowlist).
|
|
38
|
+
- **The Bot API client** -- issuing the actual HTTP calls (`sendMessage`,
|
|
39
|
+
`sendPhoto`, `editMessageText`, `answerCallbackQuery`, `getFile`, ...) with
|
|
40
|
+
whatever HTTP library you prefer.
|
|
41
|
+
- **Rate limits and retries** -- Telegram enforces per-chat and global rate
|
|
42
|
+
limits; back off and retry on `429`/`5xx` yourself.
|
|
43
|
+
- **Command dispatch** -- `telegram_command(...)` only parses and normalizes
|
|
44
|
+
a leading `/command`; your app decides what each command does.
|
|
45
|
+
- **Sessions/storage** -- pair these helpers with
|
|
46
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/)'s
|
|
47
|
+
`AgentState` / `SessionStore` (or your own) to persist `AgentSession`s across turns.
|
|
48
|
+
|
|
49
|
+
## Helpers
|
|
50
|
+
|
|
51
|
+
- `telegram_chat_id(update)` -- the chat id an update belongs to.
|
|
52
|
+
- `telegram_session_id(update, bot_id=...)` -- a bot-scoped `AgentState`
|
|
53
|
+
session id. Private chats use `telegram:<bot_id>:<user_id>`; other chats use
|
|
54
|
+
`telegram:<bot_id>:<chat_id>`.
|
|
55
|
+
- `telegram_command(update)` -- a leading slash command, with `/name@bot args`
|
|
56
|
+
normalized to `/name args`. Returns `None` if there is none.
|
|
57
|
+
- `telegram_callback_query_id(update)` -- a callback query's id, so you can
|
|
58
|
+
call `answerCallbackQuery` yourself.
|
|
59
|
+
- `telegram_media_file_id(update_or_message)` -- the `(file_id, mime_type)`
|
|
60
|
+
of inbound media (largest photo size, document, voice, audio, or video).
|
|
61
|
+
- `telegram_to_run(update, *, resolve_file_url=None, stream=False)` -- convert
|
|
62
|
+
a `message`, `edited_message`, or `callback_query` update into
|
|
63
|
+
`Agent.run` arguments. Provide `resolve_file_url` (typically backed by
|
|
64
|
+
`getFile`) to turn inbound media into content; without it (or when it
|
|
65
|
+
returns `None`), text/caption is preserved and media is otherwise dropped.
|
|
66
|
+
Media-only input with no resolvable URL raises `ValueError`.
|
|
67
|
+
- `telegram_from_run(result, *, chat_id, parse_mode=None)` -- render a
|
|
68
|
+
finished run as one `TelegramOperation` (`sendPhoto` when the response has
|
|
69
|
+
an image, otherwise `sendMessage`, falling back to `"(no response)"`).
|
|
70
|
+
- `telegram_from_streaming_run(stream, *, chat_id, message_id,
|
|
71
|
+
initial_text=None, parse_mode=None)` -- render a streaming run as
|
|
72
|
+
`editMessageText` operations with the cumulative text so far, followed by any
|
|
73
|
+
images in the final response as `sendPhoto` operations. Pass the
|
|
74
|
+
app-created placeholder text as `initial_text` so an identical first edit is
|
|
75
|
+
omitted. Image-only responses first emit `deleteMessage` for the placeholder.
|
|
76
|
+
|
|
77
|
+
`TelegramOperation` is a minimal `TypedDict` of `{"method": str, "payload": dict}`
|
|
78
|
+
-- your app is responsible for actually calling the Bot API with it.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from agent_framework_hosting import AgentState
|
|
82
|
+
from agent_framework_hosting_telegram import (
|
|
83
|
+
telegram_chat_id,
|
|
84
|
+
telegram_from_run,
|
|
85
|
+
telegram_session_id,
|
|
86
|
+
telegram_to_run,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
state = AgentState(agent)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def handle_update(update: dict) -> None:
|
|
93
|
+
chat_id = telegram_chat_id(update)
|
|
94
|
+
if chat_id is None:
|
|
95
|
+
return # Not a chat update this bot handles.
|
|
96
|
+
|
|
97
|
+
session_id = telegram_session_id(update, bot_id=bot.id)
|
|
98
|
+
session = await state.get_or_create_session(session_id)
|
|
99
|
+
run = await telegram_to_run(update, resolve_file_url=resolve_telegram_file_url)
|
|
100
|
+
result = await (await state.get_target()).run(run["messages"], session=session, options=run["options"])
|
|
101
|
+
await state.set_session(session_id, session) # type: ignore[arg-type]
|
|
102
|
+
|
|
103
|
+
operation = telegram_from_run(result, chat_id=chat_id)
|
|
104
|
+
await call_bot_api(operation["method"], operation["payload"]) # Your HTTP client.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The base execution-state helpers live in
|
|
108
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/).
|
|
109
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# agent-framework-hosting-telegram
|
|
2
|
+
|
|
3
|
+
Telegram Bot API-shaped helpers for app-owned Agent Framework hosting.
|
|
4
|
+
|
|
5
|
+
This is an alpha, helper-only package: it converts between Telegram's
|
|
6
|
+
`Update` JSON shape and Agent Framework run values in both directions. It
|
|
7
|
+
does **not** provide a bot client, a hosting/channel registry, or a
|
|
8
|
+
long-running service. Your app remains fully responsible for:
|
|
9
|
+
|
|
10
|
+
- **Fetching updates** -- long polling (`getUpdates`) or registering a
|
|
11
|
+
webhook -- and for verifying webhook authenticity (e.g. Telegram's secret
|
|
12
|
+
token header, or an IP allowlist).
|
|
13
|
+
- **The Bot API client** -- issuing the actual HTTP calls (`sendMessage`,
|
|
14
|
+
`sendPhoto`, `editMessageText`, `answerCallbackQuery`, `getFile`, ...) with
|
|
15
|
+
whatever HTTP library you prefer.
|
|
16
|
+
- **Rate limits and retries** -- Telegram enforces per-chat and global rate
|
|
17
|
+
limits; back off and retry on `429`/`5xx` yourself.
|
|
18
|
+
- **Command dispatch** -- `telegram_command(...)` only parses and normalizes
|
|
19
|
+
a leading `/command`; your app decides what each command does.
|
|
20
|
+
- **Sessions/storage** -- pair these helpers with
|
|
21
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/)'s
|
|
22
|
+
`AgentState` / `SessionStore` (or your own) to persist `AgentSession`s across turns.
|
|
23
|
+
|
|
24
|
+
## Helpers
|
|
25
|
+
|
|
26
|
+
- `telegram_chat_id(update)` -- the chat id an update belongs to.
|
|
27
|
+
- `telegram_session_id(update, bot_id=...)` -- a bot-scoped `AgentState`
|
|
28
|
+
session id. Private chats use `telegram:<bot_id>:<user_id>`; other chats use
|
|
29
|
+
`telegram:<bot_id>:<chat_id>`.
|
|
30
|
+
- `telegram_command(update)` -- a leading slash command, with `/name@bot args`
|
|
31
|
+
normalized to `/name args`. Returns `None` if there is none.
|
|
32
|
+
- `telegram_callback_query_id(update)` -- a callback query's id, so you can
|
|
33
|
+
call `answerCallbackQuery` yourself.
|
|
34
|
+
- `telegram_media_file_id(update_or_message)` -- the `(file_id, mime_type)`
|
|
35
|
+
of inbound media (largest photo size, document, voice, audio, or video).
|
|
36
|
+
- `telegram_to_run(update, *, resolve_file_url=None, stream=False)` -- convert
|
|
37
|
+
a `message`, `edited_message`, or `callback_query` update into
|
|
38
|
+
`Agent.run` arguments. Provide `resolve_file_url` (typically backed by
|
|
39
|
+
`getFile`) to turn inbound media into content; without it (or when it
|
|
40
|
+
returns `None`), text/caption is preserved and media is otherwise dropped.
|
|
41
|
+
Media-only input with no resolvable URL raises `ValueError`.
|
|
42
|
+
- `telegram_from_run(result, *, chat_id, parse_mode=None)` -- render a
|
|
43
|
+
finished run as one `TelegramOperation` (`sendPhoto` when the response has
|
|
44
|
+
an image, otherwise `sendMessage`, falling back to `"(no response)"`).
|
|
45
|
+
- `telegram_from_streaming_run(stream, *, chat_id, message_id,
|
|
46
|
+
initial_text=None, parse_mode=None)` -- render a streaming run as
|
|
47
|
+
`editMessageText` operations with the cumulative text so far, followed by any
|
|
48
|
+
images in the final response as `sendPhoto` operations. Pass the
|
|
49
|
+
app-created placeholder text as `initial_text` so an identical first edit is
|
|
50
|
+
omitted. Image-only responses first emit `deleteMessage` for the placeholder.
|
|
51
|
+
|
|
52
|
+
`TelegramOperation` is a minimal `TypedDict` of `{"method": str, "payload": dict}`
|
|
53
|
+
-- your app is responsible for actually calling the Bot API with it.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from agent_framework_hosting import AgentState
|
|
57
|
+
from agent_framework_hosting_telegram import (
|
|
58
|
+
telegram_chat_id,
|
|
59
|
+
telegram_from_run,
|
|
60
|
+
telegram_session_id,
|
|
61
|
+
telegram_to_run,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
state = AgentState(agent)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def handle_update(update: dict) -> None:
|
|
68
|
+
chat_id = telegram_chat_id(update)
|
|
69
|
+
if chat_id is None:
|
|
70
|
+
return # Not a chat update this bot handles.
|
|
71
|
+
|
|
72
|
+
session_id = telegram_session_id(update, bot_id=bot.id)
|
|
73
|
+
session = await state.get_or_create_session(session_id)
|
|
74
|
+
run = await telegram_to_run(update, resolve_file_url=resolve_telegram_file_url)
|
|
75
|
+
result = await (await state.get_target()).run(run["messages"], session=session, options=run["options"])
|
|
76
|
+
await state.set_session(session_id, session) # type: ignore[arg-type]
|
|
77
|
+
|
|
78
|
+
operation = telegram_from_run(result, chat_id=chat_id)
|
|
79
|
+
await call_bot_api(operation["method"], operation["payload"]) # Your HTTP client.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The base execution-state helpers live in
|
|
83
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Telegram Bot API-shaped helpers for app-owned Agent Framework hosting."""
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
from ._parsing import (
|
|
8
|
+
ResolveFileUrl,
|
|
9
|
+
telegram_callback_query_id,
|
|
10
|
+
telegram_chat_id,
|
|
11
|
+
telegram_command,
|
|
12
|
+
telegram_media_file_id,
|
|
13
|
+
telegram_session_id,
|
|
14
|
+
telegram_to_run,
|
|
15
|
+
)
|
|
16
|
+
from ._rendering import (
|
|
17
|
+
TELEGRAM_MAX_CAPTION_LENGTH,
|
|
18
|
+
TELEGRAM_MAX_TEXT_LENGTH,
|
|
19
|
+
TelegramOperation,
|
|
20
|
+
telegram_from_run,
|
|
21
|
+
telegram_from_streaming_run,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
__version__ = importlib.metadata.version(__name__)
|
|
26
|
+
except importlib.metadata.PackageNotFoundError:
|
|
27
|
+
__version__ = "0.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"TELEGRAM_MAX_CAPTION_LENGTH",
|
|
31
|
+
"TELEGRAM_MAX_TEXT_LENGTH",
|
|
32
|
+
"ResolveFileUrl",
|
|
33
|
+
"TelegramOperation",
|
|
34
|
+
"__version__",
|
|
35
|
+
"telegram_callback_query_id",
|
|
36
|
+
"telegram_chat_id",
|
|
37
|
+
"telegram_command",
|
|
38
|
+
"telegram_from_run",
|
|
39
|
+
"telegram_from_streaming_run",
|
|
40
|
+
"telegram_media_file_id",
|
|
41
|
+
"telegram_session_id",
|
|
42
|
+
"telegram_to_run",
|
|
43
|
+
]
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Parsing helpers for the Telegram Bot API ``Update`` object.
|
|
4
|
+
|
|
5
|
+
Telegram delivers updates as JSON objects shaped like ``{"update_id": ...,
|
|
6
|
+
"message": {...}}`` (or ``edited_message`` / ``callback_query`` instead of
|
|
7
|
+
``message``). These helpers pull out the handful of fields an app-owned route
|
|
8
|
+
needs -- chat id, a namespaced session id, a leading slash command, a
|
|
9
|
+
callback query id, and inbound media -- and translate an update into Agent
|
|
10
|
+
Framework ``Agent.run`` arguments. They do not poll for updates, register
|
|
11
|
+
webhooks, call the Telegram HTTP API, or dispatch commands; that is app-owned
|
|
12
|
+
route/bot-client code.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
from agent_framework import ChatOptions, Content, Message
|
|
22
|
+
from agent_framework_hosting import AgentRunArgs
|
|
23
|
+
|
|
24
|
+
# Telegram media fields whose objects carry a `file_id` (and, except photos,
|
|
25
|
+
# a `mime_type`) directly, mapped to the MIME type Telegram uses when the
|
|
26
|
+
# object omits `mime_type` (voice notes are always OGG/Opus, for example).
|
|
27
|
+
_MEDIA_DEFAULT_MIME_TYPES: dict[str, str] = {
|
|
28
|
+
"document": "application/octet-stream",
|
|
29
|
+
"voice": "audio/ogg",
|
|
30
|
+
"audio": "audio/mpeg",
|
|
31
|
+
"video": "video/mp4",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Matches a leading Telegram bot command: `/name`, optionally `@botname`,
|
|
35
|
+
# optionally followed by arguments. Command names are `[A-Za-z0-9_]` per the
|
|
36
|
+
# Bot API's `bot_command` entity rules.
|
|
37
|
+
_COMMAND_PATTERN = re.compile(r"^/(?P<name>[A-Za-z0-9_]+)(?:@(?P<bot>[A-Za-z0-9_]+))?(?P<rest>.*)$", re.DOTALL)
|
|
38
|
+
|
|
39
|
+
ResolveFileUrl = Callable[[str], Awaitable[str | None]]
|
|
40
|
+
"""Async callable resolving a Telegram ``file_id`` to a fetchable URL, or ``None``."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _inner_message(update: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
44
|
+
"""Return the ``message`` or ``edited_message`` object from ``update``, if present."""
|
|
45
|
+
for key in ("message", "edited_message"):
|
|
46
|
+
candidate = update.get(key)
|
|
47
|
+
if isinstance(candidate, Mapping):
|
|
48
|
+
return cast("Mapping[str, Any]", candidate)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _callback_query(update: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
53
|
+
"""Return the ``callback_query`` object from ``update``, if present."""
|
|
54
|
+
candidate = update.get("callback_query")
|
|
55
|
+
return cast("Mapping[str, Any]", candidate) if isinstance(candidate, Mapping) else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def telegram_chat_id(update: Mapping[str, Any]) -> int | None:
|
|
59
|
+
"""Return the chat id an update belongs to.
|
|
60
|
+
|
|
61
|
+
Reads ``message.chat.id`` / ``edited_message.chat.id``, falling back to
|
|
62
|
+
``callback_query.message.chat.id`` for callback-only updates.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
update: A Telegram Bot API ``Update`` object.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The chat id, or ``None`` if the update carries none of the supported shapes.
|
|
69
|
+
"""
|
|
70
|
+
message = _inner_message(update)
|
|
71
|
+
if message is None:
|
|
72
|
+
callback_query = _callback_query(update)
|
|
73
|
+
candidate = callback_query.get("message") if callback_query is not None else None
|
|
74
|
+
if not isinstance(candidate, Mapping):
|
|
75
|
+
return None
|
|
76
|
+
message = cast("Mapping[str, Any]", candidate)
|
|
77
|
+
chat_candidate = message.get("chat")
|
|
78
|
+
if not isinstance(chat_candidate, Mapping):
|
|
79
|
+
return None
|
|
80
|
+
chat = cast("Mapping[str, Any]", chat_candidate)
|
|
81
|
+
chat_id = chat.get("id")
|
|
82
|
+
return chat_id if isinstance(chat_id, int) else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def telegram_session_id(update: Mapping[str, Any], *, bot_id: int) -> str | None:
|
|
86
|
+
"""Return a session id using Telegram's native bot, user, and chat boundaries.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
update: A Telegram Bot API ``Update`` object.
|
|
90
|
+
|
|
91
|
+
Keyword Args:
|
|
92
|
+
bot_id: The Telegram bot's numeric user id.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
``telegram:<bot_id>:<user_id>`` for a private chat,
|
|
96
|
+
``telegram:<bot_id>:<chat_id>`` for other chats, or ``None`` when the
|
|
97
|
+
required Telegram identity is absent.
|
|
98
|
+
"""
|
|
99
|
+
chat_id = telegram_chat_id(update)
|
|
100
|
+
if chat_id is None:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
message = _inner_message(update)
|
|
104
|
+
callback_query = _callback_query(update)
|
|
105
|
+
if message is None and callback_query is not None:
|
|
106
|
+
callback_message = callback_query.get("message")
|
|
107
|
+
if isinstance(callback_message, Mapping):
|
|
108
|
+
message = cast("Mapping[str, Any]", callback_message)
|
|
109
|
+
|
|
110
|
+
chat_candidate = message.get("chat") if message is not None else None
|
|
111
|
+
chat = cast("Mapping[str, Any]", chat_candidate) if isinstance(chat_candidate, Mapping) else None
|
|
112
|
+
chat_type = chat.get("type") if chat is not None else None
|
|
113
|
+
if chat_type != "private":
|
|
114
|
+
return f"telegram:{bot_id}:{chat_id}"
|
|
115
|
+
|
|
116
|
+
if callback_query is not None:
|
|
117
|
+
sender_candidate = callback_query.get("from")
|
|
118
|
+
elif message is not None:
|
|
119
|
+
sender_candidate = message.get("from")
|
|
120
|
+
else:
|
|
121
|
+
return None
|
|
122
|
+
if not isinstance(sender_candidate, Mapping):
|
|
123
|
+
return None
|
|
124
|
+
sender = cast("Mapping[str, Any]", sender_candidate)
|
|
125
|
+
sender_id = sender.get("id")
|
|
126
|
+
return f"telegram:{bot_id}:{sender_id}" if isinstance(sender_id, int) else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def telegram_callback_query_id(update: Mapping[str, Any]) -> str | None:
|
|
130
|
+
"""Return the callback query id from an update, if present.
|
|
131
|
+
|
|
132
|
+
Apps need this to answer the callback query (``answerCallbackQuery``) so
|
|
133
|
+
Telegram stops showing the client-side loading spinner; this helper only
|
|
134
|
+
extracts the id, it does not call the Bot API.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
update: A Telegram Bot API ``Update`` object.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The callback query id, or ``None`` if the update has no ``callback_query``.
|
|
141
|
+
"""
|
|
142
|
+
callback_query = _callback_query(update)
|
|
143
|
+
if callback_query is None:
|
|
144
|
+
return None
|
|
145
|
+
query_id = callback_query.get("id")
|
|
146
|
+
return query_id if isinstance(query_id, str) else None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _command_source_text(update: Mapping[str, Any]) -> str | None:
|
|
150
|
+
"""Return the text a leading command should be parsed from."""
|
|
151
|
+
message = _inner_message(update)
|
|
152
|
+
if message is not None:
|
|
153
|
+
text = message.get("text")
|
|
154
|
+
if isinstance(text, str):
|
|
155
|
+
return text
|
|
156
|
+
callback_query = _callback_query(update)
|
|
157
|
+
if callback_query is not None:
|
|
158
|
+
data = callback_query.get("data")
|
|
159
|
+
if isinstance(data, str):
|
|
160
|
+
return data
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def telegram_command(update: Mapping[str, Any]) -> str | None:
|
|
165
|
+
"""Parse a leading slash command out of an update, without dispatching it.
|
|
166
|
+
|
|
167
|
+
Looks at ``message.text`` / ``edited_message.text`` first, then
|
|
168
|
+
``callback_query.data``. A bot-suffixed command (``/name@bot args``) is
|
|
169
|
+
normalized to ``/name args`` since a single Bot API integration only ever
|
|
170
|
+
serves one bot username. Callers are responsible for matching the
|
|
171
|
+
returned command name and acting on it.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
update: A Telegram Bot API ``Update`` object.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The normalized command (e.g. ``"/start"`` or ``"/start hello"``), or
|
|
178
|
+
``None`` if the source text does not start with a command.
|
|
179
|
+
"""
|
|
180
|
+
text = _command_source_text(update)
|
|
181
|
+
if text is None:
|
|
182
|
+
return None
|
|
183
|
+
match = _COMMAND_PATTERN.match(text)
|
|
184
|
+
if not match:
|
|
185
|
+
return None
|
|
186
|
+
return f"/{match.group('name')}{match.group('rest')}".rstrip()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _resolve_media_message(update_or_message: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
190
|
+
"""Return the message object to inspect for media, from either an update or a bare message."""
|
|
191
|
+
if "message" in update_or_message or "edited_message" in update_or_message or "callback_query" in update_or_message:
|
|
192
|
+
message = _inner_message(update_or_message)
|
|
193
|
+
if message is not None:
|
|
194
|
+
return message
|
|
195
|
+
callback_query = _callback_query(update_or_message)
|
|
196
|
+
if callback_query is not None:
|
|
197
|
+
candidate = callback_query.get("message")
|
|
198
|
+
if isinstance(candidate, Mapping):
|
|
199
|
+
return cast("Mapping[str, Any]", candidate)
|
|
200
|
+
return None
|
|
201
|
+
return update_or_message
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _largest_photo_file_id(photo_sizes: Sequence[Any]) -> str | None:
|
|
205
|
+
"""Return the ``file_id`` of the highest-resolution entry in a Telegram ``photo`` array."""
|
|
206
|
+
candidates = [cast("Mapping[str, Any]", size) for size in photo_sizes if isinstance(size, Mapping)]
|
|
207
|
+
if not candidates:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def _area(size: Mapping[str, Any]) -> int:
|
|
211
|
+
file_size = size.get("file_size")
|
|
212
|
+
if isinstance(file_size, int):
|
|
213
|
+
return file_size
|
|
214
|
+
width, height = size.get("width"), size.get("height")
|
|
215
|
+
return width * height if isinstance(width, int) and isinstance(height, int) else 0
|
|
216
|
+
|
|
217
|
+
largest = max(candidates, key=_area)
|
|
218
|
+
file_id = largest.get("file_id")
|
|
219
|
+
return file_id if isinstance(file_id, str) else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def telegram_media_file_id(update_or_message: Mapping[str, Any]) -> tuple[str, str] | None:
|
|
223
|
+
"""Return the ``(file_id, mime_type)`` of the inbound media attached to an update or message.
|
|
224
|
+
|
|
225
|
+
Accepts either a full ``Update`` object (media is read from ``message`` /
|
|
226
|
+
``edited_message`` / ``callback_query.message``) or a bare Telegram
|
|
227
|
+
message object. Photos pick the largest size Telegram sent (by
|
|
228
|
+
``file_size``, falling back to pixel area); documents, voice notes,
|
|
229
|
+
audio, and video use their own ``file_id`` and ``mime_type`` (falling
|
|
230
|
+
back to Telegram's known default MIME type when the field is absent).
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
update_or_message: A Telegram ``Update`` or message object.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A ``(file_id, mime_type)`` tuple, or ``None`` if there is no supported media.
|
|
237
|
+
"""
|
|
238
|
+
message = _resolve_media_message(update_or_message)
|
|
239
|
+
if message is None:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
photo = message.get("photo")
|
|
243
|
+
if isinstance(photo, Sequence) and not isinstance(photo, (str, bytes, bytearray)) and photo:
|
|
244
|
+
file_id = _largest_photo_file_id(cast("Sequence[Any]", photo))
|
|
245
|
+
if file_id is not None:
|
|
246
|
+
return file_id, "image/jpeg"
|
|
247
|
+
|
|
248
|
+
for key, default_mime_type in _MEDIA_DEFAULT_MIME_TYPES.items():
|
|
249
|
+
item_candidate = message.get(key)
|
|
250
|
+
if isinstance(item_candidate, Mapping):
|
|
251
|
+
item = cast("Mapping[str, Any]", item_candidate)
|
|
252
|
+
file_id = item.get("file_id")
|
|
253
|
+
if isinstance(file_id, str):
|
|
254
|
+
mime_type = item.get("mime_type")
|
|
255
|
+
return file_id, mime_type if isinstance(mime_type, str) and mime_type else default_mime_type
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _contents_from_message(
|
|
260
|
+
message: Mapping[str, Any],
|
|
261
|
+
resolve_file_url: ResolveFileUrl | None,
|
|
262
|
+
) -> list[Content]:
|
|
263
|
+
"""Translate one Telegram message object into Agent Framework content parts.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: If the message has no text, caption, or resolvable media.
|
|
267
|
+
"""
|
|
268
|
+
text = message.get("text")
|
|
269
|
+
caption = message.get("caption")
|
|
270
|
+
text_value = text if isinstance(text, str) and text else (caption if isinstance(caption, str) and caption else None)
|
|
271
|
+
|
|
272
|
+
contents: list[Content] = []
|
|
273
|
+
media = telegram_media_file_id(message)
|
|
274
|
+
if media is not None:
|
|
275
|
+
file_id, mime_type = media
|
|
276
|
+
resolved_url = await resolve_file_url(file_id) if resolve_file_url is not None else None
|
|
277
|
+
if resolved_url:
|
|
278
|
+
contents.append(Content.from_uri(uri=resolved_url, media_type=mime_type))
|
|
279
|
+
elif text_value is None:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
f"Cannot resolve Telegram media file_id={file_id!r}: no `resolve_file_url` was given (or it "
|
|
282
|
+
"returned None), and the message has no text/caption to fall back to."
|
|
283
|
+
)
|
|
284
|
+
if text_value is not None:
|
|
285
|
+
contents.append(Content.from_text(text=text_value))
|
|
286
|
+
|
|
287
|
+
if not contents:
|
|
288
|
+
raise ValueError("Telegram message has no text, caption, or resolvable media to convert to a run.")
|
|
289
|
+
return contents
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def telegram_to_run(
|
|
293
|
+
update: Mapping[str, Any],
|
|
294
|
+
*,
|
|
295
|
+
resolve_file_url: ResolveFileUrl | None = None,
|
|
296
|
+
stream: bool = False,
|
|
297
|
+
) -> AgentRunArgs:
|
|
298
|
+
"""Convert a Telegram update into Agent Framework run values.
|
|
299
|
+
|
|
300
|
+
Supports ``message``, ``edited_message``, and ``callback_query`` updates.
|
|
301
|
+
Message/edited-message text or caption becomes text content; inbound
|
|
302
|
+
media becomes uri content when ``resolve_file_url`` resolves it (media
|
|
303
|
+
alone with no resolvable URL and no text/caption raises rather than
|
|
304
|
+
producing an empty run). A callback query's ``data`` becomes the user's
|
|
305
|
+
text.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
update: A Telegram Bot API ``Update`` object.
|
|
309
|
+
|
|
310
|
+
Keyword Args:
|
|
311
|
+
resolve_file_url: Optional async callable that resolves a Telegram
|
|
312
|
+
``file_id`` (typically via the Bot API's ``getFile``) to a
|
|
313
|
+
fetchable URL, or ``None`` if it cannot. When omitted, media is
|
|
314
|
+
ignored and only text/caption is used.
|
|
315
|
+
stream: Whether the caller intends to run the agent in streaming mode.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Arguments corresponding to ``Agent.run``.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
ValueError: If the update has no actionable message/callback data, or
|
|
322
|
+
a message has no text, caption, or resolvable media.
|
|
323
|
+
"""
|
|
324
|
+
message = _inner_message(update)
|
|
325
|
+
if message is not None:
|
|
326
|
+
contents = await _contents_from_message(message, resolve_file_url)
|
|
327
|
+
else:
|
|
328
|
+
callback_query = _callback_query(update)
|
|
329
|
+
data = callback_query.get("data") if callback_query is not None else None
|
|
330
|
+
if not isinstance(data, str) or not data:
|
|
331
|
+
raise ValueError("Telegram update has no actionable `message`, `edited_message`, or `callback_query.data`.")
|
|
332
|
+
contents = [Content.from_text(text=data)]
|
|
333
|
+
|
|
334
|
+
return AgentRunArgs(
|
|
335
|
+
messages=[Message("user", contents)],
|
|
336
|
+
options=cast("ChatOptions[Any]", {}),
|
|
337
|
+
stream=stream,
|
|
338
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Rendering helpers that translate Agent Framework results into native Telegram Bot API calls.
|
|
4
|
+
|
|
5
|
+
Each helper returns (or yields) a small ``TelegramOperation`` -- a Telegram
|
|
6
|
+
Bot API method name plus its JSON payload. App-owned code decides how to
|
|
7
|
+
actually invoke the Bot API (``sendMessage``, ``sendPhoto``,
|
|
8
|
+
``editMessageText``, ...), including authentication, retries, and rate
|
|
9
|
+
limiting; these helpers make no network calls.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from typing import Any, TypedDict
|
|
16
|
+
|
|
17
|
+
from agent_framework import AgentResponse, AgentResponseUpdate, ResponseStream
|
|
18
|
+
|
|
19
|
+
# Telegram's documented maximum length, in UTF-16 code units, for message
|
|
20
|
+
# text (`sendMessage` / `editMessageText`) and photo captions (`sendPhoto`).
|
|
21
|
+
TELEGRAM_MAX_TEXT_LENGTH = 4096
|
|
22
|
+
TELEGRAM_MAX_CAPTION_LENGTH = 1024
|
|
23
|
+
|
|
24
|
+
_NO_RESPONSE_TEXT = "(no response)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TelegramOperation(TypedDict):
|
|
28
|
+
"""A single native Telegram Bot API call produced by a rendering helper.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
method: The Telegram Bot API method name (e.g. ``"sendMessage"``).
|
|
32
|
+
payload: The JSON-serializable request body for that method.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
method: str
|
|
36
|
+
payload: dict[str, Any]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _truncate(text: str, max_length: int) -> str:
|
|
40
|
+
"""Deterministically cap ``text`` at ``max_length`` UTF-16 code units."""
|
|
41
|
+
units = 0
|
|
42
|
+
for index, char in enumerate(text):
|
|
43
|
+
units += 2 if ord(char) > 0xFFFF else 1
|
|
44
|
+
if units > max_length:
|
|
45
|
+
return text[:index]
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _text_and_image_uris(result: AgentResponse[Any]) -> tuple[str, list[str]]:
|
|
50
|
+
"""Return the response's concatenated text and any image uris it carries."""
|
|
51
|
+
image_uris = [
|
|
52
|
+
content.uri
|
|
53
|
+
for message in result.messages
|
|
54
|
+
for content in message.contents
|
|
55
|
+
if content.type == "uri" and content.uri and (content.media_type or "").startswith("image/")
|
|
56
|
+
]
|
|
57
|
+
return result.text, image_uris
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def telegram_from_run(
|
|
61
|
+
result: AgentResponse[Any],
|
|
62
|
+
*,
|
|
63
|
+
chat_id: int,
|
|
64
|
+
parse_mode: str | None = None,
|
|
65
|
+
) -> TelegramOperation:
|
|
66
|
+
"""Render a finished agent run as one native Telegram Bot API call.
|
|
67
|
+
|
|
68
|
+
An image in the response renders as ``sendPhoto`` (using the first image
|
|
69
|
+
found; any accompanying text becomes its caption). Otherwise, response
|
|
70
|
+
text renders as ``sendMessage``, falling back to ``"(no response)"`` when
|
|
71
|
+
the response has neither text nor an image.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
result: The finished agent response to render.
|
|
75
|
+
|
|
76
|
+
Keyword Args:
|
|
77
|
+
chat_id: The Telegram chat id to address the message to.
|
|
78
|
+
parse_mode: Optional Telegram ``parse_mode`` (e.g. ``"MarkdownV2"``,
|
|
79
|
+
``"HTML"``) to attach to the rendered payload.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A ``TelegramOperation`` describing the Bot API call to make.
|
|
83
|
+
"""
|
|
84
|
+
text, image_uris = _text_and_image_uris(result)
|
|
85
|
+
if image_uris:
|
|
86
|
+
payload: dict[str, Any] = {"chat_id": chat_id, "photo": image_uris[0]}
|
|
87
|
+
if text:
|
|
88
|
+
payload["caption"] = _truncate(text, TELEGRAM_MAX_CAPTION_LENGTH)
|
|
89
|
+
if parse_mode:
|
|
90
|
+
payload["parse_mode"] = parse_mode
|
|
91
|
+
return TelegramOperation(method="sendPhoto", payload=payload)
|
|
92
|
+
|
|
93
|
+
payload = {"chat_id": chat_id, "text": _truncate(text or _NO_RESPONSE_TEXT, TELEGRAM_MAX_TEXT_LENGTH)}
|
|
94
|
+
if parse_mode:
|
|
95
|
+
payload["parse_mode"] = parse_mode
|
|
96
|
+
return TelegramOperation(method="sendMessage", payload=payload)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def telegram_from_streaming_run(
|
|
100
|
+
stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]],
|
|
101
|
+
*,
|
|
102
|
+
chat_id: int,
|
|
103
|
+
message_id: int,
|
|
104
|
+
initial_text: str | None = None,
|
|
105
|
+
parse_mode: str | None = None,
|
|
106
|
+
) -> AsyncIterator[TelegramOperation]:
|
|
107
|
+
"""Render a streaming agent run as a sequence of native Telegram Bot API calls.
|
|
108
|
+
|
|
109
|
+
Yields one ``editMessageText`` operation per update carrying new text, each
|
|
110
|
+
with the cumulative text so far (Telegram has no incremental-append edit
|
|
111
|
+
call). Interim edits omit ``parse_mode`` since intermediate text is not
|
|
112
|
+
guaranteed to be valid in the target markup. After the stream ends, yields
|
|
113
|
+
a final ``editMessageText`` reflecting the finalized response text (this
|
|
114
|
+
one includes ``parse_mode`` when given) and then one ``sendPhoto``
|
|
115
|
+
operation per image the finalized response carries. Errors raised while
|
|
116
|
+
iterating the stream or while finalizing it (``stream.get_final_response()``)
|
|
117
|
+
propagate to the caller rather than being swallowed.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
stream: The agent response stream returned by ``agent.run(..., stream=True)``.
|
|
121
|
+
|
|
122
|
+
Keyword Args:
|
|
123
|
+
chat_id: The Telegram chat id the streamed message lives in.
|
|
124
|
+
message_id: The id of the Telegram message to edit as new content arrives.
|
|
125
|
+
initial_text: The current text of the app-created placeholder message.
|
|
126
|
+
Matching edits are omitted because Telegram rejects no-op edits.
|
|
127
|
+
parse_mode: Optional Telegram ``parse_mode`` to attach to the final text edit.
|
|
128
|
+
|
|
129
|
+
Yields:
|
|
130
|
+
``TelegramOperation`` values describing the Bot API calls to make, in order.
|
|
131
|
+
"""
|
|
132
|
+
text = ""
|
|
133
|
+
last_rendered_text = _truncate(initial_text, TELEGRAM_MAX_TEXT_LENGTH) if initial_text is not None else ""
|
|
134
|
+
async for update in stream:
|
|
135
|
+
if update.text:
|
|
136
|
+
text += update.text
|
|
137
|
+
rendered_text = _truncate(text, TELEGRAM_MAX_TEXT_LENGTH)
|
|
138
|
+
if rendered_text == last_rendered_text:
|
|
139
|
+
continue
|
|
140
|
+
last_rendered_text = rendered_text
|
|
141
|
+
yield TelegramOperation(
|
|
142
|
+
method="editMessageText",
|
|
143
|
+
payload={
|
|
144
|
+
"chat_id": chat_id,
|
|
145
|
+
"message_id": message_id,
|
|
146
|
+
"text": rendered_text,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
final = await stream.get_final_response()
|
|
151
|
+
final_text, image_uris = _text_and_image_uris(final)
|
|
152
|
+
|
|
153
|
+
if final_text or not image_uris:
|
|
154
|
+
rendered_text = _truncate(final_text or _NO_RESPONSE_TEXT, TELEGRAM_MAX_TEXT_LENGTH)
|
|
155
|
+
payload: dict[str, Any] = {
|
|
156
|
+
"chat_id": chat_id,
|
|
157
|
+
"message_id": message_id,
|
|
158
|
+
"text": rendered_text,
|
|
159
|
+
}
|
|
160
|
+
if parse_mode:
|
|
161
|
+
payload["parse_mode"] = parse_mode
|
|
162
|
+
if rendered_text != last_rendered_text or parse_mode:
|
|
163
|
+
yield TelegramOperation(method="editMessageText", payload=payload)
|
|
164
|
+
else:
|
|
165
|
+
yield TelegramOperation(
|
|
166
|
+
method="deleteMessage",
|
|
167
|
+
payload={"chat_id": chat_id, "message_id": message_id},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for uri in image_uris:
|
|
171
|
+
yield TelegramOperation(method="sendPhoto", payload={"chat_id": chat_id, "photo": uri})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-framework-hosting-telegram"
|
|
3
|
+
description = "Telegram Bot API-shaped helpers for agent-framework-hosting."
|
|
4
|
+
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
version = "1.0.0a260721"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
|
+
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
11
|
+
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
|
12
|
+
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"agent-framework-core>=1.11.0,<2",
|
|
27
|
+
"agent-framework-hosting==1.0.0a260721",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.uv]
|
|
31
|
+
prerelease = "if-necessary-or-explicit"
|
|
32
|
+
environments = [
|
|
33
|
+
"sys_platform == 'darwin'",
|
|
34
|
+
"sys_platform == 'linux'",
|
|
35
|
+
"sys_platform == 'win32'"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.uv-dynamic-versioning]
|
|
39
|
+
fallback-version = "0.0.0"
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = 'tests'
|
|
43
|
+
addopts = "-ra -q -r fEX"
|
|
44
|
+
asyncio_mode = "auto"
|
|
45
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
46
|
+
filterwarnings = []
|
|
47
|
+
timeout = 120
|
|
48
|
+
markers = [
|
|
49
|
+
"integration: marks tests as integration tests that require external services",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
extend = "../../pyproject.toml"
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
omit = [
|
|
57
|
+
"**/__init__.py"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pyright]
|
|
61
|
+
extends = "../../pyproject.toml"
|
|
62
|
+
include = ["agent_framework_hosting_telegram"]
|
|
63
|
+
exclude = ['tests']
|
|
64
|
+
|
|
65
|
+
[tool.bandit]
|
|
66
|
+
targets = ["agent_framework_hosting_telegram"]
|
|
67
|
+
exclude_dirs = ["tests"]
|
|
68
|
+
|
|
69
|
+
[tool.poe]
|
|
70
|
+
executor.type = "uv"
|
|
71
|
+
include = "../../shared_tasks.toml"
|
|
72
|
+
|
|
73
|
+
[tool.poe.tasks.test]
|
|
74
|
+
help = "Run the default unit test suite for this package."
|
|
75
|
+
cmd = 'pytest -m "not integration" --cov=agent_framework_hosting_telegram --cov-report=term-missing:skip-covered tests'
|
|
76
|
+
|
|
77
|
+
[build-system]
|
|
78
|
+
requires = ["flit-core >= 3.11,<4.0"]
|
|
79
|
+
build-backend = "flit_core.buildapi"
|