Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
119 changes: 62 additions & 57 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IChatCommandRegistry> = {
id: '@jupyterlite/ai:autocompletion-registry',
id: PLUGIN_IDS.chatCommandRegistry,
description: 'Autocompletion registry',
autoStart: true,
provides: IChatCommandRegistry,
Expand All @@ -44,7 +44,7 @@ const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
};

const chatPlugin: JupyterFrontEndPlugin<void> = {
id: '@jupyterlite/ai:chat',
id: PLUGIN_IDS.chat,
description: 'LLM chat extension',
autoStart: true,
requires: [IAIProviderRegistry, IRenderMimeRegistry, IChatCommandRegistry],
Expand Down Expand Up @@ -141,7 +141,7 @@ const chatPlugin: JupyterFrontEndPlugin<void> = {
};

const completerPlugin: JupyterFrontEndPlugin<void> = {
id: '@jupyterlite/ai:completer',
id: PLUGIN_IDS.completer,
autoStart: true,
requires: [IAIProviderRegistry, ICompletionProviderManager],
activate: (
Expand All @@ -157,67 +157,72 @@ const completerPlugin: JupyterFrontEndPlugin<void> = {
}
};

const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
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<IAIProviderRegistry> =
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
* implementations that may want to fetch settings from a different source or
* endpoint.
*/
const settingsConnector: JupyterFrontEndPlugin<ISettingConnector> = {
id: '@jupyterlite/ai:settings-connector',
id: PLUGIN_IDS.settingsConnector,
description: 'Provides a settings connector which does not save passwords.',
autoStart: true,
provides: ISettingConnector,
Expand Down
43 changes: 36 additions & 7 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) => `
Expand Down Expand Up @@ -54,6 +53,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
*/
constructor(options: AIProviderRegistry.IOptions) {
this._secretsManager = options.secretsManager || null;
Private.setToken(options.token);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
}
}
44 changes: 40 additions & 4 deletions src/settings/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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++) {
Expand All @@ -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],
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
1 change: 0 additions & 1 deletion src/settings/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const SECRETS_NAMESPACE = '@jupyterlite/ai';
export const SECRETS_REPLACEMENT = '***';

export function getSecretId(provider: string, label: string) {
Expand Down
8 changes: 8 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any> {
[key: string]: T;
}
Expand Down
Loading