Skip to content

Commit 398d977

Browse files
Abigayle-MercerabmercerZsailer
authored
Add mention detection to update_message (#302)
* Add mention detection to update_message This patch extends the mention detection functionality from add_message (added in PR #235) to update_message. This enables automatic @mention detection when messages are updated, which is particularly useful for streaming scenarios where message content is built incrementally. Changes: - Refactored mention detection logic into reusable _extract_mentions() helper method - Updated add_message to use the new helper method - Added mention detection to update_message that scans the complete message body - Added three test cases covering mention updates, append with mentions, and deduplication - Fixed regex pattern to use raw string to avoid SyntaxWarning The implementation ensures no duplicate mentions are added when the same message is updated multiple times, making it safe for streaming use cases. * added a is_done flag to make sure the full message is sent before mentioning personas * changed variable name * changed update_message with mentions tests to use sorted() instead of set() for better comparison * revert test_add_message_includes_mentions to use set() * changed to a set of actions * updated doc string * changed trigger actions API to be a list of callbacks, not strings, added a utils file for future actions * updated name of callback to just be find_mentions * fixed the test naming of callback * fixed one last import name from find_mentions_callback to just find_mentions --------- Co-authored-by: Abigayle-Mercer <[email protected]> Co-authored-by: Zachary Sailer <[email protected]>
1 parent 6dc0bbe commit 398d977

File tree

3 files changed

+116
-12
lines changed

3 files changed

+116
-12
lines changed

python/jupyterlab-chat/jupyterlab_chat/tests/test_ychat.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from uuid import uuid4
1010
from ..models import message_asdict_factory, Message, NewMessage, User
1111
from ..ychat import YChat
12+
from ..utils import find_mentions
1213

1314
USER = User(
1415
username=str(uuid4()),
@@ -167,6 +168,63 @@ def test_update_message_should_append_content():
167168
assert message_dict["sender"] == msg.sender
168169

169170

171+
def test_update_message_includes_mentions():
172+
chat = YChat()
173+
chat.set_user(USER)
174+
chat.set_user(USER2)
175+
chat.set_user(USER3)
176+
177+
new_msg = create_new_message(f"@{USER2.mention_name} Hello!")
178+
msg_id = chat.add_message(new_msg)
179+
msg = chat.get_message(msg_id)
180+
assert msg
181+
assert msg.mentions == [USER2.username]
182+
183+
msg.body = f"@{USER3.mention_name} Goodbye!"
184+
chat.update_message(msg, trigger_actions=[find_mentions])
185+
updated_msg = chat.get_message(msg_id)
186+
assert updated_msg
187+
assert updated_msg.mentions == [USER3.username]
188+
189+
190+
def test_update_message_append_includes_mentions():
191+
chat = YChat()
192+
chat.set_user(USER)
193+
chat.set_user(USER2)
194+
chat.set_user(USER3)
195+
196+
new_msg = create_new_message(f"@{USER2.mention_name} Hello!")
197+
msg_id = chat.add_message(new_msg)
198+
msg = chat.get_message(msg_id)
199+
assert msg
200+
assert msg.mentions == [USER2.username]
201+
202+
msg.body = f" and @{USER3.mention_name}!"
203+
chat.update_message(msg, append=True, trigger_actions=[find_mentions])
204+
updated_msg = chat.get_message(msg_id)
205+
assert updated_msg
206+
assert sorted(updated_msg.mentions) == sorted([USER2.username, USER3.username])
207+
208+
209+
def test_update_message_append_no_duplicate_mentions():
210+
chat = YChat()
211+
chat.set_user(USER)
212+
chat.set_user(USER2)
213+
214+
new_msg = create_new_message(f"@{USER2.mention_name} Hello!")
215+
msg_id = chat.add_message(new_msg)
216+
msg = chat.get_message(msg_id)
217+
assert msg
218+
assert msg.mentions == [USER2.username]
219+
220+
msg.body = f" @{USER2.mention_name} again!"
221+
chat.update_message(msg, append=True, trigger_actions=[find_mentions])
222+
updated_msg = chat.get_message(msg_id)
223+
assert updated_msg
224+
assert updated_msg.mentions == [USER2.username]
225+
assert len(updated_msg.mentions) == 1
226+
227+
170228
def test_indexes_by_id():
171229
chat = YChat()
172230
msg = create_new_message()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
"""Utility functions for jupyter-chat."""
5+
6+
import re
7+
from typing import TYPE_CHECKING, Set
8+
9+
if TYPE_CHECKING:
10+
from .models import Message
11+
from .ychat import YChat
12+
13+
14+
def find_mentions(message: "Message", chat: "YChat") -> None:
15+
"""
16+
Callback to extract and update mentions in a message.
17+
18+
Finds all @mentions in the message body and updates the message's mentions list
19+
with the corresponding usernames.
20+
21+
Args:
22+
message: The message object to update
23+
chat: The YChat instance for accessing user data
24+
"""
25+
mention_pattern = re.compile(r"@([\w-]+):?")
26+
mentioned_names: Set[str] = set(re.findall(mention_pattern, message.body))
27+
users = chat.get_users()
28+
mentioned_usernames = []
29+
for username, user in users.items():
30+
if user.mention_name in mentioned_names and user.username not in mentioned_usernames:
31+
mentioned_usernames.append(username)
32+
33+
message.mentions = mentioned_usernames

python/jupyterlab-chat/jupyterlab_chat/ychat.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import re
1616

1717
from .models import message_asdict_factory, FileAttachment, NotebookAttachment, Message, NewMessage, User
18+
from .utils import find_mentions
1819

1920

2021
class YChat(YBaseDoc):
@@ -124,10 +125,18 @@ def _get_messages(self) -> list[dict]:
124125
"""
125126
return self._ymessages.to_py() or []
126127

127-
def add_message(self, new_message: NewMessage) -> str:
128+
def add_message(self, new_message: NewMessage, trigger_actions: list[Callable] | None = None) -> str:
128129
"""
129130
Append a message to the document.
131+
132+
Args:
133+
new_message: The message to add
134+
trigger_actions: List of callbacks to execute on the message. Defaults to [find_mentions].
135+
Each callback receives (message, chat) as arguments.
130136
"""
137+
if trigger_actions is None:
138+
trigger_actions = [find_mentions]
139+
131140
timestamp: float = time.time()
132141
uid = str(uuid4())
133142
message = Message(
@@ -136,15 +145,9 @@ def add_message(self, new_message: NewMessage) -> str:
136145
id=uid,
137146
)
138147

139-
# find all mentioned users and add them as message mentions
140-
mention_pattern = re.compile("@([\w-]+):?")
141-
mentioned_names: Set[str] = set(re.findall(mention_pattern, message.body))
142-
users = self.get_users()
143-
mentioned_usernames = []
144-
for username, user in users.items():
145-
if user.mention_name in mentioned_names and user.username not in mentioned_usernames:
146-
mentioned_usernames.append(username)
147-
message.mentions = mentioned_usernames
148+
# Execute all trigger action callbacks
149+
for callback in trigger_actions:
150+
callback(message, self)
148151

149152
with self._ydoc.transaction():
150153
index = len(self._ymessages) - next((i for i, v in enumerate(self._get_messages()[::-1]) if v["time"] < timestamp), len(self._ymessages))
@@ -155,17 +158,27 @@ def add_message(self, new_message: NewMessage) -> str:
155158

156159
return uid
157160

158-
def update_message(self, message: Message, append: bool = False):
161+
def update_message(self, message: Message, append: bool = False, trigger_actions: list[Callable] | None = None):
159162
"""
160163
Update a message of the document.
161-
If append is True, the content will be append to the previous content.
164+
165+
Args:
166+
message: The message to update
167+
append: If True, the content will be appended to the previous content
168+
trigger_actions: List of callbacks to execute on the message. Each callback receives (message, chat) as arguments.
162169
"""
163170
with self._ydoc.transaction():
164171
index = self._indexes_by_id[message.id]
165172
initial_message = self._ymessages[index]
166173
message.time = initial_message["time"] # type:ignore[index]
167174
if append:
168175
message.body = initial_message["body"] + message.body # type:ignore[index]
176+
177+
# Execute all trigger action callbacks
178+
if trigger_actions:
179+
for callback in trigger_actions:
180+
callback(message, self)
181+
169182
self._ymessages[index] = asdict(message, dict_factory=message_asdict_factory)
170183

171184
def get_attachments(self) -> dict[str, Union[FileAttachment, NotebookAttachment]]:

0 commit comments

Comments
 (0)