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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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'; + Date: Thu, 9 Apr 2026 09:19:20 -0700 Subject: [PATCH 18/26] go --- .../english/concepts/adding-agent-features.md | 16 +- docs/english/getting-started.md | 265 ++++-------------- 2 files changed, 57 insertions(+), 224 deletions(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index dae8bc449..3d2d3f916 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -7,7 +7,7 @@ sidebar_label: Adding agent features :::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. +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. @@ -272,14 +272,12 @@ 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`. ```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() +app.message('*', async ({ sayStream }) => { + const stream = sayStream(); + await stream.append({ markdown_text: "Here's my response..." }); + await stream.append({ markdown_text: "And here's more..." }); + await stream.stop(); +}); ``` --- diff --git a/docs/english/getting-started.md b/docs/english/getting-started.md index 934dd3bae..1d83b08a7 100644 --- a/docs/english/getting-started.md +++ b/docs/english/getting-started.md @@ -18,70 +18,56 @@ In search of the complete guide to building an app from scratch? Check out the [ #### Prerequisites -A few tools are needed for the following steps. We recommend using the [**Slack CLI**](/tools/slack-cli/) for the smoothest experience, but other options remain available. - -You can also begin by installing git and downloading [Python 3.7 or later](https://www.python.org/downloads/), or the latest stable version of Python. Refer to [Python's setup and building guide](https://devguide.python.org/getting-started/setup-building/) for more details. - -Install the latest version of the Slack CLI to get started: - -- [Slack CLI for macOS & Linux](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux) -- [Slack CLI for Windows](/tools/slack-cli/guides/installing-the-slack-cli-for-windows) - -Then confirm a successful installation with the following command: - -```sh -$ slack version -``` - -An authenticated login is also required if this hasn't been done before: - -```sh -$ slack login -``` - -:::info[A place to belong] - -A workspace where development can happen is also needed. - -We recommend using [developer sandboxes](/tools/developer-sandboxes) to avoid disruptions where real work gets done. - -::: - -## Creating a project {#creating-a-project} - -With the toolchain configured, it's time to set up a new Bolt project. This contains the code that handles logic for your app. - -If you don’t already have a project, let’s create a new one! - - - - -A starter template can be used to start with project scaffolding: - -```sh -$ slack create first-bolt-app --template slack-samples/bolt-python-getting-started-app -$ cd first-bolt-app -``` - -After a project is created you'll have a `requirements.txt` file for app dependencies and a `.slack` directory for Slack CLI configuration. +A few tools are needed for the following steps: +* A workspace where development can happen is also needed. We recommend using [developer sandboxes](/tools/developer-sandboxes) to avoid disruptions where real work gets done. +* Git +* [Python 3.7 or later](https://www.python.org/downloads/). Refer to [Python's setup and building guide](https://devguide.python.org/getting-started/setup-building/) for more details. + + +import QuickstartGuide from '@site/src/components/QuickstartGuide'; + + + + +You now have the Slack CLI and the starter sample app ready for use. Open up the app in your editor of choice and let's explore what the agent can actually do. + + +## Setting up your environment + +After the project is created you'll have a `requirements.txt` file for app dependencies and a `.slack` directory for Slack CLI configuration. A few other files exist too, but we'll visit these later. - - - -A starter template can be cloned to start with project scaffolding: - -```sh -$ git clone https://github.com/slack-samples/bolt-python-getting-started-app first-bolt-app -$ cd first-bolt-app -``` - -Outlines of a project are taking shape, so we can move on to running the app! - - - - We recommend using a [Python virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) to manage your project's dependencies. This is a great way to prevent conflicts with your system's Python packages. Let's create and activate a new virtual environment with [Python 3.7 or later](https://www.python.org/downloads/): ```sh @@ -101,9 +87,6 @@ $ which python3 Before you can start developing with Bolt, you will want a running Slack app. - - - The getting started app template contains a `manifest.json` file with details about an app that we will use to get started. Use the following command and select "Create a new app" to install the app to the team of choice: ```sh @@ -120,69 +103,6 @@ With the app running, you can test it out with the following steps in Slack: After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running. - - - -Navigate to your list of apps and [create a new Slack app](https://api.slack.com/apps/new) using the "from a manifest" option: - -1. Select the workspace to develop your app in. -2. Copy and paste the `manifest.json` file contents to create your app. -3. Confirm the app features and click "Create". - -You'll then land on your app's **Basic Information** page, which is an overview of your app and which contains important credentials: - -![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") - -To listen for events happening in Slack (such as a new posted message) without opening a port or exposing an endpoint, we will use [Socket Mode](/tools/bolt-python/concepts/socket-mode). This connection requires a specific app token: - -1. On the **Basic Information** page, scroll to the **App-Level Tokens** section and click **Generate Token and Scopes**. -2. Name the token "Development" or something similar and add the `connections:write` scope, then click **Generate**. -3. Save the generated `xapp` token as an environment variable within your project: - -```sh -$ export SLACK_APP_TOKEN= -``` - -The above command works on Linux and macOS but [similar commands are available on Windows](https://superuser.com/questions/212150/how-to-set-env-variable-in-windows-cmd-line/212153#212153). - -:::warning[Keep it secret. Keep it safe.] - -Treat your tokens like a password and [keep it safe](/security). Your app uses these to retrieve and send information to Slack. - -::: - -A bot token is also needed to interact with the Web API methods as your app's bot user. We can gather this as follows: - -1. Navigate to the **OAuth & Permissions** on the left sidebar and install your app to your workspace to generate a token. -2. After authorizing the installation, you'll return to the **OAuth & Permissions** page and find a **Bot User OAuth Token**: - -![OAuth Tokens](/img/bolt-python/bot-token.png "Bot OAuth Token") - -3. Copy the bot token beginning with `xoxb` from the **OAuth & Permissions page** and then store it in a new environment variable: - -```sh -$ export SLACK_BOT_TOKEN=xoxb- -``` - -After saving tokens for the app you created, it is time to run it: - -```sh -$ python3 app.py -... -⚡️ Bolt app is running! -``` - -With the app running, you can test it out with the following steps in Slack: - -1. Open a direct message with your app or invite the bot `@BoltApp` to a public channel. -2. Send "hello" to the current conversation and wait for a response. -3. Click the attached button labelled "Click Me" to post another reply. - -After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running. - - - - ## Updating the app At this point, you've successfully run the getting started Bolt for Python [app](https://github.com/slack-samples/bolt-python-getting-started-app)! @@ -207,9 +127,6 @@ def message_goodbye(say): Once the file is updated, save the changes and then we'll make sure those changes are being used. - - - Run the following command and select the app created earlier to start, or restart, your app with the latest changes: ```sh @@ -226,35 +143,10 @@ After finding the above output appears, open Slack to perform these steps: Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats. - - - -Run the following command to start, or restart, your app with the latest changes: - -```sh -$ python3 app.py -... -⚡️ Bolt app is running! -``` - -After finding the above output appears, open Slack to perform these steps: - -1. Return to the direct message or public channel with your bot. -2. Send "goodbye" to the conversation. -3. Receive a parting response from before and repeat "goodbye" to find another one. - -Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats. - - - - #### Customizing app settings The created app will have some placeholder values and a small set of [scopes](/reference/scopes) to start, but we recommend exploring the customizations possible on app settings. - - - Open app settings for your app with the following command: ```sh @@ -265,69 +157,12 @@ This will open the following page in a web browser: ![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") - - - -Browse to https://api.slack.com/apps and select your app "Getting Started Bolt App" from the list. - -This will open the following page: - -![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") - - - - On these pages you're free to make changes such as updating your app icon, configuring app features, and perhaps even distributing your app! -## Adding AI features {#ai-features} - -Now that you're familiar with a basic app setup, try it out again, this time using the AI agent template! - - - - -Get started with the agent template: - -```sh -$ slack create ai-app --template slack-samples/bolt-python-assistant-template -$ cd ai-app -``` - - - - -Get started with the agent template: - -```sh -$ git clone https://github.com/slack-samples/bolt-python-assistant-template ai-app -$ cd ai-app -``` - -Using this method, be sure to set the app and bot tokens as we did in the [Running the app](#running-the-app) section above. - - - - -Once the project is created, update the `.env.sample` file by setting the `OPENAI_API_KEY` with the value of your key and removing the `.sample` from the file name. - -In the `ai` folder of this app, you'll find default instructions for the LLM and an OpenAI client setup. - -The `listeners` include utilities intended for messaging with an LLM. Those are outlined in detail in the guide to [Using AI in apps](/tools/bolt-python/concepts/ai-apps) and [Sending messages](/tools/bolt-python/concepts/message-sending). - ## Next steps {#next-steps} -Congrats once more on getting up and running with this quick start. - -:::info[Dive deeper] - -Follow along with the steps that went into making this app on the [building an app](/tools/bolt-python/building-an-app) guide for an educational overview. - -::: - You can now continue customizing your app with various features to make it right for whatever job's at hand. Here are some ideas about what to explore next: -- Explore the different events your bot can listen to with the [`app.event()`](/tools/bolt-python/concepts/event-listening) method. See the full events reference [here](/reference/events). -- Bolt allows you to call [Web API](/tools/bolt-python/concepts/web-api) methods with the client attached to your app. There are [over 200 methods](/reference/methods) available. -- Learn more about the different [token types](/authentication/tokens) and [authentication setups](/tools/bolt-python/concepts/authenticating-oauth). Your app might need different tokens depending on the actions you want to perform or for installations to multiple workspaces. -- Receive events using HTTP for various deployment methods, such as deploying to Heroku or AWS Lambda. -- Read on [app design](/surfaces/app-design) and compose fancy messages with blocks using [Block Kit Builder](https://app.slack.com/block-kit-builder) to prototype messages. +- Follow along with the steps that went into making this app on the [building an app](/tools/bolt-python/building-an-app) guide for an educational overview. +- Check out the [Agent quickstart](/ai/agent-quickstart) to get up and running with an agent. +- Browse our [curated catalog of samples](/samples) for more apps to develop off of. \ No newline at end of file From aae04d8358da20357ab4063689cafce8a6a67b19 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 9 Apr 2026 11:55:44 -0700 Subject: [PATCH 19/26] go --- .../english/concepts/adding-agent-features.md | 71 ++++++ docs/english/getting-started.md | 214 ++++++++++++++---- 2 files changed, 238 insertions(+), 47 deletions(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index 3d2d3f916..ad2d0438e 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -379,6 +379,9 @@ def handle_feedback_button( Putting all those concepts together results in a dynamic agent ready to helpfully respond. + +
+ @@ -670,3 +673,71 @@ def handle_app_mentioned( +
+ +--- + +## Onward: adding custom tools + +Casey comes with test tools and simulated systems. You can extend it with custom tools to make it a fully functioning Slack agent. + +In this example, we'll add a tool that makes live calls to check the GitHub status. + +1. Create `agent/tools/{tool-name}.py` and define the tool with the `@tool` decorator: + +```python title="agent/tools/check_github_status.py" +from claude_agent_sdk import tool +import httpx + +@tool( + name="check_github_status", + description="Check GitHub's current operational status", + input_schema={}, +) +async def check_github_status_tool(args): + """Check if GitHub is operational.""" + async with httpx.AsyncClient() as client: + response = await client.get("https://www.githubstatus.com/api/v2/status.json") + data = response.json() + status = data["status"]["indicator"] + description = data["status"]["description"] + + return { + "content": [ + { + "type": "text", + "text": f"**GitHub Status** — {status}\n{description}", + } + ] + } +``` + +2. Import the tool in `agent/casey.py`: + +```python title="agent/casey.py" +from agent.tools import check_github_status_tool +``` + +3. Register in `casey_tools_server`: + +```python title="agent/casey.py" +casey_tools_server = create_sdk_mcp_server( + name="casey-tools", + version="1.0.0", + tools=[ + check_github_status_tool, # Add here + # ... other tools + ], +) +``` + +4. Add to `CASEY_TOOLS`: + +```python title="agent/casey.py" +CASEY_TOOLS = [ + "check_github_status", # Add here + # ... other tools +] +``` + +Use this example as a jumping off point for building out an agent with the capabilities you need! \ No newline at end of file diff --git a/docs/english/getting-started.md b/docs/english/getting-started.md index 1d83b08a7..8bbd17220 100644 --- a/docs/english/getting-started.md +++ b/docs/english/getting-started.md @@ -18,56 +18,70 @@ In search of the complete guide to building an app from scratch? Check out the [ #### Prerequisites -A few tools are needed for the following steps: -* A workspace where development can happen is also needed. We recommend using [developer sandboxes](/tools/developer-sandboxes) to avoid disruptions where real work gets done. -* Git -* [Python 3.7 or later](https://www.python.org/downloads/). Refer to [Python's setup and building guide](https://devguide.python.org/getting-started/setup-building/) for more details. - - -import QuickstartGuide from '@site/src/components/QuickstartGuide'; - - - - -You now have the Slack CLI and the starter sample app ready for use. Open up the app in your editor of choice and let's explore what the agent can actually do. - - -## Setting up your environment - -After the project is created you'll have a `requirements.txt` file for app dependencies and a `.slack` directory for Slack CLI configuration. +A few tools are needed for the following steps. We recommend using the [**Slack CLI**](/tools/slack-cli/) for the smoothest experience, but other options remain available. + +You can also begin by installing git and downloading [Python 3.7 or later](https://www.python.org/downloads/), or the latest stable version of Python. Refer to [Python's setup and building guide](https://devguide.python.org/getting-started/setup-building/) for more details. + +Install the latest version of the Slack CLI to get started: + +- [Slack CLI for macOS & Linux](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux) +- [Slack CLI for Windows](/tools/slack-cli/guides/installing-the-slack-cli-for-windows) + +Then confirm a successful installation with the following command: + +```sh +$ slack version +``` + +An authenticated login is also required if this hasn't been done before: + +```sh +$ slack login +``` + +:::info[A place to belong] + +A workspace where development can happen is also needed. + +We recommend using [developer sandboxes](/tools/developer-sandboxes) to avoid disruptions where real work gets done. + +::: + +## Creating a project {#creating-a-project} + +With the toolchain configured, it's time to set up a new Bolt project. This contains the code that handles logic for your app. + +If you don’t already have a project, let’s create a new one! + + + + +A starter template can be used to start with project scaffolding: + +```sh +$ slack create first-bolt-app --template slack-samples/bolt-python-getting-started-app +$ cd first-bolt-app +``` + +After a project is created you'll have a `requirements.txt` file for app dependencies and a `.slack` directory for Slack CLI configuration. A few other files exist too, but we'll visit these later. + + + +A starter template can be cloned to start with project scaffolding: + +```sh +$ git clone https://github.com/slack-samples/bolt-python-getting-started-app first-bolt-app +$ cd first-bolt-app +``` + +Outlines of a project are taking shape, so we can move on to running the app! + + + + We recommend using a [Python virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) to manage your project's dependencies. This is a great way to prevent conflicts with your system's Python packages. Let's create and activate a new virtual environment with [Python 3.7 or later](https://www.python.org/downloads/): ```sh @@ -87,6 +101,9 @@ $ which python3 Before you can start developing with Bolt, you will want a running Slack app. + + + The getting started app template contains a `manifest.json` file with details about an app that we will use to get started. Use the following command and select "Create a new app" to install the app to the team of choice: ```sh @@ -103,6 +120,69 @@ With the app running, you can test it out with the following steps in Slack: After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running. + + + +Navigate to your list of apps and [create a new Slack app](https://api.slack.com/apps/new) using the "from a manifest" option: + +1. Select the workspace to develop your app in. +2. Copy and paste the `manifest.json` file contents to create your app. +3. Confirm the app features and click "Create". + +You'll then land on your app's **Basic Information** page, which is an overview of your app and which contains important credentials: + +![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") + +To listen for events happening in Slack (such as a new posted message) without opening a port or exposing an endpoint, we will use [Socket Mode](/tools/bolt-python/concepts/socket-mode). This connection requires a specific app token: + +1. On the **Basic Information** page, scroll to the **App-Level Tokens** section and click **Generate Token and Scopes**. +2. Name the token "Development" or something similar and add the `connections:write` scope, then click **Generate**. +3. Save the generated `xapp` token as an environment variable within your project: + +```sh +$ export SLACK_APP_TOKEN= +``` + +The above command works on Linux and macOS but [similar commands are available on Windows](https://superuser.com/questions/212150/how-to-set-env-variable-in-windows-cmd-line/212153#212153). + +:::warning[Keep it secret. Keep it safe.] + +Treat your tokens like a password and [keep it safe](/security). Your app uses these to retrieve and send information to Slack. + +::: + +A bot token is also needed to interact with the Web API methods as your app's bot user. We can gather this as follows: + +1. Navigate to the **OAuth & Permissions** on the left sidebar and install your app to your workspace to generate a token. +2. After authorizing the installation, you'll return to the **OAuth & Permissions** page and find a **Bot User OAuth Token**: + +![OAuth Tokens](/img/bolt-python/bot-token.png "Bot OAuth Token") + +3. Copy the bot token beginning with `xoxb` from the **OAuth & Permissions page** and then store it in a new environment variable: + +```sh +$ export SLACK_BOT_TOKEN=xoxb- +``` + +After saving tokens for the app you created, it is time to run it: + +```sh +$ python3 app.py +... +⚡️ Bolt app is running! +``` + +With the app running, you can test it out with the following steps in Slack: + +1. Open a direct message with your app or invite the bot `@BoltApp` to a public channel. +2. Send "hello" to the current conversation and wait for a response. +3. Click the attached button labelled "Click Me" to post another reply. + +After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running. + + + + ## Updating the app At this point, you've successfully run the getting started Bolt for Python [app](https://github.com/slack-samples/bolt-python-getting-started-app)! @@ -127,6 +207,9 @@ def message_goodbye(say): Once the file is updated, save the changes and then we'll make sure those changes are being used. + + + Run the following command and select the app created earlier to start, or restart, your app with the latest changes: ```sh @@ -143,10 +226,35 @@ After finding the above output appears, open Slack to perform these steps: Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats. + + + +Run the following command to start, or restart, your app with the latest changes: + +```sh +$ python3 app.py +... +⚡️ Bolt app is running! +``` + +After finding the above output appears, open Slack to perform these steps: + +1. Return to the direct message or public channel with your bot. +2. Send "goodbye" to the conversation. +3. Receive a parting response from before and repeat "goodbye" to find another one. + +Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats. + + + + #### Customizing app settings The created app will have some placeholder values and a small set of [scopes](/reference/scopes) to start, but we recommend exploring the customizations possible on app settings. + + + Open app settings for your app with the following command: ```sh @@ -157,6 +265,18 @@ This will open the following page in a web browser: ![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") + + + +Browse to https://api.slack.com/apps and select your app "Getting Started Bolt App" from the list. + +This will open the following page: + +![Basic Information page](/img/bolt-python/basic-information-page.png "Basic Information page") + + + + On these pages you're free to make changes such as updating your app icon, configuring app features, and perhaps even distributing your app! ## Next steps {#next-steps} From bee551a69001baeb1e7363690ff317b02da4699f Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 9 Apr 2026 13:21:37 -0700 Subject: [PATCH 20/26] go --- .../english/concepts/adding-agent-features.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index ad2d0438e..64cb927d2 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -16,6 +16,59 @@ If you're unfamiliar with using these feature within Slack, you may want to read --- +## Slack MCP Server {#slack-mcp-server} + +Casey can harness the [Slack MCP Server](https://docs.slack.dev/ai/slack-mcp-server/developing) when deployed via an HTTP Server with OAuth. + +To enable the Slack MCP Server: + +1. Install [ngrok](https://ngrok.com/download) and start a tunnel: + +```sh +ngrok http 3000 +``` + +2. Copy the `https://*.ngrok-free.app` URL from the ngrok output. + +3. Update `manifest.json` for HTTP mode: + - Set `socket_mode_enabled` to `false` + - Replace `ngrok-free.app` with your ngrok domain (e.g. `YOUR_NGROK_SUBDOMAIN.ngrok-free.app`) + +4. Create a new local dev app: + +```sh +slack install -E local +``` + +5. Enable MCP for your app: + - Run `slack app settings` to open your app's settings + - Navigate to **Agents & AI Apps** in the left-side navigation + - Toggle **Model Context Protocol** on + +6. Update your `.env` OAuth environment variables: + - Run `slack app settings` to open App Settings + - Copy **Client ID**, **Client Secret**, and **Signing Secret** + - Update `SLACK_REDIRECT_URI` in `.env` with your ngrok domain + +```sh +SLACK_CLIENT_ID=YOUR_CLIENT_ID +SLACK_CLIENT_SECRET=YOUR_CLIENT_SECRET +SLACK_REDIRECT_URI=https://YOUR_NGROK_SUBDOMAIN.ngrok-free.app/slack/oauth_redirect +SLACK_SIGNING_SECRET=YOUR_SIGNING_SECRET +``` + +7. Start the app: + +```sh +slack run app_oauth.py +``` + +8. Click the install URL printed in the terminal to install the app to your workspace via OAuth. + +Your agent can now access the Slack MCP server! + +--- + ## Listening for user invocation Agents can be invoked throughout Slack, such as via @mentions in channels, messaging the agent, and using the assistant side panel. From 5383db09f7807fe4bfce31fda900cfa51a9b52ad Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:22:22 -0700 Subject: [PATCH 21/26] Apply suggestions from code review Co-authored-by: Tracy Rericha <108959677+technically-tracy@users.noreply.github.com> --- docs/english/concepts/adding-agent-features.md | 2 +- docs/english/concepts/using-the-assistant-class.md | 2 +- docs/english/getting-started.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index 64cb927d2..8e228f6a2 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -337,7 +337,7 @@ app.message('*', async ({ sayStream }) => { ## 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: +You can use 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 ( diff --git a/docs/english/concepts/using-the-assistant-class.md b/docs/english/concepts/using-the-assistant-class.md index 992e7a3c3..bf2d002b0 100644 --- a/docs/english/concepts/using-the-assistant-class.md +++ b/docs/english/concepts/using-the-assistant-class.md @@ -326,4 +326,4 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: 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 +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 from. \ No newline at end of file diff --git a/docs/english/getting-started.md b/docs/english/getting-started.md index 8bbd17220..9a16950d5 100644 --- a/docs/english/getting-started.md +++ b/docs/english/getting-started.md @@ -285,4 +285,4 @@ You can now continue customizing your app with various features to make it right - Follow along with the steps that went into making this app on the [building an app](/tools/bolt-python/building-an-app) guide for an educational overview. - Check out the [Agent quickstart](/ai/agent-quickstart) to get up and running with an agent. -- Browse our [curated catalog of samples](/samples) for more apps to develop off of. \ No newline at end of file +- Browse our [curated catalog of samples](/samples) for more apps to use as a starting point for development. \ No newline at end of file From e2106096440c4900326659770b34d276df1d9f1f Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 9 Apr 2026 15:52:26 -0700 Subject: [PATCH 22/26] go --- docs/english/concepts/using-the-assistant-class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/english/concepts/using-the-assistant-class.md b/docs/english/concepts/using-the-assistant-class.md index bf2d002b0..ed004dc35 100644 --- a/docs/english/concepts/using-the-assistant-class.md +++ b/docs/english/concepts/using-the-assistant-class.md @@ -324,6 +324,6 @@ def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: ... ``` -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. +See the [_Creating agents: adding and handling feedback_](/tools/bolt-python/concepts/adding-agent-features#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 from. \ No newline at end of file From bb812e179f18fff1fc57db1ee69866d26a499af5 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 9 Apr 2026 16:26:50 -0700 Subject: [PATCH 23/26] go --- .../english/concepts/adding-agent-features.md | 48 +--- docs/english/getting-started.md | 2 +- docs/japanese/concepts/assistant.md | 227 ------------------ 3 files changed, 2 insertions(+), 275 deletions(-) delete mode 100644 docs/japanese/concepts/assistant.md diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index 8e228f6a2..e59fe415e 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -196,53 +196,7 @@ async def handle_message( 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"), - ) + ... ```
diff --git a/docs/english/getting-started.md b/docs/english/getting-started.md index 9a16950d5..6964df23b 100644 --- a/docs/english/getting-started.md +++ b/docs/english/getting-started.md @@ -283,6 +283,6 @@ On these pages you're free to make changes such as updating your app icon, confi You can now continue customizing your app with various features to make it right for whatever job's at hand. Here are some ideas about what to explore next: -- Follow along with the steps that went into making this app on the [building an app](/tools/bolt-python/building-an-app) guide for an educational overview. +- Follow along with the steps that went into making this app on the [creating an app](/tools/bolt-python/creating-an-app) guide for an educational overview. - Check out the [Agent quickstart](/ai/agent-quickstart) to get up and running with an agent. - Browse our [curated catalog of samples](/samples) for more apps to use as a starting point for development. \ No newline at end of file diff --git a/docs/japanese/concepts/assistant.md b/docs/japanese/concepts/assistant.md deleted file mode 100644 index 664108607..000000000 --- a/docs/japanese/concepts/assistant.md +++ /dev/null @@ -1,227 +0,0 @@ -# エージェント・アシスタント - -このページは、Bolt を使ってエージェント・アシスタントを実装するための方法を紹介します。この機能に関する一般的な情報については、[こちらのドキュメントページ(英語)](/ai/)を参照してください。 - -この機能を実装するためには、まず[アプリの設定画面](https://api.slack.com/apps)で **Agents & Assistants** 機能を有効にし、**OAuth & Permissions** のページで [`assistant:write`](/reference/scopes/assistant.write)、[chat:write](/reference/scopes/chat.write)、[`im:history`](/reference/scopes/im.history) を**ボットの**スコープに追加し、**Event Subscriptions** のページで [`assistant_thread_started`](/reference/events/assistant_thread_started)、[`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed)、[`message.im`](/reference/events/message.im) イベントを有効にしてください。 - -また、この機能は Slack の有料プランでのみ利用可能です。もし開発用の有料プランのワークスペースをお持ちでない場合は、[Developer Program](https://api.slack.com/developer-program) に参加し、全ての有料プラン向け機能を利用可能なサンドボックス環境をつくることができます。 - -ユーザーとのアシスタントスレッド内でのやりとりを処理するには、`assistant_thread_started`、`assistant_thread_context_changed`、`message` イベントの `app.event(...)` リスナーを使うことも可能ですが、Bolt はよりシンプルなアプローチを提供しています。`Assistant` インスタンスを作り、それに必要なイベントリスナーを追加し、最後にこのアシスタント設定を `App` インスタンスに渡すだけでよいのです。 - -```python -assistant = Assistant() - -# ユーザーがアシスタントスレッドを開いたときに呼び出されます -@assistant.thread_started -def start_assistant_thread(say: Say, set_suggested_prompts: SetSuggestedPrompts): - # ユーザーに対して最初の返信を送信します - say(":wave: Hi, how can I help you today?") - - # プロンプト例を送るのは必須ではありません - set_suggested_prompts( - prompts=[ - # もしプロンプトが長い場合は {"title": "表示する短いラベル", "message": "完全なプロンプト"} を使うことができます - "What does SLACK stand for?", - "When Slack was released?", - ], - ) - -# ユーザーがスレッド内で返信したときに呼び出されます -@assistant.user_message -def respond_in_assistant_thread( - payload: dict, - logger: logging.Logger, - context: BoltContext, - set_status: SetStatus, - say: Say, - client: WebClient, -): - try: - # ユーザーにこのbotがリクエストを受信して作業中であることを伝えます - set_status("is typing...") - - # 会話の履歴を取得します - replies_in_thread = client.conversations_replies( - channel=context.channel_id, - ts=context.thread_ts, - oldest=context.thread_ts, - limit=10, - ) - messages_in_thread: List[Dict[str, str]] = [] - for message in replies_in_thread["messages"]: - role = "user" if message.get("bot_id") is None else "assistant" - messages_in_thread.append({"role": role, "content": message["text"]}) - - # プロンプトと会話の履歴を LLM に渡します(この call_llm はあなた自身のコードです) - returned_message = call_llm(messages_in_thread) - - # 結果をアシスタントスレッドに送信します - say(text=returned_message) - - except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") - # エラーになった場合は必ずメッセージを送信するようにしてください - # そうしなかった場合、'is typing...' の表示のままになってしまい、ユーザーは会話を続けることができなくなります - say(f":warning: Sorry, something went wrong during processing your request (error: {e})") - -# このミドルウェアを Bolt アプリに追加します -app.use(assistant) -``` - -リスナーに指定可能な引数の一覧は[モジュールドキュメント](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html)を参考にしてください。 - -ユーザーがチャンネルの横でアシスタントスレッドを開いた場合、そのチャンネルの情報は、そのスレッドの `AssistantThreadContext` データとして保持され、 `get_thread_context` ユーティリティを使ってアクセスすることができます。Bolt がこのユーティリティを提供している理由は、後続のユーザーメッセージ投稿のイベントペイロードに最新のスレッドのコンテキスト情報は含まれないためです。そのため、アプリはコンテキスト情報が変更されたタイミングでそれを何らかの方法で保存し、後続のメッセージイベントのリスナーコードから参照できるようにする必要があります。 - -そのユーザーがチャンネルを切り替えた場合、`assistant_thread_context_changed` イベントがあなたのアプリに送信されます。(上記のコード例のように)組み込みの `Assistant` ミドルウェアをカスタム設定なしで利用している場合、この更新されたチャンネル情報は、自動的にこのアシスタントボットからの最初の返信のメッセージメタデータとして保存されます。これは、組み込みの仕組みを使う場合は、このコンテキスト情報を自前で用意したデータストアに保存する必要はないということです。この組み込みの仕組みの唯一の短所は、追加の Slack API 呼び出しによる処理時間のオーバーヘッドです。具体的には `get_thread_context` を実行したときに、この保存されたメッセージメタデータにアクセスするために `conversations.history` API が呼び出されます。 - -このデータを別の場所に保存したい場合、自前の `AssistantThreadContextStore` 実装を `Assistant` のコンストラクターに渡すことができます。リファレンス実装として、`FileAssistantThreadContextStore` というローカルファイルシステムを使って実装を提供しています: - -```python -# これはあくまで例であり、自前のものを渡すことができます -from slack_bolt import FileAssistantThreadContextStore -assistant = Assistant(thread_context_store=FileAssistantThreadContextStore()) -``` - -このリファレンス実装はローカルファイルに依存しており、本番環境での利用は推奨しません。本番アプリでは `AssistantThreadContextStore` を継承した自前のクラスを使うようにしてください。 - -最後に、動作する完全なサンプルコード例を確認したい場合は、私たちが GitHub 上で提供している[サンプルアプリのリポジトリ](https://github.com/slack-samples/bolt-python-assistant-template)をチェックしてみてください。 - -## アシスタントスレッドでの Block Kit インタラクション - -より高度なユースケースでは、上のようなプロンプト例の提案ではなく Block Kit のボタンなどを使いたいという場合があるかもしれません。そして、後続の処理のために[構造化されたメッセージメタデータ](/messaging/message-metadata/)を含むメッセージを送信したいという場合もあるでしょう。 - -例えば、アプリが最初の返信で「参照しているチャンネルを要約」のようなボタンを表示し、ユーザーがそれをクリックして、より詳細な情報(例:要約するメッセージ数・日数、要約の目的など)を送信、アプリがそれを構造化されたメータデータに整理した上でリクエスト内容をボットのメッセージとして送信するようなシナリオです。 - -デフォルトでは、アプリはそのアプリ自身から送信したボットメッセージに応答することはできません(Bolt にはあらかじめ無限ループを防止する制御が入っているため)。`ignoring_self_assistant_message_events_enabled=False` を `App` のコンストラクターに渡し、`bot_message` リスナーを `Assistant` ミドルウェアに追加すると、上記の例のようなリクエストを伝えるボットメッセージを使って処理を継続することができるようになります。 - -```python -app = App( - token=os.environ["SLACK_BOT_TOKEN"], - # bot message を受け取るには必ずこれを指定してください - ignoring_self_assistant_message_events_enabled=False, -) - -assistant = Assistant() - -# リスナーに指定可能な引数の一覧は https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html を参照してください - -@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": [ - # 複数のボタンを配置することが可能です - { - "type": "button", - "action_id": "assistant-generate-random-numbers", - "text": {"type": "plain_text", "text": "Generate random numbers"}, - "value": "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"}, - # アシスタントスレッドの情報を app.view リスナーに引き継ぎます - "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"}, - # 自然言語のテキストではなく、あらかじめ決められた形式の入力を受け取ることができます - "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"}, - }, - } - ], - }, - ) - -# 上のモーダルが送信されたときに実行されます -@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"]) - - # 構造化された入力情報とともにボットのメッセージを送信します - # 以下の assistant.bot_message リスナーが処理を継続します - # このリスナー内で処理したい場合はそれでも構いません! - # bot_message リスナーが必要ない場合は 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)}, - }, - ) - -# このアプリのボットユーザーがメッセージを送信したときに実行されます -@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": - # 上の random-number-generation リクエストを処理します - 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: - # それ以外のパターンでは何もしません - # さらに他のパターンを追加する場合、メッセージ送信の無限ループを起こさないよう注意して実装してください - pass - - except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") - -# ユーザーが返信したときに実行されます -@assistant.user_message -def respond_to_user_messages(logger: logging.Logger, set_status: SetStatus, say: Say): - try: - set_status("is typing...") - say("Please use the buttons in the first reply instead :bow:") - except Exception as e: - logger.exception(f"Failed to respond to an inquiry: {e}") - say(f":warning: Sorry, something went wrong during processing your request (error: {e})") - -# このミドルウェアを Bolt アプリに追加します -app.use(assistant) -``` \ No newline at end of file From 90597484310237a1c7784f9371b989ed5c34853b Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 9 Apr 2026 17:06:16 -0700 Subject: [PATCH 24/26] summary oops --- docs/english/concepts/adding-agent-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index e59fe415e..65e5d4ca9 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -388,7 +388,7 @@ Putting all those concepts together results in a dynamic agent ready to helpfull
- +Full example From 4a544a5976cb3f3ac6f26952de4285db7570f2d5 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 10 Apr 2026 09:26:58 -0700 Subject: [PATCH 25/26] sync --- .../english/concepts/adding-agent-features.md | 78 +++++++++---------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index 65e5d4ca9..90ab5df0f 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -131,25 +131,25 @@ def handle_app_mentioned( ```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 slack_bolt.context import BoltContext +from slack_bolt.context.say import Say +from slack_bolt.context.say_stream import SayStream +from slack_bolt.context.set_status import SetStatus +from slack_sdk import WebClient 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, +def handle_message( + client: WebClient, + context: BoltContext, event: dict, logger: Logger, - say: AsyncSay, - say_stream: AsyncSayStream, - set_status: AsyncSetStatus, + say: Say, + say_stream: SayStream, + set_status: SetStatus, ): """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 @@ -211,9 +211,7 @@ The Assistant side panel requires additional setup. See the [Assistant class gui ```py from logging import Logger -from slack_bolt.context.set_suggested_prompts.async_set_suggested_prompts import ( - AsyncSetSuggestedPrompts, -) +from slack_bolt.context.set_suggested_prompts import SetSuggestedPrompts SUGGESTED_PROMPTS = [ {"title": "Reset Password", "message": "I need to reset my password"}, @@ -222,12 +220,12 @@ SUGGESTED_PROMPTS = [ ] -async def handle_assistant_thread_started( - set_suggested_prompts: AsyncSetSuggestedPrompts, logger: Logger +def handle_assistant_thread_started( + set_suggested_prompts: SetSuggestedPrompts, logger: Logger ): """Handle assistant thread started events by setting suggested prompts.""" try: - await set_suggested_prompts( + set_suggested_prompts( prompts=SUGGESTED_PROMPTS, title="How can I help you today?", ) @@ -279,12 +277,10 @@ 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`. ```python -app.message('*', async ({ sayStream }) => { - const stream = sayStream(); - await stream.append({ markdown_text: "Here's my response..." }); - await stream.append({ markdown_text: "And here's more..." }); - await stream.stop(); -}); +streamer = say_stream() +streamer.append(markdown_text="Here's my response...") +streamer.append(markdown_text="And here's more...") +streamer.stop() ``` --- @@ -492,25 +488,25 @@ def handle_app_mentioned( 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 slack_bolt.context import BoltContext +from slack_bolt.context.say import Say +from slack_bolt.context.say_stream import SayStream +from slack_bolt.context.set_status import SetStatus +from slack_sdk import WebClient 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, +def handle_app_mentioned( + client: WebClient, + context: BoltContext, event: dict, logger: Logger, - say: AsyncSay, - say_stream: AsyncSayStream, - set_status: AsyncSetStatus, + say: Say, + say_stream: SayStream, + set_status: SetStatus, ): """Handle @Casey mentions in channels.""" try: @@ -522,7 +518,7 @@ async def handle_app_mentioned( cleaned_text = re.sub(r"<@[A-Z0-9]+>", "", text).strip() if not cleaned_text: - await say( + 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, ) @@ -530,14 +526,14 @@ async def handle_app_mentioned( # Add eyes reaction only to the first message (not threaded replies) if not event.get("thread_ts"): - await client.reactions_add( + client.reactions_add( channel=channel_id, timestamp=event["ts"], name="eyes", ) # Set assistant thread status with loading messages - await set_status( + set_status( status="Thinking...", loading_messages=[ "Teaching the hamsters to type faster…", @@ -559,15 +555,15 @@ async def handle_app_mentioned( thread_ts=thread_ts, message_ts=event["ts"], ) - response_text, new_session_id = await run_casey_agent( + response_text, new_session_id = 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) + streamer = say_stream() + streamer.append(markdown_text=response_text) feedback_blocks = build_feedback_blocks() - await streamer.stop(blocks=feedback_blocks) + streamer.stop(blocks=feedback_blocks) # Store session ID for future context if new_session_id: From 328a7a5f743f67a795a9a4e3017260c616861b71 Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:27:32 -0700 Subject: [PATCH 26/26] Apply suggestions from code review Co-authored-by: William Bergamin --- docs/english/concepts/adding-agent-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/english/concepts/adding-agent-features.md b/docs/english/concepts/adding-agent-features.md index 90ab5df0f..cbd164630 100644 --- a/docs/english/concepts/adding-agent-features.md +++ b/docs/english/concepts/adding-agent-features.md @@ -240,7 +240,7 @@ def handle_assistant_thread_started( ## Setting status {#setting-assistant-status} -Your app can show its users action is happening behind the scenes by setting its thread status. +Your app can show actions are happening behind the scenes by setting its thread status. ```python def handle_app_mentioned(