Skip to content
Merged
Prev Previous commit
Next Next commit
prevent any possible breaking changes
  • Loading branch information
WilliamBergamin committed Mar 13, 2026
commit 807e53b9bd44536902381fa6a87028140f916656
8 changes: 8 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
IgnoringSelfEvents,
CustomMiddleware,
AttachingFunctionToken,
AttachingAgentKwargs,
)
from slack_bolt.middleware.assistant import Assistant
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(
listener_executor: Optional[Executor] = None,
# for AI Agents & Assistants
assistant_thread_context_store: Optional[AssistantThreadContextStore] = None,
attaching_agent_kwargs_enabled: bool = True,
):
"""Bolt App that provides functionalities to register middleware/listeners.

Expand Down Expand Up @@ -352,6 +354,7 @@ def message_hello(message, say):
listener_executor = ThreadPoolExecutor(max_workers=5)

self._assistant_thread_context_store = assistant_thread_context_store
self._attaching_agent_kwargs_enabled = attaching_agent_kwargs_enabled

self._process_before_response = process_before_response
self._listener_runner = ThreadListenerRunner(
Expand Down Expand Up @@ -836,10 +839,13 @@ def ask_for_introduction(event, say):
middleware: A list of lister middleware functions.
Only when all the middleware call `next()` method, the listener function can be invoked.
"""
middleware = list(middleware) if middleware else []

def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.event(event, base_logger=self._base_logger)
if self._attaching_agent_kwargs_enabled:
middleware.insert(0, AttachingAgentKwargs(self._assistant_thread_context_store))
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)

return __call__
Expand Down Expand Up @@ -897,6 +903,8 @@ def __call__(*args, **kwargs):
primary_matcher = builtin_matchers.message_event(
keyword=keyword, constraints=constraints, base_logger=self._base_logger
)
if self._attaching_agent_kwargs_enabled:
middleware.insert(0, AttachingAgentKwargs(self._assistant_thread_context_store))
middleware.insert(0, MessageListenerMatches(keyword))
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)

Expand Down
8 changes: 8 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
AsyncIgnoringSelfEvents,
AsyncUrlVerification,
AsyncAttachingFunctionToken,
AsyncAttachingAgentKwargs,
)
from slack_bolt.middleware.async_custom_middleware import (
AsyncMiddleware,
Expand Down Expand Up @@ -141,6 +142,7 @@ def __init__(
verification_token: Optional[str] = None,
# for AI Agents & Assistants
assistant_thread_context_store: Optional[AsyncAssistantThreadContextStore] = None,
attaching_agent_kwargs_enabled: bool = True,
):
"""Bolt App that provides functionalities to register middleware/listeners.

Expand Down Expand Up @@ -361,6 +363,7 @@ async def message_hello(message, say): # async function
self._async_listeners: List[AsyncListener] = []

self._assistant_thread_context_store = assistant_thread_context_store
self._attaching_agent_kwargs_enabled = attaching_agent_kwargs_enabled

self._process_before_response = process_before_response
self._async_listener_runner = AsyncioListenerRunner(
Expand Down Expand Up @@ -864,10 +867,13 @@ async def ask_for_introduction(event, say):
middleware: A list of lister middleware functions.
Only when all the middleware call `next()` method, the listener function can be invoked.
"""
middleware = list(middleware) if middleware else []

def __call__(*args, **kwargs):
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
primary_matcher = builtin_matchers.event(event, True, base_logger=self._base_logger)
if self._attaching_agent_kwargs_enabled:
middleware.insert(0, AsyncAttachingAgentKwargs(self._assistant_thread_context_store))
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)

return __call__
Expand Down Expand Up @@ -928,6 +934,8 @@ def __call__(*args, **kwargs):
asyncio=True,
base_logger=self._base_logger,
)
if self._attaching_agent_kwargs_enabled:
middleware.insert(0, AsyncAttachingAgentKwargs(self._assistant_thread_context_store))
middleware.insert(0, AsyncMessageListenerMatches(keyword))
return self._register_listener(list(functions), primary_matcher, matchers, middleware, True)

Expand Down
2 changes: 2 additions & 0 deletions slack_bolt/middleware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .ssl_check import SslCheck
from .url_verification import UrlVerification
from .attaching_function_token import AttachingFunctionToken
from .attaching_agent_kwargs import AttachingAgentKwargs

