Skip to content

Commit 9bc1094

Browse files
authored
Do not store passwords to server settings (#60)
* Update @jupyterlab/settingregistry dependency * Avoid saving the passwords in settings * update yarn.lock * Use the secrets manager to restore the saved passwords in the provider registry * Defer the loading of the provider if it is not yet in the registry * Update secrets-manager dependency
1 parent ef63e8f commit 9bc1094

File tree

11 files changed

+289
-42
lines changed

11 files changed

+289
-42
lines changed

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@jupyterlab/completer": "^4.4.0-alpha.0",
6363
"@jupyterlab/notebook": "^4.4.0-alpha.0",
6464
"@jupyterlab/rendermime": "^4.4.0-alpha.0",
65-
"@jupyterlab/settingregistry": "^4.4.0-alpha.0",
65+
"@jupyterlab/settingregistry": "^4.4.0-beta.1",
6666
"@jupyterlab/ui-components": "^4.4.0-alpha.0",
6767
"@langchain/anthropic": "^0.3.9",
6868
"@langchain/community": "^0.3.31",
@@ -77,7 +77,8 @@
7777
"@rjsf/core": "^4.2.0",
7878
"@rjsf/utils": "^5.18.4",
7979
"@rjsf/validator-ajv8": "^5.18.4",
80-
"jupyter-secrets-manager": "^0.1.1",
80+
"json5": "^2.2.3",
81+
"jupyter-secrets-manager": "^0.2.0",
8182
"react": "^18.2.0",
8283
"react-dom": "^18.2.0"
8384
},
@@ -118,6 +119,9 @@
118119
"jupyterlab": {
119120
"extension": true,
120121
"outputDir": "jupyterlite_ai/labextension",
121-
"schemaDir": "schema"
122+
"schemaDir": "schema",
123+
"disabledExtensions": [
124+
"@jupyterlab/apputils-extension:settings-connector"
125+
]
122126
}
123127
}

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ dynamic = ["version", "description", "authors", "urls", "keywords"]
2929

3030
[project.optional-dependencies]
3131
jupyter = [
32-
"jupyterlab>=4.4.0a0",
32+
"jupyterlab>=4.4.0b1",
3333
"jupyterlite>=0.6.0a0",
34-
"notebook>=7.4.0a0"
34+
"notebook>=7.4.0b1"
3535
]
3636

3737
[tool.hatch.version]

src/completion-provider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ export class CompletionProvider implements IInlineCompletionProvider {
5151

5252
export namespace CompletionProvider {
5353
export interface IOptions {
54+
/**
55+
* The registry where the completion provider belongs.
56+
*/
5457
providerRegistry: IAIProviderRegistry;
58+
/**
59+
* The request completion commands, can be useful if a provider needs to request
60+
* the completion by itself.
61+
*/
5562
requestCompletion: () => void;
5663
}
5764
}

src/index.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { ReactWidget, IThemeManager } from '@jupyterlab/apputils';
1414
import { ICompletionProviderManager } from '@jupyterlab/completer';
1515
import { INotebookTracker } from '@jupyterlab/notebook';
1616
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
17-
import { ISettingRegistry } from '@jupyterlab/settingregistry';
17+
import {
18+
ISettingConnector,
19+
ISettingRegistry
20+
} from '@jupyterlab/settingregistry';
1821
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
1922
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
2023
import { ISecretsManager } from 'jupyter-secrets-manager';
@@ -23,7 +26,7 @@ import { ChatHandler } from './chat-handler';
2326
import { CompletionProvider } from './completion-provider';
2427
import { defaultProviderPlugins } from './default-providers';
2528
import { AIProviderRegistry } from './provider';
26-
import { aiSettingsRenderer } from './settings/panel';
29+
import { aiSettingsRenderer, SettingConnector } from './settings';
2730
import { IAIProviderRegistry } from './tokens';
2831

2932
const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
@@ -138,21 +141,28 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
138141
id: '@jupyterlite/ai:provider-registry',
139142
autoStart: true,
140143
requires: [IFormRendererRegistry, ISettingRegistry],
141-
optional: [IRenderMimeRegistry, ISecretsManager],
144+
optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector],
142145
provides: IAIProviderRegistry,
143146
activate: (
144147
app: JupyterFrontEnd,
145148
editorRegistry: IFormRendererRegistry,
146149
settingRegistry: ISettingRegistry,
147150
rmRegistry?: IRenderMimeRegistry,
148-
secretsManager?: ISecretsManager
151+
secretsManager?: ISecretsManager,
152+
settingConnector?: ISettingConnector
149153
): IAIProviderRegistry => {
150-
const providerRegistry = new AIProviderRegistry();
154+
const providerRegistry = new AIProviderRegistry({ secretsManager });
151155

152156
editorRegistry.addRenderer(
153157
'@jupyterlite/ai:provider-registry.AIprovider',
154-
aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
158+
aiSettingsRenderer({
159+
providerRegistry,
160+
rmRegistry,
161+
secretsManager,
162+
settingConnector
163+
})
155164
);
165+
156166
settingRegistry
157167
.load(providerRegistryPlugin.id)
158168
.then(settings => {
@@ -161,10 +171,10 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
161171
const providerSettings = (settings.get('AIprovider').composite ?? {
162172
provider: 'None'
163173
}) as ReadonlyPartialJSONObject;
164-
providerRegistry.setProvider(
165-
providerSettings.provider as string,
166-
providerSettings
167-
);
174+
providerRegistry.setProvider({
175+
name: providerSettings.provider as string,
176+
settings: providerSettings
177+
});
168178
};
169179

170180
settings.changed.connect(() => updateProvider());
@@ -181,10 +191,25 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
181191
}
182192
};
183193

