microsoft-agents-hosting-slack 1.1.0.dev7__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.1.0.dev7/LICENSE +21 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/MANIFEST.in +1 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/PKG-INFO +71 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/VERSION.txt +1 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/__init__.py +26 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/_path_navigator.py +108 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/__init__.py +42 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/action_payload.py +26 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/chunks.py +77 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/event_content.py +45 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/event_envelope.py +56 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_api.py +120 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_channel_data.py +67 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_model.py +73 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_response.py +42 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_stream.py +160 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/slack_agent_extension.py +202 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/slack_helpers.py +61 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents_hosting_slack.egg-info/PKG-INFO +71 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents_hosting_slack.egg-info/SOURCES.txt +25 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents_hosting_slack.egg-info/dependency_links.txt +1 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents_hosting_slack.egg-info/requires.txt +2 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents_hosting_slack.egg-info/top_level.txt +1 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/pyproject.toml +25 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/readme.md +49 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/setup.cfg +4 -0
- microsoft_agents_hosting_slack-1.1.0.dev7/setup.py +18 -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 @@
|
|
|
1
|
+
include VERSION.txt
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-agents-hosting-slack
|
|
3
|
+
Version: 1.1.0.dev7
|
|
4
|
+
Summary: Integration library for Microsoft Agents with Slack
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/microsoft/Agents
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: microsoft-agents-hosting-core==1.1.0.dev7
|
|
19
|
+
Requires-Dist: aiohttp>=3.11.11
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
|
|
23
|
+
# Microsoft Agents Hosting - Slack
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/microsoft-agents-hosting-slack/)
|
|
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
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install microsoft-agents-hosting-slack
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from microsoft_agents.hosting.core.app import AgentApplication
|
|
39
|
+
from microsoft_agents.hosting.slack import SlackAgentExtension
|
|
40
|
+
from microsoft_agents.hosting.slack.api import SlackChannelData
|
|
41
|
+
|
|
42
|
+
app = AgentApplication(options)
|
|
43
|
+
slack = SlackAgentExtension(app)
|
|
44
|
+
|
|
45
|
+
@slack.on_message()
|
|
46
|
+
async def on_slack_message(context, state):
|
|
47
|
+
channel_data = SlackChannelData.from_activity(context.activity)
|
|
48
|
+
await slack.call(
|
|
49
|
+
context,
|
|
50
|
+
"chat.postMessage",
|
|
51
|
+
{
|
|
52
|
+
"channel": channel_data.get("event.channel"),
|
|
53
|
+
"text": f"You said: {context.activity.text}",
|
|
54
|
+
},
|
|
55
|
+
token=channel_data.api_token,
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Key Classes
|
|
60
|
+
|
|
61
|
+
- **`SlackAgentExtension`** — registers Slack-channel-scoped message/event handlers; exposes `call(...)` and `create_stream(...)`.
|
|
62
|
+
- **`SlackChannelData`** — typed wrapper around Bot Service's Slack channel-data payload with `get(path)` / `try_get(path)` accessors.
|
|
63
|
+
- **`SlackApi`** — async HTTP client for the Slack Web API.
|
|
64
|
+
- **`SlackStream`** — wraps Slack's streaming methods for incremental message updates.
|
|
65
|
+
- **`SlackHelpers`** — encode/decode and conversation-id parsing utilities.
|
|
66
|
+
|
|
67
|
+
# Quick Links
|
|
68
|
+
|
|
69
|
+
- 📦 [All SDK Packages on PyPI](https://pypi.org/search/?q=microsoft-agents)
|
|
70
|
+
- 📖 [Complete Documentation](https://aka.ms/agents)
|
|
71
|
+
- 🐛 [Report Issues](https://github.com/microsoft/Agents-for-python/issues)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.1.0.dev7
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
Licensed under the MIT License.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .slack_agent_extension import SlackAgentExtension
|
|
7
|
+
from .slack_helpers import (
|
|
8
|
+
create_conversation_id,
|
|
9
|
+
slack_bot_id_from_conversation_id,
|
|
10
|
+
slack_channel_id_from_conversation_id,
|
|
11
|
+
slack_decode,
|
|
12
|
+
slack_encode,
|
|
13
|
+
slack_team_id_from_conversation_id,
|
|
14
|
+
slack_thread_ts_from_conversation_id,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SlackAgentExtension",
|
|
19
|
+
"create_conversation_id",
|
|
20
|
+
"slack_bot_id_from_conversation_id",
|
|
21
|
+
"slack_channel_id_from_conversation_id",
|
|
22
|
+
"slack_decode",
|
|
23
|
+
"slack_encode",
|
|
24
|
+
"slack_team_id_from_conversation_id",
|
|
25
|
+
"slack_thread_ts_from_conversation_id",
|
|
26
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
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 typing import Any, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_path(path: str) -> Optional[list[int | str]]:
|
|
12
|
+
"""Tokenize a dot/bracket path into a list of segments.
|
|
13
|
+
|
|
14
|
+
Supports ``a.b.c``, ``a[0].b``, ``a[1][2]``. Integer brackets become ``int``
|
|
15
|
+
segments; everything else becomes a string segment. Returns ``None`` for
|
|
16
|
+
malformed bracket nesting.
|
|
17
|
+
"""
|
|
18
|
+
if not path:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
segments: list[Any] = []
|
|
22
|
+
i = 0
|
|
23
|
+
start = 0
|
|
24
|
+
|
|
25
|
+
def emit() -> None:
|
|
26
|
+
nonlocal start
|
|
27
|
+
if start < i:
|
|
28
|
+
segments.append(path[start:i])
|
|
29
|
+
start = i + 1
|
|
30
|
+
|
|
31
|
+
while i < len(path):
|
|
32
|
+
ch = path[i]
|
|
33
|
+
if ch == ".":
|
|
34
|
+
emit()
|
|
35
|
+
elif ch == "[":
|
|
36
|
+
emit()
|
|
37
|
+
nesting = 1
|
|
38
|
+
i += 1
|
|
39
|
+
inner_start = i
|
|
40
|
+
while i < len(path):
|
|
41
|
+
c = path[i]
|
|
42
|
+
if c == "[":
|
|
43
|
+
nesting += 1
|
|
44
|
+
elif c == "]":
|
|
45
|
+
nesting -= 1
|
|
46
|
+
if nesting == 0:
|
|
47
|
+
break
|
|
48
|
+
i += 1
|
|
49
|
+
if nesting != 0:
|
|
50
|
+
return None
|
|
51
|
+
inner = path[inner_start:i]
|
|
52
|
+
if inner.isdigit() or (inner.startswith("-") and inner[1:].isdigit()):
|
|
53
|
+
segments.append(int(inner))
|
|
54
|
+
else:
|
|
55
|
+
segments.append(inner)
|
|
56
|
+
start = i + 1
|
|
57
|
+
i += 1
|
|
58
|
+
|
|
59
|
+
emit()
|
|
60
|
+
return segments
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_segment(current: Any, segment: Any) -> Tuple[bool, Any]:
|
|
64
|
+
"""Resolve one path segment against the current node.
|
|
65
|
+
|
|
66
|
+
Returns ``(found, value)``. ``found`` is False when the segment cannot be
|
|
67
|
+
resolved (missing key, out-of-range index, primitive node).
|
|
68
|
+
"""
|
|
69
|
+
if current is None:
|
|
70
|
+
return False, None
|
|
71
|
+
|
|
72
|
+
if isinstance(segment, int):
|
|
73
|
+
if isinstance(current, (list, tuple)):
|
|
74
|
+
if -len(current) <= segment < len(current):
|
|
75
|
+
return True, current[segment]
|
|
76
|
+
return False, None
|
|
77
|
+
return False, None
|
|
78
|
+
|
|
79
|
+
# string segment → dict key (case-sensitive fast path, case-insensitive fallback)
|
|
80
|
+
if isinstance(current, dict):
|
|
81
|
+
if segment in current:
|
|
82
|
+
return True, current[segment]
|
|
83
|
+
lower = segment.lower()
|
|
84
|
+
for key, value in current.items():
|
|
85
|
+
if isinstance(key, str) and key.lower() == lower:
|
|
86
|
+
return True, value
|
|
87
|
+
return False, None
|
|
88
|
+
|
|
89
|
+
return False, None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def try_get_path_value(data: Any, path: str) -> Tuple[bool, Any]:
|
|
93
|
+
"""Walk ``path`` against ``data``. Returns ``(found, value)``."""
|
|
94
|
+
if data is None:
|
|
95
|
+
return False, None
|
|
96
|
+
if not path:
|
|
97
|
+
return True, data
|
|
98
|
+
|
|
99
|
+
segments = _parse_path(path)
|
|
100
|
+
if segments is None:
|
|
101
|
+
return False, None
|
|
102
|
+
|
|
103
|
+
current: Any = data
|
|
104
|
+
for segment in segments:
|
|
105
|
+
found, current = _resolve_segment(current, segment)
|
|
106
|
+
if not found:
|
|
107
|
+
return False, None
|
|
108
|
+
return True, current
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
Licensed under the MIT License.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .action_payload import ActionPayload
|
|
7
|
+
from .chunks import (
|
|
8
|
+
BlocksChunk,
|
|
9
|
+
Chunk,
|
|
10
|
+
MarkdownTextChunk,
|
|
11
|
+
SlackTaskStatus,
|
|
12
|
+
Source,
|
|
13
|
+
TaskDisplayMode,
|
|
14
|
+
TaskUpdateChunk,
|
|
15
|
+
)
|
|
16
|
+
from .event_content import EventContent
|
|
17
|
+
from .event_envelope import EventEnvelope
|
|
18
|
+
from .slack_api import SLACK_API_BASE, SlackApi
|
|
19
|
+
from .slack_channel_data import SlackChannelData
|
|
20
|
+
from .slack_model import SlackModel
|
|
21
|
+
from .slack_response import SlackResponse, SlackResponseException
|
|
22
|
+
from .slack_stream import SlackStream
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ActionPayload",
|
|
26
|
+
"BlocksChunk",
|
|
27
|
+
"Chunk",
|
|
28
|
+
"EventContent",
|
|
29
|
+
"EventEnvelope",
|
|
30
|
+
"MarkdownTextChunk",
|
|
31
|
+
"SLACK_API_BASE",
|
|
32
|
+
"SlackApi",
|
|
33
|
+
"SlackChannelData",
|
|
34
|
+
"SlackModel",
|
|
35
|
+
"SlackResponse",
|
|
36
|
+
"SlackResponseException",
|
|
37
|
+
"SlackStream",
|
|
38
|
+
"SlackTaskStatus",
|
|
39
|
+
"Source",
|
|
40
|
+
"TaskDisplayMode",
|
|
41
|
+
"TaskUpdateChunk",
|
|
42
|
+
]
|
microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/action_payload.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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 typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict
|
|
11
|
+
|
|
12
|
+
from .slack_model import SlackModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ActionPayload(SlackModel):
|
|
16
|
+
"""
|
|
17
|
+
Interactive Message / Block Kit action payload from Slack. Sent when a user
|
|
18
|
+
clicks a button or interacts with a block element.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
22
|
+
|
|
23
|
+
type: Optional[str] = None
|
|
24
|
+
channel: Optional[Any] = None
|
|
25
|
+
message: Optional[Any] = None
|
|
26
|
+
actions: Optional[Any] = None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
Licensed under the MIT License.
|
|
4
|
+
|
|
5
|
+
Streaming chunk shapes for Slack ``chat.appendStream`` / ``chat.stopStream``.
|
|
6
|
+
See https://docs.slack.dev/reference/methods/chat.appendStream
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SlackTaskStatus:
|
|
17
|
+
"""Status values accepted by :class:`TaskUpdateChunk`."""
|
|
18
|
+
|
|
19
|
+
PENDING = "pending"
|
|
20
|
+
IN_PROGRESS = "in_progress"
|
|
21
|
+
COMPLETE = "complete"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskDisplayMode:
|
|
26
|
+
"""Values accepted by ``chat.startStream``'s ``task_display_mode``."""
|
|
27
|
+
|
|
28
|
+
PLAN = "plan"
|
|
29
|
+
TIMELINE = "timeline"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Source(BaseModel):
|
|
33
|
+
"""Citation/source attached to a :class:`TaskUpdateChunk`."""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
36
|
+
|
|
37
|
+
type: str = "url"
|
|
38
|
+
url: str = ""
|
|
39
|
+
text: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MarkdownTextChunk(BaseModel):
|
|
43
|
+
"""Append a chunk of markdown-formatted text to a Slack stream."""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
46
|
+
|
|
47
|
+
type: str = Field(default="markdown_text", frozen=True)
|
|
48
|
+
text: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BlocksChunk(BaseModel):
|
|
52
|
+
"""Append a chunk of Block Kit blocks to a Slack stream."""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
55
|
+
|
|
56
|
+
type: str = Field(default="blocks", frozen=True)
|
|
57
|
+
blocks: list[Any] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TaskUpdateChunk(BaseModel):
|
|
61
|
+
"""Append a task-status update chunk to a Slack stream."""
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
64
|
+
|
|
65
|
+
type: str = Field(default="task_update", frozen=True)
|
|
66
|
+
id: str
|
|
67
|
+
title: str
|
|
68
|
+
status: str = SlackTaskStatus.IN_PROGRESS
|
|
69
|
+
details: Optional[str] = None
|
|
70
|
+
output: Optional[str] = None
|
|
71
|
+
sources: Optional[list[Source]] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Type alias for any chunk variant.
|
|
75
|
+
Chunk = (
|
|
76
|
+
BaseModel # all chunk classes are BaseModel subclasses with a `type` discriminator
|
|
77
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
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 typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict
|
|
11
|
+
|
|
12
|
+
from .slack_model import SlackModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventContent(SlackModel):
|
|
16
|
+
"""
|
|
17
|
+
The inner ``event`` object from a Slack Events API callback payload.
|
|
18
|
+
|
|
19
|
+
Slack calls this the "event content". Because event payloads vary so widely
|
|
20
|
+
by ``type``, every unmodelled field is preserved via Pydantic's
|
|
21
|
+
``extra="allow"`` and is reachable through :meth:`SlackModel.get` using the
|
|
22
|
+
same snake_case names shown in the Slack docs.
|
|
23
|
+
|
|
24
|
+
See https://docs.slack.dev/reference/events
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
28
|
+
|
|
29
|
+
# ── 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
|
|
38
|
+
|
|
39
|
+
# ── message event fields ──
|
|
40
|
+
text: Optional[str] = None
|
|
41
|
+
client_msg_id: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
# ── reaction_added / reaction_removed event fields ──
|
|
44
|
+
reaction: Optional[str] = None
|
|
45
|
+
item_user: Optional[str] = None
|
microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/event_envelope.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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 typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
from .event_content import EventContent
|
|
13
|
+
from .slack_model import SlackModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EventEnvelope(SlackModel):
|
|
17
|
+
"""
|
|
18
|
+
Outer envelope for a Slack Events API callback. Contains workspace, application,
|
|
19
|
+
and authorization context, plus the inner event payload (``event_content``).
|
|
20
|
+
|
|
21
|
+
See https://docs.slack.dev/apis/events-api/#callback-field.
|
|
22
|
+
|
|
23
|
+
Path navigation supports both the Slack JSON prefix ``event.`` and the Python
|
|
24
|
+
property prefix ``event_content.`` interchangeably::
|
|
25
|
+
|
|
26
|
+
envelope = SlackChannelData.from_activity(turn_context.activity).envelope
|
|
27
|
+
workspace_id = envelope.get("team_id")
|
|
28
|
+
channel = envelope.get("event.channel")
|
|
29
|
+
block_type = envelope.get("event.blocks[0].type")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
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
|
|
39
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
def _normalize_path(self, path: str) -> str:
|
|
51
|
+
"""Map the C# property alias ``event_content`` to JSON field ``event``."""
|
|
52
|
+
if path.lower() == "event_content":
|
|
53
|
+
return "event"
|
|
54
|
+
if path.lower().startswith("event_content."):
|
|
55
|
+
return "event" + path[len("event_content") :]
|
|
56
|
+
return path
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
Licensed under the MIT License.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from .slack_response import SlackResponse, SlackResponseException
|
|
15
|
+
|
|
16
|
+
SLACK_API_BASE = "https://slack.com/api"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _serialize_options(options: Any) -> str:
|
|
20
|
+
"""Serialize the ``options`` argument to a JSON string suitable for Slack.
|
|
21
|
+
|
|
22
|
+
- ``None`` becomes ``"{}"``.
|
|
23
|
+
- A pre-built JSON string is returned as-is.
|
|
24
|
+
- A Pydantic model is dumped with ``exclude_none=True`` to match Slack's
|
|
25
|
+
tolerance of omitted fields and the C# implementation's null-stripping.
|
|
26
|
+
- Anything else is passed through :func:`json.dumps`, after recursively
|
|
27
|
+
removing ``None`` values from dicts/lists.
|
|
28
|
+
"""
|
|
29
|
+
if options is None:
|
|
30
|
+
return "{}"
|
|
31
|
+
if isinstance(options, str):
|
|
32
|
+
return options
|
|
33
|
+
if isinstance(options, BaseModel):
|
|
34
|
+
return options.model_dump_json(by_alias=True, exclude_none=True)
|
|
35
|
+
return json.dumps(_strip_nones(options), ensure_ascii=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _strip_nones(value: Any) -> Any:
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
return {k: _strip_nones(v) for k, v in value.items() if v is not None}
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
return [_strip_nones(v) for v in value if v is not None]
|
|
43
|
+
if isinstance(value, BaseModel):
|
|
44
|
+
return value.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SlackApi:
|
|
49
|
+
"""
|
|
50
|
+
Async HTTP client for the Slack Web API.
|
|
51
|
+
|
|
52
|
+
Mirrors the C# ``SlackApi``: every call is a ``POST`` to
|
|
53
|
+
``https://slack.com/api/{method}`` with a JSON body and an optional bearer
|
|
54
|
+
token. The response body is parsed into a :class:`SlackResponse`; a
|
|
55
|
+
:class:`SlackResponseException` is raised on a non-2xx HTTP status or when
|
|
56
|
+
Slack returns ``ok=false``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
session: Optional[ClientSession] = None,
|
|
62
|
+
*,
|
|
63
|
+
base_url: str = SLACK_API_BASE,
|
|
64
|
+
request_timeout: float = 30.0,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._session = session
|
|
67
|
+
self._owns_session = session is None
|
|
68
|
+
self._base_url = base_url
|
|
69
|
+
self._timeout = ClientTimeout(total=request_timeout)
|
|
70
|
+
|
|
71
|
+
async def call(
|
|
72
|
+
self,
|
|
73
|
+
method: str,
|
|
74
|
+
options: Any = None,
|
|
75
|
+
token: str = "",
|
|
76
|
+
) -> SlackResponse:
|
|
77
|
+
"""Invoke a Slack Web API method.
|
|
78
|
+
|
|
79
|
+
:param method: The API method name (e.g. ``"chat.postMessage"``).
|
|
80
|
+
:param options: Request payload. May be ``None``, a JSON string, a
|
|
81
|
+
``dict`` / ``list``, or a Pydantic model.
|
|
82
|
+
:param token: Bearer token. Sent as ``Authorization: Bearer {token}``
|
|
83
|
+
when non-empty.
|
|
84
|
+
:returns: The parsed :class:`SlackResponse`.
|
|
85
|
+
:raises ValueError: if ``method`` is empty or whitespace.
|
|
86
|
+
:raises SlackResponseException: on HTTP error or ``ok=false``.
|
|
87
|
+
"""
|
|
88
|
+
if not method or not method.strip():
|
|
89
|
+
raise ValueError("method must be a non-empty string")
|
|
90
|
+
|
|
91
|
+
body = _serialize_options(options)
|
|
92
|
+
url = f"{self._base_url}/{method}"
|
|
93
|
+
headers = {"Content-Type": "application/json"}
|
|
94
|
+
if token and token.strip():
|
|
95
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
96
|
+
|
|
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()
|
microsoft_agents_hosting_slack-1.1.0.dev7/microsoft_agents/hosting/slack/api/slack_channel_data.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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 typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
from .action_payload import ActionPayload
|
|
13
|
+
from .event_envelope import EventEnvelope
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SlackChannelData(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Data associated with a Slack channel activity, as delivered by Azure Bot
|
|
19
|
+
Service in :attr:`Activity.channel_data`.
|
|
20
|
+
|
|
21
|
+
Bot Service historically named the envelope property ``SlackMessage`` and
|
|
22
|
+
the API token ``ApiToken``; this model accepts both the PascalCase JSON
|
|
23
|
+
names (via aliases) and snake_case Python names.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
27
|
+
|
|
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")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def channel(self) -> Optional[str]:
|
|
34
|
+
"""Slack channel id, sourced from the envelope or action payload."""
|
|
35
|
+
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")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def thread_ts(self) -> Optional[str]:
|
|
43
|
+
"""Thread timestamp, sourced from the envelope event or payload message."""
|
|
44
|
+
if self.envelope is not None:
|
|
45
|
+
return self.envelope.get("event.thread_ts") or self.envelope.get("event.ts")
|
|
46
|
+
if self.payload is not None:
|
|
47
|
+
return self.payload.get("message.thread_ts") or self.payload.get(
|
|
48
|
+
"message.ts"
|
|
49
|
+
)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_activity(cls, activity: Any) -> "SlackChannelData":
|
|
54
|
+
"""Build a :class:`SlackChannelData` from an Activity's ``channel_data``.
|
|
55
|
+
|
|
56
|
+
Accepts ``Activity.channel_data`` that is either a ``dict`` (typical for
|
|
57
|
+
deserialized incoming requests) or already a :class:`SlackChannelData`.
|
|
58
|
+
Returns an empty instance when ``channel_data`` is missing.
|
|
59
|
+
"""
|
|
60
|
+
data = getattr(activity, "channel_data", None) if activity is not None else None
|
|
61
|
+
if data is None:
|
|
62
|
+
return cls()
|
|
63
|
+
if isinstance(data, cls):
|
|
64
|
+
return data
|
|
65
|
+
if isinstance(data, BaseModel):
|
|
66
|
+
data = data.model_dump(mode="json", by_alias=True)
|
|
67
|
+
return cls.model_validate(data)
|