@jupyterlite/ai 0.4.0 → 0.5.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 (59) hide show
  1. package/lib/chat-handler.d.ts +7 -1
  2. package/lib/chat-handler.js +29 -0
  3. package/lib/completion-provider.d.ts +1 -1
  4. package/lib/{llm-models/anthropic-completer.d.ts → default-providers/Anthropic/completer.d.ts} +1 -1
  5. package/lib/{llm-models/anthropic-completer.js → default-providers/Anthropic/completer.js} +1 -1
  6. package/lib/{llm-models/chrome-completer.d.ts → default-providers/ChromeAI/completer.d.ts} +1 -1
  7. package/lib/{llm-models/chrome-completer.js → default-providers/ChromeAI/completer.js} +1 -1
  8. package/lib/default-providers/ChromeAI/instructions.d.ts +2 -0
  9. package/lib/default-providers/ChromeAI/instructions.js +24 -0
  10. package/lib/{llm-models/codestral-completer.d.ts → default-providers/MistralAI/completer.d.ts} +1 -1
  11. package/lib/{llm-models/codestral-completer.js → default-providers/MistralAI/completer.js} +1 -1
  12. package/lib/default-providers/MistralAI/instructions.d.ts +2 -0
  13. package/lib/default-providers/MistralAI/instructions.js +16 -0
  14. package/lib/{llm-models/openai-completer.d.ts → default-providers/OpenAI/completer.d.ts} +1 -1
  15. package/lib/{llm-models/openai-completer.js → default-providers/OpenAI/completer.js} +1 -1
  16. package/lib/default-providers/index.d.ts +2 -0
  17. package/lib/default-providers/index.js +60 -0
  18. package/lib/index.d.ts +2 -2
  19. package/lib/index.js +17 -32
  20. package/lib/provider.d.ts +1 -1
  21. package/lib/settings/panel.d.ts +14 -0
  22. package/lib/settings/panel.js +82 -5
  23. package/lib/tokens.d.ts +1 -1
  24. package/package.json +8 -5
  25. package/schema/provider-registry.json +6 -0
  26. package/src/chat-handler.ts +34 -0
  27. package/src/completion-provider.ts +1 -1
  28. package/src/{llm-models/anthropic-completer.ts → default-providers/Anthropic/completer.ts} +2 -2
  29. package/src/{llm-models/chrome-completer.ts → default-providers/ChromeAI/completer.ts} +3 -2
  30. package/src/default-providers/ChromeAI/instructions.ts +24 -0
  31. package/src/{llm-models/codestral-completer.ts → default-providers/MistralAI/completer.ts} +2 -2
  32. package/src/default-providers/MistralAI/instructions.ts +16 -0
  33. package/src/{llm-models/openai-completer.ts → default-providers/OpenAI/completer.ts} +2 -2
  34. package/src/default-providers/index.ts +71 -0
  35. package/src/index.ts +25 -42
  36. package/src/provider.ts +1 -1
  37. package/src/settings/panel.tsx +93 -4
  38. package/src/tokens.ts +1 -1
  39. package/lib/llm-models/index.d.ts +0 -4
  40. package/lib/llm-models/index.js +0 -43
  41. package/lib/settings/instructions.d.ts +0 -2
  42. package/lib/settings/instructions.js +0 -44
  43. package/lib/settings/schemas/index.d.ts +0 -3
  44. package/lib/settings/schemas/index.js +0 -11
  45. package/lib/slash-commands.d.ts +0 -16
  46. package/lib/slash-commands.js +0 -25
  47. package/src/llm-models/index.ts +0 -50
  48. package/src/settings/instructions.ts +0 -48
  49. package/src/settings/schemas/index.ts +0 -15
  50. package/src/slash-commands.tsx +0 -55
  51. /package/lib/{llm-models/base-completer.d.ts → base-completer.d.ts} +0 -0
  52. /package/lib/{llm-models/base-completer.js → base-completer.js} +0 -0
  53. /package/lib/{settings/schemas/_generated/Anthropic.json → default-providers/Anthropic/settings-schema.json} +0 -0
  54. /package/lib/{settings/schemas/_generated/ChromeAI.json → default-providers/ChromeAI/settings-schema.json} +0 -0
  55. /package/lib/{settings/schemas/_generated/MistralAI.json → default-providers/MistralAI/settings-schema.json} +0 -0
  56. /package/lib/{settings/schemas/_generated/OpenAI.json → default-providers/OpenAI/settings-schema.json} +0 -0
  57. /package/lib/settings/{schemas/base.json → base.json} +0 -0
  58. /package/src/{llm-models/base-completer.ts → base-completer.ts} +0 -0
  59. /package/src/{llm-models/svg.d.ts → global.d.ts} +0 -0