194+
/**
195+
* Provides the settings connector as a separate plugin to allow for alternative
196+
* implementations that may want to fetch settings from a different source or
197+
* endpoint.
198+
*/
199+
const settingsConnector: JupyterFrontEndPlugin<ISettingConnector> = {
200+
id: '@jupyterlite/ai:settings-connector',
201+
description: 'Provides a settings connector which does not save passwords.',
202+
autoStart: true,
203+
provides: ISettingConnector,
204+
activate: (app: JupyterFrontEnd) =>
205+
new SettingConnector(app.serviceManager.settings)
206+
};
207+
184208
export default [
185209
providerRegistryPlugin,
186210
chatCommandRegistryPlugin,
187211
chatPlugin,
188212
completerPlugin,
213+
settingsConnector,
189214
...defaultProviderPlugins
190215
];

src/provider.ts

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
import { ICompletionProviderManager } from '@jupyterlab/completer';
21
import { BaseLanguageModel } from '@langchain/core/language_models/base';
32
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
43
import { ISignal, Signal } from '@lumino/signaling';
54
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
5+
import { JSONSchema7 } from 'json-schema';
6+
import { ISecretsManager } from 'jupyter-secrets-manager';
67

78
import { IBaseCompleter } from './base-completer';
8-
import { IAIProvider, IAIProviderRegistry } from './tokens';
9-
import { JSONSchema7 } from 'json-schema';
9+
import {
10+
getSecretId,
11+
SECRETS_NAMESPACE,
12+
SECRETS_REPLACEMENT
13+
} from './settings';
14+
import {
15+
IAIProvider,
16+
IAIProviderRegistry,
17+
IDict,
18+
ISetProviderOptions
19+
} from './tokens';
1020

