@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
@@ -1,4 +1,4 @@
1
- import { ChatModel, IChatHistory, IChatMessage, INewMessage } from '@jupyter/chat';
1
+ import { ChatCommand, ChatModel, IChatCommandProvider, IChatHistory, IChatMessage, IInputModel, INewMessage } from '@jupyter/chat';
2
2
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
3
3
  import { IAIProviderRegistry } from './tokens';
4
4
  export type ConnectionMessage = {
@@ -33,4 +33,10 @@ export declare namespace ChatHandler {
33
33
  interface IOptions extends ChatModel.IOptions {
34
34
  providerRegistry: IAIProviderRegistry;
35
35
  }
36
+ class ClearCommandProvider implements IChatCommandProvider {
37
+ id: string;
38
+ private _slash_commands;
39
+ getChatCommands(inputModel: IInputModel): Promise<ChatCommand[]>;
40
+ handleChatCommand(command: ChatCommand, inputModel: IInputModel): Promise<void>;
41
+ }
36
42
  }
@@ -142,3 +142,32 @@ export class ChatHandler extends ChatModel {
142
142
  super.messageAdded(message);
143
143
  }
144
144
  }
145
+ (function (ChatHandler) {
146
+ class ClearCommandProvider {
147
+ constructor() {
148
+ this.id = '@jupyterlite/ai:clear-commands';
149
+ this._slash_commands = [
150
+ {
151
+ name: '/clear',
152
+ providerId: this.id,
153
+ replaceWith: '/clear',
154
+ description: 'Clear the chat'
155
+ }
156
+ ];
157
+ }
158
+ async getChatCommands(inputModel) {
159
+ var _a, _b;
160
+ const match = (_b = (_a = inputModel.currentWord) === null || _a === void 0 ? void 0 : _a.match(/^\/\w*/)) === null || _b === void 0 ? void 0 : _b[0];
161
+ if (!match) {
162
+ return [];
163
+ }
164
+ const commands = this._slash_commands.filter(cmd => cmd.name.startsWith(match));
165
+ return commands;
166
+ }
167
+ async handleChatCommand(command, inputModel) {
168
+ // no handling needed because `replaceWith` is set in each command.
169
+ return;
170
+ }
171
+ }
172
+ ChatHandler.ClearCommandProvider = ClearCommandProvider;
173
+ })(ChatHandler || (ChatHandler = {}));
@@ -1,5 +1,5 @@
1
1
  import { CompletionHandler, IInlineCompletionContext, IInlineCompletionProvider } from '@jupyterlab/completer';
2
- import { IBaseCompleter } from './llm-models';
2
+ import { IBaseCompleter } from './base-completer';
3
3
  import { IAIProviderRegistry } from './tokens';
