Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1437 +/- ##
==========================================
+ Coverage 90.54% 90.61% +0.07%
==========================================
Files 222 226 +4
Lines 7129 7206 +77
==========================================
+ Hits 6455 6530 +75
- Misses 674 676 +2 ☔ View full report in Codecov by Sentry. |
…ependency AsyncBoltAgent imports AsyncWebClient which requires aiohttp. Eagerly importing it from the agent package __init__ breaks environments where aiohttp is not installed, since slack_bolt/__init__.py imports BoltAgent from this package. Follows the existing convention of not adding async module imports at the top level.
…7 compat Replace AsyncMock usage with coroutine-returning MagicMock wrappers, matching the pattern used in the sync test suite. This avoids the Python 3.8+ AsyncMock and the need for the mock backport package.
mwbrooks
left a comment
There was a problem hiding this comment.
Comments for the kind and knowledgeable reviewers!
| Experimental: | ||
| This API is experimental and may change in future releases. |
There was a problem hiding this comment.
note: We're using an "Experimental" warning while we developer this feature. Rather than working on a long-standing branch, we'd like to merge into main under a semver:patch then release a semver:minor when the experimental status is removed.
| FIXME: chat_stream() only works when thread_ts is available (DMs and threaded replies). | ||
| It does not work on channel messages because ts is not provided to BoltAgent yet. |
There was a problem hiding this comment.
note: Important callout. I'd like to add ts support in a follow-up PR so that we can discuss the best approach.
| next_keys_required: bool = True, # False for listeners / middleware / error handlers | ||
| ) -> Dict[str, Any]: | ||
| all_available_args = { | ||
| all_available_args: Dict[str, Any] = { |
There was a problem hiding this comment.
note: This fixed a linter warning
| if name == "args": | ||
| if isinstance(request, AsyncBoltRequest): | ||
| kwargs[name] = AsyncArgs(**all_available_args) # type: ignore[arg-type] | ||
| kwargs[name] = AsyncArgs(**all_available_args) |
There was a problem hiding this comment.
note: The above fix allows us to remove this type ignore
WilliamBergamin
left a comment
There was a problem hiding this comment.
Awesome work 💯 this is already starting to take shape 🚀 left a few comments
One more thing around testing, what do you think about adding some unit tests in tests/slack_bolt(_async)/kwargs_injection/test_args.py that ensure build_required_kwargs only creates a BoltAgent when the agent keyword argument is present in required_arg_names
| if "agent" in required_arg_names or "args" in required_arg_names: | ||
| all_available_args["agent"] = request.context.agent |
There was a problem hiding this comment.
Clever 💯 I like this and wonder if we should follow this pattern for other keyword arguments 🚀
What do you think about including a warning here that informs developers that the agent is an experimental feature subject to change?
From what I can tell we should be able to do this by taking advantage of the FutureWarning like this
import warnings
class ExperimentalWarning(FutureWarning):
"""Warning for features that are still in experimental phase."""
pass
warnings.warn(
"agent is experimental and may change in future versions.",
category=ExperimentalWarning,
stacklevel=2
)IIUC every time a handler processes a request and uses the "agent" kwargs, this warning would be printed, this might be a bit annoying but it would be clear
There was a problem hiding this comment.
Great idea, @WilliamBergamin! I didn't know about warnings but it's a very nice way to let developers know that they're accessing an experimental feature.
Commit cf5ef98 adds the above suggestion and implements it. I added you as a co-author because I took your code verbatim. 👌🏻
IIUC every time a handler processes a request and uses the "agent" kwargs, this warning would be printed, this might be a bit annoying but it would be clear
It looks like there is a Warning Filter and the default is to only print the first occurrence. Personally, I'd be okay with printing on every occurrence since it's an experimental feature, but my implementation is using the default.
There was a problem hiding this comment.
I like this and wonder if we should follow this pattern for other keyword arguments
🙂 btw, I pulled this idea from the complete/fail handlers that are also constructed upon request. It's a cool feature of Python!
| from .response import BoltResponse | ||
|
|
||
| # AI Agents & Assistants | ||
| from .agent import BoltAgent |
There was a problem hiding this comment.
🤔 Maybe we should wait until the feature is GA before exporting it here?
Developers should still be able to import the class directly with something like
from slack_bolt.agent import BoltAgent
There was a problem hiding this comment.
🤔 If possible, I'd prefer to export the BoltAgent now.
- Our work will be driven through developing a sample app, it would allow us to immediately understand what the production code will looks & feels like. For example, here is what the import would look like today..
- Our new
ExperimentalWarningmakes it clear that the developer is using something that's unstable and at their own risk. - When it comes to remove the Experimental status, I'd prefer to not have to add additional code such as a public export. I'd feel more confident about removing the Experimental status if we had tested everything throughout the entire process.
👉🏻 That said, I trust you decision over ours since you're more experienced with Bolt Python. If it makes you feel more comfortable waiting until the feature is GA, just let me know.
| if TYPE_CHECKING: | ||
| from slack_bolt.agent.async_agent import AsyncBoltAgent | ||
|
|
There was a problem hiding this comment.
TIL: TYPE_CHECKING, it seems awesome and maybe we can use it elsewhere to improve our types checking for async stuff
IIUC this is to ensure that we
- Do not import
AsyncBoltAgentwheneverAsyncBoltContextis imported - Static type checking around
AsyncBoltAgentpasses AsyncBoltAgentis only imported whencontext.agentis invoked
There was a problem hiding this comment.
Static type checking around AsyncBoltAgent passes
This was my main motivation to add it. At first, I felt like it was the wrong approach, but it seems be correct and necessary with the Async modules.
| resolved_channel = channel or self._channel_id | ||
| resolved_thread_ts = thread_ts or self._thread_ts |
There was a problem hiding this comment.
I'm wondering here if falling back to the class instances channel_id, thread_ts, team_id and user_id is the best behavior 🤔
Like if a developer only passes one of these parameters and are unaware of the the fallback values they could end up chat streaming to the wrong location?
I'm not super familiar with chat_stream so this might be a non issue
There was a problem hiding this comment.
💭 i agree with @WilliamBergamin! channel_id and thread_ts seem to work as a pair so if a user changes one they should be aware they need to change the other. Maybe we throw a warning when one of these params is changed but not the other 🤔 ?
There was a problem hiding this comment.
Thanks @WilliamBergamin and @srtaalej for your thorough review! I really appreciate that you're actually taking the time to think through the logic, rather than glance over the source code. 🙇🏻
I agree, a mismatch of arguments could lead to bugs that are difficult to diagnose. An all-or-nothing approach would be more fitting, where the developer either relies on the default values set by the context or provides all of the arguments. If a partial set of arguments are provided, then we can raise an error like @srtaalej suggested.
Commit 724ea5f disallows partial overrides of the arguments and I've added some tests around it. 🚀
There was a problem hiding this comment.
🐣 ramble: I'm following this discussion curious! I agree the confusion of unknown defaults isn't good, but find extra effort in gathering the details I'm not changing...
At the moment the FIXME hints at the edge I'm finding where ts is unknown for app mentions in this class. We might find enhancement for this, but I'm wondering if overriding all defaults to stream to multiple different threads in the same channel for the same person at the same time - perhaps for subagent workflows - might be difficult.
Not a blocker at all for me but I want to share thought!
| def test_agent_chat_stream_overrides_context_defaults(self): | ||
| """Explicit kwargs to chat_stream() override context defaults.""" | ||
| client = MagicMock(spec=WebClient) | ||
| client.chat_stream.return_value = MagicMock(spec=ChatStream) | ||
|
|
||
| agent = BoltAgentDirect( | ||
| client=client, | ||
| channel_id="C111", | ||
| thread_ts="1234567890.123456", | ||
| team_id="T111", | ||
| user_id="W222", | ||
| ) | ||
| stream = agent.chat_stream( | ||
| channel="C999", | ||
| thread_ts="9999999999.999999", | ||
| recipient_team_id="T999", | ||
| recipient_user_id="U999", | ||
| ) | ||
|
|
||
| client.chat_stream.assert_called_once_with( | ||
| channel="C999", | ||
| thread_ts="9999999999.999999", | ||
| recipient_team_id="T999", | ||
| recipient_user_id="U999", | ||
| ) | ||
| assert stream is not None |
There was a problem hiding this comment.
What do you think about creating a tests.slack_bolt.agent and tests.slack_bolt_async.agent directory where unit tests dedicated to the BoltAgent like these ones can live?
There was a problem hiding this comment.
agreed 🙌 it looks like we're mixing unit tests with full app scenarios 🤔 might be nice to follow up on this in another pr ⭐
There was a problem hiding this comment.
Excellent call @WilliamBergamin and @srtaalej! 🙇🏻 I should have caught this one before sharing the PR.
In commit 0492aa0, I've moved the agent unit tests to tests/slack_bolt/agent/ and tests/slack_bolt_async/agent/.
I kept the integration tests for agent in the current file, which test that the agent is injected into the context property, available to the listeners, and print an experimental warning when accessed.
If you'd like those moved under the unit test directory, I can do that. But it seems like they belong in the integration tests directory.
srtaalej
left a comment
There was a problem hiding this comment.
left some comments but its LGTM! thank you for bringing these changes in so quickly 🚀 ⭐ ⭐ ⭐
| def test_agent_chat_stream_overrides_context_defaults(self): | ||
| """Explicit kwargs to chat_stream() override context defaults.""" | ||
| client = MagicMock(spec=WebClient) | ||
| client.chat_stream.return_value = MagicMock(spec=ChatStream) | ||
|
|
||
| agent = BoltAgentDirect( | ||
| client=client, | ||
| channel_id="C111", | ||
| thread_ts="1234567890.123456", | ||
| team_id="T111", | ||
| user_id="W222", | ||
| ) | ||
| stream = agent.chat_stream( | ||
| channel="C999", | ||
| thread_ts="9999999999.999999", | ||
| recipient_team_id="T999", | ||
| recipient_user_id="U999", | ||
| ) | ||
|
|
||
| client.chat_stream.assert_called_once_with( | ||
| channel="C999", | ||
| thread_ts="9999999999.999999", | ||
| recipient_team_id="T999", | ||
| recipient_user_id="U999", | ||
| ) | ||
| assert stream is not None |
There was a problem hiding this comment.
agreed 🙌 it looks like we're mixing unit tests with full app scenarios 🤔 might be nice to follow up on this in another pr ⭐
Adds a custom ExperimentalWarning (subclass of FutureWarning) that is
emitted when a listener explicitly requests the `agent` argument,
informing developers that this feature is experimental and subject to
change.
Co-Authored-By: William Bergamin <wbergamin@salesforce.com>
Split agent tests so unit tests live in tests/slack_bolt/agent/ and tests/slack_bolt_async/agent/, matching the existing convention where test directories mirror the source layout. Integration tests that dispatch through App remain in scenario_tests/.
|
Thanks a ton for the amazing feedback @WilliamBergamin @srtaalej! ❤️ Truly, I appreciate that you've taken the time to think deeply about the changes - especially since I'm still quite new to the Bolt Python codebase. I've followed up on the above feedback, so please take a second look when you have a spare moment. 🙇🏻 |
zimeg
left a comment
There was a problem hiding this comment.
@mwbrooks Such an impressive PR! I echo the excitement for these changes landing as experimental so fast 👾 ✨
A few comments I left are non-blocking and ask questions on error handling and context setups. I'm hoping I left enough praise too since these are great changes all around.
Whenever's right we should merge this for iterations next 🚢 💨
| resolved_channel = channel or self._channel_id | ||
| resolved_thread_ts = thread_ts or self._thread_ts |
There was a problem hiding this comment.
🐣 ramble: I'm following this discussion curious! I agree the confusion of unknown defaults isn't good, but find extra effort in gathering the details I'm not changing...
At the moment the FIXME hints at the edge I'm finding where ts is unknown for app mentions in this class. We might find enhancement for this, but I'm wondering if overriding all defaults to stream to multiple different threads in the same channel for the same person at the same time - perhaps for subagent workflows - might be difficult.
Not a blocker at all for me but I want to share thought!
| if resolved_thread_ts is None: | ||
| raise ValueError( | ||
| "thread_ts is required: provide it as an argument or ensure thread_ts is set in the event context" | ||
| ) |
There was a problem hiding this comment.
| if resolved_thread_ts is None: | |
| raise ValueError( | |
| "thread_ts is required: provide it as an argument or ensure thread_ts is set in the event context" | |
| ) |
🪓 thought(non-blocking): Perhaps against above comments too, but I have small preference to pass missing values to the API for errors from upstream. I realize though that catching error earlier might be preferred, and we have validation logic for similar parameters too!
| `AsyncBoltAgent` instance | ||
| """ | ||
| if "agent" not in self: | ||
| from slack_bolt.agent.async_agent import AsyncBoltAgent |
There was a problem hiding this comment.
👁️🗨️ question: Perhaps similar to above discussion, but I'm wondering if we can comment inline a quick note for this moreso dynamic import? I notice similar pattern in the following file and would be tempted to move this without thinking too much...
| "set_status", | ||
| "set_title", | ||
| "set_suggested_prompts", | ||
| "agent", |
There was a problem hiding this comment.
🪬 quibble: I'm wanting this to be alphabetical so much ahhaha!
There was a problem hiding this comment.
🌚 question: I'm curious if agent should be included with "context" or kept to kwargs injections?
This is unclear to me since the functions and values above seem to map well to details included with an incoming request, but I'd find it odd to access agent in such pattern:
streamer = context.agent.chat_stream()It might just be unfamiliar to me! I'm also unsure if the same message context can be gathered from the kwargs setups for chat_stream creations and don't consider this blocking at all FWIW.
| def test_agent_accessible_via_context(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("app_mention") | ||
| def handle_mention(context: BoltContext): | ||
| agent = context.agent | ||
| assert agent is not None | ||
| assert isinstance(agent, BoltAgentDirect) | ||
| # Verify the same instance is returned on subsequent access | ||
| assert context.agent is agent | ||
| state["called"] = True | ||
|
|
||
| request = BoltRequest(body=app_mention_event_body, mode="socket_mode") | ||
| response = app.dispatch(request) | ||
| assert response.status == 200 | ||
| assert_target_called() |
There was a problem hiding this comment.
🧪 note: Ahaha I notice we might be expecting context and kwargs arguments to match for the agent! Thanks for including these tests!

Changelog
Do not include in our Changelog because this is marked as an experimental feature.
Summary
This pull request adds a
agent: BoltAgentlistener argument, which will be used to access AI agent-related features such aschat_streamand more.BoltAgentandAsyncBoltAgentclasses that provide a convenientchat_stream()method pre-configured with event context defaults (channel_id,thread_ts,team_id,user_id)kwargsinjection system so listeners can declare it as a parameter (e.g.def handle(agent: BoltAgent)) or access it viacontext.agentBoltAgentconstruction is deferred - only created when the listener actually requests the agent or args parameter, avoiding unnecessary object creation on every dispatchExperimental
The
agent: BoltAgentclass and listener argument are marked as an experimental feature. We are categorizing experimental features assemver:patchuntil the experimental status is removed.No exists tests were modified and the goal is to be a low risk feature to add to
main.Example
Known limitations
chat_stream()currently only works whenthread_tsis available in the event context (DMs and threaded replies). Top-level channel messages do not havethread_ts, andtsis not yet provided toBoltAgent- tracked as aFIXMEin the code. I've experimented with a solution, but I'd prefer to introduce it in a separate PR so that we can review it independently.Testing
→ Modified Sample App with
agent: BoltAgentandagent.chat_stream()Category
slack_bolt.Appand/or its core componentsslack_bolt.async_app.AsyncAppand/or its core componentsslack_bolt.adapter/docsRequirements
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.
./scripts/install_all_and_run_tests.shafter making the changes.