Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the secrets manager #53

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@rjsf/core": "^4.2.0",
"@rjsf/utils": "^5.18.4",
"@rjsf/validator-ajv8": "^5.18.4",
"jupyter-secrets-manager": "^0.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
]
dependencies = [
"jupyter-secrets-manager"
]
dynamic = ["version", "description", "authors", "urls", "keywords"]

Expand Down
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { ISecretsManager } from 'jupyter-secrets-manager';

import { ChatHandler } from './chat-handler';
import { CompletionProvider } from './completion-provider';
Expand Down Expand Up @@ -137,19 +138,20 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
id: '@jupyterlite/ai:provider-registry',
autoStart: true,
requires: [IFormRendererRegistry, ISettingRegistry],
optional: [IRenderMimeRegistry],
optional: [IRenderMimeRegistry, ISecretsManager],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like with this change, API keys are not stored on the page anymore since it will be using the default in-memory secret manager?

Wondering if we should still have a secret manager that stores secrets in the browser, for convenience? Or maybe have an option to choose between in-memory and in-browser storage?

provides: IAIProviderRegistry,
activate: (
app: JupyterFrontEnd,
editorRegistry: IFormRendererRegistry,
settingRegistry: ISettingRegistry,
rmRegistry?: IRenderMimeRegistry
rmRegistry?: IRenderMimeRegistry,
secretsManager?: ISecretsManager
): IAIProviderRegistry => {
const providerRegistry = new AIProviderRegistry();

editorRegistry.addRenderer(
'@jupyterlite/ai:provider-registry.AIprovider',
aiSettingsRenderer({ providerRegistry, rmRegistry })
aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
);
settingRegistry
.load(providerRegistryPlugin.id)
Expand Down
58 changes: 55 additions & 3 deletions src/settings/panel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
import { ArrayExt } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { IChangeEvent } from '@rjsf/core';
import type { FieldProps } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { JSONSchema7 } from 'json-schema';
import { ISecretsManager } from 'jupyter-secrets-manager';
import React from 'react';

import baseSettings from './schemas/base.json';
import { IAIProviderRegistry, IDict } from '../tokens';

const SECRETS_NAMESPACE = '@jupyterlite/ai';
const MD_MIME_TYPE = 'text/markdown';
const STORAGE_NAME = '@jupyterlite/ai:settings';
const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';

export const aiSettingsRenderer = (options: {
providerRegistry: IAIProviderRegistry;
rmRegistry?: IRenderMimeRegistry;
secretsManager?: ISecretsManager;
}): IFormRenderer => {
return {
fieldRenderer: (props: FieldProps) => {
Expand Down Expand Up @@ -49,6 +53,7 @@ export class AiSettings extends React.Component<
}
this._providerRegistry = props.formContext.providerRegistry;
this._rmRegistry = props.formContext.rmRegistry ?? null;
this._secretsManager = props.formContext.secretsManager ?? null;
this._settings = props.formContext.settings;

// Initialize the providers schema.
Expand Down Expand Up @@ -97,6 +102,36 @@ export class AiSettings extends React.Component<
.catch(console.error);
}

async componentDidUpdate(): Promise<void> {
if (!this._secretsManager) {
return;
}
// Attach the password inputs to the secrets manager only if they have changed.
const inputs = this._formRef.current?.getElementsByTagName('input') || [];
if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
return;
}

await this._secretsManager?.detachAll(SECRETS_NAMESPACE);
this._formInputs = [...inputs];
this._unsavedFields = [];
for (let i = 0; i < inputs.length; i++) {
if (inputs[i].type.toLowerCase() === 'password') {
const label = inputs[i].getAttribute('label');
if (label) {
const id = `${this._provider}-${label}`;
this._secretsManager.attach(
SECRETS_NAMESPACE,
id,
inputs[i],
(value: string) => this._onPasswordUpdated(label, value)
);
this._unsavedFields.push(label);
}
}
}
}

/**
* Get the current provider from the local storage.
*/
Expand Down Expand Up @@ -126,8 +161,10 @@ export class AiSettings extends React.Component<
* Save settings in local storage for a given provider.
*/
saveSettings(value: IDict<any>) {
const currentSettings = { ...value };
const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) ?? '{}');
settings[this._provider] = value;
this._unsavedFields.forEach(field => delete currentSettings[field]);
settings[this._provider] = currentSettings;
localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
}

Expand Down Expand Up @@ -206,6 +243,17 @@ export class AiSettings extends React.Component<
.catch(console.error);
};

/**
* Callback function called when the password input has been programmatically updated
* with the secret manager.
*/
private _onPasswordUpdated = (fieldName: string, value: string) => {
this._currentSettings[fieldName] = value;
this._settings
.set('AIprovider', { provider: this._provider, ...this._currentSettings })
.catch(console.error);
};

