Skip to content
Merged
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
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ black .

Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on.

> [!NOTE]
> The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant/assistant.py`.

**`/listeners/assistant`**

Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes:
Expand Down
7 changes: 2 additions & 5 deletions listeners/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from listeners import actions
from listeners import assistant
from listeners import events


def register_listeners(app):

actions.register(app)
assistant.register(app)

# The following event listeners demonstrate how to implement the same on your own.
# from listeners import events
# events.register(app)
events.register(app)
83 changes: 42 additions & 41 deletions listeners/assistant/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,16 @@
from slack_bolt import Assistant, BoltContext, Say, SetStatus, SetSuggestedPrompts
from slack_bolt.context.get_thread_context import GetThreadContext
from slack_sdk import WebClient
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonObject, FeedbackButtonsElement
from slack_sdk.errors import SlackApiError

from ..views.feedback_block import create_feedback_block
from ..llm_caller import call_llm


# Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details
assistant = Assistant()


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


# This listener is invoked when a human user opened an assistant thread
@assistant.thread_started
def start_assistant_thread(
Expand Down Expand Up @@ -86,17 +59,21 @@ def start_assistant_thread(
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="Drafting...",
status="thinking...",
loading_messages=[
"Teaching the hamsters to type faster…",
"Untangling the internet cables…",
Expand All @@ -106,21 +83,45 @@ def respond_in_assistant_thread(
],
)

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"]})
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}]
Comment on lines +86 to +105

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🪓 suggestion(non-blocking): I think later we should remove this in favor of a search method to showcase other AI focused features?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Super +1


else:
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)

stream_response = client.chat_startStream(
channel=channel_id,
recipient_team_id=team_id,
recipient_user_id=user_id,
thread_ts=thread_ts,
)
stream_ts = stream_response["ts"]
Expand Down
31 changes: 2 additions & 29 deletions listeners/events/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,6 @@
# 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
from slack_bolt.request.payload_utils import is_event

from .assistant_thread_started import start_thread_with_suggested_prompts
from .asssistant_thread_context_changed import save_new_thread_context
from .user_message import respond_to_user_message
from .app_mentioned import app_mentioned_callback


def register(app: App):
app.event("assistant_thread_started")(start_thread_with_suggested_prompts)
app.event("assistant_thread_context_changed")(save_new_thread_context)
app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message)
app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack)


def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
if is_event(body):
return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im"
return False


def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share")


def just_ack():
pass
app.event("app_mention")(app_mentioned_callback)
56 changes: 56 additions & 0 deletions listeners/events/app_mentioned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from logging import Logger
from slack_sdk import WebClient
from slack_bolt import Say

from ..llm_caller import call_llm
from ..views.feedback_block import create_feedback_block

"""
Handles the event when the app is mentioned in a Slack conversation
and generates an AI response.
"""


def app_mentioned_callback(client: WebClient, event: dict, logger: Logger, say: Say):
try:
channel_id = event.get("channel")
team_id = event.get("team")
text = event.get("text")
thread_ts = event.get("thread_ts") or event.get("ts")
user_id = event.get("user")

client.assistant_threads_setStatus(
channel_id=channel_id,
thread_ts=thread_ts,
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…",
],
)

returned_message = call_llm([{"role": "user", "content": text}])

stream_response = client.chat_startStream(
channel=channel_id, recipient_team_id=team_id, recipient_user_id=user_id, thread_ts=thread_ts
)

stream_ts = stream_response["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":
client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}")
else:
continue

feedback_block = create_feedback_block()
client.chat_stopStream(channel=channel_id, ts=stream_ts, blocks=feedback_block)

except Exception as e:
logger.exception(f"Failed to handle a user message event: {e}")
say(f":warning: Something went wrong! ({e})")
68 changes: 0 additions & 68 deletions listeners/events/assistant_thread_started.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⭐ praise: Thanks so much for removing this! I'm a fan of the unlock this brings for adjacent events.

This file was deleted.

23 changes: 0 additions & 23 deletions listeners/events/asssistant_thread_context_changed.py

This file was deleted.

Loading