From 85c0bdcf1e1d01ff6a5ecb7eaac797e4d06288c5 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 11:33:58 -0400 Subject: [PATCH 1/9] feat: assistant class added --- ai/providers/__init__.py | 11 ++++ app.py | 4 +- listeners/assistant/__init__.py | 5 ++ listeners/assistant/assistant.py | 109 +++++++++++++++++++++++++++++++ manifest.json | 8 ++- 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 listeners/assistant/__init__.py create mode 100644 listeners/assistant/assistant.py diff --git a/ai/providers/__init__.py b/ai/providers/__init__.py index b5af903..42be1fa 100644 --- a/ai/providers/__init__.py +++ b/ai/providers/__init__.py @@ -7,6 +7,8 @@ from .openai import OpenAI_API from .vertexai import VertexAPI +from slack_bolt import BoltContext + """ New AI providers must be added below. `get_available_providers()` @@ -58,3 +60,12 @@ def get_provider_response( return response except Exception as e: raise e + +def get_assistant_response( + context: BoltContext, + prompt: str, + conversation_history: Optional[List] = None, + system_content=DEFAULT_SYSTEM_CONTENT, +): + user_id = context.user_id + get_provider_response(user_id, prompt, conversation_history, system_content) diff --git a/app.py b/app.py index 854f80e..a20a075 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,15 @@ import os import logging -from slack_bolt import App +from slack_bolt import App, Assistant from slack_bolt.adapter.socket_mode import SocketModeHandler from listeners import register_listeners # Initialization app = App(token=os.environ.get("SLACK_BOT_TOKEN")) +assistant = Assistant() +app.use(assistant) logging.basicConfig(level=logging.DEBUG) # Register Listeners diff --git a/listeners/assistant/__init__.py b/listeners/assistant/__init__.py new file mode 100644 index 0000000..41e017e --- /dev/null +++ b/listeners/assistant/__init__.py @@ -0,0 +1,5 @@ +from .assistant import assistant + +def register(app): + # Using assistant middleware is the recommended way. + app.assistant(assistant) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py new file mode 100644 index 0000000..b6658db --- /dev/null +++ b/listeners/assistant/assistant.py @@ -0,0 +1,109 @@ +import logging +from typing import List, Dict +from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts, SetStatus +from slack_bolt.context.get_thread_context import GetThreadContext +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from ai.providers import get_assistant_response + +# Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details +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: + say("How can I help you?") + + prompts: List[Dict[str, str]] = [ + { + "title": "What does Slack stand for?", + "message": "Slack, a business communication service, was named after an acronym. Can you guess what it stands for?", + }, + { + "title": "Write a draft announcement", + "message": "Can you write a draft announcement about a new feature my team just released? It must include how impactful it is.", + }, + { + "title": "Suggest names for my Slack app", + "message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.", + }, + ] + + thread_context = get_thread_context() + if thread_context is not None and thread_context.channel_id is not None: + summarize_channel = { + "title": "Summarize the referred channel", + "message": "Can you generate a brief summary of the referred channel?", + } + prompts.append(summarize_channel) + + set_suggested_prompts(prompts=prompts) + except Exception as e: + logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) + say(f":warning: Something went wrong! ({e})") + + +# This listener is invoked when the human user sends a reply in the assistant thread +@assistant.user_message +def respond_in_assistant_thread( + payload: dict, + logger: logging.Logger, + context: BoltContext, + set_status: SetStatus, + get_thread_context: GetThreadContext, + client: WebClient, + say: Say, +): + try: + user_message = payload["text"] + set_status("is typing...") + + if user_message == "Can you generate a brief summary of the referred channel?": + # the logic here requires the additional bot scopes: + # channels:join, channels:history, groups:history + thread_context = get_thread_context() + referred_channel_id = thread_context.get("channel_id") + try: + channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + except SlackApiError as e: + if e.response["error"] == "not_in_channel": + # If this app's bot user is not in the public channel, + # we'll try joining the channel and then calling the same API again + client.conversations_join(channel=referred_channel_id) + channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + else: + raise e + + prompt = f"Can you generate a brief summary of these messages in a Slack channel <#{referred_channel_id}>?\n\n" + for message in reversed(channel_history.get("messages")): + if message.get("user") is not None: + prompt += f"\n<@{message['user']}> says: {message['text']}\n" + messages_in_thread = [{"role": "user", "content": prompt}] + returned_message = get_assistant_response(context, messages_in_thread) + say(returned_message) + return + + 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 = get_assistant_response(context, messages_in_thread) + say(returned_message) + + except Exception as e: + logger.exception(f"Failed to handle a user message event: {e}") + say(f":warning: Something went wrong! ({e})") diff --git a/manifest.json b/manifest.json index db4465c..8a8d6af 100644 --- a/manifest.json +++ b/manifest.json @@ -7,6 +7,9 @@ "name": "Bolty" }, "features": { + "assistant_view":{ + "assistant_description": "Interact with an AI chatbot." + }, "app_home": { "home_tab_enabled": true, "messages_tab_enabled": true, @@ -27,6 +30,7 @@ "oauth_config": { "scopes": { "bot": [ + "assistant:write", "app_mentions:read", "channels:history", "channels:read", @@ -94,7 +98,9 @@ "message.channels", "message.groups", "message.im", - "message.mpim" + "message.mpim", + "assistant_thread_started", + "assistant_thread_context_changed" ] }, "interactivity": { From b3d9a592b15a2072b11d6bbd0a134a0908e4d850 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 12:05:24 -0400 Subject: [PATCH 2/9] refactor: consolidate AI provider and assistant response functions --- ai/providers/__init__.py | 11 +---------- listeners/assistant/assistant.py | 7 ++++--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/ai/providers/__init__.py b/ai/providers/__init__.py index 42be1fa..8b5b813 100644 --- a/ai/providers/__init__.py +++ b/ai/providers/__init__.py @@ -59,13 +59,4 @@ def get_provider_response( response = provider.generate_response(full_prompt, system_content) return response except Exception as e: - raise e - -def get_assistant_response( - context: BoltContext, - prompt: str, - conversation_history: Optional[List] = None, - system_content=DEFAULT_SYSTEM_CONTENT, -): - user_id = context.user_id - get_provider_response(user_id, prompt, conversation_history, system_content) + raise e \ No newline at end of file diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index b6658db..93d570c 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -5,7 +5,7 @@ from slack_sdk import WebClient from slack_sdk.errors import SlackApiError -from ai.providers import get_assistant_response +from ai.providers import get_provider_response # Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details assistant = Assistant() @@ -63,6 +63,7 @@ def respond_in_assistant_thread( say: Say, ): try: + user_id = context.user_id user_message = payload["text"] set_status("is typing...") @@ -87,7 +88,7 @@ def respond_in_assistant_thread( if message.get("user") is not None: prompt += f"\n<@{message['user']}> says: {message['text']}\n" messages_in_thread = [{"role": "user", "content": prompt}] - returned_message = get_assistant_response(context, messages_in_thread) + returned_message = get_provider_response(user_id, messages_in_thread) say(returned_message) return @@ -101,7 +102,7 @@ def respond_in_assistant_thread( 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 = get_assistant_response(context, messages_in_thread) + returned_message = get_provider_response(user_id, messages_in_thread) say(returned_message) except Exception as e: From f69ad0569a4434a5414caeba80984f69e39d67a2 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 12:47:02 -0400 Subject: [PATCH 3/9] docs: document assistant features in README and add legacy handler warning --- README.md | 10 ++++++++++ listeners/events/app_messaged.py | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c97d4b..188150c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This Slack chatbot app template offers a customizable solution for integrating AI-powered conversations into your Slack workspace. Here's what the app can do out of the box: +* **Use the new Slack Assistant UI** - Start conversations in a dedicated side panel for a focused chat experience +* **Interactive chat with suggested prompts** - Get started quickly with pre-configured conversation starters * Interact with the bot by mentioning it in conversations and threads * Send direct messages to the bot for private interactions * Use the `/ask-bolty` command to communicate with the bot in channels where it hasn't been added @@ -121,6 +123,14 @@ ruff format . Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/commands` handles incoming [Slash Commands](https://api.slack.com/interactivity/slash-commands) requests, `/listeners/events` handles [Events](https://api.slack.com/apis/events-api) and so on. + +    **`/listeners/assistant`** + +    Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This includes: +* `@assistant.thread_started` - Manages when users start new assistant threads. +* `@assistant.user_message` - Processes user messages in assistant threads and app DMs. **Replaces traditional DM handling as seen in** `/listeners/events/app_messaged.py` + + ### `/ai` * `ai_constants.py`: Defines constants used throughout the AI module. diff --git a/listeners/events/app_messaged.py b/listeners/events/app_messaged.py index 78ab15b..323e8aa 100644 --- a/listeners/events/app_messaged.py +++ b/listeners/events/app_messaged.py @@ -7,7 +7,13 @@ from ..listener_utils.parse_conversation import parse_conversation """ -Handles the event when a direct message is sent to the bot, retrieves the conversation context, +WARNING: This callback is the traditional way of handling direct messages and is only used +when Slack Assistant features are turned off (when assistant:write scope is removed from manifest.json). + +When assistant:write is enabled in the manifest, messages are handled through the Assistant handlers +in listeners/assistant/assistant.py instead of this callback. + +Handles the event when a direct message is sent to non-assistant bots, retrieves the conversation context, and generates an AI response. """ From 162ed91ca88db9f5f4f46d47468476a2664d892e Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 12:51:05 -0400 Subject: [PATCH 4/9] fix: linter errors --- ai/providers/__init__.py | 3 +-- listeners/assistant/__init__.py | 1 + listeners/assistant/assistant.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ai/providers/__init__.py b/ai/providers/__init__.py index 8b5b813..2af7802 100644 --- a/ai/providers/__init__.py +++ b/ai/providers/__init__.py @@ -7,7 +7,6 @@ from .openai import OpenAI_API from .vertexai import VertexAPI -from slack_bolt import BoltContext """ New AI providers must be added below. @@ -59,4 +58,4 @@ def get_provider_response( response = provider.generate_response(full_prompt, system_content) return response except Exception as e: - raise e \ No newline at end of file + raise e diff --git a/listeners/assistant/__init__.py b/listeners/assistant/__init__.py index 41e017e..a96baa0 100644 --- a/listeners/assistant/__init__.py +++ b/listeners/assistant/__init__.py @@ -1,5 +1,6 @@ from .assistant import assistant + def register(app): # Using assistant middleware is the recommended way. app.assistant(assistant) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 93d570c..24ece43 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -73,13 +73,17 @@ def respond_in_assistant_thread( thread_context = get_thread_context() referred_channel_id = thread_context.get("channel_id") try: - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + channel_history = client.conversations_history( + channel=referred_channel_id, limit=50 + ) except SlackApiError as e: if e.response["error"] == "not_in_channel": # If this app's bot user is not in the public channel, # we'll try joining the channel and then calling the same API again client.conversations_join(channel=referred_channel_id) - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + channel_history = client.conversations_history( + channel=referred_channel_id, limit=50 + ) else: raise e From a55c3c493a4b6081f8667e366ed30731ec0f17f0 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 12:59:39 -0400 Subject: [PATCH 5/9] build: use ruff pinned version --- listeners/functions/summary_function.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/listeners/functions/summary_function.py b/listeners/functions/summary_function.py index b335138..3b15e1b 100644 --- a/listeners/functions/summary_function.py +++ b/listeners/functions/summary_function.py @@ -17,8 +17,8 @@ def handle_summary_function_callback( inputs: dict, fail: Fail, logger: Logger, - client: WebClient, complete: Complete, + client: WebClient, ): ack() try: diff --git a/requirements.txt b/requirements.txt index dde0998..ed16fed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ slack-bolt==1.24.0 pytest -ruff +ruff==0.13.0 slack-cli-hooks==0.1.0 openai==1.102.0 anthropic==0.65.0 From c68a9a9efd6b87afc9407a7ce52b19b3320e0a90 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 13:06:54 -0400 Subject: [PATCH 6/9] docs: link to bolt-python-assistant-template --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 188150c..d30ae5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This Slack chatbot app template offers a customizable solution for integrating A * Bring Your Own Language Model [BYO LLM](#byo-llm) for customization * Custom FileStateStore creates a file in /data per user to store API/model preferences -Inspired by [ChatGPT-in-Slack](https://github.com/seratch/ChatGPT-in-Slack/tree/main) +Inspired by [ChatGPT-in-Slack](https://github.com/seratch/ChatGPT-in-Slack/tree/main) and [Bolt Python Assistant Template](https://github.com/slack-samples/bolt-python-assistant-template) Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). ## Installation From b602aa3218b9af251debfd9a9e38150049a4f478 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 16:58:27 -0400 Subject: [PATCH 7/9] fix: duplicate assistant instances removed --- README.md | 4 ++-- app.py | 5 ++--- listeners/__init__.py | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d30ae5a..c956330 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,9 @@ ruff format . Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/commands` handles incoming [Slash Commands](https://api.slack.com/interactivity/slash-commands) requests, `/listeners/events` handles [Events](https://api.slack.com/apis/events-api) and so on. -    **`/listeners/assistant`** +**`/listeners/assistant`** -    Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This includes: +Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This includes: * `@assistant.thread_started` - Manages when users start new assistant threads. * `@assistant.user_message` - Processes user messages in assistant threads and app DMs. **Replaces traditional DM handling as seen in** `/listeners/events/app_messaged.py` diff --git a/app.py b/app.py index a20a075..0157892 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,14 @@ import os import logging -from slack_bolt import App, Assistant +from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler from listeners import register_listeners # Initialization app = App(token=os.environ.get("SLACK_BOT_TOKEN")) -assistant = Assistant() -app.use(assistant) + logging.basicConfig(level=logging.DEBUG) # Register Listeners diff --git a/listeners/__init__.py b/listeners/__init__.py index 0b862a3..b2c73ca 100644 --- a/listeners/__init__.py +++ b/listeners/__init__.py @@ -1,3 +1,4 @@ +from listeners import assistant from listeners import actions from listeners import commands from listeners import events @@ -5,6 +6,7 @@ def register_listeners(app): + assistant.register(app) actions.register(app) commands.register(app) events.register(app) From 342c10d864e0d48f93e8c3e3d7d9e3b467a7a7f3 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 17 Sep 2025 17:18:02 -0400 Subject: [PATCH 8/9] feat: enhance error message display in bolty responses --- listeners/assistant/assistant.py | 4 ++-- listeners/commands/ask_command.py | 4 +++- listeners/events/app_mentioned.py | 2 +- listeners/events/app_messaged.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 24ece43..3975677 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -48,7 +48,7 @@ def start_assistant_thread( 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})") + say(f":warning: Received an error from Bolty:\n{e}") # This listener is invoked when the human user sends a reply in the assistant thread @@ -111,4 +111,4 @@ def respond_in_assistant_thread( except Exception as e: logger.exception(f"Failed to handle a user message event: {e}") - say(f":warning: Something went wrong! ({e})") + say(f":warning: Received an error from Bolty:\n{e}") diff --git a/listeners/commands/ask_command.py b/listeners/commands/ask_command.py index fcd651d..68ff2c3 100644 --- a/listeners/commands/ask_command.py +++ b/listeners/commands/ask_command.py @@ -52,5 +52,7 @@ def ask_callback( except Exception as e: logger.error(e) client.chat_postEphemeral( - channel=channel_id, user=user_id, text=f"Received an error from Bolty:\n{e}" + channel=channel_id, + user=user_id, + text=f":warning: Received an error from Bolty:\n{e}", ) diff --git a/listeners/events/app_mentioned.py b/listeners/events/app_mentioned.py index 265b016..108c1bf 100644 --- a/listeners/events/app_mentioned.py +++ b/listeners/events/app_mentioned.py @@ -50,5 +50,5 @@ def app_mentioned_callback(client: WebClient, event: dict, logger: Logger, say: client.chat_update( channel=channel_id, ts=waiting_message["ts"], - text=f"Received an error from Bolty:\n{e}", + text=f":warning: Received an error from Bolty:\n{e}", ) diff --git a/listeners/events/app_messaged.py b/listeners/events/app_messaged.py index 323e8aa..4b41b8c 100644 --- a/listeners/events/app_messaged.py +++ b/listeners/events/app_messaged.py @@ -46,5 +46,5 @@ def app_messaged_callback(client: WebClient, event: dict, logger: Logger, say: S client.chat_update( channel=channel_id, ts=waiting_message["ts"], - text=f"Received an error from Bolty:\n{e}", + text=f":warning: Received an error from Bolty:\n{e}", ) From a20d5e192e33b1f40a5892d65d1715ee4ff0e74b Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 18 Sep 2025 21:12:00 -0400 Subject: [PATCH 9/9] fix: add channels:join to manifest.json --- manifest.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 8a8d6af..db8ed6b 100644 --- a/manifest.json +++ b/manifest.json @@ -33,6 +33,7 @@ "assistant:write", "app_mentions:read", "channels:history", + "channels:join", "channels:read", "chat:write", "chat:write.public", @@ -94,13 +95,13 @@ "bot_events": [ "app_home_opened", "app_mention", + "assistant_thread_started", + "assistant_thread_context_changed", "function_executed", "message.channels", "message.groups", "message.im", - "message.mpim", - "assistant_thread_started", - "assistant_thread_context_changed" + "message.mpim" ] }, "interactivity": {