From 65ac185fcf61476d75b18c3c9b9a1b215013b9ff Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:28:26 -0700 Subject: [PATCH 01/17] docs: add say_stream notes to the sending messages page (#1463) Co-authored-by: William Bergamin --- docs/english/_sidebar.json | 6 +- docs/english/concepts/message-sending.md | 75 +++++++++++++++--------- docs/english/experiments.md | 4 -- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index eab9d94f8..61b574617 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -85,11 +85,7 @@ "tools/bolt-python/concepts/token-rotation" ] }, - { - "type": "category", - "label": "Experiments", - "items": ["tools/bolt-python/experiments"] - }, + "tools/bolt-python/experiments", { "type": "category", "label": "Legacy", diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index 87c433129..cc14f8805 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -43,37 +43,58 @@ def show_datepicker(event, say): ## Streaming messages {#streaming-messages} -You can have your app's messages stream in to replicate conventional AI chatbot behavior. This is done through three Web API methods: +You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. -* [`chat_startStream`](/reference/methods/chat.startStream) -* [`chat_appendStream`](/reference/methods/chat.appendStream) -* [`chat_stopStream`](/reference/methods/chat.stopStream) +The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient.chat_stream`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility by sourcing parameter values from the relevant event payload. -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): +| Parameter | Value | +|---|---| +| `channel_id` | Sourced from the event payload. +| `thread_ts` | Sourced from the event payload. Falls back to the `ts` value if available. +| `recipient_team_id` | Sourced from the event `team_id` (`enterprise_id` if the app is installed on an org). +| `recipient_user_id` | Sourced from the `user_id` of the event. -```python -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) +If neither a `channel_id` or `thread_ts` can be sourced, then the utility will merely be `None`. + +For information on calling the `chat_*Stream` API methods directly, see the [_Sending streaming messages_](/tools/python-slack-sdk/web#sending-streaming-messages) section of the Python Slack SDK docs. + +### Example {#example} + +```py +import os + +from slack_bolt import App, SayStream +from slack_bolt.adapter.socket_mode import SocketModeHandler +from slack_sdk import WebClient + +app = App(token=os.environ.get("SLACK_BOT_TOKEN")) + +@app.event("app_mention") +def handle_app_mention(client: WebClient, say_stream: SayStream): + stream = say_stream() + stream.append(markdown_text="Someone rang the bat signal!") + stream.stop() + +@app.message("") +def handle_message(client: WebClient, say_stream: SayStream): + stream = say_stream() + + stream.append(markdown_text="Let me consult my *vast knowledge database*...) + stream.stop() + +if __name__ == "__main__": + SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start() ``` -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. +#### Adding feedback buttons after a stream -```python +You can pass a [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element to `stream.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. + +```py +stream.stop(blocks=feedback_block) +``` + +```py def create_feedback_block() -> List[Block]: blocks: List[Block] = [ ContextActionsBlock( @@ -95,6 +116,4 @@ def create_feedback_block() -> List[Block]: ) ] return blocks -``` - -For information on calling the `chat_*Stream` API methods without the helper utility, see the [_Sending streaming messages_](/tools/python-slack-sdk/web#sending-streaming-messages) section of the Python Slack SDK docs. \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/english/experiments.md b/docs/english/experiments.md index 681c8cbc6..13adf0a32 100644 --- a/docs/english/experiments.md +++ b/docs/english/experiments.md @@ -28,7 +28,3 @@ def handle_mention(agent: BoltAgent): stream.append(markdown_text="Hello!") stream.stop() ``` - -### Limitations - -The `chat_stream()` method currently only works when the `thread_ts` field is available in the event context (DMs and threaded replies). Top-level channel messages do not have a `thread_ts` field, and the `ts` field is not yet provided to `BoltAgent`. \ No newline at end of file From b2e3d7d4a904f4575be63d426a6bcd0044bc8058 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 13:44:33 -0700 Subject: [PATCH 02/17] go --- docs/english/concepts/ai-apps.md | 579 ++++++++++++++++------- docs/english/concepts/message-sending.md | 2 +- 2 files changed, 405 insertions(+), 176 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 3b057bc7e..056529a30 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -5,6 +5,409 @@ The Slack platform offers features tailored for AI agents and assistants. Your a 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! +## Text streaming in messages {#text-streaming} + +You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. + +The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient.chat_stream`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility by sourcing parameter values from the relevant event payload. + +| Parameter | Value | +|---|---| +| `channel_id` | Sourced from the event payload. +| `thread_ts` | Sourced from the event payload. Falls back to the `ts` value if available. +| `recipient_team_id` | Sourced from the event `team_id` (`enterprise_id` if the app is installed on an org). +| `recipient_user_id` | Sourced from the `user_id` of the event. + +If neither a `channel_id` or `thread_ts` can be sourced, then the utility will be `None`. + +You can see how our example support app "Casey" uses `say_stream` when responding to DMs below. + + + + +```python title="app_mentioned.py" +import re +from logging import Logger + +from slack_bolt import BoltContext, Say, SayStream, SetStatus +from slack_sdk import WebClient + +from agent import CaseyDeps, casey_agent, get_model +from thread_context import conversation_store +from listeners.views.feedback_builder import build_feedback_blocks + + +def handle_app_mentioned( + client: WebClient, + context: BoltContext, + event: dict, + logger: Logger, + say: Say, + say_stream: SayStream, + set_status: SetStatus, +): + """Handle @Casey mentions in channels.""" + try: + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + user_id = context.user_id + + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() + + if not cleaned_text: + say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return + + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) + + # Set assistant thread status with loading messages + set_status( + 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…", + ], + ) + + # Get conversation history + history = conversation_store.get_history(channel_id, thread_ts) + + # Run the agent + deps = CaseyDeps( + client=client, + user_id=user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + result = casey_agent.run_sync( + cleaned_text, + model=get_model(), + deps=deps, + message_history=history, + ) + + # Stream response in thread with feedback buttons + streamer = say_stream() + streamer.append(markdown_text=result.output) + feedback_blocks = build_feedback_blocks() + streamer.stop(blocks=feedback_blocks) + + # Store conversation history + conversation_store.set_history(channel_id, thread_ts, result.all_messages()) + + except Exception as e: + logger.exception(f"Failed to handle app mention: {e}") + say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) +``` + + + + +```python title="app_mentioned.py" +import re +from logging import Logger + +from slack_bolt.context.async_context import AsyncBoltContext +from slack_bolt.context.say.async_say import AsyncSay +from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream +from slack_bolt.context.set_status.async_set_status import AsyncSetStatus +from slack_sdk.web.async_client import AsyncWebClient + +from agent import CaseyDeps, run_casey_agent +from thread_context import session_store +from listeners.views.feedback_builder import build_feedback_blocks + + +async def handle_app_mentioned( + client: AsyncWebClient, + context: AsyncBoltContext, + event: dict, + logger: Logger, + say: AsyncSay, + say_stream: AsyncSayStream, + set_status: AsyncSetStatus, +): + """Handle @Casey mentions in channels.""" + try: + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() + + if not cleaned_text: + await say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return + + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + await client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) + + # Set assistant thread status with loading messages + await set_status( + 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…", + ], + ) + + # Get session ID for conversation context + existing_session_id = session_store.get_session(channel_id, thread_ts) + + # Run the agent with deps for tool access + deps = CaseyDeps( + client=client, + user_id=context.user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + response_text, new_session_id = await run_casey_agent( + cleaned_text, session_id=existing_session_id, deps=deps + ) + + # Stream response in thread with feedback buttons + streamer = await say_stream() + await streamer.append(markdown_text=response_text) + feedback_blocks = build_feedback_blocks() + await streamer.stop(blocks=feedback_blocks) + + # Store session ID for future context + if new_session_id: + session_store.set_session(channel_id, thread_ts, new_session_id) + + except Exception as e: + logger.exception(f"Failed to handle app mention: {e}") + await say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) +``` + + + +```python title="app_mentioned.py +import re +from logging import Logger + +from agents import Runner +from slack_bolt import BoltContext, Say, SayStream, SetStatus +from slack_sdk import WebClient + +from agent import CaseyDeps, casey_agent +from thread_context import conversation_store +from listeners.views.feedback_builder import build_feedback_blocks + + +def handle_app_mentioned( + client: WebClient, + context: BoltContext, + event: dict, + logger: Logger, + say: Say, + say_stream: SayStream, + set_status: SetStatus, +): + """Handle @Casey mentions in channels.""" + try: + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + user_id = context.user_id + + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() + + if not cleaned_text: + say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return + + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) + + # Set assistant thread status with loading messages + set_status( + 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…", + ], + ) + + # Get conversation history + history = conversation_store.get_history(channel_id, thread_ts) + + # Build input for the agent + if history: + input_items = history + [{"role": "user", "content": cleaned_text}] + else: + input_items = cleaned_text + + # Run the agent + deps = CaseyDeps( + client=client, + user_id=user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + result = Runner.run_sync(casey_agent, input=input_items, context=deps) + + # Stream response in thread with feedback buttons + streamer = say_stream() + streamer.append(markdown_text=result.final_output) + feedback_blocks = build_feedback_blocks() + streamer.stop(blocks=feedback_blocks) + + # Store conversation history + conversation_store.set_history(channel_id, thread_ts, result.to_input_list()) + + except Exception as e: + logger.exception(f"Failed to handle app mention: {e}") + say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) +``` + + + + + +## Adding and handling feedback {#adding-and-handling-feedback} + +The above example used the [feedback buttons block element](/reference/block-kit/block-elements/feedback-buttons-element/) to allow users to immediately provide feedback regarding the app's responses. Here's what the feedback buttons look like from the Support Agent sample app: + +```py title=".../listeners/views/feedback_builder.py" +from slack_sdk.models.blocks import ( + Block, + ContextActionsBlock, + FeedbackButtonObject, + FeedbackButtonsElement, +) + + +def build_feedback_blocks() -> list[Block]: + """Build feedback blocks with thumbs up/down buttons.""" + return [ + 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", + ), + ) + ] + ) + ] +``` + +That feedback block is then rendered at the bottom of your app's message via the `say_stream` utility. + +```py +... + # Stream response in thread with feedback buttons + streamer = say_stream() + streamer.append(markdown_text=result.output) + feedback_blocks = build_feedback_blocks() + streamer.stop(blocks=feedback_blocks) +... +``` + +You can alos add a response for when the user provides feedback. + +```python title="...listeners/actions/feedback_button.py" +from logging import Logger + +from slack_bolt import Ack, BoltContext +from slack_sdk import WebClient + + +def handle_feedback_button( + ack: Ack, body: dict, client: WebClient, context: BoltContext, logger: Logger +): + """Handle thumbs up/down feedback on Casey's responses.""" + ack() + + try: + channel_id = context.channel_id + user_id = context.user_id + message_ts = body["message"]["ts"] + feedback_value = body["actions"][0]["value"] + + if feedback_value == "good-feedback": + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + thread_ts=message_ts, + text="Glad that was helpful! :tada:", + ) + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + thread_ts=message_ts, + text="Sorry that wasn't helpful. :slightly_frowning_face: Try rephrasing your question or I can create a support ticket for you.", + ) + + logger.debug( + f"Feedback received: value={feedback_value}, message_ts={message_ts}" + ) + except Exception as e: + logger.exception(f"Failed to handle feedback: {e}") +``` + +--- + ## The `Assistant` class instance {#assistant} :::info[Some features within this guide require a paid plan] @@ -333,180 +736,6 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: 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: - -* the [`chat.startStream`](/reference/methods/chat.startStream) method starts the text stream, -* the [`chat.appendStream`](/reference/methods/chat.appendStream) method appends text to the stream, and -* the [`chat.stopStream`](/reference/methods/chat.stopStream) method stops it. - -Since you're using Bolt for Python, built upon the Python Slack SDK, you can use the [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) utility to streamline all three aspects of streaming in your app's messages. - -The following example uses OpenAI's streaming API with the new `chat_stream()` functionality, but you can substitute it with the AI client of your choice. - - -```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, - ) - - # 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 {#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}") -``` - -## Full example: App Agent Template {#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. diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md index cc14f8805..090503ff2 100644 --- a/docs/english/concepts/message-sending.md +++ b/docs/english/concepts/message-sending.md @@ -54,7 +54,7 @@ The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient. | `recipient_team_id` | Sourced from the event `team_id` (`enterprise_id` if the app is installed on an org). | `recipient_user_id` | Sourced from the `user_id` of the event. -If neither a `channel_id` or `thread_ts` can be sourced, then the utility will merely be `None`. +If neither a `channel_id` or `thread_ts` can be sourced, then the utility will be `None`. For information on calling the `chat_*Stream` API methods directly, see the [_Sending streaming messages_](/tools/python-slack-sdk/web#sending-streaming-messages) section of the Python Slack SDK docs. From 3aad114afa7e7530d36937e02c5409f2407f190a Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 14:46:34 -0700 Subject: [PATCH 03/17] experiment --- docs/english/concepts/ai-apps.md | 1029 ++++++++++++++++-------------- 1 file changed, 543 insertions(+), 486 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 056529a30..72d5a684b 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -1,38 +1,24 @@ # 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). +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, and 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! +If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). Then come back here to implement them with Bolt! -## Text streaming in messages {#text-streaming} +## Listening for events -You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. - -The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient.chat_stream`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility by sourcing parameter values from the relevant event payload. - -| Parameter | Value | -|---|---| -| `channel_id` | Sourced from the event payload. -| `thread_ts` | Sourced from the event payload. Falls back to the `ts` value if available. -| `recipient_team_id` | Sourced from the event `team_id` (`enterprise_id` if the app is installed on an org). -| `recipient_user_id` | Sourced from the `user_id` of the event. - -If neither a `channel_id` or `thread_ts` can be sourced, then the utility will be `None`. -You can see how our example support app "Casey" uses `say_stream` when responding to DMs below. +Agents can be invoked throughout Slack, such as @mentions in channels. - - - -```python title="app_mentioned.py" +```python import re from logging import Logger +from agents import Runner from slack_bolt import BoltContext, Say, SayStream, SetStatus from slack_sdk import WebClient -from agent import CaseyDeps, casey_agent, get_model +from agent import CaseyDeps, casey_agent from thread_context import conversation_store from listeners.views.feedback_builder import build_feedback_blocks @@ -70,254 +56,358 @@ def handle_app_mentioned( timestamp=event["ts"], name="eyes", ) + ... +``` - # Set assistant thread status with loading messages - set_status( - 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…", - ], - ) +### Via the Assistant class (side panel) - # Get conversation history - history = conversation_store.get_history(channel_id, thread_ts) +:::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. +::: - # Run the agent - deps = CaseyDeps( - client=client, - user_id=user_id, - channel_id=channel_id, - thread_ts=thread_ts, - message_ts=event["ts"], - ) - result = casey_agent.run_sync( - cleaned_text, - model=get_model(), - deps=deps, - message_history=history, - ) +The [`Assistant`](/tools/bolt-js/reference#the-assistantconfig-configuration-object) class handles incoming events from the Slack Agents & AI Apps feature. A typical flow: - # Stream response in thread with feedback buttons - streamer = say_stream() - streamer.append(markdown_text=result.output) - feedback_blocks = build_feedback_blocks() - streamer.stop(blocks=feedback_blocks) +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](#handling-thread-context-changes). The `Assistant` class handles [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events and automatically manages context. +3. [The user responds](#handling-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event. - # Store conversation history - conversation_store.set_history(channel_id, thread_ts, result.all_messages()) +```python +assistant = Assistant() - except Exception as e: - logger.exception(f"Failed to handle app mention: {e}") - say( - text=f":warning: Something went wrong! ({e})", - thread_ts=event.get("thread_ts") or event["ts"], - ) +# 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: + ... + +# Enable this assistant middleware in your Bolt app +app.use(assistant) ``` - - +:::info[Consider the following] +You _could_ go it alone and [listen](/tools/bolt-python/concepts/event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events in order to implement the AI features in your app. That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! +::: -```python title="app_mentioned.py" -import re -from logging import Logger +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. -from slack_bolt.context.async_context import AsyncBoltContext -from slack_bolt.context.say.async_say import AsyncSay -from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream -from slack_bolt.context.set_status.async_set_status import AsyncSetStatus -from slack_sdk.web.async_client import AsyncWebClient +If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. -from agent import CaseyDeps, run_casey_agent -from thread_context import session_store -from listeners.views.feedback_builder import build_feedback_blocks +:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] +::: +#### Configuring your app to support the `Assistant` class {#configuring-assistant-class} -async def handle_app_mentioned( - client: AsyncWebClient, - context: AsyncBoltContext, - event: dict, - logger: Logger, - say: AsyncSay, - say_stream: AsyncSayStream, - set_status: AsyncSetStatus, -): - """Handle @Casey mentions in channels.""" - try: - channel_id = context.channel_id - text = event.get("text", "") - thread_ts = event.get("thread_ts") or event["ts"] +1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. - # Strip the bot mention from the text - cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() +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) - if not cleaned_text: - await say( - text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", - thread_ts=thread_ts, - ) - return +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) - # Add eyes reaction only to the first message (not threaded replies) - if not event.get("thread_ts"): - await client.reactions_add( - channel=channel_id, - timestamp=event["ts"], - name="eyes", - ) +#### Handling a new thread {#handling-new-thread} - # Set assistant thread status with loading messages - await set_status( - 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…", - ], - ) +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. - # Get session ID for conversation context - existing_session_id = session_store.get_session(channel_id, thread_ts) +:::tip[When a user opens an app thread while in a channel, the channel info is stored as the thread's `AssistantThreadContext` data.] - # Run the agent with deps for tool access - deps = CaseyDeps( - client=client, - user_id=context.user_id, - channel_id=channel_id, - thread_ts=thread_ts, - message_ts=event["ts"], - ) - response_text, new_session_id = await run_casey_agent( - cleaned_text, session_id=existing_session_id, deps=deps - ) +You can grab that info by using the `get_thread_context` utility, as subsequent user message event payloads won't include the channel info. +::: - # Stream response in thread with feedback buttons - streamer = await say_stream() - await streamer.append(markdown_text=response_text) - feedback_blocks = build_feedback_blocks() - await streamer.stop(blocks=feedback_blocks) +```python +assistant = Assistant() - # Store session ID for future context - if new_session_id: - session_store.set_session(channel_id, thread_ts, new_session_id) +@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": "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 app mention: {e}") - await say( - text=f":warning: Something went wrong! ({e})", - thread_ts=event.get("thread_ts") or event["ts"], - ) + logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) + say(f":warning: Something went wrong! ({e})") ``` - - -```python title="app_mentioned.py -import re -from logging import Logger +You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info. -from agents import Runner -from slack_bolt import BoltContext, Say, SayStream, SetStatus -from slack_sdk import WebClient +#### Handling thread context changes {#handling-thread-context-changes} -from agent import CaseyDeps, casey_agent -from thread_context import conversation_store -from listeners.views.feedback_builder import build_feedback_blocks +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. -def handle_app_mentioned( - client: WebClient, - context: BoltContext, - event: dict, - logger: Logger, - say: Say, - say_stream: SayStream, - set_status: SetStatus, -): - """Handle @Casey mentions in channels.""" - try: - channel_id = context.channel_id - text = event.get("text", "") - thread_ts = event.get("thread_ts") or event["ts"] - user_id = context.user_id +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`). - # Strip the bot mention from the text - cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() +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`. - if not cleaned_text: - say( - text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", - thread_ts=thread_ts, - ) - return +```python +from slack_bolt import FileAssistantThreadContextStore +assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) +``` - # Add eyes reaction only to the first message (not threaded replies) - if not event.get("thread_ts"): - client.reactions_add( - channel=channel_id, - timestamp=event["ts"], - name="eyes", - ) +#### Handling the user response {#handling-user-response} - # Set assistant thread status with loading messages - set_status( - 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…", +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. See [Setting assistant status](#setting-assistant-status) for implementation examples. + +#### 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"}, + }, + } ], - ) + }, + ) - # Get conversation history - history = conversation_store.get_history(channel_id, thread_ts) +# 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"]) - # Build input for the agent - if history: - input_items = history + [{"role": "user", "content": cleaned_text}] + # 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: - input_items = cleaned_text + # nothing to do for this bot message + # If you want to add more patterns here, be careful not to cause infinite loop messaging + pass - # Run the agent - deps = CaseyDeps( - client=client, - user_id=user_id, - channel_id=channel_id, - thread_ts=thread_ts, - message_ts=event["ts"], - ) - result = Runner.run_sync(casey_agent, input=input_items, context=deps) + except Exception as e: + logger.exception(f"Failed to respond to an inquiry: {e}") +... +``` - # Stream response in thread with feedback buttons - streamer = say_stream() - streamer.append(markdown_text=result.final_output) - feedback_blocks = build_feedback_blocks() - streamer.stop(blocks=feedback_blocks) +See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. - # Store conversation history - conversation_store.set_history(channel_id, thread_ts, result.to_input_list()) +## Setting assistant status {#setting-assistant-status} - except Exception as e: - logger.exception(f"Failed to handle app mention: {e}") - say( - text=f":warning: Something went wrong! ({e})", - thread_ts=event.get("thread_ts") or event["ts"], - ) +Your app can show users action is happening behind the scenes by setting the thread status. + + + + +```python +def handle_app_mentioned( + set_status: SetStatus, + ... +): + set_status( + 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…", + ], + ) +``` + + + + +```python +@assistant.user_message +def respond_in_assistant_thread( + set_status: SetStatus, + ... +): + set_status( + 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…", + ], + ) ``` +## Streaming messages {#text-streaming} + +You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. + +The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient.chat_stream`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility by sourcing parameter values from the relevant event payload. + +| Parameter | Value | +|---|---| +| `channel_id` | Sourced from the event payload. +| `thread_ts` | Sourced from the event payload. Falls back to the `ts` value if available. +| `recipient_team_id` | Sourced from the event `team_id` (`enterprise_id` if the app is installed on an org). +| `recipient_user_id` | Sourced from the `user_id` of the event. + +If neither a `channel_id` or `thread_ts` can be sourced, then the utility will be `None`. + +You can see how our [Support Agent](https://github.com/slack-samples/bolt-python-support-agent) sample app uses `say_stream` when responding to DMs below. + +```python +from slack_bolt import SayStream + +def handle_message(say_stream: SayStream): + """Stream a response to a message.""" + streamer = say_stream() + streamer.append(markdown_text="Here's my response...") + streamer.append(markdown_text="And here's more...") + streamer.stop() +``` ## Adding and handling feedback {#adding-and-handling-feedback} -The above example used the [feedback buttons block element](/reference/block-kit/block-elements/feedback-buttons-element/) to allow users to immediately provide feedback regarding the app's responses. Here's what the feedback buttons look like from the Support Agent sample app: +You can use [feedback buttons block element](/reference/block-kit/block-elements/feedback-buttons-element/) to allow users to immediately provide feedback regarding the app's responses. Here's what the feedback buttons look like from the Support Agent sample app: ```py title=".../listeners/views/feedback_builder.py" from slack_sdk.models.blocks import ( @@ -363,7 +453,7 @@ That feedback block is then rendered at the bottom of your app's message via the ... ``` -You can alos add a response for when the user provides feedback. +You can also add a response for when the user provides feedback. ```python title="...listeners/actions/feedback_button.py" from logging import Logger @@ -406,336 +496,303 @@ def handle_feedback_button( logger.exception(f"Failed to handle feedback: {e}") ``` ---- -## The `Assistant` class instance {#assistant} +## Full example -:::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. -::: +Putting all those concepts together result in a dynamic agent ready to helpfully respond. -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: +```python title="app_mentioned.py" +import re +from logging import Logger -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. +from slack_bolt import BoltContext, Say, SayStream, SetStatus +from slack_sdk import WebClient +from agent import CaseyDeps, casey_agent, get_model +from thread_context import conversation_store +from listeners.views.feedback_builder import build_feedback_blocks -```python -assistant = Assistant() -# This listener is invoked when a human user opened an assistant thread -@assistant.thread_started -def start_assistant_thread( +def handle_app_mentioned( + client: WebClient, + context: BoltContext, + event: dict, + logger: Logger, say: Say, - get_thread_context: GetThreadContext, - set_suggested_prompts: SetSuggestedPrompts, - logger: logging.Logger, + say_stream: SayStream, + set_status: SetStatus, ): + """Handle @Casey mentions in channels.""" 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: - ... + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + user_id = context.user_id -# Enable this assistant middleware in your Bolt app -app.use(assistant) -``` + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() -:::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! -::: + if not cleaned_text: + say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return -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. + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) -If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. + # Set assistant thread status with loading messages + set_status( + 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…", + ], + ) -:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] -::: + # Get conversation history + history = conversation_store.get_history(channel_id, thread_ts) -### Configuring your app to support the `Assistant` class {#configuring-assistant-class} + # Run the agent + deps = CaseyDeps( + client=client, + user_id=user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + result = casey_agent.run_sync( + cleaned_text, + model=get_model(), + deps=deps, + message_history=history, + ) -1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. + # Stream response in thread with feedback buttons + streamer = say_stream() + streamer.append(markdown_text=result.output) + feedback_blocks = build_feedback_blocks() + streamer.stop(blocks=feedback_blocks) -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) + # Store conversation history + conversation_store.set_history(channel_id, thread_ts, result.all_messages()) -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) + except Exception as e: + logger.exception(f"Failed to handle app mention: {e}") + say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) +``` -### 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. +```python title="app_mentioned.py" +import re +from logging import Logger -:::tip[When a user opens an app thread while in a channel, the channel info is stored as the thread's `AssistantThreadContext` data.] +from slack_bolt.context.async_context import AsyncBoltContext +from slack_bolt.context.say.async_say import AsyncSay +from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream +from slack_bolt.context.set_status.async_set_status import AsyncSetStatus +from slack_sdk.web.async_client import AsyncWebClient -You can grab that info by using the `get_thread_context` utility, as subsequent user message event payloads won't include the channel info. -::: +from agent import CaseyDeps, run_casey_agent +from thread_context import session_store +from listeners.views.feedback_builder import build_feedback_blocks -```python -assistant = Assistant() -@assistant.thread_started -def start_assistant_thread( - say: Say, - get_thread_context: GetThreadContext, - set_suggested_prompts: SetSuggestedPrompts, - logger: logging.Logger, +async def handle_app_mentioned( + client: AsyncWebClient, + context: AsyncBoltContext, + event: dict, + logger: Logger, + say: AsyncSay, + say_stream: AsyncSayStream, + set_status: AsyncSetStatus, ): + """Handle @Casey mentions in channels.""" 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.", - }, - ] + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] - 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) + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() - 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})") -``` + if not cleaned_text: + await say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return -You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info. + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + await client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) -### Handling thread context changes {#handling-thread-context-changes} + # Set assistant thread status with loading messages + await set_status( + 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…", + ], + ) -When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. + # Get session ID for conversation context + existing_session_id = session_store.get_session(channel_id, thread_ts) -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. + # Run the agent with deps for tool access + deps = CaseyDeps( + client=client, + user_id=context.user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + response_text, new_session_id = await run_casey_agent( + cleaned_text, session_id=existing_session_id, deps=deps + ) -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`). + # Stream response in thread with feedback buttons + streamer = await say_stream() + await streamer.append(markdown_text=response_text) + feedback_blocks = build_feedback_blocks() + await streamer.stop(blocks=feedback_blocks) -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`. + # Store session ID for future context + if new_session_id: + session_store.set_session(channel_id, thread_ts, new_session_id) -```python -from slack_bolt import FileAssistantThreadContextStore -assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) + except Exception as e: + logger.exception(f"Failed to handle app mention: {e}") + await say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) ``` + + -### 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. +```python title="app_mentioned.py" +import re +from logging import Logger -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/). +from agents import Runner +from slack_bolt import BoltContext, Say, SayStream, SetStatus +from slack_sdk import WebClient -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) +from agent import CaseyDeps, casey_agent +from thread_context import conversation_store +from listeners.views.feedback_builder import build_feedback_blocks -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( +def handle_app_mentioned( client: WebClient, context: BoltContext, - get_thread_context: GetThreadContext, - logger: logging.Logger, - payload: dict, + event: dict, + logger: Logger, say: Say, + say_stream: SayStream, set_status: SetStatus, ): + """Handle @Casey mentions in channels.""" try: - channel_id = payload["channel"] - team_id = payload["team"] - thread_ts = payload["thread_ts"] - user_id = payload["user"] - user_message = payload["text"] + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + user_id = context.user_id + + # Strip the bot mention from the text + cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() + + if not cleaned_text: + say( + text="Hey there! How can I help you? Describe your IT issue and I'll do my best to assist.", + thread_ts=thread_ts, + ) + return + + # Add eyes reaction only to the first message (not threaded replies) + if not event.get("thread_ts"): + client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) + # Set assistant thread status with loading messages set_status( - status="thinking...", + 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…", ], ) - # Collect the conversation history with this user - 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) - - # Post the result in the assistant thread - say(text=returned_message) - - except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") - # Don't forget sending a message telling the error - # Without this, the status 'is typing...' won't be cleared, therefore the end-user is unable to continue the chat - say(f":warning: Sorry, something went wrong during processing your request (error: {e})") - -# Enable this assistant middleware in your Bolt app -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", - }, - ], - }, - ], - ) + # Get conversation history + history = conversation_store.get_history(channel_id, thread_ts) -# 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"}, - }, - } - ], - }, - ) + # Build input for the agent + if history: + input_items = history + [{"role": "user", "content": cleaned_text}] + else: + input_items = cleaned_text -# 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"]) + # Run the agent + deps = CaseyDeps( + client=client, + user_id=user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + result = Runner.run_sync(casey_agent, input=input_items, context=deps) - # 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)}, - }, - ) + # Stream response in thread with feedback buttons + streamer = say_stream() + streamer.append(markdown_text=result.final_output) + feedback_blocks = build_feedback_blocks() + streamer.stop(blocks=feedback_blocks) -# 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 + # Store conversation history + conversation_store.set_history(channel_id, thread_ts, result.to_input_list()) except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") -... + logger.exception(f"Failed to handle app mention: {e}") + say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event["ts"], + ) ``` -See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. + + + +--- -### 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. From 44abe30909a60e026480f4dcc88055ddf1c838dc Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:01:19 -0700 Subject: [PATCH 04/17] more --- docs/english/_sidebar.json | 12 + docs/english/concepts/ai-apps.md | 283 +------------------ docs/english/concepts/assistant-class.md | 329 +++++++++++++++++++++++ 3 files changed, 347 insertions(+), 277 deletions(-) create mode 100644 docs/english/concepts/assistant-class.md diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index 61b574617..5422a1912 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -39,6 +39,18 @@ "tools/bolt-python/concepts/app-home" ] }, + { + "type": "category", + "label": "Creating agents", + "link": { + "type": "doc", + "id": "tools/bolt-python/concepts/ai-apps" + }, + "items": [ + "tools/bolt-python/concepts/ai-apps", + "tools/bolt-python/concepts/assistant-class" + ] + }, "tools/bolt-python/concepts/ai-apps", { "type": "category", diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 72d5a684b..262d7aec2 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -7,9 +7,13 @@ If you're unfamiliar with using these feature within Slack, you may want to read ## Listening for events - Agents can be invoked throughout Slack, such as @mentions in channels. +:::tip[Using the Assistant side panel] + +The Assistant side panel requires additional setup. See the [Assistant class guide](/concepts/assistant-class). +::: + ```python import re from logging import Logger @@ -59,283 +63,10 @@ def handle_app_mentioned( ... ``` -### Via the Assistant class (side panel) - -:::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 handles incoming events from the Slack Agents & AI Apps feature. A typical flow: - -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](#handling-thread-context-changes). The `Assistant` class handles [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events and automatically manages context. -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: - ... - -# Enable this assistant middleware in your Bolt app -app.use(assistant) -``` - -:::info[Consider the following] -You _could_ go it alone and [listen](/tools/bolt-python/concepts/event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events in order to implement the AI features in your app. That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! -::: - -While the `assistant_thread_started` and `assistant_thread_context_changed` events do provide Slack-client thread context information, the `message.im` event does not. Any subsequent user message events won't contain thread context data. For that reason, Bolt not only provides a way to store thread context — the `threadContextStore` property — but it also provides a `DefaultThreadContextStore` instance that is utilized by default. This implementation relies on storing and retrieving [message metadata](/messaging/message-metadata/) as the user interacts with the app. - -If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. - -:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] -::: - -#### Configuring your app to support the `Assistant` class {#configuring-assistant-class} - -1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. - -2. Within the App Settings **OAuth & Permissions** page, add the following scopes: - * [`assistant:write`](/reference/scopes/assistant.write) - * [`chat:write`](/reference/scopes/chat.write) - * [`im:history`](/reference/scopes/im.history) - -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() - -@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": "Suggest names for my Slack app", - "message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.", - }, - ] - - thread_context = get_thread_context() - if thread_context is not None and thread_context.channel_id is not None: - summarize_channel = { - "title": "Summarize the referred channel", - "message": "Can you generate a brief summary of the referred channel?", - } - prompts.append(summarize_channel) - - set_suggested_prompts(prompts=prompts) - except Exception as e: - logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) - say(f":warning: Something went wrong! ({e})") -``` - -You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info. - -#### Handling thread context changes {#handling-thread-context-changes} - -When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. - -If you use the built-in `Assistant` middleware without any custom configuration, the updated context data is automatically saved as [message metadata](/messaging/message-metadata/) of the first reply from the app. - -As long as you use the built-in approach, you don't need to store the context data within a datastore. The downside of this default behavior is the overhead of additional calls to the Slack API. These calls include those to `conversations.history`, which are used to look up the stored message metadata that contains the thread context (via `get_thread_context`). - -To store context elsewhere, pass a custom `AssistantThreadContextStore` implementation to the `Assistant` constructor. We provide `FileAssistantThreadContextStore`, which is a reference implementation that uses the local file system. Since this reference implementation relies on local files, it's not advised for use in production. For production apps, we recommend creating a class that inherits `AssistantThreadContextStore`. - -```python -from slack_bolt import FileAssistantThreadContextStore -assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) -``` - -#### Handling the user response {#handling-user-response} - -When the user messages your app, the [`message.im`](/reference/events/message.im) event will be sent to your app. - -Messages sent to the app do not contain a [subtype](/reference/events/message#subtypes) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/). - -There are three utilities that are particularly useful in curating the user experience: -* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say) -* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle) -* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus) - -Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. See [Setting assistant status](#setting-assistant-status) for implementation examples. - -#### 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. - ## Setting assistant status {#setting-assistant-status} Your app can show users action is happening behind the scenes by setting the thread status. - - - ```python def handle_app_mentioned( set_status: SetStatus, @@ -374,9 +105,6 @@ def respond_in_assistant_thread( ) ``` - - - ## Streaming messages {#text-streaming} You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. @@ -795,4 +523,5 @@ def handle_app_mentioned( --- +For a dedicated AI assistant experience with a side panel, see the [Assistant class guide](./assistant-class.md). diff --git a/docs/english/concepts/assistant-class.md b/docs/english/concepts/assistant-class.md new file mode 100644 index 000000000..c736856fb --- /dev/null +++ b/docs/english/concepts/assistant-class.md @@ -0,0 +1,329 @@ +## 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. + + +```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: + ... + +# Enable this assistant middleware in your Bolt app +app.use(assistant) +``` + +:::info[Consider the following] +You _could_ go it alone and [listen](/tools/bolt-python/concepts/event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events in order to implement the AI features in your app. That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you! +::: + +While the `assistant_thread_started` and `assistant_thread_context_changed` events do provide Slack-client thread context information, the `message.im` event does not. Any subsequent user message events won't contain thread context data. For that reason, Bolt not only provides a way to store thread context — the `threadContextStore` property — but it also provides a `DefaultThreadContextStore` instance that is utilized by default. This implementation relies on storing and retrieving [message metadata](/messaging/message-metadata/) as the user interacts with the app. + +If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods. + +:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.] +::: + +## Configuring your app to support the `Assistant` class {#configuring-assistant-class} + +1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. + +2. Within the App Settings **OAuth & Permissions** page, add the following scopes: + * [`assistant:write`](/reference/scopes/assistant.write) + * [`chat:write`](/reference/scopes/chat.write) + * [`im:history`](/reference/scopes/im.history) + +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() + +@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": "Suggest names for my Slack app", + "message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.", + }, + ] + + thread_context = get_thread_context() + if thread_context is not None and thread_context.channel_id is not None: + summarize_channel = { + "title": "Summarize the referred channel", + "message": "Can you generate a brief summary of the referred channel?", + } + prompts.append(summarize_channel) + + set_suggested_prompts(prompts=prompts) + except Exception as e: + logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) + say(f":warning: Something went wrong! ({e})") +``` + +You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info. + +## Handling thread context changes {#handling-thread-context-changes} + +When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. + +If you use the built-in `Assistant` middleware without any custom configuration, the updated context data is automatically saved as [message metadata](/messaging/message-metadata/) of the first reply from the app. + +As long as you use the built-in approach, you don't need to store the context data within a datastore. The downside of this default behavior is the overhead of additional calls to the Slack API. These calls include those to `conversations.history`, which are used to look up the stored message metadata that contains the thread context (via `get_thread_context`). + +To store context elsewhere, pass a custom `AssistantThreadContextStore` implementation to the `Assistant` constructor. We provide `FileAssistantThreadContextStore`, which is a reference implementation that uses the local file system. Since this reference implementation relies on local files, it's not advised for use in production. For production apps, we recommend creating a class that inherits `AssistantThreadContextStore`. + +```python +from slack_bolt import FileAssistantThreadContextStore +assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) +``` + +## Handling the user response {#handling-user-response} + +When the user messages your app, the [`message.im`](/reference/events/message.im) event will be sent to your app. + +Messages sent to the app do not contain a [subtype](/reference/events/message#subtypes) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/). + +There are three utilities that are particularly useful in curating the user experience: +* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say) +* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle) +* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus) + +Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. + +```python +# This listener is invoked when the human user sends a reply in the assistant thread +@assistant.user_message +def respond_in_assistant_thread( + client: WebClient, + context: BoltContext, + get_thread_context: GetThreadContext, + logger: logging.Logger, + payload: dict, + say: Say, + set_status: SetStatus, +): + try: + 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 = 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) + + # Post the result in the assistant thread + say(text=returned_message) + + except Exception as e: + logger.exception(f"Failed to respond to an inquiry: {e}") + # Don't forget sending a message telling the error + # Without this, the status 'is typing...' won't be cleared, therefore the end-user is unable to continue the chat + say(f":warning: Sorry, something went wrong during processing your request (error: {e})") + +# Enable this assistant middleware in your Bolt app +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. + +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. \ No newline at end of file From 51ac1818d912b8a9220ce78342630a7df03cd567 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:02:50 -0700 Subject: [PATCH 05/17] oops --- docs/english/concepts/ai-apps.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 262d7aec2..2e048ed9a 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -84,27 +84,6 @@ def handle_app_mentioned( ) ``` - - - -```python -@assistant.user_message -def respond_in_assistant_thread( - set_status: SetStatus, - ... -): - set_status( - 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…", - ], - ) -``` - ## Streaming messages {#text-streaming} You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. From b2badd1055fe7342c6f75dc375578b65ad6b1212 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:13:36 -0700 Subject: [PATCH 06/17] go --- docs/english/concepts/ai-apps.md | 189 +++++++++++++++++++++-- docs/english/concepts/assistant-class.md | 2 +- 2 files changed, 181 insertions(+), 10 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 2e048ed9a..2061211ee 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -1,18 +1,21 @@ +--- +sidebar_label: Overview +--- -# Using AI in Apps {#using-ai-in-apps} +# Creating agents with Bolt -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, and they can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). +The Slack platform offers features tailored for AI agents. Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind, If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). Then come back here to implement them with Bolt! -## Listening for events +--- -Agents can be invoked throughout Slack, such as @mentions in channels. +## Listening for user invocation -:::tip[Using the Assistant side panel] +Agents can be invoked throughout Slack, such as @mentions in channels, messages to the app, and using the assistant side panel. -The Assistant side panel requires additional setup. See the [Assistant class guide](/concepts/assistant-class). -::: + + ```python import re @@ -63,9 +66,172 @@ def handle_app_mentioned( ... ``` -## Setting assistant status {#setting-assistant-status} + + + +```python +from logging import Logger + +from slack_bolt.context.async_context import AsyncBoltContext +from slack_bolt.context.say.async_say import AsyncSay +from slack_bolt.context.say_stream.async_say_stream import AsyncSayStream +from slack_bolt.context.set_status.async_set_status import AsyncSetStatus +from slack_sdk.web.async_client import AsyncWebClient + +from agent import CaseyDeps, run_casey_agent +from thread_context import session_store +from listeners.views.feedback_builder import build_feedback_blocks + + +async def handle_message( + client: AsyncWebClient, + context: AsyncBoltContext, + event: dict, + logger: Logger, + say: AsyncSay, + say_stream: AsyncSayStream, + set_status: AsyncSetStatus, +): + """Handle messages sent to Casey via DM or in threads the bot is part of.""" + # Issue submissions are posted by the bot with metadata so the message + # handler can run the agent on behalf of the original user. + is_issue_submission = ( + event.get("metadata", {}).get("event_type") == "issue_submission" + ) + + # Skip message subtypes (edits, deletes, etc.) and bot messages that + # are not issue submissions. + if event.get("subtype"): + return + if event.get("bot_id") and not is_issue_submission: + return + + is_dm = event.get("channel_type") == "im" + is_thread_reply = event.get("thread_ts") is not None + + if is_dm: + pass + elif is_thread_reply: + # Channel thread replies are handled only if the bot is already engaged + session = session_store.get_session(context.channel_id, event["thread_ts"]) + if session is None: + return + else: + # Top-level channel messages are handled by app_mentioned + return + + try: + channel_id = context.channel_id + text = event.get("text", "") + thread_ts = event.get("thread_ts") or event["ts"] + + # Get session ID for conversation context + existing_session_id = session_store.get_session(channel_id, thread_ts) + + # Add eyes reaction only to the first message (DMs only — channel + # threads already have the reaction from the initial app_mention) + if is_dm and not existing_session_id: + await client.reactions_add( + channel=channel_id, + timestamp=event["ts"], + name="eyes", + ) + + # Set assistant thread status with loading messages + await set_status( + 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…", + ], + ) + + # For issue submissions the bot posted the message, so the real + # user_id comes from the metadata rather than the event context. + if is_issue_submission: + user_id = event["metadata"]["event_payload"]["user_id"] + else: + user_id = context.user_id + + # Run the agent with deps for tool access + deps = CaseyDeps( + client=client, + user_id=user_id, + channel_id=channel_id, + thread_ts=thread_ts, + message_ts=event["ts"], + ) + response_text, new_session_id = await run_casey_agent( + text, session_id=existing_session_id, deps=deps + ) + + # Stream response in thread with feedback buttons + streamer = await say_stream() + await streamer.append(markdown_text=response_text) + feedback_blocks = build_feedback_blocks() + await streamer.stop(blocks=feedback_blocks) + + # Store session ID for future context + if new_session_id: + session_store.set_session(channel_id, thread_ts, new_session_id) + + except Exception as e: + logger.exception(f"Failed to handle message: {e}") + await say( + text=f":warning: Something went wrong! ({e})", + thread_ts=event.get("thread_ts") or event.get("ts"), + ) +``` + + + + + + +:::tip[Using the Assistant side panel] + +The Assistant side panel requires additional setup. See the [Assistant class guide](/bolt-python/concepts/assistant-class). +::: + + +```py +from logging import Logger + +from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import ( + AsyncSetSuggestedPrompts, +) + +SUGGESTED_PROMPTS = [ + {"title": "Reset Password", "message": "I need to reset my password"}, + {"title": "Request Access", "message": "I need access to a system or tool"}, + {"title": "Network Issues", "message": "I'm having network connectivity issues"}, +] + + +async def handle_assistant_thread_started( + set_suggested_prompts: AsyncSetSuggestedPrompts, logger: Logger +): + """Handle assistant thread started events by setting suggested prompts.""" + try: + await set_suggested_prompts( + prompts=SUGGESTED_PROMPTS, + title="How can I help you today?", + ) + except Exception as e: + logger.exception(f"Failed to handle assistant thread started: {e}") +``` + + + -Your app can show users action is happening behind the scenes by setting the thread status. +--- + +## Setting status {#setting-assistant-status} + +Your app can show its users action is happening behind the scenes by setting its thread status. ```python def handle_app_mentioned( @@ -84,6 +250,8 @@ def handle_app_mentioned( ) ``` +--- + ## Streaming messages {#text-streaming} You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners. @@ -112,6 +280,8 @@ def handle_message(say_stream: SayStream): streamer.stop() ``` +--- + ## Adding and handling feedback {#adding-and-handling-feedback} You can use [feedback buttons block element](/reference/block-kit/block-elements/feedback-buttons-element/) to allow users to immediately provide feedback regarding the app's responses. Here's what the feedback buttons look like from the Support Agent sample app: @@ -203,6 +373,7 @@ def handle_feedback_button( logger.exception(f"Failed to handle feedback: {e}") ``` +--- ## Full example diff --git a/docs/english/concepts/assistant-class.md b/docs/english/concepts/assistant-class.md index c736856fb..628ef352e 100644 --- a/docs/english/concepts/assistant-class.md +++ b/docs/english/concepts/assistant-class.md @@ -1,4 +1,4 @@ -## The `Assistant` class instance {#assistant} +# The Assistant class :::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. From e5ebe30d9c01fafca0555e6fcde5398d4e7fbc41 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:15:27 -0700 Subject: [PATCH 07/17] pls --- docs/english/concepts/ai-apps.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 2061211ee..83714cf00 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -66,7 +66,7 @@ def handle_app_mentioned( ... ``` - + ```python @@ -190,9 +190,7 @@ async def handle_message( - :::tip[Using the Assistant side panel] - The Assistant side panel requires additional setup. See the [Assistant class guide](/bolt-python/concepts/assistant-class). ::: From 90e288fa4116201f45bbba907b2e2e772c91651c Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:28:40 -0700 Subject: [PATCH 08/17] go --- docs/english/_sidebar.json | 24 ++++++++++++------------ docs/english/concepts/ai-apps.md | 13 ++++++++----- docs/english/concepts/assistant-class.md | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index 5422a1912..fa2fe3813 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -8,6 +8,18 @@ "tools/bolt-python/getting-started", { "type": "html", "value": "
" }, "tools/bolt-python/building-an-app", + { + "type": "category", + "label": "Creating agents", + "link": { + "type": "doc", + "id": "tools/bolt-python/concepts/ai-apps" + }, + "items": [ + "tools/bolt-python/concepts/ai-apps", + "tools/bolt-python/concepts/assistant-class" + ] + }, { "type": "category", "label": "Slack API calls", @@ -39,18 +51,6 @@ "tools/bolt-python/concepts/app-home" ] }, - { - "type": "category", - "label": "Creating agents", - "link": { - "type": "doc", - "id": "tools/bolt-python/concepts/ai-apps" - }, - "items": [ - "tools/bolt-python/concepts/ai-apps", - "tools/bolt-python/concepts/assistant-class" - ] - }, "tools/bolt-python/concepts/ai-apps", { "type": "category", diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/ai-apps.md index 83714cf00..5a1338614 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/ai-apps.md @@ -4,7 +4,12 @@ sidebar_label: Overview # Creating agents with Bolt -The Slack platform offers features tailored for AI agents. Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind, +::tip[Check out the Support Agent sample app] + +The code snippets throughout this guide are from our [Support Agent sample app](https://github.com/slack-samples/bolt-python-support-agent), Casey, which supports integration with Pydantic, Anthropic, and OpenAI. View our [agent quickstart](/ai/agent-quickstart) to get up and running with Casey. Otherwise, read on for exploration and explanation of agent-focused Bolt features found within Casey. +::: + +The Slack platform offers features tailored for AI agents. Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/tools/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind. If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). Then come back here to implement them with Bolt! @@ -15,7 +20,7 @@ If you're unfamiliar with using these feature within Slack, you may want to read Agents can be invoked throughout Slack, such as @mentions in channels, messages to the app, and using the assistant side panel. - + ```python import re @@ -191,7 +196,7 @@ async def handle_message( :::tip[Using the Assistant side panel] -The Assistant side panel requires additional setup. See the [Assistant class guide](/bolt-python/concepts/assistant-class). +The Assistant side panel requires additional setup. See the [Assistant class guide](/tools/bolt-python/concepts/assistant-class). ::: @@ -265,8 +270,6 @@ The `say_stream` utility streamlines calling the Python Slack SDK's [`WebClient. If neither a `channel_id` or `thread_ts` can be sourced, then the utility will be `None`. -You can see how our [Support Agent](https://github.com/slack-samples/bolt-python-support-agent) sample app uses `say_stream` when responding to DMs below. - ```python from slack_bolt import SayStream diff --git a/docs/english/concepts/assistant-class.md b/docs/english/concepts/assistant-class.md index 628ef352e..d7a42fc78 100644 --- a/docs/english/concepts/assistant-class.md +++ b/docs/english/concepts/assistant-class.md @@ -4,7 +4,7 @@ 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. +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: @@ -324,6 +324,6 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: ... ``` -See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. +See the [_Creating agents: adding and handling feedback_](/tools/bolt-python/concepts/ai-apps#adding-and-handling-feedback) section for adding feedback buttons with Block Kit. 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. \ No newline at end of file From 2f9bdca7f4d7463b9d167e9bf57f77b3dbb5b7c6 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:29:54 -0700 Subject: [PATCH 09/17] oof --- docs/english/_sidebar.json | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index fa2fe3813..1ae9d05d6 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -51,7 +51,6 @@ "tools/bolt-python/concepts/app-home" ] }, - "tools/bolt-python/concepts/ai-apps", { "type": "category", "label": "Custom Steps", From e2f95ce32dfcf9d16169112338b780cd327d2809 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:34:54 -0700 Subject: [PATCH 10/17] go --- docs/english/_sidebar.json | 8 ++++---- .../concepts/{ai-apps.md => developing-an-agent.md} | 8 +------- docs/english/{building-an-app.md => developing-an-app.md} | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) rename docs/english/concepts/{ai-apps.md => developing-an-agent.md} (99%) rename docs/english/{building-an-app.md => developing-an-app.md} (99%) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index 1ae9d05d6..ab417d992 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -7,16 +7,16 @@ }, "tools/bolt-python/getting-started", { "type": "html", "value": "
" }, - "tools/bolt-python/building-an-app", + "tools/bolt-python/developing-an-app", { "type": "category", - "label": "Creating agents", + "label": "Developing agents", "link": { "type": "doc", - "id": "tools/bolt-python/concepts/ai-apps" + "id": "tools/bolt-python/concepts/developing-an-agent" }, "items": [ - "tools/bolt-python/concepts/ai-apps", + "tools/bolt-python/concepts/developing-an-agent", "tools/bolt-python/concepts/assistant-class" ] }, diff --git a/docs/english/concepts/ai-apps.md b/docs/english/concepts/developing-an-agent.md similarity index 99% rename from docs/english/concepts/ai-apps.md rename to docs/english/concepts/developing-an-agent.md index 5a1338614..68ee38291 100644 --- a/docs/english/concepts/ai-apps.md +++ b/docs/english/concepts/developing-an-agent.md @@ -2,10 +2,9 @@ sidebar_label: Overview --- -# Creating agents with Bolt +# Developing an agent with Bolt ::tip[Check out the Support Agent sample app] - The code snippets throughout this guide are from our [Support Agent sample app](https://github.com/slack-samples/bolt-python-support-agent), Casey, which supports integration with Pydantic, Anthropic, and OpenAI. View our [agent quickstart](/ai/agent-quickstart) to get up and running with Casey. Otherwise, read on for exploration and explanation of agent-focused Bolt features found within Casey. ::: @@ -671,8 +670,3 @@ def handle_app_mentioned(
- ---- - -For a dedicated AI assistant experience with a side panel, see the [Assistant class guide](./assistant-class.md). - diff --git a/docs/english/building-an-app.md b/docs/english/developing-an-app.md similarity index 99% rename from docs/english/building-an-app.md rename to docs/english/developing-an-app.md index bde340961..1ae792700 100644 --- a/docs/english/building-an-app.md +++ b/docs/english/developing-an-app.md @@ -1,8 +1,8 @@ --- -sidebar_label: Building an App +sidebar_label: Developing an app --- -# Building an App with Bolt for Python +# Developing an app with Bolt for Python This guide is meant to walk you through getting up and running with a Slack app using Bolt for Python. Along the way, we’ll create a new Slack app, set up your local environment, and develop an app that listens and responds to messages from a Slack workspace. From 76705e870cdf24b5f0ce44911735e346e7c297dc Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 15:54:31 -0700 Subject: [PATCH 11/17] go --- docs/english/_sidebar.json | 8 ++++---- ...developing-an-agent.md => creating-an-agent.md} | 14 +++++++++----- .../{developing-an-app.md => creating-an-app.md} | 6 +++--- 3 files changed, 16 insertions(+), 12 deletions(-) rename docs/english/concepts/{developing-an-agent.md => creating-an-agent.md} (97%) rename docs/english/{developing-an-app.md => creating-an-app.md} (99%) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index ab417d992..577304b44 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -7,16 +7,16 @@ }, "tools/bolt-python/getting-started", { "type": "html", "value": "
" }, - "tools/bolt-python/developing-an-app", + "tools/bolt-python/creating-an-app", { "type": "category", - "label": "Developing agents", + "label": "AI & Agents", "link": { "type": "doc", - "id": "tools/bolt-python/concepts/developing-an-agent" + "id": "tools/bolt-python/concepts/creating-an-agent" }, "items": [ - "tools/bolt-python/concepts/developing-an-agent", + "tools/bolt-python/concepts/creating-an-agent", "tools/bolt-python/concepts/assistant-class" ] }, diff --git a/docs/english/concepts/developing-an-agent.md b/docs/english/concepts/creating-an-agent.md similarity index 97% rename from docs/english/concepts/developing-an-agent.md rename to docs/english/concepts/creating-an-agent.md index 68ee38291..4e37e8c18 100644 --- a/docs/english/concepts/developing-an-agent.md +++ b/docs/english/concepts/creating-an-agent.md @@ -1,16 +1,20 @@ --- -sidebar_label: Overview +sidebar_label: Creating an agent --- -# Developing an agent with Bolt +# Creating an agent with Bolt for Python -::tip[Check out the Support Agent sample app] +:::tip[Check out the Support Agent sample app] The code snippets throughout this guide are from our [Support Agent sample app](https://github.com/slack-samples/bolt-python-support-agent), Casey, which supports integration with Pydantic, Anthropic, and OpenAI. View our [agent quickstart](/ai/agent-quickstart) to get up and running with Casey. Otherwise, read on for exploration and explanation of agent-focused Bolt features found within Casey. ::: -The Slack platform offers features tailored for AI agents. Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/tools/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind. +Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/tools/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind. -If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). Then come back here to implement them with Bolt! +If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). + +## Prerequisites + +You'll need a Slack app to mold into an agent via in this guide. Follow the [quickstart](/tools/bolt-python/getting-started), and then come back here! --- diff --git a/docs/english/developing-an-app.md b/docs/english/creating-an-app.md similarity index 99% rename from docs/english/developing-an-app.md rename to docs/english/creating-an-app.md index 1ae792700..7f06e9d42 100644 --- a/docs/english/developing-an-app.md +++ b/docs/english/creating-an-app.md @@ -1,8 +1,8 @@ --- -sidebar_label: Developing an app +sidebar_label: Creating an app --- -# Developing an app with Bolt for Python +# Creating an app with Bolt for Python This guide is meant to walk you through getting up and running with a Slack app using Bolt for Python. Along the way, we’ll create a new Slack app, set up your local environment, and develop an app that listens and responds to messages from a Slack workspace. @@ -10,7 +10,7 @@ When you're finished, you'll have created the [Getting Started app](https://gith --- -### Create an app {#create-an-app} +### Create a new app {#create-an-app} First thing's first: before you start developing with Bolt, you'll want to [create a Slack app](https://api.slack.com/apps/new). :::tip[A place to test and learn] From 26717e451e1d6080cf6688673a26a12082a99cbc Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 16:25:03 -0700 Subject: [PATCH 12/17] go --- docs/english/_sidebar.json | 6 +- ...g-an-agent.md => adding-agent-features.md} | 74 +++++++++++++++++-- ...-class.md => using-the-assistant-class.md} | 2 +- 3 files changed, 73 insertions(+), 9 deletions(-) rename docs/english/concepts/{creating-an-agent.md => adding-agent-features.md} (90%) rename docs/english/concepts/{assistant-class.md => using-the-assistant-class.md} (99%) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json index 577304b44..79721bdcd 100644 --- a/docs/english/_sidebar.json +++ b/docs/english/_sidebar.json @@ -13,11 +13,11 @@ "label": "AI & Agents", "link": { "type": "doc", - "id": "tools/bolt-python/concepts/creating-an-agent" + "id": "tools/bolt-python/concepts/adding-agent-features" }, "items": [ - "tools/bolt-python/concepts/creating-an-agent", - "tools/bolt-python/concepts/assistant-class" + "tools/bolt-python/concepts/adding-agent-features", + "tools/bolt-python/concepts/using-the-assistant-class" ] }, { diff --git a/docs/english/concepts/creating-an-agent.md b/docs/english/concepts/adding-agent-features.md similarity index 90% rename from docs/english/concepts/creating-an-agent.md rename to docs/english/concepts/adding-agent-features.md index 4e37e8c18..db65cd45a 100644 --- a/docs/english/concepts/creating-an-agent.md +++ b/docs/english/concepts/adding-agent-features.md @@ -1,11 +1,13 @@ --- -sidebar_label: Creating an agent +sidebar_label: Adding agent features --- -# Creating an agent with Bolt for Python +# Adding agent features with Bolt for Python :::tip[Check out the Support Agent sample app] -The code snippets throughout this guide are from our [Support Agent sample app](https://github.com/slack-samples/bolt-python-support-agent), Casey, which supports integration with Pydantic, Anthropic, and OpenAI. View our [agent quickstart](/ai/agent-quickstart) to get up and running with Casey. Otherwise, read on for exploration and explanation of agent-focused Bolt features found within Casey. +The code snippets throughout this guide are from our [Support Agent sample app](https://github.com/slack-samples/bolt-python-support-agent), Casey, which supports integration with Pydantic, Anthropic, and OpenAI. + +View our [agent quickstart](/ai/agent-quickstart) to get up and running with Casey. Otherwise, read on for exploration and explanation of agent-focused Bolt features found within Casey. ::: Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/tools/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind. @@ -14,7 +16,7 @@ If you're unfamiliar with using these feature within Slack, you may want to read ## Prerequisites -You'll need a Slack app to mold into an agent via in this guide. Follow the [quickstart](/tools/bolt-python/getting-started), and then come back here! +You'll need a Slack app to mold into an agent. Follow the [quickstart](/tools/bolt-python/getting-started), and then come back here! --- @@ -379,9 +381,71 @@ def handle_feedback_button( --- +## Defining agent tools + +Your agent can perform actions by defining tools. A _tool_ in an agent context is a function that calls an external system. + +The following example uses the Claude Agent SDK. + +```python title="agent/tools/create_support_ticket.py" +from claude_agent_sdk import tool + + +@tool( + name="create_support_ticket", + description="Create a new IT support ticket for issues that require human follow-up.", + input_schema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A concise title describing the issue.", + }, + "priority": { + "type": "string", + "description": "The ticket priority level.", + "enum": ["low", "medium", "high", "critical"], + }, + }, + "required": ["title", "priority"], + }, +) +async def create_support_ticket_tool(args): + """Create a new IT support ticket.""" + title = args["title"] + priority = args["priority"] + + ticket_id = f"INC-{random.randint(100000, 999999)}" + + text = ( + f"Support ticket created successfully.\n" + f"**Ticket ID:** {ticket_id}\n" + f"**Priority:** {priority}" + ) + + return {"content": [{"type": "text", "text": text}]} +``` + +Then import and register the tool: + +```python title="agent/casey.py" +from claude_agent_sdk import create_sdk_mcp_server +from agent.tools.create_support_ticket import create_support_ticket_tool + +casey_tools_server = create_sdk_mcp_server( + name="casey-tools", + version="1.0.0", + tools=[create_support_ticket_tool], +) +``` + +Your agent can then call those tools as needed. + +--- + ## Full example -Putting all those concepts together result in a dynamic agent ready to helpfully respond. +Putting all those concepts together results in a dynamic agent ready to helpfully respond. diff --git a/docs/english/concepts/assistant-class.md b/docs/english/concepts/using-the-assistant-class.md similarity index 99% rename from docs/english/concepts/assistant-class.md rename to docs/english/concepts/using-the-assistant-class.md index d7a42fc78..992e7a3c3 100644 --- a/docs/english/concepts/assistant-class.md +++ b/docs/english/concepts/using-the-assistant-class.md @@ -1,4 +1,4 @@ -# The Assistant class +# Using the Assistant class :::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. From 202f2a7fbe479af625c5e6018b8e56d364dad490 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 7 Apr 2026 16:28:30 -0700 Subject: [PATCH 13/17] go --- .../english/concepts/adding-agent-features.md | 70 +------------------ 1 file changed, 2 insertions(+), 68 deletions(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index db65cd45a..dae8bc449 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -12,17 +12,13 @@ View our [agent quickstart](/ai/agent-quickstart) to get up and running with Cas Your agent can utilize features applicable to messages throughout Slack, like [chat streaming](#text-streaming) and [feedback buttons](#adding-and-handling-feedback). They can also [utilize the `Assistant` class](/tools/bolt-python/concepts/assistant-class) for a side-panel view designed with AI in mind. -If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). - -## Prerequisites - -You'll need a Slack app to mold into an agent. Follow the [quickstart](/tools/bolt-python/getting-started), and then come back here! +If you're unfamiliar with using these feature within Slack, you may want to read the [API docs on the subject](/ai/). Then come back here to implement them with Bolt! --- ## Listening for user invocation -Agents can be invoked throughout Slack, such as @mentions in channels, messages to the app, and using the assistant side panel. +Agents can be invoked throughout Slack, such as via @mentions in channels, messaging the agent, and using the assistant side panel. @@ -381,68 +377,6 @@ def handle_feedback_button( --- -## Defining agent tools - -Your agent can perform actions by defining tools. A _tool_ in an agent context is a function that calls an external system. - -The following example uses the Claude Agent SDK. - -```python title="agent/tools/create_support_ticket.py" -from claude_agent_sdk import tool - - -@tool( - name="create_support_ticket", - description="Create a new IT support ticket for issues that require human follow-up.", - input_schema={ - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "A concise title describing the issue.", - }, - "priority": { - "type": "string", - "description": "The ticket priority level.", - "enum": ["low", "medium", "high", "critical"], - }, - }, - "required": ["title", "priority"], - }, -) -async def create_support_ticket_tool(args): - """Create a new IT support ticket.""" - title = args["title"] - priority = args["priority"] - - ticket_id = f"INC-{random.randint(100000, 999999)}" - - text = ( - f"Support ticket created successfully.\n" - f"**Ticket ID:** {ticket_id}\n" - f"**Priority:** {priority}" - ) - - return {"content": [{"type": "text", "text": text}]} -``` - -Then import and register the tool: - -```python title="agent/casey.py" -from claude_agent_sdk import create_sdk_mcp_server -from agent.tools.create_support_ticket import create_support_ticket_tool - -casey_tools_server = create_sdk_mcp_server( - name="casey-tools", - version="1.0.0", - tools=[create_support_ticket_tool], -) -``` - -Your agent can then call those tools as needed. - ---- - ## Full example Putting all those concepts together results in a dynamic agent ready to helpfully respond. From 30535b1349fa6882d53111bd8d3bd85cfe522056 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Wed, 8 Apr 2026 09:50:20 -0700 Subject: [PATCH 14/17] ai-chatbot --- .../english/tutorial/ai-chatbot/ai-chatbot.md | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/docs/english/tutorial/ai-chatbot/ai-chatbot.md b/docs/english/tutorial/ai-chatbot/ai-chatbot.md index 72005f817..beffa3ace 100644 --- a/docs/english/tutorial/ai-chatbot/ai-chatbot.md +++ b/docs/english/tutorial/ai-chatbot/ai-chatbot.md @@ -2,63 +2,70 @@ In this tutorial, you'll learn how to bring the power of AI into your Slack workspace using a chatbot called Bolty that uses Anthropic or OpenAI. Here's what we'll do with this sample app: -1. Create your app from an app manifest and clone a starter template +1. Create your app using the Slack CLI 2. Set up and run your local project 3. Create a workflow using Workflow Builder to summarize messages in conversations 4. Select your preferred API and model to customize Bolty's responses 5. Interact with Bolty via direct message, the `/ask-bolty` slash command, or by mentioning the app in conversations +Intrigued? First, grab your tools by following the three steps below. + + + +
+ ## Prerequisites {#prereqs} -Before getting started, you will need the following: +You will also need the following: -- a development workspace where you have permissions to install apps. If you don’t have a workspace, go ahead and set that up now — you can [go here](https://slack.com/get-started#create) to create one, or you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. +- a development workspace where you have permissions to install apps. If you don’t have a workspace you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. - a development environment with [Python 3.7](https://www.python.org/downloads/) or later. - an Anthropic or OpenAI account with sufficient credits, and in which you have generated a secret key. -**Skip to the code** -If you'd rather skip the tutorial and just head straight to the code, you can use our [Bolt for Python AI Chatbot sample](https://github.com/slack-samples/bolt-python-ai-chatbot) as a template. - -## Creating your app {#create-app} - -1. Navigate to the [app creation page](https://api.slack.com/apps/new) and select **From a manifest**. -2. Select the workspace you want to install the application in. -3. Copy the contents of the [`manifest.json`](https://github.com/slack-samples/bolt-python-ai-chatbot/blob/main/manifest.json) file into the text box that says **Paste your manifest code here** (within the **JSON** tab) and click **Next**. -4. Review the configuration and click **Create**. -5. You're now in your app configuration's **Basic Information** page. Navigate to the **Install App** link in the left nav and click **Install to Workspace**, then **Allow** on the screen that follows. - ### Obtaining and storing your environment variables {#environment-variables} Before you'll be able to successfully run the app, you'll need to first obtain and set some environment variables. -#### Slack tokens {#slack-tokens} - -From your app's page on [app settings](https://api.slack.com/apps) collect an app and bot token: - -1. On the **Install App** page, copy your **Bot User OAuth Token**. You will store this in your environment as `SLACK_BOT_TOKEN` (we'll get to that next). -2. Navigate to **Basic Information** and in the **App-Level Tokens** section , click **Generate Token and Scopes**. Add the [`connections:write`](/reference/scopes/connections.write) scope, name the token, and click **Generate**. (For more details, refer to [understanding OAuth scopes for bots](/authentication/tokens#bot)). Copy this token. You will store this in your environment as `SLACK_APP_TOKEN`. - -To store your tokens and environment variables, run the following commands in the terminal. Replace the placeholder values with your bot and app tokens collected above: - -**For macOS** - -```bash -export SLACK_BOT_TOKEN= -export SLACK_APP_TOKEN= -``` - -**For Windows** - -```pwsh -set SLACK_BOT_TOKEN= -set SLACK_APP_TOKEN= -``` - #### Provider tokens {#provider-tokens} Models from different AI providers are available if the corresponding environment variable is added as shown in the sections below. -##### Anthropic {#anthropic} + + To interact with Anthropic models, navigate to your Anthropic account dashboard to [create an API key](https://console.anthropic.com/settings/keys), then export the key as follows: @@ -66,7 +73,8 @@ To interact with Anthropic models, navigate to your Anthropic account dashboard export ANTHROPIC_API_KEY= ``` -##### Google Cloud Vertex AI {#google-cloud-vertex-ai} + + To use Google Cloud Vertex AI, [follow this quick start](https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart-multimodal#expandable-1) to create a project for sending requests to the Gemini API, then gather [Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc) with the strategy to match your development environment. @@ -79,7 +87,8 @@ export VERTEX_AI_LOCATION= The project location can be located under the **Region** on the [Vertex AI](https://console.cloud.google.com/vertex-ai) dashboard, as well as more details about available Gemini models. -##### OpenAI {#openai} + + Unlock the OpenAI models from your OpenAI account dashboard by clicking [create a new secret key](https://platform.openai.com/api-keys), then export the key like so: @@ -87,49 +96,46 @@ Unlock the OpenAI models from your OpenAI account dashboard by clicking [create export OPENAI_API_KEY= ``` -## Setting up and running your local project {#configure-project} - -Clone the starter template onto your machine by running the following command: - -```bash -git clone https://github.com/slack-samples/bolt-python-ai-chatbot.git -``` + + -Change into the new project directory: +## Setting up and running your local project {#configure-project} -```bash -cd bolt-python-ai-chatbot -``` Start your Python virtual environment: -**For macOS** + + ```bash python3 -m venv .venv source .venv/bin/activate ``` -**For Windows** + + ```bash py -m venv .venv .venv\Scripts\activate ``` + + + Install the required dependencies: ```bash pip install -r requirements.txt ``` -Start your local server: +RUn your app locally: ```bash -python app.py +slack run ``` -If your app is up and running, you'll see a message that says "⚡️ Bolt app is running!" +If your app is indeed up and running, you'll see a message that says "⚡️ Bolt app is running!" ## Choosing your provider {#provider} @@ -235,5 +241,4 @@ You can also navigate to **Bolty** in your **Apps** list and select the **Messag Congratulations! You've successfully integrated the power of AI into your workspace. Check out these links to take the next steps in your Bolt for Python journey. - To learn more about Bolt for Python, refer to the [Getting started](/tools/bolt-python/getting-started) documentation. -- For more details about creating workflow steps using the Bolt SDK, refer to the [workflow steps for Bolt](/workflows/workflow-steps) guide. -- To use the Bolt for Python SDK to develop on the automations platform, refer to the [Create a workflow step for Workflow Builder: Bolt for Python](/tools/bolt-python/tutorial/custom-steps-workflow-builder-new) tutorial. +- For more details about creating workflow steps using the Bolt SDK, refer to the [workflow steps for Bolt](/workflows/workflow-steps) guide. \ No newline at end of file From dc2947daae2f3e6db81831d21a16a697cf0e74ae Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Wed, 8 Apr 2026 10:14:51 -0700 Subject: [PATCH 15/17] typo --- docs/english/tutorial/ai-chatbot/ai-chatbot.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/english/tutorial/ai-chatbot/ai-chatbot.md b/docs/english/tutorial/ai-chatbot/ai-chatbot.md index beffa3ace..66a9eec52 100644 --- a/docs/english/tutorial/ai-chatbot/ai-chatbot.md +++ b/docs/english/tutorial/ai-chatbot/ai-chatbot.md @@ -39,7 +39,6 @@ Intrigued? First, grab your tools by following the three steps below. windows: 'slack create ai-chatbot --template slack-samples/bolt-python-ai-chatbot' } } - } ]} buttonText="View sample app" buttonLink="https://github.com/slack-samples/bolt-python-ai-chatbot" From 6126666927a752ab8ab282d30e8869de61f03fbb Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Wed, 8 Apr 2026 10:16:19 -0700 Subject: [PATCH 16/17] typo --- docs/english/tutorial/ai-chatbot/ai-chatbot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/english/tutorial/ai-chatbot/ai-chatbot.md b/docs/english/tutorial/ai-chatbot/ai-chatbot.md index 66a9eec52..e4698ef37 100644 --- a/docs/english/tutorial/ai-chatbot/ai-chatbot.md +++ b/docs/english/tutorial/ai-chatbot/ai-chatbot.md @@ -96,7 +96,7 @@ export OPENAI_API_KEY= ```
-
+
## Setting up and running your local project {#configure-project} From 85a52e5f507c4e8045197851249b1a16dfec8978 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Wed, 8 Apr 2026 10:24:51 -0700 Subject: [PATCH 17/17] go --- docs/english/tutorial/ai-chatbot/ai-chatbot.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/english/tutorial/ai-chatbot/ai-chatbot.md b/docs/english/tutorial/ai-chatbot/ai-chatbot.md index e4698ef37..2fcc16e9a 100644 --- a/docs/english/tutorial/ai-chatbot/ai-chatbot.md +++ b/docs/english/tutorial/ai-chatbot/ai-chatbot.md @@ -1,15 +1,17 @@ # AI Chatbot -In this tutorial, you'll learn how to bring the power of AI into your Slack workspace using a chatbot called Bolty that uses Anthropic or OpenAI. Here's what we'll do with this sample app: +In this tutorial, you'll learn how to bring the power of AI into your Slack workspace using a chatbot called Bolty that uses Anthropic or OpenAI. -1. Create your app using the Slack CLI -2. Set up and run your local project -3. Create a workflow using Workflow Builder to summarize messages in conversations -4. Select your preferred API and model to customize Bolty's responses -5. Interact with Bolty via direct message, the `/ask-bolty` slash command, or by mentioning the app in conversations +With Bolty, users can: + +- send direct messages to Bolty and get AI-powered responses in response, +- use the `/ask-bolty` slash command to ask Bolty questions, and +- receive channel summaries when joining new channels. Intrigued? First, grab your tools by following the three steps below. +import QuickstartGuide from '@site/src/components/QuickstartGuide'; +