Skip to content

Commit 3bb6c5b

Browse files
authored
Dynamic settings for providers (#14)
* Create provider settings at build time and update the settings list dynamically * lint * Bump jupyterlab dependency to 4.4.0-alpha.0 * update requirements in README and add optional dependencies to Jupyter front end applications * lint
1 parent a93cbd9 commit 3bb6c5b

File tree

10 files changed

+2025
-1237
lines changed

10 files changed

+2025
-1237
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ jupyterlite_ai/labextension
1111
# Version file is handled by hatchling
1212
jupyterlite_ai/_version.py
1313

14+
# Settings schema are built
15+
src/_provider-settings
16+
1417
# Created by https://www.gitignore.io/api/python
1518
# Edit at https://www.gitignore.io/?templates=python
1619

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
AI code completions and chat for JupyterLab, Notebook 7 and JupyterLite, powered by MistralAI ✨
77

8-
[a screencast showing the Codestral extension in JupyterLite](https://github.com/jupyterlite/ai/assets/591645/855c4e3e-3a63-4868-8052-5c9909922c21)
8+
[a screencast showing the Jupyterlite AI extension in JupyterLite](https://github.com/jupyterlite/ai/assets/591645/855c4e3e-3a63-4868-8052-5c9909922c21)
99

1010
## Requirements
1111

@@ -14,7 +14,7 @@ AI code completions and chat for JupyterLab, Notebook 7 and JupyterLite, powered
1414
> To enable more AI providers in JupyterLab and Jupyter Notebook, we recommend using the [Jupyter AI](https://github.com/jupyterlab/jupyter-ai) extension directly.
1515
> At the moment Jupyter AI is not compatible with JupyterLite, but might be to some extent in the future.
1616
17-
- JupyterLab >= 4.1.0 or Notebook >= 7.1.0
17+
- JupyterLab >= 4.4.0a0 or Notebook >= 7.4.0a0
1818

1919
> [!WARNING]
2020
> This extension is still very much experimental. It is not an official MistralAI extension.
@@ -37,13 +37,20 @@ To install the extension, execute:
3737
pip install jupyterlite-ai
3838
```
3939

40+
To install requirements (jupyterlab, jupyterlite and notebook), there is an optional dependencies argument:
41+
42+
```bash
43+
pip install jupyterlite-ai[jupyter]
44+
```
45+
4046
# Usage
4147

4248
1. Go to https://console.mistral.ai/api-keys/ and create an API key.
4349

4450
![Screenshot showing how to create an API key](./img/1-api-key.png)
4551

46-
2. Open the JupyterLab settings and go to the Codestral section to enter the API key
52+
2. Open the JupyterLab settings and go to the **Ai providers** section to select the provider
53+
(`mistral` is only supported one currently) and the API key (required).
4754

4855
![Screenshot showing how to add the API key to the settings](./img/2-jupyterlab-settings.png)
4956

package.json

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
"url": "https://github.com/jupyterlite/ai.git"
2828
},
2929
"scripts": {
30-
"build": "jlpm build:lib && jlpm build:labextension:dev",
31-
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
30+
"build": "node ./scripts/settings-generator.js && jlpm build:lib && jlpm build:labextension:dev",
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",
3233
"build:labextension": "jupyter labextension build .",
3334
"build:labextension:dev": "jupyter labextension build --development True .",
3435
"build:lib": "tsc --sourceMap",
@@ -53,13 +54,13 @@
5354
"watch:labextension": "jupyter labextension watch ."
5455
},
5556
"dependencies": {
56-
"@jupyter/chat": "^0.5.0",
57-
"@jupyterlab/application": "^4.2.0",
58-
"@jupyterlab/apputils": "^4.3.0",
59-
"@jupyterlab/completer": "^4.2.0",
60-
"@jupyterlab/notebook": "^4.2.0",
61-
"@jupyterlab/rendermime": "^4.2.0",
62-
"@jupyterlab/settingregistry": "^4.2.0",
57+
"@jupyter/chat": "^0.7.1",
58+
"@jupyterlab/application": "^4.4.0-alpha.0",
59+
"@jupyterlab/apputils": "^4.5.0-alpha.0",
60+
"@jupyterlab/completer": "^4.4.0-alpha.0",
61+
"@jupyterlab/notebook": "^4.4.0-alpha.0",
62+
"@jupyterlab/rendermime": "^4.4.0-alpha.0",
63+
"@jupyterlab/settingregistry": "^4.4.0-alpha.0",
6364
"@langchain/core": "^0.3.13",
6465
"@langchain/mistralai": "^0.1.1",
6566
"@lumino/coreutils": "^2.1.2",
@@ -87,7 +88,8 @@
8788
"stylelint-config-standard": "^34.0.0",
8889
"stylelint-csstree-validator": "^3.0.0",
8990
"stylelint-prettier": "^4.0.0",
90-
"typescript": "~5.0.2",
91+
"ts-json-schema-generator": "^2.3.0",
92+
"typescript": "~5.1.6",
9193
"yjs": "^13.5.0"
9294
},
9395
"sideEffects": [

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ dependencies = [
2626
]
2727
dynamic = ["version", "description", "authors", "urls", "keywords"]
2828

29+
[project.optional-dependencies]
30+
jupyter = [
31+
"jupyterlab>=4.4.0a0",
32+
"jupyterlite>=0.6.0a0",
33+
"notebook>=7.4.0a0"
34+
]
35+
2936
[tool.hatch.version]
3037
source = "nodejs"
3138

schema/ai-provider.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@
99
"description": "The AI provider to use for chat and completion",
1010
"default": "None",
1111
"enum": ["None", "MistralAI"]
12-
},
13-
"apiKey": {
14-
"type": "string",
15-
"title": "The Codestral API key",
16-
"description": "The API key to use for Codestral",
17-
"default": ""
1812
}
1913
},
20-
"additionalProperties": false
14+
"additionalProperties": true
2115
}

scripts/settings-generator.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const fs = require('fs');
2+
const tsj = require('ts-json-schema-generator');
3+
const path = require('path');
4+
5+
console.log('Building settings schema\n');
6+
7+
const outputDir = 'src/_provider-settings';
8+
if (!fs.existsSync(outputDir)) {
9+
fs.mkdirSync(outputDir);
10+
}
11+
12+
// Build the langchain BaseLanguageModelParams object
13+
const configBase = {
14+
path: 'node_modules/@langchain/core/dist/language_models/base.d.ts',
15+
tsconfig: './tsconfig.json',
16+
type: 'BaseLanguageModelParams'
17+
};
18+
19+
const schemaBase = tsj
20+
.createGenerator(configBase)
21+
.createSchema(configBase.type);
22+
23+
const providers = {
24+
mistralAI: {
25+
path: 'node_modules/@langchain/mistralai/dist/chat_models.d.ts',
26+
type: 'ChatMistralAIInput'
27+
}
28+
};
29+
30+
Object.entries(providers).forEach(([name, desc], index) => {
31+
const config = {
32+
path: desc.path,
33+
tsconfig: './tsconfig.json',
34+
type: desc.type
35+
};
36+
37+
const outputPath = path.join(outputDir, `${name}.json`);
38+
39+
const schema = tsj.createGenerator(config).createSchema(config.type);
40+
41+
if (!schema.definitions) {
42+
return;
43+
}
44+
45+
// Remove the properties from extended class.
46+
const providerKeys = Object.keys(schema.definitions[desc.type]['properties']);
47+
Object.keys(
48+
schemaBase.definitions?.['BaseLanguageModelParams']['properties']
49+
).forEach(key => {
50+
if (providerKeys.includes(key)) {
51+
delete schema.definitions?.[desc.type]['properties'][key];
52+
}
53+
});
54+
55+
// Remove the useless definitions.
56+
let change = true;
57+
while (change) {
58+
change = false;
59+
const temporarySchemaString = JSON.stringify(schema);
60+
61+
Object.keys(schema.definitions).forEach(key => {
62+
const index = temporarySchemaString.indexOf(`#/definitions/${key}`);
63+
if (index === -1) {
64+
delete schema.definitions?.[key];
65+
change = true;
66+
}
67+
});
68+
}
69+
70+
// Transform the default values.
71+
Object.values(schema.definitions[desc.type]['properties']).forEach(value => {
72+
const defaultValue = value.default;
73+
if (!defaultValue) {
74+
return;
75+
}
76+
if (value.type === 'number') {
77+
value.default = Number(/{(.*)}/.exec(value.default)?.[1] ?? 0);
78+
} else if (value.type === 'boolean') {
79+
value.default = /{(.*)}/.exec(value.default)?.[1] === 'true';
80+
} else if (value.type === 'string') {
81+
value.default = /{\"(.*)\"}/.exec(value.default)?.[1] ?? '';
82+
}
83+
});
84+
85+
// Write JSON file.
86+
const schemaString = JSON.stringify(schema, null, 2);
87+
fs.writeFile(outputPath, schemaString, err => {
88+
if (err) {
89+
throw err;
90+
}
91+
});
92+
});
93+
94+
console.log('Settings schema built\n');
95+
console.log('=====================\n');

