@jupyterlite/ai 0.4.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.
Files changed (72) hide show
  1. package/lib/chat-handler.d.ts +9 -1
  2. package/lib/chat-handler.js +37 -1
  3. package/lib/completion-provider.d.ts +8 -1
  4. package/lib/components/stop-button.d.ts +19 -0
  5. package/lib/components/stop-button.js +32 -0
  6. package/lib/{llm-models/anthropic-completer.d.ts → default-providers/Anthropic/completer.d.ts} +1 -1
  7. package/lib/{llm-models/anthropic-completer.js → default-providers/Anthropic/completer.js} +1 -1
  8. package/lib/{llm-models/chrome-completer.d.ts → default-providers/ChromeAI/completer.d.ts} +1 -1
  9. package/lib/{llm-models/chrome-completer.js → default-providers/ChromeAI/completer.js} +1 -1
  10. package/lib/default-providers/ChromeAI/instructions.d.ts +2 -0
  11. package/lib/default-providers/ChromeAI/instructions.js +24 -0
  12. package/lib/{llm-models/codestral-completer.d.ts → default-providers/MistralAI/completer.d.ts} +1 -1
  13. package/lib/{llm-models/codestral-completer.js → default-providers/MistralAI/completer.js} +1 -1
  14. package/lib/default-providers/MistralAI/instructions.d.ts +2 -0
  15. package/lib/default-providers/MistralAI/instructions.js +16 -0
  16. package/lib/{llm-models/openai-completer.d.ts → default-providers/OpenAI/completer.d.ts} +1 -1
  17. package/lib/{llm-models/openai-completer.js → default-providers/OpenAI/completer.js} +1 -1
  18. package/lib/default-providers/index.d.ts +2 -0
  19. package/lib/default-providers/index.js +60 -0
  20. package/lib/index.d.ts +3 -2
  21. package/lib/index.js +57 -36
  22. package/lib/provider.d.ts +13 -12
  23. package/lib/provider.js +43 -9
  24. package/lib/settings/index.d.ts +3 -0
  25. package/lib/settings/index.js +3 -0
  26. package/lib/settings/panel.d.ts +17 -0
  27. package/lib/settings/panel.js +92 -5
  28. package/lib/settings/settings-connector.d.ts +31 -0
  29. package/lib/settings/settings-connector.js +61 -0
  30. package/lib/settings/utils.d.ts +3 -0
  31. package/lib/settings/utils.js +5 -0
  32. package/lib/tokens.d.ts +16 -4
  33. package/package.json +14 -7
  34. package/schema/provider-registry.json +6 -0
  35. package/src/chat-handler.ts +43 -1
  36. package/src/completion-provider.ts +8 -1
  37. package/src/components/stop-button.tsx +56 -0
  38. package/src/{llm-models/anthropic-completer.ts → default-providers/Anthropic/completer.ts} +2 -2
  39. package/src/{llm-models/chrome-completer.ts → default-providers/ChromeAI/completer.ts} +3 -2
  40. package/src/default-providers/ChromeAI/instructions.ts +24 -0
  41. package/src/{llm-models/codestral-completer.ts → default-providers/MistralAI/completer.ts} +2 -2
  42. package/src/default-providers/MistralAI/instructions.ts +16 -0
  43. package/src/{llm-models/openai-completer.ts → default-providers/OpenAI/completer.ts} +2 -2
  44. package/src/default-providers/index.ts +71 -0
  45. package/src/index.ts +77 -49
  46. package/src/provider.ts +58 -15
  47. package/src/settings/index.ts +3 -0
  48. package/src/settings/panel.tsx +109 -5
  49. package/src/settings/settings-connector.ts +89 -0
  50. package/src/settings/utils.ts +6 -0
  51. package/src/tokens.ts +17 -4
  52. package/lib/llm-models/index.d.ts +0 -4
  53. package/lib/llm-models/index.js +0 -43
  54. package/lib/settings/instructions.d.ts +0 -2
  55. package/lib/settings/instructions.js +0 -44
  56. package/lib/settings/schemas/index.d.ts +0 -3
  57. package/lib/settings/schemas/index.js +0 -11
  58. package/lib/slash-commands.d.ts +0 -16
  59. package/lib/slash-commands.js +0 -25
  60. package/src/llm-models/index.ts +0 -50
  61. package/src/settings/instructions.ts +0 -48
  62. package/src/settings/schemas/index.ts +0 -15
  63. package/src/slash-commands.tsx +0 -55
  64. /package/lib/{llm-models/base-completer.d.ts → base-completer.d.ts} +0 -0
  65. /package/lib/{llm-models/base-completer.js → base-completer.js} +0 -0
  66. /package/lib/{settings/schemas/_generated/Anthropic.json → default-providers/Anthropic/settings-schema.json} +0 -0
  67. /package/lib/{settings/schemas/_generated/ChromeAI.json → default-providers/ChromeAI/settings-schema.json} +0 -0
  68. /package/lib/{settings/schemas/_generated/MistralAI.json → default-providers/MistralAI/settings-schema.json} +0 -0
  69. /package/lib/{settings/schemas/_generated/OpenAI.json → default-providers/OpenAI/settings-schema.json} +0 -0
  70. /package/lib/settings/{schemas/base.json → base.json} +0 -0
  71. /package/src/{llm-models/base-completer.ts → base-completer.ts} +0 -0
  72. /package/src/{llm-models/svg.d.ts → global.d.ts} +0 -0
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,12 +1,16 @@
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';
6
+ import { ISecretsManager } from 'jupyter-secrets-manager';
5
7
  import React from 'react';
