Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fadc1a4
feat: add 'agent: BoltAgent' listener argument
mwbrooks Feb 10, 2026
10c56e9
fix: export AsyncBoltAgent from agent module
mwbrooks Feb 10, 2026
e6e456a
perf: defer BoltAgent construction to when listener requests it
mwbrooks Feb 10, 2026
d0752cb
fixme: note chat_stream limitation with channel messages missing ts
mwbrooks Feb 11, 2026
c332128
Merge branch 'main' into feat-agent-argument
mwbrooks Feb 11, 2026
1a73ac4
fix: don't import AsyncBoltAgent in agent __init__ to avoid aiohttp d…
mwbrooks Feb 11, 2026
7722f3e
fix: handle AsyncMock import for older Python versions in async agent…
mwbrooks Feb 11, 2026
c7e0089
fix: remove AsyncMock dependency from async agent tests for Python 3.…
mwbrooks Feb 11, 2026
5335855
chore: use relative imports in agent/__init__.py
mwbrooks Feb 11, 2026
cf5ef98
feat: emit ExperimentalWarning when agent kwarg is used
mwbrooks Feb 12, 2026
724ea5f
fix: disallow partial overrides of context args in agent chat_stream()
mwbrooks Feb 12, 2026
0492aa0
refactor: move agent unit tests into dedicated test directories
mwbrooks Feb 12, 2026
3a85cd5
adds experiments page
lukegalbraithrussell Feb 12, 2026
f32af71
adds experiments to sidebar
lukegalbraithrussell Feb 12, 2026
29ffbb8
refactor: delegate channel/thread_ts validation to the API in agent c…
mwbrooks Feb 16, 2026
c2a590d
docs: add inline comment explaining deferred agent imports
mwbrooks Feb 16, 2026
f10f94d
refactor: remove agent from context, construct in kwargs injection in…
mwbrooks Feb 16, 2026
721b634
fix: only construct agent when explicitly requested as a listener arg…
mwbrooks Feb 16, 2026
0195ee2
refactor: remove whitespace on ExperimentalWarning
mwbrooks Feb 16, 2026
3d7f305
fix: add type ignore comments for chat_stream args delegated to the API
mwbrooks Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ venv/
.venv*
.env/

