1+ import asyncio
2+ from jupyter_server .extension .application import ExtensionApp
3+ from .handlers import RouteHandler
4+
5+ from jupyterlab_chat .models import Message
6+ from jupyter_ai_router .router import MessageRouter
7+ from jupyter_ai_persona_manager import PersonaManager
8+
9+ class JaiChatCommandsExtension (ExtensionApp ):
10+
11+ name = "jupyter_ai_chat_commands"
12+ handlers = [
13+ (r"jupyter-ai-chat-commands/get-example/?" , RouteHandler )
14+ ]
15+
16+ @property
17+ def event_loop (self ) -> asyncio .AbstractEventLoop :
18+ """
19+ Returns a reference to the asyncio event loop.
20+ """
21+ return asyncio .get_event_loop_policy ().get_event_loop ()
22+
23+ def initialize_settings (self ):
24+ """
25+ See `self.initialize_async()`.
26+ """
27+ super ().initialize_settings ()
28+ self .event_loop .create_task (self .initialize_async ())
29+
30+ async def initialize_async (self ):
31+ """
32+ This method waits for the `MessageRouter` singleton to be initialized by
33+ `jupyter_ai_router`. It attaches an observer to listen to when a chat is
34+ initialized, which in turn attaches an observer to listen to when a
35+ slash command is called in any chat.
36+ """
37+ router : MessageRouter = await self ._get_router ()
38+ self .log .debug ("Obtained reference to router." )
39+
40+ router .observe_chat_init (lambda room_id , ychat : self .on_chat_init (router , room_id ))
41+
42+ def on_chat_init (self , router : MessageRouter , room_id : str ):
43+ router .observe_slash_cmd_msg (room_id , self .on_slash_command )
44+ self .log .info ("Attached router observer." )
45+
46+ def on_slash_command (self , room_id : str , message : Message ):
47+ first_word = get_first_word (message .body )
48+ assert first_word and first_word .startswith ("/" )
49+
50+ command_id = first_word [1 :]
51+ if command_id == "refresh-personas" :
52+ self .event_loop .create_task (self .handle_refresh_personas (room_id ))
53+ return True
54+
55+ # If command is unrecognized, log an error
56+ self .log .warning (f"Unrecognized slash command: '/{ command_id } '" )
57+ return False
58+
59+ async def handle_refresh_personas (self , room_id : str ):
60+ self .log .info (f"Received /refresh-personas in room '{ room_id } '." )
61+ persona_manager = await self ._get_persona_manager (room_id )
62+ self .log .info (f"Retrieved PersonaManager in room '{ room_id } '." )
63+ await persona_manager .refresh_personas ()
64+ self .log .info (f"Handled /refresh-personas in room '{ room_id } '." )
65+
66+
67+ async def _get_router (self ) -> MessageRouter :
68+ """
69+ Returns the `MessageRouter` singleton initialized by
70+ `jupyter_ai_router`.
71+ """
72+ while True :
73+ router = self .serverapp .web_app .settings .get ("jupyter-ai" , {}).get ("router" )
74+ if router is not None :
75+ return router
76+ await asyncio .sleep (0.1 ) # Check every 100ms
77+
78+ async def _get_persona_manager (self , room_id : str ) -> PersonaManager :
79+ """
80+ Returns the `PersonaManager` for a chat given its room ID.
81+ """
82+ while True :
83+ persona_managers_by_room = self .serverapp .web_app .settings .get ("jupyter-ai" , {}).get ("persona-managers" , {})
84+ manager = persona_managers_by_room .get (room_id )
85+ if manager is not None :
86+ return manager
87+ await asyncio .sleep (0.1 ) # Check every 100ms
88+
89+
90+ def get_first_word (input_str : str ) -> str | None :
91+ """
92+ Finds the first word in a given string, ignoring leading whitespace.
93+ Returns the first word, or None if there is no first word.
94+ """
95+ start = 0
96+
97+ # Skip leading whitespace
98+ while start < len (input_str ) and input_str [start ].isspace ():
99+ start += 1
100+
101+ # Find end of first word
102+ end = start
103+ while end < len (input_str ) and not input_str [end ].isspace ():
104+ end += 1
105+
106+ first_word = input_str [start :end ]
107+ return first_word if first_word else None
0 commit comments