diff --git a/README.md b/README.md index 4c97d4b..c956330 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 @@ -10,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 @@ -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/ai/providers/__init__.py b/ai/providers/__init__.py index b5af903..2af7802 100644 --- a/ai/providers/__init__.py +++ b/ai/providers/__init__.py @@ -7,6 +7,7 @@ from .openai import OpenAI_API from .vertexai import VertexAPI + """ New AI providers must be added below. `get_available_providers()` diff --git a/app.py b/app.py index 854f80e..0157892 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ # Initialization app = App(token=os.environ.get("SLACK_BOT_TOKEN")) + 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) diff --git a/listeners/assistant/__init__.py b/listeners/assistant/__init__.py new file mode 100644 index 0000000..a96baa0 --- /dev/null +++ b/listeners/assistant/__init__.py @@ -0,0 +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 new file mode 100644 index 0000000..3975677 --- /dev/null +++ b/listeners/assistant/assistant.py @@ -0,0 +1,114 @@ +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_provider_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: Received an error from Bolty:\n{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_id = context.user_id + 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_provider_response(user_id, 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_provider_response(user_id, messages_in_thread) + say(returned_message) + + except Exception as e: + logger.exception(f"Failed to handle a user message event: {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 78ab15b..4b41b8c 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. """ @@ -40,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}", ) 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/manifest.json b/manifest.json index db4465c..db8ed6b 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,8 +30,10 @@ "oauth_config": { "scopes": { "bot": [ + "assistant:write", "app_mentions:read", "channels:history", + "channels:join", "channels:read", "chat:write", "chat:write.public", @@ -90,6 +95,8 @@ "bot_events": [ "app_home_opened", "app_mention", + "assistant_thread_started", + "assistant_thread_context_changed", "function_executed", "message.channels", "message.groups", 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