Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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)
92 changes: 43 additions & 49 deletions listeners/assistant/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,17 @@
from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts
from slack_bolt.context.get_thread_context import GetThreadContext
from slack_sdk import WebClient
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject
from slack_sdk.errors import SlackApiError

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


# 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,31 +60,49 @@ def respond_in_assistant_thread(
payload: dict,
logger: logging.Logger,
context: BoltContext,
get_thread_context: GetThreadContext,
client: WebClient,
say: Say,
):
try:
channel_id = payload["channel"]
team_id = payload["team"]
thread_ts = payload["thread_ts"]

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…",
]

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"]})
user_id = payload["user"]
user_message = payload["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)

Expand All @@ -120,6 +112,8 @@ def respond_in_assistant_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
39 changes: 22 additions & 17 deletions listeners/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,37 @@
# 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 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 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 .assistant_mentioned import assistant_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)
app.event("app_mention")(assistant_mentioned_callback)


# 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)

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.

👁️‍🗨️ thought: I'm wondering if we want to move the explicit event listener implementation to the Bolt Python upstream examples instead of commenting this out here?

🔍 ramble: We have a few related examples at this time and I think these listeners might find a nice event_app.py file or similar. Super curious if you think this makes sense for this template?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes i like this idea!

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.

❓ Do we want to keep this commented out rather than deleted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i left it commented out so that listeners/event could stay somewhat of an example folder. i can see how it might be confusing to have all of these comments in there though



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_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 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
# def just_ack():
# pass
62 changes: 62 additions & 0 deletions listeners/events/assistant_mentioned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from logging import Logger
from slack_sdk import WebClient
from slack_bolt import Say
from slack_bolt.context.get_thread_context import GetThreadContext
from typing import List, Dict

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

"""
Handles the event when the app is mentioned in a Slack channel, retrieves the conversation context,
and generates an AI response if text is provided, otherwise sends a default response
"""


def assistant_mentioned_callback(

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.

Suggested change
def assistant_mentioned_callback(
def app_mentioned_callback(

🤖 quibble: Matching the event might make this clear in lookups without limiting this to an "assistant" paradigm!

client: WebClient, event: dict, get_thread_context: GetThreadContext, logger: Logger, say: Say
):
try:

channel_id = event.get("channel")
thread_ts = event.get("thread_ts")
user_id = event.get("user")
team_id = event.get("team")
text = event.get("text")

if thread_ts:
conversation_context = client.conversations_replies(channel=channel_id, ts=thread_ts, limit=10)
else:
conversation_context = client.conversations_history(channel=channel_id, limit=50)
thread_ts = event["ts"]

messages_in_thread: List[Dict[str, str]] = []
for message in conversation_context["messages"]:
role = "user" if message.get("bot_id") is None else "assistant"
messages_in_thread.append({"role": role, "content": message["text"]})
if text:
returned_message = call_llm(messages_in_thread)

client.assistant_threads_setStatus(
channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages
)
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"]

# use of this for loop is specific to openai response method
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})")
11 changes: 11 additions & 0 deletions listeners/listeners_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This file defines default loading messages used by the Slack bot.
# Used in `assistant_mentioned.py` and `assistant.py`.


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…",
]
31 changes: 31 additions & 0 deletions listeners/views/feedback_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import List
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject


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
9 changes: 7 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"assistant:write",
"channels:join",
"im:history",
"channels:history",
"channels:read",
"groups:history",
"chat:write"
"chat:write",
"users:read"
]
}
},
Expand All @@ -34,7 +37,9 @@
"bot_events": [
"assistant_thread_context_changed",
"assistant_thread_started",
"message.im"
"message.im",
"app_mention",
"message.channels"
Comment on lines +40 to +42

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.

🧮 quibble: Hm perhaps in follow ups we revist the scopes and ordering here? I believe this makes inspecting the app a bit more simple at first glance-

]
},
"interactivity": {
Expand Down