@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/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 { ISettingRegistry } from '@jupyterlab/settingregistry';
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/panel';
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: '@jupyterlite/ai:autocompletion-registry',
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: '@jupyterlite/ai:chat',
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: '@jupyterlite/ai:completer',
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
- id: '@jupyterlite/ai:provider-registry',
139
- autoStart: true,
140
- requires: [IFormRendererRegistry, ISettingRegistry],
141
- optional: [IRenderMimeRegistry, ISecretsManager],
142
- provides: IAIProviderRegistry,
143
- activate: (
144
- app: JupyterFrontEnd,
145
- editorRegistry: IFormRendererRegistry,
146
- settingRegistry: ISettingRegistry,
147
- rmRegistry?: IRenderMimeRegistry,
148
- secretsManager?: ISecretsManager
149
- ): IAIProviderRegistry => {
150
- const providerRegistry = new AIProviderRegistry();
151
-
152
- editorRegistry.addRenderer(
153
- '@jupyterlite/ai:provider-registry.AIprovider',
154
- aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
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
- return providerRegistry;
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 { IAIProvider, IAIProviderRegistry } from './tokens';
9
- import { JSONSchema7 } from 'json-schema';
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 name - the name of the provider to use.
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(name: string, settings: ReadonlyPartialJSONObject): void {
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({ ...settings });
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({ ...settings });
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 completion provider manager in which register the LLM completer.
240
+ * The secrets manager used in the application.
190
241
  */
191
- completionProviderManager: ICompletionProviderManager;
242
+ secretsManager?: ISecretsManager;
192
243
  /**
193
- * The application commands registry.
244
+ * The token used to request the secrets manager.
194
245
  */
195
- requestCompletion: () => void;
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './panel';
2
+ export * from './settings-connector';
3
+ export * from './utils';
@@ -1,5 +1,8 @@
1
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
- import { ISettingRegistry } from '@jupyterlab/settingregistry';
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 = `${this._provider}-${label}`;
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
+ }
@@ -0,0 +1,5 @@
1
+ export const SECRETS_REPLACEMENT = '***';
2
+
3
+ export function getSecretId(provider: string, label: string) {
4
+ return `${provider}-${label}`;
5
+ }