# claude
.claude/*.local.json

# codecov / coverage
.coverage
cov_*
Expand Down
2 changes: 2 additions & 0 deletions slack_bolt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .response import BoltResponse

# AI Agents & Assistants
from .agent import BoltAgent
Copy link
Copy Markdown
Contributor

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 BoltAgent

Copy link
Copy Markdown
Member Author

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 BoltAgent now.

  1. 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..
  2. Our new ExperimentalWarning makes it clear that the developer is using something that's unstable and at their own risk.
  3. 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.

from .middleware.assistant.assistant import (
Assistant,
)
Expand All @@ -46,6 +47,7 @@
"CustomListenerMatcher",
"BoltRequest",
"BoltResponse",
"BoltAgent",
"Assistant",
"AssistantThreadContext",
"AssistantThreadContextStore",
Expand Down
5 changes: 5 additions & 0 deletions slack_bolt/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from slack_bolt.agent.agent import BoltAgent

__all__ = [
"BoltAgent",
]
77 changes: 77 additions & 0 deletions slack_bolt/agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 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.
Comment on lines +13 to +14
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Important callout. I'd like to add ts support in a follow-up PR so that we can discuss the best approach.


@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.
"""
resolved_channel = channel or self._channel_id
resolved_thread_ts = thread_ts or self._thread_ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 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 🤔 ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🚀

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐣 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!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zimeg Totally fair point! My best suggestion at moving forward is that it's easier to loosen constraints than tighten them later. So, if we start with the "all or nothing" approach above, then we can loosen it when we come up against your advanced use-cases. 🤔

if resolved_channel is None:
raise ValueError(
"channel is required: provide it as an argument or ensure channel_id is set in the event context"
)
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"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➕ I agree, we should allow the underlying chat_stream implementation and Slack API enforce the argument error handling. Commit 29ffbb8 removes this logic for all of the params. Thanks for the catch @zimeg! 🙇🏻

return self._client.chat_stream(
channel=resolved_channel,
thread_ts=resolved_thread_ts,
recipient_team_id=recipient_team_id or self._team_id,
recipient_user_id=recipient_user_id or self._user_id,
**kwargs,
)
74 changes: 74 additions & 0 deletions slack_bolt/agent/async_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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.
"""
resolved_channel = channel or self._channel_id
resolved_thread_ts = thread_ts or self._thread_ts
if resolved_channel is None:
raise ValueError(
"channel is required: provide it as an argument or ensure channel_id is set in the event context"
)
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"
)
return await self._client.chat_stream(
channel=resolved_channel,
thread_ts=resolved_thread_ts,
recipient_team_id=recipient_team_id or self._team_id,
recipient_user_id=recipient_user_id or self._user_id,
**kwargs,
)
33 changes: 32 additions & 1 deletion slack_bolt/context/async_context.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import TYPE_CHECKING, Optional

from slack_sdk.web.async_client import AsyncWebClient

Expand All @@ -15,6 +15,9 @@
from slack_bolt.context.set_title.async_set_title import AsyncSetTitle
from slack_bolt.util.utils import create_copy

if TYPE_CHECKING:
from slack_bolt.agent.async_agent import AsyncBoltAgent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AsyncBoltAgent whenever AsyncBoltContext is imported
  • Static type checking around AsyncBoltAgent passes
  • AsyncBoltAgent is only imported when context.agent is invoked

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


class AsyncBoltContext(BaseContext):
"""Context object associated with a request from Slack."""
Expand Down Expand Up @@ -187,6 +190,34 @@ async def handle_button_clicks(context):
self["fail"] = AsyncFail(client=self.client, function_execution_id=self.function_execution_id)
return self["fail"]

@property
def agent(self) -> "AsyncBoltAgent":
"""`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()

Returns:
`AsyncBoltAgent` instance
"""
if "agent" not in self:
from slack_bolt.agent.async_agent import AsyncBoltAgent
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️‍🗨️ 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...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


When this experiment is removed we probably want to remove the dynamic import as it can lead runtime error

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this @zimeg. I agree, I'd probably be the developer who "refactored" the code to readability only to introduce an unintended side effect. Commit c2a590d adds a comment.

I'm not 100% certain that we'll keep the agent argument. If we do, then we'll want to revisit @WilliamBergamin's comment about removing the dynamic import to avoid runtime errors.


self["agent"] = AsyncBoltAgent(
client=self.client,
channel_id=self.channel_id,
thread_ts=self.thread_ts,
team_id=self.team_id,
user_id=self.user_id,
)
return self["agent"]

@property
def set_title(self) -> Optional[AsyncSetTitle]:
return self.get("set_title")
Expand Down
1 change: 1 addition & 0 deletions slack_bolt/context/base_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class BaseContext(dict):
"set_status",
"set_title",
"set_suggested_prompts",
"agent",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 quibble: I'm wanting this to be alphabetical so much ahhaha!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌚 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zimeg Fantastic comment and catch! 🎣 I agree, we don't want the context.agent.chat_stream() pattern and only want def handler(agent) as an argument.

I wish I would have noticed this sooner because the refactor removed a lot of code and simplified the implementation and moves all of the agent construction to a single place (kwargs injection) 😅

Commit f10f94d removes context.agent.*.

]
# Note that these items are not copyable, so when you add new items to this list,
# you must modify ThreadListenerRunner/AsyncioListenerRunner's _build_lazy_request method to pass the values.
Expand Down
33 changes: 32 additions & 1 deletion slack_bolt/context/context.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import TYPE_CHECKING, Optional

from slack_sdk import WebClient

Expand All @@ -15,6 +15,9 @@
from slack_bolt.context.set_title import SetTitle
from slack_bolt.util.utils import create_copy

if TYPE_CHECKING:
from slack_bolt.agent.agent import BoltAgent


class BoltContext(BaseContext):
"""Context object associated with a request from Slack."""
Expand Down Expand Up @@ -188,6 +191,34 @@ def handle_button_clicks(context):
self["fail"] = Fail(client=self.client, function_execution_id=self.function_execution_id)
return self["fail"]

@property
def agent(self) -> "BoltAgent":
"""`agent` listener argument for building AI-powered Slack agents.

Experimental:
This API is experimental and may change in future releases.

@app.event("app_mention")
def handle_mention(agent):
stream = agent.chat_stream()
stream.append(markdown_text="Hello!")
stream.stop()

Returns:
`BoltAgent` instance
"""
if "agent" not in self:
from slack_bolt.agent.agent import BoltAgent

self["agent"] = BoltAgent(
client=self.client,
channel_id=self.channel_id,
thread_ts=self.thread_ts,
team_id=self.team_id,
user_id=self.user_id,
)
return self["agent"]

@property
def set_title(self) -> Optional[SetTitle]:
return self.get("set_title")
Expand Down
5 changes: 5 additions & 0 deletions slack_bolt/kwargs_injection/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from slack_bolt.context.fail import Fail
from slack_bolt.context.get_thread_context.get_thread_context import GetThreadContext
from slack_bolt.context.respond import Respond
from slack_bolt.agent.agent import BoltAgent
from slack_bolt.context.save_thread_context import SaveThreadContext
from slack_bolt.context.say import Say
from slack_bolt.context.set_status import SetStatus
Expand Down Expand Up @@ -102,6 +103,8 @@ def handle_buttons(args):
"""`get_thread_context()` utility function for AI Agents & Assistants"""
save_thread_context: Optional[SaveThreadContext]
"""`save_thread_context()` utility function for AI Agents & Assistants"""
agent: Optional[BoltAgent]
"""`agent` listener argument for AI Agents & Assistants"""
# middleware
next: Callable[[], None]
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
Expand Down Expand Up @@ -135,6 +138,7 @@ def __init__(
set_suggested_prompts: Optional[SetSuggestedPrompts] = None,
get_thread_context: Optional[GetThreadContext] = None,
save_thread_context: Optional[SaveThreadContext] = None,
agent: Optional[BoltAgent] = None,
# As this method is not supposed to be invoked by bolt-python users,
# the naming conflict with the built-in one affects
# only the internals of this method
Expand Down Expand Up @@ -168,6 +172,7 @@ def __init__(
self.set_suggested_prompts = set_suggested_prompts
self.get_thread_context = get_thread_context
self.save_thread_context = save_thread_context
self.agent = agent

self.next: Callable[[], None] = next
self.next_: Callable[[], None] = next
5 changes: 5 additions & 0 deletions slack_bolt/kwargs_injection/async_args.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from logging import Logger
from typing import Callable, Awaitable, Dict, Any, Optional

from slack_bolt.agent.async_agent import AsyncBoltAgent
from slack_bolt.context.ack.async_ack import AsyncAck
from slack_bolt.context.async_context import AsyncBoltContext
from slack_bolt.context.complete.async_complete import AsyncComplete
Expand Down Expand Up @@ -101,6 +102,8 @@ async def handle_buttons(args):
"""`get_thread_context()` utility function for AI Agents & Assistants"""
save_thread_context: Optional[AsyncSaveThreadContext]
"""`save_thread_context()` utility function for AI Agents & Assistants"""
agent: Optional[AsyncBoltAgent]
"""`agent` listener argument for AI Agents & Assistants"""
# middleware
next: Callable[[], Awaitable[None]]
"""`next()` utility function, which tells the middleware chain that it can continue with the next one"""
Expand Down Expand Up @@ -134,6 +137,7 @@ def __init__(
set_suggested_prompts: Optional[AsyncSetSuggestedPrompts] = None,
get_thread_context: Optional[AsyncGetThreadContext] = None,
save_thread_context: Optional[AsyncSaveThreadContext] = None,
agent: Optional[AsyncBoltAgent] = None,
next: Callable[[], Awaitable[None]],
**kwargs, # noqa
):
Expand Down Expand Up @@ -164,6 +168,7 @@ def __init__(
self.set_suggested_prompts = set_suggested_prompts
self.get_thread_context = get_thread_context
self.save_thread_context = save_thread_context
self.agent = agent

self.next: Callable[[], Awaitable[None]] = next
self.next_: Callable[[], Awaitable[None]] = next
8 changes: 6 additions & 2 deletions slack_bolt/kwargs_injection/async_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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] = {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -83,6 +83,10 @@ 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 or "args" in required_arg_names:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this but why would we want to initialize this if "args" in required_arg_names? Shouldn't it be only when "agent" in required_arg_names?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilliamBergamin thanks for catching this!

The "args" in required_arg_names check exists because Args has an agent field, so when a listener uses def handle(args), the Args object is constructed with all_available_args - which means agent needs to be in all_available_args or it defaults to None.

However, I agree we should only construct the agent when "agent" in required_arg_names. If someone uses args.agent, it'll be None. That seems to be the experience we want.

It also simplifies our implementation further. 🧹 ✨

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 721b634 removes the "args" in required_arg_names.

all_available_args["agent"] = request.context.agent
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Here is a preview of the warning:
image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!


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
Expand All @@ -102,7 +106,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)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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)})")

Expand Down
Loading