@jupyterlite/ai 0.9.0-a4 → 0.9.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.
- package/README.md +20 -89
- package/lib/chat-model.js +85 -1
- package/lib/components/completion-status.d.ts +20 -0
- package/lib/components/completion-status.js +51 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/index.js +29 -16
- package/lib/models/settings-model.js +9 -1
- package/lib/providers/built-in-providers.d.ts +0 -4
- package/lib/providers/built-in-providers.js +15 -23
- package/lib/tokens.d.ts +7 -0
- package/lib/widgets/ai-settings.js +4 -3
- package/lib/widgets/provider-config-dialog.js +15 -8
- package/package.json +3 -2
- package/src/chat-model.ts +103 -1
- package/src/components/completion-status.tsx +79 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +35 -16
- package/src/models/settings-model.ts +8 -1
- package/src/providers/built-in-providers.ts +15 -24
- package/src/tokens.ts +5 -0
- package/src/widgets/ai-settings.tsx +5 -3
- package/src/widgets/provider-config-dialog.tsx +43 -16
- package/style/base.css +14 -0
package/README.md
CHANGED
|
@@ -52,13 +52,25 @@ The process is different for each provider, so you may refer to their documentat
|
|
|
52
52
|
|
|
53
53
|

|
|
54
54
|
|
|
55
|
+
### Using a generic OpenAI-compatible provider
|
|
56
|
+
|
|
57
|
+
The Generic provider allows you to connect to any OpenAI-compatible API endpoint, including local servers like Ollama and LiteLLM.
|
|
58
|
+
|
|
59
|
+
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
60
|
+
2. Click on "Add a new provider"
|
|
61
|
+
3. Select the **Generic (OpenAI-compatible)** provider
|
|
62
|
+
4. Configure the following settings:
|
|
63
|
+
- **Base URL**: The base URL of your API endpoint (suggestions are provided for common local servers)
|
|
64
|
+
- **Model**: The model name to use
|
|
65
|
+
- **API Key**: Your API key (if required by the provider)
|
|
66
|
+
|
|
55
67
|
### Using Ollama
|
|
56
68
|
|
|
57
69
|
[Ollama](https://ollama.com/) allows you to run open-weight LLMs locally on your machine.
|
|
58
70
|
|
|
59
71
|
#### Setting up Ollama
|
|
60
72
|
|
|
61
|
-
1. Install Ollama following the instructions at https://ollama.com/download
|
|
73
|
+
1. Install Ollama following the instructions at <https://ollama.com/download>
|
|
62
74
|
2. Pull a model, for example:
|
|
63
75
|
|
|
64
76
|
```bash
|
|
@@ -71,21 +83,11 @@ ollama pull llama3.2
|
|
|
71
83
|
|
|
72
84
|
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
73
85
|
2. Click on "Add a new provider"
|
|
74
|
-
3. Select the **
|
|
86
|
+
3. Select the **Generic (OpenAI-compatible)** provider
|
|
75
87
|
4. Configure the following settings:
|
|
88
|
+
- **Base URL**: Select `http://localhost:11434/v1` from the suggestions (or enter manually)
|
|
76
89
|
- **Model**: The model name you pulled (e.g., `llama3.2`)
|
|
77
|
-
|
|
78
|
-
### Using a generic OpenAI-compatible provider
|
|
79
|
-
|
|
80
|
-
The Generic provider allows you to connect to any OpenAI-compatible API endpoint.
|
|
81
|
-
|
|
82
|
-
1. In JupyterLab, open the AI settings panel and go to the **Providers** section
|
|
83
|
-
2. Click on "Add a new provider"
|
|
84
|
-
3. Select the **Generic** provider
|
|
85
|
-
4. Configure the following settings:
|
|
86
|
-
- **Base URL**: The base URL of your API endpoint
|
|
87
|
-
- **Model**: The model name to use
|
|
88
|
-
- **API Key**: Your API key (if required by the provider)
|
|
90
|
+
- **API Key**: Leave empty (not required for Ollama)
|
|
89
91
|
|
|
90
92
|
### Using LiteLLM Proxy
|
|
91
93
|
|
|
@@ -97,7 +99,7 @@ Using LiteLLM Proxy with jupyterlite-ai provides flexibility to switch between d
|
|
|
97
99
|
|
|
98
100
|
1. Install LiteLLM:
|
|
99
101
|
|
|
100
|
-
Follow the instructions at https://docs.litellm.ai/docs/simple_proxy
|
|
102
|
+
Follow the instructions at <https://docs.litellm.ai/docs/simple_proxy>.
|
|
101
103
|
|
|
102
104
|
2. Create a `litellm_config.yaml` file with your model configuration:
|
|
103
105
|
|
|
@@ -144,7 +146,7 @@ Providers are based on the [Vercel AI SDK](https://sdk.vercel.ai/docs/introducti
|
|
|
144
146
|
|
|
145
147
|
### Registering a Custom Provider
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
#### Example: Registering a custom OpenAI-compatible provider
|
|
148
150
|
|
|
149
151
|
```typescript
|
|
150
152
|
import {
|
|
@@ -192,7 +194,7 @@ The provider configuration object requires the following properties:
|
|
|
192
194
|
- `supportsBaseURL`: Whether the provider supports a custom base URL
|
|
193
195
|
- `factory`: Function that creates and returns a language model (the registry automatically wraps it for chat usage)
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
#### Example: Using a custom fetch function
|
|
196
198
|
|
|
197
199
|
You can provide a custom `fetch` function to the provider, which is useful for adding custom headers, handling authentication, or routing requests through a proxy:
|
|
198
200
|
|
|
@@ -253,75 +255,4 @@ pip uninstall jupyterlite-ai
|
|
|
253
255
|
|
|
254
256
|
## Contributing
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Note: You will need NodeJS to build the extension package.
|
|
259
|
-
|
|
260
|
-
The `jlpm` command is JupyterLab's pinned version of
|
|
261
|
-
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
|
|
262
|
-
`yarn` or `npm` in lieu of `jlpm` below.
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
# Clone the repo to your local environment
|
|
266
|
-
# Change directory to the jupyterlite_ai directory
|
|
267
|
-
# Install package in development mode
|
|
268
|
-
pip install -e "."
|
|
269
|
-
# Link your development version of the extension with JupyterLab
|
|
270
|
-
jupyter labextension develop . --overwrite
|
|
271
|
-
# Rebuild extension Typescript source after making changes
|
|
272
|
-
jlpm build
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
276
|
-
|
|
277
|
-
```bash
|
|
278
|
-
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
279
|
-
jlpm watch
|
|
280
|
-
# Run JupyterLab in another terminal
|
|
281
|
-
jupyter lab
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
|
|
285
|
-
|
|
286
|
-
By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
jupyter lab build --minimize=False
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Running UI tests
|
|
293
|
-
|
|
294
|
-
The UI tests use Playwright and can be configured with environment variables:
|
|
295
|
-
|
|
296
|
-
- `PWVIDEO`: Controls video recording during tests (default: `retain-on-failure`)
|
|
297
|
-
- `on`: Record video for all tests
|
|
298
|
-
- `off`: Do not record video
|
|
299
|
-
- `retain-on-failure`: Only keep videos for failed tests
|
|
300
|
-
- `PWSLOWMO`: Adds a delay (in milliseconds) between Playwright actions for debugging (default: `0`)
|
|
301
|
-
|
|
302
|
-
Example usage:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
# Record all test videos
|
|
306
|
-
PWVIDEO=on jlpm playwright test
|
|
307
|
-
|
|
308
|
-
# Slow down test execution by 500ms per action
|
|
309
|
-
PWSLOWMO=500 jlpm playwright test
|
|
310
|
-
|
|
311
|
-
# Combine both options
|
|
312
|
-
PWVIDEO=on PWSLOWMO=1000 jlpm playwright test
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Development uninstall
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
pip uninstall jupyterlite-ai
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
|
|
322
|
-
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
|
|
323
|
-
folder is located. Then you can remove the symlink named `@jupyterlite/ai` within that folder.
|
|
324
|
-
|
|
325
|
-
### Packaging the extension
|
|
326
|
-
|
|
327
|
-
See [RELEASE](RELEASE.md)
|
|
258
|
+
See [CONTRIBUTING](CONTRIBUTING.md)
|
package/lib/chat-model.js
CHANGED
|
@@ -524,7 +524,91 @@ ${toolsList}
|
|
|
524
524
|
const code = cell.source || '';
|
|
525
525
|
const cellType = cell.cell_type;
|
|
526
526
|
const lang = cellType === 'code' ? kernelLang : cellType;
|
|
527
|
-
|
|
527
|
+
const DISPLAY_PRIORITY = [
|
|
528
|
+
'application/vnd.jupyter.widget-view+json',
|
|
529
|
+
'application/javascript',
|
|
530
|
+
'text/html',
|
|
531
|
+
'image/svg+xml',
|
|
532
|
+
'image/png',
|
|
533
|
+
'image/jpeg',
|
|
534
|
+
'text/markdown',
|
|
535
|
+
'text/latex',
|
|
536
|
+
'text/plain'
|
|
537
|
+
];
|
|
538
|
+
function extractDisplay(data) {
|
|
539
|
+
for (const mime of DISPLAY_PRIORITY) {
|
|
540
|
+
if (!(mime in data)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const value = data[mime];
|
|
544
|
+
if (!value) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
switch (mime) {
|
|
548
|
+
case 'application/vnd.jupyter.widget-view+json':
|
|
549
|
+
return `Widget: ${value.model_id ?? 'unknown model'}`;
|
|
550
|
+
case 'image/png':
|
|
551
|
+
return `}...)`;
|
|
552
|
+
case 'image/jpeg':
|
|
553
|
+
return `}...)`;
|
|
554
|
+
case 'image/svg+xml':
|
|
555
|
+
return String(value).slice(0, 500) + '...\n[svg truncated]';
|
|
556
|
+
case 'text/html':
|
|
557
|
+
return (String(value).slice(0, 1000) +
|
|
558
|
+
(String(value).length > 1000 ? '\n...[truncated]' : ''));
|
|
559
|
+
case 'text/markdown':
|
|
560
|
+
case 'text/latex':
|
|
561
|
+
case 'text/plain': {
|
|
562
|
+
let text = Array.isArray(value)
|
|
563
|
+
? value.join('')
|
|
564
|
+
: String(value);
|
|
565
|
+
if (text.length > 2000) {
|
|
566
|
+
text = text.slice(0, 2000) + '\n...[truncated]';
|
|
567
|
+
}
|
|
568
|
+
return text;
|
|
569
|
+
}
|
|
570
|
+
default:
|
|
571
|
+
return JSON.stringify(value).slice(0, 2000);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return JSON.stringify(data).slice(0, 2000);
|
|
575
|
+
}
|
|
576
|
+
let outputs = '';
|
|
577
|
+
if (cellType === 'code' && Array.isArray(cell.outputs)) {
|
|
578
|
+
outputs = cell.outputs
|
|
579
|
+
.map((output) => {
|
|
580
|
+
if (output.output_type === 'stream') {
|
|
581
|
+
return output.text;
|
|
582
|
+
}
|
|
583
|
+
else if (output.output_type === 'error') {
|
|
584
|
+
const err = output;
|
|
585
|
+
return `${err.ename}: ${err.evalue}\n${(err.traceback || []).join('\n')}`;
|
|
586
|
+
}
|
|
587
|
+
else if (output.output_type === 'execute_result' ||
|
|
588
|
+
output.output_type === 'display_data') {
|
|
589
|
+
const data = output.data;
|
|
590
|
+
if (!data) {
|
|
591
|
+
return '';
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
return extractDisplay(data);
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
console.error('Cannot extract cell output', e);
|
|
598
|
+
return '';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return '';
|
|
602
|
+
})
|
|
603
|
+
.filter(Boolean)
|
|
604
|
+
.join('\n---\n');
|
|
605
|
+
if (outputs.length > 2000) {
|
|
606
|
+
outputs = outputs.slice(0, 2000) + '\n...[truncated]';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return (`**Cell [${cellInfo.id}] (${cellType}):**\n` +
|
|
610
|
+
`\`\`\`${lang}\n${code}\n\`\`\`` +
|
|
611
|
+
(outputs ? `\n**Outputs:**\n\`\`\`text\n${outputs}\n\`\`\`` : ''));
|
|
528
612
|
})
|
|
529
613
|
.filter(Boolean)
|
|
530
614
|
.join('\n\n');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AISettingsModel } from '../models/settings-model';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
/**
|
|
4
|
+
* The completion status props.
|
|
5
|
+
*/
|
|
6
|
+
interface ICompletionStatusProps {
|
|
7
|
+
/**
|
|
8
|
+
* The settings model.
|
|
9
|
+
*/
|
|
10
|
+
settingsModel: AISettingsModel;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The completion status widget that will be added to the status bar.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CompletionStatusWidget extends ReactWidget {
|
|
16
|
+
constructor(options: ICompletionStatusProps);
|
|
17
|
+
render(): JSX.Element;
|
|
18
|
+
private _props;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { jupyternautIcon } from '../icons';
|
|
4
|
+
const COMPLETION_STATUS_CLASS = 'jp-ai-completion-status';
|
|
5
|
+
const COMPLETION_DISABLED_CLASS = 'jp-ai-completion-disabled';
|
|
6
|
+
/**
|
|
7
|
+
* The completion status component.
|
|
8
|
+
*/
|
|
9
|
+
function CompletionStatus(props) {
|
|
10
|
+
const [disabled, setDisabled] = useState(true);
|
|
11
|
+
const [title, setTitle] = useState('');
|
|
12
|
+
/**
|
|
13
|
+
* Handle changes in the settings.
|
|
14
|
+
*/
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const stateChanged = (model) => {
|
|
17
|
+
if (model.config.useSameProviderForChatAndCompleter) {
|
|
18
|
+
setDisabled(false);
|
|
19
|
+
setTitle(`Completion using ${model.getDefaultProvider()?.model}`);
|
|
20
|
+
}
|
|
21
|
+
else if (model.config.activeCompleterProvider) {
|
|
22
|
+
setDisabled(false);
|
|
23
|
+
setTitle(`Completion using ${model.getProvider(model.config.activeCompleterProvider)?.model}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setDisabled(true);
|
|
27
|
+
setTitle('No completion');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
props.settingsModel.stateChanged.connect(stateChanged);
|
|
31
|
+
stateChanged(props.settingsModel);
|
|
32
|
+
return () => {
|
|
33
|
+
props.settingsModel.stateChanged.disconnect(stateChanged);
|
|
34
|
+
};
|
|
35
|
+
}, [props.settingsModel]);
|
|
36
|
+
return (React.createElement(jupyternautIcon.react, { className: disabled ? COMPLETION_DISABLED_CLASS : '', top: '2px', width: '16px', stylesheet: 'statusBar', title: title }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The completion status widget that will be added to the status bar.
|
|
40
|
+
*/
|
|
41
|
+
export class CompletionStatusWidget extends ReactWidget {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
super();
|
|
44
|
+
this.addClass(COMPLETION_STATUS_CLASS);
|
|
45
|
+
this._props = options;
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
return React.createElement(CompletionStatus, { ...this._props });
|
|
49
|
+
}
|
|
50
|
+
_props;
|
|
51
|
+
}
|
package/lib/components/index.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { INotebookTracker } from '@jupyterlab/notebook';
|
|
|
8
8
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
9
9
|
import { IKernelSpecManager } from '@jupyterlab/services';
|
|
10
10
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
11
|
+
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
11
12
|
import { settingsIcon, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
|
|
12
13
|
import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
|
|
13
14
|
import { PromiseDelegate, UUID } from '@lumino/coreutils';
|
|
@@ -16,9 +17,9 @@ import { ProviderRegistry } from './providers/provider-registry';
|
|
|
16
17
|
import { ApprovalButtons } from './approval-buttons';
|
|
17
18
|
import { ChatModelRegistry } from './chat-model-registry';
|
|
18
19
|
import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelRegistry, IDiffManager } from './tokens';
|
|
19
|
-
import { anthropicProvider, googleProvider, mistralProvider, openaiProvider,
|
|
20
|
+
import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
|
|
20
21
|
import { AICompletionProvider } from './completion';
|
|
21
|
-
import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, TokenUsageWidget } from './components';
|
|
22
|
+
import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, TokenUsageWidget } from './components';
|
|
22
23
|
import { AISettingsModel } from './models/settings-model';
|
|
23
24
|
import { DiffManager } from './diff-manager';
|
|
24
25
|
import { ToolRegistry } from './tools/tool-registry';
|
|
@@ -87,18 +88,6 @@ const openaiProviderPlugin = {
|
|
|
87
88
|
providerRegistry.registerProvider(openaiProvider);
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
|
-
/**
|
|
91
|
-
* Ollama provider plugin
|
|
92
|
-
*/
|
|
93
|
-
const ollamaProviderPlugin = {
|
|
94
|
-
id: '@jupyterlite/ai:ollama-provider',
|
|
95
|
-
description: 'Register Ollama provider',
|
|
96
|
-
autoStart: true,
|
|
97
|
-
requires: [IProviderRegistry],
|
|
98
|
-
activate: (app, providerRegistry) => {
|
|
99
|
-
providerRegistry.registerProvider(ollamaProvider);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
91
|
/**
|
|
103
92
|
* Generic provider plugin
|
|
104
93
|
*/
|
|
@@ -336,6 +325,10 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
336
325
|
execute: async (args) => {
|
|
337
326
|
const area = args.area === 'main' ? 'main' : 'side';
|
|
338
327
|
const provider = args.provider ?? undefined;
|
|
328
|
+
// Do not open the chat if the provider in args does not exists in settings.
|
|
329
|
+
if (provider && !settingsModel.getProvider(provider)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
339
332
|
const model = modelRegistry.createModel(args.name ? args.name : undefined, provider);
|
|
340
333
|
if (!model) {
|
|
341
334
|
return false;
|
|
@@ -480,6 +473,7 @@ const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => ({
|
|
|
480
473
|
});
|
|
481
474
|
settingsWidget.id = 'jupyterlite-ai-settings';
|
|
482
475
|
settingsWidget.title.icon = settingsIcon;
|
|
476
|
+
settingsWidget.title.iconClass = 'jp-ai-settings-icon';
|
|
483
477
|
// Build the completion provider
|
|
484
478
|
if (completionManager) {
|
|
485
479
|
const completionProvider = new AICompletionProvider({
|
|
@@ -500,6 +494,7 @@ const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => ({
|
|
|
500
494
|
label: 'AI Settings',
|
|
501
495
|
caption: 'Configure AI providers and behavior',
|
|
502
496
|
icon: settingsIcon,
|
|
497
|
+
iconClass: 'jp-ai-settings-icon',
|
|
503
498
|
execute: () => {
|
|
504
499
|
// Check if the widget already exists in shell
|
|
505
500
|
let widget = Array.from(app.shell.widgets('main')).find(w => w.id === 'jupyterlite-ai-settings');
|
|
@@ -644,13 +639,30 @@ const inputToolbarFactory = {
|
|
|
644
639
|
};
|
|
645
640
|
}
|
|
646
641
|
};
|
|
642
|
+
const completionStatus = {
|
|
643
|
+
id: '@jupyterlite/ai:completion-status',
|
|
644
|
+
description: 'The completion status displayed in the status bar',
|
|
645
|
+
autoStart: true,
|
|
646
|
+
requires: [IAISettingsModel],
|
|
647
|
+
optional: [IStatusBar],
|
|
648
|
+
activate: (app, settingsModel, statusBar) => {
|
|
649
|
+
if (!statusBar) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const item = new CompletionStatusWidget({ settingsModel });
|
|
653
|
+
statusBar?.registerStatusItem('completionState', {
|
|
654
|
+
item,
|
|
655
|
+
align: 'right',
|
|
656
|
+
rank: 10
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
};
|
|
647
660
|
export default [
|
|
648
661
|
providerRegistryPlugin,
|
|
649
662
|
anthropicProviderPlugin,
|
|
650
663
|
googleProviderPlugin,
|
|
651
664
|
mistralProviderPlugin,
|
|
652
665
|
openaiProviderPlugin,
|
|
653
|
-
ollamaProviderPlugin,
|
|
654
666
|
genericProviderPlugin,
|
|
655
667
|
settingsModel,
|
|
656
668
|
diffManager,
|
|
@@ -658,7 +670,8 @@ export default [
|
|
|
658
670
|
plugin,
|
|
659
671
|
toolRegistry,
|
|
660
672
|
agentManagerFactory,
|
|
661
|
-
inputToolbarFactory
|
|
673
|
+
inputToolbarFactory,
|
|
674
|
+
completionStatus
|
|
662
675
|
];
|
|
663
676
|
// Export extension points for other extensions to use
|
|
664
677
|
export * from './tokens';
|
|
@@ -166,7 +166,7 @@ Rules:
|
|
|
166
166
|
}
|
|
167
167
|
return this._config.activeCompleterProvider
|
|
168
168
|
? this.getProvider(this._config.activeCompleterProvider)
|
|
169
|
-
:
|
|
169
|
+
: undefined;
|
|
170
170
|
}
|
|
171
171
|
async addProvider(providerConfig) {
|
|
172
172
|
const id = `${providerConfig.provider}-${Date.now()}`;
|
|
@@ -219,6 +219,11 @@ Rules:
|
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
Object.assign(provider, updates);
|
|
222
|
+
Object.keys(provider).forEach(key => {
|
|
223
|
+
if (key !== 'id' && updates[key] === undefined) {
|
|
224
|
+
delete provider[key];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
222
227
|
await this.saveSetting('providers', this._config.providers);
|
|
223
228
|
}
|
|
224
229
|
async setActiveProvider(id) {
|
|
@@ -298,6 +303,9 @@ Rules:
|
|
|
298
303
|
if (value !== undefined) {
|
|
299
304
|
await this._settings.set(key, value);
|
|
300
305
|
}
|
|
306
|
+
else {
|
|
307
|
+
await this._settings.remove(key);
|
|
308
|
+
}
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
catch (error) {
|
|
@@ -15,10 +15,6 @@ export declare const mistralProvider: IProviderInfo;
|
|
|
15
15
|
* OpenAI provider
|
|
16
16
|
*/
|
|
17
17
|
export declare const openaiProvider: IProviderInfo;
|
|
18
|
-
/**
|
|
19
|
-
* Ollama provider
|
|
20
|
-
*/
|
|
21
|
-
export declare const ollamaProvider: IProviderInfo;
|
|
22
18
|
/**
|
|
23
19
|
* Generic OpenAI-compatible provider
|
|
24
20
|
*/
|
|
@@ -2,7 +2,7 @@ import { createAnthropic } from '@ai-sdk/anthropic';
|
|
|
2
2
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
3
3
|
import { createMistral } from '@ai-sdk/mistral';
|
|
4
4
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
|
-
import {
|
|
5
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
6
|
/**
|
|
7
7
|
* Anthropic provider
|
|
8
8
|
*/
|
|
@@ -194,25 +194,6 @@ export const openaiProvider = {
|
|
|
194
194
|
return openai(modelName);
|
|
195
195
|
}
|
|
196
196
|
};
|
|
197
|
-
/**
|
|
198
|
-
* Ollama provider
|
|
199
|
-
*/
|
|
200
|
-
export const ollamaProvider = {
|
|
201
|
-
id: 'ollama',
|
|
202
|
-
name: 'Ollama',
|
|
203
|
-
apiKeyRequirement: 'none',
|
|
204
|
-
defaultModels: [],
|
|
205
|
-
supportsBaseURL: true,
|
|
206
|
-
supportsHeaders: true,
|
|
207
|
-
factory: (options) => {
|
|
208
|
-
const ollama = createOllama({
|
|
209
|
-
baseURL: options.baseURL || 'http://localhost:11434/api',
|
|
210
|
-
...(options.headers && { headers: options.headers })
|
|
211
|
-
});
|
|
212
|
-
const modelName = options.model || 'phi3';
|
|
213
|
-
return ollama(modelName);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
197
|
/**
|
|
217
198
|
* Generic OpenAI-compatible provider
|
|
218
199
|
*/
|
|
@@ -225,13 +206,24 @@ export const genericProvider = {
|
|
|
225
206
|
supportsHeaders: true,
|
|
226
207
|
supportsToolCalling: true,
|
|
227
208
|
description: 'Uses /chat/completions endpoint',
|
|
209
|
+
baseUrls: [
|
|
210
|
+
{
|
|
211
|
+
url: 'http://localhost:4000',
|
|
212
|
+
description: 'Default for local LiteLLM server'
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
url: 'http://localhost:11434/v1',
|
|
216
|
+
description: 'Default for local Ollama server'
|
|
217
|
+
}
|
|
218
|
+
],
|
|
228
219
|
factory: (options) => {
|
|
229
|
-
const
|
|
220
|
+
const openaiCompatible = createOpenAICompatible({
|
|
221
|
+
name: options.provider,
|
|
230
222
|
apiKey: options.apiKey || 'dummy',
|
|
231
|
-
|
|
223
|
+
baseURL: options.baseURL ?? '',
|
|
232
224
|
...(options.headers && { headers: options.headers })
|
|
233
225
|
});
|
|
234
226
|
const modelName = options.model || 'gpt-4o';
|
|
235
|
-
return
|
|
227
|
+
return openaiCompatible(modelName);
|
|
236
228
|
}
|
|
237
229
|
};
|
package/lib/tokens.d.ts
CHANGED
|
@@ -130,6 +130,13 @@ export interface IProviderInfo {
|
|
|
130
130
|
* Optional description shown in the UI
|
|
131
131
|
*/
|
|
132
132
|
description?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Optional URL suggestions
|
|
135
|
+
*/
|
|
136
|
+
baseUrls?: {
|
|
137
|
+
url: string;
|
|
138
|
+
description?: string;
|
|
139
|
+
}[];
|
|
133
140
|
/**
|
|
134
141
|
* Factory function for creating language models
|
|
135
142
|
*/
|
|
@@ -353,6 +353,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
353
353
|
overflow: 'auto',
|
|
354
354
|
p: 2,
|
|
355
355
|
pb: 4,
|
|
356
|
+
boxSizing: 'border-box',
|
|
356
357
|
fontSize: '0.9rem'
|
|
357
358
|
} },
|
|
358
359
|
React.createElement(Box, { sx: { mb: 2, display: 'flex', alignItems: 'center', gap: 2 } },
|
|
@@ -376,9 +377,9 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
376
377
|
}), color: "primary" }), label: "Use same provider for chat and completions" }),
|
|
377
378
|
!config.useSameProviderForChatAndCompleter && (React.createElement(FormControl, { fullWidth: true },
|
|
378
379
|
React.createElement(InputLabel, null, "Completion Provider"),
|
|
379
|
-
React.createElement(Select, { value: config.activeCompleterProvider || '', label: "Completion Provider", onChange: e => model.setActiveCompleterProvider(e.target.value || undefined) },
|
|
380
|
+
React.createElement(Select, { value: config.activeCompleterProvider || '', label: "Completion Provider", className: "jp-ai-completion-provider-select", onChange: e => model.setActiveCompleterProvider(e.target.value || undefined) },
|
|
380
381
|
React.createElement(MenuItem, { value: "" },
|
|
381
|
-
React.createElement("em", null, "
|
|
382
|
+
React.createElement("em", null, "No completion")),
|
|
382
383
|
config.providers.map(provider => (React.createElement(MenuItem, { key: provider.id, value: provider.id }, provider.name)))))))))),
|
|
383
384
|
React.createElement(Card, { elevation: 2 },
|
|
384
385
|
React.createElement(CardContent, null,
|
|
@@ -444,7 +445,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
444
445
|
useSecretsManager: e.target.checked
|
|
445
446
|
}), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
|
|
446
447
|
React.createElement("span", null, "Use the secrets manager to manage API keys"),
|
|
447
|
-
config.useSecretsManager && (React.createElement(Alert, { severity: "warning", icon: React.createElement(Error, null), sx: { mb: 2 } }, "The secrets
|
|
448
|
+
!config.useSecretsManager && (React.createElement(Alert, { severity: "warning", icon: React.createElement(Error, null), sx: { mb: 2 } }, "The secrets are stored in plain text in settings"))) })))),
|
|
448
449
|
activeTab === 1 && (React.createElement(Card, { elevation: 2 },
|
|
449
450
|
React.createElement(CardContent, null,
|
|
450
451
|
React.createElement(Typography, { variant: "h6", component: "h2", gutterBottom: true }, "Behavior Settings"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
2
2
|
import Visibility from '@mui/icons-material/Visibility';
|
|
3
3
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
4
|
-
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
/**
|
|
7
7
|
* Default parameter values for provider configuration
|
|
@@ -28,9 +28,10 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
28
28
|
label: info.name,
|
|
29
29
|
models: info.defaultModels,
|
|
30
30
|
apiKeyRequirement: info.apiKeyRequirement,
|
|
31
|
-
allowCustomModel: id === '
|
|
31
|
+
allowCustomModel: id === 'generic', // Generic allows custom models
|
|
32
32
|
supportsBaseURL: info.supportsBaseURL,
|
|
33
|
-
description: info.description
|
|
33
|
+
description: info.description,
|
|
34
|
+
baseUrls: info.baseUrls
|
|
34
35
|
};
|
|
35
36
|
});
|
|
36
37
|
}, [providerRegistry]);
|
|
@@ -124,11 +125,17 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
124
125
|
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
|
125
126
|
React.createElement(IconButton, { onClick: () => setShowApiKey(!showApiKey), edge: "end" }, showApiKey ? React.createElement(VisibilityOff, null) : React.createElement(Visibility, null))))
|
|
126
127
|
} })),
|
|
127
|
-
selectedProvider?.supportsBaseURL && (React.createElement(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
selectedProvider?.supportsBaseURL && (React.createElement(Autocomplete, { freeSolo: true, fullWidth: true, options: (selectedProvider.baseUrls ?? []).map(option => option.url), value: baseURL || '', onChange: (_, value) => {
|
|
129
|
+
if (value && typeof value === 'string') {
|
|
130
|
+
setBaseURL(value);
|
|
131
|
+
}
|
|
132
|
+
}, inputValue: baseURL || '', renderOption: (props, option) => {
|
|
133
|
+
const urlOption = (selectedProvider.baseUrls ?? []).find(u => u.url === option);
|
|
134
|
+
return (React.createElement(Box, { component: "li", ...props, key: option },
|
|
135
|
+
React.createElement(Box, null,
|
|
136
|
+
React.createElement(Typography, { variant: "body2" }, option),
|
|
137
|
+
urlOption?.description && (React.createElement(Typography, { variant: "caption", color: "text.secondary" }, urlOption.description)))));
|
|
138
|
+
}, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, label: "Base URL", placeholder: "https://api.example.com/v1", onChange: e => setBaseURL(e.target.value) })), clearOnBlur: false })),
|
|
132
139
|
React.createElement(Accordion, { expanded: expandedAdvanced, onChange: (_, isExpanded) => setExpandedAdvanced(isExpanded), sx: {
|
|
133
140
|
mt: 2,
|
|
134
141
|
bgcolor: 'transparent',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlite/ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "AI code completions and chat for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@ai-sdk/google": "^2.0.19",
|
|
59
59
|
"@ai-sdk/mistral": "^2.0.17",
|
|
60
60
|
"@ai-sdk/openai": "^2.0.44",
|
|
61
|
+
"@ai-sdk/openai-compatible": "^1.0.26",
|
|
61
62
|
"@jupyter/chat": "^0.18.2",
|
|
62
63
|
"@jupyterlab/application": "^4.0.0",
|
|
63
64
|
"@jupyterlab/apputils": "^4.5.6",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"@jupyterlab/rendermime": "^4.4.6",
|
|
72
73
|
"@jupyterlab/services": "^7.4.6",
|
|
73
74
|
"@jupyterlab/settingregistry": "^4.0.0",
|
|
75
|
+
"@jupyterlab/statusbar": "^4.4.6",
|
|
74
76
|
"@jupyterlab/ui-components": "^4.4.6",
|
|
75
77
|
"@lumino/commands": "^2.3.2",
|
|
76
78
|
"@lumino/coreutils": "^2.2.1",
|
|
@@ -86,7 +88,6 @@
|
|
|
86
88
|
"@openai/agents-extensions": "^0.1.5",
|
|
87
89
|
"ai": "^5.0.60",
|
|
88
90
|
"jupyter-secrets-manager": "^0.4.0",
|
|
89
|
-
"ollama-ai-provider-v2": "^1.4.1",
|
|
90
91
|
"zod": "^3.25.76"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
package/src/chat-model.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { AISettingsModel } from './models/settings-model';
|
|
|
24
24
|
|
|
25
25
|
import { ITokenUsage } from './tokens';
|
|
26
26
|
|
|
27
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* AI Chat Model implementation that provides chat functionality with OpenAI agents,
|
|
29
31
|
* tool integration, and MCP server support.
|
|
@@ -646,7 +648,107 @@ ${toolsList}
|
|
|
646
648
|
const cellType = cell.cell_type;
|
|
647
649
|
const lang = cellType === 'code' ? kernelLang : cellType;
|
|
648
650
|
|
|
649
|
-
|
|
651
|
+
const DISPLAY_PRIORITY = [
|
|
652
|
+
'application/vnd.jupyter.widget-view+json',
|
|
653
|
+
'application/javascript',
|
|
654
|
+
'text/html',
|
|
655
|
+
'image/svg+xml',
|
|
656
|
+
'image/png',
|
|
657
|
+
'image/jpeg',
|
|
658
|
+
'text/markdown',
|
|
659
|
+
'text/latex',
|
|
660
|
+
'text/plain'
|
|
661
|
+
];
|
|
662
|
+
|
|
663
|
+
function extractDisplay(data: any): string {
|
|
664
|
+
for (const mime of DISPLAY_PRIORITY) {
|
|
665
|
+
if (!(mime in data)) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const value = data[mime];
|
|
670
|
+
if (!value) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
switch (mime) {
|
|
675
|
+
case 'application/vnd.jupyter.widget-view+json':
|
|
676
|
+
return `Widget: ${(value as any).model_id ?? 'unknown model'}`;
|
|
677
|
+
|
|
678
|
+
case 'image/png':
|
|
679
|
+
return `}...)`;
|
|
680
|
+
|
|
681
|
+
case 'image/jpeg':
|
|
682
|
+
return `}...)`;
|
|
683
|
+
|
|
684
|
+
case 'image/svg+xml':
|
|
685
|
+
return String(value).slice(0, 500) + '...\n[svg truncated]';
|
|
686
|
+
|
|
687
|
+
case 'text/html':
|
|
688
|
+
return (
|
|
689
|
+
String(value).slice(0, 1000) +
|
|
690
|
+
(String(value).length > 1000 ? '\n...[truncated]' : '')
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
case 'text/markdown':
|
|
694
|
+
case 'text/latex':
|
|
695
|
+
case 'text/plain': {
|
|
696
|
+
let text = Array.isArray(value)
|
|
697
|
+
? value.join('')
|
|
698
|
+
: String(value);
|
|
699
|
+
if (text.length > 2000) {
|
|
700
|
+
text = text.slice(0, 2000) + '\n...[truncated]';
|
|
701
|
+
}
|
|
702
|
+
return text;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
default:
|
|
706
|
+
return JSON.stringify(value).slice(0, 2000);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return JSON.stringify(data).slice(0, 2000);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
let outputs = '';
|
|
714
|
+
if (cellType === 'code' && Array.isArray(cell.outputs)) {
|
|
715
|
+
outputs = cell.outputs
|
|
716
|
+
.map((output: nbformat.IOutput) => {
|
|
717
|
+
if (output.output_type === 'stream') {
|
|
718
|
+
return (output as nbformat.IStream).text;
|
|
719
|
+
} else if (output.output_type === 'error') {
|
|
720
|
+
const err = output as nbformat.IError;
|
|
721
|
+
return `${err.ename}: ${err.evalue}\n${(err.traceback || []).join('\n')}`;
|
|
722
|
+
} else if (
|
|
723
|
+
output.output_type === 'execute_result' ||
|
|
724
|
+
output.output_type === 'display_data'
|
|
725
|
+
) {
|
|
726
|
+
const data = (output as nbformat.IDisplayData).data;
|
|
727
|
+
if (!data) {
|
|
728
|
+
return '';
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
return extractDisplay(data);
|
|
732
|
+
} catch (e) {
|
|
733
|
+
console.error('Cannot extract cell output', e);
|
|
734
|
+
return '';
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return '';
|
|
738
|
+
})
|
|
739
|
+
.filter(Boolean)
|
|
740
|
+
.join('\n---\n');
|
|
741
|
+
|
|
742
|
+
if (outputs.length > 2000) {
|
|
743
|
+
outputs = outputs.slice(0, 2000) + '\n...[truncated]';
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
`**Cell [${cellInfo.id}] (${cellType}):**\n` +
|
|
749
|
+
`\`\`\`${lang}\n${code}\n\`\`\`` +
|
|
750
|
+
(outputs ? `\n**Outputs:**\n\`\`\`text\n${outputs}\n\`\`\`` : '')
|
|
751
|
+
);
|
|
650
752
|
})
|
|
651
753
|
.filter(Boolean)
|
|
652
754
|
.join('\n\n');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { AISettingsModel } from '../models/settings-model';
|
|
3
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
4
|
+
import { jupyternautIcon } from '../icons';
|
|
5
|
+
|
|
6
|
+
const COMPLETION_STATUS_CLASS = 'jp-ai-completion-status';
|
|
7
|
+
const COMPLETION_DISABLED_CLASS = 'jp-ai-completion-disabled';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The completion status props.
|
|
11
|
+
*/
|
|
12
|
+
interface ICompletionStatusProps {
|
|
13
|
+
/**
|
|
14
|
+
* The settings model.
|
|
15
|
+
*/
|
|
16
|
+
settingsModel: AISettingsModel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The completion status component.
|
|
21
|
+
*/
|
|
22
|
+
function CompletionStatus(props: ICompletionStatusProps): JSX.Element {
|
|
23
|
+
const [disabled, setDisabled] = useState<boolean>(true);
|
|
24
|
+
const [title, setTitle] = useState<string>('');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle changes in the settings.
|
|
28
|
+
*/
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const stateChanged = (model: AISettingsModel) => {
|
|
31
|
+
if (model.config.useSameProviderForChatAndCompleter) {
|
|
32
|
+
setDisabled(false);
|
|
33
|
+
setTitle(`Completion using ${model.getDefaultProvider()?.model}`);
|
|
34
|
+
} else if (model.config.activeCompleterProvider) {
|
|
35
|
+
setDisabled(false);
|
|
36
|
+
setTitle(
|
|
37
|
+
`Completion using ${model.getProvider(model.config.activeCompleterProvider)?.model}`
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
setDisabled(true);
|
|
41
|
+
setTitle('No completion');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
props.settingsModel.stateChanged.connect(stateChanged);
|
|
46
|
+
|
|
47
|
+
stateChanged(props.settingsModel);
|
|
48
|
+
return () => {
|
|
49
|
+
props.settingsModel.stateChanged.disconnect(stateChanged);
|
|
50
|
+
};
|
|
51
|
+
}, [props.settingsModel]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<jupyternautIcon.react
|
|
55
|
+
className={disabled ? COMPLETION_DISABLED_CLASS : ''}
|
|
56
|
+
top={'2px'}
|
|
57
|
+
width={'16px'}
|
|
58
|
+
stylesheet={'statusBar'}
|
|
59
|
+
title={title}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The completion status widget that will be added to the status bar.
|
|
66
|
+
*/
|
|
67
|
+
export class CompletionStatusWidget extends ReactWidget {
|
|
68
|
+
constructor(options: ICompletionStatusProps) {
|
|
69
|
+
super();
|
|
70
|
+
this.addClass(COMPLETION_STATUS_CLASS);
|
|
71
|
+
this._props = options;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render(): JSX.Element {
|
|
75
|
+
return <CompletionStatus {...this._props} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private _props: ICompletionStatusProps;
|
|
79
|
+
}
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { IKernelSpecManager, KernelSpec } from '@jupyterlab/services';
|
|
|
36
36
|
|
|
37
37
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
38
38
|
|
|
39
|
+
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
40
|
+
|
|
39
41
|
import {
|
|
40
42
|
settingsIcon,
|
|
41
43
|
Toolbar,
|
|
@@ -72,7 +74,6 @@ import {
|
|
|
72
74
|
googleProvider,
|
|
73
75
|
mistralProvider,
|
|
74
76
|
openaiProvider,
|
|
75
|
-
ollamaProvider,
|
|
76
77
|
genericProvider
|
|
77
78
|
} from './providers/built-in-providers';
|
|
78
79
|
|
|
@@ -83,6 +84,7 @@ import {
|
|
|
83
84
|
createModelSelectItem,
|
|
84
85
|
createToolSelectItem,
|
|
85
86
|
stopItem,
|
|
87
|
+
CompletionStatusWidget,
|
|
86
88
|
TokenUsageWidget
|
|
87
89
|
} from './components';
|
|
88
90
|
|
|
@@ -189,19 +191,6 @@ const openaiProviderPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
189
191
|
}
|
|
190
192
|
};
|
|
191
193
|
|
|
192
|
-
/**
|
|
193
|
-
* Ollama provider plugin
|
|
194
|
-
*/
|
|
195
|
-
const ollamaProviderPlugin: JupyterFrontEndPlugin<void> = {
|
|
196
|
-
id: '@jupyterlite/ai:ollama-provider',
|
|
197
|
-
description: 'Register Ollama provider',
|
|
198
|
-
autoStart: true,
|
|
199
|
-
requires: [IProviderRegistry],
|
|
200
|
-
activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
|
|
201
|
-
providerRegistry.registerProvider(ollamaProvider);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
194
|
/**
|
|
206
195
|
* Generic provider plugin
|
|
207
196
|
*/
|
|
@@ -516,6 +505,11 @@ function registerCommands(
|
|
|
516
505
|
execute: async (args): Promise<boolean> => {
|
|
517
506
|
const area = (args.area as string) === 'main' ? 'main' : 'side';
|
|
518
507
|
const provider = (args.provider as string) ?? undefined;
|
|
508
|
+
|
|
509
|
+
// Do not open the chat if the provider in args does not exists in settings.
|
|
510
|
+
if (provider && !settingsModel.getProvider(provider)) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
519
513
|
const model = modelRegistry.createModel(
|
|
520
514
|
args.name ? (args.name as string) : undefined,
|
|
521
515
|
provider
|
|
@@ -698,6 +692,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
|
698
692
|
});
|
|
699
693
|
settingsWidget.id = 'jupyterlite-ai-settings';
|
|
700
694
|
settingsWidget.title.icon = settingsIcon;
|
|
695
|
+
settingsWidget.title.iconClass = 'jp-ai-settings-icon';
|
|
701
696
|
|
|
702
697
|
// Build the completion provider
|
|
703
698
|
if (completionManager) {
|
|
@@ -723,6 +718,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
|
|
|
723
718
|
label: 'AI Settings',
|
|
724
719
|
caption: 'Configure AI providers and behavior',
|
|
725
720
|
icon: settingsIcon,
|
|
721
|
+
iconClass: 'jp-ai-settings-icon',
|
|
726
722
|
execute: () => {
|
|
727
723
|
// Check if the widget already exists in shell
|
|
728
724
|
let widget = Array.from(app.shell.widgets('main')).find(
|
|
@@ -930,13 +926,35 @@ const inputToolbarFactory: JupyterFrontEndPlugin<IInputToolbarRegistryFactory> =
|
|
|
930
926
|
}
|
|
931
927
|
};
|
|
932
928
|
|
|
929
|
+
const completionStatus: JupyterFrontEndPlugin<void> = {
|
|
930
|
+
id: '@jupyterlite/ai:completion-status',
|
|
931
|
+
description: 'The completion status displayed in the status bar',
|
|
932
|
+
autoStart: true,
|
|
933
|
+
requires: [IAISettingsModel],
|
|
934
|
+
optional: [IStatusBar],
|
|
935
|
+
activate: (
|
|
936
|
+
app: JupyterFrontEnd,
|
|
937
|
+
settingsModel: AISettingsModel,
|
|
938
|
+
statusBar: IStatusBar | null
|
|
939
|
+
) => {
|
|
940
|
+
if (!statusBar) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const item = new CompletionStatusWidget({ settingsModel });
|
|
944
|
+
statusBar?.registerStatusItem('completionState', {
|
|
945
|
+
item,
|
|
946
|
+
align: 'right',
|
|
947
|
+
rank: 10
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
933
952
|
export default [
|
|
934
953
|
providerRegistryPlugin,
|
|
935
954
|
anthropicProviderPlugin,
|
|
936
955
|
googleProviderPlugin,
|
|
937
956
|
mistralProviderPlugin,
|
|
938
957
|
openaiProviderPlugin,
|
|
939
|
-
ollamaProviderPlugin,
|
|
940
958
|
genericProviderPlugin,
|
|
941
959
|
settingsModel,
|
|
942
960
|
diffManager,
|
|
@@ -944,7 +962,8 @@ export default [
|
|
|
944
962
|
plugin,
|
|
945
963
|
toolRegistry,
|
|
946
964
|
agentManagerFactory,
|
|
947
|
-
inputToolbarFactory
|
|
965
|
+
inputToolbarFactory,
|
|
966
|
+
completionStatus
|
|
948
967
|
];
|
|
949
968
|
|
|
950
969
|
// Export extension points for other extensions to use
|
|
@@ -241,7 +241,7 @@ Rules:
|
|
|
241
241
|
}
|
|
242
242
|
return this._config.activeCompleterProvider
|
|
243
243
|
? this.getProvider(this._config.activeCompleterProvider)
|
|
244
|
-
:
|
|
244
|
+
: undefined;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
async addProvider(
|
|
@@ -311,6 +311,11 @@ Rules:
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
Object.assign(provider, updates);
|
|
314
|
+
Object.keys(provider).forEach(key => {
|
|
315
|
+
if (key !== 'id' && updates[key] === undefined) {
|
|
316
|
+
delete provider[key];
|
|
317
|
+
}
|
|
318
|
+
});
|
|
314
319
|
await this.saveSetting('providers', this._config.providers);
|
|
315
320
|
}
|
|
316
321
|
|
|
@@ -416,6 +421,8 @@ Rules:
|
|
|
416
421
|
// Only save the specific setting that changed
|
|
417
422
|
if (value !== undefined) {
|
|
418
423
|
await this._settings.set(key, value as any);
|
|
424
|
+
} else {
|
|
425
|
+
await this._settings.remove(key);
|
|
419
426
|
}
|
|
420
427
|
}
|
|
421
428
|
} catch (error) {
|
|
@@ -2,7 +2,7 @@ import { createAnthropic } from '@ai-sdk/anthropic';
|
|
|
2
2
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
3
3
|
import { createMistral } from '@ai-sdk/mistral';
|
|
4
4
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
|
-
import {
|
|
5
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
6
|
|
|
7
7
|
import type { IProviderInfo } from '../tokens';
|
|
8
8
|
import type { IModelOptions } from './models';
|
|
@@ -202,26 +202,6 @@ export const openaiProvider: IProviderInfo = {
|
|
|
202
202
|
}
|
|
203
203
|
};
|
|
204
204
|
|
|
205
|
-
/**
|
|
206
|
-
* Ollama provider
|
|
207
|
-
*/
|
|
208
|
-
export const ollamaProvider: IProviderInfo = {
|
|
209
|
-
id: 'ollama',
|
|
210
|
-
name: 'Ollama',
|
|
211
|
-
apiKeyRequirement: 'none',
|
|
212
|
-
defaultModels: [],
|
|
213
|
-
supportsBaseURL: true,
|
|
214
|
-
supportsHeaders: true,
|
|
215
|
-
factory: (options: IModelOptions) => {
|
|
216
|
-
const ollama = createOllama({
|
|
217
|
-
baseURL: options.baseURL || 'http://localhost:11434/api',
|
|
218
|
-
...(options.headers && { headers: options.headers })
|
|
219
|
-
});
|
|
220
|
-
const modelName = options.model || 'phi3';
|
|
221
|
-
return ollama(modelName);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
205
|
/**
|
|
226
206
|
* Generic OpenAI-compatible provider
|
|
227
207
|
*/
|
|
@@ -234,13 +214,24 @@ export const genericProvider: IProviderInfo = {
|
|
|
234
214
|
supportsHeaders: true,
|
|
235
215
|
supportsToolCalling: true,
|
|
236
216
|
description: 'Uses /chat/completions endpoint',
|
|
217
|
+
baseUrls: [
|
|
218
|
+
{
|
|
219
|
+
url: 'http://localhost:4000',
|
|
220
|
+
description: 'Default for local LiteLLM server'
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
url: 'http://localhost:11434/v1',
|
|
224
|
+
description: 'Default for local Ollama server'
|
|
225
|
+
}
|
|
226
|
+
],
|
|
237
227
|
factory: (options: IModelOptions) => {
|
|
238
|
-
const
|
|
228
|
+
const openaiCompatible = createOpenAICompatible({
|
|
229
|
+
name: options.provider,
|
|
239
230
|
apiKey: options.apiKey || 'dummy',
|
|
240
|
-
|
|
231
|
+
baseURL: options.baseURL ?? '',
|
|
241
232
|
...(options.headers && { headers: options.headers })
|
|
242
233
|
});
|
|
243
234
|
const modelName = options.model || 'gpt-4o';
|
|
244
|
-
return
|
|
235
|
+
return openaiCompatible(modelName);
|
|
245
236
|
}
|
|
246
237
|
};
|
package/src/tokens.ts
CHANGED
|
@@ -527,6 +527,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
|
|
|
527
527
|
overflow: 'auto',
|
|
528
528
|
p: 2,
|
|
529
529
|
pb: 4,
|
|
530
|
+
boxSizing: 'border-box',
|
|
530
531
|
fontSize: '0.9rem'
|
|
531
532
|
}}
|
|
532
533
|
>
|
|
@@ -600,6 +601,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
|
|
|
600
601
|
<Select
|
|
601
602
|
value={config.activeCompleterProvider || ''}
|
|
602
603
|
label="Completion Provider"
|
|
604
|
+
className="jp-ai-completion-provider-select"
|
|
603
605
|
onChange={e =>
|
|
604
606
|
model.setActiveCompleterProvider(
|
|
605
607
|
e.target.value || undefined
|
|
@@ -607,7 +609,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
|
|
|
607
609
|
}
|
|
608
610
|
>
|
|
609
611
|
<MenuItem value="">
|
|
610
|
-
<em>
|
|
612
|
+
<em>No completion</em>
|
|
611
613
|
</MenuItem>
|
|
612
614
|
{config.providers.map(provider => (
|
|
613
615
|
<MenuItem key={provider.id} value={provider.id}>
|
|
@@ -793,9 +795,9 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
|
|
|
793
795
|
label={
|
|
794
796
|
<div>
|
|
795
797
|
<span>Use the secrets manager to manage API keys</span>
|
|
796
|
-
{config.useSecretsManager && (
|
|
798
|
+
{!config.useSecretsManager && (
|
|
797
799
|
<Alert severity="warning" icon={<Error />} sx={{ mb: 2 }}>
|
|
798
|
-
The secrets
|
|
800
|
+
The secrets are stored in plain text in settings
|
|
799
801
|
</Alert>
|
|
800
802
|
)}
|
|
801
803
|
</div>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Accordion,
|
|
6
6
|
AccordionDetails,
|
|
7
7
|
AccordionSummary,
|
|
8
|
+
Autocomplete,
|
|
8
9
|
Box,
|
|
9
10
|
Button,
|
|
10
11
|
Chip,
|
|
@@ -83,9 +84,10 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
|
|
|
83
84
|
label: info.name,
|
|
84
85
|
models: info.defaultModels,
|
|
85
86
|
apiKeyRequirement: info.apiKeyRequirement,
|
|
86
|
-
allowCustomModel: id === '
|
|
87
|
+
allowCustomModel: id === 'generic', // Generic allows custom models
|
|
87
88
|
supportsBaseURL: info.supportsBaseURL,
|
|
88
|
-
description: info.description
|
|
89
|
+
description: info.description,
|
|
90
|
+
baseUrls: info.baseUrls
|
|
89
91
|
};
|
|
90
92
|
});
|
|
91
93
|
}, [providerRegistry]);
|
|
@@ -279,21 +281,46 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
|
|
|
279
281
|
)}
|
|
280
282
|
|
|
281
283
|
{selectedProvider?.supportsBaseURL && (
|
|
282
|
-
<
|
|
284
|
+
<Autocomplete
|
|
285
|
+
freeSolo
|
|
283
286
|
fullWidth
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
287
|
+
options={(selectedProvider.baseUrls ?? []).map(
|
|
288
|
+
option => option.url
|
|
289
|
+
)}
|
|
290
|
+
value={baseURL || ''}
|
|
291
|
+
onChange={(_, value) => {
|
|
292
|
+
if (value && typeof value === 'string') {
|
|
293
|
+
setBaseURL(value);
|
|
294
|
+
}
|
|
295
|
+
}}
|
|
296
|
+
inputValue={baseURL || ''}
|
|
297
|
+
renderOption={(props, option) => {
|
|
298
|
+
const urlOption = (selectedProvider.baseUrls ?? []).find(
|
|
299
|
+
u => u.url === option
|
|
300
|
+
);
|
|
301
|
+
return (
|
|
302
|
+
<Box component="li" {...props} key={option}>
|
|
303
|
+
<Box>
|
|
304
|
+
<Typography variant="body2">{option}</Typography>
|
|
305
|
+
{urlOption?.description && (
|
|
306
|
+
<Typography variant="caption" color="text.secondary">
|
|
307
|
+
{urlOption.description}
|
|
308
|
+
</Typography>
|
|
309
|
+
)}
|
|
310
|
+
</Box>
|
|
311
|
+
</Box>
|
|
312
|
+
);
|
|
313
|
+
}}
|
|
314
|
+
renderInput={params => (
|
|
315
|
+
<TextField
|
|
316
|
+
{...params}
|
|
317
|
+
fullWidth
|
|
318
|
+
label="Base URL"
|
|
319
|
+
placeholder="https://api.example.com/v1"
|
|
320
|
+
onChange={e => setBaseURL(e.target.value)}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
clearOnBlur={false}
|
|
297
324
|
/>
|
|
298
325
|
)}
|
|
299
326
|
|
package/style/base.css
CHANGED
|
@@ -371,6 +371,11 @@
|
|
|
371
371
|
transform: rotate(180deg);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
.jp-ai-settings-icon {
|
|
375
|
+
align-items: center;
|
|
376
|
+
display: flex;
|
|
377
|
+
}
|
|
378
|
+
|
|
374
379
|
.jp-chat-sidepanel .jp-chat-add span.jp-ToolbarButtonComponent-label {
|
|
375
380
|
display: none;
|
|
376
381
|
}
|
|
@@ -379,3 +384,12 @@
|
|
|
379
384
|
stroke: var(--jp-inverse-layout-color3);
|
|
380
385
|
stroke-width: 2;
|
|
381
386
|
}
|
|
387
|
+
|
|
388
|
+
/* Disabled color for the completion status */
|
|
389
|
+
.jp-ai-completion-status .jp-ai-completion-disabled circle {
|
|
390
|
+
fill: var(--jp-layout-color3);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.jp-ai-completion-status .jp-ai-completion-disabled path {
|
|
394
|
+
fill: var(--jp-layout-color2);
|
|
395
|
+
}
|