From 212c413465d2f74ca02623f2ba787ddb750e68a2 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 24 Sep 2025 23:00:41 -0700 Subject: [PATCH 01/13] version 1.26.0.dev0 --- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 024ee6654..416b7e521 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk>=3.35.0,<4"] +dependencies = ["slack_sdk==3.36.0.dev3"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index 7f9c19341..c82ba217e 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.25.0" +__version__ = "1.26.0.dev0" From 973af99031d7d4c7f1d4ed3dfea027346d56b4cf Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 25 Sep 2025 17:26:53 -0700 Subject: [PATCH 02/13] feat: accept markdown_text argument from the say helper (#1372) --- slack_bolt/context/say/async_say.py | 12 +++++++----- slack_bolt/context/say/say.py | 4 +++- tests/slack_bolt/context/test_say.py | 10 ++++++---- tests/slack_bolt_async/context/test_async_say.py | 13 ++++++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/slack_bolt/context/say/async_say.py b/slack_bolt/context/say/async_say.py index b771529b0..c492e5d77 100644 --- a/slack_bolt/context/say/async_say.py +++ b/slack_bolt/context/say/async_say.py @@ -1,14 +1,14 @@ -from typing import Optional, Union, Dict, Sequence, Callable, Awaitable +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union -from slack_sdk.models.metadata import Metadata - -from slack_bolt.context.say.internals import _can_say -from slack_bolt.util.utils import create_copy from slack_sdk.models.attachments import Attachment from slack_sdk.models.blocks import Block +from slack_sdk.models.metadata import Metadata from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.async_slack_response import AsyncSlackResponse +from slack_bolt.context.say.internals import _can_say +from slack_bolt.util.utils import create_copy + class AsyncSay: client: Optional[AsyncWebClient] @@ -42,6 +42,7 @@ async def __call__( icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -67,6 +68,7 @@ async def __call__( icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/slack_bolt/context/say/say.py b/slack_bolt/context/say/say.py index 6cfbcd801..a6e5904e3 100644 --- a/slack_bolt/context/say/say.py +++ b/slack_bolt/context/say/say.py @@ -1,4 +1,4 @@ -from typing import Optional, Union, Dict, Sequence, Callable +from typing import Callable, Dict, Optional, Sequence, Union from slack_sdk import WebClient from slack_sdk.models.attachments import Attachment @@ -45,6 +45,7 @@ def __call__( icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -70,6 +71,7 @@ def __call__( icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/tests/slack_bolt/context/test_say.py b/tests/slack_bolt/context/test_say.py index 9e465e5d5..6ca1fc96a 100644 --- a/tests/slack_bolt/context/test_say.py +++ b/tests/slack_bolt/context/test_say.py @@ -3,10 +3,7 @@ from slack_sdk.web import SlackResponse from slack_bolt import Say -from tests.mock_web_api_server import ( - setup_mock_web_api_server, - cleanup_mock_web_api_server, -) +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server class TestSay: @@ -24,6 +21,11 @@ def test_say(self): response: SlackResponse = say(text="Hi there!") assert response.status_code == 200 + def test_say_markdown_text(self): + say = Say(client=self.web_client, channel="C111") + response: SlackResponse = say(markdown_text="**Greetings!**") + assert response.status_code == 200 + def test_say_unfurl_options(self): say = Say(client=self.web_client, channel="C111") response: SlackResponse = say(text="Hi there!", unfurl_media=True, unfurl_links=True) diff --git a/tests/slack_bolt_async/context/test_async_say.py b/tests/slack_bolt_async/context/test_async_say.py index 77ac0cc0e..efa90febc 100644 --- a/tests/slack_bolt_async/context/test_async_say.py +++ b/tests/slack_bolt_async/context/test_async_say.py @@ -2,12 +2,9 @@ from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.async_slack_response import AsyncSlackResponse -from tests.utils import get_event_loop from slack_bolt.context.say.async_say import AsyncSay -from tests.mock_web_api_server import ( - cleanup_mock_web_api_server_async, - setup_mock_web_api_server_async, -) +from tests.mock_web_api_server import cleanup_mock_web_api_server_async, setup_mock_web_api_server_async +from tests.utils import get_event_loop class TestAsyncSay: @@ -29,6 +26,12 @@ async def test_say(self): response: AsyncSlackResponse = await say(text="Hi there!") assert response.status_code == 200 + @pytest.mark.asyncio + async def test_say_markdown_text(self): + say = AsyncSay(client=self.web_client, channel="C111") + response: AsyncSlackResponse = await say(markdown_text="**Greetings!**") + assert response.status_code == 200 + @pytest.mark.asyncio async def test_say_unfurl_options(self): say = AsyncSay(client=self.web_client, channel="C111") From c8f2d6e55f417a435119531808c33a1be9c6e5fe Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 29 Sep 2025 14:15:52 -0700 Subject: [PATCH 03/13] feat: accept assistant.threads.setStatus method arguments from the assistant class (#1371) --- docs/reference/async_app.html | 11 ++++- .../context/set_status/async_set_status.html | 11 ++++- docs/reference/context/set_status/index.html | 11 ++++- .../context/set_status/set_status.html | 11 ++++- docs/reference/index.html | 11 ++++- .../context/set_status/async_set_status.py | 13 +++++- slack_bolt/context/set_status/set_status.py | 13 +++++- tests/slack_bolt/context/test_set_status.py | 38 ++++++++++++++++ .../context/test_async_set_status.py | 45 +++++++++++++++++++ 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 tests/slack_bolt/context/test_set_status.py create mode 100644 tests/slack_bolt_async/context/test_async_set_status.py diff --git a/docs/reference/async_app.html b/docs/reference/async_app.html index ad9192253..9a87b0f32 100644 --- a/docs/reference/async_app.html +++ b/docs/reference/async_app.html @@ -5248,11 +5248,18 @@

Class variables

self.channel_id = channel_id self.thread_ts = thread_ts - async def __call__(self, status: str) -> AsyncSlackResponse: + async def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: return await self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, )
diff --git a/docs/reference/context/set_status/async_set_status.html b/docs/reference/context/set_status/async_set_status.html index 6a15d70ae..06efd6447 100644 --- a/docs/reference/context/set_status/async_set_status.html +++ b/docs/reference/context/set_status/async_set_status.html @@ -70,11 +70,18 @@

