From d19a6cb6270fad1358d5c805dc4df83a07490221 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 25 Apr 2025 13:12:10 +0200 Subject: [PATCH] Update secrets manager to >=0.3.0 --- package.json | 2 +- pyproject.toml | 2 +- src/index.ts | 119 +++++++++++++++++++++-------------------- src/provider.ts | 43 ++++++++++++--- src/settings/panel.tsx | 44 +++++++++++++-- src/settings/utils.ts | 1 - src/tokens.ts | 8 +++ yarn.lock | 13 ++--- 8 files changed, 155 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index 56885ab9..47b392e9 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@rjsf/utils": "^5.18.4", "@rjsf/validator-ajv8": "^5.18.4", "json5": "^2.2.3", - "jupyter-secrets-manager": "^0.2.0", + "jupyter-secrets-manager": "^0.3.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/pyproject.toml b/pyproject.toml index 92fb6bfd..d729c96c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "jupyter-secrets-manager" + "jupyter-secrets-manager>=0.3.0" ] dynamic = ["version", "description", "authors", "urls", "keywords"] diff --git a/src/index.ts b/src/index.ts index 8f7deaaa..ee801246 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,18 +21,18 @@ import { } from '@jupyterlab/settingregistry'; import { IFormRendererRegistry } from '@jupyterlab/ui-components'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; -import { ISecretsManager } from 'jupyter-secrets-manager'; +import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager'; import { ChatHandler } from './chat-handler'; import { CompletionProvider } from './completion-provider'; import { defaultProviderPlugins } from './default-providers'; import { AIProviderRegistry } from './provider'; import { aiSettingsRenderer, SettingConnector } from './settings'; -import { IAIProviderRegistry } from './tokens'; +import { IAIProviderRegistry, PLUGIN_IDS } from './tokens'; import { stopItem } from './components/stop-button'; const chatCommandRegistryPlugin: JupyterFrontEndPlugin = { - id: '@jupyterlite/ai:autocompletion-registry', + id: PLUGIN_IDS.chatCommandRegistry, description: 'Autocompletion registry', autoStart: true, provides: IChatCommandRegistry, @@ -44,7 +44,7 @@ const chatCommandRegistryPlugin: JupyterFrontEndPlugin = { }; const chatPlugin: JupyterFrontEndPlugin = { - id: '@jupyterlite/ai:chat', + id: PLUGIN_IDS.chat, description: 'LLM chat extension', autoStart: true, requires: [IAIProviderRegistry, IRenderMimeRegistry, IChatCommandRegistry], @@ -141,7 +141,7 @@ const chatPlugin: JupyterFrontEndPlugin = { }; const completerPlugin: JupyterFrontEndPlugin = { - id: '@jupyterlite/ai:completer', + id: PLUGIN_IDS.completer, autoStart: true, requires: [IAIProviderRegistry, ICompletionProviderManager], activate: ( @@ -157,59 +157,64 @@ const completerPlugin: JupyterFrontEndPlugin = { } }; -const providerRegistryPlugin: JupyterFrontEndPlugin = { - id: '@jupyterlite/ai:provider-registry', - autoStart: true, - requires: [IFormRendererRegistry, ISettingRegistry], - optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector], - provides: IAIProviderRegistry, - activate: ( - app: JupyterFrontEnd, - editorRegistry: IFormRendererRegistry, - settingRegistry: ISettingRegistry, - rmRegistry?: IRenderMimeRegistry, - secretsManager?: ISecretsManager, - settingConnector?: ISettingConnector - ): IAIProviderRegistry => { - const providerRegistry = new AIProviderRegistry({ secretsManager }); - - editorRegistry.addRenderer( - '@jupyterlite/ai:provider-registry.AIprovider', - aiSettingsRenderer({ - providerRegistry, - rmRegistry, - secretsManager, - settingConnector - }) - ); - - settingRegistry - .load(providerRegistryPlugin.id) - .then(settings => { - const updateProvider = () => { - // Update the settings to the AI providers. - const providerSettings = (settings.get('AIprovider').composite ?? { - provider: 'None' - }) as ReadonlyPartialJSONObject; - providerRegistry.setProvider({ - name: providerSettings.provider as string, - settings: providerSettings - }); - }; - - settings.changed.connect(() => updateProvider()); - updateProvider(); - }) - .catch(reason => { - console.error( - `Failed to load settings for ${providerRegistryPlugin.id}`, - reason - ); +const providerRegistryPlugin: JupyterFrontEndPlugin = + SecretsManager.sign(PLUGIN_IDS.providerRegistry, token => ({ + id: PLUGIN_IDS.providerRegistry, + autoStart: true, + requires: [IFormRendererRegistry, ISettingRegistry], + optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector], + provides: IAIProviderRegistry, + activate: ( + app: JupyterFrontEnd, + editorRegistry: IFormRendererRegistry, + settingRegistry: ISettingRegistry, + rmRegistry?: IRenderMimeRegistry, + secretsManager?: ISecretsManager, + settingConnector?: ISettingConnector + ): IAIProviderRegistry => { + const providerRegistry = new AIProviderRegistry({ + token, + secretsManager }); - return providerRegistry; - } -}; + editorRegistry.addRenderer( + `${PLUGIN_IDS.providerRegistry}.AIprovider`, + aiSettingsRenderer({ + providerRegistry, + secretsToken: token, + rmRegistry, + secretsManager, + settingConnector + }) + ); + + settingRegistry + .load(providerRegistryPlugin.id) + .then(settings => { + const updateProvider = () => { + // Update the settings to the AI providers. + const providerSettings = (settings.get('AIprovider').composite ?? { + provider: 'None' + }) as ReadonlyPartialJSONObject; + providerRegistry.setProvider({ + name: providerSettings.provider as string, + settings: providerSettings + }); + }; + + settings.changed.connect(() => updateProvider()); + updateProvider(); + }) + .catch(reason => { + console.error( + `Failed to load settings for ${providerRegistryPlugin.id}`, + reason + ); + }); + + return providerRegistry; + } + })); /** * Provides the settings connector as a separate plugin to allow for alternative @@ -217,7 +222,7 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = { * endpoint. */ const settingsConnector: JupyterFrontEndPlugin = { - id: '@jupyterlite/ai:settings-connector', + id: PLUGIN_IDS.settingsConnector, description: 'Provides a settings connector which does not save passwords.', autoStart: true, provides: ISettingConnector, diff --git a/src/provider.ts b/src/provider.ts index 0e87f5eb..5c3cf71a 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -6,18 +6,17 @@ import { JSONSchema7 } from 'json-schema'; import { ISecretsManager } from 'jupyter-secrets-manager'; import { IBaseCompleter } from './base-completer'; -import { - getSecretId, - SECRETS_NAMESPACE, - SECRETS_REPLACEMENT -} from './settings'; +import { getSecretId, SECRETS_REPLACEMENT } from './settings'; import { IAIProvider, IAIProviderRegistry, IDict, - ISetProviderOptions + ISetProviderOptions, + PLUGIN_IDS } from './tokens'; +const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry; + export const chatSystemPrompt = ( options: AIProviderRegistry.IPromptOptions ) => ` @@ -54,6 +53,7 @@ export class AIProviderRegistry implements IAIProviderRegistry { */ constructor(options: AIProviderRegistry.IOptions) { this._secretsManager = options.secretsManager || null; + Private.setToken(options.token); } /** @@ -171,7 +171,11 @@ export class AIProviderRegistry implements IAIProviderRegistry { for (const key of Object.keys(settings)) { if (settings[key] === SECRETS_REPLACEMENT) { const id = getSecretId(name, key); - const secrets = await this._secretsManager?.get(SECRETS_NAMESPACE, id); + const secrets = await this._secretsManager?.get( + Private.getToken(), + SECRETS_NAMESPACE, + id + ); fullSettings[key] = secrets?.value || settings[key]; continue; } @@ -236,6 +240,10 @@ export namespace AIProviderRegistry { * The secrets manager used in the application. */ secretsManager?: ISecretsManager; + /** + * The token used to request the secrets manager. + */ + token: symbol; } /** @@ -290,3 +298,24 @@ export namespace AIProviderRegistry { }); } } + +namespace Private { + /** + * The token to use with the secrets manager. + */ + let secretsToken: symbol; + + /** + * Set of the token. + */ + export function setToken(value: symbol): void { + secretsToken = value; + } + + /** + * get the token. + */ + export function getToken(): symbol { + return secretsToken; + } +} diff --git a/src/settings/panel.tsx b/src/settings/panel.tsx index 59c3d38e..451da39a 100644 --- a/src/settings/panel.tsx +++ b/src/settings/panel.tsx @@ -13,20 +13,27 @@ import { JSONSchema7 } from 'json-schema'; import { ISecretsManager } from 'jupyter-secrets-manager'; import React from 'react'; -import { getSecretId, SECRETS_NAMESPACE, SettingConnector } from '.'; +import { getSecretId, SettingConnector } from '.'; import baseSettings from './base.json'; -import { IAIProviderRegistry, IDict } from '../tokens'; +import { IAIProviderRegistry, IDict, PLUGIN_IDS } from '../tokens'; const MD_MIME_TYPE = 'text/markdown'; const STORAGE_NAME = '@jupyterlite/ai:settings'; const INSTRUCTION_CLASS = 'jp-AISettingsInstructions'; +const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry; export const aiSettingsRenderer = (options: { providerRegistry: IAIProviderRegistry; + secretsToken?: symbol; rmRegistry?: IRenderMimeRegistry; secretsManager?: ISecretsManager; settingConnector?: ISettingConnector; }): IFormRenderer => { + const { secretsToken } = options; + delete options.secretsToken; + if (secretsToken) { + Private.setToken(secretsToken); + } return { fieldRenderer: (props: FieldProps) => { props.formContext = { ...props.formContext, ...options }; @@ -128,7 +135,7 @@ export class AiSettings extends React.Component< return; } - await this._secretsManager.detachAll(SECRETS_NAMESPACE); + await this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE); this._formInputs = [...inputs]; this._unsavedFields = []; for (let i = 0; i < inputs.length; i++) { @@ -137,6 +144,7 @@ export class AiSettings extends React.Component< if (label) { const id = getSecretId(this._provider, label); this._secretsManager.attach( + Private.getToken(), SECRETS_NAMESPACE, id, inputs[i], @@ -151,6 +159,13 @@ export class AiSettings extends React.Component< } } + componentWillUnmount(): void { + if (!this._secretsManager || !this._useSecretsManager) { + return; + } + this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE); + } + /** * Get the current provider from the local storage. */ @@ -192,7 +207,7 @@ export class AiSettings extends React.Component< if (!value) { // Detach all the password inputs attached to the secrets manager, and save the // current settings to the local storage to save the password. - this._secretsManager?.detachAll(SECRETS_NAMESPACE); + this._secretsManager?.detachAll(Private.getToken(), SECRETS_NAMESPACE); this._formInputs = []; this._unsavedFields = []; if (this._settingConnector instanceof SettingConnector) { @@ -359,3 +374,24 @@ export class AiSettings extends React.Component< private _unsavedFields: string[] = []; private _formInputs: HTMLInputElement[] = []; } + +namespace Private { + /** + * The token to use with the secrets manager. + */ + let secretsToken: symbol; + + /** + * Set of the token. + */ + export function setToken(value: symbol): void { + secretsToken = value; + } + + /** + * get the token. + */ + export function getToken(): symbol { + return secretsToken; + } +} diff --git a/src/settings/utils.ts b/src/settings/utils.ts index a20333bc..15e55555 100644 --- a/src/settings/utils.ts +++ b/src/settings/utils.ts @@ -1,4 +1,3 @@ -export const SECRETS_NAMESPACE = '@jupyterlite/ai'; export const SECRETS_REPLACEMENT = '***'; export function getSecretId(provider: string, label: string) { diff --git a/src/tokens.ts b/src/tokens.ts index 03fbb3ff..b9fa2d39 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -5,6 +5,14 @@ import { JSONSchema7 } from 'json-schema'; import { IBaseCompleter } from './base-completer'; +export const PLUGIN_IDS = { + chat: '@jupyterlite/ai:chat', + chatCommandRegistry: '@jupyterlite/ai:autocompletion-registry', + completer: '@jupyterlite/ai:completer', + providerRegistry: '@jupyterlite/ai:provider-registry', + settingsConnector: '@jupyterlite/ai:settings-connector' +}; + export interface IDict { [key: string]: T; } diff --git a/yarn.lock b/yarn.lock index affb7ac5..c2d30ea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1169,7 +1169,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/coreutils@npm:^6.4.0, @jupyterlab/coreutils@npm:^6.4.1": +"@jupyterlab/coreutils@npm:^6.0.0, @jupyterlab/coreutils@npm:^6.4.0, @jupyterlab/coreutils@npm:^6.4.1": version: 6.4.1 resolution: "@jupyterlab/coreutils@npm:6.4.1" dependencies: @@ -1614,7 +1614,7 @@ __metadata: eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^5.0.0 json5: ^2.2.3 - jupyter-secrets-manager: ^0.2.0 + jupyter-secrets-manager: ^0.3.0 npm-run-all: ^4.1.5 prettier: ^3.0.0 react: ^18.2.0 @@ -5715,15 +5715,16 @@ __metadata: languageName: node linkType: hard -"jupyter-secrets-manager@npm:^0.2.0": - version: 0.2.0 - resolution: "jupyter-secrets-manager@npm:0.2.0" +"jupyter-secrets-manager@npm:^0.3.0": + version: 0.3.0 + resolution: "jupyter-secrets-manager@npm:0.3.0" dependencies: "@jupyterlab/application": ^4.0.0 + "@jupyterlab/coreutils": ^6.0.0 "@jupyterlab/statedb": ^4.0.0 "@lumino/algorithm": ^2.0.0 "@lumino/coreutils": ^2.1.2 - checksum: 7745809a2b1922247b100ed36fabf8ffaa96c73b4343233ce70bfffec1ff131ac365a190e2661516642f57f9d84c6f0a834643bbbf7ebc98579ae2961bcc645f + checksum: 8e0b9dd4acf746c3e8383a8de2621745297f7e96a323b7a5469e9cb487d4ddf0ef36af9b716a53e85868e922dfae3632dc4a961b8c059b2b79b5a0b93f2146d5 languageName: node linkType: hard