Skip to content

Commit eeeaece

Browse files
brichetpre-commit-ci[bot]dlqqq
authored
Periodically update the persona awareness to keep it alive (#1358)
* Periodically update the personna awareness state to keep it alive * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Stop updating personna awareness if there is no user in document awareness * Improve warning message * Fix typing in warning message * Update jupyter chat dependency to >=0.11.0 * Change the BOT contant from dict to User (should not be used anyway with the personna) * Fix typing for python 3.9 * Update awareness state every 0.8 * outdated_timeout, and never set _heartbeat_task to None Co-authored-by: David L. Qiu <[email protected]> * Clean unused imports * remove awareness observer --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: David L. Qiu <[email protected]> Co-authored-by: David L. Qiu <[email protected]>
1 parent 3236155 commit eeeaece

File tree

8 files changed

+77
-24
lines changed

8 files changed

+77
-24
lines changed

packages/jupyter-ai/jupyter_ai/chat_handlers/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,11 @@ def reply(self, body: str, _human_message=None) -> str:
275275
TODO: Either properly store & use reply state in YChat, or remove the
276276
`human_message` argument here.
277277
"""
278-
bot = self.ychat.get_user(BOT["username"])
278+
bot = self.ychat.get_user(BOT.username)
279279
if not bot:
280-
self.ychat.set_user(User(**BOT))
280+
self.ychat.set_user(BOT)
281281

282-
id = self.ychat.add_message(NewMessage(body=body, sender=BOT["username"]))
282+
id = self.ychat.add_message(NewMessage(body=body, sender=BOT.username))
283283
return id
284284

285285
@property

packages/jupyter-ai/jupyter_ai/chat_handlers/utils/streaming.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ def __init__(self, ychat: YChat):
3838
self._stream_id: Optional[str] = None
3939

4040
def _set_user(self):
41-
bot = self.ychat.get_user(BOT["username"])
41+
bot = self.ychat.get_user(BOT.username)
4242
if not bot:
43-
self.ychat.set_user(User(**BOT))
43+
self.ychat.set_user(BOT)
4444

4545
def open(self):
4646
self._set_user()
@@ -60,7 +60,7 @@ def write(self, chunk: str) -> str:
6060
if not self._stream_id:
6161
self._set_user()
6262
self._stream_id = self.ychat.add_message(
63-
NewMessage(body="", sender=BOT["username"])
63+
NewMessage(body="", sender=BOT.username)
6464
)
6565

6666
self._set_user()
@@ -69,7 +69,7 @@ def write(self, chunk: str) -> str:
6969
id=self._stream_id,
7070
body=chunk,
7171
time=time.time(),
72-
sender=BOT["username"],
72+
sender=BOT.username,
7373
raw_time=False,
7474
),
7575
append=True,
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from jupyterlab_chat.models import User
2+
13
# The BOT currently has a fixed username, because this username is used has key in chats,
24
# it needs to constant. Do we need to change it ?
3-
BOT = {
4-
"username": "5f6a7570-7974-6572-6e61-75742d626f74",
5-
"name": "Jupyternaut",
6-
"display_name": "Jupyternaut",
7-
"initials": "J",
8-
}
5+
BOT = User(
6+
username="5f6a7570-7974-6572-6e61-75742d626f74",
7+
name="Jupyternaut",
8+
display_name="Jupyternaut",
9+
initials="J",
10+
bot=True,
11+
)

packages/jupyter-ai/jupyter_ai/personas/base_persona.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def as_user(self) -> User:
199199
name=self.name,
200200
display_name=self.name,
201201
avatar_url=self.avatar_path,
202+
bot=True,
202203
)
203204

204205
def as_user_dict(self) -> dict[str, Any]:

packages/jupyter-ai/jupyter_ai/personas/persona_awareness.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import random
23
from contextlib import contextmanager
34
from dataclasses import asdict
@@ -21,7 +22,7 @@ class PersonaAwareness:
2122
2223
- This class optionally accepts a `User` object in the constructor. When
2324
passed, this class will automatically register this user in the awareness
24-
dictionary.
25+
dictionary on init.
2526
2627
- This class works by manually setting `ydoc.awareness.client_id` before &
2728
after each method call. This class provides a `self.as_custom_client()`
@@ -38,6 +39,7 @@ class PersonaAwareness:
3839

3940
_original_client_id: int
4041
_custom_client_id: int
42+
_heartbeat_task: asyncio.Task
4143

4244
def __init__(self, *, ychat: YChat, log: Logger, user: Optional[User]):
4345
# Bind instance attributes
@@ -54,12 +56,14 @@ def __init__(self, *, ychat: YChat, log: Logger, user: Optional[User]):
5456
self._original_client_id = self.awareness.client_id
5557
self._custom_client_id = random.getrandbits(32)
5658

57-
with self.as_custom_client():
58-
self.awareness.set_local_state({})
59-
59+
# Initialize local awareness state using the custom client ID
60+
self.set_local_state({})
6061
if self.user:
6162
self._register_user()
6263

64+
# Start the awareness heartbeat task
65+
self._heartbeat_task = asyncio.create_task(self._start_heartbeat())
66+
6367
@contextmanager
6468
def as_custom_client(self) -> "Iterator[None]":
6569
"""
@@ -77,6 +81,17 @@ def as_custom_client(self) -> "Iterator[None]":
7781
finally:
7882
self.awareness.client_id = self._original_client_id
7983

84+
@property
85+
def outdated_timeout(self) -> int:
86+
"""
87+
Returns the outdated timeout of the document awareness, in milliseconds.
88+
The timeout value should be 30000 milliseconds (30 seconds), according to the
89+
default value in `y-protocols.awareness` and `pycrdt.Awareness`.
90+
- https://github.com/yjs/y-protocols/blob/2d8cd5c06b3925fbf9b5215dc341f8096a0a8d5c/awareness.js#L13
91+
- https://github.com/y-crdt/pycrdt/blob/e269a3e63ad7986a3349e2d2bc7bd5f0dfca9c79/python/pycrdt/_awareness.py#L23
92+
"""
93+
return self.awareness._outdated_timeout
94+
8095
def _register_user(self):
8196
if not self.user:
8297
return
@@ -85,9 +100,43 @@ def _register_user(self):
85100
self.awareness.set_local_state_field("user", asdict(self.user))
86101

87102
def get_local_state(self) -> Optional[dict[str, Any]]:
103+
"""
104+
Returns the local state of the awareness instance.
105+
"""
88106
with self.as_custom_client():
89107
return self.awareness.get_local_state()
90108

109+
def set_local_state(self, state: dict[str, Any]) -> None:
110+
"""
111+
Sets the local state of the awareness instance to the provided state.
112+
This method is used to update the local state of the awareness instance
113+
with a new state dictionary.
114+
"""
115+
with self.as_custom_client():
116+
self.awareness.set_local_state(state)
117+
91118
def set_local_state_field(self, field: str, value: Any) -> None:
119+
"""
120+
Sets a specific field in the local state of the awareness instance.
121+
"""
92122
with self.as_custom_client():
93123
self.awareness.set_local_state_field(field, value)
124+
125+
async def _start_heartbeat(self):
126+
"""
127+
Background task that updates this instance's local state every
128+
`0.8 * self.outdated_timeout` milliseconds. `pycrdt` and `yjs`
129+
automatically disconnect clients if they do not make updates in
130+
a long time (default: 30000 ms). This task keeps personas alive
131+
after 30 seconds of no usage in each chat session.
132+
"""
133+
while True:
134+
await asyncio.sleep(0.8 * self.outdated_timeout / 1000)
135+
local_state = self.get_local_state() or {}
136+
self.set_local_state(local_state)
137+
138+
def stop(self) -> None:
139+
"""
140+
Stops the awareness heartbeat task if it is running.
141+
"""
142+
self._heartbeat_task.cancel()

packages/jupyter-ai/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@emotion/react": "^11.10.5",
6363
"@emotion/styled": "^11.10.5",
6464
"@jupyter-notebook/application": "^7.2.0",
65-
"@jupyter/chat": "^0.10.0",
65+
"@jupyter/chat": "^0.11.0",
6666
"@jupyterlab/application": "^4.2.0",
6767
"@jupyterlab/apputils": "^4.2.0",
6868
"@jupyterlab/codeeditor": "^4.2.0",

packages/jupyter-ai/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
# traitlets>=5.6 is required in JL4
3838
"traitlets>=5.6",
3939
"deepmerge>=2.0,<3",
40-
"jupyterlab-chat>=0.10.0,<0.11.0",
40+
"jupyterlab-chat>=0.11.0,<0.12.0",
4141
]
4242

