@jupyterlite/ai 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat-handler.d.ts +2 -0
- package/lib/chat-handler.js +8 -1
- package/lib/completion-provider.d.ts +7 -0
- package/lib/components/stop-button.d.ts +19 -0
- package/lib/components/stop-button.js +32 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +56 -16
- package/lib/provider.d.ts +15 -10
- package/lib/provider.js +67 -9
- package/lib/settings/index.d.ts +3 -0
- package/lib/settings/index.js +3 -0
- package/lib/settings/panel.d.ts +6 -5
- package/lib/settings/panel.js +66 -19
- package/lib/settings/settings-connector.d.ts +31 -0
- package/lib/settings/settings-connector.js +61 -0
- package/lib/settings/utils.d.ts +2 -0
- package/lib/settings/utils.js +4 -0
- package/lib/tokens.d.ts +22 -3
- package/lib/tokens.js +7 -0
- package/package.json +17 -12
- package/schema/provider-registry.json +6 -0
- package/src/chat-handler.ts +9 -1
- package/src/completion-provider.ts +7 -0
- package/src/components/stop-button.tsx +56 -0
- package/src/index.ts +102 -52
- package/src/provider.ts +84 -12
- package/src/settings/index.ts +3 -0
- package/src/settings/panel.tsx +72 -17
- package/src/settings/settings-connector.ts +89 -0
- package/src/settings/utils.ts +5 -0
- package/src/tokens.ts +24 -3
package/src/index.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
buildErrorWidget,
|
|
5
5
|
ChatCommandRegistry,
|
|
6
6
|
IActiveCellManager,
|
|
7
|
-
IChatCommandRegistry
|
|
7
|
+
IChatCommandRegistry,
|
|
8
|
+
InputToolbarRegistry
|
|
8
9
|
} from '@jupyter/chat';
|
|
9
10
|
import {
|
|
10
11
|
JupyterFrontEnd,
|
|
@@ -14,20 +15,24 @@ import { ReactWidget, IThemeManager } from '@jupyterlab/apputils';
|
|
|
14
15
|
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
15
16
|
import { INotebookTracker } from '@jupyterlab/notebook';
|
|
16
17
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ISettingConnector,
|
|
20
|
+
ISettingRegistry
|
|
21
|
+
} from '@jupyterlab/settingregistry';
|
|
18
22
|
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
|
|
19
23
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
20
|
-
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
24
|
+
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
21
25
|
|
|
22
26
|
import { ChatHandler } from './chat-handler';
|
|
23
27
|
import { CompletionProvider } from './completion-provider';
|
|
24
28
|
import { defaultProviderPlugins } from './default-providers';
|
|
25
29
|
import { AIProviderRegistry } from './provider';
|
|
26
|
-
import { aiSettingsRenderer } from './settings
|
|
27
|
-
import { IAIProviderRegistry } from './tokens';
|
|
30
|
+
import { aiSettingsRenderer, SettingConnector } from './settings';
|
|
31
|
+
import { IAIProviderRegistry, PLUGIN_IDS } from './tokens';
|
|
32
|
+
import { stopItem } from './components/stop-button';
|
|
28
33
|
|
|
29
34
|
const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
|
|
30
|
-
id:
|
|
35
|
+
id: PLUGIN_IDS.chatCommandRegistry,
|
|
31
36
|
description: 'Autocompletion registry',
|
|
32
37
|
autoStart: true,
|
|
33
38
|
provides: IChatCommandRegistry,
|
|
@@ -39,7 +44,7 @@ const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
|
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
const chatPlugin: JupyterFrontEndPlugin<void> = {
|
|
42
|
-
id:
|
|
47
|
+
id: PLUGIN_IDS.chat,
|
|
43
48
|
description: 'LLM chat extension',
|
|
44
49
|
autoStart: true,
|
|
45
50
|
requires: [IAIProviderRegistry, IRenderMimeRegistry, IChatCommandRegistry],
|
|
@@ -99,12 +104,30 @@ const chatPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
let chatWidget: ReactWidget | null = null;
|
|
107
|
+
|
|
108
|
+
const inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
109
|
+
const stopButton = stopItem(() => chatHandler.stopStreaming());
|
|
110
|
+
inputToolbarRegistry.addItem('stop', stopButton);
|
|
111
|
+
|
|
112
|
+
chatHandler.writersChanged.connect((_, users) => {
|
|
113
|
+
if (
|
|
114
|
+
users.filter(user => user.username === chatHandler.personaName).length
|
|
115
|
+
) {
|
|
116
|
+
inputToolbarRegistry.hide('send');
|
|
117
|
+
inputToolbarRegistry.show('stop');
|
|
118
|
+
} else {
|
|
119
|
+
inputToolbarRegistry.hide('stop');
|
|
120
|
+
inputToolbarRegistry.show('send');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
102
124
|
try {
|
|
103
125
|
chatWidget = buildChatSidebar({
|
|
104
126
|
model: chatHandler,
|
|
105
127
|
themeManager,
|
|
106
128
|
rmRegistry,
|
|
107
|
-
chatCommandRegistry
|
|
129
|
+
chatCommandRegistry,
|
|
130
|
+
inputToolbarRegistry
|
|
108
131
|
});
|
|
109
132
|
chatWidget.title.caption = 'Jupyterlite AI Chat';
|
|
110
133
|
} catch (e) {
|
|
@@ -118,7 +141,7 @@ const chatPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
118
141
|
};
|
|
119
142
|
|
|
120
143
|
const completerPlugin: JupyterFrontEndPlugin<void> = {
|
|
121
|
-
id:
|
|
144
|
+
id: PLUGIN_IDS.completer,
|
|
122
145
|
autoStart: true,
|
|
123
146
|
requires: [IAIProviderRegistry, ICompletionProviderManager],
|
|
124
147
|
activate: (
|
|
@@ -134,51 +157,77 @@ const completerPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
134
157
|
}
|
|
135
158
|
};
|
|
136
159
|
|
|
137
|
-
const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
settingRegistry
|
|
157
|
-
.load(providerRegistryPlugin.id)
|
|
158
|
-
.then(settings => {
|
|
159
|
-
const updateProvider = () => {
|
|
160
|
-
// Update the settings to the AI providers.
|
|
161
|
-
const providerSettings = (settings.get('AIprovider').composite ?? {
|
|
162
|
-
provider: 'None'
|
|
163
|
-
}) as ReadonlyPartialJSONObject;
|
|
164
|
-
providerRegistry.setProvider(
|
|
165
|
-
providerSettings.provider as string,
|
|
166
|
-
providerSettings
|
|
167
|
-
);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
settings.changed.connect(() => updateProvider());
|
|
171
|
-
updateProvider();
|
|
172
|
-
})
|
|
173
|
-
.catch(reason => {
|
|
174
|
-
console.error(
|
|
175
|
-
`Failed to load settings for ${providerRegistryPlugin.id}`,
|
|
176
|
-
reason
|
|
177
|
-
);
|
|
160
|
+
const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> =
|
|
161
|
+
SecretsManager.sign(PLUGIN_IDS.providerRegistry, token => ({
|
|
162
|
+
id: PLUGIN_IDS.providerRegistry,
|
|
163
|
+
autoStart: true,
|
|
164
|
+
requires: [IFormRendererRegistry, ISettingRegistry],
|
|
165
|
+
optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector],
|
|
166
|
+
provides: IAIProviderRegistry,
|
|
167
|
+
activate: (
|
|
168
|
+
app: JupyterFrontEnd,
|
|
169
|
+
editorRegistry: IFormRendererRegistry,
|
|
170
|
+
settingRegistry: ISettingRegistry,
|
|
171
|
+
rmRegistry?: IRenderMimeRegistry,
|
|
172
|
+
secretsManager?: ISecretsManager,
|
|
173
|
+
settingConnector?: ISettingConnector
|
|
174
|
+
): IAIProviderRegistry => {
|
|
175
|
+
const providerRegistry = new AIProviderRegistry({
|
|
176
|
+
token,
|
|
177
|
+
secretsManager
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
editorRegistry.addRenderer(
|
|
181
|
+
`${PLUGIN_IDS.providerRegistry}.AIprovider`,
|
|
182
|
+
aiSettingsRenderer({
|
|
183
|
+
providerRegistry,
|
|
184
|
+
secretsToken: token,
|
|
185
|
+
rmRegistry,
|
|
186
|
+
secretsManager,
|
|
187
|
+
settingConnector
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
settingRegistry
|
|
192
|
+
.load(providerRegistryPlugin.id)
|
|
193
|
+
.then(settings => {
|
|
194
|
+
const updateProvider = () => {
|
|
195
|
+
// Update the settings to the AI providers.
|
|
196
|
+
const providerSettings = (settings.get('AIprovider').composite ?? {
|
|
197
|
+
provider: 'None'
|
|
198
|
+
}) as ReadonlyPartialJSONObject;
|
|
199
|
+
providerRegistry.setProvider({
|
|
200
|
+
name: providerSettings.provider as string,
|
|
201
|
+
settings: providerSettings
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
settings.changed.connect(() => updateProvider());
|
|
206
|
+
updateProvider();
|
|
207
|
+
})
|
|
208
|
+
.catch(reason => {
|
|
209
|
+
console.error(
|
|
210
|
+
`Failed to load settings for ${providerRegistryPlugin.id}`,
|
|
211
|
+
reason
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return providerRegistry;
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Provides the settings connector as a separate plugin to allow for alternative
|
|
221
|
+
* implementations that may want to fetch settings from a different source or
|
|
222
|
+
* endpoint.
|
|
223
|
+
*/
|
|
224
|
+
const settingsConnector: JupyterFrontEndPlugin<ISettingConnector> = {
|
|
225
|
+
id: PLUGIN_IDS.settingsConnector,
|
|
226
|
+
description: 'Provides a settings connector which does not save passwords.',
|
|
227
|
+
autoStart: true,
|
|
228
|
+
provides: ISettingConnector,
|
|
229
|
+
activate: (app: JupyterFrontEnd) =>
|
|
230
|
+
new SettingConnector(app.serviceManager.settings)
|
|
182
231
|
};
|
|
183
232
|
|
|
184
233
|
export default [
|
|
@@ -186,5 +235,6 @@ export default [
|
|
|
186
235
|
chatCommandRegistryPlugin,
|
|
187
236
|
chatPlugin,
|
|
188
237
|
completerPlugin,
|
|
238
|
+
settingsConnector,
|
|
189
239
|
...defaultProviderPlugins
|
|
190
240
|
];
|
package/src/provider.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
2
1
|
import { BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
3
2
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
4
3
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
5
4
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
5
|
+
import { JSONSchema7 } from 'json-schema';
|
|
6
|
+
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
6
7
|
|
|
7
8
|
import { IBaseCompleter } from './base-completer';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { getSecretId, SECRETS_REPLACEMENT } from './settings';
|
|
10
|
+
import {
|
|
11
|
+
IAIProvider,
|
|
12
|
+
IAIProviderRegistry,
|
|
13
|
+
IDict,
|
|
14
|
+
ISetProviderOptions,
|
|
15
|
+
PLUGIN_IDS
|
|
16
|
+
} from './tokens';
|
|
17
|
+
|
|
18
|
+
const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
|
|
10
19
|
|
|
11
20
|
export const chatSystemPrompt = (
|
|
12
21
|
options: AIProviderRegistry.IPromptOptions
|
|
@@ -39,6 +48,14 @@ Do not include the prompt in the output, only the string that should be appended
|
|
|
39
48
|
`;
|
|
40
49
|
|
|
41
50
|
export class AIProviderRegistry implements IAIProviderRegistry {
|
|
51
|
+
/**
|
|
52
|
+
* The constructor of the provider registry.
|
|
53
|
+
*/
|
|
54
|
+
constructor(options: AIProviderRegistry.IOptions) {
|
|
55
|
+
this._secretsManager = options.secretsManager || null;
|
|
56
|
+
Private.setToken(options.token);
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/**
|
|
43
60
|
* Get the list of provider names.
|
|
44
61
|
*/
|
|
@@ -56,6 +73,11 @@ export class AIProviderRegistry implements IAIProviderRegistry {
|
|
|
56
73
|
);
|
|
57
74
|
}
|
|
58
75
|
this._providers.set(provider.name, provider);
|
|
76
|
+
|
|
77
|
+
// Set the provider if the loading has been deferred.
|
|
78
|
+
if (provider.name === this._deferredProvider?.name) {
|
|
79
|
+
this.setProvider(this._deferredProvider);
|
|
80
|
+
}
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
/**
|
|
@@ -131,15 +153,40 @@ export class AIProviderRegistry implements IAIProviderRegistry {
|
|
|
131
153
|
* Set the providers (chat model and completer).
|
|
132
154
|
* Creates the providers if the name has changed, otherwise only updates their config.
|
|
133
155
|
*
|
|
134
|
-
* @param
|
|
135
|
-
* @param settings - the settings for the models.
|
|
156
|
+
* @param options - An object with the name and the settings of the provider to use.
|
|
136
157
|
*/
|
|
137
|
-
setProvider(
|
|
158
|
+
async setProvider(options: ISetProviderOptions): Promise<void> {
|
|
159
|
+
const { name, settings } = options;
|
|
138
160
|
this._currentProvider = this._providers.get(name) ?? null;
|
|
161
|
+
if (this._currentProvider === null) {
|
|
162
|
+
// The current provider may not be loaded when the settings are first loaded.
|
|
163
|
+
// Let's defer the provider loading.
|
|
164
|
+
this._deferredProvider = options;
|
|
165
|
+
} else {
|
|
166
|
+
this._deferredProvider = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build a new settings object containing the secrets.
|
|
170
|
+
const fullSettings: IDict = {};
|
|
171
|
+
for (const key of Object.keys(settings)) {
|
|
172
|
+
if (settings[key] === SECRETS_REPLACEMENT) {
|
|
173
|
+
const id = getSecretId(name, key);
|
|
174
|
+
const secrets = await this._secretsManager?.get(
|
|
175
|
+
Private.getToken(),
|
|
176
|
+
SECRETS_NAMESPACE,
|
|
177
|
+
id
|
|
178
|
+
);
|
|
179
|
+
fullSettings[key] = secrets?.value || settings[key];
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
fullSettings[key] = settings[key];
|
|
183
|
+
}
|
|
139
184
|
|
|
140
185
|
if (this._currentProvider?.completer !== undefined) {
|
|
141
186
|
try {
|
|
142
|
-
this._completer = new this._currentProvider.completer({
|
|
187
|
+
this._completer = new this._currentProvider.completer({
|
|
188
|
+
...fullSettings
|
|
189
|
+
});
|
|
143
190
|
this._completerError = '';
|
|
144
191
|
} catch (e: any) {
|
|
145
192
|
this._completerError = e.message;
|
|
@@ -150,7 +197,9 @@ export class AIProviderRegistry implements IAIProviderRegistry {
|
|
|
150
197
|
|
|
151
198
|
if (this._currentProvider?.chatModel !== undefined) {
|
|
152
199
|
try {
|
|
153
|
-
this._chatModel = new this._currentProvider.chatModel({
|
|
200
|
+
this._chatModel = new this._currentProvider.chatModel({
|
|
201
|
+
...fullSettings
|
|
202
|
+
});
|
|
154
203
|
this._chatError = '';
|
|
155
204
|
} catch (e: any) {
|
|
156
205
|
this._chatError = e.message;
|
|
@@ -170,6 +219,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
|
|
|
170
219
|
return this._providerChanged;
|
|
171
220
|
}
|
|
172
221
|
|
|
222
|
+
private _secretsManager: ISecretsManager | null;
|
|
173
223
|
private _currentProvider: IAIProvider | null = null;
|
|
174
224
|
private _completer: IBaseCompleter | null = null;
|
|
175
225
|
private _chatModel: BaseChatModel | null = null;
|
|
@@ -178,6 +228,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
|
|
|
178
228
|
private _chatError: string = '';
|
|
179
229
|
private _completerError: string = '';
|
|
180
230
|
private _providers = new Map<string, IAIProvider>();
|
|
231
|
+
private _deferredProvider: ISetProviderOptions | null = null;
|
|
181
232
|
}
|
|
182
233
|
|
|
183
234
|
export namespace AIProviderRegistry {
|
|
@@ -186,13 +237,13 @@ export namespace AIProviderRegistry {
|
|
|
186
237
|
*/
|
|
187
238
|
export interface IOptions {
|
|
188
239
|
/**
|
|
189
|
-
* The
|
|
240
|
+
* The secrets manager used in the application.
|
|
190
241
|
*/
|
|
191
|
-
|
|
242
|
+
secretsManager?: ISecretsManager;
|
|
192
243
|
/**
|
|
193
|
-
* The
|
|
244
|
+
* The token used to request the secrets manager.
|
|
194
245
|
*/
|
|
195
|
-
|
|
246
|
+
token: symbol;
|
|
196
247
|
}
|
|
197
248
|
|
|
198
249
|
/**
|
|
@@ -247,3 +298,24 @@ export namespace AIProviderRegistry {
|
|
|
247
298
|
});
|
|
248
299
|
}
|
|
249
300
|
}
|
|
301
|
+
|
|
302
|
+
namespace Private {
|
|
303
|
+
/**
|
|
304
|
+
* The token to use with the secrets manager.
|
|
305
|
+
*/
|
|
306
|
+
let secretsToken: symbol;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Set of the token.
|
|
310
|
+
*/
|
|
311
|
+
export function setToken(value: symbol): void {
|
|
312
|
+
secretsToken = value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* get the token.
|
|
317
|
+
*/
|
|
318
|
+
export function getToken(): symbol {
|
|
319
|
+
return secretsToken;
|
|
320
|
+
}
|
|
321
|
+
}
|
package/src/settings/panel.tsx
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ISettingConnector,
|
|
4
|
+
ISettingRegistry
|
|
5
|
+
} from '@jupyterlab/settingregistry';
|
|
3
6
|
import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
|
|
4
7
|
import { ArrayExt } from '@lumino/algorithm';
|
|
5
8
|
import { JSONExt } from '@lumino/coreutils';
|
|
@@ -10,19 +13,27 @@ import { JSONSchema7 } from 'json-schema';
|
|
|
10
13
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
11
14
|
import React from 'react';
|
|
12
15
|
|
|
16
|
+
import { getSecretId, SettingConnector } from '.';
|
|
13
17
|
import baseSettings from './base.json';
|
|
14
|
-
import { IAIProviderRegistry, IDict } from '../tokens';
|
|
18
|
+
import { IAIProviderRegistry, IDict, PLUGIN_IDS } from '../tokens';
|
|
15
19
|
|
|
16
|
-
const SECRETS_NAMESPACE = '@jupyterlite/ai';
|
|
17
20
|
const MD_MIME_TYPE = 'text/markdown';
|
|
18
21
|
const STORAGE_NAME = '@jupyterlite/ai:settings';
|
|
19
22
|
const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
|
|
23
|
+
const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
|
|
20
24
|
|
|
21
25
|
export const aiSettingsRenderer = (options: {
|
|
22
26
|
providerRegistry: IAIProviderRegistry;
|
|
27
|
+
secretsToken?: symbol;
|
|
23
28
|
rmRegistry?: IRenderMimeRegistry;
|
|
24
29
|
secretsManager?: ISecretsManager;
|
|
30
|
+
settingConnector?: ISettingConnector;
|
|
25
31
|
}): IFormRenderer => {
|
|
32
|
+
const { secretsToken } = options;
|
|
33
|
+
delete options.secretsToken;
|
|
34
|
+
if (secretsToken) {
|
|
35
|
+
Private.setToken(secretsToken);
|
|
36
|
+
}
|
|
26
37
|
return {
|
|
27
38
|
fieldRenderer: (props: FieldProps) => {
|
|
28
39
|
props.formContext = { ...props.formContext, ...options };
|
|
@@ -54,10 +65,13 @@ export class AiSettings extends React.Component<
|
|
|
54
65
|
this._providerRegistry = props.formContext.providerRegistry;
|
|
55
66
|
this._rmRegistry = props.formContext.rmRegistry ?? null;
|
|
56
67
|
this._secretsManager = props.formContext.secretsManager ?? null;
|
|
68
|
+
this._settingConnector = props.formContext.settingConnector ?? null;
|
|
57
69
|
this._settings = props.formContext.settings;
|
|
58
70
|
|
|
59
71
|
this._useSecretsManager =
|
|
60
72
|
(this._settings.get('UseSecretsManager').composite as boolean) ?? true;
|
|
73
|
+
this._hideSecretFields =
|
|
74
|
+
(this._settings.get('HideSecretFields').composite as boolean) ?? true;
|
|
61
75
|
|
|
62
76
|
// Initialize the providers schema.
|
|
63
77
|
const providerSchema = JSONExt.deepCopy(baseSettings) as any;
|
|
@@ -110,6 +124,12 @@ export class AiSettings extends React.Component<
|
|
|
110
124
|
if (useSecretsManager !== this._useSecretsManager) {
|
|
111
125
|
this.updateUseSecretsManager(useSecretsManager);
|
|
112
126
|
}
|
|
127
|
+
const hideSecretFields =
|
|
128
|
+
(this._settings.get('HideSecretFields').composite as boolean) ?? true;
|
|
129
|
+
if (hideSecretFields !== this._hideSecretFields) {
|
|
130
|
+
this._hideSecretFields = hideSecretFields;
|
|
131
|
+
this._updateSchema();
|
|
132
|
+
}
|
|
113
133
|
});
|
|
114
134
|
}
|
|
115
135
|
|
|
@@ -123,15 +143,16 @@ export class AiSettings extends React.Component<
|
|
|
123
143
|
return;
|
|
124
144
|
}
|
|
125
145
|
|
|
126
|
-
await this._secretsManager.detachAll(SECRETS_NAMESPACE);
|
|
146
|
+
await this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
|
|
127
147
|
this._formInputs = [...inputs];
|
|
128
148
|
this._unsavedFields = [];
|
|
129
149
|
for (let i = 0; i < inputs.length; i++) {
|
|
130
150
|
if (inputs[i].type.toLowerCase() === 'password') {
|
|
131
151
|
const label = inputs[i].getAttribute('label');
|
|
132
152
|
if (label) {
|
|
133
|
-
const id =
|
|
153
|
+
const id = getSecretId(this._provider, label);
|
|
134
154
|
this._secretsManager.attach(
|
|
155
|
+
Private.getToken(),
|
|
135
156
|
SECRETS_NAMESPACE,
|
|
136
157
|
id,
|
|
137
158
|
inputs[i],
|
|
@@ -141,6 +162,16 @@ export class AiSettings extends React.Component<
|
|
|
141
162
|
}
|
|
142
163
|
}
|
|
143
164
|
}
|
|
165
|
+
if (this._settingConnector instanceof SettingConnector) {
|
|
166
|
+
this._settingConnector.doNotSave = this._unsavedFields;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
componentWillUnmount(): void {
|
|
171
|
+
if (!this._secretsManager || !this._useSecretsManager) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
/**
|
|
@@ -184,9 +215,12 @@ export class AiSettings extends React.Component<
|
|
|
184
215
|
if (!value) {
|
|
185
216
|
// Detach all the password inputs attached to the secrets manager, and save the
|
|
186
217
|
// current settings to the local storage to save the password.
|
|
187
|
-
this._secretsManager?.detachAll(SECRETS_NAMESPACE);
|
|
218
|
+
this._secretsManager?.detachAll(Private.getToken(), SECRETS_NAMESPACE);
|
|
188
219
|
this._formInputs = [];
|
|
189
220
|
this._unsavedFields = [];
|
|
221
|
+
if (this._settingConnector instanceof SettingConnector) {
|
|
222
|
+
this._settingConnector.doNotSave = [];
|
|
223
|
+
}
|
|
190
224
|
this.saveSettings(this._currentSettings);
|
|
191
225
|
} else {
|
|
192
226
|
// Remove all the keys stored locally and attach the password inputs to the
|
|
@@ -202,18 +236,11 @@ export class AiSettings extends React.Component<
|
|
|
202
236
|
localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
|
|
203
237
|
this.componentDidUpdate();
|
|
204
238
|
}
|
|
239
|
+
this._settings
|
|
240
|
+
.set('AIprovider', { provider: this._provider, ...this._currentSettings })
|
|
241
|
+
.catch(console.error);
|
|
205
242
|
};
|
|
206
243
|
|
|
207
|
-
/**
|
|
208
|
-
* Update the UI schema of the form.
|
|
209
|
-
* Currently use to hide API keys.
|
|
210
|
-
*/
|
|
211
|
-
private _updateUiSchema(key: string) {
|
|
212
|
-
if (key.toLowerCase().includes('key')) {
|
|
213
|
-
this._uiSchema[key] = { 'ui:widget': 'password' };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
244
|
/**
|
|
218
245
|
* Build the schema for a given provider.
|
|
219
246
|
*/
|
|
@@ -226,8 +253,13 @@ export class AiSettings extends React.Component<
|
|
|
226
253
|
|
|
227
254
|
if (settingsSchema) {
|
|
228
255
|
Object.entries(settingsSchema).forEach(([key, value]) => {
|
|
256
|
+
if (key.toLowerCase().includes('key')) {
|
|
257
|
+
if (this._hideSecretFields) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this._uiSchema[key] = { 'ui:widget': 'password' };
|
|
261
|
+
}
|
|
229
262
|
schema.properties[key] = value;
|
|
230
|
-
this._updateUiSchema(key);
|
|
231
263
|
});
|
|
232
264
|
}
|
|
233
265
|
return schema as JSONSchema7;
|
|
@@ -335,8 +367,10 @@ export class AiSettings extends React.Component<
|
|
|
335
367
|
private _provider: string;
|
|
336
368
|
private _providerSchema: JSONSchema7;
|
|
337
369
|
private _useSecretsManager: boolean;
|
|
370
|
+
private _hideSecretFields: boolean;
|
|
338
371
|
private _rmRegistry: IRenderMimeRegistry | null;
|
|
339
372
|
private _secretsManager: ISecretsManager | null;
|
|
373
|
+
private _settingConnector: ISettingConnector | null;
|
|
340
374
|
private _currentSettings: IDict<any> = { provider: 'None' };
|
|
341
375
|
private _uiSchema: IDict<any> = {};
|
|
342
376
|
private _settings: ISettingRegistry.ISettings;
|
|
@@ -344,3 +378,24 @@ export class AiSettings extends React.Component<
|
|
|
344
378
|
private _unsavedFields: string[] = [];
|
|
345
379
|
private _formInputs: HTMLInputElement[] = [];
|
|
346
380
|
}
|
|
381
|
+
|
|
382
|
+
namespace Private {
|
|
383
|
+
/**
|
|
384
|
+
* The token to use with the secrets manager.
|
|
385
|
+
*/
|
|
386
|
+
let secretsToken: symbol;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Set of the token.
|
|
390
|
+
*/
|
|
391
|
+
export function setToken(value: symbol): void {
|
|
392
|
+
secretsToken = value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* get the token.
|
|
397
|
+
*/
|
|
398
|
+
export function getToken(): symbol {
|
|
399
|
+
return secretsToken;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PageConfig } from '@jupyterlab/coreutils';
|
|
2
|
+
import {
|
|
3
|
+
ISettingConnector,
|
|
4
|
+
ISettingRegistry
|
|
5
|
+
} from '@jupyterlab/settingregistry';
|
|
6
|
+
import { DataConnector, IDataConnector } from '@jupyterlab/statedb';
|
|
7
|
+
import { Throttler } from '@lumino/polling';
|
|
8
|
+
import * as json5 from 'json5';
|
|
9
|
+
|
|
10
|
+
import { SECRETS_REPLACEMENT } from '.';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A data connector for fetching settings.
|
|
14
|
+
*
|
|
15
|
+
* #### Notes
|
|
16
|
+
* This connector adds a query parameter to the base services setting manager.
|
|
17
|
+
*/
|
|
18
|
+
export class SettingConnector
|
|
19
|
+
extends DataConnector<ISettingRegistry.IPlugin, string>
|
|
20
|
+
implements ISettingConnector
|
|
21
|
+
{
|
|
22
|
+
constructor(connector: IDataConnector<ISettingRegistry.IPlugin, string>) {
|
|
23
|
+
super();
|
|
24
|
+
this._connector = connector;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set doNotSave(fields: string[]) {
|
|
28
|
+
this._doNotSave = [...fields];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetch settings for a plugin.
|
|
33
|
+
* @param id - The plugin ID
|
|
34
|
+
*
|
|
35
|
+
* #### Notes
|
|
36
|
+
* The REST API requests are throttled at one request per plugin per 100ms.
|
|
37
|
+
*/
|
|
38
|
+
fetch(id: string): Promise<ISettingRegistry.IPlugin | undefined> {
|
|
39
|
+
const throttlers = this._throttlers;
|
|
40
|
+
if (!(id in throttlers)) {
|
|
41
|
+
throttlers[id] = new Throttler(() => this._connector.fetch(id), 100);
|
|
42
|
+
}
|
|
43
|
+
return throttlers[id].invoke();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async list(query: 'ids'): Promise<{ ids: string[] }>;
|
|
47
|
+
async list(
|
|
48
|
+
query: 'active' | 'all'
|
|
49
|
+
): Promise<{ ids: string[]; values: ISettingRegistry.IPlugin[] }>;
|
|
50
|
+
async list(
|
|
51
|
+
query: 'active' | 'all' | 'ids' = 'all'
|
|
52
|
+
): Promise<{ ids: string[]; values?: ISettingRegistry.IPlugin[] }> {
|
|
53
|
+
const { isDisabled } = PageConfig.Extension;
|
|
54
|
+
const { ids, values } = await this._connector.list(
|
|
55
|
+
query === 'ids' ? 'ids' : undefined
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (query === 'all') {
|
|
59
|
+
return { ids, values };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (query === 'ids') {
|
|
63
|
+
return { ids };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
ids: ids.filter(id => !isDisabled(id)),
|
|
68
|
+
values: values.filter(({ id }) => !isDisabled(id))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async save(id: string, raw: string): Promise<void> {
|
|
73
|
+
const settings = json5.parse(raw);
|
|
74
|
+
this._doNotSave.forEach(field => {
|
|
75
|
+
if (
|
|
76
|
+
settings['AIprovider'] !== undefined &&
|
|
77
|
+
settings['AIprovider'][field] !== undefined &&
|
|
78
|
+
settings['AIprovider'][field] !== ''
|
|
79
|
+
) {
|
|
80
|
+
settings['AIprovider'][field] = SECRETS_REPLACEMENT;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
await this._connector.save(id, json5.stringify(settings, null, 2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private _connector: IDataConnector<ISettingRegistry.IPlugin, string>;
|
|
87
|
+
private _doNotSave: string[] = [];
|
|
88
|
+
private _throttlers: { [key: string]: Throttler } = Object.create(null);
|
|
89
|
+
}
|