4
4
  /**
5
5
  * The generic completion provider to register to the completion provider manager.
@@ -1,6 +1,6 @@
1
1
  import { CompletionHandler, IInlineCompletionContext } from '@jupyterlab/completer';
2
2
  import { BaseChatModel } from '@langchain/core/language_models/chat_models';
3
- import { BaseCompleter, IBaseCompleter } from './base-completer';
3
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
4
4
  export declare class AnthropicCompleter implements IBaseCompleter {
5
5
  constructor(options: BaseCompleter.IOptions);
6
6
  get provider(): BaseChatModel;
@@ -1,6 +1,6 @@
1
1
  import { ChatAnthropic } from '@langchain/anthropic';
2
2
  import { AIMessage, SystemMessage } from '@langchain/core/messages';
3
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
3
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
4
4
  export class AnthropicCompleter {
5
5
  constructor(options) {
6
6
  this._prompt = COMPLETION_SYSTEM_PROMPT;
@@ -1,6 +1,6 @@
1
1
  import { CompletionHandler, IInlineCompletionContext } from '@jupyterlab/completer';
2
2
  import { LLM } from '@langchain/core/language_models/llms';
3
- import { BaseCompleter, IBaseCompleter } from './base-completer';
3
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
4
4
  export declare class ChromeCompleter implements IBaseCompleter {
5
5
  constructor(options: BaseCompleter.IOptions);
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { ChromeAI } from '@langchain/community/experimental/llms/chrome_ai';
2
2
  import { HumanMessage, SystemMessage } from '@langchain/core/messages';
3
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
3
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
4
4
  /**
5
5
  * Regular expression to match the '```' string at the start of a string.
6
6
  * So the completions returned by the LLM can still be kept after removing the code block formatting.
@@ -0,0 +1,2 @@
1
+ declare const _default: "\n<i class=\"fas fa-exclamation-triangle\"></i> Support for ChromeAI is still experimental and only available in Google Chrome.\n\nYou can test ChromeAI is enabled in your browser by going to the following URL: <https://chromeai.org/>\n\nEnable the proper flags in Google Chrome.\n\n- chrome://flags/#prompt-api-for-gemini-nano\n - Select: `Enabled`\n- chrome://flags/#optimization-guide-on-device-model\n - Select: `Enabled BypassPrefRequirement`\n- chrome://components\n - Click `Check for Update` on Optimization Guide On Device Model to download the model\n- [Optional] chrome://flags/#text-safety-classifier\n\n<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\">\n\nThen restart Chrome for these changes to take effect.\n\n<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).\nDuring the download, ChromeAI may not be available via the extension.\n\n<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>\n";
2
+ export default _default;
@@ -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
+ `;
@@ -1,6 +1,6 @@
1
1
  import { CompletionHandler, IInlineCompletionContext } from '@jupyterlab/completer';
2
2
  import { BaseChatModel } from '@langchain/core/language_models/chat_models';
3
- import { BaseCompleter, IBaseCompleter } from './base-completer';
3
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
4
4
  export declare class CodestralCompleter implements IBaseCompleter {
5
5
  constructor(options: BaseCompleter.IOptions);
6
6
  get provider(): BaseChatModel;
@@ -1,7 +1,7 @@
1
1
  import { HumanMessage, SystemMessage } from '@langchain/core/messages';
2
2
  import { ChatMistralAI } from '@langchain/mistralai';
3
3
  import { Throttler } from '@lumino/polling';
4
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
4
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
5
5
  /**
6
6
  * The Mistral API has a rate limit of 1 request per second
7
7
  */