4343
dynamic = ["version", "description", "authors", "urls", "keywords"]

yarn.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,7 +2237,7 @@ __metadata:
22372237
"@emotion/react": ^11.10.5
22382238
"@emotion/styled": ^11.10.5
22392239
"@jupyter-notebook/application": ^7.2.0
2240-
"@jupyter/chat": ^0.10.0
2240+
"@jupyter/chat": ^0.11.0
22412241
"@jupyterlab/application": ^4.2.0
22422242
"@jupyterlab/apputils": ^4.2.0
22432243
"@jupyterlab/builder": ^4.2.0
@@ -2322,9 +2322,9 @@ __metadata:
23222322
languageName: node
23232323
linkType: hard
23242324

2325-
"@jupyter/chat@npm:^0.10.0":
2326-
version: 0.10.0
2327-
resolution: "@jupyter/chat@npm:0.10.0"
2325+
"@jupyter/chat@npm:^0.11.0":
2326+
version: 0.11.0
2327+
resolution: "@jupyter/chat@npm:0.11.0"
23282328
dependencies:
23292329
"@emotion/react": ^11.10.5
23302330
"@emotion/styled": ^11.10.5
@@ -2346,7 +2346,7 @@ __metadata:
23462346
clsx: ^2.1.0
23472347
react: ^18.2.0
23482348
react-dom: ^18.2.0
2349-
checksum: c9b0bc1c39b966a94415f6b95c86bda1a1bedefc654cec051ca6ebd30fc218a31cc1a62628bf2c5b529284cdafd5ce3fe9e15bb9bfdaa21960d47acdc2b66ebd
2349+
checksum: e7b5c47c94b6afa3ce6926c4f9ed1a913cfb95d7187015d058550b69cf26bdbd42240b8f550646f9ddfeed34c35ac81c7a664c1873152fa4ddd2a5f36ed5eb51
23502350
languageName: node
23512351
linkType: hard
23522352

0 commit comments

Comments
 (0)