99
1010from typing import Any , Callable , Dict , List , TYPE_CHECKING
1111from functools import partial
12+ import re
13+ from dataclasses import replace
1214from jupyterlab_chat .models import Message
1315from pycrdt import ArrayEvent
1416from traitlets .config import LoggingConfigurable
1921from .utils import get_first_word
2022
2123
24+ def matches_pattern (word : str , pattern : str ) -> bool :
25+ """
26+ Check if a word matches a regex pattern.
27+
28+ Args:
29+ word: The word to match (e.g., "help", "ai-generate")
30+ pattern: The regex pattern to match against (e.g., "help", "ai-.*", "export-(json|csv)")
31+
32+ Returns:
33+ True if the word matches the pattern
34+ """
35+ try :
36+ return bool (re .match (f"^{ pattern } $" , word ))
37+ except re .error :
38+ return False
39+
40+
2241class MessageRouter (LoggingConfigurable ):
2342 """
2443 Router that manages ychat message routing.
@@ -34,7 +53,7 @@ def __init__(self, *args, **kwargs):
3453
3554 # Callback lists
3655 self .chat_init_observers : List [Callable [[str , "YChat" ], Any ]] = []
37- self .slash_cmd_observers : Dict [str , List [Callable [[str , Message ], Any ]]] = {}
56+ self .slash_cmd_observers : Dict [str , Dict [ str , List [Callable [[str , str , Message ], Any ] ]]] = {}
3857 self .chat_msg_observers : Dict [str , List [Callable [[str , Message ], Any ]]] = {}
3958
4059 # Active chat rooms
@@ -54,19 +73,28 @@ def observe_chat_init(self, callback: Callable[[str, "YChat"], Any]) -> None:
5473 self .log .info ("Registered new chat initialization callback" )
5574
5675 def observe_slash_cmd_msg (
57- self , room_id : str , callback : Callable [[str , Message ], Any ]
76+ self , room_id : str , command_pattern : str , callback : Callable [[str , str , Message ], Any ]
5877 ) -> None :
5978 """
60- Register a callback for when slash commands are received.
79+ Register a callback for when specific slash commands are received.
6180
6281 Args:
63- callback: Function called with (room_id: str, message: Message) for slash commands
82+ room_id: The chat room ID
83+ command_pattern: Regex pattern to match commands (without leading slash).
84+ Examples:
85+ - Exact match: "help" matches "/help"
86+ - Pattern match: "ai-.*" matches "/ai-generate", "/ai-review", etc.
87+ - Multiple options: "export-(json|csv)" matches "/export-json", "/export-csv"
88+ callback: Function called with (room_id: str, command: str, message: Message) for matching commands
6489 """
6590 if room_id not in self .slash_cmd_observers :
66- self .slash_cmd_observers [room_id ] = []
91+ self .slash_cmd_observers [room_id ] = {}
92+
93+ if command_pattern not in self .slash_cmd_observers [room_id ]:
94+ self .slash_cmd_observers [room_id ][command_pattern ] = []
6795
68- self .slash_cmd_observers [room_id ].append (callback )
69- self .log .info ("Registered slash command callback" )
96+ self .slash_cmd_observers [room_id ][ command_pattern ] .append (callback )
97+ self .log .info (f "Registered slash command callback for pattern: { command_pattern } " )
7098
7199 def observe_chat_msg (
72100 self , room_id : str , callback : Callable [[str , Message ], Any ]
@@ -157,10 +185,35 @@ def _route_message(self, room_id: str, message: Message) -> None:
157185
158186 # Check if it's a slash command
159187 if first_word and first_word .startswith ("/" ):
160- self ._notify_slash_cmd_observers (room_id , message )
188+ # Extract command and create trimmed message
189+ parts = message .body .split (None , 1 ) # Split into max 2 parts
190+ command = parts [0 ] if parts else ""
191+ trimmed_body = parts [1 ] if len (parts ) > 1 else ""
192+
193+ # Create a copy of the message with trimmed body (command removed)
194+ trimmed_message = replace (message , body = trimmed_body )
195+
196+ # Remove forward slash from command for cleaner API
197+ clean_command = command [1 :] if command .startswith ("/" ) else command
198+
199+ # Route to slash command observers
200+ self ._notify_slash_cmd_observers (room_id , trimmed_message , clean_command )
161201 else :
162202 self ._notify_msg_observers (room_id , message )
163203
204+
205+ def _notify_slash_cmd_observers (self , room_id : str , message : Message , clean_command : str ) -> None :
206+ """Notify observers registered for slash commands."""
207+ room_observers = self .slash_cmd_observers .get (room_id , {})
208+
209+ for registered_pattern , callbacks in room_observers .items ():
210+ if matches_pattern (clean_command , registered_pattern ):
211+ for callback in callbacks :
212+ try :
213+ callback (room_id , clean_command , message )
214+ except Exception as e :
215+ self .log .error (f"Slash command observer error for pattern '{ registered_pattern } ': { e } " )
216+
164217 def _notify_chat_init_observers (self , room_id : str , ychat : "YChat" ) -> None :
165218 """Notify all new chat observers."""
166219 for callback in self .chat_init_observers :
@@ -169,15 +222,6 @@ def _notify_chat_init_observers(self, room_id: str, ychat: "YChat") -> None:
169222 except Exception as e :
170223 self .log .error (f"New chat observer error for { room_id } : { e } " )
171224
172- def _notify_slash_cmd_observers (self , room_id : str , message : Message ) -> None :
173- """Notify all slash command observers."""
174- callbacks = self .slash_cmd_observers .get (room_id , [])
175- for callback in callbacks :
176- try :
177- callback (room_id , message )
178- except Exception as e :
179- self .log .error (f"Slash command observer error for { room_id } : { e } " )
180-
181225 def _notify_msg_observers (self , room_id : str , message : Message ) -> None :
182226 """Notify all message observers."""
183227 callbacks = self .chat_msg_observers .get (room_id , [])
0 commit comments