Skip to content

Commit cacf2ad

Browse files
feat: add support for say_stream utility function
1 parent 785b813 commit cacf2ad

11 files changed

Lines changed: 544 additions & 0 deletions

File tree

slack_bolt/context/async_context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from slack_bolt.context.get_thread_context.async_get_thread_context import AsyncGetThreadContext
1111
from slack_bolt.context.save_thread_context.async_save_thread_context import AsyncSaveThreadContext
1212
from slack_bolt.context.say.async_say import AsyncSay
13+
from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream
1314
from slack_bolt.context.set_status.async_set_status import AsyncSetStatus
1415
from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import AsyncSetSuggestedPrompts
1516
from slack_bolt.context.set_title.async_set_title import AsyncSetTitle
@@ -203,6 +204,10 @@ def set_suggested_prompts(self) -> Optional[AsyncSetSuggestedPrompts]:
203204
def get_thread_context(self) -> Optional[AsyncGetThreadContext]:
204205
return self.get("get_thread_context")
205206

207+
@property
208+
def say_stream(self) -> Optional[AsyncSayStream]:
209+
return self.get("say_stream")
210+
206211
@property
207212
def save_thread_context(self) -> Optional[AsyncSaveThreadContext]:
208213
return self.get("save_thread_context")

slack_bolt/context/context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from slack_bolt.context.respond import Respond
1111
from slack_bolt.context.save_thread_context import SaveThreadContext
1212
from slack_bolt.context.say import Say
13+
from slack_bolt.context.say_stream import SayStream
1314
from slack_bolt.context.set_status import SetStatus
1415
from slack_bolt.context.set_suggested_prompts import SetSuggestedPrompts
1516
from slack_bolt.context.set_title import SetTitle
@@ -204,6 +205,10 @@ def set_suggested_prompts(self) -> Optional[SetSuggestedPrompts]:
204205
def get_thread_context(self) -> Optional[GetThreadContext]:
205206
return self.get("get_thread_context")
206207

