@jupyterlite/ai 0.13.0 → 0.14.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.
- package/lib/agent.d.ts +12 -0
- package/lib/agent.js +88 -0
- package/lib/index.js +125 -55
- package/lib/providers/built-in-providers.js +26 -32
- package/lib/tokens.d.ts +21 -0
- package/lib/tools/commands.js +58 -20
- package/lib/widgets/ai-settings.d.ts +4 -9
- package/lib/widgets/ai-settings.js +15 -48
- package/lib/widgets/provider-config-dialog.js +5 -8
- package/package.json +8 -8
- package/src/agent.ts +107 -0
- package/src/index.ts +210 -106
- package/src/providers/built-in-providers.ts +26 -32
- package/src/tokens.ts +29 -0
- package/src/tools/commands.ts +95 -29
- package/src/widgets/ai-settings.tsx +18 -68
- package/src/widgets/provider-config-dialog.tsx +9 -9
|
@@ -13,7 +13,7 @@ import MoreVert from '@mui/icons-material/MoreVert';
|
|
|
13
13
|
import Settings from '@mui/icons-material/Settings';
|
|
14
14
|
import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem, ListItemText, Menu, MenuItem, Select, Switch, Tab, Tabs, TextField, ThemeProvider, Tooltip, Typography, createTheme } from '@mui/material';
|
|
15
15
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
16
|
-
import {
|
|
16
|
+
import { SECRETS_REPLACEMENT } from '../tokens';
|
|
17
17
|
import { ProviderConfigDialog } from './provider-config-dialog';
|
|
18
18
|
/**
|
|
19
19
|
* Create a theme that uses IThemeManager to detect theme
|
|
@@ -41,21 +41,20 @@ export class AISettingsWidget extends ReactWidget {
|
|
|
41
41
|
*/
|
|
42
42
|
constructor(options) {
|
|
43
43
|
super();
|
|
44
|
-
Private.setToken(options.token);
|
|
45
44
|
this._settingsModel = options.settingsModel;
|
|
46
45
|
this._agentManagerFactory = options.agentManagerFactory;
|
|
47
46
|
this._themeManager = options.themeManager;
|
|
48
47
|
this._providerRegistry = options.providerRegistry;
|
|
49
|
-
this.
|
|
48
|
+
this._secretsAccess = options.secretsAccess;
|
|
50
49
|
this._trans = options.trans;
|
|
51
50
|
this.id = 'jupyterlite-ai-settings';
|
|
52
51
|
this.title.label = this._trans.__('AI Settings');
|
|
53
52
|
this.title.caption = this._trans.__('Configure AI providers and behavior');
|
|
54
53
|
this.title.closable = true;
|
|
55
54
|
// Disable the secrets manager if the token is empty.
|
|
56
|
-
if (!options.
|
|
55
|
+
if (!options.secretsAccess.isAvailable) {
|
|
57
56
|
this._settingsModel.updateConfig({ useSecretsManager: false });
|
|
58
|
-
this.
|
|
57
|
+
this._secretsAccess = undefined;
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
/**
|
|
@@ -63,13 +62,13 @@ export class AISettingsWidget extends ReactWidget {
|
|
|
63
62
|
* @returns A React element containing the AI settings interface
|
|
64
63
|
*/
|
|
65
64
|
render() {
|
|
66
|
-
return (React.createElement(AISettingsComponent, { model: this._settingsModel, agentManagerFactory: this._agentManagerFactory, themeManager: this._themeManager, providerRegistry: this._providerRegistry,
|
|
65
|
+
return (React.createElement(AISettingsComponent, { model: this._settingsModel, agentManagerFactory: this._agentManagerFactory, themeManager: this._themeManager, providerRegistry: this._providerRegistry, secretsAccess: this._secretsAccess, trans: this._trans }));
|
|
67
66
|
}
|
|
68
67
|
_settingsModel;
|
|
69
68
|
_agentManagerFactory;
|
|
70
69
|
_themeManager;
|
|
71
70
|
_providerRegistry;
|
|
72
|
-
|
|
71
|
+
_secretsAccess;
|
|
73
72
|
_trans;
|
|
74
73
|
}
|
|
75
74
|
/**
|
|
@@ -77,7 +76,7 @@ export class AISettingsWidget extends ReactWidget {
|
|
|
77
76
|
* @param props - Component props containing models and theme manager
|
|
78
77
|
* @returns A React component for AI settings configuration
|
|
79
78
|
*/
|
|
80
|
-
const AISettingsComponent = ({ model, agentManagerFactory, themeManager, providerRegistry,
|
|
79
|
+
const AISettingsComponent = ({ model, agentManagerFactory, themeManager, providerRegistry, secretsAccess, trans }) => {
|
|
81
80
|
if (!model) {
|
|
82
81
|
return React.createElement("div", null, trans.__('Settings model not available'));
|
|
83
82
|
}
|
|
@@ -174,23 +173,10 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
174
173
|
void promptDebouncer.invoke();
|
|
175
174
|
};
|
|
176
175
|
const getSecretFromManager = async (provider, fieldName) => {
|
|
177
|
-
|
|
178
|
-
if (!token) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const secret = await secretsManager?.get(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`);
|
|
182
|
-
return secret?.value;
|
|
176
|
+
return secretsAccess?.get(`${provider}:${fieldName}`);
|
|
183
177
|
};
|
|
184
178
|
const setSecretToManager = async (provider, fieldName, value) => {
|
|
185
|
-
|
|
186
|
-
if (!token) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
await secretsManager?.set(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, {
|
|
190
|
-
namespace: SECRETS_NAMESPACE,
|
|
191
|
-
id: `${provider}:${fieldName}`,
|
|
192
|
-
value
|
|
193
|
-
});
|
|
179
|
+
await secretsAccess?.set(`${provider}:${fieldName}`, value);
|
|
194
180
|
};
|
|
195
181
|
/**
|
|
196
182
|
* Attach a secrets field to the secrets manager.
|
|
@@ -199,14 +185,10 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
199
185
|
* @param fieldName - the name of the field.
|
|
200
186
|
*/
|
|
201
187
|
const handleSecretField = async (input, provider, fieldName) => {
|
|
202
|
-
if (!(model.config.useSecretsManager &&
|
|
188
|
+
if (!(model.config.useSecretsManager && secretsAccess?.isAvailable)) {
|
|
203
189
|
return;
|
|
204
190
|
}
|
|
205
|
-
|
|
206
|
-
if (!token) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
await secretsManager?.attach(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, input);
|
|
191
|
+
await secretsAccess.attach(`${provider}:${fieldName}`, input);
|
|
210
192
|
};
|
|
211
193
|
/**
|
|
212
194
|
* Handle adding a new AI provider
|
|
@@ -214,7 +196,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
214
196
|
*/
|
|
215
197
|
const handleAddProvider = async (providerConfig) => {
|
|
216
198
|
if (model.config.useSecretsManager &&
|
|
217
|
-
|
|
199
|
+
secretsAccess?.isAvailable &&
|
|
218
200
|
providerConfig.apiKey) {
|
|
219
201
|
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
220
202
|
}
|
|
@@ -227,7 +209,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
227
209
|
const handleEditProvider = async (providerConfig) => {
|
|
228
210
|
if (editingProvider) {
|
|
229
211
|
if (model.config.useSecretsManager &&
|
|
230
|
-
|
|
212
|
+
secretsAccess?.isAvailable &&
|
|
231
213
|
providerConfig.apiKey) {
|
|
232
214
|
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
233
215
|
}
|
|
@@ -249,7 +231,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
249
231
|
*/
|
|
250
232
|
const openEditDialog = async (provider) => {
|
|
251
233
|
// Retrieve the API key from the secrets manager if necessary.
|
|
252
|
-
if (model.config.useSecretsManager &&
|
|
234
|
+
if (model.config.useSecretsManager && secretsAccess?.isAvailable) {
|
|
253
235
|
provider.apiKey =
|
|
254
236
|
(await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
|
|
255
237
|
}
|
|
@@ -477,7 +459,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
477
459
|
React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
|
|
478
460
|
React.createElement(MoreVert, null)))));
|
|
479
461
|
}))))),
|
|
480
|
-
|
|
462
|
+
secretsAccess?.isAvailable && (React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSecretsManager, onChange: e => handleConfigUpdate({
|
|
481
463
|
useSecretsManager: e.target.checked
|
|
482
464
|
}), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
|
|
483
465
|
React.createElement("span", null, trans.__('Use the secrets manager to manage API keys')),
|
|
@@ -761,18 +743,3 @@ const MCPServerDialog = ({ open, onClose, onSave, initialConfig, mode, trans })
|
|
|
761
743
|
React.createElement(Button, { onClick: onClose }, trans.__('Cancel')),
|
|
762
744
|
React.createElement(Button, { onClick: handleSave, variant: "contained", disabled: !canSave }, mode === 'add' ? trans.__('Add') : trans.__('Save')))));
|
|
763
745
|
};
|
|
764
|
-
var Private;
|
|
765
|
-
(function (Private) {
|
|
766
|
-
/**
|
|
767
|
-
* The token to use with the secrets manager, setter and getter.
|
|
768
|
-
*/
|
|
769
|
-
let secretsToken;
|
|
770
|
-
function setToken(value) {
|
|
771
|
-
secretsToken = value;
|
|
772
|
-
}
|
|
773
|
-
Private.setToken = setToken;
|
|
774
|
-
function getToken() {
|
|
775
|
-
return secretsToken;
|
|
776
|
-
}
|
|
777
|
-
Private.getToken = getToken;
|
|
778
|
-
})(Private || (Private = {}));
|
|
@@ -68,7 +68,6 @@ function sanitizeCustomSettingsForProvider(customSettings, capabilities) {
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mode, providerRegistry, handleSecretField, trans }) => {
|
|
71
|
-
const apiKeyRef = React.useRef();
|
|
72
71
|
const [name, setName] = React.useState(initialConfig?.name || '');
|
|
73
72
|
const [provider, setProvider] = React.useState(initialConfig?.provider || 'anthropic');
|
|
74
73
|
const [model, setModel] = React.useState(initialConfig?.model || '');
|
|
@@ -130,13 +129,11 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
130
129
|
setModel(selectedProvider.models[0]);
|
|
131
130
|
}
|
|
132
131
|
}, [provider, selectedProvider, model]);
|
|
133
|
-
React.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (open && apiKeyRef.current) {
|
|
137
|
-
handleSecretField(apiKeyRef.current, provider, 'apiKey');
|
|
132
|
+
const handleRef = React.useCallback((node) => {
|
|
133
|
+
if (open && node) {
|
|
134
|
+
handleSecretField(node, provider, 'apiKey');
|
|
138
135
|
}
|
|
139
|
-
}, [
|
|
136
|
+
}, [provider, handleSecretField, open]);
|
|
140
137
|
const updateCustomSetting = React.useCallback((section, key, value) => {
|
|
141
138
|
setCustomSettings(prev => {
|
|
142
139
|
const next = { ...prev };
|
|
@@ -275,7 +272,7 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
275
272
|
? trans.__('Code-specialized')
|
|
276
273
|
: trans.__('General purpose'))))))))),
|
|
277
274
|
selectedProvider &&
|
|
278
|
-
selectedProvider?.apiKeyRequirement !== 'none' && (React.createElement(TextField, { fullWidth: true, inputRef:
|
|
275
|
+
selectedProvider?.apiKeyRequirement !== 'none' && (React.createElement(TextField, { fullWidth: true, inputRef: handleRef, label: selectedProvider?.apiKeyRequirement === 'required'
|
|
279
276
|
? trans.__('API Key')
|
|
280
277
|
: trans.__('API Key (Optional)'), type: showApiKey ? 'text' : 'password', value: apiKey, onChange: e => setApiKey(e.target.value), placeholder: trans.__('Enter your API key...'), required: selectedProvider?.apiKeyRequirement === 'required', InputProps: {
|
|
281
278
|
endAdornment: (React.createElement(InputAdornment, { position: "end" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlite/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "AI code completions and chat for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"docs:build": "sed -e 's/\\[@/[/g' -e 's/@/\\@/g' CHANGELOG.md > docs/_changelog_content.md && jupyter book build --html"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
60
|
-
"@ai-sdk/google": "^3.0.
|
|
61
|
-
"@ai-sdk/mcp": "^1.0.
|
|
62
|
-
"@ai-sdk/mistral": "^3.0.
|
|
63
|
-
"@ai-sdk/openai": "^3.0.
|
|
64
|
-
"@ai-sdk/openai-compatible": "^2.0.
|
|
59
|
+
"@ai-sdk/anthropic": "^3.0.53",
|
|
60
|
+
"@ai-sdk/google": "^3.0.36",
|
|
61
|
+
"@ai-sdk/mcp": "^1.0.23",
|
|
62
|
+
"@ai-sdk/mistral": "^3.0.22",
|
|
63
|
+
"@ai-sdk/openai": "^3.0.39",
|
|
64
|
+
"@ai-sdk/openai-compatible": "^2.0.33",
|
|
65
65
|
"@jupyter/chat": "^0.20.0",
|
|
66
66
|
"@jupyterlab/application": "^4.0.0",
|
|
67
67
|
"@jupyterlab/apputils": "^4.5.6",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@lumino/widgets": "^2.7.1",
|
|
87
87
|
"@mui/icons-material": "^7",
|
|
88
88
|
"@mui/material": "^7",
|
|
89
|
-
"ai": "^6.0.
|
|
89
|
+
"ai": "^6.0.108",
|
|
90
90
|
"jupyter-secrets-manager": "^0.5.0",
|
|
91
91
|
"yaml": "^2.8.1",
|
|
92
92
|
"zod": "^4.3.6"
|
package/src/agent.ts
CHANGED
|
@@ -111,6 +111,20 @@ export class AgentManagerFactory {
|
|
|
111
111
|
secretsManager: this._secretsManager
|
|
112
112
|
});
|
|
113
113
|
this._agentManagers.push(agentManager);
|
|
114
|
+
|
|
115
|
+
// New chats can be created before MCP setup finishes.
|
|
116
|
+
// Reinitialize them with connected MCP tools once it does.
|
|
117
|
+
this._initQueue
|
|
118
|
+
.then(() => this.getMCPTools())
|
|
119
|
+
.then(mcpTools => {
|
|
120
|
+
if (Object.keys(mcpTools).length > 0) {
|
|
121
|
+
agentManager.initializeAgent(mcpTools);
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.catch(error =>
|
|
125
|
+
console.warn('Failed to pass MCP tools to new agent:', error)
|
|
126
|
+
);
|
|
127
|
+
|
|
114
128
|
return agentManager;
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -657,6 +671,9 @@ export class AgentManager {
|
|
|
657
671
|
data: { error: error as Error }
|
|
658
672
|
});
|
|
659
673
|
}
|
|
674
|
+
// After an error (including AbortError), sanitize the history
|
|
675
|
+
// to remove any trailing assistant messages without tool results
|
|
676
|
+
this._sanitizeHistory();
|
|
660
677
|
} finally {
|
|
661
678
|
this._controller = null;
|
|
662
679
|
}
|
|
@@ -1191,6 +1208,96 @@ WEB RETRIEVAL POLICY:
|
|
|
1191
1208
|
return `Supported MIME types in this session: ${safeMimeTypes.join(', ')}`;
|
|
1192
1209
|
}
|
|
1193
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Sanitizes history to ensure it's in a valid state in case of abort or error.
|
|
1213
|
+
*/
|
|
1214
|
+
private _sanitizeHistory(): void {
|
|
1215
|
+
if (this._history.length === 0) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const newHistory: ModelMessage[] = [];
|
|
1220
|
+
for (let i = 0; i < this._history.length; i++) {
|
|
1221
|
+
const msg = this._history[i];
|
|
1222
|
+
|
|
1223
|
+
if (msg.role === 'assistant') {
|
|
1224
|
+
const toolCallIds = this._getToolCallIds(msg);
|
|
1225
|
+
if (toolCallIds.length > 0) {
|
|
1226
|
+
// Find if there's a following tool message with results for these calls
|
|
1227
|
+
const nextMsg = this._history[i + 1];
|
|
1228
|
+
if (
|
|
1229
|
+
nextMsg &&
|
|
1230
|
+
nextMsg.role === 'tool' &&
|
|
1231
|
+
this._matchesAllToolCalls(nextMsg, toolCallIds)
|
|
1232
|
+
) {
|
|
1233
|
+
newHistory.push(msg);
|
|
1234
|
+
} else {
|
|
1235
|
+
// Message has unmatched tool calls drop it and everything after it
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
newHistory.push(msg);
|
|
1240
|
+
}
|
|
1241
|
+
} else if (msg.role === 'tool') {
|
|
1242
|
+
// Tool messages are valid if they were preceded by a valid assistant message
|
|
1243
|
+
newHistory.push(msg);
|
|
1244
|
+
} else {
|
|
1245
|
+
newHistory.push(msg);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
this._history = newHistory;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Extracts tool call IDs from a message
|
|
1254
|
+
*/
|
|
1255
|
+
private _getToolCallIds(message: ModelMessage): string[] {
|
|
1256
|
+
const ids: string[] = [];
|
|
1257
|
+
|
|
1258
|
+
// Check content array for tool-call parts
|
|
1259
|
+
if (Array.isArray(message.content)) {
|
|
1260
|
+
for (const part of message.content) {
|
|
1261
|
+
if (
|
|
1262
|
+
typeof part === 'object' &&
|
|
1263
|
+
part !== null &&
|
|
1264
|
+
'type' in part &&
|
|
1265
|
+
part.type === 'tool-call'
|
|
1266
|
+
) {
|
|
1267
|
+
ids.push(part.toolCallId);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return ids;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Checks if a tool message contains results for all specified tool call IDs
|
|
1277
|
+
*/
|
|
1278
|
+
private _matchesAllToolCalls(
|
|
1279
|
+
message: ModelMessage,
|
|
1280
|
+
callIds: string[]
|
|
1281
|
+
): boolean {
|
|
1282
|
+
if (message.role !== 'tool' || !Array.isArray(message.content)) {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const resultIds = new Set<string>();
|
|
1287
|
+
for (const part of message.content) {
|
|
1288
|
+
if (
|
|
1289
|
+
typeof part === 'object' &&
|
|
1290
|
+
part !== null &&
|
|
1291
|
+
'type' in part &&
|
|
1292
|
+
part.type === 'tool-result'
|
|
1293
|
+
) {
|
|
1294
|
+
resultIds.add(part.toolCallId);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return callIds.every(id => resultIds.has(id));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1194
1301
|
// Private attributes
|
|
1195
1302
|
private _settingsModel: AISettingsModel;
|
|
1196
1303
|
private _toolRegistry?: IToolRegistry;
|