Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions ai/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .openai import OpenAI_API
from .vertexai import VertexAPI


"""
New AI providers must be added below.
`get_available_providers()`
Expand Down
1 change: 1 addition & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# Initialization
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

logging.basicConfig(level=logging.DEBUG)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 question: I notice debug logs aren't appearing for me... Does the ordering of logger initialization matter for this?

🗣️ thought: We might hold off on such changes for another PR but this diff caught my eye for now-


# Register Listeners
Expand Down
2 changes: 2 additions & 0 deletions listeners/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from listeners import assistant
from listeners import actions
from listeners import commands
from listeners import events
from listeners import functions


def register_listeners(app):
assistant.register(app)
actions.register(app)
commands.register(app)
events.register(app)
Expand Down
6 changes: 6 additions & 0 deletions listeners/assistant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .assistant import assistant


def register(app):
# Using assistant middleware is the recommended way.
app.assistant(assistant)
114 changes: 114 additions & 0 deletions listeners/assistant/assistant.py
Original file line number Diff line number Diff line change
@@ -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.",
},
]
Comment on lines +25 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ praise: Nice alignment with the assistant template - it makes me wonder if we wonder how maintenance and updates of both projects might continue going forward...

🔗 https://github.com/slack-samples/bolt-python-assistant-template/blob/7af3ee1ea8863f1b50e8b98a89f5791d2828ee5b/listeners/assistant.py#L25-L38


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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 suggestion: The channels:join scope is required to use this method and might be good to include in the app manifest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching this!

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}")
4 changes: 3 additions & 1 deletion listeners/commands/ask_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
)
2 changes: 1 addition & 1 deletion listeners/events/app_mentioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
)
10 changes: 8 additions & 2 deletions listeners/events/app_messaged.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ praise: This was helpful for me in testing just now!

)
2 changes: 1 addition & 1 deletion listeners/functions/summary_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ def handle_summary_function_callback(
inputs: dict,
fail: Fail,
logger: Logger,
client: WebClient,
complete: Complete,
client: WebClient,
):
ack()
try:
Expand Down
7 changes: 7 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,8 +30,10 @@
"oauth_config": {
"scopes": {
"bot": [
"assistant:write",
"app_mentions:read",
"channels:history",
"channels:join",
"channels:read",
"chat:write",
"chat:write.public",
Expand Down Expand Up @@ -90,6 +95,8 @@
"bot_events": [
"app_home_opened",
"app_mention",
"assistant_thread_started",
"assistant_thread_context_changed",
"function_executed",
"message.channels",
"message.groups",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down