@jupyterlite/ai 0.5.0 → 0.6.1

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.
@@ -3,12 +3,19 @@ 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, SettingConnector } from '.';
6
7
  import baseSettings from './base.json';
7
- const SECRETS_NAMESPACE = '@jupyterlite/ai';
8
+ import { PLUGIN_IDS } from '../tokens';
8
9
  const MD_MIME_TYPE = 'text/markdown';
9
10
  const STORAGE_NAME = '@jupyterlite/ai:settings';
10
11
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
12
+ const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
11
13
  export const aiSettingsRenderer = (options) => {
14
+ const { secretsToken } = options;
15
+ delete options.secretsToken;
16
+ if (secretsToken) {
17
+ Private.setToken(secretsToken);
18
+ }
12
19
  return {
13
20
  fieldRenderer: (props) => {
14
21
  props.formContext = { ...props.formContext, ...options };
@@ -21,7 +28,7 @@ const WrappedFormComponent = (props) => {
21
28
  };
22
29
  export class AiSettings extends React.Component {
23
30
  constructor(props) {
24
- var _a, _b, _c, _d;
31
+ var _a, _b, _c, _d, _e, _f;
25
32
  super(props);
26
33
  this.updateUseSecretsManager = (value) => {
27
34
  var _a;
@@ -29,9 +36,12 @@ export class AiSettings extends React.Component {
29
36
  if (!value) {
30
37
  // Detach all the password inputs attached to the secrets manager, and save the
31
38
  // current settings to the local storage to save the password.
32
- (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(SECRETS_NAMESPACE);
39
+ (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(Private.getToken(), SECRETS_NAMESPACE);
33
40
  this._formInputs = [];
34
41
  this._unsavedFields = [];
42
+ if (this._settingConnector instanceof SettingConnector) {
43
+ this._settingConnector.doNotSave = [];
44
+ }
35
45
  this.saveSettings(this._currentSettings);
36
46
  }
37
47
  else {
@@ -48,6 +58,9 @@ export class AiSettings extends React.Component {
48
58
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
49
59
  this.componentDidUpdate();
50
60
  }
61
+ this._settings
62
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
63
+ .catch(console.error);
51
64
  };
52
65
  /**
53
66
  * Triggered when the provider hes changed, to update the schema and values.
@@ -100,9 +113,12 @@ export class AiSettings extends React.Component {
100
113
  this._providerRegistry = props.formContext.providerRegistry;
101
114
  this._rmRegistry = (_a = props.formContext.rmRegistry) !== null && _a !== void 0 ? _a : null;
102
115
  this._secretsManager = (_b = props.formContext.secretsManager) !== null && _b !== void 0 ? _b : null;
116
+ this._settingConnector = (_c = props.formContext.settingConnector) !== null && _c !== void 0 ? _c : null;
103
117
  this._settings = props.formContext.settings;
104
118
  this._useSecretsManager =
105
- (_c = this._settings.get('UseSecretsManager').composite) !== null && _c !== void 0 ? _c : true;
119
+ (_d = this._settings.get('UseSecretsManager').composite) !== null && _d !== void 0 ? _d : true;
120
+ this._hideSecretFields =
121
+ (_e = this._settings.get('HideSecretFields').composite) !== null && _e !== void 0 ? _e : true;
106
122
  // Initialize the providers schema.
107
123
  const providerSchema = JSONExt.deepCopy(baseSettings);
108
124
  providerSchema.properties.provider = {
@@ -120,7 +136,7 @@ export class AiSettings extends React.Component {
120
136
  const labSettings = this._settings.get('AIprovider').composite;
121
137
  if (labSettings && Object.keys(labSettings).includes('provider')) {
122
138
  // Get the provider name.
123
- const provider = (_d = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _d === void 0 ? void 0 : _d[1];
139
+ const provider = (_f = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _f === void 0 ? void 0 : _f[1];
124
140
  // Save the settings.
125
141
  const settings = {
126
142
  _current: provider
@@ -141,11 +157,16 @@ export class AiSettings extends React.Component {
141
157
  .set('AIprovider', this._currentSettings)
142
158
  .catch(console.error);
143
159
  this._settings.changed.connect(() => {
144
- var _a;
160
+ var _a, _b;
145
161
  const useSecretsManager = (_a = this._settings.get('UseSecretsManager').composite) !== null && _a !== void 0 ? _a : true;
146
162
  if (useSecretsManager !== this._useSecretsManager) {
147
163
  this.updateUseSecretsManager(useSecretsManager);
148
164
  }
165
+ const hideSecretFields = (_b = this._settings.get('HideSecretFields').composite) !== null && _b !== void 0 ? _b : true;
166
+ if (hideSecretFields !== this._hideSecretFields) {
167
+ this._hideSecretFields = hideSecretFields;
168
+ this._updateSchema();
169
+ }
149
170
  });
150
171
  }
151
172
  async componentDidUpdate() {
@@ -158,19 +179,28 @@ export class AiSettings extends React.Component {
158
179
  if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
159
180
  return;
160
181
  }
161
- await this._secretsManager.detachAll(SECRETS_NAMESPACE);
182
+ await this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
162
183
  this._formInputs = [...inputs];
163
184
  this._unsavedFields = [];
164
185
  for (let i = 0; i < inputs.length; i++) {
165
186
  if (inputs[i].type.toLowerCase() === 'password') {
166
187
  const label = inputs[i].getAttribute('label');
167
188
  if (label) {
168
- const id = `${this._provider}-${label}`;
169
- this._secretsManager.attach(SECRETS_NAMESPACE, id, inputs[i], (value) => this._onPasswordUpdated(label, value));
189
+ const id = getSecretId(this._provider, label);
190
+ this._secretsManager.attach(Private.getToken(), SECRETS_NAMESPACE, id, inputs[i], (value) => this._onPasswordUpdated(label, value));
170
191
  this._unsavedFields.push(label);
171
192
  }
172
193
  }
173
194
  }
195
+ if (this._settingConnector instanceof SettingConnector) {
196
+ this._settingConnector.doNotSave = this._unsavedFields;
197
+ }
198
+ }
199
+ componentWillUnmount() {
200
+ if (!this._secretsManager || !this._useSecretsManager) {
201
+ return;
202
+ }
203
+ this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
174
204
  }
175
205
  /**
176
206
  * Get the current provider from the local storage.
@@ -207,15 +237,6 @@ export class AiSettings extends React.Component {
207
237
  settings[this._provider] = currentSettings;
208
238
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
209
239
  }
210
- /**
211
- * Update the UI schema of the form.
212
- * Currently use to hide API keys.
213
- */
214
- _updateUiSchema(key) {
215
- if (key.toLowerCase().includes('key')) {
216
- this._uiSchema[key] = { 'ui:widget': 'password' };
217
- }
218
- }
219
240
  /**
220
241
  * Build the schema for a given provider.
221
242
  */
@@ -225,8 +246,13 @@ export class AiSettings extends React.Component {
225
246
  const settingsSchema = this._providerRegistry.getSettingsSchema(this._provider);
226
247
  if (settingsSchema) {
227
248
  Object.entries(settingsSchema).forEach(([key, value]) => {
249
+ if (key.toLowerCase().includes('key')) {
250
+ if (this._hideSecretFields) {
251
+ return;
252
+ }
253
+ this._uiSchema[key] = { 'ui:widget': 'password' };
254
+ }
228
255
  schema.properties[key] = value;
229
- this._updateUiSchema(key);
230
256
  });
231
257
  }
232
258
  return schema;
@@ -265,3 +291,24 @@ export class AiSettings extends React.Component {
265
291
  React.createElement(WrappedFormComponent, { formData: this._currentSettings, schema: this.state.schema, onChange: this._onFormChange, uiSchema: this._uiSchema })));
266
292
  }
267
293
  }
294
+ var Private;
295
+ (function (Private) {
296
+ /**
297
+ * The token to use with the secrets manager.
298
+ */
299
+ let secretsToken;
300
+ /**
301
+ * Set of the token.
302
+ */
303
+ function setToken(value) {
304
+ secretsToken = value;
305
+ }
306
+ Private.setToken = setToken;
307
+ /**
308
+ * get the token.
309
+ */
310
+ function getToken() {
311
+ return secretsToken;
312
+ }
313
+ Private.getToken = getToken;
314
+ })(Private || (Private = {}));
@@ -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,2 @@
1
+ export declare const SECRETS_REPLACEMENT = "***";
2
+ export declare function getSecretId(provider: string, label: string): string;
@@ -0,0 +1,4 @@
1
+ export const SECRETS_REPLACEMENT = '***';
2
+ export function getSecretId(provider, label) {
3
+ return `${provider}-${label}`;
4
+ }
package/lib/tokens.d.ts CHANGED
@@ -3,6 +3,13 @@ import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
3
3
  import { ISignal } from '@lumino/signaling';
4
4
  import { JSONSchema7 } from 'json-schema';
5
5
  import { IBaseCompleter } from './base-completer';
6
+ export declare const PLUGIN_IDS: {
7
+ chat: string;
8
+ chatCommandRegistry: string;
9
+ completer: string;
10
+ providerRegistry: string;
11
+ settingsConnector: string;
12
+ };
6
13
  export interface IDict<T = any> {
7
14
  [key: string]: T;
8
15
  }
@@ -80,10 +87,9 @@ export interface IAIProviderRegistry {
80
87
  * Set the providers (chat model and completer).
81
88
  * Creates the providers if the name has changed, otherwise only updates their config.
82
89
  *
83
- * @param name - the name of the provider to use.
84
- * @param settings - the settings for the models.
90
+ * @param options - an object with the name and the settings of the provider to use.
85
91
  */
86
- setProvider(name: string, settings: ReadonlyPartialJSONObject): void;
92
+ setProvider(options: ISetProviderOptions): void;
87
93
  /**
88
94
  * A signal emitting when the provider or its settings has changed.
89
95
  */
@@ -97,6 +103,19 @@ export interface IAIProviderRegistry {
97
103
  */
98
104
  readonly completerError: string;
99
105
  }
106
+ /**
107
+ * The set provider options.
108
+ */
109
+ export interface ISetProviderOptions {
110
+ /**
111
+ * The name of the provider.
112
+ */
113
+ name: string;
114
+ /**
115
+ * The settings of the provider.
116
+ */
117
+ settings: ReadonlyPartialJSONObject;
118
+ }
100
119
  /**
101
120
  * The provider registry token.
102
121
  */
package/lib/tokens.js CHANGED
@@ -1,4 +1,11 @@
1
1
  import { Token } from '@lumino/coreutils';
2
+ export const PLUGIN_IDS = {
3
+ chat: '@jupyterlite/ai:chat',
4
+ chatCommandRegistry: '@jupyterlite/ai:autocompletion-registry',
5
+ completer: '@jupyterlite/ai:completer',
6
+ providerRegistry: '@jupyterlite/ai:provider-registry',
7
+ settingsConnector: '@jupyterlite/ai:settings-connector'
8
+ };
2
9
  /**
3
10
  * The provider registry token.
4
11
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/ai",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -56,14 +56,15 @@
56
56
  "watch:labextension": "jupyter labextension watch ."
57
57
  },
58
58
  "dependencies": {
59
- "@jupyter/chat": "^0.8.1",
60
- "@jupyterlab/application": "^4.4.0-alpha.0",
61
- "@jupyterlab/apputils": "^4.5.0-alpha.0",
62
- "@jupyterlab/completer": "^4.4.0-alpha.0",
63
- "@jupyterlab/notebook": "^4.4.0-alpha.0",
64
- "@jupyterlab/rendermime": "^4.4.0-alpha.0",
65
- "@jupyterlab/settingregistry": "^4.4.0-alpha.0",
66
- "@jupyterlab/ui-components": "^4.4.0-alpha.0",
59
+ "@jupyter/chat": "^0.9.0",
60
+ "@jupyterlab/application": "^4.4.0",
61
+ "@jupyterlab/apputils": "^4.5.0",
62
+ "@jupyterlab/completer": "^4.4.0",
63
+ "@jupyterlab/coreutils": "^6.4.0",
64
+ "@jupyterlab/notebook": "^4.4.0",
65
+ "@jupyterlab/rendermime": "^4.4.0",
66
+ "@jupyterlab/settingregistry": "^4.4.0",
67
+ "@jupyterlab/ui-components": "^4.4.0",
67
68
  "@langchain/anthropic": "^0.3.9",
68
69
  "@langchain/community": "^0.3.31",
69
70
  "@langchain/core": "^0.3.40",
@@ -77,12 +78,13 @@
77
78
  "@rjsf/core": "^4.2.0",
78
79
  "@rjsf/utils": "^5.18.4",
79
80
  "@rjsf/validator-ajv8": "^5.18.4",
80
- "jupyter-secrets-manager": "^0.1.1",
81
+ "json5": "^2.2.3",
82
+ "jupyter-secrets-manager": "^0.3.0",
81
83
  "react": "^18.2.0",
82
84
  "react-dom": "^18.2.0"
83
85
  },
84
86
  "devDependencies": {
85
- "@jupyterlab/builder": "^4.0.0",
87
+ "@jupyterlab/builder": "^4.4.0",
86
88
  "@stylistic/eslint-plugin": "^3.0.1",
87
89
  "@types/json-schema": "^7.0.11",
88
90
  "@types/react": "^18.0.26",
@@ -118,6 +120,9 @@
118
120
  "jupyterlab": {
119
121
  "extension": true,
120
122
  "outputDir": "jupyterlite_ai/labextension",
121
- "schemaDir": "schema"
123
+ "schemaDir": "schema",
124
+ "disabledExtensions": [
125
+ "@jupyterlab/apputils-extension:settings-connector"
126
+ ]
122
127
  }
123
128
  }
@@ -11,6 +11,12 @@
11
11
  "description": "Whether to use or not the secrets manager. If not, secrets will be stored in the browser (local storage)",
12
12
  "default": true
13
13
  },
14
+ "HideSecretFields": {
15
+ "type": "boolean",
16
+ "title": "Hide secret fields",
17
+ "description": "Whether to hide the secret fields in the UI or not",
18
+ "default": true
19
+ },
14
20
  "AIprovider": {
15
21
  "type": "object",
16
22
  "title": "AI provider",
@@ -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
+ }