Skip to content

Commit 19fced4

Browse files
WilliamBergaminClaude
andcommitted
refactor: scope set_title to assistant threads via AssistantUtilities
Move set_title construction into AssistantUtilities as a property and attach it from the assistant-event branch of AttachingConversationKwargs (sync and async). Previously set_title was attached to any IM message event that had a thread_ts; it is now assistant-thread-only, matching say/get_thread_context/save_thread_context. set_suggested_prompts remains available for any DM to the app. AssistantUtilities now derives channel_id/thread_ts directly from the payload: from the assistant_thread property for assistant_* events (via the new has_channel_id_and_thread_ts helper in internals.py) and from channel/thread_ts for message events, instead of reading them off the BoltContext. Also simplify the assistant-thread message predicates in payload_utils, since is_im_message_event / is_any_im_message_event already imply is_event. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
1 parent 8da62af commit 19fced4

8 files changed

Lines changed: 119 additions & 28 deletions

File tree

slack_bolt/context/assistant/assistant_utilities.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
from slack_bolt.context.context import BoltContext
99
from slack_bolt.context.say import Say
10+
from .internals import has_channel_id_and_thread_ts
1011
from ..get_thread_context.get_thread_context import GetThreadContext
1112
from ..save_thread_context import SaveThreadContext
13+
from ..set_title import SetTitle
1214

1315

1416
class AssistantUtilities:
@@ -29,9 +31,15 @@ def __init__(
2931
self.client = context.client
3032
self.thread_context_store = thread_context_store or DefaultAssistantThreadContextStore(context)
3133

32-
if context.channel_id is not None and context.thread_ts is not None:
33-
self.channel_id = context.channel_id
34-
self.thread_ts = context.thread_ts
34+
if has_channel_id_and_thread_ts(self.payload):
35+
# assistant_thread_started
36+
thread = self.payload["assistant_thread"]
37+
self.channel_id = thread["channel_id"]
38+
self.thread_ts = thread["thread_ts"]
39+
elif self.payload.get("channel") is not None and self.payload.get("thread_ts") is not None:
40+
# message event
41+
self.channel_id = self.payload["channel"]
42+
self.thread_ts = self.payload["thread_ts"]
3543
else:
3644
# When moving this code to Bolt internals, no need to raise an exception for this pattern
3745
raise ValueError(f"Cannot instantiate Assistant for this event pattern ({self.payload})")
@@ -51,6 +59,10 @@ def build_metadata() -> Optional[dict]:
5159
build_metadata=build_metadata,
5260
)
5361

62+
@property
63+
def set_title(self) -> SetTitle:
64+
return SetTitle(self.client, self.channel_id, self.thread_ts)
65+
5466
@property
5567
def get_thread_context(self) -> GetThreadContext:
5668
return GetThreadContext(self.thread_context_store, self.channel_id, self.thread_ts, self.payload)

slack_bolt/context/assistant/async_assistant_utilities.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
from slack_bolt.context.async_context import AsyncBoltContext
1212
from slack_bolt.context.say.async_say import AsyncSay
13+
from .internals import has_channel_id_and_thread_ts
1314
from ..get_thread_context.async_get_thread_context import AsyncGetThreadContext
1415
from ..save_thread_context.async_save_thread_context import AsyncSaveThreadContext
16+
from ..set_title.async_set_title import AsyncSetTitle
1517

1618

1719
class AsyncAssistantUtilities:
@@ -32,9 +34,15 @@ def __init__(
3234
self.client = context.client
3335
self.thread_context_store = thread_context_store or DefaultAsyncAssistantThreadContextStore(context)
3436

35-
if context.channel_id is not None and context.thread_ts is not None:
36-
self.channel_id = context.channel_id
37-
self.thread_ts = context.thread_ts
37+
if has_channel_id_and_thread_ts(self.payload):
38+
# assistant_thread_started
39+
thread = self.payload["assistant_thread"]
40+
self.channel_id = thread["channel_id"]
41+
self.thread_ts = thread["thread_ts"]
42+
elif self.payload.get("channel") is not None and self.payload.get("thread_ts") is not None:
43+
# message event
44+
self.channel_id = self.payload["channel"]
45+
self.thread_ts = self.payload["thread_ts"]
3846
else:
3947
# When moving this code to Bolt internals, no need to raise an exception for this pattern
4048
raise ValueError(f"Cannot instantiate Assistant for this event pattern ({self.payload})")
@@ -48,6 +56,10 @@ def say(self) -> AsyncSay:
4856
build_metadata=self._build_message_metadata,
4957
)
5058