@@ -5,6 +5,12 @@
5
5
  "jupyter.lab.setting-icon-label": "JupyterLite AI Chat",
6
6
  "type": "object",
7
7
  "properties": {
8
+ "UseSecretsManager": {
9
+ "type": "boolean",
10
+ "title": "Use secrets manager",
11
+ "description": "Whether to use or not the secrets manager. If not, secrets will be stored in the browser (local storage)",
12
+ "default": true
13
+ },
8
14
  "AIprovider": {
9
15
  "type": "object",
10
16
  "title": "AI provider",
@@ -4,9 +4,12 @@
4
4
  */
5
5
 
6
6
  import {
7
+ ChatCommand,
7
8
  ChatModel,
9
+ IChatCommandProvider,
8
10
  IChatHistory,
9
11
  IChatMessage,
12
+ IInputModel,
10
13
  INewMessage
11
14
  } from '@jupyter/chat';
12
15
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
@@ -186,4 +189,35 @@ export namespace ChatHandler {
186
189
  export interface IOptions extends ChatModel.IOptions {
187
190
  providerRegistry: IAIProviderRegistry;
188
191
  }
192
+
193
+ export class ClearCommandProvider implements IChatCommandProvider {
194
+ public id: string = '@jupyterlite/ai:clear-commands';
195
+ private _slash_commands: ChatCommand[] = [
196
+ {
197
+ name: '/clear',
198
+ providerId: this.id,
199
+ replaceWith: '/clear',
200
+ description: 'Clear the chat'
201
+ }
202
+ ];
203
+ async getChatCommands(inputModel: IInputModel) {
204
+ const match = inputModel.currentWord?.match(/^\/\w*/)?.[0];
205
+ if (!match) {
206
+ return [];
207
+ }
208
+
209
+ const commands = this._slash_commands.filter(cmd =>
210
+ cmd.name.startsWith(match)
211
+ );
212
+ return commands;
213
+ }
214
+
215
+ async handleChatCommand(
216
+ command: ChatCommand,
217
+ inputModel: IInputModel
218
+ ): Promise<void> {
219
+ // no handling needed because `replaceWith` is set in each command.
220
+ return;
221
+ }
222
+ }
189
223
  }
@@ -4,7 +4,7 @@ import {
4
4
  IInlineCompletionProvider
5
5
  } from '@jupyterlab/completer';
6
6
 
7
- import { IBaseCompleter } from './llm-models';
7
+ import { IBaseCompleter } from './base-completer';
8
8
  import { IAIProviderRegistry } from './tokens';
9
9
 
10
10
  /**
@@ -6,8 +6,8 @@ import { ChatAnthropic } from '@langchain/anthropic';
6
6
  import { BaseChatModel } from '@langchain/core/language_models/chat_models';
7
7
  import { AIMessage, SystemMessage } from '@langchain/core/messages';
8
8
 
9
- import { BaseCompleter, IBaseCompleter } from './base-completer';
10
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
9
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
10
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
11
11
 
12
12
  export class AnthropicCompleter implements IBaseCompleter {
13
13
  constructor(options: BaseCompleter.IOptions) {
@@ -5,8 +5,9 @@ import {
5
5
  import { ChromeAI } from '@langchain/community/experimental/llms/chrome_ai';
6
6
  import { LLM } from '@langchain/core/language_models/llms';
7
7
  import { HumanMessage, SystemMessage } from '@langchain/core/messages';
8
- import { BaseCompleter, IBaseCompleter } from './base-completer';
9
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
8
+
9
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
10
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
10
11
 
11
12
  /**
12
13
  * Regular expression to match the '```' string at the start of a string.
@@ -0,0 +1,24 @@
1
+ export default `
2
+ <i class="fas fa-exclamation-triangle"></i> Support for ChromeAI is still experimental and only available in Google Chrome.
3
+
4
+ You can test ChromeAI is enabled in your browser by going to the following URL: <https://chromeai.org/>
5
+
6
+ Enable the proper flags in Google Chrome.
7
+
8
+ - chrome://flags/#prompt-api-for-gemini-nano
9
+ - Select: \`Enabled\`
10
+ - chrome://flags/#optimization-guide-on-device-model
11
+ - Select: \`Enabled BypassPrefRequirement\`
12
+ - chrome://components
13
+ - Click \`Check for Update\` on Optimization Guide On Device Model to download the model
14
+ - [Optional] chrome://flags/#text-safety-classifier
15
+
16
+ <img src="https://github.com/user-attachments/assets/d48f46cc-52ee-4ce5-9eaf-c763cdbee04c" alt="A screenshot showing how to enable the ChromeAI flag in Google Chrome" width="500px">
17
+
18
+ Then restart Chrome for these changes to take effect.
19
+
20
+ <i class="fas fa-exclamation-triangle"></i> On first use, Chrome will download the on-device model, which can be as large as 22GB (according to their docs and at the time of writing).
21
+ During the download, ChromeAI may not be available via the extension.
22
+
23
+ <i class="fa fa-info-circle" aria-hidden="true"></i> For more information about Chrome Built-in AI: <https://developer.chrome.com/docs/ai/get-started>
24
+ `;
@@ -11,8 +11,8 @@ import {
11
11
  import { ChatMistralAI } from '@langchain/mistralai';
12
12
  import { Throttler } from '@lumino/polling';
13
13
 
14
- import { BaseCompleter, IBaseCompleter } from './base-completer';
15
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
14
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
15
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
16
16
 
17
17
  /**
18
18
  * The Mistral API has a rate limit of 1 request per second
@@ -0,0 +1,16 @@
1
+ export default `
2
+ <i class="fas fa-exclamation-triangle"></i> This extension is still very much experimental. It is not an official MistralAI extension.
3
+
4
+ 1. Go to <https://console.mistral.ai/api-keys/> and create an API key.
5
+
6
+ <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/1-api-key.png" alt="Screenshot showing how to create an API key" width="500px">
7
+
8
+ 2. Open the JupyterLab settings and go to the **Ai providers** section to select the \`MistralAI\`
9
+ provider and the API key (required).
10
+
11
+ <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/2-jupyterlab-settings.png" alt="Screenshot showing how to add the API key to the settings" width="500px">
12
+
13
+ 3. Open the chat, or use the inline completer
14
+
15
+ <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/3-usage.png" alt="Screenshot showing how to use the chat" width="500px">
16
+ `;
@@ -6,8 +6,8 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
6
6
  import { AIMessage, SystemMessage } from '@langchain/core/messages';
7
7
  import { ChatOpenAI } from '@langchain/openai';
8
8
 
9
- import { BaseCompleter, IBaseCompleter } from './base-completer';
10
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
9
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
10
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
11
11
 
12
12
  export class OpenAICompleter implements IBaseCompleter {
13
13
  constructor(options: BaseCompleter.IOptions) {
@@ -0,0 +1,71 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+ import { ChatAnthropic } from '@langchain/anthropic';
6
+ import { ChromeAI } from '@langchain/community/experimental/llms/chrome_ai';
7
+ import { ChatMistralAI } from '@langchain/mistralai';
8
+ import { ChatOpenAI } from '@langchain/openai';
9
+
10
+ import { IAIProvider, IAIProviderRegistry } from '../tokens';
11
+
12
+ // Import completers
13
+ import { AnthropicCompleter } from './Anthropic/completer';
14
+ import { ChromeCompleter } from './ChromeAI/completer';
15
+ import { CodestralCompleter } from './MistralAI/completer';
16
+ import { OpenAICompleter } from './OpenAI/completer';
17
+
18
+ // Import Settings
19
+ import AnthropicSettings from './Anthropic/settings-schema.json';
20
+ import ChromeAISettings from './ChromeAI/settings-schema.json';
21
+ import MistralAISettings from './MistralAI/settings-schema.json';
22
+ import OpenAISettings from './OpenAI/settings-schema.json';
23
+
24
+ // Import instructions
25
+ import ChromeAIInstructions from './ChromeAI/instructions';
26
+ import MistralAIInstructions from './MistralAI/instructions';
27
+
28
+ // Build the AIProvider list
29
+ const AIProviders: IAIProvider[] = [
30
+ {
31
+ name: 'Anthropic',
32
+ chatModel: ChatAnthropic,
33
+ completer: AnthropicCompleter,
34
+ settingsSchema: AnthropicSettings,
35
+ errorMessage: (error: any) => error.error.error.message
36
+ },
37
+ {
38
+ name: 'ChromeAI',
39
+ // TODO: fix
40
+ // @ts-expect-error: missing properties
41
+ chatModel: ChromeAI,
42
+ completer: ChromeCompleter,
43
+ instructions: ChromeAIInstructions,
44
+ settingsSchema: ChromeAISettings
45
+ },
46
+ {
47
+ name: 'MistralAI',
48
+ chatModel: ChatMistralAI,
49
+ completer: CodestralCompleter,
50
+ instructions: MistralAIInstructions,
51
+ settingsSchema: MistralAISettings
52
+ },
53
+ {
54
+ name: 'OpenAI',
55
+ chatModel: ChatOpenAI,
56
+ completer: OpenAICompleter,
57
+ settingsSchema: OpenAISettings
58
+ }
59
+ ];
60
+
61
+ export const defaultProviderPlugins: JupyterFrontEndPlugin<void>[] =
62
+ AIProviders.map(provider => {
63
+ return {
64
+ id: `@jupyterlite/ai:${provider.name}`,
65
+ autoStart: true,
66
+ requires: [IAIProviderRegistry],
67
+ activate: (app: JupyterFrontEnd, registry: IAIProviderRegistry) => {
68
+ registry.add(provider);
69
+ }
70
+ };
71
+ });
package/src/index.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import {
2
2
  ActiveCellManager,
3
- AutocompletionRegistry,
4
3
  buildChatSidebar,
5
4
  buildErrorWidget,
5
+ ChatCommandRegistry,
6
6
  IActiveCellManager,
7
- IAutocompletionCommandsProps,
8
- IAutocompletionRegistry
7
+ IChatCommandRegistry
9
8
  } from '@jupyter/chat';
10
9
  import {
11
10
  JupyterFrontEnd,
@@ -18,53 +17,38 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
18
17
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
19
18
  import { IFormRendererRegistry } from '@jupyterlab/ui-components';
20
19
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
20
+ import { ISecretsManager } from 'jupyter-secrets-manager';
21
21
 
22
22
  import { ChatHandler } from './chat-handler';
23
23
  import { CompletionProvider } from './completion-provider';
24
- import { AIProviders } from './llm-models';
24
+ import { defaultProviderPlugins } from './default-providers';
25
25
  import { AIProviderRegistry } from './provider';
26
26
  import { aiSettingsRenderer } from './settings/panel';
27
- import { renderSlashCommandOption } from './slash-commands';
28
27
  import { IAIProviderRegistry } from './tokens';
29
28
 
30
- const autocompletionRegistryPlugin: JupyterFrontEndPlugin<IAutocompletionRegistry> =
31
- {
32
- id: '@jupyterlite/ai:autocompletion-registry',
33
- description: 'Autocompletion registry',
34
- autoStart: true,
35
- provides: IAutocompletionRegistry,
36
- activate: () => {
37
- const autocompletionRegistry = new AutocompletionRegistry();
38
- const options = ['/clear'];
39
- const autocompletionCommands: IAutocompletionCommandsProps = {
40
- opener: '/',
41
- commands: options.map(option => {
42
- return {
43
- id: option.slice(1),
44
- label: option,
45
- description: 'Clear the chat window'
46
- };
47
- }),
48
- props: {
49
- renderOption: renderSlashCommandOption
50
- }
51
- };
52
- autocompletionRegistry.add('jupyterlite-ai', autocompletionCommands);
53
- return autocompletionRegistry;
54
- }
55
- };
29
+ const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
30
+ id: '@jupyterlite/ai:autocompletion-registry',
31
+ description: 'Autocompletion registry',
32
+ autoStart: true,
33
+ provides: IChatCommandRegistry,
34
+ activate: () => {
35
+ const registry = new ChatCommandRegistry();
36
+ registry.addProvider(new ChatHandler.ClearCommandProvider());
37
+ return registry;
38
+ }
39
+ };
56
40
 
57
41
  const chatPlugin: JupyterFrontEndPlugin<void> = {
58
42
  id: '@jupyterlite/ai:chat',
59
43
  description: 'LLM chat extension',
60
44
  autoStart: true,
61
- requires: [IAIProviderRegistry, IRenderMimeRegistry, IAutocompletionRegistry],
45
+ requires: [IAIProviderRegistry, IRenderMimeRegistry, IChatCommandRegistry],
62
46
  optional: [INotebookTracker, ISettingRegistry, IThemeManager],
63
47
  activate: async (
64
48
  app: JupyterFrontEnd,
65
49
  providerRegistry: IAIProviderRegistry,
66
50
  rmRegistry: IRenderMimeRegistry,
67
- autocompletionRegistry: IAutocompletionRegistry,
51
+ chatCommandRegistry: IChatCommandRegistry,
68
52
  notebookTracker: INotebookTracker | null,
69
53
  settingsRegistry: ISettingRegistry | null,
70
54
  themeManager: IThemeManager | null
@@ -120,7 +104,7 @@ const chatPlugin: JupyterFrontEndPlugin<void> = {
120
104
  model: chatHandler,
121
105
  themeManager,
122
106
  rmRegistry,
123
- autocompletionRegistry
107
+ chatCommandRegistry
124
108
  });
125
109
  chatWidget.title.caption = 'Jupyterlite AI Chat';
126
110
  } catch (e) {
@@ -154,19 +138,20 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
154
138
  id: '@jupyterlite/ai:provider-registry',
155
139
  autoStart: true,
156
140
  requires: [IFormRendererRegistry, ISettingRegistry],
157
- optional: [IRenderMimeRegistry],
141
+ optional: [IRenderMimeRegistry, ISecretsManager],
158
142
  provides: IAIProviderRegistry,
159
143
  activate: (
160
144
  app: JupyterFrontEnd,
161
145
  editorRegistry: IFormRendererRegistry,
162
146
  settingRegistry: ISettingRegistry,
163
- rmRegistry?: IRenderMimeRegistry
147
+ rmRegistry?: IRenderMimeRegistry,
148
+ secretsManager?: ISecretsManager
164
149
  ): IAIProviderRegistry => {
165
150
  const providerRegistry = new AIProviderRegistry();
166
151
 
167
152
  editorRegistry.addRenderer(
168
153
  '@jupyterlite/ai:provider-registry.AIprovider',
169
- aiSettingsRenderer({ providerRegistry, rmRegistry })
154
+ aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
170
155
  );
171
156
  settingRegistry
172
157
  .load(providerRegistryPlugin.id)
@@ -192,16 +177,14 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
192
177
  );
193
178
  });
194
179
 
195
- // Initialize the registry with the default providers
196
- AIProviders.forEach(provider => providerRegistry.add(provider));
197
-
198
180
  return providerRegistry;
199
181
  }
200
182
  };
201
183
 
202
184
  export default [
203
185
  providerRegistryPlugin,
204
- autocompletionRegistryPlugin,
186
+ chatCommandRegistryPlugin,
205
187
  chatPlugin,
206
- completerPlugin
188
+ completerPlugin,
189
+ ...defaultProviderPlugins
207
190
  ];
package/src/provider.ts CHANGED
@@ -4,7 +4,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
4
4
  import { ISignal, Signal } from '@lumino/signaling';
5
5
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
6
6
 
7
- import { IBaseCompleter } from './llm-models';
7
+ import { IBaseCompleter } from './base-completer';
8
8
  import { IAIProvider, IAIProviderRegistry } from './tokens';
9
9
  import { JSONSchema7 } from 'json-schema';
10
10
 
@@ -1,16 +1,19 @@
1
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
2
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
3
3
  import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
4
+ import { ArrayExt } from '@lumino/algorithm';
4
5
  import { JSONExt } from '@lumino/coreutils';
5
6
  import { IChangeEvent } from '@rjsf/core';
6
7
  import type { FieldProps } from '@rjsf/utils';
7
8
  import validator from '@rjsf/validator-ajv8';
8
9
  import { JSONSchema7 } from 'json-schema';
10
+ import { ISecretsManager } from 'jupyter-secrets-manager';
9
11
  import React from 'react';
10
12
 
11
- import baseSettings from './schemas/base.json';
13
+ import baseSettings from './base.json';
12
14
  import { IAIProviderRegistry, IDict } from '../tokens';
13
15
 
16
+ const SECRETS_NAMESPACE = '@jupyterlite/ai';
14
17
  const MD_MIME_TYPE = 'text/markdown';
15
18
  const STORAGE_NAME = '@jupyterlite/ai:settings';
16
19
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
@@ -18,6 +21,7 @@ const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
18
21
  export const aiSettingsRenderer = (options: {
19
22
  providerRegistry: IAIProviderRegistry;
20
23
  rmRegistry?: IRenderMimeRegistry;
24
+ secretsManager?: ISecretsManager;
21
25
  }): IFormRenderer => {
22
26
  return {
23
27
  fieldRenderer: (props: FieldProps) => {
@@ -49,8 +53,12 @@ export class AiSettings extends React.Component<
49
53
  }
50
54
  this._providerRegistry = props.formContext.providerRegistry;
51
55
  this._rmRegistry = props.formContext.rmRegistry ?? null;
56
+ this._secretsManager = props.formContext.secretsManager ?? null;
52
57
  this._settings = props.formContext.settings;
53
58
 
59
+ this._useSecretsManager =
60
+ (this._settings.get('UseSecretsManager').composite as boolean) ?? true;
61
+
54
62
  // Initialize the providers schema.
55
63
  const providerSchema = JSONExt.deepCopy(baseSettings) as any;
56
64
  providerSchema.properties.provider = {
@@ -95,6 +103,44 @@ export class AiSettings extends React.Component<
95
103
  this._settings
96
104
  .set('AIprovider', this._currentSettings)
97
105
  .catch(console.error);
106
+
107
+ this._settings.changed.connect(() => {
108
+ const useSecretsManager =
109
+ (this._settings.get('UseSecretsManager').composite as boolean) ?? true;
110
+ if (useSecretsManager !== this._useSecretsManager) {
111
+ this.updateUseSecretsManager(useSecretsManager);
112
+ }
113
+ });
114
+ }
115
+
116
+ async componentDidUpdate(): Promise<void> {
117
+ if (!this._secretsManager || !this._useSecretsManager) {
118
+ return;
119
+ }
120
+ // Attach the password inputs to the secrets manager only if they have changed.
121
+ const inputs = this._formRef.current?.getElementsByTagName('input') || [];
122
+ if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
123
+ return;
124
+ }
125
+
126
+ await this._secretsManager.detachAll(SECRETS_NAMESPACE);
127
+ this._formInputs = [...inputs];
128
+ this._unsavedFields = [];
129
+ for (let i = 0; i < inputs.length; i++) {
130
+ if (inputs[i].type.toLowerCase() === 'password') {
131
+ const label = inputs[i].getAttribute('label');
132
+ if (label) {
133
+ const id = `${this._provider}-${label}`;
134
+ this._secretsManager.attach(
135
+ SECRETS_NAMESPACE,
136
+ id,
137
+ inputs[i],
138
+ (value: string) => this._onPasswordUpdated(label, value)
139
+ );
140
+ this._unsavedFields.push(label);
141
+ }
142
+ }
143
+ }
98
144
  }
99
145
 
100
146
  /**
@@ -126,11 +172,38 @@ export class AiSettings extends React.Component<
126
172
  * Save settings in local storage for a given provider.
127
173
  */
128
174
  saveSettings(value: IDict<any>) {
175
+ const currentSettings = { ...value };
129
176
  const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) ?? '{}');
130
- settings[this._provider] = value;
177
+ this._unsavedFields.forEach(field => delete currentSettings[field]);
178
+ settings[this._provider] = currentSettings;
131
179
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
132
180
  }
