Skip to content

Commit d789fac

Browse files
zimegsrtaalej
andauthored
feat(agent): default to message 'ts' when no 'thread_ts' is avaialble for 'agent.chat_stream(...)' (#1444)
Co-authored-by: Ale Mercado <maria.mercado@slack-corp.com>
1 parent 92bff60 commit d789fac

File tree

7 files changed

+113
-11
lines changed

7 files changed

+113
-11
lines changed

slack_bolt/agent/agent.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ class BoltAgent:
1111
Experimental:
1212
This API is experimental and may change in future releases.
1313
14-
FIXME: chat_stream() only works when thread_ts is available (DMs and threaded replies).
15-
It does not work on channel messages because ts is not provided to BoltAgent yet.
16-
1714
@app.event("app_mention")
1815
def handle_mention(agent):
1916
stream = agent.chat_stream()
@@ -27,12 +24,14 @@ def __init__(
2724
client: WebClient,
2825
channel_id: Optional[str] = None,
2926
thread_ts: Optional[str] = None,
27+
ts: Optional[str] = None,
3028
team_id: Optional[str] = None,
3129
user_id: Optional[str] = None,
3230
):
3331
self._client = client
3432
self._channel_id = channel_id
3533
self._thread_ts = thread_ts
34+
self._ts = ts
3635
self._team_id = team_id
3736
self._user_id = user_id
3837

@@ -67,7 +66,7 @@ def chat_stream(
6766
# Argument validation is delegated to chat_stream() and the API
6867
return self._client.chat_stream(
6968
channel=channel or self._channel_id, # type: ignore[arg-type]
70-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
69+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
7170
recipient_team_id=recipient_team_id or self._team_id,
7271
recipient_user_id=recipient_user_id or self._user_id,
7372
**kwargs,
@@ -96,7 +95,7 @@ def set_status(
9695
"""
9796
return self._client.assistant_threads_setStatus(
9897
channel_id=channel or self._channel_id, # type: ignore[arg-type]
99-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
98+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
10099
status=status,
101100
loading_messages=loading_messages,
102101
**kwargs,
@@ -133,7 +132,7 @@ def set_suggested_prompts(
133132

134133
return self._client.assistant_threads_setSuggestedPrompts(
135134
channel_id=channel or self._channel_id, # type: ignore[arg-type]
136-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
135+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
137136
prompts=prompts_arg,
138137
title=title,
139138
**kwargs,

slack_bolt/agent/async_agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ def __init__(
2323
client: AsyncWebClient,
2424
channel_id: Optional[str] = None,
2525
thread_ts: Optional[str] = None,
26+
ts: Optional[str] = None,
2627
team_id: Optional[str] = None,
2728
user_id: Optional[str] = None,
2829
):
2930
self._client = client
3031
self._channel_id = channel_id
3132
self._thread_ts = thread_ts
33+
self._ts = ts
3234
self._team_id = team_id
3335
self._user_id = user_id
3436

@@ -63,7 +65,7 @@ async def chat_stream(
6365
# Argument validation is delegated to chat_stream() and the API
6466
return await self._client.chat_stream(
6567
channel=channel or self._channel_id, # type: ignore[arg-type]
66-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
68+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
6769
recipient_team_id=recipient_team_id or self._team_id,
6870
recipient_user_id=recipient_user_id or self._user_id,
6971
**kwargs,
@@ -92,7 +94,7 @@ async def set_status(
9294
"""
9395
return await self._client.assistant_threads_setStatus(
9496
channel_id=channel or self._channel_id, # type: ignore[arg-type]
95-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
97+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
9698
status=status,
9799
loading_messages=loading_messages,
98100
**kwargs,
@@ -129,7 +131,7 @@ async def set_suggested_prompts(
129131

130132
return await self._client.assistant_threads_setSuggestedPrompts(
131133
channel_id=channel or self._channel_id, # type: ignore[arg-type]
132-
thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type]
134+
thread_ts=thread_ts or self._thread_ts or self._ts, # type: ignore[arg-type]
133135
prompts=prompts_arg,
134136
title=title,
135137
**kwargs,

slack_bolt/kwargs_injection/async_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,13 @@ def build_async_required_kwargs(
8989
if "agent" in required_arg_names:
9090
from slack_bolt.agent.async_agent import AsyncBoltAgent
9191

92+
event = request.body.get("event", {})
93+
9294
all_available_args["agent"] = AsyncBoltAgent(
9395
client=request.context.client,
9496
channel_id=request.context.channel_id,
95-
thread_ts=request.context.thread_ts,
97+
thread_ts=request.context.thread_ts or event.get("thread_ts"),
98+
ts=event.get("ts"),
9699
team_id=request.context.team_id,
97100
user_id=request.context.user_id,
98101
)

slack_bolt/kwargs_injection/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,13 @@ def build_required_kwargs(
8888
if "agent" in required_arg_names:
8989
from slack_bolt.agent.agent import BoltAgent
9090

91+
event = request.body.get("event", {})
92+
9193
all_available_args["agent"] = BoltAgent(
9294
client=request.context.client,
9395
channel_id=request.context.channel_id,
94-
thread_ts=request.context.thread_ts,
96+
thread_ts=request.context.thread_ts or event.get("thread_ts"),
97+
ts=event.get("ts"),
9598
team_id=request.context.team_id,
9699
user_id=request.context.user_id,
97100
)

slack_bolt/request/internals.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def extract_thread_ts(payload: Dict[str, Any]) -> Optional[str]:
218218
# This utility initially supports only the use cases for AI assistants, but it may be fine to add more patterns.
219219
# That said, note that thread_ts is always required for assistant threads, but it's not for channels.
220220
# Thus, blindly setting this thread_ts to say utility can break existing apps' behaviors.
221+
#
222+
# The BoltAgent class handles non-assistant thread_ts separately by reading from the event directly,
223+
# allowing it to work correctly without affecting say() behavior.
221224
if is_assistant_event(payload):
222225
event = payload["event"]
223226
if (

tests/slack_bolt/agent/test_agent.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,51 @@ def test_chat_stream_passes_extra_kwargs(self):
9292
buffer_size=512,
9393
)
9494

95+
def test_chat_stream_falls_back_to_ts(self):
96+
"""When thread_ts is not set, chat_stream() falls back to ts."""
97+
client = MagicMock(spec=WebClient)
98+
client.chat_stream.return_value = MagicMock(spec=ChatStream)
99+
100+
agent = BoltAgent(
101+
client=client,
102+
channel_id="C111",
103+
team_id="T111",
104+
ts="1111111111.111111",
105+
user_id="W222",
106+
)
107+
stream = agent.chat_stream()
108+
109+
client.chat_stream.assert_called_once_with(
110+
channel="C111",
111+
thread_ts="1111111111.111111",
112+
recipient_team_id="T111",
113+
recipient_user_id="W222",
114+
)
115+
assert stream is not None
116+
117+
def test_chat_stream_prefers_thread_ts_over_ts(self):
118+
"""thread_ts takes priority over ts."""
119+
client = MagicMock(spec=WebClient)
120+
client.chat_stream.return_value = MagicMock(spec=ChatStream)
121+
122+
agent = BoltAgent(
123+
client=client,
124+
channel_id="C111",
125+
team_id="T111",
126+
thread_ts="1234567890.123456",
127+
ts="1111111111.111111",
128+
user_id="W222",
129+
)
130+
stream = agent.chat_stream()
131+
132+
client.chat_stream.assert_called_once_with(
133+
channel="C111",
134+
thread_ts="1234567890.123456",
135+
recipient_team_id="T111",
136+
recipient_user_id="W222",
137+
)
138+
assert stream is not None
139+
95140
def test_set_status_uses_context_defaults(self):
96141
"""BoltAgent.set_status() passes context defaults to WebClient.assistant_threads_setStatus()."""
97142
client = MagicMock(spec=WebClient)

tests/slack_bolt_async/agent/test_async_agent.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,53 @@ async def test_chat_stream_passes_extra_kwargs(self):
118118
buffer_size=512,
119119
)
120120

121+
@pytest.mark.asyncio
122+
async def test_chat_stream_falls_back_to_ts(self):
123+
"""When thread_ts is not set, chat_stream() falls back to ts."""
124+
client = MagicMock(spec=AsyncWebClient)
125+
client.chat_stream, call_tracker, _ = _make_async_chat_stream_mock()
126+
127+
agent = AsyncBoltAgent(
128+
client=client,
129+
channel_id="C111",
130+
team_id="T111",
131+
ts="1111111111.111111",
132+
user_id="W222",
133+
)
134+
stream = await agent.chat_stream()
135+
136+
call_tracker.assert_called_once_with(
137+
channel="C111",
138+
thread_ts="1111111111.111111",
139+
recipient_team_id="T111",
140+
recipient_user_id="W222",
141+
)
142+
assert stream is not None
143+
144+
@pytest.mark.asyncio
145+
async def test_chat_stream_prefers_thread_ts_over_ts(self):
146+
"""thread_ts takes priority over ts."""
147+
client = MagicMock(spec=AsyncWebClient)
148+
client.chat_stream, call_tracker, _ = _make_async_chat_stream_mock()
149+
150+
agent = AsyncBoltAgent(
151+
client=client,
152+
channel_id="C111",
153+
team_id="T111",
154+
thread_ts="1234567890.123456",
155+
ts="1111111111.111111",
156+
user_id="W222",
157+
)
158+
stream = await agent.chat_stream()
159+
160+
call_tracker.assert_called_once_with(
161+
channel="C111",
162+
thread_ts="1234567890.123456",
163+
recipient_team_id="T111",
164+
recipient_user_id="W222",
165+
)
166+
assert stream is not None
167+
121168
@pytest.mark.asyncio
122169
async def test_set_status_uses_context_defaults(self):
123170
"""AsyncBoltAgent.set_status() passes context defaults to AsyncWebClient.assistant_threads_setStatus()."""

0 commit comments

Comments
 (0)