@jupyterlite/ai 0.5.0 → 0.6.0

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.
@@ -22,12 +22,14 @@ export declare class ChatHandler extends ChatModel {
22
22
  getHistory(): Promise<IChatHistory>;
23
23
  dispose(): void;
24
24
  messageAdded(message: IChatMessage): void;
25
+ stopStreaming(): void;
25
26
  private _providerRegistry;
26
27
  private _personaName;
27
28
  private _prompt;
28
29
  private _errorMessage;
29
30
  private _history;
30
31
  private _defaultErrorMessage;
32
+ private _controller;
31
33
  }
32
34
  export declare namespace ChatHandler {
33
35
  interface IOptions extends ChatModel.IOptions {
@@ -20,6 +20,7 @@ export class ChatHandler extends ChatModel {
20
20
  this._errorMessage = '';
21
21
  this._history = { messages: [] };
22
22
  this._defaultErrorMessage = 'AI provider not configured';
23
+ this._controller = null;
23
24
  this._providerRegistry = options.providerRegistry;
24
25
  this._prompt = chatSystemPrompt({
25
26
  provider_name: this._providerRegistry.currentName
@@ -107,8 +108,9 @@ export class ChatHandler extends ChatModel {
107
108
  type: 'msg'
108
109
  };
109
110
  let content = '';
111
+ this._controller = new AbortController();
110
112
  try {
111
- for await (const chunk of await this._providerRegistry.currentChatModel.stream(messages)) {
113
+ for await (const chunk of await this._providerRegistry.currentChatModel.stream(messages, { signal: this._controller.signal })) {
112
114
  content += (_a = chunk.content) !== null && _a !== void 0 ? _a : chunk;
113
115
  botMsg.body = content;
114
116
  this.messageAdded(botMsg);
@@ -130,6 +132,7 @@ export class ChatHandler extends ChatModel {
130
132
  }
131
133
  finally {
132
134
  this.updateWriters([]);
135
+ this._controller = null;
133
136
  }
134
137
  }
135
138
  async getHistory() {
@@ -141,6 +144,10 @@ export class ChatHandler extends ChatModel {
141
144
  messageAdded(message) {
142
145
  super.messageAdded(message);
143
146
  }
147
+ stopStreaming() {
148
+ var _a;
149
+ (_a = this._controller) === null || _a === void 0 ? void 0 : _a.abort();
150
+ }
144
151
  }
145
152
  (function (ChatHandler) {
146
153
  class ClearCommandProvider {
@@ -21,7 +21,14 @@ export declare class CompletionProvider implements IInlineCompletionProvider {
21
21
  }
22
22
  export declare namespace CompletionProvider {
23
23
  interface IOptions {
24
+ /**
25
+ * The registry where the completion provider belongs.
26
+ */
24
27
  providerRegistry: IAIProviderRegistry;
28
+ /**
29
+ * The request completion commands, can be useful if a provider needs to request
30
+ * the completion by itself.
31
+ */
25
32
  requestCompletion: () => void;
26
33
  }
27
34
  }
@@ -0,0 +1,19 @@
1
+ /// <reference types="react" />
2
+ import { InputToolbarRegistry } from '@jupyter/chat';
3
+ /**
4
+ * Properties of the stop button.
5
+ */
6
+ export interface IStopButtonProps extends InputToolbarRegistry.IToolbarItemProps {
7
+ /**
8
+ * The function to stop streaming.
9
+ */
10
+ stopStreaming: () => void;
11
+ }
12
+ /**
13
+ * The stop button.
14
+ */
15
+ export declare function StopButton(props: IStopButtonProps): JSX.Element;
16
+ /**
17
+ * factory returning the toolbar item.
18
+ */
19
+ export declare function stopItem(stopStreaming: () => void): InputToolbarRegistry.IToolbarItem;
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import StopIcon from '@mui/icons-material/Stop';
6
+ import React from 'react';
7
+ import { TooltippedButton } from '@jupyter/chat';
8
+ /**
9
+ * The stop button.
10
+ */
11
+ export function StopButton(props) {
12
+ const tooltip = 'Stop streaming';
13
+ return (React.createElement(TooltippedButton, { onClick: props.stopStreaming, tooltip: tooltip, buttonProps: {
14
+ size: 'small',
15
+ variant: 'contained',
16
+ title: tooltip
17
+ } },
18
+ React.createElement(StopIcon, null)));
19
+ }
20
+ /**
21
+ * factory returning the toolbar item.
22
+ */
23
+ export function stopItem(stopStreaming) {
24
+ return {
25
+ element: (props) => {
26
+ const stopProps = { ...props, stopStreaming };
27
+ return StopButton(stopProps);
28
+ },
29
+ position: 50,
30
+ hidden: true /* hidden by default */
31
+ };
32
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { IChatCommandRegistry } from '@jupyter/chat';
2
2
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
3
+ import { ISettingConnector } from '@jupyterlab/settingregistry';
3
4
  import { IAIProviderRegistry } from './tokens';
4
- declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IChatCommandRegistry> | JupyterFrontEndPlugin<IAIProviderRegistry>)[];
5
+ declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IChatCommandRegistry> | JupyterFrontEndPlugin<IAIProviderRegistry> | JupyterFrontEndPlugin<ISettingConnector>)[];
5
6
  export default _default;
package/lib/index.js CHANGED
@@ -1,17 +1,18 @@
1
- import { ActiveCellManager, buildChatSidebar, buildErrorWidget, ChatCommandRegistry, IChatCommandRegistry } from '@jupyter/chat';
1
+ import { ActiveCellManager, buildChatSidebar, buildErrorWidget, ChatCommandRegistry, IChatCommandRegistry, InputToolbarRegistry } from '@jupyter/chat';
2
2
  import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { ICompletionProviderManager } from '@jupyterlab/completer';
4
4
  import { INotebookTracker } from '@jupyterlab/notebook';
5
5
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
6
- import { ISettingRegistry } from '@jupyterlab/settingregistry';
6
+ import { ISettingConnector, ISettingRegistry } from '@jupyterlab/settingregistry';
7
7
  import { IFormRendererRegistry } from '@jupyterlab/ui-components';
8
8
  import { ISecretsManager } from 'jupyter-secrets-manager';
9
9
  import { ChatHandler } from './chat-handler';
10
10
  import { CompletionProvider } from './completion-provider';
11
11
  import { defaultProviderPlugins } from './default-providers';
12
12
  import { AIProviderRegistry } from './provider';
13
- import { aiSettingsRenderer } from './settings/panel';
13
+ import { aiSettingsRenderer, SettingConnector } from './settings';
14
14
  import { IAIProviderRegistry } from './tokens';
15
+ import { stopItem } from './components/stop-button';
15
16
  const chatCommandRegistryPlugin = {
16
17
  id: '@jupyterlite/ai:autocompletion-registry',
17
18
  description: 'Autocompletion registry',
@@ -66,12 +67,26 @@ const chatPlugin = {
66
67
  console.error(`Something went wrong when reading the settings.\n${reason}`);
67
68
  });
68
69
  let chatWidget = null;
70
+ const inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
71
+ const stopButton = stopItem(() => chatHandler.stopStreaming());
72
+ inputToolbarRegistry.addItem('stop', stopButton);
73
+ chatHandler.writersChanged.connect((_, users) => {
74
+ if (users.filter(user => user.username === chatHandler.personaName).length) {
75
+ inputToolbarRegistry.hide('send');
76
+ inputToolbarRegistry.show('stop');
77
+ }
78
+ else {
79
+ inputToolbarRegistry.hide('stop');
80
+ inputToolbarRegistry.show('send');
81
+ }
82
+ });
69
83
  try {
70
84
  chatWidget = buildChatSidebar({
71
85
  model: chatHandler,
72
86
  themeManager,
73
87
  rmRegistry,
74
- chatCommandRegistry
88
+ chatCommandRegistry,
89
+ inputToolbarRegistry
75
90
  });
76
91
  chatWidget.title.caption = 'Jupyterlite AI Chat';
77
92
  }
@@ -98,11 +113,16 @@ const providerRegistryPlugin = {
98
113
  id: '@jupyterlite/ai:provider-registry',
99
114
  autoStart: true,
100
115
  requires: [IFormRendererRegistry, ISettingRegistry],
101
- optional: [IRenderMimeRegistry, ISecretsManager],
116
+ optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector],
102
117
  provides: IAIProviderRegistry,
103
- activate: (app, editorRegistry, settingRegistry, rmRegistry, secretsManager) => {
104
- const providerRegistry = new AIProviderRegistry();
105
- editorRegistry.addRenderer('@jupyterlite/ai:provider-registry.AIprovider', aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager }));
118
+ activate: (app, editorRegistry, settingRegistry, rmRegistry, secretsManager, settingConnector) => {
119
+ const providerRegistry = new AIProviderRegistry({ secretsManager });
120
+ editorRegistry.addRenderer('@jupyterlite/ai:provider-registry.AIprovider', aiSettingsRenderer({
121
+ providerRegistry,
122
+ rmRegistry,
123
+ secretsManager,
124
+ settingConnector
125
+ }));
106
126
  settingRegistry
107
127
  .load(providerRegistryPlugin.id)
108
128
  .then(settings => {
@@ -112,7 +132,10 @@ const providerRegistryPlugin = {
112
132
  const providerSettings = ((_a = settings.get('AIprovider').composite) !== null && _a !== void 0 ? _a : {
113
133
  provider: 'None'
114
134
  });
115
- providerRegistry.setProvider(providerSettings.provider, providerSettings);
135
+ providerRegistry.setProvider({
136
+ name: providerSettings.provider,
137
+ settings: providerSettings
138
+ });
116
139
  };
117
140
  settings.changed.connect(() => updateProvider());
118
141
  updateProvider();
@@ -123,10 +146,23 @@ const providerRegistryPlugin = {
123
146
  return providerRegistry;
124
147
  }
125
148
  };
149
+ /**
150
+ * Provides the settings connector as a separate plugin to allow for alternative
151
+ * implementations that may want to fetch settings from a different source or
152
+ * endpoint.
153
+ */
154
+ const settingsConnector = {
155
+ id: '@jupyterlite/ai:settings-connector',
156
+ description: 'Provides a settings connector which does not save passwords.',
157
+ autoStart: true,
158
+ provides: ISettingConnector,
159
+ activate: (app) => new SettingConnector(app.serviceManager.settings)
160
+ };
126
161
  export default [
127
162
  providerRegistryPlugin,
128
163
  chatCommandRegistryPlugin,
129
164
  chatPlugin,
130
165
  completerPlugin,
166
+ settingsConnector,
131
167
  ...defaultProviderPlugins
132
168
  ];
package/lib/provider.d.ts CHANGED
@@ -1,14 +1,18 @@
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 } from '@lumino/signaling';
5
4
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
6
- import { IBaseCompleter } from './base-completer';
7
- import { IAIProvider, IAIProviderRegistry } from './tokens';
8
5
  import { JSONSchema7 } from 'json-schema';
6
+ import { ISecretsManager } from 'jupyter-secrets-manager';
7
+ import { IBaseCompleter } from './base-completer';
8
+ import { IAIProvider, IAIProviderRegistry, ISetProviderOptions } from './tokens';
9
9
  export declare const chatSystemPrompt: (options: AIProviderRegistry.IPromptOptions) => string;
10
10
  export declare const COMPLETION_SYSTEM_PROMPT = "\nYou are an application built to provide helpful code completion suggestions.\nYou should only produce code. Keep comments to minimum, use the\nprogramming language comment syntax. Produce clean code.\nThe code is written in JupyterLab, a data analysis and code development\nenvironment which can execute code extended with additional syntax for\ninteractive features, such as magics.\nOnly give raw strings back, do not format the response using backticks.\nThe output should be a single string, and should correspond to what a human users\nwould write.\nDo not include the prompt in the output, only the string that should be appended to the current input.\n";
11
11
  export declare class AIProviderRegistry implements IAIProviderRegistry {
12
+ /**
13
+ * The constructor of the provider registry.
14
+ */
15
+ constructor(options: AIProviderRegistry.IOptions);
12
16
  /**
13
17
  * Get the list of provider names.
14
18
  */
@@ -53,14 +57,14 @@ export declare class AIProviderRegistry implements IAIProviderRegistry {
53
57
  * Set the providers (chat model and completer).
54
58
  * Creates the providers if the name has changed, otherwise only updates their config.
55
59
  *
56
- * @param name - the name of the provider to use.
57
- * @param settings - the settings for the models.
60
+ * @param options - An object with the name and the settings of the provider to use.
58
61
  */
59
- setProvider(name: string, settings: ReadonlyPartialJSONObject): void;
62
+ setProvider(options: ISetProviderOptions): Promise<void>;
60
63
  /**
61
64
  * A signal emitting when the provider or its settings has changed.
62
65
  */
63
66
  get providerChanged(): ISignal<IAIProviderRegistry, void>;
67
+ private _secretsManager;
64
68
  private _currentProvider;
65
69
  private _completer;
66
70
  private _chatModel;
@@ -69,6 +73,7 @@ export declare class AIProviderRegistry implements IAIProviderRegistry {
69
73
  private _chatError;
70
74
  private _completerError;
71
75
  private _providers;
76
+ private _deferredProvider;
72
77
  }
73
78
  export declare namespace AIProviderRegistry {
74
79
  /**
@@ -76,13 +81,9 @@ export declare namespace AIProviderRegistry {
76
81
  */
77
82
  interface IOptions {
78
83
  /**
79
- * The completion provider manager in which register the LLM completer.
80
- */
81
- completionProviderManager: ICompletionProviderManager;
82
- /**
83
- * The application commands registry.
84
+ * The secrets manager used in the application.
84
85
  */
85
- requestCompletion: () => void;
86
+ secretsManager?: ISecretsManager;
86
87
  }
87
88
  /**
88
89
  * The options for the Chat system prompt.
package/lib/provider.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Signal } from '@lumino/signaling';
2
+ import { getSecretId, SECRETS_NAMESPACE, SECRETS_REPLACEMENT } from './settings';
2
3
  export const chatSystemPrompt = (options) => `
3
4
  You are Jupyternaut, a conversational assistant living in JupyterLab to help users.
4
5
  You are not a language model, but rather an application built on a foundation model from ${options.provider_name}.
@@ -26,7 +27,10 @@ would write.
26
27
  Do not include the prompt in the output, only the string that should be appended to the current input.
27
28
  `;
28
29
  export class AIProviderRegistry {
29
- constructor() {
30
+ /**
31
+ * The constructor of the provider registry.
32
+ */
33
+ constructor(options) {
30
34
  this._currentProvider = null;
31
35
  this._completer = null;
32
36
  this._chatModel = null;
@@ -35,6 +39,8 @@ export class AIProviderRegistry {
35
39
  this._chatError = '';
36
40
  this._completerError = '';
37
41
  this._providers = new Map();
42
+ this._deferredProvider = null;
43
+ this._secretsManager = options.secretsManager || null;
38
44
  }
39
45
  /**
40
46
  * Get the list of provider names.
@@ -46,10 +52,15 @@ export class AIProviderRegistry {
46
52
  * Add a new provider.
47
53
  */
48
54
  add(provider) {
55
+ var _a;
49
56
  if (this._providers.has(provider.name)) {
50
57
  throw new Error(`A AI provider named '${provider.name}' is already registered`);
51
58
  }
52
59
  this._providers.set(provider.name, provider);
60
+ // Set the provider if the loading has been deferred.
61
+ if (provider.name === ((_a = this._deferredProvider) === null || _a === void 0 ? void 0 : _a.name)) {
62
+ this.setProvider(this._deferredProvider);
63
+ }
53
64
  }
54
65
  /**
55
66
  * Get the current provider name.
@@ -119,15 +130,36 @@ export class AIProviderRegistry {
119
130
  * Set the providers (chat model and completer).
120
131
  * Creates the providers if the name has changed, otherwise only updates their config.
121
132
  *
122
- * @param name - the name of the provider to use.
123
- * @param settings - the settings for the models.
133
+ * @param options - An object with the name and the settings of the provider to use.
124
134
  */
125
- setProvider(name, settings) {
126
- var _a, _b, _c;
135
+ async setProvider(options) {
136
+ var _a, _b, _c, _d;
137
+ const { name, settings } = options;
127
138
  this._currentProvider = (_a = this._providers.get(name)) !== null && _a !== void 0 ? _a : null;
128
- if (((_b = this._currentProvider) === null || _b === void 0 ? void 0 : _b.completer) !== undefined) {
139
+ if (this._currentProvider === null) {
140
+ // The current provider may not be loaded when the settings are first loaded.
141
+ // Let's defer the provider loading.
142
+ this._deferredProvider = options;
143
+ }
144
+ else {
145
+ this._deferredProvider = null;
146
+ }
147
+ // Build a new settings object containing the secrets.
148
+ const fullSettings = {};
149
+ for (const key of Object.keys(settings)) {
150
+ if (settings[key] === SECRETS_REPLACEMENT) {
151
+ const id = getSecretId(name, key);
152
+ const secrets = await ((_b = this._secretsManager) === null || _b === void 0 ? void 0 : _b.get(SECRETS_NAMESPACE, id));
153
+ fullSettings[key] = (secrets === null || secrets === void 0 ? void 0 : secrets.value) || settings[key];
154
+ continue;
155
+ }
156
+ fullSettings[key] = settings[key];
157
+ }
158
+ if (((_c = this._currentProvider) === null || _c === void 0 ? void 0 : _c.completer) !== undefined) {
129
159
  try {
130
- this._completer = new this._currentProvider.completer({ ...settings });
160
+ this._completer = new this._currentProvider.completer({
161
+ ...fullSettings
162
+ });
131
163
  this._completerError = '';
132
164
  }
133
165
  catch (e) {
@@ -137,9 +169,11 @@ export class AIProviderRegistry {
137
169
  else {
138
170
  this._completer = null;
139
171
  }
140
- if (((_c = this._currentProvider) === null || _c === void 0 ? void 0 : _c.chatModel) !== undefined) {
172
+ if (((_d = this._currentProvider) === null || _d === void 0 ? void 0 : _d.chatModel) !== undefined) {
141
173
  try {
142
- this._chatModel = new this._currentProvider.chatModel({ ...settings });
174
+ this._chatModel = new this._currentProvider.chatModel({
175
+ ...fullSettings
176
+ });
143
177
  this._chatError = '';
144
178
  }
145
179
  catch (e) {
@@ -0,0 +1,3 @@
1
+ export * from './panel';
2
+ export * from './settings-connector';
3
+ export * from './utils';
@@ -0,0 +1,3 @@
1
+ export * from './panel';
2
+ export * from './settings-connector';
3
+ export * from './utils';
@@ -1,4 +1,5 @@
1
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
+ import { ISettingConnector } from '@jupyterlab/settingregistry';
2
3
  import { IFormRenderer } from '@jupyterlab/ui-components';
3
4
  import type { FieldProps } from '@rjsf/utils';
4
5
  import { JSONSchema7 } from 'json-schema';
@@ -9,6 +10,7 @@ export declare const aiSettingsRenderer: (options: {
9
10
  providerRegistry: IAIProviderRegistry;
10
11
  rmRegistry?: IRenderMimeRegistry;
11
12
  secretsManager?: ISecretsManager;
13
+ settingConnector?: ISettingConnector;
12
14
  }) => IFormRenderer;
13
15
  export interface ISettingsFormStates {
14
16
  schema: JSONSchema7;
@@ -75,6 +77,7 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
75
77
  private _useSecretsManager;
76
78
  private _rmRegistry;
77
79
  private _secretsManager;
80
+ private _settingConnector;
78
81
  private _currentSettings;
79
82
  private _uiSchema;
80
83
  private _settings;
@@ -3,8 +3,8 @@ import { ArrayExt } from '@lumino/algorithm';
3
3
  import { JSONExt } from '@lumino/coreutils';
4
4
  import validator from '@rjsf/validator-ajv8';
5
5
  import React from 'react';
6
+ import { getSecretId, SECRETS_NAMESPACE, SettingConnector } from '.';
6
7
  import baseSettings from './base.json';
7
- const SECRETS_NAMESPACE = '@jupyterlite/ai';
8
8
  const MD_MIME_TYPE = 'text/markdown';
9
9
  const STORAGE_NAME = '@jupyterlite/ai:settings';
10
10
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
@@ -21,7 +21,7 @@ const WrappedFormComponent = (props) => {
21
21
  };
22
22
  export class AiSettings extends React.Component {
23
23
  constructor(props) {
24
- var _a, _b, _c, _d;
24
+ var _a, _b, _c, _d, _e;
25
25
  super(props);
26
26
  this.updateUseSecretsManager = (value) => {
27
27
  var _a;
@@ -32,6 +32,9 @@ export class AiSettings extends React.Component {
32
32
  (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(SECRETS_NAMESPACE);
33
33
  this._formInputs = [];
34
34
  this._unsavedFields = [];
35
+ if (this._settingConnector instanceof SettingConnector) {
36
+ this._settingConnector.doNotSave = [];
37
+ }
35
38
  this.saveSettings(this._currentSettings);
36
39
  }
37
40
  else {
@@ -48,6 +51,9 @@ export class AiSettings extends React.Component {
48
51
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
49
52
  this.componentDidUpdate();
50
53
  }
54
+ this._settings
55
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
56
+ .catch(console.error);
51
57
  };
52
58
  /**
53
59
  * Triggered when the provider hes changed, to update the schema and values.
@@ -100,9 +106,10 @@ export class AiSettings extends React.Component {
100
106
  this._providerRegistry = props.formContext.providerRegistry;
101
107
  this._rmRegistry = (_a = props.formContext.rmRegistry) !== null && _a !== void 0 ? _a : null;
102
108
  this._secretsManager = (_b = props.formContext.secretsManager) !== null && _b !== void 0 ? _b : null;
109
+ this._settingConnector = (_c = props.formContext.settingConnector) !== null && _c !== void 0 ? _c : null;
103
110
  this._settings = props.formContext.settings;
104
111
  this._useSecretsManager =
105
- (_c = this._settings.get('UseSecretsManager').composite) !== null && _c !== void 0 ? _c : true;
112
+ (_d = this._settings.get('UseSecretsManager').composite) !== null && _d !== void 0 ? _d : true;
106
113
  // Initialize the providers schema.
107
114
  const providerSchema = JSONExt.deepCopy(baseSettings);
108
115
  providerSchema.properties.provider = {
@@ -120,7 +127,7 @@ export class AiSettings extends React.Component {
120
127
  const labSettings = this._settings.get('AIprovider').composite;
121
128
  if (labSettings && Object.keys(labSettings).includes('provider')) {
122
129
  // Get the provider name.
123
- const provider = (_d = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _d === void 0 ? void 0 : _d[1];
130
+ const provider = (_e = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _e === void 0 ? void 0 : _e[1];
124
131
  // Save the settings.
125
132
  const settings = {
126
133
  _current: provider
@@ -165,12 +172,15 @@ export class AiSettings extends React.Component {
165
172
  if (inputs[i].type.toLowerCase() === 'password') {
166
173
  const label = inputs[i].getAttribute('label');
167
174
  if (label) {
168
- const id = `${this._provider}-${label}`;
175
+ const id = getSecretId(this._provider, label);
169
176
  this._secretsManager.attach(SECRETS_NAMESPACE, id, inputs[i], (value) => this._onPasswordUpdated(label, value));
170
177
  this._unsavedFields.push(label);
171
178
  }
172
179
  }
173
180
  }
181
+ if (this._settingConnector instanceof SettingConnector) {
182
+ this._settingConnector.doNotSave = this._unsavedFields;
183
+ }
174
184
  }
175
185
  /**
176
186
  * Get the current provider from the local storage.
@@ -0,0 +1,31 @@
1
+ import { ISettingConnector, ISettingRegistry } from '@jupyterlab/settingregistry';
2
+ import { DataConnector, IDataConnector } from '@jupyterlab/statedb';
3
+ /**
4
+ * A data connector for fetching settings.
5
+ *
6
+ * #### Notes
7
+ * This connector adds a query parameter to the base services setting manager.
8
+ */
9
+ export declare class SettingConnector extends DataConnector<ISettingRegistry.IPlugin, string> implements ISettingConnector {
10
+ constructor(connector: IDataConnector<ISettingRegistry.IPlugin, string>);
11
+ set doNotSave(fields: string[]);
12
+ /**
13
+ * Fetch settings for a plugin.
14
+ * @param id - The plugin ID
15
+ *
16
+ * #### Notes
17
+ * The REST API requests are throttled at one request per plugin per 100ms.
18
+ */
19
+ fetch(id: string): Promise<ISettingRegistry.IPlugin | undefined>;
20
+ list(query: 'ids'): Promise<{
21
+ ids: string[];
22
+ }>;
23
+ list(query: 'active' | 'all'): Promise<{
24
+ ids: string[];
25
+ values: ISettingRegistry.IPlugin[];
26
+ }>;
27
+ save(id: string, raw: string): Promise<void>;
28
+ private _connector;
29
+ private _doNotSave;
30
+ private _throttlers;
31
+ }
@@ -0,0 +1,61 @@
1
+ import { PageConfig } from '@jupyterlab/coreutils';
2
+ import { DataConnector } from '@jupyterlab/statedb';
3
+ import { Throttler } from '@lumino/polling';
4
+ import * as json5 from 'json5';
5
+ import { SECRETS_REPLACEMENT } from '.';
6
+ /**
7
+ * A data connector for fetching settings.
8
+ *
9
+ * #### Notes
10
+ * This connector adds a query parameter to the base services setting manager.
11
+ */
12
+ export class SettingConnector extends DataConnector {
13
+ constructor(connector) {
14
+ super();
15
+ this._doNotSave = [];
16
+ this._throttlers = Object.create(null);
17
+ this._connector = connector;
18
+ }
19
+ set doNotSave(fields) {
20
+ this._doNotSave = [...fields];
21
+ }
22
+ /**
23
+ * Fetch settings for a plugin.
24
+ * @param id - The plugin ID
25
+ *
26
+ * #### Notes
27
+ * The REST API requests are throttled at one request per plugin per 100ms.
28
+ */
29
+ fetch(id) {
30
+ const throttlers = this._throttlers;
31
+ if (!(id in throttlers)) {
32
+ throttlers[id] = new Throttler(() => this._connector.fetch(id), 100);
33
+ }
34
+ return throttlers[id].invoke();
35
+ }
36
+ async list(query = 'all') {
37
+ const { isDisabled } = PageConfig.Extension;
38
+ const { ids, values } = await this._connector.list(query === 'ids' ? 'ids' : undefined);
39
+ if (query === 'all') {
40
+ return { ids, values };
41
+ }
42
+ if (query === 'ids') {
43
+ return { ids };
44
+ }
45
+ return {
46
+ ids: ids.filter(id => !isDisabled(id)),
47
+ values: values.filter(({ id }) => !isDisabled(id))
48
+ };
49
+ }
50
+ async save(id, raw) {
51
+ const settings = json5.parse(raw);
52
+ this._doNotSave.forEach(field => {
53
+ if (settings['AIprovider'] !== undefined &&
54
+ settings['AIprovider'][field] !== undefined &&
55
+ settings['AIprovider'][field] !== '') {
56
+ settings['AIprovider'][field] = SECRETS_REPLACEMENT;
57
+ }
58
+ });
59
+ await this._connector.save(id, json5.stringify(settings, null, 2));
60
+ }
61
+ }
@@ -0,0 +1,3 @@
1
+ export declare const SECRETS_NAMESPACE = "@jupyterlite/ai";
2
+ export declare const SECRETS_REPLACEMENT = "***";
3
+ export declare function getSecretId(provider: string, label: string): string;
@@ -0,0 +1,5 @@
1
+ export const SECRETS_NAMESPACE = '@jupyterlite/ai';
2
+ export const SECRETS_REPLACEMENT = '***';
3
+ export function getSecretId(provider, label) {
4
+ return `${provider}-${label}`;
5
+ }
package/lib/tokens.d.ts CHANGED
@@ -80,10 +80,9 @@ export interface IAIProviderRegistry {
80
80
  * Set the providers (chat model and completer).
81
81
  * Creates the providers if the name has changed, otherwise only updates their config.
82
82
  *
83
- * @param name - the name of the provider to use.
84
- * @param settings - the settings for the models.
83
+ * @param options - an object with the name and the settings of the provider to use.
85
84
  */
86
- setProvider(name: string, settings: ReadonlyPartialJSONObject): void;
85
+ setProvider(options: ISetProviderOptions): void;
87
86
  /**
88
87
  * A signal emitting when the provider or its settings has changed.
89
88
  */
@@ -97,6 +96,19 @@ export interface IAIProviderRegistry {
97
96
  */
98
97
  readonly completerError: string;
99
98
  }
99
+ /**
100
+ * The set provider options.
101
+ */
102
+ export interface ISetProviderOptions {
103
+ /**
104
+ * The name of the provider.
105
+ */
106
+ name: string;
107
+ /**
108
+ * The settings of the provider.
109
+ */
110
+ settings: ReadonlyPartialJSONObject;
111
+ }
100
112
  /**
101
113
  * The provider registry token.
102
114
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -56,13 +56,13 @@
56
56
  "watch:labextension": "jupyter labextension watch ."
57
57
  },
58
58
  "dependencies": {
59
- "@jupyter/chat": "^0.8.1",
59
+ "@jupyter/chat": "^0.9.0",
60
60
  "@jupyterlab/application": "^4.4.0-alpha.0",
61
61
  "@jupyterlab/apputils": "^4.5.0-alpha.0",
62
62
  "@jupyterlab/completer": "^4.4.0-alpha.0",
63
63
  "@jupyterlab/notebook": "^4.4.0-alpha.0",
64
64
  "@jupyterlab/rendermime": "^4.4.0-alpha.0",
65
- "@jupyterlab/settingregistry": "^4.4.0-alpha.0",
65
+ "@jupyterlab/settingregistry": "^4.4.0-beta.1",
66
66
  "@jupyterlab/ui-components": "^4.4.0-alpha.0",
67
67
  "@langchain/anthropic": "^0.3.9",
68
68
  "@langchain/community": "^0.3.31",
@@ -77,7 +77,8 @@
77
77
  "@rjsf/core": "^4.2.0",
78
78
  "@rjsf/utils": "^5.18.4",
79
79
  "@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",
81
82
  "react": "^18.2.0",
82
83
  "react-dom": "^18.2.0"
83
84
  },
@@ -118,6 +119,9 @@
118
119
  "jupyterlab": {
119
120
  "extension": true,
120
121
  "outputDir": "jupyterlite_ai/labextension",
121
- "schemaDir": "schema"
122
+ "schemaDir": "schema",
123
+ "disabledExtensions": [
124
+ "@jupyterlab/apputils-extension:settings-connector"
125
+ ]
122
126
  }
123
127
  }
@@ -139,9 +139,11 @@ export class ChatHandler extends ChatModel {
139
139
 
140
140
  let content = '';
141
141
 
142
+ this._controller = new AbortController();
142
143
  try {
143
144
  for await (const chunk of await this._providerRegistry.currentChatModel.stream(
144
- messages
145
+ messages,
146
+ { signal: this._controller.signal }
145
147
  )) {
146
148
  content += chunk.content ?? chunk;
147
149
  botMsg.body = content;
@@ -162,6 +164,7 @@ export class ChatHandler extends ChatModel {
162
164
  return false;
163
165
  } finally {
164
166
  this.updateWriters([]);
167
+ this._controller = null;
165
168
  }
166
169
  }
167
170
 
@@ -177,12 +180,17 @@ export class ChatHandler extends ChatModel {
177
180
  super.messageAdded(message);
178
181
  }
179
182
 
183
+ stopStreaming(): void {
184
+ this._controller?.abort();
185
+ }
186
+
180
187
  private _providerRegistry: IAIProviderRegistry;
181
188
  private _personaName = 'AI';
182
189
  private _prompt: string;
183
190
  private _errorMessage: string = '';
184
191
  private _history: IChatHistory = { messages: [] };
185
192
  private _defaultErrorMessage = 'AI provider not configured';
193
+ private _controller: AbortController | null = null;
186
194
  }
187
195
 
188
196
  export namespace ChatHandler {
@@ -51,7 +51,14 @@ export class CompletionProvider implements IInlineCompletionProvider {
51
51
 
52
52
  export namespace CompletionProvider {
53
53
  export interface IOptions {
54
+ /**
55
+ * The registry where the completion provider belongs.
56
+ */
54
57
  providerRegistry: IAIProviderRegistry;
58
+ /**
59
+ * The request completion commands, can be useful if a provider needs to request
60
+ * the completion by itself.
61
+ */
55
62
  requestCompletion: () => void;
56
63
  }
57
64
  }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import StopIcon from '@mui/icons-material/Stop';
7
+ import React from 'react';
8
+
9
+ import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
10
+
11
+ /**
12
+ * Properties of the stop button.
13
+ */
14
+ export interface IStopButtonProps
15
+ extends InputToolbarRegistry.IToolbarItemProps {
16
+ /**
17
+ * The function to stop streaming.
18
+ */
19
+ stopStreaming: () => void;
20
+ }
21
+
22
+ /**
23
+ * The stop button.
24
+ */
25
+ export function StopButton(props: IStopButtonProps): JSX.Element {
26
+ const tooltip = 'Stop streaming';
27
+ return (
28
+ <TooltippedButton
29
+ onClick={props.stopStreaming}
30
+ tooltip={tooltip}
31
+ buttonProps={{
32
+ size: 'small',
33
+ variant: 'contained',
34
+ title: tooltip
35
+ }}
36
+ >
37
+ <StopIcon />
38
+ </TooltippedButton>
39
+ );
40
+ }
41
+
42
+ /**
43
+ * factory returning the toolbar item.
44
+ */
45
+ export function stopItem(
46
+ stopStreaming: () => void
47
+ ): InputToolbarRegistry.IToolbarItem {
48
+ return {
49
+ element: (props: InputToolbarRegistry.IToolbarItemProps) => {
50
+ const stopProps: IStopButtonProps = { ...props, stopStreaming };
51
+ return StopButton(stopProps);
52
+ },
53
+ position: 50,
54
+ hidden: true /* hidden by default */
55
+ };
56
+ }
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,7 +15,10 @@ 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
24
  import { ISecretsManager } from 'jupyter-secrets-manager';
@@ -23,8 +27,9 @@ 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';
30
+ import { aiSettingsRenderer, SettingConnector } from './settings';
27
31
  import { IAIProviderRegistry } from './tokens';
32
+ import { stopItem } from './components/stop-button';
28
33
 
29
34
  const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
30
35
  id: '@jupyterlite/ai:autocompletion-registry',
@@ -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) {
@@ -138,21 +161,28 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
138
161
  id: '@jupyterlite/ai:provider-registry',
139
162
  autoStart: true,
140
163
  requires: [IFormRendererRegistry, ISettingRegistry],
141
- optional: [IRenderMimeRegistry, ISecretsManager],
164
+ optional: [IRenderMimeRegistry, ISecretsManager, ISettingConnector],
142
165
  provides: IAIProviderRegistry,
143
166
  activate: (
144
167
  app: JupyterFrontEnd,
145
168
  editorRegistry: IFormRendererRegistry,
146
169
  settingRegistry: ISettingRegistry,
147
170
  rmRegistry?: IRenderMimeRegistry,
148
- secretsManager?: ISecretsManager
171
+ secretsManager?: ISecretsManager,
172
+ settingConnector?: ISettingConnector
149
173
  ): IAIProviderRegistry => {
150
- const providerRegistry = new AIProviderRegistry();
174
+ const providerRegistry = new AIProviderRegistry({ secretsManager });
151
175
 
152
176
  editorRegistry.addRenderer(
153
177
  '@jupyterlite/ai:provider-registry.AIprovider',
154
- aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
178
+ aiSettingsRenderer({
179
+ providerRegistry,
180
+ rmRegistry,
181
+ secretsManager,
182
+ settingConnector
183
+ })
155
184
  );
185
+
156
186
  settingRegistry
157
187
  .load(providerRegistryPlugin.id)
158
188
  .then(settings => {
@@ -161,10 +191,10 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
161
191
  const providerSettings = (settings.get('AIprovider').composite ?? {
162
192
  provider: 'None'
163
193
  }) as ReadonlyPartialJSONObject;
164
- providerRegistry.setProvider(
165
- providerSettings.provider as string,
166
- providerSettings
167
- );
194
+ providerRegistry.setProvider({
195
+ name: providerSettings.provider as string,
196
+ settings: providerSettings
197
+ });
168
198
  };
169
199
 
170
200
  settings.changed.connect(() => updateProvider());
@@ -181,10 +211,25 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
181
211
  }
182
212
  };
183
213
 
214
+ /**
215
+ * Provides the settings connector as a separate plugin to allow for alternative
216
+ * implementations that may want to fetch settings from a different source or
217
+ * endpoint.
218
+ */
219
+ const settingsConnector: JupyterFrontEndPlugin<ISettingConnector> = {
220
+ id: '@jupyterlite/ai:settings-connector',
221
+ description: 'Provides a settings connector which does not save passwords.',
222
+ autoStart: true,
223
+ provides: ISettingConnector,
224
+ activate: (app: JupyterFrontEnd) =>
225
+ new SettingConnector(app.serviceManager.settings)
226
+ };
227
+
184
228
  export default [
185
229
  providerRegistryPlugin,
186
230
  chatCommandRegistryPlugin,
187
231
  chatPlugin,
188
232
  completerPlugin,
233
+ settingsConnector,
189
234
  ...defaultProviderPlugins
190
235
  ];
package/src/provider.ts CHANGED
@@ -1,12 +1,22 @@
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 {
10
+ getSecretId,
11
+ SECRETS_NAMESPACE,
12
+ SECRETS_REPLACEMENT
13
+ } from './settings';
14
+ import {
15
+ IAIProvider,
16
+ IAIProviderRegistry,
17
+ IDict,
18
+ ISetProviderOptions
19
+ } from './tokens';
10
20
 
11
21
  export const chatSystemPrompt = (
12
22
  options: AIProviderRegistry.IPromptOptions
@@ -39,6 +49,13 @@ Do not include the prompt in the output, only the string that should be appended
39
49
  `;
40
50
 
41
51
  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
+
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,36 @@ 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(SECRETS_NAMESPACE, id);
175
+ fullSettings[key] = secrets?.value || settings[key];
176
+ continue;
177
+ }
178
+ fullSettings[key] = settings[key];
179
+ }
139
180
 
140
181
  if (this._currentProvider?.completer !== undefined) {
141
182
  try {
142
- this._completer = new this._currentProvider.completer({ ...settings });
183
+ this._completer = new this._currentProvider.completer({
184
+ ...fullSettings
185
+ });
143
186
  this._completerError = '';
144
187
  } catch (e: any) {
145
188
  this._completerError = e.message;
@@ -150,7 +193,9 @@ export class AIProviderRegistry implements IAIProviderRegistry {
150
193
 
151
194
  if (this._currentProvider?.chatModel !== undefined) {
152
195
  try {
153
- this._chatModel = new this._currentProvider.chatModel({ ...settings });
196
+ this._chatModel = new this._currentProvider.chatModel({
197
+ ...fullSettings
198
+ });
154
199
  this._chatError = '';
155
200
  } catch (e: any) {
156
201
  this._chatError = e.message;
@@ -170,6 +215,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
170
215
  return this._providerChanged;
171
216
  }
172
217
 
218
+ private _secretsManager: ISecretsManager | null;
173
219
  private _currentProvider: IAIProvider | null = null;
174
220
  private _completer: IBaseCompleter | null = null;
175
221
  private _chatModel: BaseChatModel | null = null;
@@ -178,6 +224,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
178
224
  private _chatError: string = '';
179
225
  private _completerError: string = '';
180
226
  private _providers = new Map<string, IAIProvider>();
227
+ private _deferredProvider: ISetProviderOptions | null = null;
181
228
  }
182
229
 
183
230
  export namespace AIProviderRegistry {
@@ -186,13 +233,9 @@ export namespace AIProviderRegistry {
186
233
  */
187
234
  export interface IOptions {
188
235
  /**
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.
194
237
  */
195
- requestCompletion: () => void;
238
+ secretsManager?: ISecretsManager;
196
239
  }
197
240
 
198
241
  /**
@@ -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,10 +13,10 @@ import { JSONSchema7 } from 'json-schema';
10
13
  import { ISecretsManager } from 'jupyter-secrets-manager';
11
14
  import React from 'react';
12
15
 
16
+ import { getSecretId, SECRETS_NAMESPACE, SettingConnector } from '.';
13
17
  import baseSettings from './base.json';
14
18
  import { IAIProviderRegistry, IDict } 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';
@@ -22,6 +25,7 @@ export const aiSettingsRenderer = (options: {
22
25
  providerRegistry: IAIProviderRegistry;
23
26
  rmRegistry?: IRenderMimeRegistry;
24
27
  secretsManager?: ISecretsManager;
28
+ settingConnector?: ISettingConnector;
25
29
  }): IFormRenderer => {
26
30
  return {
27
31
  fieldRenderer: (props: FieldProps) => {
@@ -54,6 +58,7 @@ export class AiSettings extends React.Component<
54
58
  this._providerRegistry = props.formContext.providerRegistry;
55
59
  this._rmRegistry = props.formContext.rmRegistry ?? null;
56
60
  this._secretsManager = props.formContext.secretsManager ?? null;
61
+ this._settingConnector = props.formContext.settingConnector ?? null;
57
62
  this._settings = props.formContext.settings;
58
63
 
59
64
  this._useSecretsManager =
@@ -130,7 +135,7 @@ export class AiSettings extends React.Component<
130
135
  if (inputs[i].type.toLowerCase() === 'password') {
131
136
  const label = inputs[i].getAttribute('label');
132
137
  if (label) {
133
- const id = `${this._provider}-${label}`;
138
+ const id = getSecretId(this._provider, label);
134
139
  this._secretsManager.attach(
135
140
  SECRETS_NAMESPACE,
136
141
  id,
@@ -141,6 +146,9 @@ export class AiSettings extends React.Component<
141
146
  }
142
147
  }
143
148
  }
149
+ if (this._settingConnector instanceof SettingConnector) {
150
+ this._settingConnector.doNotSave = this._unsavedFields;
151
+ }
144
152
  }
145
153
 
146
154
  /**
@@ -187,6 +195,9 @@ export class AiSettings extends React.Component<
187
195
  this._secretsManager?.detachAll(SECRETS_NAMESPACE);
188
196
  this._formInputs = [];
189
197
  this._unsavedFields = [];
198
+ if (this._settingConnector instanceof SettingConnector) {
199
+ this._settingConnector.doNotSave = [];
200
+ }
190
201
  this.saveSettings(this._currentSettings);
191
202
  } else {
192
203
  // Remove all the keys stored locally and attach the password inputs to the
@@ -202,6 +213,9 @@ export class AiSettings extends React.Component<
202
213
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
203
214
  this.componentDidUpdate();
204
215
  }
216
+ this._settings
217
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
218
+ .catch(console.error);
205
219
  };
206
220
 
207
221
  /**
@@ -337,6 +351,7 @@ export class AiSettings extends React.Component<
337
351
  private _useSecretsManager: boolean;
338
352
  private _rmRegistry: IRenderMimeRegistry | null;
339
353
  private _secretsManager: ISecretsManager | null;
354
+ private _settingConnector: ISettingConnector | null;
340
355
  private _currentSettings: IDict<any> = { provider: 'None' };
341
356
  private _uiSchema: IDict<any> = {};
342
357
  private _settings: ISettingRegistry.ISettings;
@@ -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,6 @@
1
+ export const SECRETS_NAMESPACE = '@jupyterlite/ai';
2
+ export const SECRETS_REPLACEMENT = '***';
3
+
4
+ export function getSecretId(provider: string, label: string) {
5
+ return `${provider}-${label}`;
6
+ }
package/src/tokens.ts CHANGED
@@ -85,10 +85,9 @@ export interface IAIProviderRegistry {
85
85
  * Set the providers (chat model and completer).
86
86
  * Creates the providers if the name has changed, otherwise only updates their config.
87
87
  *
88
- * @param name - the name of the provider to use.
89
- * @param settings - the settings for the models.
88
+ * @param options - an object with the name and the settings of the provider to use.
90
89
  */
91
- setProvider(name: string, settings: ReadonlyPartialJSONObject): void;
90
+ setProvider(options: ISetProviderOptions): void;
92
91
  /**
93
92
  * A signal emitting when the provider or its settings has changed.
94
93
  */
@@ -103,6 +102,20 @@ export interface IAIProviderRegistry {
103
102
  readonly completerError: string;
104
103
  }
105
104
 
105
+ /**
106
+ * The set provider options.
107
+ */
108
+ export interface ISetProviderOptions {
109
+ /**
110
+ * The name of the provider.
111
+ */
112
+ name: string;
113
+ /**
114
+ * The settings of the provider.
115
+ */
116
+ settings: ReadonlyPartialJSONObject;
117
+ }
118
+
106
119
  /**
107
120
  * The provider registry token.
108
121
  */