@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.
Files changed (49) hide show
  1. package/lib/agent.d.ts +24 -2
  2. package/lib/agent.js +161 -24
  3. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  4. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  5. package/lib/chat-model.d.ts +8 -0
  6. package/lib/chat-model.js +156 -8
  7. package/lib/completion/completion-provider.d.ts +1 -1
  8. package/lib/completion/completion-provider.js +14 -2
  9. package/lib/components/model-select.js +4 -4
  10. package/lib/components/tool-select.d.ts +11 -2
  11. package/lib/components/tool-select.js +77 -18
  12. package/lib/index.d.ts +3 -3
  13. package/lib/index.js +128 -66
  14. package/lib/models/settings-model.d.ts +2 -0
  15. package/lib/models/settings-model.js +2 -0
  16. package/lib/providers/built-in-providers.js +7 -0
  17. package/lib/providers/provider-tools.d.ts +36 -0
  18. package/lib/providers/provider-tools.js +93 -0
  19. package/lib/rendered-message-outputarea.d.ts +24 -0
  20. package/lib/rendered-message-outputarea.js +48 -0
  21. package/lib/tokens.d.ts +44 -7
  22. package/lib/tokens.js +1 -1
  23. package/lib/tools/commands.js +4 -2
  24. package/lib/tools/web.d.ts +8 -0
  25. package/lib/tools/web.js +196 -0
  26. package/lib/widgets/ai-settings.d.ts +1 -1
  27. package/lib/widgets/ai-settings.js +125 -38
  28. package/lib/widgets/main-area-chat.d.ts +6 -0
  29. package/lib/widgets/main-area-chat.js +28 -0
  30. package/lib/widgets/provider-config-dialog.js +207 -4
  31. package/package.json +10 -4
  32. package/schema/settings-model.json +89 -1
  33. package/src/agent.ts +220 -42
  34. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  35. package/src/chat-model.ts +223 -14
  36. package/src/completion/completion-provider.ts +26 -12
  37. package/src/components/model-select.tsx +4 -5
  38. package/src/components/tool-select.tsx +110 -7
  39. package/src/index.ts +153 -82
  40. package/src/models/settings-model.ts +6 -0
  41. package/src/providers/built-in-providers.ts +7 -0
  42. package/src/providers/provider-tools.ts +179 -0
  43. package/src/rendered-message-outputarea.ts +62 -0
  44. package/src/tokens.ts +53 -9
  45. package/src/tools/commands.ts +4 -2
  46. package/src/tools/web.ts +238 -0
  47. package/src/widgets/ai-settings.tsx +282 -77
  48. package/src/widgets/main-area-chat.ts +34 -1
  49. 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 { ChatModelRegistry } from './chat-model-registry';
22
- import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelRegistry, IDiffManager } from './tokens';
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 registry.
148
+ * The chat model handler.
147
149
  */
148
- const chatModelRegistry = {
149
- id: '@jupyterlite/ai:chat-model-registry',
150
- description: 'Registry for the current chat model',
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: [IAISettingsModel, IAgentManagerFactory, IDocumentManager],
154
+ requires: [
155
+ IAISettingsModel,
156
+ IAgentManagerFactory,
157
+ IDocumentManager,
158
+ IRenderMimeRegistry
159
+ ],
153
160
  optional: [IProviderRegistry, IToolRegistry, ITranslator],
154
- provides: IChatModelRegistry,
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 ChatModelRegistry({
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
- IChatModelRegistry,
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, modelRegistry, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator) => {
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
- modelRegistry.activeCellManager = activeCellManager;
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 (name) => {
216
- const model = modelRegistry.createModel(name);
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
- renameChat: async (oldName, newName) => {
220
- const model = modelRegistry.get(oldName);
221
- const concurrencyModel = modelRegistry.get(newName);
222
- if (model && !concurrencyModel) {
223
- model.name = newName;
224
- return true;
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
- chatPanel.sectionAdded.connect((_, section) => {
245
- const { widget } = section;
246
- const model = section.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(() => tracker.save(widget));
281
+ model.nameChanged.connect(saveTracker);
251
282
  // Update the tracker if the active provider changed.
252
- model.agentManager.activeProviderChanged.connect(() => tracker.save(widget));
253
- const tokenUsageWidget = new TokenUsageWidget({
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
- section.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
260
- model.writersChanged?.connect((_, writers) => {
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
- // Remove the model from the registry when the widget is disposed.
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 (!modelRegistry.getAll().length &&
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, modelRegistry, trans, themeManager, labShell);
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(() => tracker.save(widget));
419
+ model.nameChanged.connect(saveTracker);
378
420
  // Update the tracker if the active provider changed.
379
- model.agentManager.activeProviderChanged.connect(() => tracker.save(widget));
380
- // Remove the model from the registry when the widget is disposed.
421
+ model.agentManager.activeProviderChanged.connect(saveTracker);
381
422
  widget.disposed.connect(() => {
382
- modelRegistry.remove(model.name);
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
- // Do not open the chat if the provider in args does not exists in settings.
391
- if (provider && !settingsModel.getProvider(provider)) {
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
- const model = modelRegistry.createModel(args.name ? args.name : undefined, provider);
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.addChat({ model });
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
- const previousModel = modelRegistry.get(args.name);
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?.agentManager.activeProvider, previousModel?.agentManager.tokenUsage);
459
- previousModel?.messages.forEach(message => model?.messageAdded(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
- const current = app.shell.currentWidget;
476
- // Remove the current main area chat.
477
- if (current instanceof MainAreaChat &&
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 name of the area to move the chat to')
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 settings model.
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
- chatModelRegistry,
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
+ }