Skip to content

Commit 5cc81ce

Browse files
authored
Merge pull request #11 from John-Lin/wip/thread-mention-only
Wip/thread mention only
2 parents d03c6c1 + ee0f6d1 commit 5cc81ce

2 files changed

Lines changed: 102 additions & 28 deletions

File tree

bot/slack.py

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ def __init__(
3434
self.agent = openai_agent
3535
self._user_name_cache: dict[str, str] = {}
3636

37-
# Maps thread_ts to the user_id who first @mentioned the bot, so only
38-
# that user's follow-up messages in the thread are handled without a mention.
39-
self.active_threads: dict[str, str] = {}
40-
4137
# Set up event handlers
4238
self.app.event("app_mention")(self.handle_mention)
4339
self.app.event("message")(self.handle_message)
@@ -58,33 +54,21 @@ async def initialize_bot_info(self) -> None:
5854
self.bot_id = None
5955

6056
async def handle_mention(self, event, say, ack):
61-
"""Handle mentions of the bot in channels."""
57+
"""Handle mentions of the bot in channels and threads."""
6258
await ack()
63-
thread_ts = event.get("thread_ts", event.get("ts"))
64-
user_id = event.get("user")
65-
if thread_ts not in self.active_threads and user_id:
66-
self.active_threads[thread_ts] = user_id
6759
await self._process_message(event, say)
6860

6961
async def handle_message(self, message, say, ack):
70-
"""Handle direct messages and follow-up messages in active threads."""
62+
"""Handle direct messages. Channel and thread messages must @mention
63+
the bot and are handled by ``handle_mention`` via the ``app_mention``
64+
event — every turn in a thread requires an explicit mention, regardless
65+
of who started the thread.
66+
"""
7167
await ack()
7268
if message.get("subtype"):
7369
return
7470

75-
channel_type = message.get("channel_type")
76-
thread_ts = message.get("thread_ts")
77-
78-
is_dm = channel_type == "im"
79-
# Respond only to the user who first @mentioned the bot in this thread,
80-
# and skip messages that @mention the bot (handle_mention handles those).
81-
is_active_thread_followup = (
82-
thread_ts is not None
83-
and self.active_threads.get(thread_ts) == message.get("user")
84-
and not self._is_bot_mentioned(message)
85-
)
86-
87-
if is_dm or is_active_thread_followup:
71+
if message.get("channel_type") == "im":
8872
await self._process_message(message, say)
8973

9074
async def _get_display_name(self, user_id: str) -> str:
@@ -99,11 +83,6 @@ async def _get_display_name(self, user_id: str) -> str:
9983
self._user_name_cache[user_id] = name
10084
return name
10185

102-
def _is_bot_mentioned(self, message) -> bool:
103-
if not getattr(self, "bot_id", None):
104-
return False
105-
return f"<@{self.bot_id}>" in message.get("text", "")
106-
10786
async def _process_message(self, event, say):
10887
"""Process incoming messages and generate responses."""
10988
channel = event["channel"]

tests/test_slack.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,101 @@ async def test_bot_own_messages_are_ignored(self, bot):
174174

175175
bot.agent.run.assert_not_called()
176176

177+
@pytest.mark.anyio
178+
async def test_thread_followup_without_mention_is_ignored(self, bot):
179+
"""A plain (non-@mention) message in a thread is ignored, even if the
180+
same user previously @mentioned the bot in that thread. Every turn
181+
requires an explicit mention.
182+
"""
183+
# First turn: user @mentions in thread.
184+
mention_event = {
185+
"channel": "C001",
186+
"user": "U123",
187+
"text": "<@U_BOT> first question",
188+
"thread_ts": "1111111111.111111",
189+
"ts": "1111111111.111111",
190+
}
191+
await bot.handle_mention(mention_event, AsyncMock(), AsyncMock())
192+
assert bot.agent.run.call_count == 1
193+
194+
# Second turn: same user, same thread, no @mention — should be ignored.
195+
followup = {
196+
"channel": "C001",
197+
"channel_type": "channel",
198+
"user": "U123",
199+
"text": "follow up without mention",
200+
"thread_ts": "1111111111.111111",
201+
"ts": "2222222222.222222",
202+
}
203+
await bot.handle_message(followup, AsyncMock(), AsyncMock())
204+
205+
# Still only the original mention call.
206+
assert bot.agent.run.call_count == 1
207+
208+
209+
class TestMultiUserThread:
210+
@pytest.mark.anyio
211+
async def test_second_user_mention_in_same_thread_shares_history(self, bot):
212+
"""When a second user joins a thread by @mentioning the bot, they engage
213+
the same conversation key (thread_ts) so the bot has shared history.
214+
"""
215+
thread_ts = "1111111111.111111"
216+
217+
alice_mention = {
218+
"channel": "C001",
219+
"user": "U_ALICE",
220+
"text": "<@U_BOT> what is the capital of France?",
221+
"thread_ts": thread_ts,
222+
"ts": thread_ts,
223+
}
224+
bob_mention = {
225+
"channel": "C001",
226+
"user": "U_BOB",
227+
"text": "<@U_BOT> and Germany?",
228+
"thread_ts": thread_ts,
229+
"ts": "2222222222.222222",
230+
}
231+
232+
await bot.handle_mention(alice_mention, AsyncMock(), AsyncMock())
233+
await bot.handle_mention(bob_mention, AsyncMock(), AsyncMock())
234+
235+
# Both calls used the same thread_ts as the conversation key.
236+
assert bot.agent.run.call_count == 2
237+
assert bot.agent.run.call_args_list[0].args[0] == thread_ts
238+
assert bot.agent.run.call_args_list[1].args[0] == thread_ts
239+
240+
@pytest.mark.anyio
241+
async def test_second_user_plain_message_in_thread_is_ignored(self, bot):
242+
"""A second user typing a plain (non-@mention) message in an active
243+
thread is ignored — they must @mention to participate.
244+
"""
245+
thread_ts = "1111111111.111111"
246+
247+
# Alice opens the thread with a mention.
248+
alice_mention = {
249+
"channel": "C001",
250+
"user": "U_ALICE",
251+
"text": "<@U_BOT> hi",
252+
"thread_ts": thread_ts,
253+
"ts": thread_ts,
254+
}
255+
await bot.handle_mention(alice_mention, AsyncMock(), AsyncMock())
256+
assert bot.agent.run.call_count == 1
257+
258+
# Bob types in the thread without mentioning the bot.
259+
bob_plain = {
260+
"channel": "C001",
261+
"channel_type": "channel",
262+
"user": "U_BOB",
263+
"text": "hey what's going on",
264+
"thread_ts": thread_ts,
265+
"ts": "2222222222.222222",
266+
}
267+
await bot.handle_message(bob_plain, AsyncMock(), AsyncMock())
268+
269+
# Bob's plain message did not trigger the bot.
270+
assert bot.agent.run.call_count == 1
271+
177272

178273
class TestErrorHandling:
179274
@pytest.mark.anyio

0 commit comments

Comments
 (0)