133
181
 
182
+ private updateUseSecretsManager = (value: boolean) => {
183
+ this._useSecretsManager = value;
184
+ if (!value) {
185
+ // Detach all the password inputs attached to the secrets manager, and save the
186
+ // current settings to the local storage to save the password.
187
+ this._secretsManager?.detachAll(SECRETS_NAMESPACE);
188
+ this._formInputs = [];
189
+ this._unsavedFields = [];
190
+ this.saveSettings(this._currentSettings);
191
+ } else {
192
+ // Remove all the keys stored locally and attach the password inputs to the
193
+ // secrets manager.
194
+ const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
195
+ Object.keys(settings).forEach(provider => {
196
+ Object.keys(settings[provider])
197
+ .filter(key => key.toLowerCase().includes('key'))
198
+ .forEach(key => {
199
+ delete settings[provider][key];
200
+ });
201
+ });
202
+ localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
203
+ this.componentDidUpdate();
204
+ }
205
+ };
206
+
134
207
  /**
135
208
  * Update the UI schema of the form.
136
209
  * Currently use to hide API keys.
@@ -206,6 +279,17 @@ export class AiSettings extends React.Component<
206
279
  .catch(console.error);
207
280
  };
208
281
 
282
+ /**
283
+ * Callback function called when the password input has been programmatically updated
284
+ * with the secret manager.
285
+ */
286
+ private _onPasswordUpdated = (fieldName: string, value: string) => {
287
+ this._currentSettings[fieldName] = value;
288
+ this._settings
289
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
290
+ .catch(console.error);
291
+ };
292
+
209
293
  /**
210
294
  * Triggered when the form value has changed, to update the current settings and save
211
295
  * it in local storage.
@@ -221,7 +305,7 @@ export class AiSettings extends React.Component<
221
305
 
222
306
  render(): JSX.Element {
223
307
  return (
224
- <>
308
+ <div ref={this._formRef}>
225
309
  <WrappedFormComponent
226
310
  formData={{ provider: this._provider }}
227
311
  schema={this._providerSchema}
@@ -243,15 +327,20 @@ export class AiSettings extends React.Component<
243
327
  onChange={this._onFormChange}
244
328
  uiSchema={this._uiSchema}
245
329
  />
246
- </>
330
+ </div>
247
331
  );
248
332
  }
249
333
 
250
334
  private _providerRegistry: IAIProviderRegistry;
251
335
  private _provider: string;
252
336
  private _providerSchema: JSONSchema7;
337
+ private _useSecretsManager: boolean;
253
338
  private _rmRegistry: IRenderMimeRegistry | null;
339
+ private _secretsManager: ISecretsManager | null;
254
340
  private _currentSettings: IDict<any> = { provider: 'None' };
255
341
  private _uiSchema: IDict<any> = {};
256
342
  private _settings: ISettingRegistry.ISettings;
343
+ private _formRef = React.createRef<HTMLDivElement>();
344
+ private _unsavedFields: string[] = [];
345
+ private _formInputs: HTMLInputElement[] = [];
257
346
  }
package/src/tokens.ts CHANGED
@@ -3,7 +3,7 @@ import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
3
3
  import { ISignal } from '@lumino/signaling';
4
4
  import { JSONSchema7 } from 'json-schema';
5
5
 
6
- import { IBaseCompleter } from './llm-models';
6
+ import { IBaseCompleter } from './base-completer';
7
7
 
8
8
  export interface IDict<T = any> {
9
9
  [key: string]: T;
@@ -1,4 +0,0 @@
1
- import { IAIProvider } from '../tokens';
2
- export * from './base-completer';
3
- declare const AIProviders: IAIProvider[];
4
- export { AIProviders };
@@ -1,43 +0,0 @@
1
- import { ChatAnthropic } from '@langchain/anthropic';
2
- import { ChromeAI } from '@langchain/community/experimental/llms/chrome_ai';
3
- import { ChatMistralAI } from '@langchain/mistralai';
4
- import { ChatOpenAI } from '@langchain/openai';
5
- import { AnthropicCompleter } from './anthropic-completer';
6
- import { CodestralCompleter } from './codestral-completer';
7
- import { ChromeCompleter } from './chrome-completer';
8
- import { OpenAICompleter } from './openai-completer';
9
- import { instructions } from '../settings/instructions';
10
- import { ProviderSettings } from '../settings/schemas';
11
- export * from './base-completer';
12
- const AIProviders = [
13
- {
14
- name: 'Anthropic',
15
- chatModel: ChatAnthropic,
16
- completer: AnthropicCompleter,
17
- settingsSchema: ProviderSettings.Anthropic,
18
- errorMessage: (error) => error.error.error.message
19
- },
20
- {
21
- name: 'ChromeAI',
22
- // TODO: fix
23
- // @ts-expect-error: missing properties
24
- chatModel: ChromeAI,
25
- completer: ChromeCompleter,
26
- instructions: instructions.ChromeAI,
27
- settingsSchema: ProviderSettings.ChromeAI
28
- },
29
- {
30
- name: 'MistralAI',
31
- chatModel: ChatMistralAI,
32
- completer: CodestralCompleter,
33
- instructions: instructions.MistralAI,
34
- settingsSchema: ProviderSettings.MistralAI
35
- },
36
- {
37
- name: 'OpenAI',
38
- chatModel: ChatOpenAI,
39
- completer: OpenAICompleter,
40
- settingsSchema: ProviderSettings.OpenAI
41
- }
42
- ];
43
- export { AIProviders };
@@ -1,2 +0,0 @@
1
- import { IDict } from '../tokens';
2
- export declare const instructions: IDict;
@@ -1,44 +0,0 @@
1
- const chromeAiInstructions = `
2
- <i class="fas fa-exclamation-triangle"></i> Support for ChromeAI is still experimental and only available in Google Chrome.
3
-
4
- You can test ChromeAI is enabled in your browser by going to the following URL: https://chromeai.org/
5
-
6
- Enable the proper flags in Google Chrome.
7
-
8
- - chrome://flags/#prompt-api-for-gemini-nano
9
- - Select: \`Enabled\`
10
- - chrome://flags/#optimization-guide-on-device-model
11
- - Select: \`Enabled BypassPrefRequirement\`
12
- - chrome://components
13
- - Click \`Check for Update\` on Optimization Guide On Device Model to download the model
14
- - [Optional] chrome://flags/#text-safety-classifier
15
-
16
- <img src="https://github.com/user-attachments/assets/d48f46cc-52ee-4ce5-9eaf-c763cdbee04c" alt="A screenshot showing how to enable the ChromeAI flag in Google Chrome" width="500px">
17
-
18
- Then restart Chrome for these changes to take effect.
19
-
20
- <i class="fas fa-exclamation-triangle"></i> On first use, Chrome will download the on-device model, which can be as large as 22GB (according to their docs and at the time of writing).
21
- During the download, ChromeAI may not be available via the extension.
22
-
23
- <i class="fa fa-info-circle" aria-hidden="true"></i> For more information about Chrome Built-in AI: https://developer.chrome.com/docs/ai/get-started
24
- `;
25
- const mistralAIInstructions = `
26
- <i class="fas fa-exclamation-triangle"></i> This extension is still very much experimental. It is not an official MistralAI extension.
27
-
28
- 1. Go to https://console.mistral.ai/api-keys/ and create an API key.
29
-
30
- <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/1-api-key.png" alt="Screenshot showing how to create an API key" width="500px">
31
-
32
- 2. Open the JupyterLab settings and go to the **Ai providers** section to select the \`MistralAI\`
33
- provider and the API key (required).
34
-
35
- <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/2-jupyterlab-settings.png" alt="Screenshot showing how to add the API key to the settings" width="500px">
36
-
37
- 3. Open the chat, or use the inline completer
38
-
39
- <img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/3-usage.png" alt="Screenshot showing how to use the chat" width="500px">
40
- `;
41
- export const instructions = {
42
- ChromeAI: chromeAiInstructions,
43
- MistralAI: mistralAIInstructions
44
- };