microsoft-agents-hosting-slack 1.9.0.dev3__tar.gz → 1.9.0.dev10__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.
- {microsoft_agents_hosting_slack-1.9.0.dev3/microsoft_agents_hosting_slack.egg-info → microsoft_agents_hosting_slack-1.9.0.dev10}/PKG-INFO +34 -93
- microsoft_agents_hosting_slack-1.9.0.dev10/VERSION.txt +1 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/__init__.py +4 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/_path_navigator.py +2 -2
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/action_payload.py +5 -5
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/chunks.py +4 -4
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/event_content.py +12 -14
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/event_envelope.py +14 -14
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/slack_api.py +34 -26
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/slack_channel_data.py +32 -13
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents/hosting/slack/api/slack_model.py +96 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/slack_response.py +6 -6
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/slack_stream.py +4 -4
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents/hosting/slack/route_handlers.py +49 -0
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents/hosting/slack/slack_activity.py +31 -0
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents/hosting/slack/slack_agent_extension.py +368 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/slack_helpers.py +8 -10
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents/hosting/slack/slack_turn_context.py +67 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents_hosting_slack.egg-info}/PKG-INFO +34 -93
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents_hosting_slack.egg-info/SOURCES.txt +3 -0
- microsoft_agents_hosting_slack-1.9.0.dev10/microsoft_agents_hosting_slack.egg-info/requires.txt +2 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/readme.md +32 -91
- microsoft_agents_hosting_slack-1.9.0.dev3/VERSION.txt +0 -1
- microsoft_agents_hosting_slack-1.9.0.dev3/microsoft_agents/hosting/slack/api/slack_model.py +0 -73
- microsoft_agents_hosting_slack-1.9.0.dev3/microsoft_agents/hosting/slack/slack_agent_extension.py +0 -200
- microsoft_agents_hosting_slack-1.9.0.dev3/microsoft_agents_hosting_slack.egg-info/requires.txt +0 -2
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/LICENSE +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/MANIFEST.in +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/api/__init__.py +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents/hosting/slack/py.typed +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents_hosting_slack.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/microsoft_agents_hosting_slack.egg-info/top_level.txt +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/pyproject.toml +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/setup.cfg +0 -0
- {microsoft_agents_hosting_slack-1.9.0.dev3 → microsoft_agents_hosting_slack-1.9.0.dev10}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-agents-hosting-slack
|
|
3
|
-
Version: 1.9.0.
|
|
3
|
+
Version: 1.9.0.dev10
|
|
4
4
|
Summary: Integration library for Microsoft Agents with Slack
|
|
5
5
|
Author: Microsoft Corporation
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,7 +15,7 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: microsoft-agents-hosting-core==1.9.0.
|
|
18
|
+
Requires-Dist: microsoft-agents-hosting-core==1.9.0.dev10
|
|
19
19
|
Requires-Dist: aiohttp>=3.12
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
Dynamic: requires-dist
|
|
@@ -24,81 +24,8 @@ Dynamic: requires-dist
|
|
|
24
24
|
|
|
25
25
|
[](https://pypi.org/project/microsoft-agents-hosting-slack/)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Release Notes
|
|
31
|
-
|
|
32
|
-
<table style="width:100%">
|
|
33
|
-
<tr>
|
|
34
|
-
<th style="width:20%">Version</th>
|
|
35
|
-
<th style="width:20%">Date</th>
|
|
36
|
-
<th style="width:60%">Release Notes</th>
|
|
37
|
-
</tr>
|
|
38
|
-
<tr>
|
|
39
|
-
<td>1.8.0</td>
|
|
40
|
-
<td>2026-10-01</td>
|
|
41
|
-
<td>
|
|
42
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v180">
|
|
43
|
-
1.8.0 Release Notes
|
|
44
|
-
</a>
|
|
45
|
-
</td>
|
|
46
|
-
</tr>
|
|
47
|
-
<tr>
|
|
48
|
-
<td>1.7.0</td>
|
|
49
|
-
<td>2026-09-17</td>
|
|
50
|
-
<td>
|
|
51
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v170">
|
|
52
|
-
1.7.0 Release Notes
|
|
53
|
-
</a>
|
|
54
|
-
</td>
|
|
55
|
-
</tr>
|
|
56
|
-
<tr>
|
|
57
|
-
<td>1.5.0</td>
|
|
58
|
-
<td>2026-08-26</td>
|
|
59
|
-
<td>
|
|
60
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v150">
|
|
61
|
-
1.5.0 Release Notes
|
|
62
|
-
</a>
|
|
63
|
-
</td>
|
|
64
|
-
</tr>
|
|
65
|
-
<tr>
|
|
66
|
-
<td>1.4.0</td>
|
|
67
|
-
<td>2026-08-18</td>
|
|
68
|
-
<td>
|
|
69
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v140">
|
|
70
|
-
1.4.0 Release Notes
|
|
71
|
-
</a>
|
|
72
|
-
</td>
|
|
73
|
-
</tr>
|
|
74
|
-
<tr>
|
|
75
|
-
<td>1.3.0</td>
|
|
76
|
-
<td>2026-07-30</td>
|
|
77
|
-
<td>
|
|
78
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v130">
|
|
79
|
-
1.3.0 Release Notes
|
|
80
|
-
</a>
|
|
81
|
-
</td>
|
|
82
|
-
</tr>
|
|
83
|
-
<tr>
|
|
84
|
-
<td>1.2.0</td>
|
|
85
|
-
<td>2026-07-17</td>
|
|
86
|
-
<td>
|
|
87
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v120">
|
|
88
|
-
1.2.0 Release Notes
|
|
89
|
-
</a>
|
|
90
|
-
</td>
|
|
91
|
-
</tr>
|
|
92
|
-
<tr>
|
|
93
|
-
<td>1.1.0</td>
|
|
94
|
-
<td>2026-06-19</td>
|
|
95
|
-
<td>
|
|
96
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v110">
|
|
97
|
-
1.1.0 Release Notes
|
|
98
|
-
</a>
|
|
99
|
-
</td>
|
|
100
|
-
</tr>
|
|
101
|
-
</table>
|
|
27
|
+
Build Slack agents with the Microsoft 365 Agents SDK using typed message and event
|
|
28
|
+
handlers, direct Slack Web API calls, and streaming responses.
|
|
102
29
|
|
|
103
30
|
## Packages Overview
|
|
104
31
|
|
|
@@ -116,7 +43,7 @@ We offer the following PyPI packages to create conversational experiences based
|
|
|
116
43
|
| `microsoft-agents-storage-blob` | [](https://pypi.org/project/microsoft-agents-storage-blob/) | Extension to use Azure Blob as storage. |
|
|
117
44
|
| `microsoft-agents-storage-cosmos` | [](https://pypi.org/project/microsoft-agents-storage-cosmos/) | Extension to use CosmosDB as storage. |
|
|
118
45
|
| `microsoft-agents-authentication-msal` | [](https://pypi.org/project/microsoft-agents-authentication-msal/) | MSAL-based authentication for Microsoft Agents. |
|
|
119
|
-
| `microsoft-agents-authentication-entra-auth-sidecar` | [](https://pypi.org/project/microsoft-agents-authentication-entra-auth-sidecar/) | Credential-free Entra ID Agent ID authentication via the sidecar. |
|
|
46
|
+
| `microsoft-agents-authentication-entra-auth-sidecar` | [](https://pypi.org/project/microsoft-agents-authentication-entra-auth-sidecar/) | Credential-free Entra ID Agent ID authentication via the sidecar. |
|
|
120
47
|
|
|
121
48
|
## Installation
|
|
122
49
|
|
|
@@ -126,38 +53,52 @@ pip install microsoft-agents-hosting-slack
|
|
|
126
53
|
|
|
127
54
|
## Usage
|
|
128
55
|
|
|
56
|
+
Given configured application options (including storage) and a connection manager:
|
|
57
|
+
|
|
129
58
|
```python
|
|
130
59
|
from microsoft_agents.hosting.core.app import AgentApplication
|
|
131
|
-
from microsoft_agents.hosting.
|
|
132
|
-
from microsoft_agents.hosting.slack
|
|
60
|
+
from microsoft_agents.hosting.core.app.state import TurnState
|
|
61
|
+
from microsoft_agents.hosting.slack import SlackAgentExtension, SlackTurnContext
|
|
133
62
|
|
|
134
|
-
app = AgentApplication(options)
|
|
63
|
+
app = AgentApplication[TurnState](options, connection_manager=connections)
|
|
135
64
|
slack = SlackAgentExtension(app)
|
|
136
65
|
|
|
137
|
-
@slack.
|
|
138
|
-
async def on_slack_message(context, state):
|
|
139
|
-
channel_data =
|
|
66
|
+
@slack.message()
|
|
67
|
+
async def on_slack_message(context: SlackTurnContext, state: TurnState) -> None:
|
|
68
|
+
channel_data = context.activity.slack_channel_data
|
|
140
69
|
await slack.call(
|
|
141
70
|
context,
|
|
142
71
|
"chat.postMessage",
|
|
143
72
|
{
|
|
144
|
-
"channel": channel_data.
|
|
73
|
+
"channel": channel_data.channel,
|
|
145
74
|
"text": f"You said: {context.activity.text}",
|
|
146
75
|
},
|
|
147
|
-
token=channel_data.api_token,
|
|
76
|
+
token=channel_data.api_token or "",
|
|
148
77
|
)
|
|
149
78
|
```
|
|
150
79
|
|
|
151
|
-
|
|
80
|
+
Use `@slack.message("hello")` for a specific message or `@slack.event("reaction_added")`
|
|
81
|
+
for a specific event. Both accept a string, a compiled regular expression, or a list of
|
|
82
|
+
either, and match only Slack activities. When called without a selector, they match every
|
|
83
|
+
Slack message or event and register at `RouteRank.LAST`, so more specific routes win.
|
|
84
|
+
`on_message()` and `on_event()` are deprecated aliases.
|
|
85
|
+
|
|
86
|
+
`@slack.conversation_update(...)`, `@slack.members_added()`, `@slack.members_removed()`,
|
|
87
|
+
and `@slack.installation_update()` mirror the corresponding `AgentApplication` route
|
|
88
|
+
hooks, but match only Slack activities and pass a `SlackTurnContext` to the handler.
|
|
89
|
+
|
|
90
|
+
Handlers receive a `SlackTurnContext` with typed `activity.slack_channel_data` and
|
|
91
|
+
access to the per-turn Slack API client through `context.client`. Envelope, payload,
|
|
92
|
+
and response models support nested lookups such as `envelope.get("event.channel")`
|
|
93
|
+
and typed lookups such as `response.get("count", type_=int)`.
|
|
152
94
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- **`SlackStream`** — wraps Slack's streaming methods for incremental message updates.
|
|
157
|
-
- **`SlackHelpers`** — encode/decode and conversation-id parsing utilities.
|
|
95
|
+
Use `slack.create_stream(context, thread_ts=...)` to start a `SlackStream`, then
|
|
96
|
+
append content and stop the stream. See the API docstrings for routing options,
|
|
97
|
+
client configuration, streaming requirements, and lookup behavior.
|
|
158
98
|
|
|
159
|
-
|
|
99
|
+
## Quick Links
|
|
160
100
|
|
|
101
|
+
- [Release Notes](https://github.com/microsoft/Agents-for-python/blob/main/changelog.md)
|
|
161
102
|
- 📦 [All SDK Packages on PyPI](https://pypi.org/search/?q=microsoft-agents)
|
|
162
103
|
- 📖 [Complete Documentation](https://aka.ms/agents)
|
|
163
104
|
- 🐛 [Report Issues](https://github.com/microsoft/Agents-for-python/issues)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.9.0.dev10
|
|
@@ -3,7 +3,9 @@ Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
3
3
|
Licensed under the MIT License.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from .slack_activity import SlackActivity
|
|
6
7
|
from .slack_agent_extension import SlackAgentExtension
|
|
8
|
+
from .slack_turn_context import SlackTurnContext
|
|
7
9
|
from .slack_helpers import (
|
|
8
10
|
create_conversation_id,
|
|
9
11
|
slack_bot_id_from_conversation_id,
|
|
@@ -15,7 +17,9 @@ from .slack_helpers import (
|
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
20
|
+
"SlackActivity",
|
|
18
21
|
"SlackAgentExtension",
|
|
22
|
+
"SlackTurnContext",
|
|
19
23
|
"create_conversation_id",
|
|
20
24
|
"slack_bot_id_from_conversation_id",
|
|
21
25
|
"slack_channel_id_from_conversation_id",
|
|
@@ -5,10 +5,10 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _parse_path(path: str) ->
|
|
11
|
+
def _parse_path(path: str) -> list[int | str] | None:
|
|
12
12
|
"""Tokenize a dot/bracket path into a list of segments.
|
|
13
13
|
|
|
14
14
|
Supports ``a.b.c``, ``a[0].b``, ``a[1][2]``. Integer brackets become ``int``
|
|
@@ -5,7 +5,7 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import ConfigDict
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ class ActionPayload(SlackModel):
|
|
|
20
20
|
|
|
21
21
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
22
22
|
|
|
23
|
-
type:
|
|
24
|
-
channel:
|
|
25
|
-
message:
|
|
26
|
-
actions:
|
|
23
|
+
type: str | None = None
|
|
24
|
+
channel: Any | None = None
|
|
25
|
+
message: Any | None = None
|
|
26
|
+
actions: Any | None = None
|
|
@@ -8,7 +8,7 @@ See https://docs.slack.dev/reference/methods/chat.appendStream
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
14
|
|
|
@@ -66,9 +66,9 @@ class TaskUpdateChunk(BaseModel):
|
|
|
66
66
|
id: str
|
|
67
67
|
title: str
|
|
68
68
|
status: str = SlackTaskStatus.IN_PROGRESS
|
|
69
|
-
details:
|
|
70
|
-
output:
|
|
71
|
-
sources:
|
|
69
|
+
details: str | None = None
|
|
70
|
+
output: str | None = None
|
|
71
|
+
sources: list[Source] | None = None
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
# Type alias for any chunk variant.
|
|
@@ -5,8 +5,6 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
8
|
from pydantic import ConfigDict
|
|
11
9
|
|
|
12
10
|
from .slack_model import SlackModel
|
|
@@ -27,19 +25,19 @@ class EventContent(SlackModel):
|
|
|
27
25
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
28
26
|
|
|
29
27
|
# ── Common event fields (https://docs.slack.dev/apis/events-api/#event-type-structure)
|
|
30
|
-
type:
|
|
31
|
-
event_ts:
|
|
32
|
-
user:
|
|
33
|
-
ts:
|
|
34
|
-
subtype:
|
|
35
|
-
channel:
|
|
36
|
-
channel_type:
|
|
37
|
-
team:
|
|
28
|
+
type: str | None = None
|
|
29
|
+
event_ts: str | None = None
|
|
30
|
+
user: str | None = None
|
|
31
|
+
ts: str | None = None
|
|
32
|
+
subtype: str | None = None
|
|
33
|
+
channel: str | None = None
|
|
34
|
+
channel_type: str | None = None
|
|
35
|
+
team: str | None = None
|
|
38
36
|
|
|
39
37
|
# ── message event fields ──
|
|
40
|
-
text:
|
|
41
|
-
client_msg_id:
|
|
38
|
+
text: str | None = None
|
|
39
|
+
client_msg_id: str | None = None
|
|
42
40
|
|
|
43
41
|
# ── reaction_added / reaction_removed event fields ──
|
|
44
|
-
reaction:
|
|
45
|
-
item_user:
|
|
42
|
+
reaction: str | None = None
|
|
43
|
+
item_user: str | None = None
|
|
@@ -5,7 +5,7 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import ConfigDict, Field
|
|
11
11
|
|
|
@@ -31,21 +31,21 @@ class EventEnvelope(SlackModel):
|
|
|
31
31
|
|
|
32
32
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
33
33
|
|
|
34
|
-
token:
|
|
35
|
-
team_id:
|
|
36
|
-
context_team_id:
|
|
37
|
-
context_enterprise_id:
|
|
38
|
-
api_app_id:
|
|
34
|
+
token: str | None = None
|
|
35
|
+
team_id: str | None = None
|
|
36
|
+
context_team_id: str | None = None
|
|
37
|
+
context_enterprise_id: str | None = None
|
|
38
|
+
api_app_id: str | None = None
|
|
39
39
|
|
|
40
40
|
# Python name `event_content` ↔ JSON field `event`
|
|
41
|
-
event_content:
|
|
42
|
-
|
|
43
|
-
type:
|
|
44
|
-
event_id:
|
|
45
|
-
event_time:
|
|
46
|
-
authorizations:
|
|
47
|
-
is_ext_shared_channel:
|
|
48
|
-
event_context:
|
|
41
|
+
event_content: EventContent | None = Field(default=None, alias="event")
|
|
42
|
+
|
|
43
|
+
type: str | None = None
|
|
44
|
+
event_id: str | None = None
|
|
45
|
+
event_time: int | None = None
|
|
46
|
+
authorizations: Any | None = None
|
|
47
|
+
is_ext_shared_channel: bool | None = None
|
|
48
|
+
event_context: str | None = None
|
|
49
49
|
|
|
50
50
|
def _normalize_path(self, path: str) -> str:
|
|
51
51
|
"""Map the C# property alias ``event_content`` to JSON field ``event``."""
|
|
@@ -6,7 +6,7 @@ Licensed under the MIT License.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
from aiohttp import ClientSession, ClientTimeout
|
|
12
12
|
from pydantic import BaseModel
|
|
@@ -58,11 +58,17 @@ class SlackApi:
|
|
|
58
58
|
|
|
59
59
|
def __init__(
|
|
60
60
|
self,
|
|
61
|
-
session:
|
|
61
|
+
session: ClientSession | None = None,
|
|
62
62
|
*,
|
|
63
63
|
base_url: str = SLACK_API_BASE,
|
|
64
64
|
request_timeout: float = 30.0,
|
|
65
65
|
) -> None:
|
|
66
|
+
"""Initialize the SlackApi client.
|
|
67
|
+
|
|
68
|
+
:param session: An optional aiohttp ClientSession. If not provided, a new session is created.
|
|
69
|
+
:param base_url: The base URL for the Slack API.
|
|
70
|
+
:param request_timeout: The request timeout in seconds.
|
|
71
|
+
"""
|
|
66
72
|
self._session = session
|
|
67
73
|
self._owns_session = session is None
|
|
68
74
|
self._base_url = base_url
|
|
@@ -94,27 +100,29 @@ class SlackApi:
|
|
|
94
100
|
if token and token.strip():
|
|
95
101
|
headers["Authorization"] = f"Bearer {token}"
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
if not self._session:
|
|
104
|
+
self._session = ClientSession()
|
|
105
|
+
async with self._session.post(
|
|
106
|
+
url, data=body, headers=headers, timeout=self._timeout
|
|
107
|
+
) as response:
|
|
108
|
+
text = await response.text()
|
|
109
|
+
try:
|
|
110
|
+
payload = json.loads(text) if text else {}
|
|
111
|
+
data = SlackResponse.model_validate(payload)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
raise SlackResponseException(
|
|
114
|
+
f"Slack API error on {method} (HTTP {response.status}):\n{text}"
|
|
115
|
+
) from exc
|
|
116
|
+
|
|
117
|
+
if not response.ok or not data.ok:
|
|
118
|
+
raise SlackResponseException(
|
|
119
|
+
f"Slack API error on {method} (HTTP {response.status}):\n{text}",
|
|
120
|
+
data,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
async def aclose(self) -> None:
|
|
126
|
+
"""Close the owned ClientSession, if any."""
|
|
127
|
+
if self._owns_session and self._session:
|
|
128
|
+
await self._session.close()
|
|
@@ -5,7 +5,8 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing_extensions import Self
|
|
9
10
|
|
|
10
11
|
from pydantic import BaseModel, ConfigDict, Field
|
|
11
12
|
|
|
@@ -21,36 +22,54 @@ class SlackChannelData(BaseModel):
|
|
|
21
22
|
Bot Service historically named the envelope property ``SlackMessage`` and
|
|
22
23
|
the API token ``ApiToken``; this model accepts both the PascalCase JSON
|
|
23
24
|
names (via aliases) and snake_case Python names.
|
|
25
|
+
|
|
26
|
+
Access this model through ``SlackActivity.slack_channel_data`` or use
|
|
27
|
+
:meth:`from_activity` with a plain turn context's activity. Nested path
|
|
28
|
+
lookups belong to the envelope and payload, not this wrapper. For example,
|
|
29
|
+
when an envelope is present, ``envelope.get("event.channel")`` reads the
|
|
30
|
+
channel and ``envelope.get("event.blocks[0].type")`` reads an array field.
|
|
31
|
+
Unknown Slack fields are preserved.
|
|
24
32
|
"""
|
|
25
33
|
|
|
26
34
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
27
35
|
|
|
28
|
-
envelope:
|
|
29
|
-
payload:
|
|
30
|
-
api_token:
|
|
36
|
+
envelope: EventEnvelope | None = Field(default=None, alias="SlackMessage")
|
|
37
|
+
payload: ActionPayload | None = Field(default=None, alias="Payload")
|
|
38
|
+
api_token: str | None = Field(default=None, alias="ApiToken")
|
|
31
39
|
|
|
32
40
|
@property
|
|
33
|
-
def channel(self) ->
|
|
41
|
+
def channel(self) -> str | None:
|
|
34
42
|
"""Slack channel id, sourced from the envelope or action payload."""
|
|
43
|
+
obj: Any = None
|
|
35
44
|
if self.envelope is not None:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
obj = self.envelope.get("event.channel")
|
|
46
|
+
if isinstance(obj, dict) and "id" in obj:
|
|
47
|
+
obj = obj.get("id")
|
|
48
|
+
elif self.payload is not None:
|
|
49
|
+
obj = self.payload.get("channel")
|
|
50
|
+
if isinstance(obj, dict) and "id" in obj:
|
|
51
|
+
obj = obj.get("id")
|
|
52
|
+
if not isinstance(obj, str):
|
|
53
|
+
obj = self.payload.get("container.channel_id")
|
|
54
|
+
if isinstance(obj, str):
|
|
55
|
+
return obj
|
|
39
56
|
return None
|
|
40
57
|
|
|
41
58
|
@property
|
|
42
|
-
def thread_ts(self) ->
|
|
59
|
+
def thread_ts(self) -> str | None:
|
|
43
60
|
"""Thread timestamp, sourced from the envelope event or payload message."""
|
|
44
61
|
if self.envelope is not None:
|
|
45
|
-
return self.envelope.get("event.thread_ts") or self.envelope.get(
|
|
62
|
+
return self.envelope.get("event.thread_ts", type_=str) or self.envelope.get(
|
|
63
|
+
"event.ts", type_=str
|
|
64
|
+
)
|
|
46
65
|
if self.payload is not None:
|
|
47
|
-
return self.payload.get("message.thread_ts") or self.payload.get(
|
|
48
|
-
"message.ts"
|
|
66
|
+
return self.payload.get("message.thread_ts", type_=str) or self.payload.get(
|
|
67
|
+
"message.ts", type_=str
|
|
49
68
|
)
|
|
50
69
|
return None
|
|
51
70
|
|
|
52
71
|
@classmethod
|
|
53
|
-
def from_activity(cls, activity: Any) ->
|
|
72
|
+
def from_activity(cls, activity: Any) -> Self:
|
|
54
73
|
"""Build a :class:`SlackChannelData` from an Activity's ``channel_data``.
|
|
55
74
|
|
|
56
75
|
Accepts ``Activity.channel_data`` that is either a ``dict`` (typical for
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
Licensed under the MIT License.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from types import NoneType
|
|
9
|
+
from typing import Any, TypeVar, overload
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|
12
|
+
|
|
13
|
+
from .._path_navigator import try_get_path_value
|
|
14
|
+
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
_DefaultT = TypeVar("_DefaultT")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SlackModel(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Base class for Slack model objects that expose dot-notation path navigation via
|
|
22
|
+
:meth:`get` and :meth:`try_get`.
|
|
23
|
+
|
|
24
|
+
Subclasses are Pydantic models with ``extra="allow"`` so any unmodelled fields
|
|
25
|
+
from Slack are preserved and reachable by path. The path navigator walks the
|
|
26
|
+
serialized form (``by_alias=True``), so paths use the same snake_case names
|
|
27
|
+
that appear in the Slack docs.
|
|
28
|
+
|
|
29
|
+
Subclasses whose JSON field names differ from their Python property names
|
|
30
|
+
override :meth:`_normalize_path` to remap the alias before navigation (e.g.
|
|
31
|
+
:class:`EventEnvelope` maps ``event_content`` → ``event``).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
35
|
+
|
|
36
|
+
def _data(self) -> dict[str, Any]:
|
|
37
|
+
"""Return the serialized form used for path navigation. Subclasses with
|
|
38
|
+
their own backing store may override this."""
|
|
39
|
+
return self.model_dump(mode="json", by_alias=True, exclude_none=False)
|
|
40
|
+
|
|
41
|
+
def _normalize_path(self, path: str) -> str:
|
|
42
|
+
"""Remap caller-supplied path before navigation. Default: identity."""
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def get(self, path: str, default: _DefaultT, type_: type[_T]) -> _T | _DefaultT: ...
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
def get(self, path: str, *, type_: type[_T]) -> _T | None: ...
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def get(self, path: str, default: Any = None, type_: None = None) -> Any: ...
|
|
53
|
+
|
|
54
|
+
def get(
|
|
55
|
+
self,
|
|
56
|
+
path: str,
|
|
57
|
+
default: _DefaultT | None = None,
|
|
58
|
+
type_: type[_T] | None = None,
|
|
59
|
+
) -> _T | _DefaultT | None:
|
|
60
|
+
"""Get a value at the dot-notation ``path``. Supports dot separators and
|
|
61
|
+
bracket array indexing (e.g. ``"message.attachments[0].text"``). Returns
|
|
62
|
+
``default`` (or ``None``) when the path does not exist.
|
|
63
|
+
|
|
64
|
+
When ``type_`` is supplied, Pydantic's ``TypeAdapter`` converts and
|
|
65
|
+
validates non-null values, raising ``ValidationError`` on failure.
|
|
66
|
+
Existing null values return ``default`` (or ``None``) for typed lookups,
|
|
67
|
+
except when ``type_`` is ``type(None)``, which validates null directly.
|
|
68
|
+
Defaults are returned unchanged, without validation.
|
|
69
|
+
Without ``type_``, existing values are returned as-is, including null.
|
|
70
|
+
An empty path selects the entire serialized model.
|
|
71
|
+
|
|
72
|
+
For example, ``response.get("count", type_=int)`` converts a value to
|
|
73
|
+
an integer, while ``envelope.get("event", type_=EventContent)``
|
|
74
|
+
validates it as an event model. Invalid non-null values raise
|
|
75
|
+
``pydantic.ValidationError`` even when a default is provided.
|
|
76
|
+
|
|
77
|
+
Typed overloads return the requested type plus ``None`` when no
|
|
78
|
+
default is supplied, or the requested type plus the default's type
|
|
79
|
+
when one is supplied. Untyped lookups remain dynamically typed.
|
|
80
|
+
"""
|
|
81
|
+
found, value = self.try_get(path)
|
|
82
|
+
if not found:
|
|
83
|
+
return default
|
|
84
|
+
if type_ is not None and type_ is not NoneType and value is None:
|
|
85
|
+
return default
|
|
86
|
+
return TypeAdapter(type_).validate_python(value) if type_ is not None else value
|
|
87
|
+
|
|
88
|
+
def try_get(self, path: str) -> tuple[bool, Any]:
|
|
89
|
+
"""Return a raw ``(found, value)`` tuple without conversion or validation.
|
|
90
|
+
|
|
91
|
+
An existing null value returns ``(True, None)``; a missing path returns
|
|
92
|
+
``(False, None)``. An empty path selects the entire serialized model.
|
|
93
|
+
"""
|
|
94
|
+
if not path:
|
|
95
|
+
return True, self._data()
|
|
96
|
+
return try_get_path_value(self._data(), self._normalize_path(path))
|
|
@@ -5,7 +5,7 @@ Licensed under the MIT License.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import ConfigDict
|
|
11
11
|
|
|
@@ -28,15 +28,15 @@ class SlackResponse(SlackModel):
|
|
|
28
28
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
29
29
|
|
|
30
30
|
ok: bool = False
|
|
31
|
-
error:
|
|
32
|
-
warning:
|
|
33
|
-
ts:
|
|
34
|
-
response_metadata:
|
|
31
|
+
error: str | None = None
|
|
32
|
+
warning: str | None = None
|
|
33
|
+
ts: str | None = None
|
|
34
|
+
response_metadata: Any | None = None
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class SlackResponseException(Exception):
|
|
38
38
|
"""Raised when a Slack Web API call returns a non-2xx status or ``ok=false``."""
|
|
39
39
|
|
|
40
|
-
def __init__(self, message: str, response:
|
|
40
|
+
def __init__(self, message: str, response: SlackResponse | None = None) -> None:
|
|
41
41
|
super().__init__(message)
|
|
42
42
|
self.response = response
|