diff --git a/listeners/__init__.py b/listeners/__init__.py index fee0334..523ce09 100644 --- a/listeners/__init__.py +++ b/listeners/__init__.py @@ -1,5 +1,10 @@ -from listeners import events +from .assistant import assistant def register_listeners(app): - events.register(app) + # Using assistant middleware is the recommended way. + app.assistant(assistant) + + # The following event listeners demonstrate how to implement the same on your own. + # from listeners import events + # events.register(app) diff --git a/listeners/assistant.py b/listeners/assistant.py new file mode 100644 index 0000000..817030f --- /dev/null +++ b/listeners/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 .llm_caller import call_llm + +# 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 = call_llm(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 = call_llm(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/listeners/events/__init__.py b/listeners/events/__init__.py index fe3e3c5..a2029af 100644 --- a/listeners/events/__init__.py +++ b/listeners/events/__init__.py @@ -1,3 +1,7 @@ +# This sample app repository contains event listener code to help developers understand what's happening under the hood. +# We recommend using assistant middleware instead of these event listeners. +# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/. + from typing import Dict, Any from slack_bolt import App diff --git a/listeners/events/assistant_thread_started.py b/listeners/events/assistant_thread_started.py index 0866d64..0d5abc6 100644 --- a/listeners/events/assistant_thread_started.py +++ b/listeners/events/assistant_thread_started.py @@ -1,3 +1,7 @@ +# This sample app repository contains event listener code to help developers understand what's happening under the hood. +# We recommend using assistant middleware instead of these event listeners. +# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/. + from typing import List, Dict from logging import Logger diff --git a/listeners/events/asssistant_thread_context_changed.py b/listeners/events/asssistant_thread_context_changed.py index 502f528..4541f20 100644 --- a/listeners/events/asssistant_thread_context_changed.py +++ b/listeners/events/asssistant_thread_context_changed.py @@ -1,3 +1,7 @@ +# This sample app repository contains event listener code to help developers understand what's happening under the hood. +# We recommend using assistant middleware instead of these event listeners. +# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/. + from slack_sdk import WebClient from slack_bolt import BoltContext diff --git a/listeners/events/thread_context_store.py b/listeners/events/thread_context_store.py index 4413b5c..282cdfb 100644 --- a/listeners/events/thread_context_store.py +++ b/listeners/events/thread_context_store.py @@ -1,3 +1,7 @@ +# This sample app repository contains event listener code to help developers understand what's happening under the hood. +# We recommend using assistant middleware instead of these event listeners. +# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/. + from typing import Optional from slack_sdk import WebClient from slack_bolt import BoltContext diff --git a/listeners/events/user_message.py b/listeners/events/user_message.py index 1e1b1b4..3d3b5cf 100644 --- a/listeners/events/user_message.py +++ b/listeners/events/user_message.py @@ -1,10 +1,14 @@ +# This sample app repository contains event listener code to help developers understand what's happening under the hood. +# We recommend using assistant middleware instead of these event listeners. +# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/. + from typing import List, Dict from logging import Logger from slack_sdk.web import WebClient from slack_sdk.errors import SlackApiError from slack_bolt import BoltContext -from .llm_caller import call_llm +from ..llm_caller import call_llm from .thread_context_store import get_thread_context diff --git a/listeners/events/llm_caller.py b/listeners/llm_caller.py similarity index 94% rename from listeners/events/llm_caller.py rename to listeners/llm_caller.py index a1e656c..863c0eb 100644 --- a/listeners/events/llm_caller.py +++ b/listeners/llm_caller.py @@ -13,7 +13,10 @@ """ -def call_llm(messages_in_thread: List[Dict[str, str]], system_content: str = DEFAULT_SYSTEM_CONTENT) -> str: +def call_llm( + messages_in_thread: List[Dict[str, str]], + system_content: str = DEFAULT_SYSTEM_CONTENT, +) -> str: openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) messages = [{"role": "system", "content": system_content}] messages.extend(messages_in_thread) diff --git a/requirements.txt b/requirements.txt index eb94218..6150416 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -slack-bolt>=1.20.1,<2 +slack-bolt>=1.21,<2 slack-sdk>=3.33.1,<4 # If you use a different LLM vendor, replace this dependency openai