Skip to content

Commit b8e1ba6

Browse files
authored
Fix the notification in settings (#119)
* Better handling of error notifications when setting the models * Fix and clean the provider registry * Use a debouncer instead of a throttler (did not work as expected) * Fix tests
1 parent 87f3fb6 commit b8e1ba6

File tree

6 files changed

+70
-63
lines changed

6 files changed

+70
-63
lines changed

src/default-providers/index.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,16 @@ const webLLMProviderPlugin: JupyterFrontEndPlugin<void> = {
114114
});
115115

116116
registry.providerChanged.connect(async (sender, role) => {
117-
const { currentChatModel, chatError } = registry;
118-
if (currentChatModel === null) {
119-
Notification.emit(chatError, 'error', {
120-
autoClose: 2000
121-
});
122-
return;
123-
}
117+
const { currentChatModel } = registry;
124118

125119
// TODO: implement a proper way to handle models that may need to be initialized before being used.
126120
// Mostly applies to WebLLM and ChromeAI as they may need to download the model in the browser first.
127121
if (registry.currentName(role) === 'WebLLM') {
122+
// Leaving this check here, but it should never happen, this check is done in
123+
// the provider registry, and the current name is set to 'None' if there is a
124+
// compatibility error.
128125
const compatibilityError = await webLLMCompatibilityCheck();
129-
130126
if (compatibilityError) {
131-
Notification.dismiss();
132-
Notification.emit(compatibilityError, 'error', {
133-
autoClose: 2000
134-
});
135127
return;
136128
}
137129

src/provider.ts

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { Notification } from '@jupyterlab/apputils';
12
import {
23
CompletionHandler,
34
IInlineCompletionContext
45
} from '@jupyterlab/completer';
56
import { BaseLanguageModel } from '@langchain/core/language_models/base';
67
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
7-
import { ISignal, Signal } from '@lumino/signaling';
88
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
9+
import { Debouncer } from '@lumino/polling';
10+
import { ISignal, Signal } from '@lumino/signaling';
911
import { JSONSchema7 } from 'json-schema';
1012
import { ISecretsManager } from 'jupyter-secrets-manager';
1113

@@ -21,37 +23,7 @@ import {
2123
import { AIChatModel, AICompleter } from './types/ai-model';
2224

2325
const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
24-
25-
export const chatSystemPrompt = (
26-
options: AIProviderRegistry.IPromptOptions
27-
) => `
28-
You are Jupyternaut, a conversational assistant living in JupyterLab to help users.
29-
You are not a language model, but rather an application built on a foundation model from ${options.provider_name}.
30-
You are talkative and you provide lots of specific details from the foundation model's context.
31-
You may use Markdown to format your response.
32-
If your response includes code, they must be enclosed in Markdown fenced code blocks (with triple backticks before and after).
33-
If your response includes mathematical notation, they must be expressed in LaTeX markup and enclosed in LaTeX delimiters.
34-
All dollar quantities (of USD) must be formatted in LaTeX, with the \`$\` symbol escaped by a single backslash \`\\\`.
35-
- Example prompt: \`If I have \\\\$100 and spend \\\\$20, how much money do I have left?\`
36-
- **Correct** response: \`You have \\(\\$80\\) remaining.\`
37-
- **Incorrect** response: \`You have $80 remaining.\`
38-
If you do not know the answer to a question, answer truthfully by responding that you do not know.
39-
The following is a friendly conversation between you and a human.
40-
`;
41-
42-
export const COMPLETION_SYSTEM_PROMPT = `
43-
You are an application built to provide helpful code completion suggestions.
44-
You should only produce code. Keep comments to minimum, use the
45-
programming language comment syntax. Produce clean code.
46-
The code is written in JupyterLab, a data analysis and code development
47-
environment which can execute code extended with additional syntax for
48-
interactive features, such as magics.
49-
Only give raw strings back, do not format the response using backticks.
50-
The output should be a single string, and should only contain the code that will complete the
51-
give code passed as input, no explanation whatsoever.
52-
Do not include the prompt in the output, only the string that should be appended to the current input.
53-
Here is the code to complete:
54-
`;
26+
const NOTIFICATION_DELAY = 2000;
5527

5628
export class AIProviderRegistry implements IAIProviderRegistry {
5729
/**
@@ -60,6 +32,11 @@ export class AIProviderRegistry implements IAIProviderRegistry {
6032
constructor(options: AIProviderRegistry.IOptions) {
6133
this._secretsManager = options.secretsManager || null;
6234
Private.setToken(options.token);
35+
36+
this._notifications = {
37+
chat: new Debouncer(this._emitErrorNotification, NOTIFICATION_DELAY),
38+
completer: new Debouncer(this._emitErrorNotification, NOTIFICATION_DELAY)
39+
};
6340
}
6441

6542
/**
@@ -206,18 +183,39 @@ export class AIProviderRegistry implements IAIProviderRegistry {
206183
}
207184

208185
/**
209-
* Get the current chat error;
186+
* Get/set the current chat error;
210187
*/
211188
get chatError(): string {
212189
return this._chatError;
213190
}
191+
private set chatError(error: string) {
192+
this._chatError = error;
193+
if (error !== '') {
194+
this._notifications.chat.invoke(`Chat: ${error}`);
195+
}
196+
}
214197

215198
/**
216-
* Get the current completer error.
199+
* Get/set the current completer error.
217200
*/
218201
get completerError(): string {
219202
return this._completerError;
220203
}
204+
private set completerError(error: string) {
205+
this._completerError = error;
206+
if (error !== '') {
207+
this._notifications.completer.invoke(`Completer: ${error}`);
208+
}
209+
}
210+
211+
/**
212+
* A function to emit a notification error.
213+
*/
214+
private _emitErrorNotification(error: string) {
215+
Notification.emit(error, 'error', {
216+
autoClose: NOTIFICATION_DELAY
217+
});
218+
}
221219

222220
/**
223221
* Set the completer provider.
@@ -228,11 +226,11 @@ export class AIProviderRegistry implements IAIProviderRegistry {
228226
async setCompleterProvider(
229227
settings: ReadonlyPartialJSONObject
230228
): Promise<void> {
231-
this._completerError = '';
229+
this.completerError = '';
232230
if (!Object.keys(settings).includes('provider')) {
233231
Private.setName('completer', 'None');
234232
Private.setCompleter(null);
235-
this._completerError =
233+
this.completerError =
236234
'The provider is missing from the completer settings';
237235
return;
238236
}
@@ -253,7 +251,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
253251
if (compatibilityCheck !== undefined) {
254252
const error = await compatibilityCheck();
255253
if (error !== null) {
256-
this._completerError = error.trim();
254+
this.completerError = error.trim();
257255
Private.setName('completer', 'None');
258256
this._providerChanged.emit('completer');
259257
return;
@@ -272,7 +270,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
272270
})
273271
);
274272
} catch (e: any) {
275-
this._completerError = e.message;
273+
this.completerError = e.message;
276274
}
277275
} else {
278276
Private.setCompleter(null);
@@ -288,11 +286,11 @@ export class AIProviderRegistry implements IAIProviderRegistry {
288286
* @param options - An object with the name and the settings of the provider to use.
289287
*/
290288
async setChatProvider(settings: ReadonlyPartialJSONObject): Promise<void> {
291-
this._chatError = '';
289+
this.chatError = '';
292290
if (!Object.keys(settings).includes('provider')) {
293-
Private.setName('completer', 'None');
294-
Private.setCompleter(null);
295-
this._chatError = 'The provider is missing from the chat settings';
291+
Private.setName('chat', 'None');
292+
Private.setChatModel(null);
293+
this.chatError = 'The provider is missing from the chat settings';
296294
return;
297295
}
298296
const provider = settings['provider'] as string;
@@ -312,7 +310,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
312310
if (compatibilityCheck !== undefined) {
313311
const error = await compatibilityCheck();
314312
if (error !== null) {
315-
this._chatError = error.trim();
313+
this.chatError = error.trim();
316314
Private.setName('chat', 'None');
317315
this._providerChanged.emit('chat');
318316
return;
@@ -330,7 +328,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
330328
})
331329
);
332330
} catch (e: any) {
333-
this._chatError = e.message;
331+
this.chatError = e.message;
334332
Private.setChatModel(null);
335333
}
336334
} else {
@@ -378,6 +376,9 @@ export class AIProviderRegistry implements IAIProviderRegistry {
378376
private _providerChanged = new Signal<IAIProviderRegistry, ModelRole>(this);
379377
private _chatError: string = '';
380378
private _completerError: string = '';
379+
private _notifications: {
380+
[key in ModelRole]: Debouncer;
381+
};
381382
private _deferredProvider: {
382383
[key in ModelRole]: ReadonlyPartialJSONObject | null;
383384
} = {

src/settings/panel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export class AiProviderSettings extends React.Component<
481481
if (key.toLowerCase().includes('key')) {
482482
this._secretFields.push(key);
483483

484-
// If the secrets manager is not used, do not show the secrets fields.
484+
// If the secrets manager is not used, show the secrets fields.
485485
// If the secrets manager is used, check if the fields should be visible.
486486
const showSecretFields =
487487
!this._useSecretsManager ||
@@ -651,7 +651,7 @@ export class AiProviderSettings extends React.Component<
651651
formData={{ provider: this._provider }}
652652
schema={this._providerSchema}
653653
onChange={this._onProviderChanged}
654-
idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}`}
654+
idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}`}
655655
/>
656656
{this.state.compatibilityError !== null && (
657657
<div className={ERROR_CLASS}>
@@ -684,7 +684,7 @@ export class AiProviderSettings extends React.Component<
684684
schema={this.state.schema}
685685
onChange={this._onFormChanged}
686686
uiSchema={this._uiSchema}
687-
idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}`}
687+
idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}`}
688688
formContext={{
689689
...this.props.formContext,
690690
defaultFormData: this._defaultFormData

ui-tests/tests/chat-panel.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import { Locator } from '@playwright/test';
1313
import { setUpOllama } from './test-utils';
1414

1515
test.use({
16-
mockSettings: { ...galata.DEFAULT_SETTINGS }
16+
mockSettings: {
17+
...galata.DEFAULT_SETTINGS,
18+
'@jupyterlab/apputils-extension:notification': {
19+
checkForUpdates: false,
20+
fetchNews: 'false',
21+
doNotDisturbMode: true
22+
}
23+
}
1724
});
1825

1926
async function openChatPanel(page: IJupyterLabPageFixture): Promise<Locator> {

ui-tests/tests/code-completion.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import { Locator, Request } from '@playwright/test';
1313
import { openSettings, setUpOllama } from './test-utils';
1414

1515
test.use({
16-
mockSettings: { ...galata.DEFAULT_SETTINGS }
16+
mockSettings: {
17+
...galata.DEFAULT_SETTINGS,
18+
'@jupyterlab/apputils-extension:notification': {
19+
checkForUpdates: false,
20+
fetchNews: 'false',
21+
doNotDisturbMode: true
22+
}
23+
}
1724
});
1825

1926
// Set up Ollama with default model.

ui-tests/tests/test-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ export const setUpOllama = async (
3535

3636
const settingsPanel = await openSettings(page, 'AI provider');
3737
const providerSelect = settingsPanel.locator(
38-
'select[name="jp-SettingsEditor-@jupyterlite/ai:provider-registry_provider"]'
38+
'select[name="jp-SettingsEditor-@jupyterlite/ai:provider-registry-chat_provider"]'
3939
);
4040
await providerSelect.selectOption('Ollama');
4141
const modelInput = settingsPanel.locator(
42-
'input[name="jp-SettingsEditor-@jupyterlite/ai:provider-registry_model"]'
42+
'input[name="jp-SettingsEditor-@jupyterlite/ai:provider-registry-chat_model"]'
4343
);
4444

4545
await modelInput.scrollIntoViewIfNeeded();

0 commit comments

Comments
 (0)