59+
@property
60+
def set_title(self) -> AsyncSetTitle:
61+
return AsyncSetTitle(self.client, self.channel_id, self.thread_ts)
62+
5163
async def _build_message_metadata(self) -> dict:
5264
return {
5365
"event_type": "assistant_thread_context",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def has_channel_id_and_thread_ts(payload: dict) -> bool:
2+
"""Verifies if the given payload has both channel_id and thread_ts under assistant_thread property.
3+
This data pattern is available for assistant_* events.
4+
"""
5+
return (
6+
payload.get("assistant_thread") is not None
7+
and payload["assistant_thread"].get("channel_id") is not None
8+
and payload["assistant_thread"].get("thread_ts") is not None
9+
)

slack_bolt/middleware/attaching_conversation_kwargs/async_attaching_conversation_kwargs.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream
66
from slack_bolt.context.set_status.async_set_status import AsyncSetStatus
77
from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import AsyncSetSuggestedPrompts
8-
from slack_bolt.context.set_title.async_set_title import AsyncSetTitle
98
from slack_bolt.middleware.async_middleware import AsyncMiddleware
109
from slack_bolt.request.async_request import AsyncBoltRequest
1110
from slack_bolt.request.payload_utils import (
1211
is_assistant_event,
1312
is_assistant_thread_context_changed_event,
1413
is_assistant_thread_started_event,
15-
to_event,
1614
is_im_message_event,
15+
to_event,
1716
)
1817
from slack_bolt.response import BoltResponse
1918

@@ -46,6 +45,7 @@ async def async_process(
4645
thread_context_store=self.thread_context_store,
4746
)
4847
req.context["say"] = assistant.say
48+
req.context["set_title"] = assistant.set_title
4949
req.context["get_thread_context"] = assistant.get_thread_context
5050
req.context["save_thread_context"] = assistant.save_thread_context
5151

@@ -59,8 +59,6 @@ async def async_process(
5959
channel_id=req.context.channel_id,
6060
thread_ts=req.context.thread_ts,
6161
)
62-
if req.context.thread_ts:
63-
req.context["set_title"] = AsyncSetTitle(req.context.client, req.context.channel_id, req.context.thread_ts)
6462

6563
# TODO: in the future we might want to introduce a "proper" extract_ts utility
6664
thread_ts_or_ts = req.context.thread_ts or event.get("ts")

slack_bolt/middleware/attaching_conversation_kwargs/attaching_conversation_kwargs.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from slack_bolt.context.say_stream.say_stream import SayStream
55
from slack_bolt.context.set_status.set_status import SetStatus
66
from slack_bolt.context.set_suggested_prompts.set_suggested_prompts import SetSuggestedPrompts
7-
from slack_bolt.context.set_title import SetTitle
87
from slack_bolt.middleware import Middleware
98
from slack_bolt.context.assistant.assistant_utilities import AssistantUtilities
109
from slack_bolt.request.payload_utils import (
@@ -40,6 +39,7 @@ def process(self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], Bo
4039
thread_context_store=self.thread_context_store,
4140
)
4241
req.context["say"] = assistant.say
42+
req.context["set_title"] = assistant.set_title
4343
req.context["get_thread_context"] = assistant.get_thread_context
4444
req.context["save_thread_context"] = assistant.save_thread_context
4545

@@ -53,8 +53,6 @@ def process(self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], Bo
5353
channel_id=req.context.channel_id,
5454
thread_ts=req.context.thread_ts,
5555
)
56-
if req.context.thread_ts:
57-
req.context["set_title"] = SetTitle(req.context.client, req.context.channel_id, req.context.thread_ts)
5856

5957
# TODO: in the future we might want to introduce a "proper" extract_ts utility
6058
thread_ts_or_ts = req.context.thread_ts or event.get("ts")

slack_bolt/request/payload_utils.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,15 @@ def is_assistant_thread_context_changed_event(body: Dict[str, Any]) -> bool:
7272

7373

7474
def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
75-
if is_event(body):
76-
return (
77-
is_im_message_event(body) and body["event"].get("thread_ts") is not None and body["event"].get("bot_id") is None
78-
)
75+
if is_im_message_event(body):
76+
return body["event"].get("thread_ts") is not None and body["event"].get("bot_id") is None
7977
return False
8078

8179

8280
def is_bot_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
83-
if is_event(body):
81+
if is_any_im_message_event(body):
8482
return (
85-
is_any_im_message_event(body)
86-
and body["event"].get("subtype") is None
83+
body["event"].get("subtype") is None
8784
and body["event"].get("thread_ts") is not None
8885
and body["event"].get("bot_id") is not None
8986
)
@@ -92,14 +89,10 @@ def is_bot_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
9289

