Skip to content

Commit fadc1a4

Browse files
committed
feat: add 'agent: BoltAgent' listener argument
1 parent e5cf0f7 commit fadc1a4

14 files changed

Lines changed: 740 additions & 6 deletions

File tree

.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_*

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/agent/__init__.py

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

slack_bolt/agent/agent.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
@app.event("app_mention")
14+
def handle_mention(agent):
15+
stream = agent.chat_stream()
16+
stream.append(markdown_text="Hello!")
17+
stream.stop()
18+
"""
19+
20+
def __init__(
21+
self,
22+
*,
23+
client: WebClient,
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+
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+
) -> ChatStream:
44+
"""Creates a ChatStream 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 ``WebClient.chat_stream()``.
54+
55+
Returns:
56+
A new ``ChatStream`` instance.
57+
"""
58+
resolved_channel = channel or self._channel_id
59+
resolved_thread_ts = thread_ts or self._thread_ts
60+
if resolved_channel is None:
61+
raise ValueError(
62+
"channel is required: provide it as an argument or ensure channel_id is set in the event context"
63+
)
64+
if resolved_thread_ts is None:
65+
raise ValueError(
66+
"thread_ts is required: provide it as an argument or ensure thread_ts is set in the event context"
67+
)
68+
return self._client.chat_stream(
69+
channel=resolved_channel,
70+
thread_ts=resolved_thread_ts,
71+
recipient_team_id=recipient_team_id or self._team_id,
72+
recipient_user_id=recipient_user_id or self._user_id,
73+
**kwargs,
74+
)

slack_bolt/agent/async_agent.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
resolved_channel = channel or self._channel_id
59+
resolved_thread_ts = thread_ts or self._thread_ts
60+
if resolved_channel is None:
61+
raise ValueError(
62+
"channel is required: provide it as an argument or ensure channel_id is set in the event context"
63+
)
64+
if resolved_thread_ts is None:
65+
raise ValueError(
66+
"thread_ts is required: provide it as an argument or ensure thread_ts is set in the event context"
67+
)
68+
return await self._client.chat_stream(
69+
channel=resolved_channel,
70+
thread_ts=resolved_thread_ts,
71+
recipient_team_id=recipient_team_id or self._team_id,
72+
recipient_user_id=recipient_user_id or self._user_id,
73+
**kwargs,
74+
)

slack_bolt/context/async_context.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import TYPE_CHECKING, Optional
22

33
from slack_sdk.web.async_client import AsyncWebClient
44

@@ -15,6 +15,9 @@
1515
from slack_bolt.context.set_title.async_set_title import AsyncSetTitle
1616
from slack_bolt.util.utils import create_copy
1717

18+
if TYPE_CHECKING:
19+
from slack_bolt.agent.async_agent import AsyncBoltAgent
20+
1821

1922
class AsyncBoltContext(BaseContext):
2023
"""Context object associated with a request from Slack."""
@@ -187,6 +190,34 @@ async def handle_button_clicks(context):
187190
self["fail"] = AsyncFail(client=self.client, function_execution_id=self.function_execution_id)
188191
return self["fail"]
189192

193+
@property
194+
def agent(self) -> "AsyncBoltAgent":
195+
"""`agent` listener argument for building AI-powered Slack agents.
196+
197+
Experimental:
198+
This API is experimental and may change in future releases.
199+
200+
@app.event("app_mention")
201+
async def handle_mention(agent):
202+
stream = await agent.chat_stream()
203+
await stream.append(markdown_text="Hello!")
204+
await stream.stop()
205+
206+
Returns:
207+
`AsyncBoltAgent` instance
208+
"""
209+
if "agent" not in self:
210+
from slack_bolt.agent.async_agent import AsyncBoltAgent
211+
212+
self["agent"] = AsyncBoltAgent(
213+
client=self.client,
214+
channel_id=self.channel_id,
215+
thread_ts=self.thread_ts,
216+
team_id=self.team_id,
217+
user_id=self.user_id,
218+
)
219+
return self["agent"]
220+
190221
@property
191222
def set_title(self) -> Optional[AsyncSetTitle]:
192223
return self.get("set_title")

slack_bolt/context/base_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BaseContext(dict):
3838
"set_status",
3939
"set_title",
4040
"set_suggested_prompts",
41+
"agent",
4142
]
4243
# Note that these items are not copyable, so when you add new items to this list,
4344
# you must modify ThreadListenerRunner/AsyncioListenerRunner's _build_lazy_request method to pass the values.

slack_bolt/context/context.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import TYPE_CHECKING, Optional
22

33
from slack_sdk import WebClient
44

@@ -15,6 +15,9 @@
1515
from slack_bolt.context.set_title import SetTitle
1616
from slack_bolt.util.utils import create_copy
1717

18+
if TYPE_CHECKING:
19+
from slack_bolt.agent.agent import BoltAgent
20+
1821

1922
class BoltContext(BaseContext):
2023
"""Context object associated with a request from Slack."""
@@ -188,6 +191,34 @@ def handle_button_clicks(context):
188191
self["fail"] = Fail(client=self.client, function_execution_id=self.function_execution_id)
189192
return self["fail"]
190193

194+
@property
195+
def agent(self) -> "BoltAgent":
196+
"""`agent` listener argument for building AI-powered Slack agents.
197+
198+
Experimental:
199+
This API is experimental and may change in future releases.
200+
201+
@app.event("app_mention")
202+
def handle_mention(agent):
203+
stream = agent.chat_stream()
204+
stream.append(markdown_text="Hello!")
205+
stream.stop()
206+
207+
Returns:
208+
`BoltAgent` instance
209+
"""
210+
if "agent" not in self:
211+
from slack_bolt.agent.agent import BoltAgent
212+
213+
self["agent"] = BoltAgent(
214+
client=self.client,
215+
channel_id=self.channel_id,
216+
thread_ts=self.thread_ts,
217+
team_id=self.team_id,
218+
user_id=self.user_id,
219+
)
220+
return self["agent"]
221+
191222
@property
192223
def set_title(self) -> Optional[SetTitle]:
193224
return self.get("set_title")

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)