6
8
  import { IAIProviderRegistry, IDict } from '../tokens';
7
9
  export declare const aiSettingsRenderer: (options: {
8
10
  providerRegistry: IAIProviderRegistry;
9
11
  rmRegistry?: IRenderMimeRegistry;
12
+ secretsManager?: ISecretsManager;
13
+ settingConnector?: ISettingConnector;
10
14
  }) => IFormRenderer;
11
15
  export interface ISettingsFormStates {
12
16
  schema: JSONSchema7;
@@ -14,6 +18,7 @@ export interface ISettingsFormStates {
14
18
  }
15
19
  export declare class AiSettings extends React.Component<FieldProps, ISettingsFormStates> {
16
20
  constructor(props: FieldProps);
21
+ componentDidUpdate(): Promise<void>;
17
22
  /**
18
23
  * Get the current provider from the local storage.
19
24
  */
@@ -30,6 +35,7 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
30
35
  * Save settings in local storage for a given provider.
31
36
  */
32
37
  saveSettings(value: IDict<any>): void;
38
+ private updateUseSecretsManager;
33
39
  /**
34
40
  * Update the UI schema of the form.
35
41
  * Currently use to hide API keys.
@@ -53,6 +59,11 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
53
59
  * Update the Jupyterlab settings accordingly.
54
60
  */
55
61
  private _onProviderChanged;
62
+ /**
63
+ * Callback function called when the password input has been programmatically updated
64
+ * with the secret manager.
65
+ */
66
+ private _onPasswordUpdated;
56
67
  /**
57
68
  * Triggered when the form value has changed, to update the current settings and save
58
69
  * it in local storage.
@@ -63,8 +74,14 @@ export declare class AiSettings extends React.Component<FieldProps, ISettingsFor
63
74
  private _providerRegistry;
64
75
  private _provider;
65
76
  private _providerSchema;
77
+ private _useSecretsManager;
66
78
  private _rmRegistry;
79
+ private _secretsManager;
80
+ private _settingConnector;
67
81
  private _currentSettings;
68
82
  private _uiSchema;
69
83
  private _settings;
84
+ private _formRef;
85
+ private _unsavedFields;
86
+ private _formInputs;
70
87
  }
@@ -1,8 +1,10 @@
1
1
  import { FormComponent } from '@jupyterlab/ui-components';
2
+ import { ArrayExt } from '@lumino/algorithm';
2
3
  import { JSONExt } from '@lumino/coreutils';
3
4
  import validator from '@rjsf/validator-ajv8';
4
5
  import React from 'react';
5
- import baseSettings from './schemas/base.json';
6
+ import { getSecretId, SECRETS_NAMESPACE, SettingConnector } from '.';
7
+ import baseSettings from './base.json';
6
8
  const MD_MIME_TYPE = 'text/markdown';
7
9
  const STORAGE_NAME = '@jupyterlite/ai:settings';
8
10
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
@@ -19,8 +21,40 @@ const WrappedFormComponent = (props) => {
19
21
  };
20
22
  export class AiSettings extends React.Component {
21
23
  constructor(props) {
22
- var _a, _b;
24
+ var _a, _b, _c, _d, _e;
23
25
  super(props);
26
+ this.updateUseSecretsManager = (value) => {
27
+ var _a;
28
+ this._useSecretsManager = value;
29
+ if (!value) {
30
+ // Detach all the password inputs attached to the secrets manager, and save the
31
+ // current settings to the local storage to save the password.
32
+ (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(SECRETS_NAMESPACE);
33
+ this._formInputs = [];
34
+ this._unsavedFields = [];
35
+ if (this._settingConnector instanceof SettingConnector) {
36
+ this._settingConnector.doNotSave = [];
37
+ }
38
+ this.saveSettings(this._currentSettings);
39
+ }
40
+ else {
41
+ // Remove all the keys stored locally and attach the password inputs to the
42
+ // secrets manager.
43
+ const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
44
+ Object.keys(settings).forEach(provider => {
45
+ Object.keys(settings[provider])
46
+ .filter(key => key.toLowerCase().includes('key'))
47
+ .forEach(key => {
48
+ delete settings[provider][key];
49
+ });
50
+ });
51
+ localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
52
+ this.componentDidUpdate();
53
+ }
54
+ this._settings
55
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
56
+ .catch(console.error);
57
+ };
24
58
  /**
25
59
  * Triggered when the provider hes changed, to update the schema and values.
26
60
  * Update the Jupyterlab settings accordingly.
@@ -39,6 +73,16 @@ export class AiSettings extends React.Component {
39
73
  .set('AIprovider', { provider: this._provider, ...this._currentSettings })
40
74
  .catch(console.error);
41
75
  };
76
+ /**
77
+ * Callback function called when the password input has been programmatically updated
78
+ * with the secret manager.
79
+ */
80
+ this._onPasswordUpdated = (fieldName, value) => {
81
+ this._currentSettings[fieldName] = value;
82
+ this._settings
83
+ .set('AIprovider', { provider: this._provider, ...this._currentSettings })
84
+ .catch(console.error);
85
+ };
42
86
  /**
43
87
  * Triggered when the form value has changed, to update the current settings and save
44
88
  * it in local storage.
@@ -53,12 +97,19 @@ export class AiSettings extends React.Component {
53
97
  };
54
98
  this._currentSettings = { provider: 'None' };
55
99
  this._uiSchema = {};
100
+ this._formRef = React.createRef();
101
+ this._unsavedFields = [];
102
+ this._formInputs = [];
56
103
  if (!props.formContext.providerRegistry) {
57
104
  throw new Error('The provider registry is needed to enable the jupyterlite-ai settings panel');
58
105
  }
59
106
  this._providerRegistry = props.formContext.providerRegistry;
60
107
  this._rmRegistry = (_a = props.formContext.rmRegistry) !== null && _a !== void 0 ? _a : null;
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;
61
110
  this._settings = props.formContext.settings;
111
+ this._useSecretsManager =
112
+ (_d = this._settings.get('UseSecretsManager').composite) !== null && _d !== void 0 ? _d : true;
62
113
  // Initialize the providers schema.
63
114
  const providerSchema = JSONExt.deepCopy(baseSettings);
64
115
  providerSchema.properties.provider = {
@@ -76,7 +127,7 @@ export class AiSettings extends React.Component {
76
127
  const labSettings = this._settings.get('AIprovider').composite;
77
128
  if (labSettings && Object.keys(labSettings).includes('provider')) {
78
129
  // Get the provider name.
79
- const provider = (_b = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _b === void 0 ? void 0 : _b[1];
130
+ const provider = (_e = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _e === void 0 ? void 0 : _e[1];
80
131
  // Save the settings.
81
132
  const settings = {
82
133
  _current: provider
@@ -96,6 +147,40 @@ export class AiSettings extends React.Component {
96
147
  this._settings
97
148
  .set('AIprovider', this._currentSettings)
98
149
  .catch(console.error);
150
+ this._settings.changed.connect(() => {
151
+ var _a;
152
+ const useSecretsManager = (_a = this._settings.get('UseSecretsManager').composite) !== null && _a !== void 0 ? _a : true;
153
+ if (useSecretsManager !== this._useSecretsManager) {
154
+ this.updateUseSecretsManager(useSecretsManager);
155
+ }
156
+ });
157
+ }
158
+ async componentDidUpdate() {
159
+ var _a;
160
+ if (!this._secretsManager || !this._useSecretsManager) {
161
+ return;
162
+ }
163
+ // Attach the password inputs to the secrets manager only if they have changed.
164
+ const inputs = ((_a = this._formRef.current) === null || _a === void 0 ? void 0 : _a.getElementsByTagName('input')) || [];
165
+ if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
166
+ return;
167
+ }
168
+ await this._secretsManager.detachAll(SECRETS_NAMESPACE);
169
+ this._formInputs = [...inputs];
170
+ this._unsavedFields = [];
171
+ for (let i = 0; i < inputs.length; i++) {
172
+ if (inputs[i].type.toLowerCase() === 'password') {
173
+ const label = inputs[i].getAttribute('label');
174
+ if (label) {
175
+ const id = getSecretId(this._provider, label);
176
+ this._secretsManager.attach(SECRETS_NAMESPACE, id, inputs[i], (value) => this._onPasswordUpdated(label, value));
177
+ this._unsavedFields.push(label);
178
+ }
179
+ }
180
+ }
181
+ if (this._settingConnector instanceof SettingConnector) {
182
+ this._settingConnector.doNotSave = this._unsavedFields;
183
+ }
99
184
  }
100
185
  /**
101
186
  * Get the current provider from the local storage.
@@ -126,8 +211,10 @@ export class AiSettings extends React.Component {
126
211
  */
127
212
  saveSettings(value) {
128
213
  var _a;
214
+ const currentSettings = { ...value };
129
215
  const settings = JSON.parse((_a = localStorage.getItem(STORAGE_NAME)) !== null && _a !== void 0 ? _a : '{}');
130
- settings[this._provider] = value;
216
+ this._unsavedFields.forEach(field => delete currentSettings[field]);
217
+ settings[this._provider] = currentSettings;
131
218
  localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
132
219
  }
133
220
  /**
@@ -180,7 +267,7 @@ export class AiSettings extends React.Component {
180
267
  this.setState({ instruction: renderer.node });
181
268
  }
182
269
  render() {
183
- return (React.createElement(React.Fragment, null,
270
+ return (React.createElement("div", { ref: this._formRef },
184
271
  React.createElement(WrappedFormComponent, { formData: { provider: this._provider }, schema: this._providerSchema, onChange: this._onProviderChanged }),
185
272
  this.state.instruction !== null && (React.createElement("details", null,
186
273
  React.createElement("summary", { className: INSTRUCTION_CLASS }, "Instructions"),
@@ -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
@@ -2,7 +2,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
2
2
  import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
3
3
  import { ISignal } from '@lumino/signaling';
4
4
  import { JSONSchema7 } from 'json-schema';
5
- import { IBaseCompleter } from './llm-models';
5
+ import { IBaseCompleter } from './base-completer';
6
6
  export interface IDict<T = any> {
7
7
  [key: string]: T;
8
8
  }
@@ -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.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -14,7 +14,7 @@
14
14
  "license": "BSD-3-Clause",
15
15
  "author": "JupyterLite Contributors",
16
16
  "files": [
17
- "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
17
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf,md}",
18
18
  "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
19
19
  "src/**/*.{ts,tsx}",
20
20
  "schema/*.json"
@@ -27,9 +27,9 @@
27
27
  "url": "https://github.com/jupyterlite/ai.git"
28
28
  },
29
29
  "scripts": {
30
- "build": "node ./scripts/settings-generator.js && jlpm build:lib && jlpm build:labextension:dev",
30
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
31
31
  "build:dev": "jlpm build:lib && jlpm build:labextension:dev",
32
- "build:prod": "node ./scripts/settings-generator.js && jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
32
+ "build:prod": "jlpm settings:build && jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
33
33
  "build:labextension": "jupyter labextension build .",
34
34
  "build:labextension:dev": "jupyter labextension build --development True .",
35
35
  "build:lib": "tsc --sourceMap",
@@ -47,6 +47,8 @@
47
47
  "prettier": "jlpm prettier:base --write --list-different",
48
48
  "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
49
49
  "prettier:check": "jlpm prettier:base --check",
50
+ "settings:build": "node ./scripts/settings-checker.js --generate",
51
+ "settings:check": "node ./scripts/settings-checker.js",
50
52
  "stylelint": "jlpm stylelint:check --fix",
51
53
  "stylelint:check": "stylelint --cache \"style/**/*.css\"",
52
54
  "watch": "run-p watch:src watch:labextension",
@@ -54,13 +56,13 @@
54
56
  "watch:labextension": "jupyter labextension watch ."
55
57
  },
56
58
  "dependencies": {
57
- "@jupyter/chat": "^0.7.1",
59
+ "@jupyter/chat": "^0.9.0",
58
60
  "@jupyterlab/application": "^4.4.0-alpha.0",
59
61
  "@jupyterlab/apputils": "^4.5.0-alpha.0",
60
62
  "@jupyterlab/completer": "^4.4.0-alpha.0",
61
63
  "@jupyterlab/notebook": "^4.4.0-alpha.0",
62
64
  "@jupyterlab/rendermime": "^4.4.0-alpha.0",
63
- "@jupyterlab/settingregistry": "^4.4.0-alpha.0",
65
+ "@jupyterlab/settingregistry": "^4.4.0-beta.1",
64
66
  "@jupyterlab/ui-components": "^4.4.0-alpha.0",
65
67
  "@langchain/anthropic": "^0.3.9",
66
68
  "@langchain/community": "^0.3.31",
@@ -75,6 +77,8 @@
75
77
  "@rjsf/core": "^4.2.0",
76
78
  "@rjsf/utils": "^5.18.4",
77
79
  "@rjsf/validator-ajv8": "^5.18.4",
80
+ "json5": "^2.2.3",
81
+ "jupyter-secrets-manager": "^0.2.0",
78
82
  "react": "^18.2.0",
79
83
  "react-dom": "^18.2.0"
80
84
  },
@@ -115,6 +119,9 @@
115
119
  "jupyterlab": {
116
120
  "extension": true,
117
121
  "outputDir": "jupyterlite_ai/labextension",
118
- "schemaDir": "schema"
122
+ "schemaDir": "schema",
123
+ "disabledExtensions": [
124
+ "@jupyterlab/apputils-extension:settings-connector"
125
+ ]
119
126
  }
120
127
  }
