Skip to content

Commit 9a4e4ee

Browse files
authored
Allow consumers to observe document resets on YChat instances (#10)
* remove jcollab < 4 compatibility code * call dedicated method on Router when YChat is reset * impl observe_chat_reset()
1 parent b073a02 commit 9a4e4ee

File tree

2 files changed

+90
-19
lines changed

2 files changed

+90
-19
lines changed

jupyter_ai_router/extension.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from typing import TYPE_CHECKING
23
import time
34
from jupyter_events import EventLogger
45
from jupyter_server.extension.application import ExtensionApp
@@ -7,21 +8,26 @@
78

89
from .router import MessageRouter
910

10-
# Check jupyter-collaboration version for compatibility
1111
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
12+
from jupyter_server_ydoc.utils import JUPYTER_COLLABORATION_EVENTS_URI
1913
except ImportError:
2014
# Fallback if jupyter-collaboration is not available
2115
JUPYTER_COLLABORATION_EVENTS_URI = (
2216
"https://events.jupyter.org/jupyter_collaboration"
2317
)
2418

19+
# Define `JSD_PRESENT` to indicate whether `jupyter_server_documents` is
20+
# installed in the current environment.
21+
JSD_PRESENT = False
22+
try:
23+
import jupyter_server_documents
24+
JSD_PRESENT = True
25+
except ImportError:
26+
pass
27+
28+
if TYPE_CHECKING:
29+
from jupyterlab_chat.ychat import YChat
30+
2531

2632
class RouterExtension(ExtensionApp):
2733
"""
@@ -33,6 +39,8 @@ class RouterExtension(ExtensionApp):
3339
(r"jupyter-ai-router/health/?", RouteHandler),
3440
]
3541

42+
router: MessageRouter
43+
3644
def initialize_settings(self):
3745
"""Initialize router settings and event listeners."""
3846
start = time.time()
@@ -79,25 +87,61 @@ async def _on_chat_event(
7987
# Connect chat to router
8088
self.router.connect_chat(room_id, ychat)
8189

82-
async def _get_chat(self, room_id: str):
83-
"""Get YChat instance for a room ID."""
90+
async def _get_chat(self, room_id: str) -> YChat | None:
91+
"""
92+
Get YChat instance for a room ID.
93+
94+
Dispatches to either `_get_chat_jcollab()` or `_get_chat_jsd()` based on
95+
whether `jupyter_server_documents` is installed.
96+
"""
97+
98+
if JSD_PRESENT:
99+
return await self._get_chat_jsd(room_id)
100+
else:
101+
return await self._get_chat_jcollab(room_id)
102+
103+
async def _get_chat_jcollab(self, room_id: str) -> YChat | None:
104+
"""
105+
Method used to retrieve the `YChat` instance for a given room when
106+
`jupyter_server_documents` **is not** installed.
107+
"""
84108
if not self.serverapp:
85109
return None
86110

87111
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-
112+
collaboration = self.serverapp.web_app.settings["jupyter_server_ydoc"]
113+
document = await collaboration.get_document(room_id=room_id, copy=False)
97114
return document
98115
except Exception as e:
99116
self.log.error(f"Error getting chat document for {room_id}: {e}")
100117
return None
118+
119+
async def _get_chat_jsd(self, room_id: str) -> YChat | None:
120+
"""
121+
Method used to retrieve the `YChat` instance for a given room when
122+
`jupyter_server_documents` **is** installed.
123+
124+
This method uniquely attaches a callback which is fired whenever the
125+
`YChat` is reset.
126+
"""
127+
if not self.serverapp:
128+
return None
129+
130+
try:
131+
jcollab_api = self.serverapp.web_app.settings["jupyter_server_ydoc"]
132+
yroom_manager = jcollab_api.yroom_manager
133+
yroom = yroom_manager.get_room(room_id)
134+
135+
def _on_ychat_reset(new_ychat: YChat):
136+
self.router._on_chat_reset(room_id, new_ychat)
137+
138+
ychat = await yroom.get_jupyter_ydoc(on_reset=_on_ychat_reset)
139+
return ychat
140+
except Exception as e:
141+
self.log.error(f"Error getting chat document for {room_id}: {e}")
142+
return None
143+
144+
101145

102146
async def stop_extension(self):
103147
"""Clean up router when extension stops."""

jupyter_ai_router/router.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self, *args, **kwargs):
5555
self.chat_init_observers: List[Callable[[str, "YChat"], Any]] = []
5656
self.slash_cmd_observers: Dict[str, Dict[str, List[Callable[[str, str, Message], Any]]]] = {}
5757
self.chat_msg_observers: Dict[str, List[Callable[[str, Message], Any]]] = {}
58+
self.chat_reset_observers: List[Callable[[str, "YChat"], Any]] = []
5859

5960
# Active chat rooms
6061
self.active_chats: Dict[str, "YChat"] = {}
@@ -71,7 +72,18 @@ def observe_chat_init(self, callback: Callable[[str, "YChat"], Any]) -> None:
7172
"""
7273
self.chat_init_observers.append(callback)
7374
self.log.info("Registered new chat initialization callback")
75+
76+
def observe_chat_reset(self, callback: Callable[[str, "YChat"], Any]) -> None:
77+
"""
78+
Register a callback for when a `YChat` document is reset. This will only
79+
occur if `jupyter_server_documents` is installed.
7480
81+
Args:
82+
callback: Function called with (room_id: str, new_ychat: YChat) when chat resets
83+
"""
84+
self.chat_reset_observers.append(callback)
85+
self.log.info("Registered new chat reset callback")
86+
7587
def observe_slash_cmd_msg(
7688
self, room_id: str, command_pattern: str, callback: Callable[[str, str, Message], Any]
7789
) -> None:
@@ -230,6 +242,21 @@ def _notify_msg_observers(self, room_id: str, message: Message) -> None:
230242
callback(room_id, message)
231243
except Exception as e:
232244
self.log.error(f"Message observer error for {room_id}: {e}")
245+
246+
def _on_chat_reset(self, room_id, ychat: "YChat") -> None:
247+
"""
248+
Method to call when the YChat undergoes a document reset, e.g. when the
249+
`.chat` file is modified directly on disk.
250+
251+
NOTE: Document resets will only occur when `jupyter_server_documents` is
252+
installed.
253+
"""
254+
self.log.warning(f"Detected `YChat` document reset in room '{room_id}'.")
255+
for callback in self.chat_reset_observers:
256+
try:
257+
callback(room_id, ychat)
258+
except Exception as e:
259+
self.log.error(f"Reset chat observer error for {room_id}: {e}")
233260

234261
def cleanup(self) -> None:
235262
"""Clean up router resources."""

0 commit comments

Comments
 (0)