builtin_middleware_classes = [
SslCheck,
Expand All @@ -41,5 +42,6 @@
"SslCheck",
"UrlVerification",
"AttachingFunctionToken",
"AttachingAgentKwargs",
"builtin_middleware_classes",
]
4 changes: 2 additions & 2 deletions slack_bolt/middleware/assistant/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from slack_bolt.context.assistant.thread_context_store.store import AssistantThreadContextStore
from slack_bolt.listener_matcher.builtins import build_listener_matcher

from slack_bolt.middleware.assistant.attaching_assistant_kwargs import AttachingAssistantKwargs
from slack_bolt.middleware.attaching_agent_kwargs import AttachingAgentKwargs
from slack_bolt.request.request import BoltRequest
from slack_bolt.response.response import BoltResponse
from slack_bolt.listener_matcher import CustomListenerMatcher
Expand Down Expand Up @@ -272,7 +272,7 @@ def build_listener(
return listener_or_functions
elif isinstance(listener_or_functions, list):
middleware = middleware if middleware else []
middleware.insert(0, AttachingAssistantKwargs(self.thread_context_store))
middleware.insert(0, AttachingAgentKwargs(self.thread_context_store))
functions = listener_or_functions
ack_function = functions.pop(0)

Expand Down
4 changes: 2 additions & 2 deletions slack_bolt/middleware/assistant/async_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from slack_bolt.listener.asyncio_runner import AsyncioListenerRunner
from slack_bolt.listener_matcher.builtins import build_listener_matcher
from slack_bolt.middleware.assistant.async_attaching_assistant_kwargs import AsyncAttachingAssistantKwargs
from slack_bolt.middleware.attaching_agent_kwargs.async_attaching_agent_kwargs import AsyncAttachingAgentKwargs
from slack_bolt.request.async_request import AsyncBoltRequest
from slack_bolt.response import BoltResponse
from slack_bolt.error import BoltError
Expand Down Expand Up @@ -301,7 +301,7 @@ def build_listener(
return listener_or_functions
elif isinstance(listener_or_functions, list):
middleware = middleware if middleware else []
middleware.insert(0, AsyncAttachingAssistantKwargs(self.thread_context_store))
middleware.insert(0, AsyncAttachingAgentKwargs(self.thread_context_store))
functions = listener_or_functions
ack_function = functions.pop(0)

Expand Down
32 changes: 0 additions & 32 deletions slack_bolt/middleware/assistant/attaching_assistant_kwargs.py

This file was deleted.

2 changes: 2 additions & 0 deletions slack_bolt/middleware/async_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
AsyncMessageListenerMatches,
)
from .attaching_function_token.async_attaching_function_token import AsyncAttachingFunctionToken
from .attaching_agent_kwargs.async_attaching_agent_kwargs import AsyncAttachingAgentKwargs

__all__ = [
"AsyncIgnoringSelfEvents",
Expand All @@ -18,4 +19,5 @@
"AsyncUrlVerification",
"AsyncMessageListenerMatches",
"AsyncAttachingFunctionToken",
"AsyncAttachingAgentKwargs",
]
5 changes: 5 additions & 0 deletions slack_bolt/middleware/attaching_agent_kwargs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .attaching_agent_kwargs import AttachingAgentKwargs

__all__ = [
"AttachingAgentKwargs",
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from slack_bolt.context.assistant.thread_context_store.async_store import AsyncAssistantThreadContextStore
from slack_bolt.middleware.async_middleware import AsyncMiddleware
from slack_bolt.request.async_request import AsyncBoltRequest
from slack_bolt.request.payload_utils import to_event
from slack_bolt.request.payload_utils import is_assistant_event, to_event
from slack_bolt.response import BoltResponse


class AsyncAttachingAssistantKwargs(AsyncMiddleware):
class AsyncAttachingAgentKwargs(AsyncMiddleware):

thread_context_store: Optional[AsyncAssistantThreadContextStore]

def __init__(self, thread_context_store: Optional[AsyncAssistantThreadContextStore]):
def __init__(self, thread_context_store: Optional[AsyncAssistantThreadContextStore] = None):
self.thread_context_store = thread_context_store

async def async_process(
Expand All @@ -24,15 +24,16 @@ async def async_process(
) -> Optional[BoltResponse]:
event = to_event(req.body)
if event is not None:
assistant = AsyncAssistantUtilities(
payload=event,
context=req.context,
thread_context_store=self.thread_context_store,
)
req.context["say"] = assistant.say
req.context["set_status"] = assistant.set_status
req.context["set_title"] = assistant.set_title
req.context["set_suggested_prompts"] = assistant.set_suggested_prompts
req.context["get_thread_context"] = assistant.get_thread_context
req.context["save_thread_context"] = assistant.save_thread_context
if is_assistant_event(req.body):
assistant = AsyncAssistantUtilities(
payload=event,
context=req.context,
thread_context_store=self.thread_context_store,
)
req.context["say"] = assistant.say
req.context["set_status"] = assistant.set_status
req.context["set_title"] = assistant.set_title
req.context["set_suggested_prompts"] = assistant.set_suggested_prompts
req.context["get_thread_context"] = assistant.get_thread_context
req.context["save_thread_context"] = assistant.save_thread_context
return await next()
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional, Callable

from slack_bolt.context.assistant.assistant_utilities import AssistantUtilities
from slack_bolt.context.assistant.thread_context_store.store import AssistantThreadContextStore
from slack_bolt.middleware import Middleware
from slack_bolt.request.payload_utils import is_assistant_event, to_event
from slack_bolt.request.request import BoltRequest
from slack_bolt.response.response import BoltResponse


class AttachingAgentKwargs(Middleware):

thread_context_store: Optional[AssistantThreadContextStore]

def __init__(self, thread_context_store: Optional[AssistantThreadContextStore] = None):
self.thread_context_store = thread_context_store

def process(self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], BoltResponse]) -> Optional[BoltResponse]:
event = to_event(req.body)
if event is not None:
if is_assistant_event(req.body):
assistant = AssistantUtilities(
payload=event,
context=req.context,
thread_context_store=self.thread_context_store,
)
req.context["say"] = assistant.say
req.context["set_status"] = assistant.set_status
req.context["set_title"] = assistant.set_title
req.context["set_suggested_prompts"] = assistant.set_suggested_prompts
req.context["get_thread_context"] = assistant.get_thread_context
req.context["save_thread_context"] = assistant.save_thread_context
return next()
74 changes: 74 additions & 0 deletions tests/scenario_tests/test_events_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,80 @@ def handle_user_message(say: Say, set_status: SetStatus, context: BoltContext):
assert_target_called()
assert state["middleware_called"] is True

def test_assistant_events_without_assistant_middleware(self):
app = App(client=self.web_client)

state = {"called": False}

def assert_target_called():
count = 0
while state["called"] is False and count < 20:
sleep(0.1)
count += 1
assert state["called"] is True
state["called"] = False

@app.event("assistant_thread_started")
def start_thread(say: Say, set_suggested_prompts: SetSuggestedPrompts, set_status: SetStatus, context: BoltContext):
assert context.channel_id == "D111"
assert context.thread_ts == "1726133698.626339"
assert say.thread_ts == context.thread_ts
assert set_status is not None
assert set_suggested_prompts is not None
assert context.get("set_title") is not None
assert context.get("get_thread_context") is not None
assert context.get("save_thread_context") is not None
say("Hi, how can I help you today?")
state["called"] = True

@app.event("message")
def handle_message(say: Say, context: BoltContext, body: dict):
if context.get("set_status") is not None:
assert say.thread_ts == context.thread_ts
state["called"] = True

request = BoltRequest(body=thread_started_event_body, mode="socket_mode")
response = app.dispatch(request)
assert response.status == 200
assert_target_called()

request = BoltRequest(body=user_message_event_body, mode="socket_mode")
response = app.dispatch(request)
assert response.status == 200
assert_target_called()

# Non-assistant events should not get kwargs injected
request = BoltRequest(body=channel_user_message_event_body, mode="socket_mode")
response = app.dispatch(request)
assert response.status == 200

def test_assistant_events_kwargs_disabled(self):
app = App(client=self.web_client, attaching_agent_kwargs_enabled=False)

state = {"called": False}

def assert_target_called():
count = 0
while state["called"] is False and count < 20:
sleep(0.1)
count += 1
assert state["called"] is True
state["called"] = False

@app.event("assistant_thread_started")
def start_thread(context: BoltContext):
assert context.get("set_status") is None
assert context.get("set_title") is None
assert context.get("set_suggested_prompts") is None
assert context.get("get_thread_context") is None
assert context.get("save_thread_context") is None
state["called"] = True

request = BoltRequest(body=thread_started_event_body, mode="socket_mode")
response = app.dispatch(request)
assert response.status == 200
assert_target_called()

def test_assistant_threads_custom_middleware_can_short_circuit(self):
app = App(client=self.web_client)
assistant = Assistant()
Expand Down
Loading
Loading