1121
export const chatSystemPrompt = (
1222
options: AIProviderRegistry.IPromptOptions
@@ -39,6 +49,13 @@ Do not include the prompt in the output, only the string that should be appended
3949
`;
4050

4151
export class AIProviderRegistry implements IAIProviderRegistry {
52+
/**
53+
* The constructor of the provider registry.
54+
*/
55+
constructor(options: AIProviderRegistry.IOptions) {
56+
this._secretsManager = options.secretsManager || null;
57+
}
58+
4259
/**
4360
* Get the list of provider names.
4461
*/
@@ -56,6 +73,11 @@ export class AIProviderRegistry implements IAIProviderRegistry {
5673
);
5774
}
5875
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+
}
5981
}
6082

6183
/**
@@ -131,15 +153,36 @@ export class AIProviderRegistry implements IAIProviderRegistry {
131153
* Set the providers (chat model and completer).
132154
* Creates the providers if the name has changed, otherwise only updates their config.
133155
*
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.
136157
*/
137-
setProvider(name: string, settings: ReadonlyPartialJSONObject): void {
158+
async setProvider(options: ISetProviderOptions): Promise<void> {
159+
const { name, settings } = options;
138160
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(SECRETS_NAMESPACE, id);
175+
fullSettings[key] = secrets?.value || settings[key];
176+
continue;
177+
}
178+
fullSettings[key] = settings[key];
179+
}
139180

140181
if (this._currentProvider?.completer !== undefined) {
141182
try {
142-
this._completer = new this._currentProvider.completer({ ...settings });
183+
this._completer = new this._currentProvider.completer({
184+
...fullSettings
185+
});
143186
this._completerError = '';
144187
} catch (e: any) {
145188
this._completerError = e.message;
@@ -150,7 +193,9 @@ export class AIProviderRegistry implements IAIProviderRegistry {
150193

151194
if (this._currentProvider?.chatModel !== undefined) {
152195
try {
153-
this._chatModel = new this._currentProvider.chatModel({ ...settings });
196+
this._chatModel = new this._currentProvider.chatModel({
197+
...fullSettings
198+
});
154199
this._chatError = '';
155200
} catch (e: any) {
156201
this._chatError = e.message;
@@ -170,6 +215,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
170215
return this._providerChanged;
171216
}
172217

218+
private _secretsManager: ISecretsManager | null;
173219
private _currentProvider: IAIProvider | null = null;
174220
private _completer: IBaseCompleter | null = null;
175221
private _chatModel: BaseChatModel | null = null;
@@ -178,6 +224,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
178224
private _chatError: string = '';
179225
private _completerError: string = '';
180226
private _providers = new Map<string, IAIProvider>();
227+
private _deferredProvider: ISetProviderOptions | null = null;
181228
}
182229

183230
export namespace AIProviderRegistry {
@@ -186,13 +233,9 @@ export namespace AIProviderRegistry {
186233
*/
187234
export interface IOptions {
188235
/**
189-
* The completion provider manager in which register the LLM completer.
190-
*/
191-
completionProviderManager: ICompletionProviderManager;
192-
/**
193-
* The application commands registry.
236+
* The secrets manager used in the application.
194237
*/
195-
requestCompletion: () => void;
238+
secretsManager?: ISecretsManager;
196239
}
197240

198241
/**

src/settings/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './panel';
2+
export * from './settings-connector';
3+
export * from './utils';

src/settings/panel.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2-
import { ISettingRegistry } from '@jupyterlab/settingregistry';
2+
import {
3+
ISettingConnector,
4+
ISettingRegistry
5+
} from '@jupyterlab/settingregistry';
36
import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
47
import { ArrayExt } from '@lumino/algorithm';
58
import { JSONExt } from '@lumino/coreutils';
@@ -10,10 +13,10 @@ import { JSONSchema7 } from 'json-schema';
1013
import { ISecretsManager } from 'jupyter-secrets-manager';
1114
import React from 'react';
1215

16+
import { getSecretId, SECRETS_NAMESPACE, SettingConnector } from '.';
1317
import baseSettings from './base.json';
1418
import { IAIProviderRegistry, IDict } from '../tokens';
1519

16-
const SECRETS_NAMESPACE = '@jupyterlite/ai';
1720
const MD_MIME_TYPE = 'text/markdown';
1821
const STORAGE_NAME = '@jupyterlite/ai:settings';
1922
const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
@@ -22,6 +25,7 @@ export const aiSettingsRenderer = (options: {
2225
providerRegistry: IAIProviderRegistry;
2326
rmRegistry?: IRenderMimeRegistry;
2427
secretsManager?: ISecretsManager;
28+
settingConnector?: ISettingConnector;
2529
}): IFormRenderer => {
2630
return {
2731
fieldRenderer: (props: FieldProps) => {
@@ -54,6 +58,7 @@ export class AiSettings extends React.Component<
5458
this._providerRegistry = props.formContext.providerRegistry;
5559
this._rmRegistry = props.formContext.rmRegistry ?? null;
5660
this._secretsManager = props.formContext.secretsManager ?? null;
61+
this._settingConnector = props.formContext.settingConnector ?? null;
5762
this._settings = props.formContext.settings;
5863

5964
this._useSecretsManager =
@@ -130,7 +135,7 @@ export class AiSettings extends React.Component<
130135
if (inputs[i].type.toLowerCase() === 'password') {
131136
const label = inputs[i].getAttribute('label');
132137
if (label) {
133-
const id = `${this._provider}-${label}`;
138+
const id = getSecretId(this._provider, label);
134139
this._secretsManager.attach(
135140
SECRETS_NAMESPACE,
136141
id,
@@ -141,6 +146,9 @@ export class AiSettings extends React.Component<
141146
}
142147
}
143148
}
149+
if (this._settingConnector instanceof SettingConnector) {
150+
this._settingConnector.doNotSave = this._unsavedFields;
151+
}
144152
}
145153

146154
/**
@@ -187,6 +195,9 @@ export class AiSettings extends React.Component<
187195
this._secretsManager?.detachAll(SECRETS_NAMESPACE);
188196
this._formInputs = [];
189197
this._unsavedFields = [];
198+
if (this._settingConnector instanceof SettingConnector) {
199+
this._settingConnector.doNotSave = [];
200+
}
190201
this.saveSettings(this._currentSettings);
191202
} else {
192203
// Remove all the keys stored locally and attach the password inputs to the
@@ -202,6 +213,9 @@ export class AiSettings extends React.Component<
202213
localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
203214
this.componentDidUpdate();
204215
}
216+
this._settings
217+
.set('AIprovider', { provider: this._provider, ...this._currentSettings })
218+
.catch(console.error);
205219
};
206220

207221
/**
@@ -337,6 +351,7 @@ export class AiSettings extends React.Component<
337351
private _useSecretsManager: boolean;
338352
private _rmRegistry: IRenderMimeRegistry | null;
339353
private _secretsManager: ISecretsManager | null;
354+
private _settingConnector: ISettingConnector | null;
340355
private _currentSettings: IDict<any> = { provider: 'None' };
341356
private _uiSchema: IDict<any> = {};
342357
private _settings: ISettingRegistry.ISettings;

0 commit comments

Comments
 (0)