Skip to content

Commit 0f19ab1

Browse files
committed
feat: add app_mention event handler
1 parent c4ba883 commit 0f19ab1

7 files changed

Lines changed: 178 additions & 73 deletions

File tree

listeners/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
from listeners import actions
22
from listeners import assistant
3+
from listeners import events
34

45

56
def register_listeners(app):
6-
77
actions.register(app)
88
assistant.register(app)
9-
10-
# The following event listeners demonstrate how to implement the same on your own.
11-
# from listeners import events
12-
# events.register(app)
9+
events.register(app)

listeners/assistant/assistant.py

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,17 @@
33
from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts
44
from slack_bolt.context.get_thread_context import GetThreadContext
55
from slack_sdk import WebClient
6-
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject
6+
from slack_sdk.errors import SlackApiError
77

8+
from ..views.feedback_block import create_feedback_block
89
from ..llm_caller import call_llm
10+
from ..listeners_constants import loading_messages
11+
912

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

1316

14-
def create_feedback_block() -> List[Block]:
15-
"""
16-
Create feedback block with thumbs up/down buttons
17-
18-
Returns:
19-
Block Kit context_actions block
20-
"""
21-
blocks: List[Block] = [
22-
ContextActionsBlock(
23-
elements=[
24-
FeedbackButtonsElement(
25-
action_id="feedback",
26-
positive_button=FeedbackButtonObject(
27-
text="Good Response",
28-
accessibility_label="Submit positive feedback on this response",
29-
value="good-feedback",
30-
),
31-
negative_button=FeedbackButtonObject(
32-
text="Bad Response",
33-
accessibility_label="Submit negative feedback on this response",
34-
value="bad-feedback",
35-
),
36-
)
37-
]
38-
)
39-
]
40-
return blocks
41-
42-
4317
# This listener is invoked when a human user opened an assistant thread
4418
@assistant.thread_started
4519
def start_assistant_thread(
@@ -86,31 +60,49 @@ def respond_in_assistant_thread(
8660
payload: dict,
8761
logger: logging.Logger,
8862
context: BoltContext,
63+
get_thread_context: GetThreadContext,
8964
client: WebClient,
9065
say: Say,
9166
):
9267
try:
9368
channel_id = payload["channel"]
69+
team_id = payload["team"]
9470
thread_ts = payload["thread_ts"]
95-
96-
loading_messages = [
97-
"Teaching the hamsters to type faster…",
98-
"Untangling the internet cables…",
99-
"Consulting the office goldfish…",
100-
"Polishing up the response just for you…",
101-
"Convincing the AI to stop overthinking…",
102-
]
103-
104-
replies = client.conversations_replies(
105-
channel=context.channel_id,
106-
ts=context.thread_ts,
107-
oldest=context.thread_ts,
108-
limit=10,
109-
)
110-
messages_in_thread: List[Dict[str, str]] = []
111-
for message in replies["messages"]:
112-
role = "user" if message.get("bot_id") is None else "assistant"
113-
messages_in_thread.append({"role": role, "content": message["text"]})
71+
user_id = payload["user"]
72+
user_message = payload["text"]
73+
74+
if user_message == "Can you generate a brief summary of the referred channel?":
75+
# the logic here requires the additional bot scopes:
76+
# channels:join, channels:history, groups:history
77+
thread_context = get_thread_context()
78+
referred_channel_id = thread_context.get("channel_id")
79+
try:
80+
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
81+
except SlackApiError as e:
82+
if e.response["error"] == "not_in_channel":
83+
# If this app's bot user is not in the public channel,
84+
# we'll try joining the channel and then calling the same API again
85+
client.conversations_join(channel=referred_channel_id)
86+
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
87+
else:
88+
raise e
89+
prompt = f"Can you generate a brief summary of these messages in a Slack channel <#{referred_channel_id}>?\n\n"
90+
for message in reversed(channel_history.get("messages")):
91+
if message.get("user") is not None:
92+
prompt += f"\n<@{message['user']}> says: {message['text']}\n"
93+
messages_in_thread = [{"role": "user", "content": prompt}]
94+
95+
else:
96+
replies = client.conversations_replies(
97+
channel=context.channel_id,
98+
ts=context.thread_ts,
99+
oldest=context.thread_ts,
100+
limit=10,
101+
)
102+
messages_in_thread: List[Dict[str, str]] = []
103+
for message in replies["messages"]:
104+
role = "user" if message.get("bot_id") is None else "assistant"
105+
messages_in_thread.append({"role": role, "content": message["text"]})
114106

115107
returned_message = call_llm(messages_in_thread)
116108

@@ -120,6 +112,8 @@ def respond_in_assistant_thread(
120112

121113
stream_response = client.chat_startStream(
122114
channel=channel_id,
115+
recipient_team_id=team_id,
116+
recipient_user_id=user_id,
123117
thread_ts=thread_ts,
124118
)
125119
stream_ts = stream_response["ts"]

listeners/events/__init__.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,37 @@
22
# We recommend using assistant middleware instead of these event listeners.
33
# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/.
44

5-
from typing import Dict, Any
5+
# from typing import Dict, Any
66

77
from slack_bolt import App
8-
from slack_bolt.request.payload_utils import is_event
98

10-
from .assistant_thread_started import start_thread_with_suggested_prompts
11-
from .asssistant_thread_context_changed import save_new_thread_context
12-
from .user_message import respond_to_user_message
9+
# from slack_bolt.request.payload_utils import is_event
10+
11+
# from .assistant_thread_started import start_thread_with_suggested_prompts
12+
# from .asssistant_thread_context_changed import save_new_thread_context
13+
# from .user_message import respond_to_user_message
14+
from .assistant_mentioned import assistant_mentioned_callback
1315

1416

1517
def register(app: App):
16-
app.event("assistant_thread_started")(start_thread_with_suggested_prompts)
17-
app.event("assistant_thread_context_changed")(save_new_thread_context)
18-
app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message)
19-
app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack)
18+
app.event("app_mention")(assistant_mentioned_callback)
19+
20+
21+
# app.event("assistant_thread_started")(start_thread_with_suggested_prompts)
22+
# app.event("assistant_thread_context_changed")(save_new_thread_context)
23+
# app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message)
24+
# app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack)
2025