/**
* Triggered when the form value has changed, to update the current settings and save
* it in local storage.
Expand All @@ -221,7 +269,7 @@ export class AiSettings extends React.Component<

render(): JSX.Element {
return (
<>
<div ref={this._formRef}>
<WrappedFormComponent
formData={{ provider: this._provider }}
schema={this._providerSchema}
Expand All @@ -243,15 +291,19 @@ export class AiSettings extends React.Component<
onChange={this._onFormChange}
uiSchema={this._uiSchema}
/>
</>
</div>
);
}

private _providerRegistry: IAIProviderRegistry;
private _provider: string;
private _providerSchema: JSONSchema7;
private _rmRegistry: IRenderMimeRegistry | null;
private _secretsManager: ISecretsManager | null;
private _currentSettings: IDict<any> = { provider: 'None' };
private _uiSchema: IDict<any> = {};
private _settings: ISettingRegistry.ISettings;
private _formRef = React.createRef<HTMLDivElement>();
private _unsavedFields: string[] = [];
private _formInputs: HTMLInputElement[] = [];
}
45 changes: 43 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,34 @@ __metadata:
languageName: node
linkType: hard

"@jupyterlab/application@npm:^4.0.0":
version: 4.3.5
resolution: "@jupyterlab/application@npm:4.3.5"
dependencies:
"@fortawesome/fontawesome-free": ^5.12.0
"@jupyterlab/apputils": ^4.4.5
"@jupyterlab/coreutils": ^6.3.5
"@jupyterlab/docregistry": ^4.3.5
"@jupyterlab/rendermime": ^4.3.5
"@jupyterlab/rendermime-interfaces": ^3.11.5
"@jupyterlab/services": ^7.3.5
"@jupyterlab/statedb": ^4.3.5
"@jupyterlab/translation": ^4.3.5
"@jupyterlab/ui-components": ^4.3.5
"@lumino/algorithm": ^2.0.2
"@lumino/application": ^2.4.1
"@lumino/commands": ^2.3.1
"@lumino/coreutils": ^2.2.0
"@lumino/disposable": ^2.1.3
"@lumino/messaging": ^2.0.2
"@lumino/polling": ^2.1.3
"@lumino/properties": ^2.0.2
"@lumino/signaling": ^2.1.3
"@lumino/widgets": ^2.5.0
checksum: 2111efe2caafed74a78c2ddd6220baa6ccc459c27bde3cc80a0a53a403ad08ae2f0b3ea2e56e24b13440bc6fb3d254fc5519536127f47bd50a496bc44458aeb1
languageName: node
linkType: hard

"@jupyterlab/application@npm:^4.2.0, @jupyterlab/application@npm:^4.4.0-alpha.0":
version: 4.4.0-alpha.2
resolution: "@jupyterlab/application@npm:4.4.0-alpha.2"
Expand Down Expand Up @@ -1843,7 +1871,7 @@ __metadata:
languageName: node
linkType: hard

"@jupyterlab/statedb@npm:^4.3.5":
"@jupyterlab/statedb@npm:^4.0.0, @jupyterlab/statedb@npm:^4.3.5":
version: 4.3.5
resolution: "@jupyterlab/statedb@npm:4.3.5"
dependencies:
Expand Down Expand Up @@ -2071,6 +2099,7 @@ __metadata:
eslint: ^8.36.0
eslint-config-prettier: ^8.8.0
eslint-plugin-prettier: ^5.0.0
jupyter-secrets-manager: ^0.1.1
npm-run-all: ^4.1.5
prettier: ^3.0.0
react: ^18.2.0
Expand Down Expand Up @@ -2739,7 +2768,7 @@ __metadata:
languageName: node
linkType: hard

"@lumino/algorithm@npm:^2.0.1, @lumino/algorithm@npm:^2.0.2":
"@lumino/algorithm@npm:^2.0.0, @lumino/algorithm@npm:^2.0.1, @lumino/algorithm@npm:^2.0.2":
version: 2.0.2
resolution: "@lumino/algorithm@npm:2.0.2"
checksum: 34b25684b845f1bdbf78ca45ebd99a97b67b2992440c9643aafe5fc5a99fae1ddafa9e5890b246b233dc3a12d9f66aa84afe4a2aac44cf31071348ed217740db
Expand Down Expand Up @@ -6188,6 +6217,18 @@ __metadata:
languageName: node
linkType: hard

"jupyter-secrets-manager@npm:^0.1.1":
version: 0.1.1
resolution: "jupyter-secrets-manager@npm:0.1.1"
dependencies:
"@jupyterlab/application": ^4.0.0
"@jupyterlab/statedb": ^4.0.0
"@lumino/algorithm": ^2.0.0
"@lumino/coreutils": ^2.1.2
checksum: 608b1ad45dd037362caf0db1555d833203985d4069091da91f4407c80a21d6acadbf86a322d8e0b6c476089d8edd8a634f0978a57565fe76842920822e9ce2c0
languageName: node
linkType: hard

"keyv@npm:^4.5.3":
version: 4.5.4
resolution: "keyv@npm:4.5.4"
Expand Down