Classes

self.channel_id = channel_id self.thread_ts = thread_ts - async def __call__(self, status: str) -> AsyncSlackResponse: + async def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: return await self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, )
diff --git a/docs/reference/context/set_status/index.html b/docs/reference/context/set_status/index.html index 9e53da9a5..aa11815e3 100644 --- a/docs/reference/context/set_status/index.html +++ b/docs/reference/context/set_status/index.html @@ -81,11 +81,18 @@

Classes

self.channel_id = channel_id self.thread_ts = thread_ts - def __call__(self, status: str) -> SlackResponse: + def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: return self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, )
diff --git a/docs/reference/context/set_status/set_status.html b/docs/reference/context/set_status/set_status.html index 0ec8df5da..e4d839f64 100644 --- a/docs/reference/context/set_status/set_status.html +++ b/docs/reference/context/set_status/set_status.html @@ -70,11 +70,18 @@

Classes

self.channel_id = channel_id self.thread_ts = thread_ts - def __call__(self, status: str) -> SlackResponse: + def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: return self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, )
diff --git a/docs/reference/index.html b/docs/reference/index.html index 430e36813..e3a2b1169 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -5786,11 +5786,18 @@

Class variables

self.channel_id = channel_id self.thread_ts = thread_ts - def __call__(self, status: str) -> SlackResponse: + def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: return self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, )
diff --git a/slack_bolt/context/set_status/async_set_status.py b/slack_bolt/context/set_status/async_set_status.py index 926ec6de8..e2c451f46 100644 --- a/slack_bolt/context/set_status/async_set_status.py +++ b/slack_bolt/context/set_status/async_set_status.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.async_slack_response import AsyncSlackResponse @@ -17,9 +19,16 @@ def __init__( self.channel_id = channel_id self.thread_ts = thread_ts - async def __call__(self, status: str) -> AsyncSlackResponse: + async def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: return await self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, ) diff --git a/slack_bolt/context/set_status/set_status.py b/slack_bolt/context/set_status/set_status.py index 8df0d49a7..0ed612e16 100644 --- a/slack_bolt/context/set_status/set_status.py +++ b/slack_bolt/context/set_status/set_status.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from slack_sdk import WebClient from slack_sdk.web import SlackResponse @@ -17,9 +19,16 @@ def __init__( self.channel_id = channel_id self.thread_ts = thread_ts - def __call__(self, status: str) -> SlackResponse: + def __call__( + self, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: return self.client.assistant_threads_setStatus( - status=status, channel_id=self.channel_id, thread_ts=self.thread_ts, + status=status, + loading_messages=loading_messages, + **kwargs, ) diff --git a/tests/slack_bolt/context/test_set_status.py b/tests/slack_bolt/context/test_set_status.py new file mode 100644 index 000000000..fe998df5e --- /dev/null +++ b/tests/slack_bolt/context/test_set_status.py @@ -0,0 +1,38 @@ +import pytest +from slack_sdk import WebClient +from slack_sdk.web import SlackResponse + +from slack_bolt.context.set_status import SetStatus +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server + + +class TestSetStatus: + def setup_method(self): + setup_mock_web_api_server(self) + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + self.web_client = WebClient(token=valid_token, base_url=mock_api_server_base_url) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + + def test_set_status(self): + set_status = SetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: SlackResponse = set_status("Thinking...") + assert response.status_code == 200 + + def test_set_status_loading_messages(self): + set_status = SetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: SlackResponse = set_status( + status="Thinking...", + loading_messages=[ + "Sitting...", + "Waiting...", + ], + ) + assert response.status_code == 200 + + def test_set_status_invalid(self): + set_status = SetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + with pytest.raises(TypeError): + set_status() diff --git a/tests/slack_bolt_async/context/test_async_set_status.py b/tests/slack_bolt_async/context/test_async_set_status.py new file mode 100644 index 000000000..8df34171f --- /dev/null +++ b/tests/slack_bolt_async/context/test_async_set_status.py @@ -0,0 +1,45 @@ +import pytest +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.async_slack_response import AsyncSlackResponse + +from slack_bolt.context.set_status.async_set_status import AsyncSetStatus +from tests.mock_web_api_server import cleanup_mock_web_api_server_async, setup_mock_web_api_server_async +from tests.utils import get_event_loop + + +class TestAsyncSetStatus: + @pytest.fixture + def event_loop(self): + setup_mock_web_api_server_async(self) + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + self.web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url) + + loop = get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server_async(self) + + @pytest.mark.asyncio + async def test_set_status(self): + set_status = AsyncSetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: AsyncSlackResponse = await set_status("Thinking...") + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_set_status_loading_messages(self): + set_status = AsyncSetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: AsyncSlackResponse = await set_status( + status="Thinking...", + loading_messages=[ + "Sitting...", + "Waiting...", + ], + ) + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_set_status_invalid(self): + set_status = AsyncSetStatus(client=self.web_client, channel_id="C111", thread_ts="123.123") + with pytest.raises(TypeError): + await set_status() From 6a127edebdbbc23e72db9951b762c507e70daec9 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Sep 2025 11:07:44 -0700 Subject: [PATCH 04/13] chore(release): version 1.26.0.dev1 --- docs/reference/async_app.html | 2 ++ .../context/assistant/thread_context_store/file/index.html | 2 +- docs/reference/context/say/async_say.html | 2 ++ docs/reference/context/say/index.html | 2 ++ docs/reference/context/say/say.html | 2 ++ docs/reference/index.html | 4 +++- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 8 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/reference/async_app.html b/docs/reference/async_app.html index 9a87b0f32..763745a54 100644 --- a/docs/reference/async_app.html +++ b/docs/reference/async_app.html @@ -5158,6 +5158,7 @@

Class variables

icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -5183,6 +5184,7 @@

Class variables

icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/docs/reference/context/assistant/thread_context_store/file/index.html b/docs/reference/context/assistant/thread_context_store/file/index.html index 4a5d944e1..cbb4e4db6 100644 --- a/docs/reference/context/assistant/thread_context_store/file/index.html +++ b/docs/reference/context/assistant/thread_context_store/file/index.html @@ -48,7 +48,7 @@

Classes

class FileAssistantThreadContextStore -(base_dir: str = '/Users/wbergamin/.bolt-app-assistant-thread-contexts') +(base_dir: str = '/Users/eden.zimbelman/.bolt-app-assistant-thread-contexts')
diff --git a/docs/reference/context/say/async_say.html b/docs/reference/context/say/async_say.html index 8547a1188..e170251fe 100644 --- a/docs/reference/context/say/async_say.html +++ b/docs/reference/context/say/async_say.html @@ -87,6 +87,7 @@

Classes

icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -112,6 +113,7 @@

Classes

icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/docs/reference/context/say/index.html b/docs/reference/context/say/index.html index 7a5850760..e2ed0d03f 100644 --- a/docs/reference/context/say/index.html +++ b/docs/reference/context/say/index.html @@ -105,6 +105,7 @@

Classes

icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -130,6 +131,7 @@

Classes

icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/docs/reference/context/say/say.html b/docs/reference/context/say/say.html index 5db4f24ba..c66e2776f 100644 --- a/docs/reference/context/say/say.html +++ b/docs/reference/context/say/say.html @@ -90,6 +90,7 @@

Classes

icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -115,6 +116,7 @@

Classes

icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/docs/reference/index.html b/docs/reference/index.html index e3a2b1169..3653e10af 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -5225,7 +5225,7 @@

Class variables

class FileAssistantThreadContextStore -(base_dir: str = '/Users/wbergamin/.bolt-app-assistant-thread-contexts') +(base_dir: str = '/Users/eden.zimbelman/.bolt-app-assistant-thread-contexts')
@@ -5691,6 +5691,7 @@

Class variables

icon_emoji: Optional[str] = None, icon_url: Optional[str] = None, username: Optional[str] = None, + markdown_text: Optional[str] = None, mrkdwn: Optional[bool] = None, link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full @@ -5716,6 +5717,7 @@

Class variables

icon_emoji=icon_emoji, icon_url=icon_url, username=username, + markdown_text=markdown_text, mrkdwn=mrkdwn, link_names=link_names, parse=parse, diff --git a/pyproject.toml b/pyproject.toml index 416b7e521..1c5d7c597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.36.0.dev3"] +dependencies = ["slack_sdk==3.36.0.dev4"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index c82ba217e..d2c5aa4ba 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.26.0.dev0" +__version__ = "1.26.0.dev1" From e3a082114c818f2a157f1a0c71d9eaa5ce1d1751 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Sep 2025 11:26:59 -0700 Subject: [PATCH 05/13] chore(release): version 1.26.0.dev2 --- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c5d7c597..aeebdfee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.36.0.dev4"] +dependencies = ["slack_sdk==3.36.0.dev5"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index d2c5aa4ba..b47d8dfb5 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.26.0.dev1" +__version__ = "1.26.0.dev2" From 23a4731582c5909ac5644aef7e792fa76893926a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 2 Oct 2025 11:49:23 -0700 Subject: [PATCH 06/13] chore(release): version 1.26.0.dev3 --- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aeebdfee3..7f0bbf437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.36.0.dev5"] +dependencies = ["slack_sdk==3.36.0.dev6"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index b47d8dfb5..acf1eab7f 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.26.0.dev2" +__version__ = "1.26.0.dev3" From aaabe50eea898fab124dcae92da1702de7dbcb6f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 3 Oct 2025 10:26:18 -0700 Subject: [PATCH 07/13] fix(types): accept a sequence of suggested prompts in assistant context (#1382) --- .../async_set_suggested_prompts.py | 4 +- .../set_suggested_prompts.py | 4 +- .../context/test_set_suggested_prompts.py | 37 +++++++++++++++ .../test_async_set_suggested_prompts.py | 45 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 tests/slack_bolt/context/test_set_suggested_prompts.py create mode 100644 tests/slack_bolt_async/context/test_async_set_suggested_prompts.py diff --git a/slack_bolt/context/set_suggested_prompts/async_set_suggested_prompts.py b/slack_bolt/context/set_suggested_prompts/async_set_suggested_prompts.py index aeeb244d7..2079b6448 100644 --- a/slack_bolt/context/set_suggested_prompts/async_set_suggested_prompts.py +++ b/slack_bolt/context/set_suggested_prompts/async_set_suggested_prompts.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Union, Optional +from typing import Dict, List, Optional, Sequence, Union from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.async_slack_response import AsyncSlackResponse @@ -21,7 +21,7 @@ def __init__( async def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> AsyncSlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/slack_bolt/context/set_suggested_prompts/set_suggested_prompts.py b/slack_bolt/context/set_suggested_prompts/set_suggested_prompts.py index fc9304b17..21ff815e1 100644 --- a/slack_bolt/context/set_suggested_prompts/set_suggested_prompts.py +++ b/slack_bolt/context/set_suggested_prompts/set_suggested_prompts.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Union, Optional +from typing import Dict, List, Optional, Sequence, Union from slack_sdk import WebClient from slack_sdk.web import SlackResponse @@ -21,7 +21,7 @@ def __init__( def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> SlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/tests/slack_bolt/context/test_set_suggested_prompts.py b/tests/slack_bolt/context/test_set_suggested_prompts.py new file mode 100644 index 000000000..792b974b5 --- /dev/null +++ b/tests/slack_bolt/context/test_set_suggested_prompts.py @@ -0,0 +1,37 @@ +import pytest +from slack_sdk import WebClient +from slack_sdk.web import SlackResponse + +from slack_bolt.context.set_suggested_prompts import SetSuggestedPrompts +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server + + +class TestSetSuggestedPrompts: + def setup_method(self): + setup_mock_web_api_server(self) + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + self.web_client = WebClient(token=valid_token, base_url=mock_api_server_base_url) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + + def test_set_suggested_prompts(self): + set_suggested_prompts = SetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: SlackResponse = set_suggested_prompts(prompts=["One", "Two"]) + assert response.status_code == 200 + + def test_set_suggested_prompts_objects(self): + set_suggested_prompts = SetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: SlackResponse = set_suggested_prompts( + prompts=[ + "One", + {"title": "Two", "message": "What's before addition?"}, + ], + ) + assert response.status_code == 200 + + def test_set_suggested_prompts_invalid(self): + set_suggested_prompts = SetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + with pytest.raises(TypeError): + set_suggested_prompts() diff --git a/tests/slack_bolt_async/context/test_async_set_suggested_prompts.py b/tests/slack_bolt_async/context/test_async_set_suggested_prompts.py new file mode 100644 index 000000000..70a24efcb --- /dev/null +++ b/tests/slack_bolt_async/context/test_async_set_suggested_prompts.py @@ -0,0 +1,45 @@ +import asyncio + +import pytest +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.async_slack_response import AsyncSlackResponse + +from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import AsyncSetSuggestedPrompts +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server + + +class TestAsyncSetSuggestedPrompts: + @pytest.fixture + def event_loop(self): + setup_mock_web_api_server(self) + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + self.web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url) + + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + + @pytest.mark.asyncio + async def test_set_suggested_prompts(self): + set_suggested_prompts = AsyncSetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: AsyncSlackResponse = await set_suggested_prompts(prompts=["One", "Two"]) + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_set_suggested_prompts_objects(self): + set_suggested_prompts = AsyncSetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + response: AsyncSlackResponse = await set_suggested_prompts( + prompts=[ + "One", + {"title": "Two", "message": "What's before addition?"}, + ], + ) + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_set_suggested_prompts_invalid(self): + set_suggested_prompts = AsyncSetSuggestedPrompts(client=self.web_client, channel_id="C111", thread_ts="123.123") + with pytest.raises(TypeError): + await set_suggested_prompts() From 502ffe6f0029f46e5fd0c77116e13a6e686c199d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 3 Oct 2025 11:36:54 -0700 Subject: [PATCH 08/13] chore(release): version 1.26.0.dev4 --- docs/reference/app/app.html | 11 ++++++++--- docs/reference/app/async_app.html | 11 ++++++++--- docs/reference/app/index.html | 11 ++++++++--- docs/reference/async_app.html | 13 +++++++++---- .../async_set_suggested_prompts.html | 2 +- .../context/set_suggested_prompts/index.html | 2 +- .../set_suggested_prompts.html | 2 +- docs/reference/index.html | 13 +++++++++---- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 10 files changed, 47 insertions(+), 22 deletions(-) diff --git a/docs/reference/app/app.html b/docs/reference/app/app.html index 3ee02b07c..c91d020ef 100644 --- a/docs/reference/app/app.html +++ b/docs/reference/app/app.html @@ -1195,7 +1195,9 @@

Classes

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3018,7 +3020,9 @@

Args

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3028,7 +3032,8 @@

Args

return __call__

Registers a new view_submission listener. -Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.

+Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for +details.

diff --git a/docs/reference/app/async_app.html b/docs/reference/app/async_app.html index 78959986b..9cbc801d0 100644 --- a/docs/reference/app/async_app.html +++ b/docs/reference/app/async_app.html @@ -1215,7 +1215,9 @@

Classes

middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None, ) -> Callable[..., Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3075,7 +3077,9 @@

Args

middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None, ) -> Callable[..., Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3085,7 +3089,8 @@

Args

return __call__

Registers a new view_submission listener. -Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.

+Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for +details.

def web_app(self, path: str = '/slack/events', port: int = 3000) ‑> aiohttp.web_app.Application diff --git a/docs/reference/app/index.html b/docs/reference/app/index.html index a46bc2e71..8821e5af9 100644 --- a/docs/reference/app/index.html +++ b/docs/reference/app/index.html @@ -1214,7 +1214,9 @@

Classes

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3037,7 +3039,9 @@

Args

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3047,7 +3051,8 @@

Args

return __call__

Registers a new view_submission listener. -Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.

+Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for +details.

diff --git a/docs/reference/async_app.html b/docs/reference/async_app.html index c2c1b6f54..8fd975be9 100644 --- a/docs/reference/async_app.html +++ b/docs/reference/async_app.html @@ -1306,7 +1306,9 @@

Class variables

middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None, ) -> Callable[..., Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3166,7 +3168,9 @@

Args

middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]] = None, ) -> Callable[..., Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3176,7 +3180,8 @@

Args

return __call__

Registers a new view_submission listener. -Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.

+Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for +details.

def web_app(self, path: str = '/slack/events', port: int = 3000) ‑> aiohttp.web_app.Application @@ -5307,7 +5312,7 @@

Class variables

async def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> AsyncSlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/docs/reference/context/set_suggested_prompts/async_set_suggested_prompts.html b/docs/reference/context/set_suggested_prompts/async_set_suggested_prompts.html index 449a72117..4feda52ba 100644 --- a/docs/reference/context/set_suggested_prompts/async_set_suggested_prompts.html +++ b/docs/reference/context/set_suggested_prompts/async_set_suggested_prompts.html @@ -72,7 +72,7 @@

Classes

async def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> AsyncSlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/docs/reference/context/set_suggested_prompts/index.html b/docs/reference/context/set_suggested_prompts/index.html index ee5371cea..12d864dde 100644 --- a/docs/reference/context/set_suggested_prompts/index.html +++ b/docs/reference/context/set_suggested_prompts/index.html @@ -83,7 +83,7 @@

Classes

def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> SlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/docs/reference/context/set_suggested_prompts/set_suggested_prompts.html b/docs/reference/context/set_suggested_prompts/set_suggested_prompts.html index 133d3a55a..6c0385e57 100644 --- a/docs/reference/context/set_suggested_prompts/set_suggested_prompts.html +++ b/docs/reference/context/set_suggested_prompts/set_suggested_prompts.html @@ -72,7 +72,7 @@

Classes

def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> SlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/docs/reference/index.html b/docs/reference/index.html index dc2a2fa4c..7bd6d117e 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -1335,7 +1335,9 @@

Class variables

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3158,7 +3160,9 @@

Args

middleware: Optional[Sequence[Union[Callable, Middleware]]] = None, ) -> Callable[..., Optional[Callable[..., Optional[BoltResponse]]]]: """Registers a new `view_submission` listener. - Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.""" + Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for + details. + """ def __call__(*args, **kwargs): functions = self._to_listener_functions(kwargs) if kwargs else list(args) @@ -3168,7 +3172,8 @@

Args

return __call__

Registers a new view_submission listener. -Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for details.

+Refer to https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload/#view_submission for +details.

@@ -5845,7 +5850,7 @@

Class variables

def __call__( self, - prompts: List[Union[str, Dict[str, str]]], + prompts: Sequence[Union[str, Dict[str, str]]], title: Optional[str] = None, ) -> SlackResponse: prompts_arg: List[Dict[str, str]] = [] diff --git a/pyproject.toml b/pyproject.toml index 2d1897c4c..aab53aae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.36.0.dev6"] +dependencies = ["slack_sdk==3.36.0.dev7"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index acf1eab7f..8a11da6a7 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.26.0.dev3" +__version__ = "1.26.0.dev4" From 36ddda719779d90543d22cc07e0afca83042bdf9 Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:49:38 -0700 Subject: [PATCH 09/13] docs: adds streaming messages section to sending message page (#1384) Co-authored-by: Eden Zimbelman --- docs/english/concepts/ai-apps.md | 408 ++++++++++++++++++----- docs/english/concepts/message-sending.md | 60 +++- 2 files changed, 376 insertions(+), 92 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index b294c6688..44bd08df1 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -1,83 +1,195 @@ -# Using AI in Apps -:::info[This feature requires a paid plan] +# Using AI in Apps {#using-ai-in-apps} + +The Slack platform offers features tailored for AI agents and assistants. Your apps can [utilize the `Assistant` class](#assistant) for a side-panel view designed with AI in mind, or they can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). + +If you're unfamiliar with using these feature within Slack, you may want to read the [API documentation on the subject](/ai/). Then come back here to implement them with Bolt! + +## The `Assistant` class instance {#assistant} + +:::info[Some features within this guide require a paid plan] If you don't have a paid workspace for development, you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. ::: -The Agents & AI Apps feature comprises a unique messaging experience for Slack. If you're unfamiliar with using the Agents & AI Apps feature within Slack, you'll want to read the [API documentation on the subject](/ai/). Then come back here to implement them with Bolt! +The [`Assistant`](/tools/bolt-js/reference#the-assistantconfig-configuration-object) class can be used to handle the incoming events expected from a user interacting with an app in Slack that has the Agents & AI Apps feature enabled. -## Configuring your app to support AI features {#configuring-your-app} +A typical flow would look like: -1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. +1. [The user starts a thread](#handling-new-thread). The `Assistant` class handles the incoming [`assistant_thread_started`](/reference/events/assistant_thread_started) event. +2. [The thread context may change at any point](#handling-thread-context-changes). The `Assistant` class can handle any incoming [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events. The class also provides a default `context` store to keep track of thread context changes as the user moves through Slack. +3. [The user responds](#handling-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event. -2. Within the App Settings **OAuth & Permissions** page, add the following scopes: -* [`assistant:write`](/reference/scopes/assistant.write) -* [`chat:write`](/reference/scopes/chat.write) -* [`im:history`](/reference/scopes/im.history) -3. Within the App Settings **Event Subscriptions** page, subscribe to the following events: -* [`assistant_thread_started`](/reference/events/assistant_thread_started) -* [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) -* [`message.im`](/reference/events/message.im) +```python +assistant = Assistant() -:::info[You _could_ implement your own AI app by [listening](event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events (see implementation details below).] +# This listener is invoked when a human user opened an assistant thread +@assistant.thread_started +def start_assistant_thread( + say: Say, + get_thread_context: GetThreadContext, + set_suggested_prompts: SetSuggestedPrompts, + logger: logging.Logger, +): + try: + ... -That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! +# This listener is invoked when the human user sends a reply in the assistant thread +@assistant.user_message +def respond_in_assistant_thread( + client: WebClient, + context: BoltContext, + get_thread_context: GetThreadContext, + logger: logging.Logger, + payload: dict, + say: Say, + set_status: SetStatus, +): + try: + ... + +# Enable this assistant middleware in your Bolt app +app.use(assistant) +``` + +:::info[Consider the following] +You _could_ go it alone and [listen](/tools/bolt-python/concepts/event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events in order to implement the AI features in your app. That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! +::: + +While the `assistant_thread_started` and `assistant_thread_context_changed` events do provide Slack-client thread context information, the `message.im` event does not. Any subsequent user message events won't contain thread context data. For that reason, Bolt not only provides a way to store thread context — the `threadContextStore` property — but it also provides a `DefaultThreadContextStore` instance that is utilized by default. This implementation relies on storing and retrieving [message metadata](/messaging/message-metadata/) as the user interacts with the app. -## The `Assistant` class instance {#assistant-class} +If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. + +:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] +::: + +### Configuring your app to support the `Assistant` class {#configuring-assistant-class} + +1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. + +2. Within the App Settings **OAuth & Permissions** page, add the following scopes: + * [`assistant:write`](/reference/scopes/assistant.write) + * [`chat:write`](/reference/scopes/chat.write) + * [`im:history`](/reference/scopes/im.history) -The `Assistant` class can be used to handle the incoming events expected from a user interacting with an app in Slack that has the Agents & AI Apps feature enabled. A typical flow would look like: +3. Within the App Settings **Event Subscriptions** page, subscribe to the following events: + * [`assistant_thread_started`](/reference/events/assistant_thread_started) + * [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) + * [`message.im`](/reference/events/message.im) -1. [The user starts a thread](#handling-a-new-thread). The `Assistant` class handles the incoming [`assistant_thread_started`](/reference/events/assistant_thread_started) event. -2. [The thread context may change at any point](#handling-thread-context-changes). The `Assistant` class can handle any incoming [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events. The class also provides a default context store to keep track of thread context changes as the user moves through Slack. -3. [The user responds](#handling-the-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event. +### Handling a new thread {#handling-new-thread} +When the user opens a new thread with your AI-enabled app, the [`assistant_thread_started`](/reference/events/assistant_thread_started) event will be sent to your app. + +:::tip[When a user opens an app thread while in a channel, the channel info is stored as the thread's `AssistantThreadContext` data.] + +You can grab that info by using the `get_thread_context` utility, as subsequent user message event payloads won't include the channel info. +::: ```python assistant = Assistant() -# This listener is invoked when a human user opened an assistant thread @assistant.thread_started -def start_assistant_thread(say: Say, set_suggested_prompts: SetSuggestedPrompts): - # Send the first reply to the human who started chat with your app's assistant bot - say(":wave: Hi, how can I help you today?") - - # Setting suggested prompts is optional - set_suggested_prompts( - prompts=[ - # If the suggested prompt is long, you can use {"title": "short one to display", "message": "full prompt"} instead - "What does SLACK stand for?", - "When Slack was released?", - ], - ) +def start_assistant_thread( + say: Say, + get_thread_context: GetThreadContext, + set_suggested_prompts: SetSuggestedPrompts, + logger: logging.Logger, +): + try: + say("How can I help you?") + + prompts: List[Dict[str, str]] = [ + { + "title": "Suggest names for my Slack app", + "message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.", + }, + ] + + thread_context = get_thread_context() + if thread_context is not None and thread_context.channel_id is not None: + summarize_channel = { + "title": "Summarize the referred channel", + "message": "Can you generate a brief summary of the referred channel?", + } + prompts.append(summarize_channel) + + set_suggested_prompts(prompts=prompts) + except Exception as e: + logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) + say(f":warning: Something went wrong! ({e})") +``` + +You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info. + +### Handling thread context changes {#handling-thread-context-changes} + +When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. + +If you use the built-in `Assistant` middleware without any custom configuration, the updated context data is automatically saved as [message metadata](/messaging/message-metadata/) of the first reply from the app. + +As long as you use the built-in approach, you don't need to store the context data within a datastore. The downside of this default behavior is the overhead of additional calls to the Slack API. These calls include those to `conversations.history`, which are used to look up the stored message metadata that contains the thread context (via `get_thread_context`). + +To store context elsewhere, pass a custom `AssistantThreadContextStore` implementation to the `Assistant` constructor. We provide `FileAssistantThreadContextStore`, which is a reference implementation that uses the local file system. Since this reference implementation relies on local files, it's not advised for use in production. For production apps, we recommend creating a class that inherits `AssistantThreadContextStore`. + +```python +from slack_bolt import FileAssistantThreadContextStore +assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) +``` + +### Handling the user response {#handling-user-response} + +When the user messages your app, the [`message.im`](/reference/events/message.im) event will be sent to your app. +Messages sent to the app do not contain a [subtype](/reference/events/message#subtypes) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/). + +There are three utilities that are particularly useful in curating the user experience: +* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say) +* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle) +* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus) + +Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. + +```python # This listener is invoked when the human user sends a reply in the assistant thread @assistant.user_message def respond_in_assistant_thread( - payload: dict, - logger: logging.Logger, - context: BoltContext, - set_status: SetStatus, client: WebClient, + context: BoltContext, + get_thread_context: GetThreadContext, + logger: logging.Logger, + payload: dict, say: Say, + set_status: SetStatus, ): try: - # Tell the human user the assistant bot acknowledges the request and is working on it - set_status("is typing...") + channel_id = payload["channel"] + team_id = payload["team"] + thread_ts = payload["thread_ts"] + user_id = payload["user"] + user_message = payload["text"] + + set_status( + status="thinking...", + loading_messages=[ + "Untangling the internet cables…", + "Consulting the office goldfish…", + "Convincing the AI to stop overthinking…", + ], + ) # Collect the conversation history with this user - replies_in_thread = client.conversations_replies( + replies = client.conversations_replies( channel=context.channel_id, ts=context.thread_ts, oldest=context.thread_ts, limit=10, ) messages_in_thread: List[Dict[str, str]] = [] - for message in replies_in_thread["messages"]: + for message in replies["messages"]: role = "user" if message.get("bot_id") is None else "assistant" messages_in_thread.append({"role": role, "content": message["text"]}) - # Pass the latest prompt and chat history to the LLM (call_llm is your own code) returned_message = call_llm(messages_in_thread) # Post the result in the assistant thread @@ -93,23 +205,7 @@ def respond_in_assistant_thread( app.use(assistant) ``` -While the `assistant_thread_started` and `assistant_thread_context_changed` events do provide Slack-client thread context information, the `message.im` event does not. Any subsequent user message events won't contain thread context data. For that reason, Bolt not only provides a way to store thread context — the `threadContextStore` property — but it also provides an instance that is utilized by default. This implementation relies on storing and retrieving [message metadata](/messaging/message-metadata/) as the user interacts with the app. - -If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. - -:::tip[Refer to the [module document](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] -::: - -## Handling a new thread {#handling-a-new-thread} - -When the user opens a new thread with your AI-enabled app, the [`assistant_thread_started`](/reference/events/assistant_thread_started) event will be sent to your app. - -:::tip[When a user opens an app thread while in a channel, the channel info is stored as the thread's `AssistantThreadContext` data.] - -You can grab that info by using the `get_thread_context` utility, as subsequent user message event payloads won't include the channel info. -::: - -### Block Kit interactions in the app thread {#block-kit-interactions} +### Sending Block Kit alongside messages {#block-kit-interactions} For advanced use cases, Block Kit buttons may be used instead of suggested prompts, as well as the sending of messages with structured [metadata](/messaging/message-metadata/) to trigger subsequent interactions with the user. @@ -235,52 +331,182 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: ... ``` -## Handling thread context changes {#handling-thread-context-changes} +See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. -When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. +## Text streaming in messages {#text-streaming} -If you use the built-in `Assistant` middleware without any custom configuration, the updated context data is automatically saved as [message metadata](/messaging/message-metadata/) of the first reply from the app. +Three Web API methods work together to provide users a text streaming experience: -As long as you use the built-in approach, you don't need to store the context data within a datastore. The downside of this default behavior is the overhead of additional calls to the Slack API. These calls include those to `conversations.history`, which are used to look up the stored message metadata that contains the thread context (via `get_thread_context`). +* the [`chat.startStream`](/reference/methods/chat.startstream) method starts the text stream, +* the [`chat.appendStream`](/reference/methods/chat.appendstream) method appends text to the stream, and +* the [`chat.stopStream`](/reference/methods/chat.stopstream) method stops it. + +Since you're using Bolt for Python, built upon the Python Slack SDK, you can use the [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) utility to streamline all three aspects of streaming in your app's messages. + +The following example uses OpenAI's streaming API with the new `chat_stream()` functionality, but you can substitute it with the AI client of your choice. -To store context elsewhere, pass a custom `AssistantThreadContextStore` implementation to the `Assistant` constructor. We provide `FileAssistantThreadContextStore`, which is a reference implementation that uses the local file system. Since this reference implementation relies on local files, it's not advised for use in production. For production apps, we recommend creating a class that inherits `AssistantThreadContextStore`. ```python -from slack_bolt import FileAssistantThreadContextStore -assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) -``` +import os +from typing import List, Dict + +import openai +from openai import Stream +from openai.types.responses import ResponseStreamEvent + +DEFAULT_SYSTEM_CONTENT = """ +You're an assistant in a Slack workspace. +Users in the workspace will ask you to help them write something or to think better about a specific topic. +You'll respond to those questions in a professional way. +When you include markdown text, convert them to Slack compatible ones. +When a prompt has Slack's special syntax like <@USER_ID> or <#CHANNEL_ID>, you must keep them as-is in your response. +""" + +def call_llm( + messages_in_thread: List[Dict[str, str]], + system_content: str = DEFAULT_SYSTEM_CONTENT, +) -> Stream[ResponseStreamEvent]: + openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + messages = [{"role": "system", "content": system_content}] + messages.extend(messages_in_thread) + response = openai_client.responses.create(model="gpt-4o-mini", input=messages, stream=True) + return response + +@assistant.user_message +def respond_in_assistant_thread( + ... +): + try: + ... + replies = client.conversations_replies( + channel=context.channel_id, + ts=context.thread_ts, + oldest=context.thread_ts, + limit=10, + ) + messages_in_thread: List[Dict[str, str]] = [] + for message in replies["messages"]: + role = "user" if message.get("bot_id") is None else "assistant" + messages_in_thread.append({"role": role, "content": message["text"]}) -## Handling the user response {#handling-the-user-response} + returned_message = call_llm(messages_in_thread) -When the user messages your app, the [`message.im`](/reference/events/message.im) event will be sent to your app. + streamer = client.chat_stream( + channel=channel_id, + recipient_team_id=team_id, + recipient_user_id=user_id, + thread_ts=thread_ts, + ) -Messages sent to the app do not contain a [subtype](/reference/events/message#subtypes) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/). + # Loop over OpenAI response stream + # https://platform.openai.com/docs/api-reference/responses/create + for event in returned_message: + if event.type == "response.output_text.delta": + streamer.append(markdown_text=f"{event.delta}") + else: + continue -There are three utilities that are particularly useful in curating the user experience: -* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say) -* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle) -* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus) + streamer.stop() -```python -... -# This listener is invoked when the human user posts a reply -@assistant.user_message -def respond_to_user_messages(logger: logging.Logger, set_status: SetStatus, say: Say): - try: - set_status("is typing...") - say("Please use the buttons in the first reply instead :bow:") except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") - say(f":warning: Sorry, something went wrong during processing your request (error: {e})") + logger.exception(f"Failed to handle a user message event: {e}") + say(f":warning: Something went wrong! ({e})") +``` -# Enable this assistant middleware in your Bolt app -app.use(assistant) +## Adding and handling feedback {#adding-and-handling-feedback} + +Use the [feedback buttons block element](/reference/block-kit/block-elements/feedback-buttons-element/) to allow users to immediately provide feedback regarding your app's responses. Here's a quick example: + +```py +from typing import List +from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject + + +def create_feedback_block() -> List[Block]: + """ + Create feedback block with thumbs up/down buttons + + Returns: + Block Kit context_actions block + """ + blocks: List[Block] = [ + ContextActionsBlock( + elements=[ + FeedbackButtonsElement( + action_id="feedback", + positive_button=FeedbackButtonObject( + text="Good Response", + accessibility_label="Submit positive feedback on this response", + value="good-feedback", + ), + negative_button=FeedbackButtonObject( + text="Bad Response", + accessibility_label="Submit negative feedback on this response", + value="bad-feedback", + ), + ) + ] + ) + ] + return blocks +``` + +Use the `chat_stream` utility to render the feedback block at the bottom of your app's message. + +```js +... + streamer = client.chat_stream( + channel=channel_id, + recipient_team_id=team_id, + recipient_user_id=user_id, + thread_ts=thread_ts, + ) + + # Loop over OpenAI response stream + # https://platform.openai.com/docs/api-reference/responses/create + for event in returned_message: + if event.type == "response.output_text.delta": + streamer.append(markdown_text=f"{event.delta}") + else: + continue + + feedback_block = create_feedback_block() + streamer.stop(blocks=feedback_block) +... ``` -## Full example: Assistant Template {#full-example} +Then add a response for when the user provides feedback. + +```python +# Handle feedback buttons (thumbs up/down) +def handle_feedback(ack, body, client, logger: logging.Logger): + try: + ack() + message_ts = body["message"]["ts"] + channel_id = body["channel"]["id"] + feedback_type = body["actions"][0]["value"] + is_positive = feedback_type == "good-feedback" + + if is_positive: + client.chat_postEphemeral( + channel=channel_id, + user=body["user"]["id"], + thread_ts=message_ts, + text="We're glad you found this useful.", + ) + else: + client.chat_postEphemeral( + channel=channel_id, + user=body["user"]["id"], + thread_ts=message_ts, + text="Sorry to hear that response wasn't up to par :slightly_frowning_face: Starting a new chat may help with AI mistakes and hallucinations.", + ) + + logger.debug(f"Handled feedback: type={feedback_type}, message_ts={message_ts}") + except Exception as error: + logger.error(f":warning: Something went wrong! {error}") +``` -Below is the `assistant.py` listener file of the [Assistant Template repo](https://github.com/slack-samples/bolt-python-assistant-template) we've created for you to build off of. +## Full example: App Agent Template {#app-agent-template} -```py reference title="assistant.py" -https://github.com/slack-samples/bolt-python-assistant-template/blob/main/listeners/assistant.py -``` \ No newline at end of file +Want to see the functionality described throughout this guide in action? We've created a [App Agent Template](https://github.com/slack-samples/bolt-python-assistant-template) repo for you to build off of. diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 228a7b6b8..730af76ea 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -5,6 +5,7 @@ Within your listener function, `say()` is available whenever there is an associa In the case that you'd like to send a message outside of a listener or you want to do something more advanced (like handle specific errors), you can call `client.chat_postMessage` [using the client attached to your Bolt instance](/tools/bolt-python/concepts/web-api). Refer to [the module document](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments. + ```python # Listens for messages containing "knock knock" and responds with an italicized "who's there?" @app.message("knock knock") @@ -38,4 +39,61 @@ def show_datepicker(event, say): blocks=blocks, text="Pick a date for me to remind you" ) -``` \ No newline at end of file +``` + +## Streaming messages {#streaming-messages} + +You can have your app's messages stream in to replicate conventional AI chatbot behavior. This is done through three Web API methods: + +* [`chat_startStream`](/reference/methods/chat.startstream) +* [`chat_appendStream`](/reference/methods/chat.appendstream) +* [`chat_stopStream`](/reference/methods/chat.stopstream) + +The Python Slack SDK provides a [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility to streamline calling these methods. Here's an excerpt from our [Assistant template app](https://github.com/slack-samples/bolt-python-assistant-template) + +```python +streamer = client.chat_stream( + channel=channel_id, + recipient_team_id=team_id, + recipient_user_id=user_id, + thread_ts=thread_ts, +) + +# response from your LLM of choice; OpenAI is the example here +for event in returned_message: + if event.type == "response.output_text.delta": + streamer.append(markdown_text=f"{event.delta}") + else: + continue + +feedback_block = create_feedback_block() +streamer.stop(blocks=feedback_block) +``` + +In that example, a [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element is passed to `streamer.stop` to provide feedback buttons to the user at the bottom of the message. Interaction with these buttons will send a block action event to your app to receive the feedback. + +```python +def create_feedback_block() -> List[Block]: + blocks: List[Block] = [ + ContextActionsBlock( + elements=[ + FeedbackButtonsElement( + action_id="feedback", + positive_button=FeedbackButtonObject( + text="Good Response", + accessibility_label="Submit positive feedback on this response", + value="good-feedback", + ), + negative_button=FeedbackButtonObject( + text="Bad Response", + accessibility_label="Submit negative feedback on this response", + value="bad-feedback", + ), + ) + ] + ) + ] + return blocks +``` + +For information on calling the `chat_*Stream` API methods without the helper utility, see the [_Sending streaming messages_](/tools/python-slack-sdk/web#sending-streaming-messages) section of the Python Slack SDK docs. \ No newline at end of file From e742d14dbca24ba36090f3ec5ea3da4cb37a8e90 Mon Sep 17 00:00:00 2001 From: Maria Alejandra <104795114+srtaalej@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:42:24 -0400 Subject: [PATCH 10/13] docs: precede code snippet with standard sentence formatting (#1386) --- docs/english/concepts/message-sending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 730af76ea..3fa992e20 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -49,7 +49,7 @@ You can have your app's messages stream in to replicate conventional AI chatbot * [`chat_appendStream`](/reference/methods/chat.appendstream) * [`chat_stopStream`](/reference/methods/chat.stopstream) -The Python Slack SDK provides a [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility to streamline calling these methods. Here's an excerpt from our [Assistant template app](https://github.com/slack-samples/bolt-python-assistant-template) +The Python Slack SDK provides a [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility to streamline calling these methods. Here's an excerpt from our [Assistant template app](https://github.com/slack-samples/bolt-python-assistant-template): ```python streamer = client.chat_stream( From ceeabcfc7e816184b098ab229f3de92985ed0eb3 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Oct 2025 16:16:08 -0700 Subject: [PATCH 11/13] chore: bump slack_sdk to 3.37.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aab53aae6..f6e200009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.36.0.dev7"] +dependencies = ["slack_sdk==3.37.0"] [project.urls] From 9e5bb32844e35dd01e9763bcf03f869851e674b4 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Oct 2025 16:22:05 -0700 Subject: [PATCH 12/13] fix: use expected versions before a release Co-authored-by: Michael Brooks --- pyproject.toml | 2 +- slack_bolt/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6e200009..5361ef1b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.7" -dependencies = ["slack_sdk==3.37.0"] +dependencies = ["slack_sdk>=3.37.0,<4"] [project.urls] diff --git a/slack_bolt/version.py b/slack_bolt/version.py index 8a11da6a7..7f9c19341 100644 --- a/slack_bolt/version.py +++ b/slack_bolt/version.py @@ -1,3 +1,3 @@ """Check the latest version at https://pypi.org/project/slack-bolt/""" -__version__ = "1.26.0.dev4" +__version__ = "1.25.0" From ca89676ff65d8e17eed3c51742a41677b2751293 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Oct 2025 16:27:49 -0700 Subject: [PATCH 13/13] docs: match the sample app implementation Co-authored-by: Maria Alejandra <104795114+srtaalej@users.noreply.github.com> --- docs/english/concepts/message-sending.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 3fa992e20..9741bb396 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -59,7 +59,8 @@ streamer = client.chat_stream( thread_ts=thread_ts, ) -# response from your LLM of choice; OpenAI is the example here +# Loop over OpenAI response stream +# https://platform.openai.com/docs/api-reference/responses/create for event in returned_message: if event.type == "response.output_text.delta": streamer.append(markdown_text=f"{event.delta}")