src/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
1515
import { ISettingRegistry } from '@jupyterlab/settingregistry';
1616

1717
import { ChatHandler } from './chat-handler';
18+
import { getSettings } from './llm-models';
1819
import { AIProvider } from './provider';
1920
import { IAIProvider } from './token';
2021

@@ -79,7 +80,7 @@ const chatPlugin: JupyterFrontEndPlugin<void> = {
7980
themeManager,
8081
rmRegistry
8182
});
82-
chatWidget.title.caption = 'Codestral Chat';
83+
chatWidget.title.caption = 'Jupyterlite AI Chat';
8384
} catch (e) {
8485
chatWidget = buildErrorWidget(themeManager);
8586
}
@@ -105,11 +106,34 @@ const aiProviderPlugin: JupyterFrontEndPlugin<IAIProvider> = {
105106
requestCompletion: () => app.commands.execute('inline-completer:invoke')
106107
});
107108

109+
let currentProvider = 'None';
108110
settingRegistry
109111
.load(aiProviderPlugin.id)
110112
.then(settings => {
111113
const updateProvider = () => {
112114
const provider = settings.get('provider').composite as string;
115+
if (provider !== currentProvider) {
116+
// Update the settings panel.
117+
currentProvider = provider;
118+
const settingsProperties = settings.schema.properties;
119+
if (settingsProperties) {
120+
const schemaKeys = Object.keys(settingsProperties);
121+
schemaKeys.forEach(key => {
122+
if (key !== 'provider') {
123+
delete settings.schema.properties?.[key];
124+
}
125+
});
126+
const properties = getSettings(provider);
127+
if (properties === null) {
128+
return;
129+
}
130+
Object.entries(properties).forEach(([name, value], index) => {
131+
settingsProperties[name] = value as ISettingRegistry.IProperty;
132+
});
133+
}
134+
}
135+
136+
// Update the settings to the AI providers.
113137
aiProvider.setModels(provider, settings.composite);
114138
};
115139

src/llm-models/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
22
import { ChatMistralAI } from '@langchain/mistralai';
3+
import { JSONObject } from '@lumino/coreutils';
4+
35
import { IBaseCompleter } from './base-completer';
46
import { CodestralCompleter } from './codestral-completer';
57
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
68

9+
import mistralAI from '../_provider-settings/mistralAI.json';
10+
711
/**
812
* Get an LLM completer from the name.
913
*/
@@ -39,3 +43,13 @@ export function getErrorMessage(name: string, error: any): string {
3943
}
4044
return 'Unknown provider';
4145
}
46+
47+
/*
48+
* Get an LLM completer from the name.
49+
*/
50+
export function getSettings(name: string): JSONObject | null {
51+
if (name === 'MistralAI') {
52+
return mistralAI.definitions.ChatMistralAIInput.properties;
53+
}
54+
return null;
55+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@
1919
"strictNullChecks": true,
2020
"target": "ES2018"
2121
},
22-
"include": ["src/*", "src/**/*"]
22+
"include": ["src/*", "src/**/*", "src/_provider-settings/*.json"]
2323
}

0 commit comments

Comments
 (0)