208+
@property
209+
def say_stream(self) -> Optional[SayStream]:
210+
return self.get("say_stream")
211+
207212
@property
208213
def save_thread_context(self) -> Optional[SaveThreadContext]:
209214
return self.get("save_thread_context")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Don't add async module imports here
2+
from .say_stream import SayStream
3+
4+
__all__ = [
5+
"SayStream",
6+
]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import warnings
2+
from typing import Optional
3+
4+
from slack_sdk.web.async_client import AsyncWebClient
5+
from slack_sdk.web.async_chat_stream import AsyncChatStream
6+
7+
from slack_bolt.warning import ExperimentalWarning
8+
9+
10+
class AsyncSayStream:
11+
client: AsyncWebClient
12+
channel_id: Optional[str]
13+
thread_ts: Optional[str]
14+
team_id: Optional[str]
15+
user_id: Optional[str]
16+
17+
def __init__(
18+
self,
19+
*,
20+
client: AsyncWebClient,
21+
channel_id: Optional[str] = None,
22+
thread_ts: Optional[str] = None,
23+
team_id: Optional[str] = None,
24+
user_id: Optional[str] = None,
25+
):
26+
self.client = client
27+
self.channel_id = channel_id
28+
self.thread_ts = thread_ts
29+
self.team_id = team_id
30+
self.user_id = user_id
31+
32+
async def __call__(
33+
self,
34+
*,
35+
buffer_size: Optional[int] = None,
36+
channel: Optional[str] = None,
37+
thread_ts: Optional[str] = None,
38+
recipient_team_id: Optional[str] = None,
39+
recipient_user_id: Optional[str] = None,
40+
**kwargs,
41+
) -> AsyncChatStream:
42+
warnings.warn(
43+
"say_stream is experimental and may change in future versions.",
44+
category=ExperimentalWarning,
45+
stacklevel=2,
46+
)
47+
48+
channel = channel or self.channel_id
49+
thread_ts = thread_ts or self.thread_ts
50+
if channel is None:
51+
raise ValueError("say_stream without channel here is unsupported")
52+
if thread_ts is None:
53+
raise ValueError("say_stream without thread_ts here is unsupported")
54+
55+
if buffer_size:
56+
return await self.client.chat_stream(
57+
buffer_size=buffer_size,
58+
channel=channel,
59+
thread_ts=thread_ts,
60+
recipient_team_id=recipient_team_id or self.team_id,
61+
recipient_user_id=recipient_user_id or self.user_id,
62+
**kwargs,
63+
)
64+
65+
return await self.client.chat_stream(
66+
channel=channel,
67+
thread_ts=thread_ts,
68+
recipient_team_id=recipient_team_id or self.team_id,
69+
recipient_user_id=recipient_user_id or self.user_id,
70+
**kwargs,
71+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import warnings
2+
from typing import Optional
3+
4+
from slack_sdk import WebClient
5+
from slack_sdk.web.chat_stream import ChatStream
6+
7+
from slack_bolt.warning import ExperimentalWarning
8+
9+
10+
class SayStream:
11+
client: WebClient
12+
channel_id: Optional[str]
13+
thread_ts: Optional[str]
14+
team_id: Optional[str]
15+
user_id: Optional[str]
16+
17+
def __init__(
18+
self,
19+
*,
20+
client: WebClient,
21+
channel_id: Optional[str] = None,
22+
thread_ts: Optional[str] = None,
23+
team_id: Optional[str] = None,
24+
user_id: Optional[str] = None,
25+
):
26+
self.client = client
27+
self.channel_id = channel_id
28+
self.thread_ts = thread_ts
29+
self.team_id = team_id
30+
self.user_id = user_id
31+
32+
def __call__(
33+
self,
34+
*,
35+
buffer_size: Optional[int] = None,
36+
channel: Optional[str] = None,
37+
thread_ts: Optional[str] = None,
38+
recipient_team_id: Optional[str] = None,
39+
recipient_user_id: Optional[str] = None,
40+
**kwargs,
41+
) -> ChatStream:
42+
warnings.warn(
43+
"say_stream is experimental and may change in future versions.",
44+
category=ExperimentalWarning,
45+
stacklevel=2,
46+
)
47+
48+
channel = channel or self.channel_id
49+
thread_ts = thread_ts or self.thread_ts
50+
if channel is None:
51+
raise ValueError("say_stream without channel here is unsupported")
52+
if thread_ts is None:
53+
raise ValueError("say_stream without thread_ts here is unsupported")
54+
55+
if buffer_size:
56+
return self.client.chat_stream(
57+
buffer_size=buffer_size,
58+
channel=channel,
59+
thread_ts=thread_ts,
60+
recipient_team_id=recipient_team_id or self.team_id,
61+
recipient_user_id=recipient_user_id or self.user_id,
62+
**kwargs,
63+
)
64+
65+
return self.client.chat_stream(
66+
channel=channel,
67+
thread_ts=thread_ts,
68+
recipient_team_id=recipient_team_id or self.team_id,
69+
recipient_user_id=recipient_user_id or self.user_id,
70+
**kwargs,
71+
)

slack_bolt/kwargs_injection/args.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from slack_bolt.agent.agent import BoltAgent
1212
from slack_bolt.context.save_thread_context import SaveThreadContext
1313
from slack_bolt.context.say import Say
14+
from slack_bolt.context.say_stream import SayStream
1415
from slack_bolt.context.set_status import SetStatus
1516
from slack_bolt.context.set_suggested_prompts import SetSuggestedPrompts
1617
from slack_bolt.context.set_title import SetTitle
@@ -105,6 +106,8 @@ def handle_buttons(args):
105106
"""`save_thread_context()` utility function for AI Agents & Assistants"""
106107
agent: Optional[BoltAgent]
107108
"""`agent` listener argument for AI Agents & Assistants"""
109+
say_stream: Optional[SayStream]
110+
"""`say_stream()` utility function for AI Agents & Assistants"""
108111
# middleware
109112
next: Callable[[], None]
110113
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
@@ -139,6 +142,7 @@ def __init__(
139142
get_thread_context: Optional[GetThreadContext] = None,
140143
save_thread_context: Optional[SaveThreadContext] = None,
141144
agent: Optional[BoltAgent] = None,
145+
say_stream: Optional[SayStream] = None,
142146
# As this method is not supposed to be invoked by bolt-python users,
143147
# the naming conflict with the built-in one affects
144148
# only the internals of this method
@@ -173,6 +177,7 @@ def __init__(
173177
self.get_thread_context = get_thread_context
174178
self.save_thread_context = save_thread_context
175179
self.agent = agent
180+
self.say_stream = say_stream
176181

177182
self.next: Callable[[], None] = next
178183
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
@@ -10,6 +10,7 @@
1010
from slack_bolt.context.get_thread_context.async_get_thread_context import AsyncGetThreadContext
1111
from slack_bolt.context.save_thread_context.async_save_thread_context import AsyncSaveThreadContext
1212
from slack_bolt.context.say.async_say import AsyncSay
13+
from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream
1314
from slack_bolt.context.set_status.async_set_status import AsyncSetStatus
1415
from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import AsyncSetSuggestedPrompts
1516
from slack_bolt.context.set_title.async_set_title import AsyncSetTitle
@@ -104,6 +105,8 @@ async def handle_buttons(args):
104105
"""`save_thread_context()` utility function for AI Agents & Assistants"""
105106
agent: Optional[AsyncBoltAgent]
106107
"""`agent` listener argument for AI Agents & Assistants"""
108+
say_stream: Optional[AsyncSayStream]
109+
"""`say_stream()` utility function for AI Agents & Assistants"""
107110
# middleware
108111
next: Callable[[], Awaitable[None]]
109112
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
@@ -138,6 +141,7 @@ def __init__(
138141
get_thread_context: Optional[AsyncGetThreadContext] = None,
139142
save_thread_context: Optional[AsyncSaveThreadContext] = None,
140143
agent: Optional[AsyncBoltAgent] = None,
144+
say_stream: Optional[AsyncSayStream] = None,
141145
next: Callable[[], Awaitable[None]],
142146
**kwargs, # noqa
143147
):
@@ -169,6 +173,7 @@ def __init__(
169173
self.get_thread_context = get_thread_context
170174
self.save_thread_context = save_thread_context
171175
self.agent = agent
176+
self.say_stream = say_stream
172177

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

slack_bolt/middleware/attaching_agent_kwargs/async_attaching_agent_kwargs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from slack_bolt.context.assistant.async_assistant_utilities import AsyncAssistantUtilities
44
from slack_bolt.context.assistant.thread_context_store.async_store import AsyncAssistantThreadContextStore
5+
from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream
56
from slack_bolt.middleware.async_middleware import AsyncMiddleware
67
from slack_bolt.request.async_request import AsyncBoltRequest
78
from slack_bolt.request.payload_utils import is_assistant_event, to_event
@@ -36,4 +37,15 @@ async def async_process(
3637
req.context["set_suggested_prompts"] = assistant.set_suggested_prompts
3738
req.context["get_thread_context"] = assistant.get_thread_context
3839
req.context["save_thread_context"] = assistant.save_thread_context
40+
41+
# TODO: in the future we might want to introduce a "proper" extract_ts utility
42+
thread_ts = req.context.thread_ts or event.get("ts")
43+
if req.context.channel_id and thread_ts:
44+
req.context["say_stream"] = AsyncSayStream(
45+
client=req.context.client,
46+
channel_id=req.context.channel_id,
47+
thread_ts=thread_ts,
48+
team_id=req.context.team_id,
49+
user_id=req.context.user_id,
50+
)
3951
return await next()

slack_bolt/middleware/attaching_agent_kwargs/attaching_agent_kwargs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from slack_bolt.context.assistant.assistant_utilities import AssistantUtilities
44
from slack_bolt.context.assistant.thread_context_store.store import AssistantThreadContextStore
5+
from slack_bolt.context.say_stream.say_stream import SayStream
56
from slack_bolt.middleware import Middleware
67
from slack_bolt.request.payload_utils import is_assistant_event, to_event
78
from slack_bolt.request.request import BoltRequest
@@ -30,4 +31,15 @@ def process(self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], Bo
3031
req.context["set_suggested_prompts"] = assistant.set_suggested_prompts
3132
req.context["get_thread_context"] = assistant.get_thread_context
3233
req.context["save_thread_context"] = assistant.save_thread_context
34+
35+
# TODO: in the future we might want to introduce a "proper" extract_ts utility
36+
thread_ts = req.context.thread_ts or event.get("ts")
37+
if req.context.channel_id and thread_ts:
38+
req.context["say_stream"] = SayStream(
39+
client=req.context.client,
40+
channel_id=req.context.channel_id,
41+
thread_ts=thread_ts,
42+
team_id=req.context.team_id,
43+
user_id=req.context.user_id,
44+
)
3345
return next()

0 commit comments

Comments
 (0)