Skip to content

Commit fcdd104

Browse files
authored
Merge pull request #1 from 3coins/message-router
Core message routing layer for Jupyter AI
2 parents cae70c5 + 82e06b1 commit fcdd104

File tree

11 files changed

+519
-49
lines changed

11 files changed

+519
-49
lines changed

README.md

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,46 @@
22

33
[![Github Actions Status](https://github.com/jupyter-ai-contrib/jupyter-ai-router/workflows/Build/badge.svg)](https://github.com/jupyter-ai-contrib/jupyter-ai-router/actions/workflows/build.yml)
44

5-
Core routing layer of Jupyter AI
5+
Core message routing layer for Jupyter AI
66

7-
This extension is composed of a Python package named `jupyter_ai_router`
8-
for the server extension and a NPM package named `@jupyter-ai/router`
9-
for the frontend extension.
7+
This extension provides the foundational message routing functionality for Jupyter AI. It automatically detects new chat sessions and routes messages to registered callbacks based on message type (slash commands vs regular messages). Extensions can register callbacks to handle specific chat events without needing to manage chat lifecycle directly.
108

11-
## QUICK START
9+
## Usage
1210

13-
Everything that follows after this section was from the extension template. We
14-
will need to revise the rest of this README.
11+
### Basic MessageRouter Setup
1512

16-
Development install:
13+
```python
14+
# The router is available in other extensions via settings
15+
router = self.serverapp.web_app.settings.get("jupyter-ai", {}).get("router")
1716

17+
# Register callbacks for different event types
18+
def on_new_chat(room_id: str, ychat: YChat):
19+
print(f"New chat connected: {room_id}")
20+
21+
def on_slash_command(room_id: str, message: Message):
22+
print(f"Slash command in {room_id}: {message.body}")
23+
24+
def on_regular_message(room_id: str, message: Message):`
25+
print(f"Regular message in {room_id}: {message.body}")
26+
27+
# Register the callbacks
28+
router.observe_chat_init(on_new_chat)
29+
router.observe_slash_cmd_msg("room-id", on_slash_command)
30+
router.observe_chat_msg("room-id", on_regular_message)
1831
```
19-
micromamba install uv jupyterlab nodejs=22
20-
jlpm
21-
jlpm dev:install
22-
```
2332

24-
## Requirements
33+
### Message Flow
34+
35+
1. **Router detects new chats** - Automatically listens for chat room initialization events
36+
2. **Router connects chats** - Establishes observers on YChat message streams
37+
3. **Router routes messages** - Calls appropriate callbacks based on message type (slash vs regular)
38+
4. **Extensions respond** - Your callbacks receive room_id and message data
39+
40+
### Available Methods
2541

26-
- JupyterLab >= 4.0.0
42+
- `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
44+
- `observe_chat_msg(room_id, callback)` - Called for regular (non-slash) messages in a specific room
2745

2846
## Install
2947

jupyter_ai_router/__init__.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import warnings
88
warnings.warn("Importing 'jupyter_ai_router' outside a proper installation.")
99
__version__ = "dev"
10-
from .handlers import setup_handlers
10+
11+
from .extension import RouterExtension
1112

1213

1314
def _jupyter_labextension_paths():
@@ -18,19 +19,4 @@ def _jupyter_labextension_paths():
1819

1920

2021
def _jupyter_server_extension_points():
21-
return [{
22-
"module": "jupyter_ai_router"
23-
}]
24-
25-
26-
def _load_jupyter_server_extension(server_app):
27-
"""Registers the API handler to receive HTTP requests from the frontend extension.
28-
29-
Parameters
30-
----------
31-
server_app: jupyterlab.labapp.LabApp
32-
JupyterLab application instance
33-
"""
34-
setup_handlers(server_app.web_app)
35-
name = "jupyter_ai_router"
36-
server_app.log.info(f"Registered {name} server extension")
22+
return [{"module": "jupyter_ai_router", "app": RouterExtension}]

jupyter_ai_router/extension.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
import time
3+
from jupyter_events import EventLogger
4+
from jupyter_server.extension.application import ExtensionApp
5+
6+
from jupyter_ai_router.handlers import RouteHandler
7+
8+
from .router import MessageRouter
9+
10+
# Check jupyter-collaboration version for compatibility
11+
try:
12+
from jupyter_collaboration import __version__ as jupyter_collaboration_version
13+
14+
JCOLLAB_VERSION = int(jupyter_collaboration_version[0])
15+
if JCOLLAB_VERSION >= 3:
16+
from jupyter_server_ydoc.utils import JUPYTER_COLLABORATION_EVENTS_URI
17+
else:
18+
from jupyter_collaboration.utils import JUPYTER_COLLABORATION_EVENTS_URI
19+
except ImportError:
20+
# Fallback if jupyter-collaboration is not available
21+
JUPYTER_COLLABORATION_EVENTS_URI = (
22+
"https://events.jupyter.org/jupyter_collaboration"
23+
)
24+
25+
26+
class RouterExtension(ExtensionApp):
27+
"""
28+
Jupyter AI Router Extension
29+
"""
30+
31+
name = "jupyter_ai_router"
32+
handlers = [
33+
(r"jupyter-ai-router/health/?", RouteHandler),
34+
]
35+
36+
def initialize_settings(self):
37+
"""Initialize router settings and event listeners."""
38+
start = time.time()
39+
40+
# Create MessageRouter instance
41+
self.router = MessageRouter(parent=self)
42+
43+
# Make router available to other extensions
44+
if "jupyter-ai" not in self.settings:
45+
self.settings["jupyter-ai"] = {}
46+
self.settings["jupyter-ai"]["router"] = self.router
47+
48+
# Listen for new chat room events
49+
if self.serverapp is not None:
50+
self.event_logger = self.serverapp.web_app.settings["event_logger"]
51+
self.event_logger.add_listener(
52+
schema_id=JUPYTER_COLLABORATION_EVENTS_URI, listener=self._on_chat_event
53+
)
54+
55+
elapsed = time.time() - start
56+
self.log.info(f"Initialized RouterExtension in {elapsed:.2f}s")
57+
58+
async def _on_chat_event(
59+
self, logger: EventLogger, schema_id: str, data: dict
60+
) -> None:
61+
"""Handle chat room events and connect new chats to router."""
62+
# Only handle chat room initialization events
63+
if not (
64+
data["room"].startswith("text:chat:")
65+
and data["action"] == "initialize"
66+
and data["msg"] == "Room initialized"
67+
):
68+
return
69+
70+
room_id = data["room"]
71+
self.log.info(f"New chat room detected: {room_id}")
72+
73+
# Get YChat document for the room
74+
ychat = await self._get_chat(room_id)
75+
if ychat is None:
76+
self.log.error(f"Failed to get YChat for room {room_id}")
77+
return
78+
79+
# Connect chat to router
80+
self.router.connect_chat(room_id, ychat)
81+
82+
async def _get_chat(self, room_id: str):
83+
"""Get YChat instance for a room ID."""
84+
if not self.serverapp:
85+
return None
86+
87+
try:
88+
if JCOLLAB_VERSION >= 3:
89+
collaboration = self.serverapp.web_app.settings["jupyter_server_ydoc"]
90+
document = await collaboration.get_document(room_id=room_id, copy=False)
91+
else:
92+
collaboration = self.serverapp.web_app.settings["jupyter_collaboration"]
93+
server = collaboration.ywebsocket_server
94+
room = await server.get_room(room_id)
95+
document = room._document
96+
97+
return document
98+
except Exception as e:
99+
self.log.error(f"Error getting chat document for {room_id}: {e}")
100+
return None
101+
102+
async def stop_extension(self):
103+
"""Clean up router when extension stops."""
104+
try:
105+
if hasattr(self, "router"):
106+
self.router.cleanup()
107+
except Exception as e:
108+
self.log.error(f"Error during router cleanup: {e}")

jupyter_ai_router/handlers.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22

33
from jupyter_server.base.handlers import APIHandler
4-
from jupyter_server.utils import url_path_join
54
import tornado
65

76
class RouteHandler(APIHandler):
@@ -11,14 +10,5 @@ class RouteHandler(APIHandler):
1110
@tornado.web.authenticated
1211
def get(self):
1312
self.finish(json.dumps({
14-
"data": "This is /jupyter-ai-router/get-example endpoint!"
13+
"data": "JupyterLab extension @jupyter-ai/router is activated!"
1514
}))
16-
17-
18-
def setup_handlers(web_app):
19-
host_pattern = ".*$"
20-
21-
base_url = web_app.settings["base_url"]
22-
route_pattern = url_path_join(base_url, "jupyter-ai-router", "get-example")
23-
handlers = [(route_pattern, RouteHandler)]
24-
web_app.add_handlers(host_pattern, handlers)

0 commit comments

Comments
 (0)