Skip to content

Commit 257c41f

Browse files
3coinsdlqqq
andauthored
Update user message routing rules (#1399)
* Handles single user, persona, routes to last mentioned persona with @-mention * Updated docs * Added a method to get active users from awareness, updated process message logic. * Fixed multi-user * Update packages/jupyter-ai/jupyter_ai/personas/persona_manager.py Co-authored-by: David L. Qiu <[email protected]> * Update packages/jupyter-ai/jupyter_ai/personas/persona_manager.py Co-authored-by: David L. Qiu <[email protected]> * Simplified message processing logic * Cleaned up debug persona --------- Co-authored-by: David L. Qiu <[email protected]>
1 parent f75f887 commit 257c41f

File tree

2 files changed

+64
-13
lines changed

2 files changed

+64
-13
lines changed

packages/jupyter-ai-test/jupyter_ai_test/debug_persona.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from jupyter_ai.personas.base_persona import BasePersona, PersonaDefaults
2-
from jupyterlab_chat.models import Message, NewMessage
2+
from jupyterlab_chat.models import Message
33

44

55
class DebugPersona(BasePersona):
@@ -20,5 +20,5 @@ def defaults(self):
2020
)
2121

2222
async def process_message(self, message: Message):
23-
self.ychat.add_message(NewMessage(body="Hello!", sender=self.id))
24-
return
23+
# Note: Echoing messages with @persona mentions causes infinite loops
24+
self.send_message("Hello!, how are you?")

packages/jupyter-ai/jupyter_ai/personas/persona_manager.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class PersonaManager(LoggingConfigurable):
4545
root_dir: str
4646
event_loop: AbstractEventLoop
4747
_mcp_config_loader: MCPConfigLoader
48+
last_mentioned_persona: BasePersona | None
4849

4950
log: Logger # type: ignore
5051
"""
@@ -89,6 +90,9 @@ def __init__(
8990

9091
# Initialize MCP config loader
9192
self._mcp_config_loader = MCPConfigLoader()
93+
# Initialize last_mentioned_persona to None
94+
self.last_mentioned_persona = None
95+
9296
self._init_persona_classes()
9397
self.log.info("Persona classes loaded!")
9498
self._personas = self._init_personas()
@@ -281,22 +285,55 @@ def route_message(self, new_message: Message):
281285
Method that routes an incoming message to the correct persona by calling
282286
its `process_message()` method.
283287
284-
- (TODO) If the chat contains only one persona & one user, then this
285-
method routes all new messages to that persona.
288+
- If the chat has multiple users, then each persona only replies
289+
when `@`-mentioned.
290+
291+
- If there is only one user, the last mentioned persona replies
292+
unless another persona is `@`-mentioned. If only one persona exists
293+
as well, then the persona always replies, regardless of whether
294+
it is `@`-mentioned.
286295
287-
- Otherwise, this method only routes new messages to personas that are
288-
`@`-mentioned in the message.
289296
"""
297+
298+
# Gather routing context
299+
human_users = self.get_active_human_users()
300+
sender_is_persona = is_persona(new_message.sender)
290301
mentioned_personas = self.get_mentioned_personas(new_message)
291-
if not len(mentioned_personas):
302+
303+
human_user_count = len(human_users)
304+
persona_count = len(self.personas)
305+
mentioned_count = len(mentioned_personas)
306+
307+
# Don't route persona replies without mentions
308+
if sender_is_persona and mentioned_count == 0:
292309
return
293310

294-
mentioned_persona_names = [persona.name for persona in mentioned_personas]
295-
self.log.info(
296-
f"Received new user message mentioning the following personas: {mentioned_persona_names}."
297-
)
298-
for persona in mentioned_personas:
311+
# Multi-user chat: require explicit @-mentions only
312+
if human_user_count > 1:
313+
for persona in mentioned_personas:
314+
self.event_loop.create_task(persona.process_message(new_message))
315+
return
316+
317+
# Single user + multiple personas case: route to mentions if they are present,
318+
# otherwise route to last mentioned
319+
if persona_count > 1:
320+
if mentioned_personas:
321+
# Update last mentioned persona if sender is human
322+
if not sender_is_persona:
323+
self.last_mentioned_persona = mentioned_personas[0]
324+
for persona in mentioned_personas:
325+
asyncio.create_task(persona.process_message(new_message))
326+
elif self.last_mentioned_persona:
327+
asyncio.create_task(
328+
self.last_mentioned_persona.process_message(new_message)
329+
)
330+
return
331+
332+
# Default case (single user, 0/1 personas): persona always replies if present
333+
for persona in self.personas.values():
299334
self.event_loop.create_task(persona.process_message(new_message))
335+
break
336+
return
300337

301338
def get_chat_path(self, relative: bool = False) -> str:
302339
"""
@@ -345,6 +382,20 @@ def get_mcp_config(self) -> dict[str, Any]:
345382
else:
346383
return self._mcp_config_loader.get_config(jdir)
347384

385+
def get_active_human_users(self):
386+
"""Returns active human users in a chat session"""
387+
users = []
388+
for value in [k for k in self.ychat.awareness.states.values()]:
389+
if "user" in value.keys() and not is_persona(value["user"]["username"]):
390+
users.append(value["user"])
391+
392+
return users
393+
394+
395+
def is_persona(username: str):
396+
"""Returns true if username belongs to a persona"""
397+
return username.startswith("jupyter-ai-personas")
398+
348399

349400
def load_from_dir(dir: str, log: Logger) -> list[dict]:
350401
"""

0 commit comments

Comments
 (0)