Skip to content

Commit 1ad642e

Browse files
mwbrookslukegalbraithrussellWilliamBergamin
authored
Add 'agent: BoltAgent' listener argument (#1437)
Co-authored-by: Luke Russell <luke.russell@slack-corp.com> Co-authored-by: William Bergamin <wbergamin@salesforce.com>
1 parent 868cedb commit 1ad642e

File tree

19 files changed

+799
-6
lines changed

19 files changed

+799
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ venv/
1717
.venv*
1818
.env/
1919

20+
# claude
21+
.claude/*.local.json
22+
2023
# codecov / coverage
2124
.coverage
2225
cov_*

docs/english/_sidebar.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
"tools/bolt-python/concepts/token-rotation"
8686
]
8787
},
88+
{
89+
"type": "category",
90+
"label": "Experiments",
91+
"items": ["tools/bolt-python/experiments"]
92+
},
8893
{
8994
"type": "category",
9095
"label": "Legacy",

docs/english/experiments.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Experiments
2+
3+
Bolt for Python includes experimental features still under active development. These features may be fleeting, may not be perfectly polished, and should be thought of as available for use "at your own risk."
4+
5+
Experimental features are categorized as `semver:patch` until the experimental status is removed.
6+
7+
We love feedback from our community, so we encourage you to explore and interact with the [GitHub repo](https://github.com/slackapi/bolt-python). Contributions, bug reports, and any feedback are all helpful; let us nurture the Slack CLI together to help make building Slack apps more pleasant for everyone.
8+
9+
## Available experiments
10+
* [Agent listener argument](#agent)
11+
12+
## Agent listener argument {#agent}
13+
14+
The `agent: BoltAgent` listener argument provides access to AI agent-related features.
15+
16+
The `BoltAgent` and `AsyncBoltAgent` classes offer a `chat_stream()` method that comes pre-configured with event context defaults: `channel_id`, `thread_ts`, `team_id`, and `user_id` fields.
17+
18+
The listener argument is wired into the Bolt `kwargs` injection system, so listeners can declare it as a parameter or access it via the `context.agent` property.
19+
20+
### Example
21+
22+
```python
23+
from slack_bolt import BoltAgent
24+
25+
@app.event("app_mention")
26+
def handle_mention(agent: BoltAgent):
27+
stream = agent.chat_stream()
28+
stream.append(markdown_text="Hello!")
29+
stream.stop()
30+
```
31+
32+
### Limitations
33+
34+
The `chat_stream()` method currently only works when the `thread_ts` field is available in the event context (DMs and threaded replies). Top-level channel messages do not have a `thread_ts` field, and the `ts` field is not yet provided to `BoltAgent`.

slack_bolt/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .response import BoltResponse
2222

2323
# AI Agents & Assistants
24+
from .agent import BoltAgent
2425
from .middleware.assistant.assistant import (
2526
Assistant,
2627
)
@@ -46,6 +47,7 @@
4647
"CustomListenerMatcher",
4748
"BoltRequest",
4849
"BoltResponse",
50+
"BoltAgent",
4951
"Assistant",
5052
"AssistantThreadContext",
5153
"AssistantThreadContextStore",

slack_bolt/adapter/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
"""Adapter modules for running Bolt apps along with Web frameworks or Socket Mode.
2-
"""
1+
"""Adapter modules for running Bolt apps along with Web frameworks or Socket Mode."""

slack_bolt/agent/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .agent import BoltAgent
2+
3+
__all__ = [
4+
"BoltAgent",
5+
]

slack_bolt/agent/agent.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Optional
2+
3+
from slack_sdk import WebClient
4+
from slack_sdk.web.chat_stream import ChatStream
5+
6+
7+
class BoltAgent:
8+
"""Agent listener argument for building AI-powered Slack agents.
9+
10+
Experimental:
11+
This API is experimental and may change in future releases.
12+
13+
FIXME: chat_stream() only works when thread_ts is available (DMs and threaded replies).
14+
It does not work on channel messages because ts is not provided to BoltAgent yet.
15+
16+
@app.event("app_mention")
17+
def handle_mention(agent):
18+
stream = agent.chat_stream()
19+
stream.append(markdown_text="Hello!")
20+
stream.stop()
21+
"""
22+
23+
def __init__(
24+
self,
25+
*,
26+
client: WebClient,
27+
channel_id: Optional[str] = None,
28+
thread_ts: Optional[str] = None,
29+
team_id: Optional[str] = None,
30+
user_id: Optional[str] = None,
31+
):
32+
self._client = client
33+
self._channel_id = channel_id
34+
self._thread_ts = thread_ts
35+
self._team_id = team_id
36+
self._user_id = user_id
37+
38+
def chat_stream(
39+
self,
40+
*,
41+
channel: Optional[str] = None,
42+
thread_ts: Optional[str] = None,
43+
recipient_team_id: Optional[str] = None,
44+
recipient_user_id: Optional[str] = None,
45+
**kwargs,
46+
) -> ChatStream:
47+
"""Creates a ChatStream with defaults from event context.
48+
49+
Each call creates a new instance. Create multiple for parallel streams.
50+
51+
Args:
52+
channel: Channel ID. Defaults to the channel from the event context.
53+
thread_ts: Thread timestamp. Defaults to the thread_ts from the event context.
54+
recipient_team_id: Team ID of the recipient. Defaults to the team from the event context.
55+
recipient_user_id: User ID of the recipient. Defaults to the user from the event context.
56+
**kwargs: Additional arguments passed to ``WebClient.chat_stream()``.
57+
58+
Returns:
59+
A new ``ChatStream`` instance.
60+
"""
61+
provided = [arg for arg in (channel, thread_ts, recipient_team_id, recipient_user_id) if arg is not None]
62+
if provided and len(provided) < 4:
63+
raise ValueError(
64+
"Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them"
65+
)
66+
# Argument validation is delegated to chat_stream() and the API
67+
return self._client.chat_stream(
68+
channel=channel or self._channel_id, # type: ignore[arg-type]
69+
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
70+
recipient_team_id=recipient_team_id or self._team_id,
71+
recipient_user_id=recipient_user_id or self._user_id,
72+
**kwargs,
73+
)

slack_bolt/agent/async_agent.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from typing import Optional
2+
3+
from slack_sdk.web.async_client import AsyncWebClient
4+
from slack_sdk.web.async_chat_stream import AsyncChatStream
5+
6+
7+
class AsyncBoltAgent:
8+
"""Async agent listener argument for building AI-powered Slack agents.
9+
10+
Experimental:
11+
This API is experimental and may change in future releases.
12+
13+
@app.event("app_mention")
14+
async def handle_mention(agent):
15+
stream = await agent.chat_stream()
16+
await stream.append(markdown_text="Hello!")
17+
await stream.stop()
18+
"""
19+
20+
def __init__(
21+
self,
22+
*,
23+
client: AsyncWebClient,
24+
channel_id: Optional[str] = None,
25+
thread_ts: Optional[str] = None,
26+
team_id: Optional[str] = None,
27+
user_id: Optional[str] = None,
28+
):
29+
self._client = client
30+
self._channel_id = channel_id
31+
self._thread_ts = thread_ts
32+
self._team_id = team_id
33+
self._user_id = user_id
34+
35+
async def chat_stream(
36+
self,
37+
*,
38+
channel: Optional[str] = None,
39+
thread_ts: Optional[str] = None,
40+
recipient_team_id: Optional[str] = None,
41+
recipient_user_id: Optional[str] = None,
42+
**kwargs,
43+
) -> AsyncChatStream:
44+
"""Creates an AsyncChatStream with defaults from event context.
45+
46+
Each call creates a new instance. Create multiple for parallel streams.
47+
48+
Args:
49+
channel: Channel ID. Defaults to the channel from the event context.
50+
thread_ts: Thread timestamp. Defaults to the thread_ts from the event context.
51+
recipient_team_id: Team ID of the recipient. Defaults to the team from the event context.
52+
recipient_user_id: User ID of the recipient. Defaults to the user from the event context.
53+
**kwargs: Additional arguments passed to ``AsyncWebClient.chat_stream()``.
54+
55+
Returns:
56+
A new ``AsyncChatStream`` instance.
57+
"""
58+
provided = [arg for arg in (channel, thread_ts, recipient_team_id, recipient_user_id) if arg is not None]
59+
if provided and len(provided) < 4:
60+
raise ValueError(
61+
"Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them"
62+
)
63+
# Argument validation is delegated to chat_stream() and the API
64+
return await self._client.chat_stream(
65+
channel=channel or self._channel_id, # type: ignore[arg-type]
66+
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
67+
recipient_team_id=recipient_team_id or self._team_id,
68+
recipient_user_id=recipient_user_id or self._user_id,
69+
**kwargs,
70+
)

slack_bolt/kwargs_injection/args.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from slack_bolt.context.fail import Fail
99
from slack_bolt.context.get_thread_context.get_thread_context import GetThreadContext
1010
from slack_bolt.context.respond import Respond
11+
from slack_bolt.agent.agent import BoltAgent
1112
from slack_bolt.context.save_thread_context import SaveThreadContext
1213
from slack_bolt.context.say import Say
1314
from slack_bolt.context.set_status import SetStatus
@@ -102,6 +103,8 @@ def handle_buttons(args):
102103
"""`get_thread_context()` utility function for AI Agents & Assistants"""
103104
save_thread_context: Optional[SaveThreadContext]
104105
"""`save_thread_context()` utility function for AI Agents & Assistants"""
106+
agent: Optional[BoltAgent]
107+
"""`agent` listener argument for AI Agents & Assistants"""
105108
# middleware
106109
next: Callable[[], None]
107110
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
@@ -135,6 +138,7 @@ def __init__(
135138
set_suggested_prompts: Optional[SetSuggestedPrompts] = None,
136139
get_thread_context: Optional[GetThreadContext] = None,
137140
save_thread_context: Optional[SaveThreadContext] = None,
141+
agent: Optional[BoltAgent] = None,
138142
# As this method is not supposed to be invoked by bolt-python users,
139143
# the naming conflict with the built-in one affects
140144
# only the internals of this method
@@ -168,6 +172,7 @@ def __init__(
168172
self.set_suggested_prompts = set_suggested_prompts
169173
self.get_thread_context = get_thread_context
170174
self.save_thread_context = save_thread_context
175+
self.agent = agent
171176

172177
self.next: Callable[[], None] = next
173178
self.next_: Callable[[], None] = next

slack_bolt/kwargs_injection/async_args.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from logging import Logger
22
from typing import Callable, Awaitable, Dict, Any, Optional
33

4+
from slack_bolt.agent.async_agent import AsyncBoltAgent
45
from slack_bolt.context.ack.async_ack import AsyncAck
56
from slack_bolt.context.async_context import AsyncBoltContext
67
from slack_bolt.context.complete.async_complete import AsyncComplete
@@ -101,6 +102,8 @@ async def handle_buttons(args):
101102
"""`get_thread_context()` utility function for AI Agents & Assistants"""
102103
save_thread_context: Optional[AsyncSaveThreadContext]
103104
"""`save_thread_context()` utility function for AI Agents & Assistants"""
105+
agent: Optional[AsyncBoltAgent]
106+
"""`agent` listener argument for AI Agents & Assistants"""
104107
# middleware
105108
next: Callable[[], Awaitable[None]]
106109
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
@@ -134,6 +137,7 @@ def __init__(
134137
set_suggested_prompts: Optional[AsyncSetSuggestedPrompts] = None,
135138
get_thread_context: Optional[AsyncGetThreadContext] = None,
136139
save_thread_context: Optional[AsyncSaveThreadContext] = None,
140+
agent: Optional[AsyncBoltAgent] = None,
137141
next: Callable[[], Awaitable[None]],
138142
**kwargs, # noqa
139143
):
@@ -164,6 +168,7 @@ def __init__(
164168
self.set_suggested_prompts = set_suggested_prompts
165169
self.get_thread_context = get_thread_context
166170
self.save_thread_context = save_thread_context
171+
self.agent = agent
167172

168173
self.next: Callable[[], Awaitable[None]] = next
169174
self.next_: Callable[[], Awaitable[None]] = next

0 commit comments

Comments
 (0)