microsoft-agents-hosting-slack 1.9.0.dev1__tar.gz → 1.9.0.dev5__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.
Files changed (35) hide show
  1. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/PKG-INFO +29 -94
  2. microsoft_agents_hosting_slack-1.9.0.dev5/VERSION.txt +1 -0
  3. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/__init__.py +4 -0
  4. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/_path_navigator.py +2 -2
  5. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/action_payload.py +5 -5
  6. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/chunks.py +4 -4
  7. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/event_content.py +12 -14
  8. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/event_envelope.py +14 -14
  9. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/slack_api.py +34 -26
  10. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/slack_channel_data.py +32 -13
  11. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents/hosting/slack/api/slack_model.py +96 -0
  12. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/slack_response.py +6 -6
  13. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/slack_stream.py +4 -4
  14. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents/hosting/slack/route_handlers.py +49 -0
  15. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents/hosting/slack/slack_activity.py +31 -0
  16. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents/hosting/slack/slack_agent_extension.py +297 -0
  17. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/slack_helpers.py +8 -10
  18. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents/hosting/slack/slack_turn_context.py +67 -0
  19. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents_hosting_slack.egg-info/PKG-INFO +29 -94
  20. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents_hosting_slack.egg-info/SOURCES.txt +3 -0
  21. microsoft_agents_hosting_slack-1.9.0.dev5/microsoft_agents_hosting_slack.egg-info/requires.txt +2 -0
  22. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/readme.md +26 -91
  23. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/setup.py +1 -1
  24. microsoft_agents_hosting_slack-1.9.0.dev1/VERSION.txt +0 -1
  25. microsoft_agents_hosting_slack-1.9.0.dev1/microsoft_agents/hosting/slack/api/slack_model.py +0 -73
  26. microsoft_agents_hosting_slack-1.9.0.dev1/microsoft_agents/hosting/slack/slack_agent_extension.py +0 -200
  27. microsoft_agents_hosting_slack-1.9.0.dev1/microsoft_agents_hosting_slack.egg-info/requires.txt +0 -2
  28. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/LICENSE +0 -0
  29. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/MANIFEST.in +0 -0
  30. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/api/__init__.py +0 -0
  31. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents/hosting/slack/py.typed +0 -0
  32. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents_hosting_slack.egg-info/dependency_links.txt +0 -0
  33. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/microsoft_agents_hosting_slack.egg-info/top_level.txt +0 -0
  34. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/pyproject.toml +0 -0
  35. {microsoft_agents_hosting_slack-1.9.0.dev1 → microsoft_agents_hosting_slack-1.9.0.dev5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-hosting-slack
3
- Version: 1.9.0.dev1
3
+ Version: 1.9.0.dev5
4
4
  Summary: Integration library for Microsoft Agents with Slack
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT
@@ -15,8 +15,8 @@ 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.dev1
19
- Requires-Dist: aiohttp>=3.11.11
18
+ Requires-Dist: microsoft-agents-hosting-core==1.9.0.dev5
19
+ Requires-Dist: aiohttp>=3.12
20
20
  Dynamic: license-file
21
21
  Dynamic: requires-dist
22
22
 
@@ -24,81 +24,8 @@ Dynamic: requires-dist
24
24
 
25
25
  [![PyPI version](https://img.shields.io/pypi/v/microsoft-agents-hosting-slack)](https://pypi.org/project/microsoft-agents-hosting-slack/)
26
26
 
27
- Integration library for building Slack agents using the Microsoft 365 Agents SDK. Provides direct-to-Slack responses (the full Slack Web API surface, beyond what Azure Bot Service exposes), a typed `SlackChannelData` envelope with dot-notation property access, and a `SlackStream` helper for `chat.startStream` / `chat.appendStream` / `chat.stopStream`.
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` | [![PyPI](https://img.shields.io/pypi/v/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` | [![PyPI](https://img.shields.io/pypi/v/microsoft-agents-storage-cosmos)](https://pypi.org/project/microsoft-agents-storage-cosmos/) | Extension to use CosmosDB as storage. |
118
45
  | `microsoft-agents-authentication-msal` | [![PyPI](https://img.shields.io/pypi/v/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` | [![PyPI](https://img.shields.io/pypi/v/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` | [![PyPI](https://img.shields.io/pypi/v/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,46 @@ 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.slack import SlackAgentExtension
132
- from microsoft_agents.hosting.slack.api import SlackChannelData
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.on_message()
138
- async def on_slack_message(context, state):
139
- channel_data = SlackChannelData.from_activity(context.activity)
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.get("event.channel"),
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
- ## Key Classes
80
+ Use `@slack.message("hello")` for a specific message or `@slack.event("reaction_added")`
81
+ for a specific event. Both accept compiled regular expressions and match only Slack
82
+ activities. `on_message()` and `on_event()` are deprecated aliases.
83
+
84
+ Handlers receive a `SlackTurnContext` with typed `activity.slack_channel_data` and
85
+ access to the per-turn Slack API client through `context.client`. Envelope, payload,
86
+ and response models support nested lookups such as `envelope.get("event.channel")`
87
+ and typed lookups such as `response.get("count", type_=int)`.
152
88
 
153
- - **`SlackAgentExtension`** — registers Slack-channel-scoped message/event handlers; exposes `call(...)` and `create_stream(...)`.
154
- - **`SlackChannelData`** — typed wrapper around Bot Service's Slack channel-data payload with `get(path)` / `try_get(path)` accessors.
155
- - **`SlackApi`** — async HTTP client for the Slack Web API.
156
- - **`SlackStream`** — wraps Slack's streaming methods for incremental message updates.
157
- - **`SlackHelpers`** — encode/decode and conversation-id parsing utilities.
89
+ Use `slack.create_stream(context, thread_ts=...)` to start a `SlackStream`, then
90
+ append content and stop the stream. See the API docstrings for routing options,
91
+ client configuration, streaming requirements, and lookup behavior.
158
92
 
159
- # Quick Links
93
+ ## Quick Links
160
94
 
95
+ - [Release Notes](https://github.com/microsoft/Agents-for-python/blob/main/changelog.md)
161
96
  - 📦 [All SDK Packages on PyPI](https://pypi.org/search/?q=microsoft-agents)
162
97
  - 📖 [Complete Documentation](https://aka.ms/agents)
163
98
  - 🐛 [Report Issues](https://github.com/microsoft/Agents-for-python/issues)
@@ -0,0 +1 @@
1
+ 1.9.0.dev5
@@ -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, Optional
8
+ from typing import Any
9
9
 
10
10
 
11
- def _parse_path(path: str) -> Optional[list[int | 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, Optional
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: Optional[str] = None
24
- channel: Optional[Any] = None
25
- message: Optional[Any] = None
26
- actions: Optional[Any] = None
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, Optional
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: Optional[str] = None
70
- output: Optional[str] = None
71
- sources: Optional[list[Source]] = None
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: Optional[str] = None
31
- event_ts: Optional[str] = None
32
- user: Optional[str] = None
33
- ts: Optional[str] = None
34
- subtype: Optional[str] = None
35
- channel: Optional[str] = None
36
- channel_type: Optional[str] = None
37
- team: Optional[str] = None
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: Optional[str] = None
41
- client_msg_id: Optional[str] = None
38
+ text: str | None = None
39
+ client_msg_id: str | None = None
42
40
 
43
41
  # ── reaction_added / reaction_removed event fields ──
44
- reaction: Optional[str] = None
45
- item_user: Optional[str] = None
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, Optional
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: Optional[str] = None
35
- team_id: Optional[str] = None
36
- context_team_id: Optional[str] = None
37
- context_enterprise_id: Optional[str] = None
38
- api_app_id: Optional[str] = None
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: Optional[EventContent] = Field(default=None, alias="event")
42
-
43
- type: Optional[str] = None
44
- event_id: Optional[str] = None
45
- event_time: Optional[int] = None
46
- authorizations: Optional[Any] = None
47
- is_ext_shared_channel: Optional[bool] = None
48
- event_context: Optional[str] = None
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, Optional
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: Optional[ClientSession] = None,
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
- session = self._session or ClientSession()
98
- try:
99
- async with session.post(
100
- url, data=body, headers=headers, timeout=self._timeout
101
- ) as response:
102
- text = await response.text()
103
- try:
104
- payload = json.loads(text) if text else {}
105
- data = SlackResponse.model_validate(payload)
106
- except Exception as exc:
107
- raise SlackResponseException(
108
- f"Slack API error on {method} (HTTP {response.status}):\n{text}"
109
- ) from exc
110
-
111
- if not response.ok or not data.ok:
112
- raise SlackResponseException(
113
- f"Slack API error on {method} (HTTP {response.status}):\n{text}",
114
- data,
115
- )
116
-
117
- return data
118
- finally:
119
- if self._owns_session:
120
- await session.close()
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, Optional
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: Optional[EventEnvelope] = Field(default=None, alias="SlackMessage")
29
- payload: Optional[ActionPayload] = Field(default=None, alias="Payload")
30
- api_token: Optional[str] = Field(default=None, alias="ApiToken")
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) -> Optional[str]:
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
- return self.envelope.get("event.channel")
37
- if self.payload is not None:
38
- return self.payload.get("channel")
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) -> Optional[str]:
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("event.ts")
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) -> "SlackChannelData":
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, Optional
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: Optional[str] = None
32
- warning: Optional[str] = None
33
- ts: Optional[str] = None
34
- response_metadata: Optional[Any] = None
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: Optional[SlackResponse] = None) -> None:
40
+ def __init__(self, message: str, response: SlackResponse | None = None) -> None:
41
41
  super().__init__(message)
42
42
  self.response = response
@@ -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, Optional
9
+ from typing import Any
10
10
 
11
11
  from pydantic import BaseModel
12
12
 
@@ -41,7 +41,7 @@ class SlackStream:
41
41
  self._channel = channel
42
42
  self._thread_ts = thread_ts
43
43
  self._token = token
44
- self._message_ts: Optional[str] = None
44
+ self._message_ts: str | None = None
45
45
 
46
46
  async def start(
47
47
  self, task_display_mode: str = TaskDisplayMode.PLAN
@@ -106,7 +106,7 @@ class SlackStream:
106
106
 
107
107
  async def stop(
108
108
  self,
109
- chunks: Optional[list[BaseModel]] = None,
109
+ chunks: list[BaseModel] | None = None,
110
110
  blocks: str | list[Any] | dict | None = None,
111
111
  ) -> None:
112
112
  """Stop the active stream, optionally finalizing with chunks and/or
@@ -139,7 +139,7 @@ class SlackStream:
139
139
  await self._slack_api.call("chat.stopStream", body, self._token)
140
140
 
141
141
  @staticmethod
142
- def _resolve_blocks(blocks: Any) -> Optional[list[Any]]:
142
+ def _resolve_blocks(blocks: Any) -> list[Any] | None:
143
143
  if blocks is None:
144
144
  return None
145
145
  if isinstance(blocks, str):