Skip to content

Commit 4e5a422

Browse files
committed
Simplified pattern matching
1 parent 9a43922 commit 4e5a422

File tree

3 files changed

+56
-83
lines changed

3 files changed

+56
-83
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ router.observe_chat_msg("room-id", on_regular_message)
4545

4646
### Command Pattern Matching
4747

48-
The `observe_slash_cmd_msg` method supports flexible command pattern matching:
48+
The `observe_slash_cmd_msg` method supports regex pattern matching:
4949

5050
```python
5151
# Exact match: Only matches "/help"
5252
router.observe_slash_cmd_msg("room-id", "help", callback)
5353

54-
# Wildcard match: Matches "/ai-generate", "/ai-review", etc.
55-
router.observe_slash_cmd_msg("room-id", "ai-*", callback)
54+
# Regex pattern: Matches "/ai-generate", "/ai-review", etc.
55+
router.observe_slash_cmd_msg("room-id", "ai-.*", callback)
5656

57-
# Regex pattern: Matches "/export-json", "/export-csv", "/export-xml"
57+
# Regex with groups: Matches "/export-json", "/export-csv", "/export-xml"
5858
router.observe_slash_cmd_msg("room-id", r"export-(json|csv|xml)", callback)
5959
```
6060

jupyter_ai_router/router.py

Lines changed: 28 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@
2121
from .utils import get_first_word
2222

2323

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+
2441
class MessageRouter(LoggingConfigurable):
2542
"""
2643
Router that manages ychat message routing.
@@ -63,10 +80,11 @@ def observe_slash_cmd_msg(
6380
6481
Args:
6582
room_id: The chat room ID
66-
command_pattern: Command pattern to match (without leading slash). Supports:
83+
command_pattern: Regex pattern to match commands (without leading slash).
84+
Examples:
6785
- Exact match: "help" matches "/help"
68-
- Wildcard: "ai-*" matches "/ai-generate", "/ai-review", etc.
69-
- Regex: Any valid Python regex pattern like "export-(json|csv)"
86+
- Pattern match: "ai-.*" matches "/ai-generate", "/ai-review", etc.
87+
- Multiple options: "export-(json|csv)" matches "/export-json", "/export-csv"
7088
callback: Function called with (room_id: str, command: str, message: Message) for matching commands
7189
"""
7290
if room_id not in self.slash_cmd_observers:
@@ -171,63 +189,25 @@ def _route_message(self, room_id: str, message: Message) -> None:
171189
parts = message.body.split(None, 1) # Split into max 2 parts
172190
command = parts[0] if parts else ""
173191
trimmed_body = parts[1] if len(parts) > 1 else ""
174-
192+
175193
# Create a copy of the message with trimmed body (command removed)
176194
trimmed_message = replace(message, body=trimmed_body)
177-
195+
178196
# Remove forward slash from command for cleaner API
179197
clean_command = command[1:] if command.startswith("/") else command
180-
198+
181199
# Route to slash command observers
182-
self._notify_slash_cmd_observers(room_id, trimmed_message, command, clean_command)
200+
self._notify_slash_cmd_observers(room_id, trimmed_message, clean_command)
183201
else:
184202
self._notify_msg_observers(room_id, message)
185203

186-
def _command_matches(self, command: str, pattern: str) -> bool:
187-
"""
188-
Check if a command matches a pattern.
189-
190-
Args:
191-
command: The actual command with slash (e.g., "/help")
192-
pattern: The pattern to match against without slash (e.g., "help", "ai-*", regex)
193-
194-
Returns:
195-
True if the command matches the pattern
196-
"""
197-
# Convert pattern to include slash for matching
198-
# Pattern "help" should match command "/help"
199-
if not pattern.startswith("/"):
200-
full_pattern = "/" + pattern
201-
else:
202-
# Handle case where pattern accidentally includes slash
203-
full_pattern = pattern
204-
205-
# Exact match
206-
if command == full_pattern:
207-
return True
208-
209-
# Wildcard pattern (convert to regex)
210-
if "*" in full_pattern:
211-
# Escape special regex chars except *, then convert * to .*
212-
escaped_pattern = re.escape(full_pattern).replace(r"\*", ".*")
213-
regex_pattern = f"^{escaped_pattern}$"
214-
try:
215-
return bool(re.match(regex_pattern, command))
216-
except re.error:
217-
return False
218-
219-
# Try as regex pattern (add slash if not present)
220-
try:
221-
return bool(re.match(full_pattern, command))
222-
except re.error:
223-
return False
224204

225-
def _notify_slash_cmd_observers(self, room_id: str, message: Message, original_command: str, clean_command: str) -> None:
205+
def _notify_slash_cmd_observers(self, room_id: str, message: Message, clean_command: str) -> None:
226206
"""Notify observers registered for slash commands."""
227207
room_observers = self.slash_cmd_observers.get(room_id, {})
228-
208+
229209
for registered_pattern, callbacks in room_observers.items():
230-
if self._command_matches(original_command, registered_pattern):
210+
if matches_pattern(clean_command, registered_pattern):
231211
for callback in callbacks:
232212
try:
233213
callback(room_id, clean_command, message)

