Skip to content

Commit aefde63

Browse files
authored
Upgrade to Jupyter Chat v0.15.0 (#1389)
* remove error log added in dev * upgrade to Jupyter Chat v0.14.0 * use new 0.14 API in @file command * upgrade jupyter_server min dependency * use negative lookbehind to avoid escaping spaces multiple times * upgrade to Jupyter Chat v0.15.0
1 parent 7bffb75 commit aefde63

File tree

6 files changed

+100
-46
lines changed

6 files changed

+100
-46
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ def __init__(
9292
assert isinstance(PersonaManager._persona_classes, list)
9393

9494
self._personas = self._init_personas()
95-
self.log.error(self.get_chat_dir())
9695

9796
def _init_persona_classes(self) -> None:
9897
"""

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.12.0",
65+
"@jupyter/chat": "^0.15.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: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ classifiers = [
2222
"Programming Language :: Python :: 3.12",
2323
]
2424
dependencies = [
25-
# jupyter_server>=2.4 is required in JL4
26-
# jupyter_server>=2.11.1 is required by jupyter_server_ydoc>=3
27-
"jupyter_server>=2.11.1,<3",
25+
# `jupyter_collaboration>=4` requires `jupyter_server_ydoc>=2.0.0`,
26+
# which requires `jupyter_server>=2.15.0`.
27+
"jupyter_server>=2.15.0,<3",
2828
"importlib_metadata>=5.2.0",
2929
# pydantic <2.10.0 raises a "protected namespaces" error in JAI
3030
# - See: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.protected_namespaces
@@ -33,16 +33,18 @@ dependencies = [
3333
# traitlets>=5.6 is required in JL4
3434
"traitlets>=5.6",
3535
"deepmerge>=2.0,<3",
36-
"jupyterlab-chat>=0.12.0,<0.14.0",
36+
# NOTE: Make sure to update the corresponding dependency in
37+
# `packages/jupyter-ai/package.json` to match the version range below
38+
"jupyterlab-chat>=0.15.0,<0.16.0",
3739
]
3840

3941
dynamic = ["version", "description", "authors", "urls", "keywords"]
4042

4143
[project.optional-dependencies]
4244
test = [
43-
# jupyter_server>=2.4 is required in JL4
44-
# jupyter_server>=2.11.1 is required by jupyter_server_ydoc>=3
45-
"jupyter_server[test]>=2.11.1,<3",
45+
# `jupyter_collaboration>=4` requires `jupyter_server_ydoc>=2.0.0`,
46+
# which requires `jupyter_server>=2.15.0`.
47+
"jupyter_server[test]>=2.15.0,<3",
4648
"coverage",
4749
"pytest",
4850
"pytest-asyncio",

packages/jupyter-ai/src/chat-commands/context-commands.ts renamed to packages/jupyter-ai/src/chat-commands/context-commands.tsx

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Distributed under the terms of the Modified BSD License.
44
*/
55

6+
import React from 'react';
67
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
78
import type { Contents } from '@jupyterlab/services';
89
import type { DocumentRegistry } from '@jupyterlab/docregistry';
@@ -12,6 +13,7 @@ import {
1213
IInputModel,
1314
ChatCommand
1415
} from '@jupyter/chat';
16+
import FindInPage from '@mui/icons-material/FindInPage';
1517

1618
const CONTEXT_COMMANDS_PROVIDER_ID =
1719
'@jupyter-ai/core:context-commands-provider';
@@ -21,17 +23,18 @@ const CONTEXT_COMMANDS_PROVIDER_ID =
2123
*/
2224
export class ContextCommandsProvider implements IChatCommandProvider {
2325
public id: string = CONTEXT_COMMANDS_PROVIDER_ID;
24-
private _context_commands: ChatCommand[] = [
25-
// TODO: add an icon!
26-
// import FindInPage from '@mui/icons-material/FindInPage';
27-
// may need to change the API to allow JSX els as icons
28-
{
29-
name: '@file',
30-
providerId: this.id,
31-
replaceWith: '@file:',
32-
description: 'Include a file with your prompt'
33-
}
34-
];
26+
27+
/**
28+
* Regex that matches all valid `@file` calls. The first capturing group
29+
* captures the path specified by the user. Paths may contain any combination
30+
* of:
31+
*
32+
* `[a-zA-Z0-9], '/', '-', '_', '.', '@', '\\ ' (escaped space)`
33+
*
34+
* IMPORTANT: `+` ensures this regex only matches an occurrence of "@file:" if
35+
* the captured path is non-empty.
36+
*/
37+
_regex: RegExp = /@file:(([\w/\-_.@]|\\ )+)/g;
3538

3639
constructor(
3740
contentsManager: Contents.IManager,
@@ -41,15 +44,17 @@ export class ContextCommandsProvider implements IChatCommandProvider {
4144
this._docRegistry = docRegistry;
4245
}
4346

44-
async getChatCommands(inputModel: IInputModel) {
47+
async listCommandCompletions(
48+
inputModel: IInputModel
49+
): Promise<ChatCommand[]> {
4550
// do nothing if the current word does not start with '@'.
4651
const currentWord = inputModel.currentWord;
4752
if (!currentWord || !currentWord.startsWith('@')) {
4853
return [];
4954
}
5055

5156
// if the current word starts with `@file:`, return a list of valid file
52-
// paths.
57+
// paths that complete the currently specified path.
5358
if (currentWord.startsWith('@file:')) {
5459
const searchPath = currentWord.split('@file:')[1];
5560
const commands = await getPathCompletions(
@@ -60,19 +65,55 @@ export class ContextCommandsProvider implements IChatCommandProvider {
6065
return commands;
6166
}
6267

63-
// otherwise, a context command has not yet been specified. return a list of
64-
// valid context commands.
65-
const commands = this._context_commands.filter(cmd =>
66-
cmd.name.startsWith(currentWord)
67-
);
68-
return commands;
68+
// if the current word matches the start of @file, complete it
69+
if ('@file'.startsWith(currentWord)) {
70+
return [
71+
{
72+
name: '@file:',
73+
providerId: this.id,
74+
description: 'Include a file with your prompt',
75+
icon: <FindInPage />
76+
}
77+
];
78+
}
79+
80+
// otherwise, return nothing as this provider cannot provide any completions
81+
// for the current word.
82+
return [];
6983
}
7084

71-
async handleChatCommand(
72-
command: ChatCommand,
73-
inputModel: IInputModel
74-
): Promise<void> {
75-
// no handling needed because `replaceWith` is set in each command.
85+
async onSubmit(inputModel: IInputModel): Promise<void> {
86+
// search entire input for valid @file commands using `this._regex`
87+
const matches = Array.from(inputModel.value.matchAll(this._regex));
88+
89+
// aggregate all file paths specified by @file commands in the input
90+
const paths: string[] = [];
91+
for (const match of matches) {
92+
if (match.length < 2) {
93+
continue;
94+
}
95+
// `this._regex` contains exactly 1 group that captures the path, so
96+
// match[1] will contain the path specified by a @file command.
97+
paths.push(match[1]);
98+
}
99+
100+
// add each specified file path as an attachment, unescaping ' ' characters
101+
// before doing so
102+
for (let path of paths) {
103+
path = path.replaceAll('\\ ', ' ');
104+
inputModel.addAttachment?.({
105+
type: 'file',
106+
value: path
107+
});
108+
}
109+
110+
// replace each @file command with the path in an inline Markdown code block
111+
// for readability, both to humans & to the AI.
112+
inputModel.value = inputModel.value.replaceAll(
113+
this._regex,
114+
(command, path) => `\`${path}\``
115+
);
116+
76117
return;
77118
}
78119

@@ -109,9 +150,15 @@ async function getPathCompletions(
109150
contentsManager: Contents.IManager,
110151
docRegistry: DocumentRegistry,
111152
searchPath: string
112-
) {
153+
): Promise<ChatCommand[]> {
154+
// get parent directory & the partial basename to be completed
113155
const [parentPath, basename] = getParentAndBase(searchPath);
114-
const parentDir = await contentsManager.get(parentPath);
156+
157+
// query the parent directory through the CM, un-escaping spaces beforehand
158+
const parentDir = await contentsManager.get(
159+
parentPath.replaceAll('\\ ', ' ')
160+
);
161+
115162
const commands: ChatCommand[] = [];
116163

117164
if (!Array.isArray(parentDir.content)) {
@@ -140,25 +187,29 @@ async function getPathCompletions(
140187
// get icon
141188
const { icon } = docRegistry.getFileTypeForModel(child);
142189

143-
// compute list of results, while handling directories and non-directories
144-
// appropriately.
145-
const isDirectory = child.type === 'directory';
190+
// calculate completion string, escaping any unescaped spaces
191+
let completion = '@file:' + parentPath + child.name;
192+
completion = completion.replaceAll(/(?<!\\) /g, '\\ ');
193+
194+
// add command completion to the list
146195
let newCommand: ChatCommand;
196+
const isDirectory = child.type === 'directory';
147197
if (isDirectory) {
148198
newCommand = {
149199
name: child.name + '/',
150200
providerId: CONTEXT_COMMANDS_PROVIDER_ID,
151201
icon,
152202
description: 'Search this directory',
153-
replaceWith: '@file:' + parentPath + child.name + '/'
203+
replaceWith: completion + '/'
154204
};
155205
} else {
156206
newCommand = {
157207
name: child.name,
158208
providerId: CONTEXT_COMMANDS_PROVIDER_ID,
159209
icon,
160210
description: 'Attach this file',
161-
replaceWith: '@file:' + parentPath + child.name + ' '
211+
replaceWith: completion,
212+
spaceOnAccept: true
162213
};
163214
}
164215
commands.push(newCommand);

packages/jupyter-ai/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"strict": true,
1919
"strictNullChecks": true,
2020
"skipLibCheck": true,
21-
"target": "ES2018",
21+
"target": "es2021",
2222
"types": ["jest"]
2323
},
2424
"include": ["src/**/*"]

yarn.lock

Lines changed: 7 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.12.0
2240+
"@jupyter/chat": ^0.15.0
22412241
"@jupyterlab/application": ^4.2.0
22422242
"@jupyterlab/apputils": ^4.2.0
22432243
"@jupyterlab/builder": ^4.2.0
@@ -2322,15 +2322,17 @@ __metadata:
23222322
languageName: node
23232323
linkType: hard
23242324

2325-
"@jupyter/chat@npm:^0.12.0":
2326-
version: 0.12.0
2327-
resolution: "@jupyter/chat@npm:0.12.0"
2325+
"@jupyter/chat@npm:^0.15.0":
2326+
version: 0.15.0
2327+
resolution: "@jupyter/chat@npm:0.15.0"
23282328
dependencies:
23292329
"@emotion/react": ^11.10.5
23302330
"@emotion/styled": ^11.10.5
23312331
"@jupyter/react-components": ^0.15.2
23322332
"@jupyterlab/application": ^4.2.0
23332333
"@jupyterlab/apputils": ^4.3.0
2334+
"@jupyterlab/codeeditor": ^4.2.0
2335+
"@jupyterlab/codemirror": ^4.2.0
23342336
"@jupyterlab/docmanager": ^4.2.0
23352337
"@jupyterlab/filebrowser": ^4.2.0
23362338
"@jupyterlab/fileeditor": ^4.2.0
@@ -2347,7 +2349,7 @@ __metadata:
23472349
clsx: ^2.1.0
23482350
react: ^18.2.0
23492351
react-dom: ^18.2.0
2350-
checksum: 4c36487123a5a176f2a865b7686b1f7d56930c3991d338be3a47286d7ebce5aac216ecd25e4f548e09e2592506a20e5756e489ed1a63ef93dec425b04295f9a9
2352+
checksum: 3255d164756073a889fd29019da66f6cff947a5edb22cb9f7ae9ee08a6265f58eefa127133177422ed17b2ecae6482b6c1f61c549688eff9997adcdee66bde31
23512353
languageName: node
23522354
linkType: hard
23532355

0 commit comments

Comments
 (0)