@@ -5,6 +5,12 @@
5
5
  "jupyter.lab.setting-icon-label": "JupyterLite AI Chat",
6
6
  "type": "object",
7
7
  "properties": {
8
+ "UseSecretsManager": {
9
+ "type": "boolean",
10
+ "title": "Use secrets manager",
11
+ "description": "Whether to use or not the secrets manager. If not, secrets will be stored in the browser (local storage)",
12
+ "default": true
13
+ },
8
14
  "AIprovider": {
9
15
  "type": "object",
10
16
  "title": "AI provider",
@@ -4,9 +4,12 @@
4
4
  */
5
5
 
6
6
  import {
7
+ ChatCommand,
7
8
  ChatModel,
9
+ IChatCommandProvider,
8
10
  IChatHistory,
9
11
  IChatMessage,
12
+ IInputModel,
10
13
  INewMessage
11
14
  } from '@jupyter/chat';
12
15
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
@@ -136,9 +139,11 @@ export class ChatHandler extends ChatModel {
136
139
 
137
140
  let content = '';
138
141
 
142
+ this._controller = new AbortController();
139
143
  try {
140
144
  for await (const chunk of await this._providerRegistry.currentChatModel.stream(
141
- messages
145
+ messages,
146
+ { signal: this._controller.signal }
142
147
  )) {
143
148
  content += chunk.content ?? chunk;
144
149
  botMsg.body = content;
@@ -159,6 +164,7 @@ export class ChatHandler extends ChatModel {
159
164
  return false;
160
165
  } finally {
161
166
  this.updateWriters([]);
167
+ this._controller = null;
162
168
  }
163
169
  }
164
170
 
@@ -174,16 +180,52 @@ export class ChatHandler extends ChatModel {
174
180
  super.messageAdded(message);
175
181
  }
176
182
 
183
+ stopStreaming(): void {
184
+ this._controller?.abort();
185
+ }
186
+
177
187
  private _providerRegistry: IAIProviderRegistry;
178
188
  private _personaName = 'AI';
179
189
  private _prompt: string;
180
190
  private _errorMessage: string = '';
181
191
  private _history: IChatHistory = { messages: [] };
182
192
  private _defaultErrorMessage = 'AI provider not configured';
193
+ private _controller: AbortController | null = null;
183
194
  }
184
195
 
185
196
  export namespace ChatHandler {
186
197
  export interface IOptions extends ChatModel.IOptions {
187
198
  providerRegistry: IAIProviderRegistry;
188
199
  }
200
+
201
+ export class ClearCommandProvider implements IChatCommandProvider {
202
+ public id: string = '@jupyterlite/ai:clear-commands';
203
+ private _slash_commands: ChatCommand[] = [
204
+ {
205
+ name: '/clear',
206
+ providerId: this.id,
207
+ replaceWith: '/clear',
208
+ description: 'Clear the chat'
209
+ }
210
+ ];
211
+ async getChatCommands(inputModel: IInputModel) {
212
+ const match = inputModel.currentWord?.match(/^\/\w*/)?.[0];
213
+ if (!match) {
214
+ return [];
215
+ }
216
+
217
+ const commands = this._slash_commands.filter(cmd =>
218
+ cmd.name.startsWith(match)
219
+ );
220
+ return commands;
221
+ }
222
+
223
+ async handleChatCommand(
224
+ command: ChatCommand,
225
+ inputModel: IInputModel
226
+ ): Promise<void> {
227
+ // no handling needed because `replaceWith` is set in each command.
228
+ return;
229
+ }
230
+ }
189
231
  }
@@ -4,7 +4,7 @@ import {
4
4
  IInlineCompletionProvider
5
5
  } from '@jupyterlab/completer';
6
6
 
7
- import { IBaseCompleter } from './llm-models';
7
+ import { IBaseCompleter } from './base-completer';
8
8
  import { IAIProviderRegistry } from './tokens';
9
9
 
10
10
  /**
@@ -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
  }