2126

22-
def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
23-
if is_event(body):
24-
return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im"
25-
return False
27+
# def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
28+
# if is_event(body):
29+
# return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im"
30+
# return False
2631

2732

28-
def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
29-
return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share")
33+
# def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
34+
# return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share")
3035

3136

32-
def just_ack():
33-
pass
37+
# def just_ack():
38+
# pass
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from logging import Logger
2+
from slack_sdk import WebClient
3+
from slack_bolt import Say
4+
from slack_bolt.context.get_thread_context import GetThreadContext
5+
from typing import List, Dict
6+
7+
from ..llm_caller import call_llm
8+
from ..views.feedback_block import create_feedback_block
9+
from ..listeners_constants import loading_messages
10+
11+
"""
12+
Handles the event when the app is mentioned in a Slack channel, retrieves the conversation context,
13+
and generates an AI response if text is provided, otherwise sends a default response
14+
"""
15+
16+
17+
def assistant_mentioned_callback(
18+
client: WebClient, event: dict, get_thread_context: GetThreadContext, logger: Logger, say: Say
19+
):
20+
try:
21+
22+
channel_id = event.get("channel")
23+
thread_ts = event.get("thread_ts")
24+
user_id = event.get("user")
25+
team_id = event.get("team")
26+
text = event.get("text")
27+
28+
if thread_ts:
29+
conversation_context = client.conversations_replies(channel=channel_id, ts=thread_ts, limit=10)
30+
else:
31+
conversation_context = client.conversations_history(channel=channel_id, limit=50)
32+
thread_ts = event["ts"]
33+
34+
messages_in_thread: List[Dict[str, str]] = []
35+
for message in conversation_context["messages"]:
36+
role = "user" if message.get("bot_id") is None else "assistant"
37+
messages_in_thread.append({"role": role, "content": message["text"]})
38+
if text:
39+
returned_message = call_llm(messages_in_thread)
40+
41+
client.assistant_threads_setStatus(
42+
channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages
43+
)
44+
stream_response = client.chat_startStream(
45+
channel=channel_id, recipient_team_id=team_id, recipient_user_id=user_id, thread_ts=thread_ts
46+
)
47+
48+
stream_ts = stream_response["ts"]
49+
50+
# use of this for loop is specific to openai response method
51+
for event in returned_message:
52+
if event.type == "response.output_text.delta":
53+
client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}")
54+
else:
55+
continue
56+
57+
feedback_block = create_feedback_block()
58+
client.chat_stopStream(channel=channel_id, ts=stream_ts, blocks=feedback_block)
59+
60+
except Exception as e:
61+
logger.exception(f"Failed to handle a user message event: {e}")
62+
say(f":warning: Something went wrong! ({e})")

listeners/listeners_constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This file defines default loading messages used by the Slack bot.
2+
# Used in `assistant_mentioned.py` and `assistant.py`.
3+
4+
5+
loading_messages = [
6+
"Teaching the hamsters to type faster…",
7+
"Untangling the internet cables…",
8+
"Consulting the office goldfish…",
9+
"Polishing up the response just for you…",
10+
"Convincing the AI to stop overthinking…",
11+
]

listeners/views/feedback_block.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import List
2+
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonsElement, FeedbackButtonObject
3+
4+
5+
def create_feedback_block() -> List[Block]:
6+
"""
7+
Create feedback block with thumbs up/down buttons
8+
9+
Returns:
10+
Block Kit context_actions block
11+
"""
12+
blocks: List[Block] = [
13+
ContextActionsBlock(
14+
elements=[
15+
FeedbackButtonsElement(
16+
action_id="feedback",
17+
positive_button=FeedbackButtonObject(
18+
text="Good Response",
19+
accessibility_label="Submit positive feedback on this response",
20+
value="good-feedback",
21+
),
22+
negative_button=FeedbackButtonObject(
23+
text="Bad Response",
24+
accessibility_label="Submit negative feedback on this response",
25+
value="bad-feedback",
26+
),
27+
)
28+
]
29+
)
30+
]
31+
return blocks

manifest.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
"oauth_config": {
2121
"scopes": {
2222
"bot": [
23+
"app_mentions:read",
2324
"assistant:write",
2425
"channels:join",
2526
"im:history",
2627
"channels:history",
28+
"channels:read",
2729
"groups:history",
28-
"chat:write"
30+
"chat:write",
31+
"users:read"
2932
]
3033
}
3134
},
@@ -34,7 +37,9 @@
3437
"bot_events": [
3538
"assistant_thread_context_changed",
3639
"assistant_thread_started",
37-
"message.im"
40+
"message.im",
41+
"app_mention",
42+
"message.channels"
3843
]
3944
},
4045
"interactivity": {

0 commit comments

Comments
 (0)