2
2
import re
3
3
import time
4
4
import types
5
- from asyncio import get_event_loop_policy
6
5
from functools import partial
7
- from typing import TYPE_CHECKING , Dict , Optional
6
+ from typing import Dict
8
7
9
8
import traitlets
10
9
from dask .distributed import Client as DaskClient
13
12
from jupyter_ai_magics .utils import get_em_providers , get_lm_providers
14
13
from jupyter_events import EventLogger
15
14
from jupyter_server .extension .application import ExtensionApp
15
+ from jupyter_server .utils import url_path_join
16
16
from jupyterlab_chat .models import Message
17
17
from jupyterlab_chat .ychat import YChat
18
18
from pycrdt import ArrayEvent
22
22
from .chat_handlers .base import BaseChatHandler
23
23
from .completions .handlers import DefaultInlineCompletionHandler
24
24
from .config_manager import ConfigManager
25
+ from .constants import BOT
25
26
from .context_providers import BaseCommandContextProvider , FileContextProvider
26
27
from .handlers import (
27
28
ApiKeysHandler ,
32
33
SlashCommandsInfoHandler ,
33
34
)
34
35
from .history import YChatHistory
35
- from .personas import PersonaManager
36
-
37
- if TYPE_CHECKING :
38
- from asyncio import AbstractEventLoop
39
36
40
37
from jupyter_collaboration import ( # type:ignore[import-untyped] # isort:skip
41
38
__version__ as jupyter_collaboration_version ,
@@ -247,13 +244,6 @@ def initialize(self):
247
244
schema_id = JUPYTER_COLLABORATION_EVENTS_URI , listener = self .connect_chat
248
245
)
249
246
250
- @property
251
- def event_loop (self ) -> "AbstractEventLoop" :
252
- """
253
- Returns a reference to the asyncio event loop.
254
- """
255
- return get_event_loop_policy ().get_event_loop ()
256
-
257
247
async def connect_chat (
258
248
self , logger : EventLogger , schema_id : str , data : dict
259
249
) -> None :
@@ -274,19 +264,17 @@ async def connect_chat(
274
264
if ychat is None :
275
265
return
276
266
267
+ # Add the bot user to the chat document awareness.
268
+ BOT ["avatar_url" ] = url_path_join (
269
+ self .settings .get ("base_url" , "/" ), "api/ai/static/jupyternaut.svg"
270
+ )
271
+ if ychat .awareness is not None :
272
+ ychat .awareness .set_local_state_field ("user" , BOT )
273
+
277
274
# initialize chat handlers for new chat
278
275
self .chat_handlers_by_room [room_id ] = self ._init_chat_handlers (ychat )
279
276
280
- # initialize persona manager
281
- persona_manager = self ._init_persona_manager (ychat )
282
- if not persona_manager :
283
- self .log .error (
284
- "Jupyter AI was unable to initialize its AI personas. They are not available for use in chat until this error is resolved. "
285
- + "Please verify your configuration and open a new issue on GitHub if this error persists."
286
- )
287
- return
288
-
289
- callback = partial (self .on_change , room_id , persona_manager )
277
+ callback = partial (self .on_change , room_id )
290
278
ychat .ymessages .observe (callback )
291
279
292
280
async def get_chat (self , room_id : str ) -> YChat :
@@ -313,26 +301,21 @@ async def get_chat(self, room_id: str) -> YChat:
313
301
self .ychats_by_room [room_id ] = document
314
302
return document
315
303
316
- def on_change (
317
- self , room_id : str , persona_manager : PersonaManager , events : ArrayEvent
318
- ) -> None :
304
+ def on_change (self , room_id : str , events : ArrayEvent ) -> None :
319
305
assert self .serverapp
320
306
321
307
for change in events .delta : # type:ignore[attr-defined]
322
308
if not "insert" in change .keys ():
323
309
continue
310
+ messages = change ["insert" ]
311
+ for message_dict in messages :
312
+ message = Message (** message_dict )
313
+ if message .sender == BOT ["username" ] or message .raw_time :
314
+ continue
324
315
325
- # the "if not m['raw_time']" clause is necessary because every new
326
- # message triggers 2 events, one with `raw_time` set to `True` and
327
- # another with `raw_time` set to `False` milliseconds later.
328
- # we should explore fixing this quirk in Jupyter Chat.
329
- #
330
- # Ref: https://github.com/jupyterlab/jupyter-chat/issues/212
331
- new_messages = [
332
- Message (** m ) for m in change ["insert" ] if not m .get ("raw_time" , False )
333
- ]
334
- for new_message in new_messages :
335
- persona_manager .route_message (new_message )
316
+ self .serverapp .io_loop .asyncio_loop .create_task ( # type:ignore[attr-defined]
317
+ self .route_human_message (room_id , message )
318
+ )
336
319
337
320
async def route_human_message (self , room_id : str , message : Message ):
338
321
"""
@@ -417,15 +400,18 @@ def initialize_settings(self):
417
400
418
401
self .log .info (f"Registered { self .name } server extension" )
419
402
420
- self .settings ["jai_event_loop" ] = self .event_loop
403
+ # get reference to event loop
404
+ # `asyncio.get_event_loop()` is deprecated in Python 3.11+, in favor of
405
+ # the more readable `asyncio.get_event_loop_policy().get_event_loop()`.
406
+ # it's easier to just reference the loop directly.
407
+ loop = self .serverapp .io_loop .asyncio_loop
408
+ self .settings ["jai_event_loop" ] = loop
421
409
422
410
# We cannot instantiate the Dask client directly here because it
423
411
# requires the event loop to be running on init. So instead we schedule
424
412
# this as a task that is run as soon as the loop starts, and pass
425
413
# consumers a Future that resolves to the Dask client when awaited.
426
- self .settings ["dask_client_future" ] = self .event_loop .create_task (
427
- self ._get_dask_client ()
428
- )
414
+ self .settings ["dask_client_future" ] = loop .create_task (self ._get_dask_client ())
429
415
430
416
# Create empty context providers dict to be filled later.
431
417
# This is created early to use as kwargs for chat handlers.
@@ -470,7 +456,10 @@ async def _stop_extension(self):
470
456
471
457
def _init_chat_handlers (self , ychat : YChat ) -> Dict [str , BaseChatHandler ]:
472
458
"""
473
- Initializes a set of chat handlers for a given `YChat` instance.
459
+ Initializes a set of chat handlers. May accept a YChat instance for
460
+ collaborative chats.
461
+
462
+ TODO: Make `ychat` required once Jupyter Chat migration is complete.
474
463
"""
475
464
assert self .serverapp
476
465
@@ -617,32 +606,3 @@ def _init_context_providers(self):
617
606
** context_providers_kwargs
618
607
)
619
608
self .log .info (f"Registered context provider `{ context_provider .id } `." )
620
-
621
- def _init_persona_manager (self , ychat : YChat ) -> Optional [PersonaManager ]:
622
- """
623
- Initializes a `PersonaManager` instance scoped to a `YChat`.
624
-
625
- This method should not raise an exception. Upon encountering an
626
- exception, this method will catch it, log it, and return `None`.
627
- """
628
- persona_manager : Optional [PersonaManager ]
629
-
630
- try :
631
- config_manager = self .settings .get ("jai_config_manager" , None )
632
- assert config_manager and isinstance (config_manager , ConfigManager )
633
-
634
- persona_manager = PersonaManager (
635
- ychat = ychat ,
636
- config_manager = config_manager ,
637
- event_loop = self .event_loop ,
638
- log = self .log ,
639
- )
640
- except Exception as e :
641
- # TODO: how to stop the extension when this fails
642
- # also why do uncaught exceptions produce an empty error log in Jupyter Server?
643
- self .log .error (
644
- f"Unable to initialize PersonaManager in YChat with ID '{ ychat .get_id ()} ' due to an exception printed below."
645
- )
646
- self .log .exception (e )
647
- finally :
648
- return persona_manager
0 commit comments