Skip to content

Commit 56f79c6

Browse files
committed
implement backend handling of /refresh-personas
1 parent 74125c4 commit 56f79c6

File tree

3 files changed

+116
-15
lines changed

3 files changed

+116
-15
lines changed

jupyter_ai_chat_commands/__init__.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import warnings
88
warnings.warn("Importing 'jupyter_ai_chat_commands' outside a proper installation.")
99
__version__ = "dev"
10-
from .handlers import setup_handlers
10+
from .extension_app import JaiChatCommandsExtension
1111

1212

1313
def _jupyter_labextension_paths():
@@ -19,18 +19,7 @@ def _jupyter_labextension_paths():
1919

2020
def _jupyter_server_extension_points():
2121
return [{
22-
"module": "jupyter_ai_chat_commands"
22+
"module": "jupyter_ai_chat_commands",
23+
"app": JaiChatCommandsExtension
2324
}]
2425

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_chat_commands"
36-
server_app.log.info(f"Registered {name} server extension")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ classifiers = [
2626
"Programming Language :: Python :: 3.12",
2727
"Programming Language :: Python :: 3.13",
2828
]
29-
dependencies = ["jupyter_server>=2.4.0,<3", "jupyterlab_chat>=0.17.0,<0.18.0"]
29+
dependencies = [
30+
"jupyter_server>=2.4.0,<3",
31+
"jupyterlab_chat>=0.17.0,<0.18.0",
32+
"jupyter_ai_router>=0.0.1",
33+
"jupyter_ai_persona_manager>=0.0.3",
34+
]
3035
dynamic = ["version", "description", "authors", "urls", "keywords"]
3136

3237
[project.optional-dependencies]

0 commit comments

Comments
 (0)