Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions docs/english/_sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
"tools/bolt-python/concepts/token-rotation"
]
},
{
"type": "category",
"label": "Experiments",
"items": ["tools/bolt-python/experiments"]
},
{
"type": "category",
"label": "Legacy",
Expand Down
34 changes: 34 additions & 0 deletions docs/english/experiments.md
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`.
4 changes: 4 additions & 0 deletions slack_bolt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from .response import BoltResponse

# AI Agents & Assistants
from .agent import BoltAgent
from .agent import Tools
from .middleware.assistant.assistant import (
Assistant,
)
Expand All @@ -46,6 +48,8 @@
"CustomListenerMatcher",
"BoltRequest",
"BoltResponse",
"BoltAgent",
"Tools",
"Assistant",
"AssistantThreadContext",
"AssistantThreadContextStore",
Expand Down
3 changes: 1 addition & 2 deletions slack_bolt/adapter/__init__.py
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."""
7 changes: 7 additions & 0 deletions slack_bolt/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .agent import BoltAgent
from .tools import Tools

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

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.

@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"
)
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 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,
)
79 changes: 79 additions & 0 deletions slack_bolt/agent/async_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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"
)
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,
)
Loading
Loading