11from __future__ import annotations
2+ import asyncio
3+ import json
24from typing import TYPE_CHECKING
35import time
46from jupyter_events import EventLogger
57from jupyter_server .extension .application import ExtensionApp
8+ from jupyter_ydoc .ybasedoc import YBaseDoc
69
710from jupyter_ai_router .handlers import RouteHandler
811
@@ -41,6 +44,18 @@ class RouterExtension(ExtensionApp):
4144
4245 router : MessageRouter
4346
47+ @property
48+ def event_loop (self ) -> asyncio .AbstractEventLoop :
49+ """
50+ Returns a reference to the asyncio event loop.
51+ """
52+ return asyncio .get_event_loop_policy ().get_event_loop ()
53+
54+ @property
55+ def fileid_manager (self ):
56+ return self .serverapp .web_app .settings ["file_id_manager" ]
57+
58+
4459 def initialize_settings (self ):
4560 """Initialize router settings and event listeners."""
4661 start = time .time ()
@@ -59,10 +74,53 @@ def initialize_settings(self):
5974 self .event_logger .add_listener (
6075 schema_id = JUPYTER_COLLABORATION_EVENTS_URI , listener = self ._on_chat_event
6176 )
62-
77+ self .event_loop .create_task (self ._check_notebook_observer ())
78+
6379 elapsed = time .time () - start
6480 self .log .info (f"Initialized RouterExtension in { elapsed :.2f} s" )
6581
82+
83+ async def _check_notebook_observer (self ):
84+ await asyncio .sleep (20 )
85+ def callback (username , prev_active_cell , notebook_path ):
86+ self .log .info (
87+ f"notebook observer callback : { username = } , { prev_active_cell = } , { notebook_path = } "
88+ )
89+
90+ jcollab_api = self .serverapp .web_app .settings ["jupyter_server_ydoc" ]
91+ yroom_manager = jcollab_api .yroom_manager
92+ yroom = yroom_manager .get_room ("JupyterLab:globalAwareness" )
93+ awareness = yroom .get_awareness ()
94+ for _ , state in awareness .states .items ():
95+ if username := state .get ("user" , {}).get ("username" , None ):
96+ self .router .observe_notebook_activity (
97+ username = username , callback = callback
98+ )
99+ break
100+
101+
102+ def _get_global_awareness (self ):
103+ # TODO: make this compatible with jcollab
104+ jcollab_api = self .serverapp .web_app .settings ["jupyter_server_ydoc" ]
105+ yroom_manager = jcollab_api .yroom_manager
106+ yroom = yroom_manager .get_room ("JupyterLab:globalAwareness" )
107+ return yroom .get_awareness ()
108+
109+ async def _room_id_from_path (self , path : str ) -> str | None :
110+ """Returns room_id from document path"""
111+ # TODO: Make this compatible with jcollab
112+ yroom_manager = self .serverapp .web_app .settings ["yroom_manager" ]
113+ for room_id in yroom_manager ._rooms_by_id :
114+ if room_id == "JupyterLab:globalAwareness" :
115+ continue
116+ ydoc = await self ._get_doc (room_id )
117+ state = ydoc .awareness .get_local_state ()
118+ file_id = state ["file_id" ]
119+ ydoc_path = self .fileid_manager .get_path (file_id )
120+ if ydoc_path == path :
121+ print (f"Found match in path { path } " )
122+ return room_id
123+
66124 async def _on_chat_event (
67125 self , logger : EventLogger , schema_id : str , data : dict
68126 ) -> None :
@@ -87,6 +145,84 @@ async def _on_chat_event(
87145 # Connect chat to router
88146 self .router .connect_chat (room_id , ychat )
89147
148+ async def _on_notebook_event (
149+ self , logger : EventLogger , schema_id : str , data : dict
150+ ) -> None :
151+ """Handle notebook room events and connect new chats to router."""
152+ # Only handle notebook room initialization events
153+ if not (
154+ data ["room" ].startswith ("json:notebook:" )
155+ and data ["action" ] == "initialize"
156+ and data ["msg" ] == "Room initialized"
157+ ):
158+ return
159+
160+ room_id = data ["room" ]
161+ self .log .info (f"New notebook room detected: { room_id } " )
162+
163+ # Get YDoc document for the room
164+ ydoc = await self ._get_doc (room_id )
165+ if ydoc is None :
166+ self .log .error (f"Failed to get YDoc for room { room_id } " )
167+ return
168+
169+ # Connect notebook to router
170+ self .router .connect_notebook (room_id , ydoc )
171+
172+ async def _get_doc (self , room_id : str ) -> YBaseDoc | None :
173+ """
174+ Get YDoc instance for a room ID.
175+
176+ Dispatches to either `_get_doc_jcollab()` or `_get_doc_jsd()` based on
177+ whether `jupyter_server_documents` is installed.
178+ """
179+
180+ if JSD_PRESENT :
181+ return await self ._get_doc_jsd (room_id )
182+ else :
183+ return await self ._get_doc_jcollab (room_id )
184+
185+ async def _get_doc_jcollab (self , room_id : str ) -> YBaseDoc | None :
186+ """
187+ Method used to retrieve the `YDoc` instance for a given room when
188+ `jupyter_server_documents` **is not** installed.
189+ """
190+ if not self .serverapp :
191+ return None
192+
193+ try :
194+ collaboration = self .serverapp .web_app .settings ["jupyter_server_ydoc" ]
195+ document = await collaboration .get_document (room_id = room_id , copy = False )
196+ return document
197+ except Exception as e :
198+ self .log .error (f"Error getting ydoc for { room_id } : { e } " )
199+ return None
200+
201+ async def _get_doc_jsd (self , room_id : str ) -> YBaseDoc | None :
202+ """
203+ Method used to retrieve the `YDoc` instance for a given room when
204+ `jupyter_server_documents` **is** installed.
205+
206+ This method uniquely attaches a callback which is fired whenever the
207+ `YDoc` is reset.
208+ """
209+ if not self .serverapp :
210+ return None
211+
212+ try :
213+ jcollab_api = self .serverapp .web_app .settings ["jupyter_server_ydoc" ]
214+ yroom_manager = jcollab_api .yroom_manager
215+ yroom = yroom_manager .get_room (room_id )
216+
217+ def _on_ydoc_reset (new_ydoc : YBaseDoc ):
218+ self .router ._on_notebook_reset (room_id , new_ydoc )
219+
220+ ydoc = await yroom .get_jupyter_ydoc (on_reset = _on_ydoc_reset )
221+ return ydoc
222+ except Exception as e :
223+ self .log .error (f"Error getting ydoc for { room_id } : { e } " )
224+ return None
225+
90226 async def _get_chat (self , room_id : str ) -> YChat | None :
91227 """
92228 Get YChat instance for a room ID.
0 commit comments