@jupyterlite/ai 0.12.0 → 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 +24 -2
- package/lib/agent.js +161 -24
- 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 +8 -0
- package/lib/chat-model.js +156 -8
- 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 +128 -66
- package/lib/models/settings-model.d.ts +2 -0
- package/lib/models/settings-model.js +2 -0
- package/lib/providers/built-in-providers.js +7 -0
- 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/tokens.d.ts +44 -7
- package/lib/tokens.js +1 -1
- package/lib/tools/commands.js +4 -2
- 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 +125 -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 +10 -4
- package/schema/settings-model.json +89 -1
- package/src/agent.ts +220 -42
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +223 -14
- 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 +153 -82
- package/src/models/settings-model.ts +6 -0
- package/src/providers/built-in-providers.ts +7 -0
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/tokens.ts +53 -9
- package/src/tools/commands.ts +4 -2
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +282 -77
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +496 -3
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
|
|
2
|
-
import { ActiveCellManager, AttachmentOpenerRegistry, chatIcon, ChatCommandRegistry, ChatWidget, IChatCommandRegistry, IInputToolbarRegistryFactory, InputToolbarRegistry, MultiChatPanel } from '@jupyter/chat';
|
|
3
|
-
import { ICommandPalette, IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
|
|
2
|
+
import { ActiveCellManager, AttachmentOpenerRegistry, chatIcon, ChatCommandRegistry, ChatWidget, IChatCommandRegistry, IChatTracker, IInputToolbarRegistryFactory, InputToolbarRegistry, MultiChatPanel } from '@jupyter/chat';
|
|
3
|
+
import { ICommandPalette, IThemeManager, showErrorMessage, WidgetTracker } from '@jupyterlab/apputils';
|
|
4
4
|
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
5
5
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
6
6
|
import { INotebookTracker } from '@jupyterlab/notebook';
|
|
@@ -14,12 +14,13 @@ import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
|
14
14
|
import { PromiseDelegate, UUID } from '@lumino/coreutils';
|
|
15
15
|
import { DisposableSet } from '@lumino/disposable';
|
|
16
16
|
import { AgentManagerFactory } from './agent';
|
|
17
|
+
import { RenderedMessageOutputAreaCompat } from './rendered-message-outputarea';
|
|
17
18
|
import { ClearCommandProvider } from './chat-commands/clear';
|
|
18
19
|
import { SkillsCommandProvider } from './chat-commands/skills';
|
|
19
20
|
import { ProviderRegistry } from './providers/provider-registry';
|
|
20
21
|
import { ApprovalButtons } from './approval-buttons';
|
|
21
|
-
import {
|
|
22
|
-
import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel,
|
|
22
|
+
import { ChatModelHandler } from './chat-model-handler';
|
|
23
|
+
import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelHandler, IDiffManager } from './tokens';
|
|
23
24
|
import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
|
|
24
25
|
import { AICompletionProvider } from './completion';
|
|
25
26
|
import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, TokenUsageWidget } from './components';
|
|
@@ -29,6 +30,7 @@ import { DiffManager } from './diff-manager';
|
|
|
29
30
|
import { ToolRegistry } from './tools/tool-registry';
|
|
30
31
|
import { createDiscoverCommandsTool, createExecuteCommandTool } from './tools/commands';
|
|
31
32
|
import { createDiscoverSkillsTool, createLoadSkillTool } from './tools/skills';
|
|
33
|
+
import { createBrowserFetchTool } from './tools/web';
|
|
32
34
|
import { AISettingsWidget } from './widgets/ai-settings';
|
|
33
35
|
import { MainAreaChat } from './widgets/main-area-chat';
|
|
34
36
|
/**
|
|
@@ -143,21 +145,27 @@ const skillsCommandPlugin = {
|
|
|
143
145
|
}
|
|
144
146
|
};
|
|
145
147
|
/**
|
|
146
|
-
* The chat model
|
|
148
|
+
* The chat model handler.
|
|
147
149
|
*/
|
|
148
|
-
const
|
|
149
|
-
id: '@jupyterlite/ai:chat-model-
|
|
150
|
-
description: '
|
|
150
|
+
const chatModelHandler = {
|
|
151
|
+
id: '@jupyterlite/ai:chat-model-handler',
|
|
152
|
+
description: 'A handler to create current chat model',
|
|
151
153
|
autoStart: true,
|
|
152
|
-
requires: [
|
|
154
|
+
requires: [
|
|
155
|
+
IAISettingsModel,
|
|
156
|
+
IAgentManagerFactory,
|
|
157
|
+
IDocumentManager,
|
|
158
|
+
IRenderMimeRegistry
|
|
159
|
+
],
|
|
153
160
|
optional: [IProviderRegistry, IToolRegistry, ITranslator],
|
|
154
|
-
provides:
|
|
155
|
-
activate: (app, settingsModel, agentManagerFactory, docManager, providerRegistry, toolRegistry, translator) => {
|
|
161
|
+
provides: IChatModelHandler,
|
|
162
|
+
activate: (app, settingsModel, agentManagerFactory, docManager, rmRegistry, providerRegistry, toolRegistry, translator) => {
|
|
156
163
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
157
|
-
return new
|
|
164
|
+
return new ChatModelHandler({
|
|
158
165
|
settingsModel,
|
|
159
166
|
agentManagerFactory,
|
|
160
167
|
docManager,
|
|
168
|
+
rmRegistry,
|
|
161
169
|
providerRegistry,
|
|
162
170
|
toolRegistry,
|
|
163
171
|
trans
|
|
@@ -171,10 +179,11 @@ const plugin = {
|
|
|
171
179
|
id: '@jupyterlite/ai:plugin',
|
|
172
180
|
description: 'AI in JupyterLab',
|
|
173
181
|
autoStart: true,
|
|
182
|
+
provides: IChatTracker,
|
|
174
183
|
requires: [
|
|
175
184
|
IRenderMimeRegistry,
|
|
176
185
|
IInputToolbarRegistryFactory,
|
|
177
|
-
|
|
186
|
+
IChatModelHandler,
|
|
178
187
|
IAISettingsModel,
|
|
179
188
|
IChatCommandRegistry
|
|
180
189
|
],
|
|
@@ -185,7 +194,7 @@ const plugin = {
|
|
|
185
194
|
INotebookTracker,
|
|
186
195
|
ITranslator
|
|
187
196
|
],
|
|
188
|
-
activate: (app, rmRegistry, inputToolbarFactory,
|
|
197
|
+
activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator) => {
|
|
189
198
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
190
199
|
// Create attachment opener registry to handle file attachments
|
|
191
200
|
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
|
|
@@ -204,7 +213,10 @@ const plugin = {
|
|
|
204
213
|
shell: app.shell
|
|
205
214
|
});
|
|
206
215
|
}
|
|
207
|
-
|
|
216
|
+
modelHandler.activeCellManager = activeCellManager;
|
|
217
|
+
// Creating the tracker for the chat widgets
|
|
218
|
+
const namespace = 'ai-chat';
|
|
219
|
+
const tracker = new WidgetTracker({ namespace });
|
|
208
220
|
// Create chat panel with drag/drop functionality
|
|
209
221
|
const chatPanel = new MultiChatPanel({
|
|
210
222
|
rmRegistry,
|
|
@@ -212,19 +224,35 @@ const plugin = {
|
|
|
212
224
|
inputToolbarFactory,
|
|
213
225
|
attachmentOpenerRegistry,
|
|
214
226
|
chatCommandRegistry,
|
|
215
|
-
createModel: async (
|
|
216
|
-
|
|
227
|
+
createModel: async (provider) => {
|
|
228
|
+
if (!provider) {
|
|
229
|
+
provider = settingsModel.getDefaultProvider()?.id;
|
|
230
|
+
if (!provider) {
|
|
231
|
+
showErrorMessage('Error creating chat', 'Please set up a provider');
|
|
232
|
+
app.commands.execute('@jupyterlite/ai:open-settings');
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let name = settingsModel.getProvider(provider)?.name ?? UUID.uuid4();
|
|
237
|
+
const modelName = name;
|
|
238
|
+
const existingName = new Set(chatPanel.getLoadedModelNames());
|
|
239
|
+
tracker.forEach(widget => existingName.add(widget.model.name));
|
|
240
|
+
let i = 1;
|
|
241
|
+
while (existingName.has(name)) {
|
|
242
|
+
name = `${modelName}-${i}`;
|
|
243
|
+
i += 1;
|
|
244
|
+
}
|
|
245
|
+
const model = modelHandler.createModel(name, provider);
|
|
217
246
|
return { model };
|
|
218
247
|
},
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
248
|
+
getChatNames: async () => {
|
|
249
|
+
const names = {};
|
|
250
|
+
settingsModel.config.providers.forEach(provider => {
|
|
251
|
+
names[provider.name] = provider.id;
|
|
252
|
+
});
|
|
253
|
+
return names;
|
|
227
254
|
},
|
|
255
|
+
renameChat: true,
|
|
228
256
|
openInMain: (name) => app.commands.execute(CommandIds.moveChat, {
|
|
229
257
|
area: 'main',
|
|
230
258
|
name
|
|
@@ -241,23 +269,29 @@ const plugin = {
|
|
|
241
269
|
},
|
|
242
270
|
tooltip: trans.__('Open AI Settings')
|
|
243
271
|
}));
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const model =
|
|
272
|
+
let tokenUsageWidget = null;
|
|
273
|
+
chatPanel.chatOpened.connect((_, widget) => {
|
|
274
|
+
const model = widget.model;
|
|
247
275
|
// Add the widget to the tracker.
|
|
248
276
|
tracker.add(widget);
|
|
277
|
+
function saveTracker() {
|
|
278
|
+
tracker.save(widget);
|
|
279
|
+
}
|
|
249
280
|
// Update the tracker if the model name changed.
|
|
250
|
-
model.nameChanged.connect(
|
|
281
|
+
model.nameChanged.connect(saveTracker);
|
|
251
282
|
// Update the tracker if the active provider changed.
|
|
252
|
-
model.agentManager.activeProviderChanged.connect(
|
|
253
|
-
|
|
283
|
+
model.agentManager.activeProviderChanged.connect(saveTracker);
|
|
284
|
+
// Update the token usage widget.
|
|
285
|
+
tokenUsageWidget?.dispose();
|
|
286
|
+
tokenUsageWidget = new TokenUsageWidget({
|
|
254
287
|
tokenUsageChanged: model.tokenUsageChanged,
|
|
255
288
|
settingsModel,
|
|
256
289
|
initialTokenUsage: model.agentManager.tokenUsage,
|
|
257
290
|
translator: trans
|
|
258
291
|
});
|
|
259
|
-
|
|
260
|
-
|
|
292
|
+
chatPanel.current?.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
|
|
293
|
+
// Listen for writers change to display the stop button.
|
|
294
|
+
function writersChanged(_, writers) {
|
|
261
295
|
// Check if AI is currently writing (streaming)
|
|
262
296
|
const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
|
|
263
297
|
if (aiWriting) {
|
|
@@ -268,23 +302,28 @@ const plugin = {
|
|
|
268
302
|
widget.inputToolbarRegistry?.hide('stop');
|
|
269
303
|
widget.inputToolbarRegistry?.show('send');
|
|
270
304
|
}
|
|
271
|
-
}
|
|
305
|
+
}
|
|
306
|
+
model.writersChanged?.connect(writersChanged);
|
|
272
307
|
// Associate an approval buttons object to the chat.
|
|
273
308
|
const approvalButton = new ApprovalButtons({
|
|
274
309
|
chatPanel: widget,
|
|
275
310
|
agentManager: model.agentManager
|
|
276
311
|
});
|
|
312
|
+
// Temporary compat: keep output-area CSS context for MIME renderers
|
|
313
|
+
// until jupyter-chat provides it natively.
|
|
314
|
+
const outputAreaCompat = new RenderedMessageOutputAreaCompat({
|
|
315
|
+
chatPanel: widget
|
|
316
|
+
});
|
|
277
317
|
widget.disposed.connect(() => {
|
|
318
|
+
model.nameChanged.disconnect(saveTracker);
|
|
319
|
+
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
320
|
+
model.writersChanged?.disconnect(writersChanged);
|
|
278
321
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
279
322
|
approvalButton.dispose();
|
|
280
|
-
|
|
281
|
-
modelRegistry.remove(model.name);
|
|
323
|
+
outputAreaCompat.dispose();
|
|
282
324
|
});
|
|
283
325
|
});
|
|
284
326
|
app.shell.add(chatPanel, 'left', { rank: 1000 });
|
|
285
|
-
// Creating the tracker for the document
|
|
286
|
-
const namespace = 'ai-chat';
|
|
287
|
-
const tracker = new WidgetTracker({ namespace });
|
|
288
327
|
if (restorer) {
|
|
289
328
|
restorer.add(chatPanel, chatPanel.id);
|
|
290
329
|
void restorer.restore(tracker, {
|
|
@@ -302,12 +341,12 @@ const plugin = {
|
|
|
302
341
|
}
|
|
303
342
|
// Create a chat with default provider at startup.
|
|
304
343
|
app.restored.then(() => {
|
|
305
|
-
if (!
|
|
306
|
-
settingsModel.config.defaultProvider) {
|
|
344
|
+
if (!tracker.size && settingsModel.config.defaultProvider) {
|
|
307
345
|
app.commands.execute(CommandIds.openChat);
|
|
308
346
|
}
|
|
309
347
|
});
|
|
310
|
-
registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker,
|
|
348
|
+
registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelHandler, trans, themeManager, labShell);
|
|
349
|
+
return tracker;
|
|
311
350
|
}
|
|
312
351
|
};
|
|
313
352
|
function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelRegistry, trans, themeManager, labShell) {
|
|
@@ -373,13 +412,16 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
373
412
|
app.shell.add(widget, 'main');
|
|
374
413
|
// Add the widget to the tracker.
|
|
375
414
|
tracker.add(widget);
|
|
415
|
+
function saveTracker() {
|
|
416
|
+
tracker.save(widget);
|
|
417
|
+
}
|
|
376
418
|
// Update the tracker if the model name changed.
|
|
377
|
-
model.nameChanged.connect(
|
|
419
|
+
model.nameChanged.connect(saveTracker);
|
|
378
420
|
// Update the tracker if the active provider changed.
|
|
379
|
-
model.agentManager.activeProviderChanged.connect(
|
|
380
|
-
// Remove the model from the registry when the widget is disposed.
|
|
421
|
+
model.agentManager.activeProviderChanged.connect(saveTracker);
|
|
381
422
|
widget.disposed.connect(() => {
|
|
382
|
-
|
|
423
|
+
model.nameChanged.disconnect(saveTracker);
|
|
424
|
+
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
383
425
|
});
|
|
384
426
|
};
|
|
385
427
|
commands.addCommand(CommandIds.openChat, {
|
|
@@ -387,11 +429,23 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
387
429
|
execute: async (args) => {
|
|
388
430
|
const area = args.area === 'main' ? 'main' : 'side';
|
|
389
431
|
const provider = args.provider ?? undefined;
|
|
390
|
-
|
|
391
|
-
|
|
432
|
+
let name = args.name ?? undefined;
|
|
433
|
+
let providerConfig = undefined;
|
|
434
|
+
if (provider) {
|
|
435
|
+
providerConfig = settingsModel.getProvider(provider);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
providerConfig = settingsModel.getDefaultProvider();
|
|
439
|
+
}
|
|
440
|
+
// Do not open the chat if the provider in args does not exists in settings or
|
|
441
|
+
// if there is no default provider.
|
|
442
|
+
if (!providerConfig) {
|
|
392
443
|
return false;
|
|
393
444
|
}
|
|
394
|
-
|
|
445
|
+
if (!name) {
|
|
446
|
+
name = providerConfig.name;
|
|
447
|
+
}
|
|
448
|
+
const model = modelRegistry.createModel(name, provider);
|
|
395
449
|
if (!model) {
|
|
396
450
|
return false;
|
|
397
451
|
}
|
|
@@ -399,7 +453,7 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
399
453
|
openInMain(model);
|
|
400
454
|
}
|
|
401
455
|
else {
|
|
402
|
-
chatPanel.
|
|
456
|
+
chatPanel.open({ model });
|
|
403
457
|
}
|
|
404
458
|
return true;
|
|
405
459
|
},
|
|
@@ -436,7 +490,14 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
436
490
|
console.error('Error while moving the chat to main area: the name has not been provided');
|
|
437
491
|
return false;
|
|
438
492
|
}
|
|
439
|
-
|
|
493
|
+
let previousWidget;
|
|
494
|
+
let previousModel;
|
|
495
|
+
tracker.forEach(widget => {
|
|
496
|
+
if (widget.model.name === args.name) {
|
|
497
|
+
previousWidget = widget;
|
|
498
|
+
previousModel = widget.model;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
440
501
|
if (!previousModel) {
|
|
441
502
|
console.error('Error while moving the chat to main area: there is no reference model');
|
|
442
503
|
return false;
|
|
@@ -455,8 +516,10 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
455
516
|
// model. The previous is intended to be disposed anyway.
|
|
456
517
|
previousModel.name = UUID.uuid4();
|
|
457
518
|
// Create a new model by duplicating the previous model attributes.
|
|
458
|
-
const model = modelRegistry.createModel(args.name, previousModel
|
|
459
|
-
previousModel?.messages.forEach(message =>
|
|
519
|
+
const model = modelRegistry.createModel(args.name, previousModel.agentManager.activeProvider, previousModel.agentManager.tokenUsage);
|
|
520
|
+
previousModel?.messages.forEach(message => {
|
|
521
|
+
model?.messageAdded({ ...message.content });
|
|
522
|
+
});
|
|
460
523
|
// Wait (with timeout) for the tracker to have updated the previous widget.
|
|
461
524
|
const status = await Promise.any([
|
|
462
525
|
trackerUpdated.promise,
|
|
@@ -472,13 +535,9 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
472
535
|
openInMain(model);
|
|
473
536
|
}
|
|
474
537
|
else {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
current.model.name === previousModel.name) {
|
|
479
|
-
current.dispose();
|
|
480
|
-
}
|
|
481
|
-
chatPanel.addChat({ model });
|
|
538
|
+
previousWidget?.dispose();
|
|
539
|
+
previousModel.dispose();
|
|
540
|
+
chatPanel.open({ model });
|
|
482
541
|
}
|
|
483
542
|
return true;
|
|
484
543
|
},
|
|
@@ -489,7 +548,7 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
489
548
|
area: {
|
|
490
549
|
type: 'string',
|
|
491
550
|
enum: ['main', 'side'],
|
|
492
|
-
description: trans.__('The
|
|
551
|
+
description: trans.__('The area to move the chat to')
|
|
493
552
|
},
|
|
494
553
|
name: {
|
|
495
554
|
type: 'string',
|
|
@@ -503,7 +562,9 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
503
562
|
}
|
|
504
563
|
}
|
|
505
564
|
/**
|
|
506
|
-
* A plugin to provide the
|
|
565
|
+
* A plugin to provide the agent manager factory, the completion provider and
|
|
566
|
+
* the settings model.
|
|
567
|
+
* All these objects require the secrets manager token with the same namespace.
|
|
507
568
|
*/
|
|
508
569
|
const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => ({
|
|
509
570
|
id: SECRETS_NAMESPACE,
|
|
@@ -643,6 +704,7 @@ const toolRegistry = {
|
|
|
643
704
|
const executeCommandTool = createExecuteCommandTool(app.commands, settingsModel);
|
|
644
705
|
toolRegistry.add('discover_commands', discoverCommandsTool);
|
|
645
706
|
toolRegistry.add('execute_command', executeCommandTool);
|
|
707
|
+
toolRegistry.add('browser_fetch', createBrowserFetchTool());
|
|
646
708
|
if (skillRegistry) {
|
|
647
709
|
toolRegistry.add('discover_skills', createDiscoverSkillsTool(skillRegistry));
|
|
648
710
|
toolRegistry.add('load_skill', createLoadSkillTool(skillRegistry));
|
|
@@ -658,13 +720,13 @@ const inputToolbarFactory = {
|
|
|
658
720
|
description: 'The input toolbar registry plugin.',
|
|
659
721
|
autoStart: true,
|
|
660
722
|
provides: IInputToolbarRegistryFactory,
|
|
661
|
-
requires: [IAISettingsModel, IToolRegistry],
|
|
723
|
+
requires: [IAISettingsModel, IToolRegistry, IProviderRegistry],
|
|
662
724
|
optional: [ITranslator],
|
|
663
|
-
activate: (app, settingsModel, toolRegistry, translator) => {
|
|
725
|
+
activate: (app, settingsModel, toolRegistry, providerRegistry, translator) => {
|
|
664
726
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
665
727
|
const stopButton = stopItem(trans);
|
|
666
728
|
const clearButton = clearItem(trans);
|
|
667
|
-
const toolSelectButton = createToolSelectItem(toolRegistry, settingsModel.config.toolsEnabled, trans);
|
|
729
|
+
const toolSelectButton = createToolSelectItem(toolRegistry, settingsModel, providerRegistry, settingsModel.config.toolsEnabled, trans);
|
|
668
730
|
const modelSelectButton = createModelSelectItem(settingsModel, trans);
|
|
669
731
|
return {
|
|
670
732
|
create() {
|
|
@@ -828,7 +890,7 @@ export default [
|
|
|
828
890
|
clearCommandPlugin,
|
|
829
891
|
skillRegistryPlugin,
|
|
830
892
|
skillsCommandPlugin,
|
|
831
|
-
|
|
893
|
+
chatModelHandler,
|
|
832
894
|
plugin,
|
|
833
895
|
toolRegistry,
|
|
834
896
|
agentManagerFactory,
|
|
@@ -41,6 +41,8 @@ export interface IAIConfig {
|
|
|
41
41
|
sendWithShiftEnter: boolean;
|
|
42
42
|
showTokenUsage: boolean;
|
|
43
43
|
commandsRequiringApproval: string[];
|
|
44
|
+
commandsAutoRenderMimeBundles: string[];
|
|
45
|
+
trustedMimeTypesForAutoRender: string[];
|
|
44
46
|
showCellDiff: boolean;
|
|
45
47
|
showFileDiff: boolean;
|
|
46
48
|
diffDisplayMode: 'split' | 'unified';
|
|
@@ -33,6 +33,8 @@ export class AISettingsModel extends VDomModel {
|
|
|
33
33
|
'runmenu:run-all',
|
|
34
34
|
'jupyterlab-ai-commands:run-cell'
|
|
35
35
|
],
|
|
36
|
+
commandsAutoRenderMimeBundles: ['jupyterlab-ai-commands:execute-in-kernel'],
|
|
37
|
+
trustedMimeTypesForAutoRender: ['text/html'],
|
|
36
38
|
systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
|
|
37
39
|
|
|
38
40
|
## Your Core Mission
|
|
@@ -32,6 +32,10 @@ export const anthropicProvider = {
|
|
|
32
32
|
],
|
|
33
33
|
supportsBaseURL: true,
|
|
34
34
|
supportsHeaders: true,
|
|
35
|
+
providerToolCapabilities: {
|
|
36
|
+
webSearch: { implementation: 'anthropic' },
|
|
37
|
+
webFetch: { implementation: 'anthropic' }
|
|
38
|
+
},
|
|
35
39
|
factory: (options) => {
|
|
36
40
|
if (!options.apiKey) {
|
|
37
41
|
throw new Error('API key required for Anthropic');
|
|
@@ -198,6 +202,9 @@ export const openaiProvider = {
|
|
|
198
202
|
],
|
|
199
203
|
supportsBaseURL: true,
|
|
200
204
|
supportsHeaders: true,
|
|
205
|
+
providerToolCapabilities: {
|
|
206
|
+
webSearch: { implementation: 'openai' }
|
|
207
|
+
},
|
|
201
208
|
factory: (options) => {
|
|
202
209
|
if (!options.apiKey) {
|
|
203
210
|
throw new Error('API key required for OpenAI');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
import type { IProviderInfo } from '../tokens';
|
|
3
|
+
type ToolMap = Record<string, Tool>;
|
|
4
|
+
interface IWebSearchSettings {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
externalWebAccess?: boolean;
|
|
7
|
+
searchContextSize?: 'low' | 'medium' | 'high';
|
|
8
|
+
allowedDomains?: string[];
|
|
9
|
+
blockedDomains?: string[];
|
|
10
|
+
maxUses?: number;
|
|
11
|
+
}
|
|
12
|
+
interface IWebFetchSettings {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
maxUses?: number;
|
|
15
|
+
maxContentTokens?: number;
|
|
16
|
+
allowedDomains?: string[];
|
|
17
|
+
blockedDomains?: string[];
|
|
18
|
+
citationsEnabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Provider-level custom settings that control built-in web tools.
|
|
22
|
+
*/
|
|
23
|
+
export interface IProviderCustomSettings {
|
|
24
|
+
webSearch?: IWebSearchSettings;
|
|
25
|
+
webFetch?: IWebFetchSettings;
|
|
26
|
+
}
|
|
27
|
+
interface IProviderToolContext {
|
|
28
|
+
providerInfo?: IProviderInfo | null;
|
|
29
|
+
customSettings?: IProviderCustomSettings;
|
|
30
|
+
hasFunctionTools: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create provider-defined tools from custom settings and provider capabilities.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createProviderTools(options: IProviderToolContext): ToolMap;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES = 2;
|
|
4
|
+
const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS = 12000;
|
|
5
|
+
function normalizeDomain(value) {
|
|
6
|
+
const normalized = (value || '').trim().toLowerCase();
|
|
7
|
+
const withoutProtocol = normalized.replace(/^https?:\/\//, '');
|
|
8
|
+
const hostname = withoutProtocol.split('/')[0].trim();
|
|
9
|
+
// Treat "*.example.com" as "example.com" for provider domain filters.
|
|
10
|
+
return hostname.startsWith('*.') ? hostname.slice(2) : hostname;
|
|
11
|
+
}
|
|
12
|
+
function collectDomains(value) {
|
|
13
|
+
value = value || [];
|
|
14
|
+
const values = Array.from(new Set(value.map(normalizeDomain).filter(domain => domain.length > 0)));
|
|
15
|
+
return values;
|
|
16
|
+
}
|
|
17
|
+
function createOpenAIWebSearchTool(webSearchSettings) {
|
|
18
|
+
const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
|
|
19
|
+
return openai.tools.webSearch({
|
|
20
|
+
externalWebAccess: webSearchSettings.externalWebAccess,
|
|
21
|
+
searchContextSize: webSearchSettings.searchContextSize,
|
|
22
|
+
filters: allowedDomains.length > 0 ? { allowedDomains } : undefined
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function createAnthropicWebSearchTool(webSearchSettings) {
|
|
26
|
+
const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
|
|
27
|
+
const blockedDomains = collectDomains(webSearchSettings.blockedDomains);
|
|
28
|
+
return anthropic.tools.webSearch_20250305({
|
|
29
|
+
maxUses: webSearchSettings.maxUses,
|
|
30
|
+
allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
|
|
31
|
+
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function createAnthropicWebFetchTool(webFetchSettings) {
|
|
35
|
+
const maxUses = webFetchSettings.maxUses ?? DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES;
|
|
36
|
+
const maxContentTokens = webFetchSettings.maxContentTokens ??
|
|
37
|
+
DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS;
|
|
38
|
+
const allowedDomains = collectDomains(webFetchSettings.allowedDomains);
|
|
39
|
+
const blockedDomains = collectDomains(webFetchSettings.blockedDomains);
|
|
40
|
+
const citationsEnabled = webFetchSettings.citationsEnabled;
|
|
41
|
+
return anthropic.tools.webFetch_20250910({
|
|
42
|
+
maxUses,
|
|
43
|
+
maxContentTokens,
|
|
44
|
+
allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
|
|
45
|
+
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined,
|
|
46
|
+
citations: citationsEnabled !== undefined ? { enabled: citationsEnabled } : undefined
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function createWebSearchTool(implementation, webSearchSettings) {
|
|
50
|
+
switch (implementation) {
|
|
51
|
+
case 'openai':
|
|
52
|
+
return createOpenAIWebSearchTool(webSearchSettings);
|
|
53
|
+
case 'anthropic':
|
|
54
|
+
return createAnthropicWebSearchTool(webSearchSettings);
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported web search implementation: ${implementation}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createWebFetchTool(implementation, webFetchSettings) {
|
|
60
|
+
switch (implementation) {
|
|
61
|
+
case 'anthropic':
|
|
62
|
+
return createAnthropicWebFetchTool(webFetchSettings);
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported web fetch implementation: ${implementation}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create provider-defined tools from custom settings and provider capabilities.
|
|
69
|
+
*/
|
|
70
|
+
export function createProviderTools(options) {
|
|
71
|
+
const tools = {};
|
|
72
|
+
if (!options.customSettings ||
|
|
73
|
+
!options.providerInfo?.providerToolCapabilities) {
|
|
74
|
+
return tools;
|
|
75
|
+
}
|
|
76
|
+
const capabilities = options.providerInfo.providerToolCapabilities;
|
|
77
|
+
const webSearchSettings = options.customSettings.webSearch;
|
|
78
|
+
const webFetchSettings = options.customSettings.webFetch;
|
|
79
|
+
const webSearchEnabled = webSearchSettings?.enabled === true;
|
|
80
|
+
const webFetchEnabled = webFetchSettings?.enabled === true;
|
|
81
|
+
const webSearchCapability = capabilities.webSearch;
|
|
82
|
+
if (webSearchEnabled && webSearchSettings && webSearchCapability) {
|
|
83
|
+
const requiresNoFunctionTools = webSearchCapability.requiresNoFunctionTools === true;
|
|
84
|
+
if (!requiresNoFunctionTools || !options.hasFunctionTools) {
|
|
85
|
+
tools.web_search = createWebSearchTool(webSearchCapability.implementation, webSearchSettings);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const webFetchCapability = capabilities.webFetch;
|
|
89
|
+
if (webFetchEnabled && webFetchSettings && webFetchCapability) {
|
|
90
|
+
tools.web_fetch = createWebFetchTool(webFetchCapability.implementation, webFetchSettings);
|
|
91
|
+
}
|
|
92
|
+
return tools;
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChatWidget } from '@jupyter/chat';
|
|
2
|
+
import { IDisposable } from '@lumino/disposable';
|
|
3
|
+
/**
|
|
4
|
+
* Ensures chat-rendered MIME outputs also expose the OutputArea class so
|
|
5
|
+
* renderer extensions can reuse their notebook/output-area CSS rules.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Remove this compatibility layer once jupyter-chat applies
|
|
8
|
+
* `jp-OutputArea` (or equivalent output-area context) to rendered MIME
|
|
9
|
+
* messages by default.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RenderedMessageOutputAreaCompat implements IDisposable {
|
|
12
|
+
constructor(options: RenderedMessageOutputAreaCompat.IOptions);
|
|
13
|
+
get isDisposed(): boolean;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
private _scheduleSync;
|
|
16
|
+
private readonly _chatPanel;
|
|
17
|
+
private _isDisposed;
|
|
18
|
+
private _raf;
|
|
19
|
+
}
|
|
20
|
+
export declare namespace RenderedMessageOutputAreaCompat {
|
|
21
|
+
interface IOptions {
|
|
22
|
+
chatPanel: ChatWidget;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const OUTPUT_AREA_CLASS = 'jp-OutputArea';
|
|
2
|
+
const CHAT_RENDERED_MESSAGE_SELECTOR = `.jp-chat-rendered-message:not(.${OUTPUT_AREA_CLASS})`;
|
|
3
|
+
/**
|
|
4
|
+
* Ensures chat-rendered MIME outputs also expose the OutputArea class so
|
|
5
|
+
* renderer extensions can reuse their notebook/output-area CSS rules.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Remove this compatibility layer once jupyter-chat applies
|
|
8
|
+
* `jp-OutputArea` (or equivalent output-area context) to rendered MIME
|
|
9
|
+
* messages by default.
|
|
10
|
+
*/
|
|
11
|
+
export class RenderedMessageOutputAreaCompat {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this._chatPanel = options.chatPanel;
|
|
14
|
+
this._chatPanel.model.messagesUpdated.connect(this._scheduleSync, this);
|
|
15
|
+
this._scheduleSync();
|
|
16
|
+
}
|
|
17
|
+
get isDisposed() {
|
|
18
|
+
return this._isDisposed;
|
|
19
|
+
}
|
|
20
|
+
dispose() {
|
|
21
|
+
if (this._isDisposed) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this._isDisposed = true;
|
|
25
|
+
this._chatPanel.model.messagesUpdated.disconnect(this._scheduleSync, this);
|
|
26
|
+
if (this._raf !== 0) {
|
|
27
|
+
cancelAnimationFrame(this._raf);
|
|
28
|
+
this._raf = 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
_scheduleSync() {
|
|
32
|
+
if (this._isDisposed || this._raf !== 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this._raf = requestAnimationFrame(() => {
|
|
36
|
+
this._raf = 0;
|
|
37
|
+
if (this._isDisposed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this._chatPanel.node
|
|
41
|
+
.querySelectorAll(CHAT_RENDERED_MESSAGE_SELECTOR)
|
|
42
|
+
.forEach(element => element.classList.add(OUTPUT_AREA_CLASS));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
_chatPanel;
|
|
46
|
+
_isDisposed = false;
|
|
47
|
+
_raf = 0;
|
|
48
|
+
}
|