Skip to content

Commit ec9fb79

Browse files
srtaalejmwbrooks
andauthored
feat: add app_mention event handler (#14)
* feat: add app_mention event handler * refactor: remove commented event handlers * refactor: lint * fix(flake8): remove unused listeners_constants.py * feat: update app_mentioned_callback to not require conversations history * docs: update the app_mentioned_callback comment block --------- Co-authored-by: Michael Brooks <mbrooks@slack-corp.com>
1 parent 3827d72 commit ec9fb79

11 files changed

Lines changed: 140 additions & 329 deletions

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ black .
7676

7777
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.
7878

79-
> [!NOTE]
80-
> 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`.
81-
8279
**`/listeners/assistant`**
8380

8481
Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes:

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: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,16 @@
44
from slack_bolt import Assistant, BoltContext, Say, SetStatus, SetSuggestedPrompts
55
from slack_bolt.context.get_thread_context import GetThreadContext
66
from slack_sdk import WebClient
7-
from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonObject, FeedbackButtonsElement
7+
from slack_sdk.errors import SlackApiError
88

9+
from ..views.feedback_block import create_feedback_block
910
from ..llm_caller import call_llm
1011

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

1416

15-
def create_feedback_block() -> List[Block]:
16-
"""
17-
Create feedback block with thumbs up/down buttons
18-
19-
Returns:
20-
Block Kit context_actions block
21-
"""
22-
blocks: List[Block] = [
23-
ContextActionsBlock(
24-
elements=[
25-
FeedbackButtonsElement(
26-
action_id="feedback",
27-
positive_button=FeedbackButtonObject(
28-
text="Good Response",
29-
accessibility_label="Submit positive feedback on this response",
30-
value="good-feedback",
31-
),
32-
negative_button=FeedbackButtonObject(
33-
text="Bad Response",
34-
accessibility_label="Submit negative feedback on this response",
35-
value="bad-feedback",
36-
),
37-
)
38-
]
39-
)
40-
]
41-
return blocks
42-
43-
4417
# This listener is invoked when a human user opened an assistant thread
4518
@assistant.thread_started
4619
def start_assistant_thread(
@@ -86,17 +59,21 @@ def start_assistant_thread(
8659
def respond_in_assistant_thread(
8760
client: WebClient,
8861
context: BoltContext,
62+
get_thread_context: GetThreadContext,
8963
logger: logging.Logger,
9064
payload: dict,
9165
say: Say,
9266
set_status: SetStatus,
9367
):
9468
try:
9569
channel_id = payload["channel"]
70+
team_id = payload["team"]
9671
thread_ts = payload["thread_ts"]
72+
user_id = payload["user"]
73+
user_message = payload["text"]
9774

9875
set_status(
99-
status="Drafting...",
76+
status="thinking...",
10077
loading_messages=[
10178
"Teaching the hamsters to type faster…",
10279
"Untangling the internet cables…",
@@ -106,21 +83,45 @@ def respond_in_assistant_thread(
10683
],
10784
)
10885

109-
replies = client.conversations_replies(
110-
channel=context.channel_id,
111-
ts=context.thread_ts,
112-
oldest=context.thread_ts,
113-
limit=10,
114-
)
115-
messages_in_thread: List[Dict[str, str]] = []
116-
for message in replies["messages"]:
117-
role = "user" if message.get("bot_id") is None else "assistant"
118-
messages_in_thread.append({"role": role, "content": message["text"]})
86+
if user_message == "Can you generate a brief summary of the referred channel?":
87+
# the logic here requires the additional bot scopes:
88+
# channels:join, channels:history, groups:history
89+
thread_context = get_thread_context()
90+
referred_channel_id = thread_context.get("channel_id")
91+
try:
92+
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
93+
except SlackApiError as e:
94+
if e.response["error"] == "not_in_channel":
95+
# If this app's bot user is not in the public channel,
96+
# we'll try joining the channel and then calling the same API again
97+
client.conversations_join(channel=referred_channel_id)
98+
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
99+
else:
100+
raise e
101+
prompt = f"Can you generate a brief summary of these messages in a Slack channel <#{referred_channel_id}>?\n\n"
102+
for message in reversed(channel_history.get("messages")):
103+
if message.get("user") is not None:
104+
prompt += f"\n<@{message['user']}> says: {message['text']}\n"
105+
messages_in_thread = [{"role": "user", "content": prompt}]
106+
107+
else:
108+
replies = client.conversations_replies(
109+
channel=context.channel_id,
110+
ts=context.thread_ts,
111+
oldest=context.thread_ts,
112+
limit=10,
113+
)
114+
messages_in_thread: List[Dict[str, str]] = []
115+
for message in replies["messages"]:
116+
role = "user" if message.get("bot_id") is None else "assistant"
117+
messages_in_thread.append({"role": role, "content": message["text"]})
119118

120119
returned_message = call_llm(messages_in_thread)
121120

122121
stream_response = client.chat_startStream(
123122
channel=channel_id,
123+
recipient_team_id=team_id,
124+
recipient_user_id=user_id,
124125
thread_ts=thread_ts,
125126
)
126127
stream_ts = stream_response["ts"]

listeners/events/__init__.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,6 @@
1-
# This sample app repository contains event listener code to help developers understand what's happening under the hood.
2-
# We recommend using assistant middleware instead of these event listeners.
3-
# For more details, refer to https://tools.slack.dev/bolt-python/concepts/assistant/.
4-
5-
from typing import Dict, Any
6-
71
from slack_bolt import App
8-
from slack_bolt.request.payload_utils import is_event
9-
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
2+
from .app_mentioned import app_mentioned_callback
133

144

155
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)
20-
21-
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
26-
27-
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")
30-
31-
32-
def just_ack():
33-
pass
6+
app.event("app_mention")(app_mentioned_callback)

listeners/events/app_mentioned.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from logging import Logger
2+
from slack_sdk import WebClient
3+
from slack_bolt import Say
4+
5+
from ..llm_caller import call_llm
6+
from ..views.feedback_block import create_feedback_block
7+
8+
"""
9+
Handles the event when the app is mentioned in a Slack conversation
10+
and generates an AI response.
11+
"""
12+
13+
14+
def app_mentioned_callback(client: WebClient, event: dict, logger: Logger, say: Say):
15+
try:
16+
channel_id = event.get("channel")
17+
team_id = event.get("team")
18+
text = event.get("text")
19+
thread_ts = event.get("thread_ts") or event.get("ts")
20+
user_id = event.get("user")
21+
22+
client.assistant_threads_setStatus(
23+
channel_id=channel_id,
24+
thread_ts=thread_ts,
25+
status="thinking...",
26+
loading_messages=[
27+
"Teaching the hamsters to type faster…",
28+
"Untangling the internet cables…",
29+
"Consulting the office goldfish…",
30+
"Polishing up the response just for you…",
31+
"Convincing the AI to stop overthinking…",
32+
],
33+
)
34+
35+
returned_message = call_llm([{"role": "user", "content": text}])
36+
37+
stream_response = client.chat_startStream(
38+
channel=channel_id, recipient_team_id=team_id, recipient_user_id=user_id, thread_ts=thread_ts
39+
)
40+
41+
stream_ts = stream_response["ts"]
42+
43+
# Loop over OpenAI response stream
44+
# https://platform.openai.com/docs/api-reference/responses/create
45+
for event in returned_message:
46+
if event.type == "response.output_text.delta":
47+
client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}")
48+
else:
49+
continue
50+
51+
feedback_block = create_feedback_block()
52+
client.chat_stopStream(channel=channel_id, ts=stream_ts, blocks=feedback_block)
53+
54+
except Exception as e:
55+
logger.exception(f"Failed to handle a user message event: {e}")
56+
say(f":warning: Something went wrong! ({e})")

listeners/events/assistant_thread_started.py

Lines changed: 0 additions & 68 deletions
This file was deleted.

listeners/events/asssistant_thread_context_changed.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)