@jupyterlite/ai 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent.d.ts +61 -7
- package/lib/agent.js +286 -103
- package/lib/chat-commands/clear.d.ts +8 -0
- package/lib/chat-commands/clear.js +30 -0
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +2 -0
- package/lib/chat-commands/skills.d.ts +19 -0
- package/lib/chat-commands/skills.js +57 -0
- package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
- package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
- package/lib/chat-model.d.ts +16 -0
- package/lib/chat-model.js +191 -11
- package/lib/completion/completion-provider.d.ts +1 -1
- package/lib/completion/completion-provider.js +14 -2
- package/lib/components/model-select.js +4 -4
- package/lib/components/tool-select.d.ts +11 -2
- package/lib/components/tool-select.js +77 -18
- package/lib/index.d.ts +3 -3
- package/lib/index.js +311 -72
- package/lib/models/settings-model.d.ts +3 -0
- package/lib/models/settings-model.js +63 -14
- package/lib/providers/built-in-providers.js +12 -7
- package/lib/providers/provider-tools.d.ts +36 -0
- package/lib/providers/provider-tools.js +93 -0
- package/lib/rendered-message-outputarea.d.ts +24 -0
- package/lib/rendered-message-outputarea.js +48 -0
- package/lib/skills/index.d.ts +4 -0
- package/lib/skills/index.js +7 -0
- package/lib/skills/parse-skill.d.ts +25 -0
- package/lib/skills/parse-skill.js +69 -0
- package/lib/skills/skill-loader.d.ts +25 -0
- package/lib/skills/skill-loader.js +133 -0
- package/lib/skills/skill-registry.d.ts +31 -0
- package/lib/skills/skill-registry.js +100 -0
- package/lib/skills/types.d.ts +29 -0
- package/lib/skills/types.js +5 -0
- package/lib/tokens.d.ts +77 -7
- package/lib/tokens.js +6 -1
- package/lib/tools/commands.js +4 -2
- package/lib/tools/skills.d.ts +9 -0
- package/lib/tools/skills.js +73 -0
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +1 -1
- package/lib/widgets/ai-settings.js +157 -38
- package/lib/widgets/main-area-chat.d.ts +6 -0
- package/lib/widgets/main-area-chat.js +28 -0
- package/lib/widgets/provider-config-dialog.js +207 -4
- package/package.json +18 -11
- package/schema/settings-model.json +97 -2
- package/src/agent.ts +397 -123
- package/src/chat-commands/clear.ts +46 -0
- package/src/chat-commands/index.ts +2 -0
- package/src/chat-commands/skills.ts +87 -0
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +270 -23
- package/src/completion/completion-provider.ts +26 -12
- package/src/components/model-select.tsx +4 -5
- package/src/components/tool-select.tsx +110 -7
- package/src/index.ts +395 -87
- package/src/models/settings-model.ts +70 -15
- package/src/providers/built-in-providers.ts +12 -7
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/parse-skill.ts +91 -0
- package/src/skills/skill-loader.ts +175 -0
- package/src/skills/skill-registry.ts +137 -0
- package/src/skills/types.ts +37 -0
- package/src/tokens.ts +109 -9
- package/src/tools/commands.ts +4 -2
- package/src/tools/skills.ts +84 -0
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +357 -77
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +496 -3
package/src/index.ts
CHANGED
|
@@ -9,16 +9,21 @@ import {
|
|
|
9
9
|
ActiveCellManager,
|
|
10
10
|
AttachmentOpenerRegistry,
|
|
11
11
|
chatIcon,
|
|
12
|
+
ChatCommandRegistry,
|
|
12
13
|
ChatWidget,
|
|
13
14
|
IAttachmentOpenerRegistry,
|
|
15
|
+
IChatCommandRegistry,
|
|
16
|
+
IChatTracker,
|
|
14
17
|
IInputToolbarRegistryFactory,
|
|
15
18
|
InputToolbarRegistry,
|
|
16
|
-
MultiChatPanel
|
|
19
|
+
MultiChatPanel,
|
|
20
|
+
IChatModel
|
|
17
21
|
} from '@jupyter/chat';
|
|
18
22
|
|
|
19
23
|
import {
|
|
20
24
|
ICommandPalette,
|
|
21
25
|
IThemeManager,
|
|
26
|
+
showErrorMessage,
|
|
22
27
|
WidgetTracker
|
|
23
28
|
} from '@jupyterlab/apputils';
|
|
24
29
|
|
|
@@ -34,6 +39,8 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
|
34
39
|
|
|
35
40
|
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
36
41
|
|
|
42
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
43
|
+
|
|
37
44
|
import {
|
|
38
45
|
ITranslator,
|
|
39
46
|
nullTranslator,
|
|
@@ -49,25 +56,31 @@ import {
|
|
|
49
56
|
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
50
57
|
|
|
51
58
|
import { PromiseDelegate, UUID } from '@lumino/coreutils';
|
|
59
|
+
import { DisposableSet } from '@lumino/disposable';
|
|
52
60
|
|
|
53
61
|
import { AgentManagerFactory } from './agent';
|
|
54
62
|
|
|
55
63
|
import { AIChatModel } from './chat-model';
|
|
64
|
+
import { RenderedMessageOutputAreaCompat } from './rendered-message-outputarea';
|
|
65
|
+
|
|
66
|
+
import { ClearCommandProvider } from './chat-commands/clear';
|
|
67
|
+
import { SkillsCommandProvider } from './chat-commands/skills';
|
|
56
68
|
|
|
57
69
|
import { ProviderRegistry } from './providers/provider-registry';
|
|
58
70
|
|
|
59
71
|
import { ApprovalButtons } from './approval-buttons';
|
|
60
72
|
|
|
61
|
-
import {
|
|
73
|
+
import { ChatModelHandler } from './chat-model-handler';
|
|
62
74
|
|
|
63
75
|
import {
|
|
64
76
|
CommandIds,
|
|
65
77
|
IAgentManagerFactory,
|
|
66
78
|
IProviderRegistry,
|
|
67
79
|
IToolRegistry,
|
|
80
|
+
ISkillRegistry,
|
|
68
81
|
SECRETS_NAMESPACE,
|
|
69
82
|
IAISettingsModel,
|
|
70
|
-
|
|
83
|
+
IChatModelHandler,
|
|
71
84
|
IDiffManager
|
|
72
85
|
} from './tokens';
|
|
73
86
|
|
|
@@ -90,7 +103,9 @@ import {
|
|
|
90
103
|
TokenUsageWidget
|
|
91
104
|
} from './components';
|
|
92
105
|
|
|
93
|
-
import { AISettingsModel } from './models/settings-model';
|
|
106
|
+
import { AISettingsModel, IProviderConfig } from './models/settings-model';
|
|
107
|
+
|
|
108
|
+
import { loadSkillsFromPaths, SkillRegistry } from './skills';
|
|
94
109
|
|
|
95
110
|
import { DiffManager } from './diff-manager';
|
|
96
111
|
|
|
@@ -100,6 +115,8 @@ import {
|
|
|
100
115
|
createDiscoverCommandsTool,
|
|
101
116
|
createExecuteCommandTool
|
|
102
117
|
} from './tools/commands';
|
|
118
|
+
import { createDiscoverSkillsTool, createLoadSkillTool } from './tools/skills';
|
|
119
|
+
import { createBrowserFetchTool } from './tools/web';
|
|
103
120
|
|
|
104
121
|
import { AISettingsWidget } from './widgets/ai-settings';
|
|
105
122
|
|
|
@@ -184,30 +201,85 @@ const genericProviderPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
184
201
|
};
|
|
185
202
|
|
|
186
203
|
/**
|
|
187
|
-
*
|
|
204
|
+
* Chat command registry plugin.
|
|
205
|
+
*/
|
|
206
|
+
const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
|
|
207
|
+
id: '@jupyterlite/ai:chat-command-registry',
|
|
208
|
+
description: 'Provide the chat command registry for JupyterLite AI.',
|
|
209
|
+
autoStart: true,
|
|
210
|
+
provides: IChatCommandRegistry,
|
|
211
|
+
activate: () => {
|
|
212
|
+
return new ChatCommandRegistry();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clear chat command plugin.
|
|
218
|
+
*/
|
|
219
|
+
const clearCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
220
|
+
id: '@jupyterlite/ai:clear-command',
|
|
221
|
+
description: 'Register the /clear chat command.',
|
|
222
|
+
autoStart: true,
|
|
223
|
+
requires: [IChatCommandRegistry],
|
|
224
|
+
activate: (app, registry: IChatCommandRegistry) => {
|
|
225
|
+
registry.addProvider(new ClearCommandProvider());
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Skills chat command plugin.
|
|
231
|
+
*/
|
|
232
|
+
const skillsCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
233
|
+
id: '@jupyterlite/ai:skills-command',
|
|
234
|
+
description: 'Register the /skills chat command.',
|
|
235
|
+
autoStart: true,
|
|
236
|
+
requires: [IChatCommandRegistry, ISkillRegistry],
|
|
237
|
+
activate: (
|
|
238
|
+
app,
|
|
239
|
+
registry: IChatCommandRegistry,
|
|
240
|
+
skillRegistry: ISkillRegistry
|
|
241
|
+
) => {
|
|
242
|
+
registry.addProvider(
|
|
243
|
+
new SkillsCommandProvider({
|
|
244
|
+
skillRegistry,
|
|
245
|
+
commands: app.commands
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The chat model handler.
|
|
188
253
|
*/
|
|
189
|
-
const
|
|
190
|
-
id: '@jupyterlite/ai:chat-model-
|
|
191
|
-
description: '
|
|
254
|
+
const chatModelHandler: JupyterFrontEndPlugin<IChatModelHandler> = {
|
|
255
|
+
id: '@jupyterlite/ai:chat-model-handler',
|
|
256
|
+
description: 'A handler to create current chat model',
|
|
192
257
|
autoStart: true,
|
|
193
|
-
requires: [
|
|
258
|
+
requires: [
|
|
259
|
+
IAISettingsModel,
|
|
260
|
+
IAgentManagerFactory,
|
|
261
|
+
IDocumentManager,
|
|
262
|
+
IRenderMimeRegistry
|
|
263
|
+
],
|
|
194
264
|
optional: [IProviderRegistry, IToolRegistry, ITranslator],
|
|
195
|
-
provides:
|
|
265
|
+
provides: IChatModelHandler,
|
|
196
266
|
activate: (
|
|
197
267
|
app: JupyterFrontEnd,
|
|
198
268
|
settingsModel: AISettingsModel,
|
|
199
269
|
agentManagerFactory: AgentManagerFactory,
|
|
200
270
|
docManager: IDocumentManager,
|
|
271
|
+
rmRegistry: IRenderMimeRegistry,
|
|
201
272
|
providerRegistry?: IProviderRegistry,
|
|
202
273
|
toolRegistry?: IToolRegistry,
|
|
203
274
|
translator?: ITranslator
|
|
204
|
-
):
|
|
275
|
+
): IChatModelHandler => {
|
|
205
276
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
206
277
|
|
|
207
|
-
return new
|
|
278
|
+
return new ChatModelHandler({
|
|
208
279
|
settingsModel,
|
|
209
280
|
agentManagerFactory,
|
|
210
281
|
docManager,
|
|
282
|
+
rmRegistry,
|
|
211
283
|
providerRegistry,
|
|
212
284
|
toolRegistry,
|
|
213
285
|
trans
|
|
@@ -218,15 +290,17 @@ const chatModelRegistry: JupyterFrontEndPlugin<IChatModelRegistry> = {
|
|
|
218
290
|
/**
|
|
219
291
|
* Initialization data for the extension.
|
|
220
292
|
*/
|
|
221
|
-
const plugin: JupyterFrontEndPlugin<
|
|
293
|
+
const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
222
294
|
id: '@jupyterlite/ai:plugin',
|
|
223
295
|
description: 'AI in JupyterLab',
|
|
224
296
|
autoStart: true,
|
|
297
|
+
provides: IChatTracker,
|
|
225
298
|
requires: [
|
|
226
299
|
IRenderMimeRegistry,
|
|
227
300
|
IInputToolbarRegistryFactory,
|
|
228
|
-
|
|
229
|
-
IAISettingsModel
|
|
301
|
+
IChatModelHandler,
|
|
302
|
+
IAISettingsModel,
|
|
303
|
+
IChatCommandRegistry
|
|
230
304
|
],
|
|
231
305
|
optional: [
|
|
232
306
|
IThemeManager,
|
|
@@ -239,14 +313,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
239
313
|
app: JupyterFrontEnd,
|
|
240
314
|
rmRegistry: IRenderMimeRegistry,
|
|
241
315
|
inputToolbarFactory: IInputToolbarRegistryFactory,
|
|
242
|
-
|
|
316
|
+
modelHandler: IChatModelHandler,
|
|
243
317
|
settingsModel: AISettingsModel,
|
|
318
|
+
chatCommandRegistry: IChatCommandRegistry,
|
|
244
319
|
themeManager?: IThemeManager,
|
|
245
320
|
restorer?: ILayoutRestorer,
|
|
246
321
|
labShell?: ILabShell,
|
|
247
322
|
notebookTracker?: INotebookTracker,
|
|
248
323
|
translator?: ITranslator
|
|
249
|
-
):
|
|
324
|
+
): IChatTracker => {
|
|
250
325
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
251
326
|
// Create attachment opener registry to handle file attachments
|
|
252
327
|
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
|
|
@@ -267,7 +342,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
267
342
|
shell: app.shell
|
|
268
343
|
});
|
|
269
344
|
}
|
|
270
|
-
|
|
345
|
+
modelHandler.activeCellManager = activeCellManager;
|
|
346
|
+
|
|
347
|
+
// Creating the tracker for the chat widgets
|
|
348
|
+
const namespace = 'ai-chat';
|
|
349
|
+
const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
|
|
271
350
|
|
|
272
351
|
// Create chat panel with drag/drop functionality
|
|
273
352
|
const chatPanel = new MultiChatPanel({
|
|
@@ -275,19 +354,36 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
275
354
|
themeManager: themeManager ?? null,
|
|
276
355
|
inputToolbarFactory,
|
|
277
356
|
attachmentOpenerRegistry,
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
chatCommandRegistry,
|
|
358
|
+
createModel: async (provider?: string) => {
|
|
359
|
+
if (!provider) {
|
|
360
|
+
provider = settingsModel.getDefaultProvider()?.id;
|
|
361
|
+
if (!provider) {
|
|
362
|
+
showErrorMessage('Error creating chat', 'Please set up a provider');
|
|
363
|
+
app.commands.execute('@jupyterlite/ai:open-settings');
|
|
364
|
+
return {};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
let name = settingsModel.getProvider(provider)?.name ?? UUID.uuid4();
|
|
368
|
+
const modelName = name;
|
|
369
|
+
const existingName = new Set(chatPanel.getLoadedModelNames());
|
|
370
|
+
tracker.forEach(widget => existingName.add(widget.model.name));
|
|
371
|
+
let i = 1;
|
|
372
|
+
while (existingName.has(name)) {
|
|
373
|
+
name = `${modelName}-${i}`;
|
|
374
|
+
i += 1;
|
|
375
|
+
}
|
|
376
|
+
const model = modelHandler.createModel(name, provider);
|
|
280
377
|
return { model };
|
|
281
378
|
},
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
379
|
+
getChatNames: async () => {
|
|
380
|
+
const names: { [name: string]: string } = {};
|
|
381
|
+
settingsModel.config.providers.forEach(provider => {
|
|
382
|
+
names[provider.name] = provider.id;
|
|
383
|
+
});
|
|
384
|
+
return names;
|
|
290
385
|
},
|
|
386
|
+
renameChat: true,
|
|
291
387
|
openInMain: (name: string) =>
|
|
292
388
|
app.commands.execute(CommandIds.moveChat, {
|
|
293
389
|
area: 'main',
|
|
@@ -311,28 +407,40 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
311
407
|
})
|
|
312
408
|
);
|
|
313
409
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const model =
|
|
410
|
+
let tokenUsageWidget: TokenUsageWidget | null = null;
|
|
411
|
+
chatPanel.chatOpened.connect((_, widget) => {
|
|
412
|
+
const model = widget.model as AIChatModel;
|
|
317
413
|
|
|
318
414
|
// Add the widget to the tracker.
|
|
319
415
|
tracker.add(widget);
|
|
320
416
|
|
|
417
|
+
function saveTracker() {
|
|
418
|
+
tracker.save(widget);
|
|
419
|
+
}
|
|
420
|
+
|
|
321
421
|
// Update the tracker if the model name changed.
|
|
322
|
-
model.nameChanged.connect(
|
|
422
|
+
model.nameChanged.connect(saveTracker);
|
|
423
|
+
|
|
323
424
|
// Update the tracker if the active provider changed.
|
|
324
|
-
model.agentManager.activeProviderChanged.connect(
|
|
325
|
-
|
|
326
|
-
|
|
425
|
+
model.agentManager.activeProviderChanged.connect(saveTracker);
|
|
426
|
+
|
|
427
|
+
// Update the token usage widget.
|
|
428
|
+
tokenUsageWidget?.dispose();
|
|
327
429
|
|
|
328
|
-
|
|
430
|
+
tokenUsageWidget = new TokenUsageWidget({
|
|
329
431
|
tokenUsageChanged: model.tokenUsageChanged,
|
|
330
432
|
settingsModel,
|
|
331
433
|
initialTokenUsage: model.agentManager.tokenUsage,
|
|
332
434
|
translator: trans
|
|
333
435
|
});
|
|
334
|
-
|
|
335
|
-
|
|
436
|
+
chatPanel.current?.toolbar.insertBefore(
|
|
437
|
+
'markRead',
|
|
438
|
+
'token-usage',
|
|
439
|
+
tokenUsageWidget
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Listen for writers change to display the stop button.
|
|
443
|
+
function writersChanged(_: IChatModel, writers: IChatModel.IWriter[]) {
|
|
336
444
|
// Check if AI is currently writing (streaming)
|
|
337
445
|
const aiWriting = writers.some(
|
|
338
446
|
writer => writer.user.username === 'ai-assistant'
|
|
@@ -345,28 +453,34 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
345
453
|
widget.inputToolbarRegistry?.hide('stop');
|
|
346
454
|
widget.inputToolbarRegistry?.show('send');
|
|
347
455
|
}
|
|
348
|
-
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
model.writersChanged?.connect(writersChanged);
|
|
349
459
|
|
|
350
460
|
// Associate an approval buttons object to the chat.
|
|
351
461
|
const approvalButton = new ApprovalButtons({
|
|
352
462
|
chatPanel: widget,
|
|
353
463
|
agentManager: model.agentManager
|
|
354
464
|
});
|
|
465
|
+
// Temporary compat: keep output-area CSS context for MIME renderers
|
|
466
|
+
// until jupyter-chat provides it natively.
|
|
467
|
+
const outputAreaCompat = new RenderedMessageOutputAreaCompat({
|
|
468
|
+
chatPanel: widget
|
|
469
|
+
});
|
|
355
470
|
|
|
356
471
|
widget.disposed.connect(() => {
|
|
472
|
+
model.nameChanged.disconnect(saveTracker);
|
|
473
|
+
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
474
|
+
model.writersChanged?.disconnect(writersChanged);
|
|
475
|
+
|
|
357
476
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
358
477
|
approvalButton.dispose();
|
|
359
|
-
|
|
360
|
-
modelRegistry.remove(model.name);
|
|
478
|
+
outputAreaCompat.dispose();
|
|
361
479
|
});
|
|
362
480
|
});
|
|
363
481
|
|
|
364
482
|
app.shell.add(chatPanel, 'left', { rank: 1000 });
|
|
365
483
|
|
|
366
|
-
// Creating the tracker for the document
|
|
367
|
-
const namespace = 'ai-chat';
|
|
368
|
-
const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
|
|
369
|
-
|
|
370
484
|
if (restorer) {
|
|
371
485
|
restorer.add(chatPanel, chatPanel.id);
|
|
372
486
|
void restorer.restore(tracker, {
|
|
@@ -385,10 +499,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
385
499
|
|
|
386
500
|
// Create a chat with default provider at startup.
|
|
387
501
|
app.restored.then(() => {
|
|
388
|
-
if (
|
|
389
|
-
!modelRegistry.getAll().length &&
|
|
390
|
-
settingsModel.config.defaultProvider
|
|
391
|
-
) {
|
|
502
|
+
if (!tracker.size && settingsModel.config.defaultProvider) {
|
|
392
503
|
app.commands.execute(CommandIds.openChat);
|
|
393
504
|
}
|
|
394
505
|
});
|
|
@@ -400,12 +511,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
400
511
|
attachmentOpenerRegistry,
|
|
401
512
|
inputToolbarFactory,
|
|
402
513
|
settingsModel,
|
|
514
|
+
chatCommandRegistry,
|
|
403
515
|
tracker,
|
|
404
|
-
|
|
516
|
+
modelHandler,
|
|
405
517
|
trans,
|
|
406
518
|
themeManager,
|
|
407
519
|
labShell
|
|
408
520
|
);
|
|
521
|
+
|
|
522
|
+
return tracker;
|
|
409
523
|
}
|
|
410
524
|
};
|
|
411
525
|
|
|
@@ -416,8 +530,9 @@ function registerCommands(
|
|
|
416
530
|
attachmentOpenerRegistry: IAttachmentOpenerRegistry,
|
|
417
531
|
inputToolbarFactory: IInputToolbarRegistryFactory,
|
|
418
532
|
settingsModel: AISettingsModel,
|
|
533
|
+
chatCommandRegistry: IChatCommandRegistry,
|
|
419
534
|
tracker: WidgetTracker<MainAreaChat | ChatWidget>,
|
|
420
|
-
modelRegistry:
|
|
535
|
+
modelRegistry: IChatModelHandler,
|
|
421
536
|
trans: TranslationBundle,
|
|
422
537
|
themeManager?: IThemeManager,
|
|
423
538
|
labShell?: ILabShell
|
|
@@ -481,7 +596,8 @@ function registerCommands(
|
|
|
481
596
|
rmRegistry,
|
|
482
597
|
themeManager: themeManager ?? null,
|
|
483
598
|
inputToolbarRegistry: inputToolbarFactory.create(),
|
|
484
|
-
attachmentOpenerRegistry
|
|
599
|
+
attachmentOpenerRegistry,
|
|
600
|
+
chatCommandRegistry
|
|
485
601
|
});
|
|
486
602
|
const widget = new MainAreaChat({
|
|
487
603
|
content,
|
|
@@ -494,16 +610,18 @@ function registerCommands(
|
|
|
494
610
|
// Add the widget to the tracker.
|
|
495
611
|
tracker.add(widget);
|
|
496
612
|
|
|
613
|
+
function saveTracker() {
|
|
614
|
+
tracker.save(widget);
|
|
615
|
+
}
|
|
616
|
+
|
|
497
617
|
// Update the tracker if the model name changed.
|
|
498
|
-
model.nameChanged.connect(
|
|
618
|
+
model.nameChanged.connect(saveTracker);
|
|
499
619
|
// Update the tracker if the active provider changed.
|
|
500
|
-
model.agentManager.activeProviderChanged.connect(
|
|
501
|
-
tracker.save(widget)
|
|
502
|
-
);
|
|
620
|
+
model.agentManager.activeProviderChanged.connect(saveTracker);
|
|
503
621
|
|
|
504
|
-
// Remove the model from the registry when the widget is disposed.
|
|
505
622
|
widget.disposed.connect(() => {
|
|
506
|
-
|
|
623
|
+
model.nameChanged.disconnect(saveTracker);
|
|
624
|
+
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
507
625
|
});
|
|
508
626
|
};
|
|
509
627
|
|
|
@@ -512,15 +630,26 @@ function registerCommands(
|
|
|
512
630
|
execute: async (args): Promise<boolean> => {
|
|
513
631
|
const area = (args.area as string) === 'main' ? 'main' : 'side';
|
|
514
632
|
const provider = (args.provider as string) ?? undefined;
|
|
633
|
+
let name = (args.name as string) ?? undefined;
|
|
515
634
|
|
|
516
|
-
|
|
517
|
-
if (provider
|
|
635
|
+
let providerConfig: IProviderConfig | undefined = undefined;
|
|
636
|
+
if (provider) {
|
|
637
|
+
providerConfig = settingsModel.getProvider(provider);
|
|
638
|
+
} else {
|
|
639
|
+
providerConfig = settingsModel.getDefaultProvider();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Do not open the chat if the provider in args does not exists in settings or
|
|
643
|
+
// if there is no default provider.
|
|
644
|
+
if (!providerConfig) {
|
|
518
645
|
return false;
|
|
519
646
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
647
|
+
|
|
648
|
+
if (!name) {
|
|
649
|
+
name = providerConfig.name;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const model = modelRegistry.createModel(name, provider);
|
|
524
653
|
if (!model) {
|
|
525
654
|
return false;
|
|
526
655
|
}
|
|
@@ -528,7 +657,7 @@ function registerCommands(
|
|
|
528
657
|
if (area === 'main') {
|
|
529
658
|
openInMain(model);
|
|
530
659
|
} else {
|
|
531
|
-
chatPanel.
|
|
660
|
+
chatPanel.open({ model });
|
|
532
661
|
}
|
|
533
662
|
return true;
|
|
534
663
|
},
|
|
@@ -570,7 +699,15 @@ function registerCommands(
|
|
|
570
699
|
);
|
|
571
700
|
return false;
|
|
572
701
|
}
|
|
573
|
-
|
|
702
|
+
let previousWidget: ChatWidget | MainAreaChat | undefined;
|
|
703
|
+
let previousModel: AIChatModel | undefined;
|
|
704
|
+
tracker.forEach(widget => {
|
|
705
|
+
if (widget.model.name === args.name) {
|
|
706
|
+
previousWidget = widget;
|
|
707
|
+
previousModel = widget.model as AIChatModel;
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
574
711
|
if (!previousModel) {
|
|
575
712
|
console.error(
|
|
576
713
|
'Error while moving the chat to main area: there is no reference model'
|
|
@@ -596,12 +733,12 @@ function registerCommands(
|
|
|
596
733
|
// Create a new model by duplicating the previous model attributes.
|
|
597
734
|
const model = modelRegistry.createModel(
|
|
598
735
|
args.name as string,
|
|
599
|
-
previousModel
|
|
600
|
-
previousModel
|
|
601
|
-
);
|
|
602
|
-
previousModel?.messages.forEach(message =>
|
|
603
|
-
model?.messageAdded(message)
|
|
736
|
+
previousModel.agentManager.activeProvider,
|
|
737
|
+
previousModel.agentManager.tokenUsage
|
|
604
738
|
);
|
|
739
|
+
previousModel?.messages.forEach(message => {
|
|
740
|
+
model?.messageAdded({ ...message.content });
|
|
741
|
+
});
|
|
605
742
|
|
|
606
743
|
// Wait (with timeout) for the tracker to have updated the previous widget.
|
|
607
744
|
const status = await Promise.any([
|
|
@@ -621,15 +758,9 @@ function registerCommands(
|
|
|
621
758
|
if (area === 'main') {
|
|
622
759
|
openInMain(model);
|
|
623
760
|
} else {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
current instanceof MainAreaChat &&
|
|
628
|
-
current.model.name === previousModel.name
|
|
629
|
-
) {
|
|
630
|
-
current.dispose();
|
|
631
|
-
}
|
|
632
|
-
chatPanel.addChat({ model });
|
|
761
|
+
previousWidget?.dispose();
|
|
762
|
+
previousModel.dispose();
|
|
763
|
+
chatPanel.open({ model });
|
|
633
764
|
}
|
|
634
765
|
|
|
635
766
|
return true;
|
|
@@ -641,7 +772,7 @@ function registerCommands(
|
|
|
641
772
|
area: {
|
|
642
773
|
type: 'string',
|
|
643
774
|
enum: ['main', 'side'],
|
|
644
|
-
description: trans.__('The
|
|
775
|
+
description: trans.__('The area to move the chat to')
|
|
645
776
|
},
|
|
646
777
|
name: {
|
|
647
778
|
type: 'string',
|
|
@@ -656,7 +787,9 @@ function registerCommands(
|
|
|
656
787
|
}
|
|
657
788
|
|
|
658
789
|
/**
|
|
659
|
-
* A plugin to provide the
|
|
790
|
+
* A plugin to provide the agent manager factory, the completion provider and
|
|
791
|
+
* the settings model.
|
|
792
|
+
* All these objects require the secrets manager token with the same namespace.
|
|
660
793
|
*/
|
|
661
794
|
const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
662
795
|
SecretsManager.sign(SECRETS_NAMESPACE, token => ({
|
|
@@ -666,6 +799,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
|
666
799
|
provides: IAgentManagerFactory,
|
|
667
800
|
requires: [IAISettingsModel, IProviderRegistry],
|
|
668
801
|
optional: [
|
|
802
|
+
ISkillRegistry,
|
|
669
803
|
ICommandPalette,
|
|
670
804
|
ICompletionProviderManager,
|
|
671
805
|
ILayoutRestorer,
|
|
@@ -677,7 +811,8 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
|
677
811
|
app: JupyterFrontEnd,
|
|
678
812
|
settingsModel: AISettingsModel,
|
|
679
813
|
providerRegistry: IProviderRegistry,
|
|
680
|
-
|
|
814
|
+
skillRegistry?: ISkillRegistry,
|
|
815
|
+
palette?: ICommandPalette,
|
|
681
816
|
completionManager?: ICompletionProviderManager,
|
|
682
817
|
restorer?: ILayoutRestorer,
|
|
683
818
|
secretsManager?: ISecretsManager,
|
|
@@ -687,6 +822,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
|
687
822
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
688
823
|
const agentManagerFactory = new AgentManagerFactory({
|
|
689
824
|
settingsModel,
|
|
825
|
+
skillRegistry,
|
|
690
826
|
secretsManager,
|
|
691
827
|
token
|
|
692
828
|
});
|
|
@@ -797,13 +933,31 @@ const diffManager: JupyterFrontEndPlugin<IDiffManager> = {
|
|
|
797
933
|
}
|
|
798
934
|
};
|
|
799
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Skill registry plugin
|
|
938
|
+
*/
|
|
939
|
+
const skillRegistryPlugin: JupyterFrontEndPlugin<ISkillRegistry> = {
|
|
940
|
+
id: '@jupyterlite/ai:skill-registry',
|
|
941
|
+
description: 'Provide the skill registry',
|
|
942
|
+
autoStart: true,
|
|
943
|
+
provides: ISkillRegistry,
|
|
944
|
+
activate: () => {
|
|
945
|
+
return new SkillRegistry();
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
800
949
|
const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
|
|
801
950
|
id: '@jupyterlite/ai:tool-registry',
|
|
802
951
|
description: 'Provide the AI tool registry',
|
|
803
952
|
autoStart: true,
|
|
804
953
|
requires: [IAISettingsModel],
|
|
954
|
+
optional: [ISkillRegistry],
|
|
805
955
|
provides: IToolRegistry,
|
|
806
|
-
activate: (
|
|
956
|
+
activate: (
|
|
957
|
+
app: JupyterFrontEnd,
|
|
958
|
+
settingsModel: AISettingsModel,
|
|
959
|
+
skillRegistry?: ISkillRegistry
|
|
960
|
+
) => {
|
|
807
961
|
const toolRegistry = new ToolRegistry();
|
|
808
962
|
|
|
809
963
|
// Add command operation tools
|
|
@@ -815,6 +969,14 @@ const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
|
|
|
815
969
|
|
|
816
970
|
toolRegistry.add('discover_commands', discoverCommandsTool);
|
|
817
971
|
toolRegistry.add('execute_command', executeCommandTool);
|
|
972
|
+
toolRegistry.add('browser_fetch', createBrowserFetchTool());
|
|
973
|
+
if (skillRegistry) {
|
|
974
|
+
toolRegistry.add(
|
|
975
|
+
'discover_skills',
|
|
976
|
+
createDiscoverSkillsTool(skillRegistry)
|
|
977
|
+
);
|
|
978
|
+
toolRegistry.add('load_skill', createLoadSkillTool(skillRegistry));
|
|
979
|
+
}
|
|
818
980
|
|
|
819
981
|
return toolRegistry;
|
|
820
982
|
}
|
|
@@ -829,12 +991,13 @@ const inputToolbarFactory: JupyterFrontEndPlugin<IInputToolbarRegistryFactory> =
|
|
|
829
991
|
description: 'The input toolbar registry plugin.',
|
|
830
992
|
autoStart: true,
|
|
831
993
|
provides: IInputToolbarRegistryFactory,
|
|
832
|
-
requires: [IAISettingsModel, IToolRegistry],
|
|
994
|
+
requires: [IAISettingsModel, IToolRegistry, IProviderRegistry],
|
|
833
995
|
optional: [ITranslator],
|
|
834
996
|
activate: (
|
|
835
997
|
app: JupyterFrontEnd,
|
|
836
998
|
settingsModel: AISettingsModel,
|
|
837
999
|
toolRegistry: IToolRegistry,
|
|
1000
|
+
providerRegistry: IProviderRegistry,
|
|
838
1001
|
translator?: ITranslator
|
|
839
1002
|
): IInputToolbarRegistryFactory => {
|
|
840
1003
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
@@ -842,6 +1005,8 @@ const inputToolbarFactory: JupyterFrontEndPlugin<IInputToolbarRegistryFactory> =
|
|
|
842
1005
|
const clearButton = clearItem(trans);
|
|
843
1006
|
const toolSelectButton = createToolSelectItem(
|
|
844
1007
|
toolRegistry,
|
|
1008
|
+
settingsModel,
|
|
1009
|
+
providerRegistry,
|
|
845
1010
|
settingsModel.config.toolsEnabled,
|
|
846
1011
|
trans
|
|
847
1012
|
);
|
|
@@ -900,6 +1065,144 @@ const completionStatus: JupyterFrontEndPlugin<void> = {
|
|
|
900
1065
|
}
|
|
901
1066
|
};
|
|
902
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Skills plugin: discovers and registers agent skills from the filesystem.
|
|
1070
|
+
*/
|
|
1071
|
+
const skillsPlugin: JupyterFrontEndPlugin<void> = {
|
|
1072
|
+
id: '@jupyterlite/ai:skills',
|
|
1073
|
+
description: 'Discover and register agent skills',
|
|
1074
|
+
autoStart: true,
|
|
1075
|
+
requires: [IAISettingsModel, IDocumentManager, ISkillRegistry],
|
|
1076
|
+
optional: [ICommandPalette, ITranslator],
|
|
1077
|
+
activate: async (
|
|
1078
|
+
app: JupyterFrontEnd,
|
|
1079
|
+
settingsModel: AISettingsModel,
|
|
1080
|
+
docManager: IDocumentManager,
|
|
1081
|
+
skillRegistry: ISkillRegistry,
|
|
1082
|
+
palette?: ICommandPalette,
|
|
1083
|
+
translator?: ITranslator
|
|
1084
|
+
) => {
|
|
1085
|
+
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
1086
|
+
const validateResourcePath = (resourcePath: string): string | null => {
|
|
1087
|
+
if (resourcePath.startsWith('/')) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const normalized = PathExt.normalize(resourcePath);
|
|
1092
|
+
if (normalized.startsWith('..') || normalized === '') {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return normalized;
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
let currentSkillsPaths = settingsModel.config.skillsPaths;
|
|
1100
|
+
let currentSkillDisposables = new DisposableSet();
|
|
1101
|
+
|
|
1102
|
+
const loadAndRegister = async () => {
|
|
1103
|
+
const skillsPaths = settingsModel.config.skillsPaths;
|
|
1104
|
+
const skills = await loadSkillsFromPaths(
|
|
1105
|
+
docManager.services.contents,
|
|
1106
|
+
skillsPaths
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
const registrations = skills.map(skill => ({
|
|
1110
|
+
name: skill.name,
|
|
1111
|
+
description: skill.description,
|
|
1112
|
+
instructions: skill.instructions,
|
|
1113
|
+
resources: skill.resources,
|
|
1114
|
+
loadResource: async (resource: string) => {
|
|
1115
|
+
const validatedPath = validateResourcePath(resource);
|
|
1116
|
+
if (validatedPath === null) {
|
|
1117
|
+
return {
|
|
1118
|
+
name: skill.name,
|
|
1119
|
+
resource,
|
|
1120
|
+
error: 'Invalid resource path: path traversal not allowed'
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (!skill.resources.includes(validatedPath)) {
|
|
1125
|
+
return {
|
|
1126
|
+
name: skill.name,
|
|
1127
|
+
resource,
|
|
1128
|
+
error: `Resource not found: ${resource}`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const resourcePath = `${skill.path}/${validatedPath}`;
|
|
1133
|
+
try {
|
|
1134
|
+
const fileModel = await docManager.services.contents.get(
|
|
1135
|
+
resourcePath,
|
|
1136
|
+
{
|
|
1137
|
+
content: true
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
if (typeof fileModel.content !== 'string') {
|
|
1141
|
+
return {
|
|
1142
|
+
name: skill.name,
|
|
1143
|
+
resource,
|
|
1144
|
+
error: 'Resource content is not a string'
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
name: skill.name,
|
|
1149
|
+
resource,
|
|
1150
|
+
content: fileModel.content
|
|
1151
|
+
};
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
return {
|
|
1154
|
+
name: skill.name,
|
|
1155
|
+
resource,
|
|
1156
|
+
error: `Failed to read resource: ${error}`
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}));
|
|
1161
|
+
|
|
1162
|
+
currentSkillDisposables.dispose();
|
|
1163
|
+
currentSkillDisposables = new DisposableSet();
|
|
1164
|
+
for (const registration of registrations) {
|
|
1165
|
+
currentSkillDisposables.add(skillRegistry.registerSkill(registration));
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
app.commands.addCommand(CommandIds.refreshSkills, {
|
|
1170
|
+
label: trans.__('Refresh Agents Skills'),
|
|
1171
|
+
caption: trans.__(
|
|
1172
|
+
'Re-scan the agents skills directory and update the registry'
|
|
1173
|
+
),
|
|
1174
|
+
execute: async () => {
|
|
1175
|
+
await loadAndRegister();
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
if (palette) {
|
|
1180
|
+
palette.addItem({
|
|
1181
|
+
command: CommandIds.refreshSkills,
|
|
1182
|
+
category: trans.__('AI Assistant')
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
loadAndRegister().catch(error =>
|
|
1187
|
+
console.warn('Failed to load skills on activation:', error)
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
settingsModel.stateChanged.connect(() => {
|
|
1191
|
+
const newPaths = settingsModel.config.skillsPaths;
|
|
1192
|
+
if (
|
|
1193
|
+
newPaths.length === currentSkillsPaths.length &&
|
|
1194
|
+
newPaths.every((p, i) => p === currentSkillsPaths[i])
|
|
1195
|
+
) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
currentSkillsPaths = newPaths;
|
|
1199
|
+
loadAndRegister().catch(error =>
|
|
1200
|
+
console.warn('Failed to reload skills:', error)
|
|
1201
|
+
);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
903
1206
|
export default [
|
|
904
1207
|
providerRegistryPlugin,
|
|
905
1208
|
anthropicProviderPlugin,
|
|
@@ -909,12 +1212,17 @@ export default [
|
|
|
909
1212
|
genericProviderPlugin,
|
|
910
1213
|
settingsModel,
|
|
911
1214
|
diffManager,
|
|
912
|
-
|
|
1215
|
+
chatCommandRegistryPlugin,
|
|
1216
|
+
clearCommandPlugin,
|
|
1217
|
+
skillRegistryPlugin,
|
|
1218
|
+
skillsCommandPlugin,
|
|
1219
|
+
chatModelHandler,
|
|
913
1220
|
plugin,
|
|
914
1221
|
toolRegistry,
|
|
915
1222
|
agentManagerFactory,
|
|
916
1223
|
inputToolbarFactory,
|
|
917
|
-
completionStatus
|
|
1224
|
+
completionStatus,
|
|
1225
|
+
skillsPlugin
|
|
918
1226
|
];
|
|
919
1227
|
|
|
920
1228
|
// Export extension points for other extensions to use
|