Skip to content

Commit 1cdc578

Browse files
authored
Merge branch 'main' into add-claude-md
2 parents d7cff0c + 1e77a19 commit 1cdc578

File tree

4 files changed

+332
-27
lines changed

4 files changed

+332
-27
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,6 @@ dmypy.json
125125
.yarn/
126126
*.chat
127127
.jupyter_ystore.db
128+
129+
# For local testing
130+
playground/

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ router = self.serverapp.web_app.settings.get("jupyter-ai", {}).get("router")
1818
def on_new_chat(room_id: str, ychat: YChat):
1919
print(f"New chat connected: {room_id}")
2020

21-
def on_slash_command(room_id: str, message: Message):
22-
print(f"Slash command in {room_id}: {message.body}")
21+
def on_slash_command(room_id: str, command: str, message: Message):
22+
print(f"Slash command '{command}' in {room_id}: {message.body}")
2323

24-
def on_regular_message(room_id: str, message: Message):`
24+
def on_regular_message(room_id: str, message: Message):
2525
print(f"Regular message in {room_id}: {message.body}")
2626

2727
# Register the callbacks
2828
router.observe_chat_init(on_new_chat)
29-
router.observe_slash_cmd_msg("room-id", on_slash_command)
29+
router.observe_slash_cmd_msg("room-id", "help", on_slash_command) # Only /help commands
3030
router.observe_chat_msg("room-id", on_regular_message)
3131
```
3232

@@ -40,9 +40,30 @@ router.observe_chat_msg("room-id", on_regular_message)
4040
### Available Methods
4141

4242
- `observe_chat_init(callback)` - Called when new chat sessions are initialized with `(room_id, ychat)`
43-
- `observe_slash_cmd_msg(room_id, callback)` - Called for messages starting with `/` in a specific room
43+
- `observe_slash_cmd_msg(room_id, command_pattern, callback)` - Called for specific slash commands matching the pattern in a specific room
4444
- `observe_chat_msg(room_id, callback)` - Called for regular (non-slash) messages in a specific room
4545

46+
### Command Pattern Matching
47+
48+
The `observe_slash_cmd_msg` method supports regex pattern matching:
49+
50+
```python
51+
# Exact match: Only matches "/help"
52+
router.observe_slash_cmd_msg("room-id", "help", callback)
53+
54+
# Regex pattern: Matches "/ai-generate", "/ai-review", etc.
55+
router.observe_slash_cmd_msg("room-id", "ai-.*", callback)
56+
57+
# Regex with groups: Matches "/export-json", "/export-csv", "/export-xml"
58+
router.observe_slash_cmd_msg("room-id", r"export-(json|csv|xml)", callback)
59+
```
60+
61+
**Callback signature**: `callback(room_id: str, command: str, message: Message)`
62+
63+
- `room_id`: The chat room identifier
64+
- `command`: The matched command without the leading slash (e.g., "help", "ai-generate")
65+
- `message`: Message object with the command removed from the body (only arguments remain)
66+
4667
## Install
4768

4869
To install the extension, execute:

jupyter_ai_router/router.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from typing import Any, Callable, Dict, List, TYPE_CHECKING
1111
from functools import partial
12+
import re
13+
from dataclasses import replace
1214
from jupyterlab_chat.models import Message
1315
from pycrdt import ArrayEvent
1416
from traitlets.config import LoggingConfigurable
@@ -19,6 +21,23 @@
1921
from .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+
2241
class 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

Comments
 (0)