From 8e86d265fb46ff5d2e770ee6109622ba301c7bc3 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 25 Sep 2025 10:30:04 -0700 Subject: [PATCH 01/10] adds streams --- docs/english/concepts/message-sending.md | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 228a7b6b8..53e17d48a 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,100 @@ def show_datepicker(event, say): blocks=blocks, text="Pick a date for me to remind you" ) +``` + +## Streaming messages + +You can have your app's messages stream in for those AI chatbot vibes. This is done through three methods: + +* `chat_startStream` +* `chat_appendStream` +* `chat_stopStream` + +### Starting the message stream + +First you need to begin the message stream. + +```python +# Example: Stream a response to any message +@app.message() +def handle_message(message, client): + channel_id = payload["channel"] + thread_ts = payload["thread_ts"] + + # Start a new message stream + stream_response = client.chat_startStream( + channel=channel_id, + thread_ts=thread_ts, + ) + stream_ts = stream_response["ts"] +``` + +### Appending content to the message stream + +With the stream started, you can then append text to it in chunks to convey a streaming effect. + +The structure of the text coming in will depend on your source. The following code snippet uses OpenAI's response structure as an example. + +```python +# continued from above + for event in returned_message: + if event.type == "response.output_text.delta": + client.chat_appendStream( + channel=channel_id, + ts=stream_ts, + markdown_text=f"{event.delta}" + ) + else: + continue +``` + +### Finishing the message stream + +Your app can then end the stream with the `chat_stopStream` method. + +```python +# continued from above + client.chat_stopStream( + channel=channel_id, + ts=stream_ts + ) +``` + +The method also provides you an opportunity to request user feedback on your app's responses using the [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element within the [context actions](/reference/block-kit/blocks/context-actions-block) block. The user will be presented with thumbs up and thumbs down buttons + +```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 + +@app.message() +def handle_message(message, client): + # ... previous streaming code ... + + # Stop the stream and add interactive elements + feedback_block = create_feedback_block() + client.chat_stopStream( + channel=channel_id, + ts=stream_ts, + blocks=feedback_block + ) ``` \ No newline at end of file From 17b9b9a8729cfe0a17a923baa21cee221d65aa01 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 26 Sep 2025 09:21:09 -0700 Subject: [PATCH 02/10] adds streaming --- docs/english/concepts/assistant-example.py | 139 +++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/english/concepts/assistant-example.py diff --git a/docs/english/concepts/assistant-example.py b/docs/english/concepts/assistant-example.py new file mode 100644 index 000000000..8d80187ab --- /dev/null +++ b/docs/english/concepts/assistant-example.py @@ -0,0 +1,139 @@ +import logging +from typing import List, Dict +from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts +from slack_bolt.context.get_thread_context import GetThreadContext +from slack_sdk import WebClient +from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject + +from ..llm_caller import call_llm + +# Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details +assistant = Assistant() + + +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 + + +# 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: + say("How can I help you?") + + prompts: List[Dict[str, str]] = [ + { + "title": "What does Slack stand for?", + "message": "Slack, a business communication service, was named after an acronym. Can you guess what it stands for?", + }, + { + "title": "Write a draft announcement", + "message": "Can you write a draft announcement about a new feature my team just released? It must include how impactful it is.", + }, + { + "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})") + + +# 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, + client: WebClient, + say: Say, +): + try: + channel_id = payload["channel"] + thread_ts = payload["thread_ts"] + + loading_messages = [ + "Teaching the hamsters to type faster…", + "Untangling the internet cables…", + "Consulting the office goldfish…", + "Polishing up the response just for you…", + "Convincing the AI to stop overthinking…", + ] + + 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"]}) + + returned_message = call_llm(messages_in_thread) + + client.assistant_threads_setStatus( + channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages + ) + + stream_response = client.chat_startStream( + channel=channel_id, + thread_ts=thread_ts, + ) + stream_ts = stream_response["ts"] + + # use of this for loop is specific to openai response method + for event in returned_message: + if event.type == "response.output_text.delta": + client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}") + else: + continue + + feedback_block = create_feedback_block() + client.chat_stopStream(channel=channel_id, ts=stream_ts, blocks=feedback_block) + + except Exception as e: + logger.exception(f"Failed to handle a user message event: {e}") + say(f":warning: Something went wrong! ({e})") \ No newline at end of file From 4d62d3830e6879b8baf390893d1b373bb79139fa Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 10:44:12 -0700 Subject: [PATCH 03/10] go --- docs/english/concepts/assistant-example.py | 139 --------------------- 1 file changed, 139 deletions(-) delete mode 100644 docs/english/concepts/assistant-example.py diff --git a/docs/english/concepts/assistant-example.py b/docs/english/concepts/assistant-example.py deleted file mode 100644 index 8d80187ab..000000000 --- a/docs/english/concepts/assistant-example.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -from typing import List, Dict -from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts -from slack_bolt.context.get_thread_context import GetThreadContext -from slack_sdk import WebClient -from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject - -from ..llm_caller import call_llm - -# Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details -assistant = Assistant() - - -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 - - -# 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: - say("How can I help you?") - - prompts: List[Dict[str, str]] = [ - { - "title": "What does Slack stand for?", - "message": "Slack, a business communication service, was named after an acronym. Can you guess what it stands for?", - }, - { - "title": "Write a draft announcement", - "message": "Can you write a draft announcement about a new feature my team just released? It must include how impactful it is.", - }, - { - "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})") - - -# 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, - client: WebClient, - say: Say, -): - try: - channel_id = payload["channel"] - thread_ts = payload["thread_ts"] - - loading_messages = [ - "Teaching the hamsters to type faster…", - "Untangling the internet cables…", - "Consulting the office goldfish…", - "Polishing up the response just for you…", - "Convincing the AI to stop overthinking…", - ] - - 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"]}) - - returned_message = call_llm(messages_in_thread) - - client.assistant_threads_setStatus( - channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages - ) - - stream_response = client.chat_startStream( - channel=channel_id, - thread_ts=thread_ts, - ) - stream_ts = stream_response["ts"] - - # use of this for loop is specific to openai response method - for event in returned_message: - if event.type == "response.output_text.delta": - client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}") - else: - continue - - feedback_block = create_feedback_block() - client.chat_stopStream(channel=channel_id, ts=stream_ts, blocks=feedback_block) - - except Exception as e: - logger.exception(f"Failed to handle a user message event: {e}") - say(f":warning: Something went wrong! ({e})") \ No newline at end of file From b59950dfeda562ae97e2147528490eec317877e4 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 11:31:51 -0700 Subject: [PATCH 04/10] go --- docs/english/concepts/message-sending.md | 88 +++++++----------------- 1 file changed, 24 insertions(+), 64 deletions(-) diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 53e17d48a..a44d3c5c8 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -41,65 +41,35 @@ def show_datepicker(event, say): ) ``` -## Streaming messages +## Streaming messages {#streaming-messages} -You can have your app's messages stream in for those AI chatbot vibes. This is done through three methods: +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` -* `chat_appendStream` -* `chat_stopStream` +* [`chat_startStream`](/reference/methods/chat.startstream) +* [`chat_appendStream`](/reference/methods/chat.appendstream) +* [`chat_stopStream`](/reference/methods/chat.stopstream) -### Starting the message stream - -First you need to begin the message stream. +The Python Slack SDK provides a [`chat_stream()`](https://docss.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/blob/main/listeners/assistant/assistant.py) ```python -# Example: Stream a response to any message -@app.message() -def handle_message(message, client): - channel_id = payload["channel"] - thread_ts = payload["thread_ts"] - - # Start a new message stream - stream_response = client.chat_startStream( - channel=channel_id, - thread_ts=thread_ts, - ) - stream_ts = stream_response["ts"] +streamer = client.chat_stream( + channel=channel_id, + recipient_team_id=team_id, + recipient_user_id=user_id, + thread_ts=thread_ts, +) + +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) ``` -### Appending content to the message stream - -With the stream started, you can then append text to it in chunks to convey a streaming effect. - -The structure of the text coming in will depend on your source. The following code snippet uses OpenAI's response structure as an example. - -```python -# continued from above - for event in returned_message: - if event.type == "response.output_text.delta": - client.chat_appendStream( - channel=channel_id, - ts=stream_ts, - markdown_text=f"{event.delta}" - ) - else: - continue -``` - -### Finishing the message stream - -Your app can then end the stream with the `chat_stopStream` method. - -```python -# continued from above - client.chat_stopStream( - channel=channel_id, - ts=stream_ts - ) -``` - -The method also provides you an opportunity to request user feedback on your app's responses using the [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element within the [context actions](/reference/block-kit/blocks/context-actions-block) block. The user will be presented with thumbs up and thumbs down buttons +In that example, a [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element is passed to `streamer.stop` — this provides feedback buttons to the user at the bottom of the message. ```python def create_feedback_block() -> List[Block]: @@ -123,16 +93,6 @@ def create_feedback_block() -> List[Block]: ) ] return blocks +``` -@app.message() -def handle_message(message, client): - # ... previous streaming code ... - - # Stop the stream and add interactive elements - feedback_block = create_feedback_block() - client.chat_stopStream( - channel=channel_id, - ts=stream_ts, - blocks=feedback_block - ) -``` \ No newline at end of file +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 9712cfaeb49db8bc8442b023856b82f623f2e855 Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:54:43 -0700 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Eden Zimbelman --- docs/english/concepts/message-sending.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index a44d3c5c8..f1f9939b0 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://docss.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/blob/main/listeners/assistant/assistant.py) +The Python Slack SDK provides a [`chat_stream()`](/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( @@ -69,7 +69,7 @@ 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` — this provides feedback buttons to the user at the bottom of the message. +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]: From e0085536e69c7d31975447f3491ee0c554e063be Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 12:55:34 -0700 Subject: [PATCH 06/10] feedback --- 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 f1f9939b0..730af76ea 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()`](/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( @@ -59,6 +59,7 @@ streamer = client.chat_stream( 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}") From 96cbbb4a038252e89f67b6490a6706f0abf72097 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 14:44:24 -0700 Subject: [PATCH 07/10] updates ai apps page --- docs/english/concepts/ai-apps.md | 424 +++++++++++++++++++++++-------- 1 file changed, 325 insertions(+), 99 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index b294c6688..2204439c4 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] +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. + + +```python +assistant = Assistant() + +# 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: + ... + +# 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: + ... -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) +# Enable this assistant middleware in your Bolt app +app.use(assistant) +``` -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) +:::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! +::: -:::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).] +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. -That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! +If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. -## The `Assistant` class instance {#assistant-class} +:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] +::: -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: +### Configuring your app to support the `Assistant` class -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. +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) + +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) + +### 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-the-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,183 @@ 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. +## Text streaming in messages {#text-streaming} -If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. +Three Web API methods work together to provide users a text streaming experience: -:::tip[Refer to the [module document](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] -::: +* 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. -## Handling a new thread {#handling-a-new-thread} +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. -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. +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. -:::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 +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"]}) + + returned_message = call_llm(messages_in_thread) + + streamer = client.chat_stream( + channel=channel_id, + recipient_team_id=team_id, + recipient_user_id=user_id, + thread_ts=thread_ts, + ) -### Block Kit interactions in the app thread {#block-kit-interactions} + # 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 + + streamer.stop() + + except Exception as e: + logger.exception(f"Failed to handle a user message event: {e}") + say(f":warning: Something went wrong! ({e})") +``` + +## 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) +... +``` + +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}") +``` + +Keep reading for more Block Kit possibilities for your AI-enabled app. + +## 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 +507,6 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: ... ``` -## 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-the-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) - -```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})") - -# Enable this assistant middleware in your Bolt app -app.use(assistant) -``` - -## Full example: Assistant Template {#full-example} - -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 -```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. From a23f455b88f13077e26fbf40b198ee5be94eb115 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 14:57:11 -0700 Subject: [PATCH 08/10] header misalign --- docs/english/concepts/Untitled-1.md | 414 ++++++++++++++++++++++++++++ docs/english/concepts/ai-apps.md | 2 +- 2 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 docs/english/concepts/Untitled-1.md diff --git a/docs/english/concepts/Untitled-1.md b/docs/english/concepts/Untitled-1.md new file mode 100644 index 000000000..98b55feab --- /dev/null +++ b/docs/english/concepts/Untitled-1.md @@ -0,0 +1,414 @@ + +# 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 [`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. + +A typical flow would look like: + +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. + +```js +const assistant = new Assistant({ + /** + * (Recommended) A custom ThreadContextStore can be provided, inclusive of methods to + * get and save thread context. When provided, these methods will override the `getThreadContext` + * and `saveThreadContext` utilities that are made available in other Assistant event listeners. + */ + // threadContextStore: { + // get: async ({ context, client, payload }) => {}, + // save: async ({ context, client, payload }) => {}, + // }, + + /** + * `assistant_thread_started` is sent when a user opens the Assistant container. + * This can happen via DM with the app or as a side-container within a channel. + */ + threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => {}, + + /** + * `assistant_thread_context_changed` is sent when a user switches channels + * while the Assistant container is open. If `threadContextChanged` is not + * provided, context will be saved using the AssistantContextStore's `save` + * method (either the DefaultAssistantContextStore or custom, if provided). + */ + threadContextChanged: async ({ logger, saveThreadContext }) => {}, + + /** + * Messages sent from the user to the Assistant are handled in this listener. + */ + userMessage: async ({ client, context, logger, message, getThreadContext, say, setTitle, setStatus }) => {}, +}); +``` + +:::info[Consider the following] +You _could_ go it alone and [listen](/tools/bolt-js/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. + +If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. + +:::tip[Be sure to give the [reference docs](/tools/bolt-js/reference#agents--assistants) a look!] +::: + +### Configuring your app to support the `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) + +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) + +### 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. Capture this with the `threadStarted` handler to allow your app to respond. + +In the example below, the app is sending a message — containing thread context [message metadata](/messaging/message-metadata/) behind the scenes — to the user, along with a single [prompt](/reference/methods/assistant.threads.setSuggestedPrompts). + +```js +... +threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => { + const { context } = event.assistant_thread; + + try { + /** + * Since context is not sent along with individual user messages, it's necessary to keep + * track of the context of the conversation to better assist the user. Sending an initial + * message to the user with context metadata facilitates this, and allows us to update it + * whenever the user changes context (via the `assistant_thread_context_changed` event). + * The `say` utility sends this metadata along automatically behind the scenes. + * !! Please note: this is only intended for development and demonstrative purposes. + */ + await say('Hi, how can I help?'); + + await saveThreadContext(); + + /** + * Provide the user up to 4 optional, preset prompts to choose from. + * + * The first `title` prop is an optional label above the prompts that + * defaults to 'Try these prompts:' if not provided. + */ + if (!context.channel_id) { + await setSuggestedPrompts({ + title: 'Start with this suggested prompt:', + prompts: [ + { + title: 'This is a suggested prompt', + message: + 'When a user clicks a prompt, the resulting prompt message text ' + + 'can be passed directly to your LLM for processing.\n\n' + + 'Assistant, please create some helpful prompts I can provide to ' + + 'my users.', + }, + ], + }); + } + + /** + * If the user opens the Assistant container in a channel, additional + * context is available. This can be used to provide conditional prompts + * that only make sense to appear in that context. + */ + if (context.channel_id) { + await setSuggestedPrompts({ + title: 'Perform an action based on the channel', + prompts: [ + { + title: 'Summarize channel', + message: 'Assistant, please summarize the activity in this channel!', + }, + ], + }); + } + } catch (e) { + logger.error(e); + } + }, +... +``` + +:::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 using the `getThreadContext()` utility, as subsequent user message event payloads won't include the channel 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. Capture this with the `threadContextChanged` handler. + +```js +... + threadContextChanged: async ({ saveThreadContext }) => { + await saveThreadContext(); + }, +... +``` + +If you use the built-in `AssistantThreadContextStore` without any custom configuration, you can skip this — the updated thread context data is automatically saved as [message metadata](/messaging/message-metadata/) on the first reply from the app. + +### 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. Capture this with the `userMessage` handler. + +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](/tools/bolt-js/reference#the-assistantconfig-configuration-object) that are particularly useful in curating the user experience: +* `say` +* `setTitle` +* `setStatus` + +Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. + + +```js +// LLM system prompt +const 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.`; +... +const assistant = new Assistant({ + ... + userMessage: async ({ client, context, logger, message, getThreadContext, say, setTitle, setStatus }) => { + /** + * Messages sent to the Assistant can have a specific message subtype. + * + * Here we check that the message has "text" and was sent to a thread to + * skip unexpected message subtypes. + */ + if (!('text' in message) || !('thread_ts' in message) || !message.text || !message.thread_ts) { + return; + } + const { channel, thread_ts } = message; + const { userId, teamId } = context; + + try { + /** + * Set the title of the Assistant thread to capture the initial topic/question + * as a way to facilitate future reference by the user. + */ + await setTitle(message.text); + + /** + * Set the status of the Assistant to give the appearance of active processing. + */ + await setStatus({ + status: 'thinking...', + loading_messages: [ + 'Teaching the hamsters to type faster…', + 'Untangling the internet cables…', + 'Consulting the office goldfish…', + 'Polishing up the response just for you…', + 'Convincing the AI to stop overthinking…', + ], + }); +``` + +The following example uses OpenAI but you can substitute it with the LLM provider of your choice. + +```js + ... + // Retrieve the Assistant thread history for context of question being asked + const thread = await client.conversations.replies({ + channel, + ts: thread_ts, + oldest: thread_ts, + }); + + // Prepare and tag each message for LLM processing + const threadHistory = thread.messages.map((m) => { + const role = m.bot_id ? 'Assistant' : 'User'; + return `${role}: ${m.text || ''}`; + }); + // parsed threadHistory to align with openai.responses api input format + const parsedThreadHistory = threadHistory.join('\n'); + + // Send message history and newest question to LLM + const llmResponse = await openai.responses.create({ + model: 'gpt-4o-mini', + input: `System: ${DEFAULT_SYSTEM_CONTENT}\n\n${parsedThreadHistory}\nUser: ${message.text}` + }); + + // Provide a response to the user + await say({ markdown_text: llmResponse.choices[0].message.content }); + } catch (e) { + logger.error(e); + + // Send message to advise user and clear processing status if a failure occurs + await say({ text: 'Sorry, something went wrong!' }); + } + }, +}); + +app.assistant(assistant); +... +``` + +## Text streaming in messages {#text-streaming} + +Three Web API methods work together to provide users a text streaming experience: + +* 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 JS, built upon the Node Slack SDK, you can use the [`chatStream()`](/tools/node-slack-sdk/reference/web-api/classes/WebClient#chatstream) utility to streamline all three aspects of streaming in your app's messages. + +The following example uses OpenAI's streaming API with the new `chatStream` functionality, but you can substitute it with the AI client of your choice. + +```js +... + // Send message history and newest question to LLM + const llmResponse = await openai.responses.create({ + model: 'gpt-4o-mini', + input: `System: ${DEFAULT_SYSTEM_CONTENT}\n\n${parsedThreadHistory}\nUser: ${message.text}`, + stream: true, + }); + + const streamer = client.chatStream({ + channel: channel, + recipient_team_id: teamId, + recipient_user_id: userId, + thread_ts: thread_ts, + }); + + for await (const chunk of llmResponse) { + if (chunk.type === 'response.output_text.delta') { + await streamer.append({ + markdown_text: chunk.delta, + }); + } + } + await streamer.stop(); + } catch (e) { + logger.error(e); + + // Send message to advise user and clear processing status if a failure occurs + await say({ text: `Sorry, something went wrong! ${e}` }); + } + }, +}); +... +``` + +## 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: + +```js +const feedbackBlock = { + type: 'context_actions', + elements: [ + { + type: 'feedback_buttons', + action_id: 'feedback', + positive_button: { + text: { type: 'plain_text', text: 'Good Response' }, + accessibility_label: 'Submit positive feedback on this response', + value: 'good-feedback', + }, + negative_button: { + text: { type: 'plain_text', text: 'Bad Response' }, + accessibility_label: 'Submit negative feedback on this response', + value: 'bad-feedback', + }, + }, + ], +}; +``` + +Use the `chatStream` utility to render the feedback block at the bottom of your app's message. + +```js +... +// Provide a response to the user +const streamer = client.chatStream({ + channel: channel, + recipient_team_id: teamId, + recipient_user_id: userId, + thread_ts: thread_ts, +}); + +// Feed-in stream from LLM +for await (const chunk of llmResponse) { + if (chunk.type === 'response.output_text.delta') { + await streamer.append({ + markdown_text: chunk.delta, + }); + } +} + +// End stream and provide feedback buttons to user +await streamer.stop({ blocks: [feedbackBlock] }); +return; +... +``` + +Then add a response for when the user provides feedback. + +```js +/** + * Handle feedback button interactions + */ +app.action('feedback', async ({ ack, body, client, logger }) => { + try { + await ack(); + + if (body.type !== 'block_actions') { + return; + } + + const message_ts = body.message.ts; + const channel_id = body.channel.id; + const user_id = body.user.id; + + const feedback_type = body.actions[0]; + if (!('value' in feedback_type)) { + return; + } + + const is_positive = feedback_type.value === 'good-feedback'; + if (is_positive) { + await client.chat.postEphemeral({ + channel: channel_id, + user: user_id, + thread_ts: message_ts, + text: "We're glad you found this useful.", + }); + } else { + await client.chat.postEphemeral({ + channel: channel_id, + user: 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.", + }); + } + } catch (error) { + logger.error(`:warning: Something went wrong! ${error}`); + } +}); +``` + +## Full example: App Agent Template + +Want to see all the functionality described above in action? We've created a [App Agent Template](https://github.com/slack-samples/bolt-js-assistant-template) repo for you to build off of. diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 2204439c4..0fbed8c0b 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -137,7 +137,7 @@ from slack_bolt import FileAssistantThreadContextStore assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) ``` -## Handling the user response {#handling-the-user-response} +## 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. From e5bf232be6a44b4e31874056dc87e4a00add9f6e Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 14:57:28 -0700 Subject: [PATCH 09/10] oops --- docs/english/concepts/Untitled-1.md | 414 ---------------------------- 1 file changed, 414 deletions(-) delete mode 100644 docs/english/concepts/Untitled-1.md diff --git a/docs/english/concepts/Untitled-1.md b/docs/english/concepts/Untitled-1.md deleted file mode 100644 index 98b55feab..000000000 --- a/docs/english/concepts/Untitled-1.md +++ /dev/null @@ -1,414 +0,0 @@ - -# 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 [`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. - -A typical flow would look like: - -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. - -```js -const assistant = new Assistant({ - /** - * (Recommended) A custom ThreadContextStore can be provided, inclusive of methods to - * get and save thread context. When provided, these methods will override the `getThreadContext` - * and `saveThreadContext` utilities that are made available in other Assistant event listeners. - */ - // threadContextStore: { - // get: async ({ context, client, payload }) => {}, - // save: async ({ context, client, payload }) => {}, - // }, - - /** - * `assistant_thread_started` is sent when a user opens the Assistant container. - * This can happen via DM with the app or as a side-container within a channel. - */ - threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => {}, - - /** - * `assistant_thread_context_changed` is sent when a user switches channels - * while the Assistant container is open. If `threadContextChanged` is not - * provided, context will be saved using the AssistantContextStore's `save` - * method (either the DefaultAssistantContextStore or custom, if provided). - */ - threadContextChanged: async ({ logger, saveThreadContext }) => {}, - - /** - * Messages sent from the user to the Assistant are handled in this listener. - */ - userMessage: async ({ client, context, logger, message, getThreadContext, say, setTitle, setStatus }) => {}, -}); -``` - -:::info[Consider the following] -You _could_ go it alone and [listen](/tools/bolt-js/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. - -If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. - -:::tip[Be sure to give the [reference docs](/tools/bolt-js/reference#agents--assistants) a look!] -::: - -### Configuring your app to support the `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) - -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) - -### 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. Capture this with the `threadStarted` handler to allow your app to respond. - -In the example below, the app is sending a message — containing thread context [message metadata](/messaging/message-metadata/) behind the scenes — to the user, along with a single [prompt](/reference/methods/assistant.threads.setSuggestedPrompts). - -```js -... -threadStarted: async ({ event, logger, say, setSuggestedPrompts, saveThreadContext }) => { - const { context } = event.assistant_thread; - - try { - /** - * Since context is not sent along with individual user messages, it's necessary to keep - * track of the context of the conversation to better assist the user. Sending an initial - * message to the user with context metadata facilitates this, and allows us to update it - * whenever the user changes context (via the `assistant_thread_context_changed` event). - * The `say` utility sends this metadata along automatically behind the scenes. - * !! Please note: this is only intended for development and demonstrative purposes. - */ - await say('Hi, how can I help?'); - - await saveThreadContext(); - - /** - * Provide the user up to 4 optional, preset prompts to choose from. - * - * The first `title` prop is an optional label above the prompts that - * defaults to 'Try these prompts:' if not provided. - */ - if (!context.channel_id) { - await setSuggestedPrompts({ - title: 'Start with this suggested prompt:', - prompts: [ - { - title: 'This is a suggested prompt', - message: - 'When a user clicks a prompt, the resulting prompt message text ' + - 'can be passed directly to your LLM for processing.\n\n' + - 'Assistant, please create some helpful prompts I can provide to ' + - 'my users.', - }, - ], - }); - } - - /** - * If the user opens the Assistant container in a channel, additional - * context is available. This can be used to provide conditional prompts - * that only make sense to appear in that context. - */ - if (context.channel_id) { - await setSuggestedPrompts({ - title: 'Perform an action based on the channel', - prompts: [ - { - title: 'Summarize channel', - message: 'Assistant, please summarize the activity in this channel!', - }, - ], - }); - } - } catch (e) { - logger.error(e); - } - }, -... -``` - -:::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 using the `getThreadContext()` utility, as subsequent user message event payloads won't include the channel 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. Capture this with the `threadContextChanged` handler. - -```js -... - threadContextChanged: async ({ saveThreadContext }) => { - await saveThreadContext(); - }, -... -``` - -If you use the built-in `AssistantThreadContextStore` without any custom configuration, you can skip this — the updated thread context data is automatically saved as [message metadata](/messaging/message-metadata/) on the first reply from the app. - -### 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. Capture this with the `userMessage` handler. - -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](/tools/bolt-js/reference#the-assistantconfig-configuration-object) that are particularly useful in curating the user experience: -* `say` -* `setTitle` -* `setStatus` - -Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. - - -```js -// LLM system prompt -const 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.`; -... -const assistant = new Assistant({ - ... - userMessage: async ({ client, context, logger, message, getThreadContext, say, setTitle, setStatus }) => { - /** - * Messages sent to the Assistant can have a specific message subtype. - * - * Here we check that the message has "text" and was sent to a thread to - * skip unexpected message subtypes. - */ - if (!('text' in message) || !('thread_ts' in message) || !message.text || !message.thread_ts) { - return; - } - const { channel, thread_ts } = message; - const { userId, teamId } = context; - - try { - /** - * Set the title of the Assistant thread to capture the initial topic/question - * as a way to facilitate future reference by the user. - */ - await setTitle(message.text); - - /** - * Set the status of the Assistant to give the appearance of active processing. - */ - await setStatus({ - status: 'thinking...', - loading_messages: [ - 'Teaching the hamsters to type faster…', - 'Untangling the internet cables…', - 'Consulting the office goldfish…', - 'Polishing up the response just for you…', - 'Convincing the AI to stop overthinking…', - ], - }); -``` - -The following example uses OpenAI but you can substitute it with the LLM provider of your choice. - -```js - ... - // Retrieve the Assistant thread history for context of question being asked - const thread = await client.conversations.replies({ - channel, - ts: thread_ts, - oldest: thread_ts, - }); - - // Prepare and tag each message for LLM processing - const threadHistory = thread.messages.map((m) => { - const role = m.bot_id ? 'Assistant' : 'User'; - return `${role}: ${m.text || ''}`; - }); - // parsed threadHistory to align with openai.responses api input format - const parsedThreadHistory = threadHistory.join('\n'); - - // Send message history and newest question to LLM - const llmResponse = await openai.responses.create({ - model: 'gpt-4o-mini', - input: `System: ${DEFAULT_SYSTEM_CONTENT}\n\n${parsedThreadHistory}\nUser: ${message.text}` - }); - - // Provide a response to the user - await say({ markdown_text: llmResponse.choices[0].message.content }); - } catch (e) { - logger.error(e); - - // Send message to advise user and clear processing status if a failure occurs - await say({ text: 'Sorry, something went wrong!' }); - } - }, -}); - -app.assistant(assistant); -... -``` - -## Text streaming in messages {#text-streaming} - -Three Web API methods work together to provide users a text streaming experience: - -* 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 JS, built upon the Node Slack SDK, you can use the [`chatStream()`](/tools/node-slack-sdk/reference/web-api/classes/WebClient#chatstream) utility to streamline all three aspects of streaming in your app's messages. - -The following example uses OpenAI's streaming API with the new `chatStream` functionality, but you can substitute it with the AI client of your choice. - -```js -... - // Send message history and newest question to LLM - const llmResponse = await openai.responses.create({ - model: 'gpt-4o-mini', - input: `System: ${DEFAULT_SYSTEM_CONTENT}\n\n${parsedThreadHistory}\nUser: ${message.text}`, - stream: true, - }); - - const streamer = client.chatStream({ - channel: channel, - recipient_team_id: teamId, - recipient_user_id: userId, - thread_ts: thread_ts, - }); - - for await (const chunk of llmResponse) { - if (chunk.type === 'response.output_text.delta') { - await streamer.append({ - markdown_text: chunk.delta, - }); - } - } - await streamer.stop(); - } catch (e) { - logger.error(e); - - // Send message to advise user and clear processing status if a failure occurs - await say({ text: `Sorry, something went wrong! ${e}` }); - } - }, -}); -... -``` - -## 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: - -```js -const feedbackBlock = { - type: 'context_actions', - elements: [ - { - type: 'feedback_buttons', - action_id: 'feedback', - positive_button: { - text: { type: 'plain_text', text: 'Good Response' }, - accessibility_label: 'Submit positive feedback on this response', - value: 'good-feedback', - }, - negative_button: { - text: { type: 'plain_text', text: 'Bad Response' }, - accessibility_label: 'Submit negative feedback on this response', - value: 'bad-feedback', - }, - }, - ], -}; -``` - -Use the `chatStream` utility to render the feedback block at the bottom of your app's message. - -```js -... -// Provide a response to the user -const streamer = client.chatStream({ - channel: channel, - recipient_team_id: teamId, - recipient_user_id: userId, - thread_ts: thread_ts, -}); - -// Feed-in stream from LLM -for await (const chunk of llmResponse) { - if (chunk.type === 'response.output_text.delta') { - await streamer.append({ - markdown_text: chunk.delta, - }); - } -} - -// End stream and provide feedback buttons to user -await streamer.stop({ blocks: [feedbackBlock] }); -return; -... -``` - -Then add a response for when the user provides feedback. - -```js -/** - * Handle feedback button interactions - */ -app.action('feedback', async ({ ack, body, client, logger }) => { - try { - await ack(); - - if (body.type !== 'block_actions') { - return; - } - - const message_ts = body.message.ts; - const channel_id = body.channel.id; - const user_id = body.user.id; - - const feedback_type = body.actions[0]; - if (!('value' in feedback_type)) { - return; - } - - const is_positive = feedback_type.value === 'good-feedback'; - if (is_positive) { - await client.chat.postEphemeral({ - channel: channel_id, - user: user_id, - thread_ts: message_ts, - text: "We're glad you found this useful.", - }); - } else { - await client.chat.postEphemeral({ - channel: channel_id, - user: 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.", - }); - } - } catch (error) { - logger.error(`:warning: Something went wrong! ${error}`); - } -}); -``` - -## Full example: App Agent Template - -Want to see all the functionality described above in action? We've created a [App Agent Template](https://github.com/slack-samples/bolt-js-assistant-template) repo for you to build off of. From 221397a1c1a048742eef65e9bbec107944b206d1 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Oct 2025 15:01:24 -0700 Subject: [PATCH 10/10] moving it around --- docs/english/concepts/ai-apps.md | 268 +++++++++++++++---------------- 1 file changed, 134 insertions(+), 134 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 0fbed8c0b..44bd08df1 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -1,5 +1,5 @@ -# Using AI in Apps +# 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). @@ -63,7 +63,7 @@ If you do provide your own `threadContextStore` property, it must feature `get` :::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 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. @@ -122,7 +122,7 @@ def start_assistant_thread( 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} +### 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. @@ -137,7 +137,7 @@ from slack_bolt import FileAssistantThreadContextStore assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) ``` -## Handling the user response {#handling-user-response} +### 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. @@ -205,6 +205,134 @@ def respond_in_assistant_thread( app.use(assistant) ``` +### 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. + +For example, an app can display a button such as "Summarize the referring channel" in the initial reply. When the user clicks the button and submits detailed information (such as the number of messages, days to check, purpose of the summary, etc.), the app can handle that information and post a message that describes the request with structured metadata. + +By default, apps can't respond to their own bot messages (Bolt prevents infinite loops by default). However, if you pass `ignoring_self_assistant_message_events_enabled=False` to the `App` constructor and add a `bot_message` listener to your `Assistant` middleware, your app can continue processing the request as shown below: + +```python +app = App( + token=os.environ["SLACK_BOT_TOKEN"], + # This must be set to handle bot message events + ignoring_self_assistant_message_events_enabled=False, +) + +assistant = Assistant() + +@assistant.thread_started +def start_assistant_thread(say: Say): + say( + text=":wave: Hi, how can I help you today?", + blocks=[ + { + "type": "section", + "text": {"type": "mrkdwn", "text": ":wave: Hi, how can I help you today?"}, + }, + { + "type": "actions", + "elements": [ + # You can have multiple buttons here + { + "type": "button", + "action_id": "assistant-generate-random-numbers", + "text": {"type": "plain_text", "text": "Generate random numbers"}, + "value": "clicked", + }, + ], + }, + ], + ) + +# This listener is invoked when the above button is clicked +@app.action("assistant-generate-random-numbers") +def configure_random_number_generation(ack: Ack, client: WebClient, body: dict): + ack() + client.views_open( + trigger_id=body["trigger_id"], + view={ + "type": "modal", + "callback_id": "configure_assistant_summarize_channel", + "title": {"type": "plain_text", "text": "My Assistant"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + # Relay the assistant thread information to app.view listener + "private_metadata": json.dumps( + { + "channel_id": body["channel"]["id"], + "thread_ts": body["message"]["thread_ts"], + } + ), + "blocks": [ + { + "type": "input", + "block_id": "num", + "label": {"type": "plain_text", "text": "# of outputs"}, + # You can have this kind of predefined input from a user instead of parsing human text + "element": { + "type": "static_select", + "action_id": "input", + "placeholder": {"type": "plain_text", "text": "How many numbers do you need?"}, + "options": [ + {"text": {"type": "plain_text", "text": "5"}, "value": "5"}, + {"text": {"type": "plain_text", "text": "10"}, "value": "10"}, + {"text": {"type": "plain_text", "text": "20"}, "value": "20"}, + ], + "initial_option": {"text": {"type": "plain_text", "text": "5"}, "value": "5"}, + }, + } + ], + }, + ) + +# This listener is invoked when the above modal is submitted +@app.view("configure_assistant_summarize_channel") +def receive_random_number_generation_details(ack: Ack, client: WebClient, payload: dict): + ack() + num = payload["state"]["values"]["num"]["input"]["selected_option"]["value"] + thread = json.loads(payload["private_metadata"]) + + # Post a bot message with structured input data + # The following assistant.bot_message will continue processing + # If you prefer processing this request within this listener, it also works! + # If you don't need bot_message listener, no need to set ignoring_self_assistant_message_events_enabled=False + client.chat_postMessage( + channel=thread["channel_id"], + thread_ts=thread["thread_ts"], + text=f"OK, you need {num} numbers. I will generate it shortly!", + metadata={ + "event_type": "assistant-generate-random-numbers", + "event_payload": {"num": int(num)}, + }, + ) + +# This listener is invoked whenever your app's bot user posts a message +@assistant.bot_message +def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: Say, payload: dict): + try: + if payload.get("metadata", {}).get("event_type") == "assistant-generate-random-numbers": + # Handle the above random-number-generation request + set_status("is generating an array of random numbers...") + time.sleep(1) + nums: Set[str] = set() + num = payload["metadata"]["event_payload"]["num"] + while len(nums) < num: + nums.add(str(random.randint(1, 100))) + say(f"Here you are: {', '.join(nums)}") + else: + # nothing to do for this bot message + # If you want to add more patterns here, be careful not to cause infinite loop messaging + pass + + except Exception as e: + logger.exception(f"Failed to respond to an inquiry: {e}") +... +``` + +See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. + ## Text streaming in messages {#text-streaming} Three Web API methods work together to provide users a text streaming experience: @@ -285,7 +413,7 @@ def respond_in_assistant_thread( say(f":warning: Something went wrong! ({e})") ``` -## Adding and handling feedback +## 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: @@ -379,134 +507,6 @@ def handle_feedback(ack, body, client, logger: logging.Logger): logger.error(f":warning: Something went wrong! {error}") ``` -Keep reading for more Block Kit possibilities for your AI-enabled app. - -## 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. - -For example, an app can display a button such as "Summarize the referring channel" in the initial reply. When the user clicks the button and submits detailed information (such as the number of messages, days to check, purpose of the summary, etc.), the app can handle that information and post a message that describes the request with structured metadata. - -By default, apps can't respond to their own bot messages (Bolt prevents infinite loops by default). However, if you pass `ignoring_self_assistant_message_events_enabled=False` to the `App` constructor and add a `bot_message` listener to your `Assistant` middleware, your app can continue processing the request as shown below: - -```python -app = App( - token=os.environ["SLACK_BOT_TOKEN"], - # This must be set to handle bot message events - ignoring_self_assistant_message_events_enabled=False, -) - -assistant = Assistant() - -@assistant.thread_started -def start_assistant_thread(say: Say): - say( - text=":wave: Hi, how can I help you today?", - blocks=[ - { - "type": "section", - "text": {"type": "mrkdwn", "text": ":wave: Hi, how can I help you today?"}, - }, - { - "type": "actions", - "elements": [ - # You can have multiple buttons here - { - "type": "button", - "action_id": "assistant-generate-random-numbers", - "text": {"type": "plain_text", "text": "Generate random numbers"}, - "value": "clicked", - }, - ], - }, - ], - ) - -# This listener is invoked when the above button is clicked -@app.action("assistant-generate-random-numbers") -def configure_random_number_generation(ack: Ack, client: WebClient, body: dict): - ack() - client.views_open( - trigger_id=body["trigger_id"], - view={ - "type": "modal", - "callback_id": "configure_assistant_summarize_channel", - "title": {"type": "plain_text", "text": "My Assistant"}, - "submit": {"type": "plain_text", "text": "Submit"}, - "close": {"type": "plain_text", "text": "Cancel"}, - # Relay the assistant thread information to app.view listener - "private_metadata": json.dumps( - { - "channel_id": body["channel"]["id"], - "thread_ts": body["message"]["thread_ts"], - } - ), - "blocks": [ - { - "type": "input", - "block_id": "num", - "label": {"type": "plain_text", "text": "# of outputs"}, - # You can have this kind of predefined input from a user instead of parsing human text - "element": { - "type": "static_select", - "action_id": "input", - "placeholder": {"type": "plain_text", "text": "How many numbers do you need?"}, - "options": [ - {"text": {"type": "plain_text", "text": "5"}, "value": "5"}, - {"text": {"type": "plain_text", "text": "10"}, "value": "10"}, - {"text": {"type": "plain_text", "text": "20"}, "value": "20"}, - ], - "initial_option": {"text": {"type": "plain_text", "text": "5"}, "value": "5"}, - }, - } - ], - }, - ) - -# This listener is invoked when the above modal is submitted -@app.view("configure_assistant_summarize_channel") -def receive_random_number_generation_details(ack: Ack, client: WebClient, payload: dict): - ack() - num = payload["state"]["values"]["num"]["input"]["selected_option"]["value"] - thread = json.loads(payload["private_metadata"]) - - # Post a bot message with structured input data - # The following assistant.bot_message will continue processing - # If you prefer processing this request within this listener, it also works! - # If you don't need bot_message listener, no need to set ignoring_self_assistant_message_events_enabled=False - client.chat_postMessage( - channel=thread["channel_id"], - thread_ts=thread["thread_ts"], - text=f"OK, you need {num} numbers. I will generate it shortly!", - metadata={ - "event_type": "assistant-generate-random-numbers", - "event_payload": {"num": int(num)}, - }, - ) - -# This listener is invoked whenever your app's bot user posts a message -@assistant.bot_message -def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: Say, payload: dict): - try: - if payload.get("metadata", {}).get("event_type") == "assistant-generate-random-numbers": - # Handle the above random-number-generation request - set_status("is generating an array of random numbers...") - time.sleep(1) - nums: Set[str] = set() - num = payload["metadata"]["event_payload"]["num"] - while len(nums) < num: - nums.add(str(random.randint(1, 100))) - say(f"Here you are: {', '.join(nums)}") - else: - # nothing to do for this bot message - # If you want to add more patterns here, be careful not to cause infinite loop messaging - pass - - except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") -... -``` - -## Full example: App Agent Template +## Full example: App Agent Template {#app-agent-template} 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.