9390
def is_other_message_sub_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
9491
# message_changed, message_deleted etc.
95-
if is_event(body):
96-
return (
97-
is_any_im_message_event(body)
98-
and not is_user_message_event_in_assistant_thread(body)
99-
and (
100-
_is_other_message_sub_event(body["event"].get("message"))
101-
or _is_other_message_sub_event(body["event"].get("previous_message"))
102-
)
92+
if is_any_im_message_event(body):
93+
return not is_user_message_event_in_assistant_thread(body) and (
94+
_is_other_message_sub_event(body["event"].get("message"))
95+
or _is_other_message_sub_event(body["event"].get("previous_message"))
10396
)
10497
return False
10598

tests/slack_bolt/middleware/attaching_conversation_kwargs/test_attaching_conversation_kwargs.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from slack_bolt.request import BoltRequest
55
from slack_bolt.response import BoltResponse
66
from tests.scenario_tests.test_events_assistant import (
7+
build_payload,
78
thread_started_event_body,
89
user_message_event_body,
910
channel_user_message_event_body,
@@ -16,6 +17,19 @@ def next():
1617

1718
ASSISTANT_KWARGS = ("say", "set_title", "set_suggested_prompts", "get_thread_context", "save_thread_context")
1819

20+
# A top-level DM (not in a thread) is not an assistant thread, but set_suggested_prompts is still attached.
21+
top_level_im_message_event_body = build_payload(
22+
{
23+
"user": "W222",
24+
"type": "message",
25+
"ts": "1726133700.887259",
26+
"text": "A top-level DM, not in a thread",
27+
"channel": "D111",
28+
"event_ts": "1726133700.887259",
29+
"channel_type": "im",
30+
}
31+
)
32+
1933

2034
class TestAttachingConversationKwargs:
2135
def test_assistant_event_attaches_kwargs(self):
@@ -46,6 +60,26 @@ def test_user_message_event_attaches_kwargs(self):
4660
assert "say_stream" in req.context
4761
assert "set_status" in req.context
4862

63+
def test_top_level_dm_attaches_suggested_prompts_but_not_set_title(self):
64+
middleware = AttachingConversationKwargs()
65+
req = BoltRequest(body=top_level_im_message_event_body, mode="socket_mode")
66+
req.context["client"] = WebClient(token="xoxb-test")
67+
68+
resp = middleware.process(req=req, resp=BoltResponse(status=404), next=next)
69+
70+
assert resp.status == 200
71+
# set_suggested_prompts is available for any DM to the app
72+
assert "set_suggested_prompts" in req.context
73+
# set_title is assistant-thread-only; a top-level DM is not an assistant thread
74+
assert "set_title" not in req.context
75+
# say/get_thread_context/save_thread_context remain assistant-only
76+
assert "say" not in req.context
77+
assert "get_thread_context" not in req.context
78+
assert "save_thread_context" not in req.context
79+
# set_status / say_stream are attached whenever a ts is resolvable
80+
assert "say_stream" in req.context
81+
assert "set_status" in req.context
82+
4983
def test_non_assistant_event_does_not_attach_kwargs(self):
5084
middleware = AttachingConversationKwargs()
5185
req = BoltRequest(body=channel_user_message_event_body, mode="socket_mode")

tests/slack_bolt_async/middleware/attaching_conversation_kwargs/test_async_attaching_conversation_kwargs.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from slack_bolt.request.async_request import AsyncBoltRequest
88
from slack_bolt.response import BoltResponse
99
from tests.scenario_tests_async.test_events_assistant import (
10+
build_payload,
1011
thread_started_event_body,
1112
user_message_event_body,
1213
channel_user_message_event_body,
@@ -19,6 +20,19 @@ async def next():
1920

2021
ASSISTANT_KWARGS = ("say", "set_title", "set_suggested_prompts", "get_thread_context", "save_thread_context")
2122

23+
# A top-level DM (not in a thread) is not an assistant thread, but set_suggested_prompts is still attached.
24+
top_level_im_message_event_body = build_payload(
25+
{
26+
"user": "W222",
27+
"type": "message",
28+
"ts": "1726133700.887259",
29+
"text": "A top-level DM, not in a thread",
30+
"channel": "D111",
31+
"event_ts": "1726133700.887259",
32+
"channel_type": "im",
33+
}
34+
)
35+
2236

2337
class TestAsyncAttachingConversationKwargs:
2438
@pytest.mark.asyncio
@@ -51,6 +65,27 @@ async def test_user_message_event_attaches_kwargs(self):
5165
assert "say_stream" in req.context
5266
assert "set_status" in req.context
5367

68+
@pytest.mark.asyncio
69+
async def test_top_level_dm_attaches_suggested_prompts_but_not_set_title(self):
70+
middleware = AsyncAttachingConversationKwargs()
71+
req = AsyncBoltRequest(body=top_level_im_message_event_body, mode="socket_mode")
72+
req.context["client"] = AsyncWebClient(token="xoxb-test")
73+
74+
resp = await middleware.async_process(req=req, resp=BoltResponse(status=404), next=next)
75+
76+
assert resp.status == 200
77+
# set_suggested_prompts is available for any DM to the app
78+
assert "set_suggested_prompts" in req.context
79+
# set_title is assistant-thread-only; a top-level DM is not an assistant thread
80+
assert "set_title" not in req.context
81+
# say/get_thread_context/save_thread_context remain assistant-only
82+
assert "say" not in req.context
83+
assert "get_thread_context" not in req.context
84+
assert "save_thread_context" not in req.context
85+
# set_status / say_stream are attached whenever a ts is resolvable
86+
assert "say_stream" in req.context
87+
assert "set_status" in req.context
88+
5489
@pytest.mark.asyncio
5590
async def test_non_assistant_event_does_not_attach_kwargs(self):
5691
middleware = AsyncAttachingConversationKwargs()

0 commit comments

Comments
 (0)