jupyter_ai_router/tests/test_message_router.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from unittest.mock import Mock, MagicMock
77
from jupyterlab_chat.models import Message
88
from jupyterlab_chat.ychat import YChat
9-
from jupyter_ai_router.router import MessageRouter
9+
from jupyter_ai_router.router import MessageRouter, matches_pattern
1010
from jupyter_ai_router.utils import get_first_word, is_persona
1111

1212

@@ -131,34 +131,27 @@ def test_cleanup(self):
131131
assert len(self.router.slash_cmd_observers) == 0
132132
assert len(self.router.chat_msg_observers) == 0
133133

134-
def test_observe_slash_cmd_patterns(self):
135-
"""Test registering specific slash command callback with patterns."""
136-
room_id = "test-room"
137-
command_pattern = "help"
138-
self.router.observe_slash_cmd_msg(room_id, command_pattern, self.mock_specific_cmd_callback)
139-
140-
assert command_pattern in self.router.slash_cmd_observers[room_id]
141-
assert self.mock_specific_cmd_callback in self.router.slash_cmd_observers[room_id][command_pattern]
142134

143-
def test_command_matches_exact(self):
135+
def test_matches_pattern_exact(self):
144136
"""Test exact command matching."""
145-
assert self.router._command_matches("/help", "help") is True
146-
assert self.router._command_matches("/help", "status") is False
147-
148-
def test_command_matches_wildcard(self):
149-
"""Test wildcard command matching."""
150-
assert self.router._command_matches("/ai-generate", "ai-*") is True
151-
assert self.router._command_matches("/ai-review", "ai-*") is True
152-
assert self.router._command_matches("/help", "ai-*") is False
153-
assert self.router._command_matches("/export-csv", "export-*") is True
154-
155-
def test_command_matches_regex(self):
156-
"""Test regex command matching."""
137+
assert matches_pattern("help", "help") is True
138+
assert matches_pattern("help", "status") is False
139+
140+
def test_matches_pattern_regex(self):
141+
"""Test regex pattern matching."""
142+
# Pattern with .* (formerly wildcard)
143+
assert matches_pattern("ai-generate", "ai-.*") is True
144+
assert matches_pattern("ai-review", "ai-.*") is True
145+
assert matches_pattern("help", "ai-.*") is False
146+
assert matches_pattern("export-csv", "export-.*") is True
147+
148+
def test_matches_pattern_regex_groups(self):
149+
"""Test regex command matching with groups."""
157150
pattern = r"export-(json|csv|xml)"
158-
assert self.router._command_matches("/export-json", pattern) is True
159-
assert self.router._command_matches("/export-csv", pattern) is True
160-
assert self.router._command_matches("/export-xml", pattern) is True
161-
assert self.router._command_matches("/export-pdf", pattern) is False
151+
assert matches_pattern("export-json", pattern) is True
152+
assert matches_pattern("export-csv", pattern) is True
153+
assert matches_pattern("export-xml", pattern) is True
154+
assert matches_pattern("export-pdf", pattern) is False
162155

163156
def test_specific_command_routing_exact(self):
164157
"""Test routing of specific slash commands with exact match."""
@@ -183,10 +176,10 @@ def test_specific_command_routing_exact(self):
183176
self.router._route_message(room_id, status_msg)
184177
self.mock_specific_cmd_callback.assert_not_called()
185178

186-
def test_specific_command_routing_wildcard(self):
187-
"""Test routing of specific slash commands with wildcard pattern."""
179+
def test_specific_command_routing_regex(self):
180+
"""Test routing of specific slash commands with regex pattern."""
188181
room_id = "test-room"
189-
self.router.observe_slash_cmd_msg(room_id, "ai-*", self.mock_specific_cmd_callback)
182+
self.router.observe_slash_cmd_msg(room_id, "ai-.*", self.mock_specific_cmd_callback)
190183

191184
# Test matching commands
192185
generate_msg = Message(id="1", body="/ai-generate code", sender="user", time=123)
@@ -267,7 +260,7 @@ def test_multiple_patterns_different_commands(self):
267260
export_callback = Mock()
268261

269262
self.router.observe_slash_cmd_msg(room_id, "help", help_callback)
270-
self.router.observe_slash_cmd_msg(room_id, "export-*", export_callback)
263+
self.router.observe_slash_cmd_msg(room_id, "export-.*", export_callback)
271264

272265
help_msg = Message(id="1", body="/help topic", sender="user", time=123)
273266
self.router._route_message(room_id, help_msg)
@@ -304,7 +297,7 @@ def test_specific_command_error_handling(self):
304297
def test_invalid_regex_pattern(self):
305298
"""Test handling of invalid regex patterns."""
306299
# Invalid regex should not match anything
307-
assert self.router._command_matches("/help", "[invalid") is False
300+
assert matches_pattern("help", "[invalid") is False
308301

309302
def test_message_trimming_and_command_cleaning(self):
310303
"""Test that messages are properly trimmed and commands cleaned."""

0 commit comments

Comments
 (0)