-
Notifications
You must be signed in to change notification settings - Fork 277
Add 'agent: BoltAgent' listener argument #1437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fadc1a4
10c56e9
e6e456a
d0752cb
c332128
1a73ac4
7722f3e
c7e0089
5335855
cf5ef98
724ea5f
0492aa0
3a85cd5
f32af71
29ffbb8
c2a590d
f10f94d
721b634
0195ee2
3d7f305
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,9 @@ venv/ | |
| .venv* | ||
| .env/ | ||
|
|
||
| # claude | ||
| .claude/*.local.json | ||
|
|
||
| # codecov / coverage | ||
| .coverage | ||
| cov_* | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Experiments | ||
|
|
||
| Bolt for Python includes experimental features still under active development. These features may be fleeting, may not be perfectly polished, and should be thought of as available for use "at your own risk." | ||
|
|
||
| Experimental features are categorized as `semver:patch` until the experimental status is removed. | ||
|
|
||
| We love feedback from our community, so we encourage you to explore and interact with the [GitHub repo](https://github.com/slackapi/bolt-python). Contributions, bug reports, and any feedback are all helpful; let us nurture the Slack CLI together to help make building Slack apps more pleasant for everyone. | ||
|
|
||
| ## Available experiments | ||
| * [Agent listener argument](#agent) | ||
|
|
||
| ## Agent listener argument {#agent} | ||
|
|
||
| The `agent: BoltAgent` listener argument provides access to AI agent-related features. | ||
|
|
||
| The `BoltAgent` and `AsyncBoltAgent` classes offer a `chat_stream()` method that comes pre-configured with event context defaults: `channel_id`, `thread_ts`, `team_id`, and `user_id` fields. | ||
|
|
||
| The listener argument is wired into the Bolt `kwargs` injection system, so listeners can declare it as a parameter or access it via the `context.agent` property. | ||
|
|
||
| ### Example | ||
|
|
||
| ```python | ||
| from slack_bolt import BoltAgent | ||
|
|
||
| @app.event("app_mention") | ||
| def handle_mention(agent: BoltAgent): | ||
| stream = agent.chat_stream() | ||
| stream.append(markdown_text="Hello!") | ||
| stream.stop() | ||
| ``` | ||
|
|
||
| ### Limitations | ||
|
|
||
| The `chat_stream()` method currently only works when the `thread_ts` field is available in the event context (DMs and threaded replies). Top-level channel messages do not have a `thread_ts` field, and the `ts` field is not yet provided to `BoltAgent`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1 @@ | ||
| """Adapter modules for running Bolt apps along with Web frameworks or Socket Mode. | ||
| """ | ||
| """Adapter modules for running Bolt apps along with Web frameworks or Socket Mode.""" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from .agent import BoltAgent | ||
|
|
||
| __all__ = [ | ||
| "BoltAgent", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from typing import Optional | ||
|
|
||
| from slack_sdk import WebClient | ||
| from slack_sdk.web.chat_stream import ChatStream | ||
|
|
||
|
|
||
| class BoltAgent: | ||
| """Agent listener argument for building AI-powered Slack agents. | ||
|
|
||
| Experimental: | ||
| This API is experimental and may change in future releases. | ||
|
Comment on lines
+10
to
+11
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
|
|
||
| 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. | ||
|
Comment on lines
+13
to
+14
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Important callout. I'd like to add |
||
|
|
||
| @app.event("app_mention") | ||
| def handle_mention(agent): | ||
| stream = agent.chat_stream() | ||
| stream.append(markdown_text="Hello!") | ||
| stream.stop() | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| client: WebClient, | ||
| channel_id: Optional[str] = None, | ||
| thread_ts: Optional[str] = None, | ||
| team_id: Optional[str] = None, | ||
| user_id: Optional[str] = None, | ||
| ): | ||
| self._client = client | ||
| self._channel_id = channel_id | ||
| self._thread_ts = thread_ts | ||
| self._team_id = team_id | ||
| self._user_id = user_id | ||
|
|
||
| def chat_stream( | ||
| self, | ||
| *, | ||
| channel: Optional[str] = None, | ||
| thread_ts: Optional[str] = None, | ||
| recipient_team_id: Optional[str] = None, | ||
| recipient_user_id: Optional[str] = None, | ||
| **kwargs, | ||
| ) -> ChatStream: | ||
| """Creates a ChatStream with defaults from event context. | ||
|
|
||
| Each call creates a new instance. Create multiple for parallel streams. | ||
|
|
||
| Args: | ||
| channel: Channel ID. Defaults to the channel from the event context. | ||
| thread_ts: Thread timestamp. Defaults to the thread_ts from the event context. | ||
| recipient_team_id: Team ID of the recipient. Defaults to the team from the event context. | ||
| recipient_user_id: User ID of the recipient. Defaults to the user from the event context. | ||
| **kwargs: Additional arguments passed to ``WebClient.chat_stream()``. | ||
|
|
||
| Returns: | ||
| A new ``ChatStream`` instance. | ||
| """ | ||
| provided = [arg for arg in (channel, thread_ts, recipient_team_id, recipient_user_id) if arg is not None] | ||
| if provided and len(provided) < 4: | ||
| raise ValueError( | ||
| "Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them" | ||
| ) | ||
| # Argument validation is delegated to chat_stream() and the API | ||
| return self._client.chat_stream( | ||
| channel=channel or self._channel_id, # type: ignore[arg-type] | ||
| thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type] | ||
| recipient_team_id=recipient_team_id or self._team_id, | ||
| recipient_user_id=recipient_user_id or self._user_id, | ||
| **kwargs, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| from typing import Optional | ||
|
|
||
| from slack_sdk.web.async_client import AsyncWebClient | ||
| from slack_sdk.web.async_chat_stream import AsyncChatStream | ||
|
|
||
|
|
||
| class AsyncBoltAgent: | ||
| """Async agent listener argument for building AI-powered Slack agents. | ||
|
|
||
| Experimental: | ||
| This API is experimental and may change in future releases. | ||
|
|
||
| @app.event("app_mention") | ||
| async def handle_mention(agent): | ||
| stream = await agent.chat_stream() | ||
| await stream.append(markdown_text="Hello!") | ||
| await stream.stop() | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| client: AsyncWebClient, | ||
| channel_id: Optional[str] = None, | ||
| thread_ts: Optional[str] = None, | ||
| team_id: Optional[str] = None, | ||
| user_id: Optional[str] = None, | ||
| ): | ||
| self._client = client | ||
| self._channel_id = channel_id | ||
| self._thread_ts = thread_ts | ||
| self._team_id = team_id | ||
| self._user_id = user_id | ||
|
|
||
| async def chat_stream( | ||
| self, | ||
| *, | ||
| channel: Optional[str] = None, | ||
| thread_ts: Optional[str] = None, | ||
| recipient_team_id: Optional[str] = None, | ||
| recipient_user_id: Optional[str] = None, | ||
| **kwargs, | ||
| ) -> AsyncChatStream: | ||
| """Creates an AsyncChatStream with defaults from event context. | ||
|
|
||
| Each call creates a new instance. Create multiple for parallel streams. | ||
|
|
||
| Args: | ||
| channel: Channel ID. Defaults to the channel from the event context. | ||
| thread_ts: Thread timestamp. Defaults to the thread_ts from the event context. | ||
| recipient_team_id: Team ID of the recipient. Defaults to the team from the event context. | ||
| recipient_user_id: User ID of the recipient. Defaults to the user from the event context. | ||
| **kwargs: Additional arguments passed to ``AsyncWebClient.chat_stream()``. | ||
|
|
||
| Returns: | ||
| A new ``AsyncChatStream`` instance. | ||
| """ | ||
| provided = [arg for arg in (channel, thread_ts, recipient_team_id, recipient_user_id) if arg is not None] | ||
| if provided and len(provided) < 4: | ||
| raise ValueError( | ||
| "Either provide all of channel, thread_ts, recipient_team_id, and recipient_user_id, or none of them" | ||
| ) | ||
| # Argument validation is delegated to chat_stream() and the API | ||
| return await self._client.chat_stream( | ||
| channel=channel or self._channel_id, # type: ignore[arg-type] | ||
| thread_ts=thread_ts or self._thread_ts, # type: ignore[arg-type] | ||
| recipient_team_id=recipient_team_id or self._team_id, | ||
| recipient_user_id=recipient_user_id or self._user_id, | ||
| **kwargs, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| import inspect | ||
| import logging | ||
| import warnings | ||
| from typing import Callable, Dict, MutableSequence, Optional, Any | ||
|
|
||
| from slack_bolt.request.async_request import AsyncBoltRequest | ||
| from slack_bolt.response import BoltResponse | ||
| from slack_bolt.warning import ExperimentalWarning | ||
| from .async_args import AsyncArgs | ||
| from slack_bolt.request.payload_utils import ( | ||
| to_options, | ||
|
|
@@ -29,7 +31,7 @@ def build_async_required_kwargs( | |
| error: Optional[Exception] = None, # for error handlers | ||
| next_keys_required: bool = True, # False for listeners / middleware / error handlers | ||
| ) -> Dict[str, Any]: | ||
| all_available_args = { | ||
| all_available_args: Dict[str, Any] = { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: This fixed a linter warning |
||
| "logger": logger, | ||
| "client": request.context.client, | ||
| "req": request, | ||
|
|
@@ -83,6 +85,23 @@ def build_async_required_kwargs( | |
| if k not in all_available_args: | ||
| all_available_args[k] = v | ||
|
|
||
| # Defer agent creation to avoid constructing AsyncBoltAgent on every request | ||
| if "agent" in required_arg_names: | ||
| from slack_bolt.agent.async_agent import AsyncBoltAgent | ||
|
|
||
| all_available_args["agent"] = AsyncBoltAgent( | ||
| client=request.context.client, | ||
| channel_id=request.context.channel_id, | ||
| thread_ts=request.context.thread_ts, | ||
| team_id=request.context.team_id, | ||
| user_id=request.context.user_id, | ||
| ) | ||
| warnings.warn( | ||
| "The agent listener argument is experimental and may change in future versions.", | ||
| category=ExperimentalWarning, | ||
| stacklevel=2, # Point to the caller, not this internal helper | ||
| ) | ||
|
|
||
| if len(required_arg_names) > 0: | ||
| # To support instance/class methods in a class for listeners/middleware, | ||
| # check if the first argument is either self or cls | ||
|
|
@@ -102,7 +121,7 @@ def build_async_required_kwargs( | |
| for name in required_arg_names: | ||
| if name == "args": | ||
| if isinstance(request, AsyncBoltRequest): | ||
| kwargs[name] = AsyncArgs(**all_available_args) # type: ignore[arg-type] | ||
| kwargs[name] = AsyncArgs(**all_available_args) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: The above fix allows us to remove this type ignore |
||
| else: | ||
| logger.warning(f"Unknown Request object type detected ({type(request)})") | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 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 BoltAgentThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 If possible, I'd prefer to export the
BoltAgentnow.ExperimentalWarningmakes it clear that the developer is using something that's unstable and at their own risk.👉🏻 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.