@@ -0,0 +1,2 @@
1
+ declare const _default: "\n<i class=\"fas fa-exclamation-triangle\"></i> This extension is still very much experimental. It is not an official MistralAI extension.\n\n1. Go to <https://console.mistral.ai/api-keys/> and create an API key.\n\n <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\">\n\n2. Open the JupyterLab settings and go to the **Ai providers** section to select the `MistralAI`\n provider and the API key (required).\n\n <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\">\n\n3. Open the chat, or use the inline completer\n\n <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\">\n";
2
+ export default _default;
@@ -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
+ `;
@@ -1,6 +1,6 @@
1
1
  import { CompletionHandler, IInlineCompletionContext } from '@jupyterlab/completer';
2
2
  import { BaseChatModel } from '@langchain/core/language_models/chat_models';
3
- import { BaseCompleter, IBaseCompleter } from './base-completer';
3
+ import { BaseCompleter, IBaseCompleter } from '../../base-completer';
4
4
  export declare class OpenAICompleter implements IBaseCompleter {
5
5
  constructor(options: BaseCompleter.IOptions);
6
6
  get provider(): BaseChatModel;
@@ -1,6 +1,6 @@
1
1
  import { AIMessage, SystemMessage } from '@langchain/core/messages';
2
2
  import { ChatOpenAI } from '@langchain/openai';
3
- import { COMPLETION_SYSTEM_PROMPT } from '../provider';
3
+ import { COMPLETION_SYSTEM_PROMPT } from '../../provider';
4
4
  export class OpenAICompleter {
5
5
  constructor(options) {
6
6
  this._prompt = COMPLETION_SYSTEM_PROMPT;
@@ -0,0 +1,2 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ export declare const defaultProviderPlugins: JupyterFrontEndPlugin<void>[];
@@ -0,0 +1,60 @@
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 { IAIProviderRegistry } from '../tokens';
6
+ // Import completers
7
+ import { AnthropicCompleter } from './Anthropic/completer';
8
+ import { ChromeCompleter } from './ChromeAI/completer';
9
+ import { CodestralCompleter } from './MistralAI/completer';
10
+ import { OpenAICompleter } from './OpenAI/completer';
11
+ // Import Settings
12
+ import AnthropicSettings from './Anthropic/settings-schema.json';
13
+ import ChromeAISettings from './ChromeAI/settings-schema.json';
14
+ import MistralAISettings from './MistralAI/settings-schema.json';
15
+ import OpenAISettings from './OpenAI/settings-schema.json';
16
+ // Import instructions
17
+ import ChromeAIInstructions from './ChromeAI/instructions';
18
+ import MistralAIInstructions from './MistralAI/instructions';
19
+ // Build the AIProvider list
20
+ const AIProviders = [
21
+ {
22
+ name: 'Anthropic',
23
+ chatModel: ChatAnthropic,
24
+ completer: AnthropicCompleter,
25
+ settingsSchema: AnthropicSettings,
26
+ errorMessage: (error) => error.error.error.message
27
+ },
28
+ {
29
+ name: 'ChromeAI',
30
+ // TODO: fix
31
+ // @ts-expect-error: missing properties
32
+ chatModel: ChromeAI,
33
+ completer: ChromeCompleter,
34
+ instructions: ChromeAIInstructions,
35
+ settingsSchema: ChromeAISettings
36
+ },
37
+ {
38
+ name: 'MistralAI',
39
+ chatModel: ChatMistralAI,
40
+ completer: CodestralCompleter,
41
+ instructions: MistralAIInstructions,
42
+ settingsSchema: MistralAISettings
43
+ },
44
+ {
45
+ name: 'OpenAI',
46
+ chatModel: ChatOpenAI,
47
+ completer: OpenAICompleter,
48
+ settingsSchema: OpenAISettings
49
+ }
50
+ ];
51
+ export const defaultProviderPlugins = AIProviders.map(provider => {
52
+ return {
53
+ id: `@jupyterlite/ai:${provider.name}`,
54
+ autoStart: true,
55
+ requires: [IAIProviderRegistry],
56
+ activate: (app, registry) => {
57
+ registry.add(provider);
58
+ }
59
+ };
60
+ });
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { IAutocompletionRegistry } from '@jupyter/chat';
1
+ import { IChatCommandRegistry } from '@jupyter/chat';
2
2
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
3
3
  import { IAIProviderRegistry } from './tokens';
4
- declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IAutocompletionRegistry> | JupyterFrontEndPlugin<IAIProviderRegistry>)[];
4
+ declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IChatCommandRegistry> | JupyterFrontEndPlugin<IAIProviderRegistry>)[];
5
5
  export default _default;
package/lib/index.js CHANGED
@@ -1,49 +1,35 @@
1
- import { ActiveCellManager, AutocompletionRegistry, buildChatSidebar, buildErrorWidget, IAutocompletionRegistry } from '@jupyter/chat';
1
+ import { ActiveCellManager, buildChatSidebar, buildErrorWidget, ChatCommandRegistry, IChatCommandRegistry } from '@jupyter/chat';
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { ICompletionProviderManager } from '@jupyterlab/completer';
4
4
  import { INotebookTracker } from '@jupyterlab/notebook';
5
5
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
6
6
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
7
7
  import { IFormRendererRegistry } from '@jupyterlab/ui-components';
8
+ import { ISecretsManager } from 'jupyter-secrets-manager';
8
9
  import { ChatHandler } from './chat-handler';
9
10
  import { CompletionProvider } from './completion-provider';
10
- import { AIProviders } from './llm-models';
11
+ import { defaultProviderPlugins } from './default-providers';
11
12
  import { AIProviderRegistry } from './provider';
12
13
  import { aiSettingsRenderer } from './settings/panel';
13
- import { renderSlashCommandOption } from './slash-commands';
14
14
  import { IAIProviderRegistry } from './tokens';
15
- const autocompletionRegistryPlugin = {
15
+ const chatCommandRegistryPlugin = {
16
16
  id: '@jupyterlite/ai:autocompletion-registry',
17
17
  description: 'Autocompletion registry',
18
18
  autoStart: true,
19
- provides: IAutocompletionRegistry,
19
+ provides: IChatCommandRegistry,
20
20
  activate: () => {
21
- const autocompletionRegistry = new AutocompletionRegistry();
22
- const options = ['/clear'];
23
- const autocompletionCommands = {
24
- opener: '/',
25
- commands: options.map(option => {
26
- return {
27
- id: option.slice(1),
28
- label: option,
29
- description: 'Clear the chat window'
30
- };
31
- }),
32
- props: {
33
- renderOption: renderSlashCommandOption
34
- }
35
- };
36
- autocompletionRegistry.add('jupyterlite-ai', autocompletionCommands);
37
- return autocompletionRegistry;
21
+ const registry = new ChatCommandRegistry();
22
+ registry.addProvider(new ChatHandler.ClearCommandProvider());
23
+ return registry;
38
24
  }
39
25
  };
40
26
  const chatPlugin = {
41
27
  id: '@jupyterlite/ai:chat',
42
28
  description: 'LLM chat extension',
43
29
  autoStart: true,
44
- requires: [IAIProviderRegistry, IRenderMimeRegistry, IAutocompletionRegistry],
30
+ requires: [IAIProviderRegistry, IRenderMimeRegistry, IChatCommandRegistry],
45
31
  optional: [INotebookTracker, ISettingRegistry, IThemeManager],
46
- activate: async (app, providerRegistry, rmRegistry, autocompletionRegistry, notebookTracker, settingsRegistry, themeManager) => {
32
+ activate: async (app, providerRegistry, rmRegistry, chatCommandRegistry, notebookTracker, settingsRegistry, themeManager) => {
47
33
  let activeCellManager = null;
48
34
  if (notebookTracker) {
49
35
  activeCellManager = new ActiveCellManager({
@@ -85,7 +71,7 @@ const chatPlugin = {
85
71
  model: chatHandler,
86
72
  themeManager,
87
73
  rmRegistry,
88
- autocompletionRegistry
74
+ chatCommandRegistry
89
75
  });
90
76
  chatWidget.title.caption = 'Jupyterlite AI Chat';
91
77
  }
@@ -112,11 +98,11 @@ const providerRegistryPlugin = {
112
98
  id: '@jupyterlite/ai:provider-registry',
113
99
  autoStart: true,
114
100
  requires: [IFormRendererRegistry, ISettingRegistry],
115
- optional: [IRenderMimeRegistry],
101
+ optional: [IRenderMimeRegistry, ISecretsManager],
116
102
  provides: IAIProviderRegistry,
117
- activate: (app, editorRegistry, settingRegistry, rmRegistry) => {
103
+ activate: (app, editorRegistry, settingRegistry, rmRegistry, secretsManager) => {
118
104
  const providerRegistry = new AIProviderRegistry();
119
- editorRegistry.addRenderer('@jupyterlite/ai:provider-registry.AIprovider', aiSettingsRenderer({ providerRegistry, rmRegistry }));
105
+ editorRegistry.addRenderer('@jupyterlite/ai:provider-registry.AIprovider', aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager }));
120
106
  settingRegistry
121
107
  .load(providerRegistryPlugin.id)
122
108
  .then(settings => {
@@ -134,14 +120,13 @@ const providerRegistryPlugin = {
134
120
  .catch(reason => {
135
121
  console.error(`Failed to load settings for ${providerRegistryPlugin.id}`, reason);
136
122
  });
137
- // Initialize the registry with the default providers
138
- AIProviders.forEach(provider => providerRegistry.add(provider));
139
123
  return providerRegistry;
140
124
  }
141
125
  };
142
126
  export default [
143
127
  providerRegistryPlugin,
144
- autocompletionRegistryPlugin,
128
+ chatCommandRegistryPlugin,
145
129
  chatPlugin,
146
- completerPlugin
130
+ completerPlugin,
131
+ ...defaultProviderPlugins
147
132
  ];
package/lib/provider.d.ts CHANGED
@@ -3,7 +3,7 @@ import { BaseLanguageModel } from '@langchain/core/language_models/base';
3
3
  import { BaseChatModel } from '@langchain/core/language_models/chat_models';
4
4
  import { ISignal } from '@lumino/signaling';
5
5
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
6
- import { IBaseCompleter } from './llm-models';
6
+ import { IBaseCompleter } from './base-completer';
7
7
  import { IAIProvider, IAIProviderRegistry } from './tokens';
8
8
  import { JSONSchema7 } from 'json-schema';
9
9
  export declare const chatSystemPrompt: (options: AIProviderRegistry.IPromptOptions) => string;
@@ -2,11 +2,13 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
2
  import { IFormRenderer } from '@jupyterlab/ui-components';
3
3
  import type { FieldProps } from '@rjsf/utils';
4
4
  import { JSONSchema7 } from 'json-schema';
5
+ import { ISecretsManager } from 'jupyter-secrets-manager';
5
6
  import React from 'react';
6
7
  import { IAIProviderRegistry, IDict } from '../tokens';
7
8
  export declare const aiSettingsRenderer: (options: {
8
9
  providerRegistry: IAIProviderRegistry;
9
10
  rmRegistry?: IRenderMimeRegistry;
11
+ secretsManager?: ISecretsManager;
10
12
  }) => IFormRenderer;
11
13
  export interface ISettingsFormStates {
12
14
  schema: JSONSchema7;
@@ -14,6 +16,7 @@ export interface ISettingsFormStates {
14
16
  }
15
17
  export declare class AiSettings extends React.Component<FieldProps, ISettingsFormStates> {
16
18
  constructor(props: FieldProps);
19
+ componentDidUpdate(): Promise<void>;
17
20
  /**
18
21
  * Get the current provider from the local storage.
19
22
  */
@@ -30,6 +33,7 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
30
33
  * Save settings in local storage for a given provider.
31
34
  */
32
35
  saveSettings(value: IDict<any>): void;
36
+ private updateUseSecretsManager;
33
37
  /**
34
38
  * Update the UI schema of the form.
35
39
  * Currently use to hide API keys.
@@ -53,6 +57,11 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
53
57
  * Update the Jupyterlab settings accordingly.
54
58
  */
55
59
  private _onProviderChanged;
60
+ /**
61
+ * Callback function called when the password input has been programmatically updated
62
+ * with the secret manager.
63
+ */
64
+ private _onPasswordUpdated;
56
65
  /**
57
66
  * Triggered when the form value has changed, to update the current settings and save
58
67
  * it in local storage.
@@ -63,8 +72,13 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
63
72
  private _providerRegistry;
64
73
  private _provider;
65
74
  private _providerSchema;
75
+ private _useSecretsManager;
66
76
  private _rmRegistry;
77
+ private _secretsManager;
67
78
  private _currentSettings;
68
79
  private _uiSchema;
69
80
  private _settings;
81
+ private _formRef;
82
+ private _unsavedFields;
83
+ private _formInputs;
70
84
  }
@@ -1,8 +1,10 @@
1
1
  import { FormComponent } from '@jupyterlab/ui-components';
2
+ import { ArrayExt } from '@lumino/algorithm';
2
3
  import { JSONExt } from '@lumino/coreutils';
3
4
  import validator from '@rjsf/validator-ajv8';
4
5
  import React from 'react';
5
- import baseSettings from './schemas/base.json';
6
+ import baseSettings from './base.json';
7
+ const SECRETS_NAMESPACE = '@jupyterlite/ai';
6
8
  const MD_MIME_TYPE = 'text/markdown';
7
9
  const STORAGE_NAME = '@jupyterlite/ai:settings';
8
10
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
@@ -19,8 +21,34 @@ const WrappedFormComponent = (props) => {
19
21
  };
20
22
  export class AiSettings extends React.Component {
21
23
  constructor(props) {
22
- var _a, _b;
24
+ var _a, _b, _c, _d;
23
25
  super(props);
26
+ this.updateUseSecretsManager = (value) => {
27
+ var _a;
28
+ this._useSecretsManager = value;
29
+ if (!value) {
30
+ // Detach all the password inputs attached to the secrets manager, and save the
31
+ // current settings to the local storage to save the password.
32
+ (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(SECRETS_NAMESPACE);
33
+ this._formInputs = [];
34
+ this._unsavedFields = [];
35
+ this.saveSettings(this._currentSettings);
36
+ }
37
+ else {
38
+ // Remove all the keys stored locally and attach the password inputs to the
39
+ // secrets manager.
40
+ const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
41
+ Object.keys(settings).forEach(provider => {
42
+ Object.keys(settings[provider])
43
+ .filter(key => key.toLowerCase().includes('key'))
44
+ .forEach(key => {
45
+ delete settings[provider][key];
46
+ });
47
+ });
48
+ localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
49
+ this.componentDidUpdate();
50
+ }
51
+ };
24
52
  /**
25
53
  * Triggered when the provider hes changed, to update the schema and values.
26
54
  * Update the Jupyterlab settings accordingly.
@@ -39,6 +67,16 @@ export class AiSettings extends React.Component {
39
67
  .set('AIprovider', { provider: this._provider, ...this._currentSettings })
40
68
  .catch(console.error);
41
69
  };
70
+ /**
71
+ * Callback function called when the password input has been programmatically updated
72
+ * with the secret manager.
73
+ */
74
+ this._onPasswordUpdated = (fieldName, value) => {
75
+ this._currentSettings[fieldName] = value;
76
+ this._settings
77
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
78
+ .catch(console.error);
79
+ };
42
80
  /**
43
81
  * Triggered when the form value has changed, to update the current settings and save
44
82
  * it in local storage.
@@ -53,12 +91,18 @@ export class AiSettings extends React.Component {
53
91
  };
54
92
  this._currentSettings = { provider: 'None' };
55
93
  this._uiSchema = {};
94
+ this._formRef = React.createRef();
95
+ this._unsavedFields = [];
96
+ this._formInputs = [];
56
97
  if (!props.formContext.providerRegistry) {
57
98
  throw new Error('The provider registry is needed to enable the jupyterlite-ai settings panel');
58
99
  }
59
100
  this._providerRegistry = props.formContext.providerRegistry;
60
101
  this._rmRegistry = (_a = props.formContext.rmRegistry) !== null && _a !== void 0 ? _a : null;
102
+ this._secretsManager = (_b = props.formContext.secretsManager) !== null && _b !== void 0 ? _b : null;
61
103
  this._settings = props.formContext.settings;
104
+ this._useSecretsManager =
105
+ (_c = this._settings.get('UseSecretsManager').composite) !== null && _c !== void 0 ? _c : true;
62
106
  // Initialize the providers schema.
63
107
  const providerSchema = JSONExt.deepCopy(baseSettings);
64
108
  providerSchema.properties.provider = {
@@ -76,7 +120,7 @@ export class AiSettings extends React.Component {
76
120
  const labSettings = this._settings.get('AIprovider').composite;
77
121
  if (labSettings && Object.keys(labSettings).includes('provider')) {
78
122
  // Get the provider name.
79
- const provider = (_b = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _b === void 0 ? void 0 : _b[1];
123
+ const provider = (_d = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _d === void 0 ? void 0 : _d[1];
80
124
  // Save the settings.
81
125
  const settings = {
82
126
  _current: provider
@@ -96,6 +140,37 @@ export class AiSettings extends React.Component {
96
140
  this._settings
97
141
  .set('AIprovider', this._currentSettings)
98
142
  .catch(console.error);
143
+ this._settings.changed.connect(() => {
144
+ var _a;
145
+ const useSecretsManager = (_a = this._settings.get('UseSecretsManager').composite) !== null && _a !== void 0 ? _a : true;
146
+ if (useSecretsManager !== this._useSecretsManager) {
147
+ this.updateUseSecretsManager(useSecretsManager);
148
+ }
149
+ });
150
+ }
151
+ async componentDidUpdate() {
152
+ var _a;
153
+ if (!this._secretsManager || !this._useSecretsManager) {
154
+ return;
155
+ }
156
+ // Attach the password inputs to the secrets manager only if they have changed.
157
+ const inputs = ((_a = this._formRef.current) === null || _a === void 0 ? void 0 : _a.getElementsByTagName('input')) || [];
158
+ if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
159
+ return;
160
+ }
161
+ await this._secretsManager.detachAll(SECRETS_NAMESPACE);
162
+ this._formInputs = [...inputs];
163
+ this._unsavedFields = [];
164
+ for (let i = 0; i < inputs.length; i++) {
165
+ if (inputs[i].type.toLowerCase() === 'password') {
166
+ const label = inputs[i].getAttribute('label');
167
+ if (label) {
168
+ const id = `${this._provider}-${label}`;
169
+ this._secretsManager.attach(SECRETS_NAMESPACE, id, inputs[i], (value) => this._onPasswordUpdated(label, value));
170
+ this._unsavedFields.push(label);
171
+ }
172
+ }
173
+ }
99
174
  }
100
175
  /**
101
176
  * Get the current provider from the local storage.
@@ -126,8 +201,10 @@ export class AiSettings extends React.Component {
126
201
  */
127
202
  saveSettings(value) {
128
203
  var _a;
204
+ const currentSettings = { ...value };
129
205
  const settings = JSON.parse((_a = localStorage.getItem(STORAGE_NAME)) !== null && _a !== void 0 ? _a : '{}');
130
- settings[this._provider] = value;
206
+ this._unsavedFields.forEach(field => delete currentSettings[field]);
207
+ settings[this._provider] = currentSettings;
131
208
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
132
209
  }
133
210
  /**
@@ -180,7 +257,7 @@ export class AiSettings extends React.Component {
180
257
  this.setState({ instruction: renderer.node });
181
258
  }
182
259
  render() {
183
- return (React.createElement(React.Fragment, null,
260
+ return (React.createElement("div", { ref: this._formRef },
184
261
  React.createElement(WrappedFormComponent, { formData: { provider: this._provider }, schema: this._providerSchema, onChange: this._onProviderChanged }),
185
262
  this.state.instruction !== null && (React.createElement("details", null,
186
263
  React.createElement("summary", { className: INSTRUCTION_CLASS }, "Instructions"),
package/lib/tokens.d.ts CHANGED
@@ -2,7 +2,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
2
2
  import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
3
3
  import { ISignal } from '@lumino/signaling';
4
4
  import { JSONSchema7 } from 'json-schema';
5
- import { IBaseCompleter } from './llm-models';
5
+ import { IBaseCompleter } from './base-completer';
6
6
  export interface IDict<T = any> {
7
7
  [key: string]: T;
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -14,7 +14,7 @@
14
14
  "license": "BSD-3-Clause",
15
15
  "author": "JupyterLite Contributors",
16
16
  "files": [
17
- "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
17
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf,md}",
18
18
  "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
19
19
  "src/**/*.{ts,tsx}",
20
20
  "schema/*.json"
@@ -27,9 +27,9 @@
27
27
  "url": "https://github.com/jupyterlite/ai.git"
28
28
  },
29
29
  "scripts": {
30
- "build": "node ./scripts/settings-generator.js && jlpm build:lib && jlpm build:labextension:dev",
30
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
31
31
  "build:dev": "jlpm build:lib && jlpm build:labextension:dev",
32
- "build:prod": "node ./scripts/settings-generator.js && jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
32
+ "build:prod": "jlpm settings:build && jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
33
33
  "build:labextension": "jupyter labextension build .",
34
34
  "build:labextension:dev": "jupyter labextension build --development True .",
35
35
  "build:lib": "tsc --sourceMap",
@@ -47,6 +47,8 @@
47
47
  "prettier": "jlpm prettier:base --write --list-different",
48
48
  "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
49
49
  "prettier:check": "jlpm prettier:base --check",
50
+ "settings:build": "node ./scripts/settings-checker.js --generate",
51
+ "settings:check": "node ./scripts/settings-checker.js",
50
52
  "stylelint": "jlpm stylelint:check --fix",
51
53
  "stylelint:check": "stylelint --cache \"style/**/*.css\"",
52
54
  "watch": "run-p watch:src watch:labextension",
@@ -54,7 +56,7 @@
54
56
  "watch:labextension": "jupyter labextension watch ."
55
57
  },
56
58
  "dependencies": {
57
- "@jupyter/chat": "^0.7.1",
59
+ "@jupyter/chat": "^0.8.1",
58
60
  "@jupyterlab/application": "^4.4.0-alpha.0",
59
61
  "@jupyterlab/apputils": "^4.5.0-alpha.0",
60
62
  "@jupyterlab/completer": "^4.4.0-alpha.0",
@@ -75,6 +77,7 @@
75
77
  "@rjsf/core": "^4.2.0",
76
78
  "@rjsf/utils": "^5.18.4",
77
79
  "@rjsf/validator-ajv8": "^5.18.4",
80
+ "jupyter-secrets-manager": "^0.1.1",
78
81
  "react": "^18.2.0",
79
82
  "react-dom": "^18.2.0"
80
83
  },