@jupyterlite/ai 0.12.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 +36 -2
- package/lib/agent.js +249 -24
- package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
- package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
- package/lib/chat-model.d.ts +8 -0
- package/lib/chat-model.js +156 -8
- package/lib/completion/completion-provider.d.ts +1 -1
- package/lib/completion/completion-provider.js +14 -2
- package/lib/components/model-select.js +4 -4
- package/lib/components/tool-select.d.ts +11 -2
- package/lib/components/tool-select.js +77 -18
- package/lib/index.d.ts +3 -3
- package/lib/index.js +249 -117
- package/lib/models/settings-model.d.ts +2 -0
- package/lib/models/settings-model.js +2 -0
- package/lib/providers/built-in-providers.js +33 -32
- package/lib/providers/provider-tools.d.ts +36 -0
- package/lib/providers/provider-tools.js +93 -0
- package/lib/rendered-message-outputarea.d.ts +24 -0
- package/lib/rendered-message-outputarea.js +48 -0
- package/lib/tokens.d.ts +65 -7
- package/lib/tokens.js +1 -1
- package/lib/tools/commands.js +62 -22
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +4 -9
- package/lib/widgets/ai-settings.js +123 -69
- package/lib/widgets/main-area-chat.d.ts +6 -0
- package/lib/widgets/main-area-chat.js +28 -0
- package/lib/widgets/provider-config-dialog.js +211 -11
- package/package.json +17 -11
- package/schema/settings-model.json +89 -1
- package/src/agent.ts +327 -42
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +223 -14
- package/src/completion/completion-provider.ts +26 -12
- package/src/components/model-select.tsx +4 -5
- package/src/components/tool-select.tsx +110 -7
- package/src/index.ts +359 -184
- package/src/models/settings-model.ts +6 -0
- package/src/providers/built-in-providers.ts +33 -32
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/tokens.ts +82 -9
- package/src/tools/commands.ts +99 -31
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +279 -124
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +504 -11
package/lib/tools/web.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const DEFAULT_MAX_CONTENT_CHARS = 20000;
|
|
4
|
+
const MAX_ALLOWED_CONTENT_CHARS = 100000;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 120000;
|
|
7
|
+
/**
|
|
8
|
+
* Read response body text with a character cap.
|
|
9
|
+
*
|
|
10
|
+
* Stops early once the cap is reached to avoid buffering arbitrarily large
|
|
11
|
+
* payloads in memory.
|
|
12
|
+
*/
|
|
13
|
+
async function readResponseText(response, maxContentChars) {
|
|
14
|
+
if (!response.body) {
|
|
15
|
+
const body = await response.text();
|
|
16
|
+
return {
|
|
17
|
+
content: body.slice(0, maxContentChars),
|
|
18
|
+
isTruncated: body.length > maxContentChars,
|
|
19
|
+
totalChars: body.length,
|
|
20
|
+
totalCharsExact: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const reader = response.body.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let content = '';
|
|
26
|
+
let totalChars = 0;
|
|
27
|
+
let isTruncated = false;
|
|
28
|
+
let done = false;
|
|
29
|
+
while (!done) {
|
|
30
|
+
const readResult = await reader.read();
|
|
31
|
+
done = readResult.done;
|
|
32
|
+
if (done) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
36
|
+
if (!chunk) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
totalChars += chunk.length;
|
|
40
|
+
if (!isTruncated) {
|
|
41
|
+
const remaining = maxContentChars - content.length;
|
|
42
|
+
if (chunk.length <= remaining) {
|
|
43
|
+
content += chunk;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
content += chunk.slice(0, remaining);
|
|
47
|
+
isTruncated = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (isTruncated) {
|
|
51
|
+
await reader.cancel();
|
|
52
|
+
return {
|
|
53
|
+
content,
|
|
54
|
+
isTruncated: true,
|
|
55
|
+
totalChars,
|
|
56
|
+
totalCharsExact: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const tail = decoder.decode();
|
|
61
|
+
if (tail) {
|
|
62
|
+
totalChars += tail.length;
|
|
63
|
+
const remaining = maxContentChars - content.length;
|
|
64
|
+
if (tail.length <= remaining) {
|
|
65
|
+
content += tail;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
content += tail.slice(0, remaining);
|
|
69
|
+
isTruncated = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
content,
|
|
74
|
+
isTruncated,
|
|
75
|
+
totalChars,
|
|
76
|
+
totalCharsExact: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a browser-native URL fetch tool.
|
|
81
|
+
*
|
|
82
|
+
* This is best-effort and subject to normal browser constraints (CORS, CSP,
|
|
83
|
+
* mixed content, bot protections).
|
|
84
|
+
*/
|
|
85
|
+
export function createBrowserFetchTool() {
|
|
86
|
+
return tool({
|
|
87
|
+
title: 'Browser Fetch',
|
|
88
|
+
description: 'Fetch a URL directly from the browser using HTTP GET for exact URL inspection when CORS/access permits.',
|
|
89
|
+
inputSchema: z.object({
|
|
90
|
+
url: z.string().describe('HTTP(S) URL to fetch'),
|
|
91
|
+
maxContentChars: z
|
|
92
|
+
.number()
|
|
93
|
+
.int()
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(MAX_ALLOWED_CONTENT_CHARS)
|
|
96
|
+
.optional()
|
|
97
|
+
.describe(`Maximum number of response characters to return (default: ${DEFAULT_MAX_CONTENT_CHARS})`),
|
|
98
|
+
timeoutMs: z
|
|
99
|
+
.number()
|
|
100
|
+
.int()
|
|
101
|
+
.min(1000)
|
|
102
|
+
.max(MAX_TIMEOUT_MS)
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(`Timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS})`)
|
|
105
|
+
}),
|
|
106
|
+
execute: async (input) => {
|
|
107
|
+
const maxContentChars = Math.min(input.maxContentChars ?? DEFAULT_MAX_CONTENT_CHARS, MAX_ALLOWED_CONTENT_CHARS);
|
|
108
|
+
const timeoutMs = Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
109
|
+
let parsedUrl;
|
|
110
|
+
try {
|
|
111
|
+
parsedUrl = new URL(input.url);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
errorType: 'invalid_url',
|
|
117
|
+
error: 'Invalid URL format',
|
|
118
|
+
url: input.url
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
errorType: 'unsupported_protocol',
|
|
125
|
+
error: 'Only http:// and https:// URLs are supported',
|
|
126
|
+
url: input.url
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(parsedUrl.toString(), {
|
|
133
|
+
method: 'GET',
|
|
134
|
+
credentials: 'omit',
|
|
135
|
+
redirect: 'follow',
|
|
136
|
+
signal: controller.signal,
|
|
137
|
+
headers: {
|
|
138
|
+
Accept: 'text/html,text/plain,application/json,text/markdown,*/*;q=0.8'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const contentType = response.headers.get('content-type') || '';
|
|
142
|
+
const contentLength = response.headers.get('content-length');
|
|
143
|
+
const body = await readResponseText(response, maxContentChars);
|
|
144
|
+
const success = response.ok;
|
|
145
|
+
return {
|
|
146
|
+
success,
|
|
147
|
+
url: response.url,
|
|
148
|
+
requestedUrl: parsedUrl.toString(),
|
|
149
|
+
status: response.status,
|
|
150
|
+
statusText: response.statusText,
|
|
151
|
+
contentType,
|
|
152
|
+
contentLength,
|
|
153
|
+
...(success
|
|
154
|
+
? {}
|
|
155
|
+
: {
|
|
156
|
+
errorType: 'http_error',
|
|
157
|
+
error: `HTTP ${response.status} ${response.statusText}`
|
|
158
|
+
}),
|
|
159
|
+
isTruncated: body.isTruncated,
|
|
160
|
+
returnedChars: body.content.length,
|
|
161
|
+
totalChars: body.totalChars,
|
|
162
|
+
totalCharsExact: body.totalCharsExact,
|
|
163
|
+
content: body.content,
|
|
164
|
+
limitations: 'Browser fetch is subject to CORS, site bot protections, and browser network policy.'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.name === 'AbortError') {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
errorType: 'timeout',
|
|
172
|
+
error: `Request timed out after ${timeoutMs} ms`,
|
|
173
|
+
url: parsedUrl.toString()
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
errorType: 'network_or_cors',
|
|
179
|
+
error: error instanceof Error && error.message
|
|
180
|
+
? error.message
|
|
181
|
+
: 'Fetch failed',
|
|
182
|
+
url: parsedUrl.toString(),
|
|
183
|
+
likelyCauses: [
|
|
184
|
+
'CORS blocked by the target website',
|
|
185
|
+
'DNS/network resolution failure',
|
|
186
|
+
'TLS/certificate issue',
|
|
187
|
+
'Target server rejected browser access'
|
|
188
|
+
]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
clearTimeout(timeoutHandle);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
3
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
4
|
-
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
5
4
|
import React from 'react';
|
|
6
5
|
import { AgentManagerFactory } from '../agent';
|
|
7
6
|
import { AISettingsModel } from '../models/settings-model';
|
|
8
|
-
import { type IProviderRegistry } from '../tokens';
|
|
7
|
+
import { type IAISecretsAccess, type IProviderRegistry } from '../tokens';
|
|
9
8
|
/**
|
|
10
9
|
* A JupyterLab widget for AI settings configuration
|
|
11
10
|
*/
|
|
@@ -24,7 +23,7 @@ export declare class AISettingsWidget extends ReactWidget {
|
|
|
24
23
|
private _agentManagerFactory?;
|
|
25
24
|
private _themeManager?;
|
|
26
25
|
private _providerRegistry;
|
|
27
|
-
private
|
|
26
|
+
private _secretsAccess?;
|
|
28
27
|
private _trans;
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
@@ -40,13 +39,9 @@ export declare namespace AISettingsWidget {
|
|
|
40
39
|
themeManager?: IThemeManager;
|
|
41
40
|
providerRegistry: IProviderRegistry;
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
42
|
+
* Access to provider secrets in the shared namespace.
|
|
44
43
|
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The token used to request the secrets manager.
|
|
48
|
-
*/
|
|
49
|
-
token: symbol;
|
|
44
|
+
secretsAccess: IAISecretsAccess;
|
|
50
45
|
/**
|
|
51
46
|
* The application language translation bundle.
|
|
52
47
|
*/
|
|
@@ -11,9 +11,9 @@ import ErrorOutline from '@mui/icons-material/ErrorOutline';
|
|
|
11
11
|
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
|
12
12
|
import MoreVert from '@mui/icons-material/MoreVert';
|
|
13
13
|
import Settings from '@mui/icons-material/Settings';
|
|
14
|
-
import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem,
|
|
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,30 +41,34 @@ 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;
|
|
54
|
+
// Disable the secrets manager if the token is empty.
|
|
55
|
+
if (!options.secretsAccess.isAvailable) {
|
|
56
|
+
this._settingsModel.updateConfig({ useSecretsManager: false });
|
|
57
|
+
this._secretsAccess = undefined;
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* Render the AI settings component
|
|
58
62
|
* @returns A React element containing the AI settings interface
|
|
59
63
|
*/
|
|
60
64
|
render() {
|
|
61
|
-
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 }));
|
|
62
66
|
}
|
|
63
67
|
_settingsModel;
|
|
64
68
|
_agentManagerFactory;
|
|
65
69
|
_themeManager;
|
|
66
70
|
_providerRegistry;
|
|
67
|
-
|
|
71
|
+
_secretsAccess;
|
|
68
72
|
_trans;
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
@@ -72,7 +76,7 @@ export class AISettingsWidget extends ReactWidget {
|
|
|
72
76
|
* @param props - Component props containing models and theme manager
|
|
73
77
|
* @returns A React component for AI settings configuration
|
|
74
78
|
*/
|
|
75
|
-
const AISettingsComponent = ({ model, agentManagerFactory, themeManager, providerRegistry,
|
|
79
|
+
const AISettingsComponent = ({ model, agentManagerFactory, themeManager, providerRegistry, secretsAccess, trans }) => {
|
|
76
80
|
if (!model) {
|
|
77
81
|
return React.createElement("div", null, trans.__('Settings model not available'));
|
|
78
82
|
}
|
|
@@ -169,15 +173,10 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
169
173
|
void promptDebouncer.invoke();
|
|
170
174
|
};
|
|
171
175
|
const getSecretFromManager = async (provider, fieldName) => {
|
|
172
|
-
|
|
173
|
-
return secret?.value;
|
|
176
|
+
return secretsAccess?.get(`${provider}:${fieldName}`);
|
|
174
177
|
};
|
|
175
178
|
const setSecretToManager = async (provider, fieldName, value) => {
|
|
176
|
-
await
|
|
177
|
-
namespace: SECRETS_NAMESPACE,
|
|
178
|
-
id: `${provider}:${fieldName}`,
|
|
179
|
-
value
|
|
180
|
-
});
|
|
179
|
+
await secretsAccess?.set(`${provider}:${fieldName}`, value);
|
|
181
180
|
};
|
|
182
181
|
/**
|
|
183
182
|
* Attach a secrets field to the secrets manager.
|
|
@@ -186,10 +185,10 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
186
185
|
* @param fieldName - the name of the field.
|
|
187
186
|
*/
|
|
188
187
|
const handleSecretField = async (input, provider, fieldName) => {
|
|
189
|
-
if (!(model.config.useSecretsManager &&
|
|
188
|
+
if (!(model.config.useSecretsManager && secretsAccess?.isAvailable)) {
|
|
190
189
|
return;
|
|
191
190
|
}
|
|
192
|
-
await
|
|
191
|
+
await secretsAccess.attach(`${provider}:${fieldName}`, input);
|
|
193
192
|
};
|
|
194
193
|
/**
|
|
195
194
|
* Handle adding a new AI provider
|
|
@@ -197,7 +196,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
197
196
|
*/
|
|
198
197
|
const handleAddProvider = async (providerConfig) => {
|
|
199
198
|
if (model.config.useSecretsManager &&
|
|
200
|
-
|
|
199
|
+
secretsAccess?.isAvailable &&
|
|
201
200
|
providerConfig.apiKey) {
|
|
202
201
|
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
203
202
|
}
|
|
@@ -210,7 +209,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
210
209
|
const handleEditProvider = async (providerConfig) => {
|
|
211
210
|
if (editingProvider) {
|
|
212
211
|
if (model.config.useSecretsManager &&
|
|
213
|
-
|
|
212
|
+
secretsAccess?.isAvailable &&
|
|
214
213
|
providerConfig.apiKey) {
|
|
215
214
|
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
216
215
|
}
|
|
@@ -232,7 +231,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
232
231
|
*/
|
|
233
232
|
const openEditDialog = async (provider) => {
|
|
234
233
|
// Retrieve the API key from the secrets manager if necessary.
|
|
235
|
-
if (model.config.useSecretsManager &&
|
|
234
|
+
if (model.config.useSecretsManager && secretsAccess?.isAvailable) {
|
|
236
235
|
provider.apiKey =
|
|
237
236
|
(await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
|
|
238
237
|
}
|
|
@@ -271,22 +270,29 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
271
270
|
if (updates.useSecretsManager !== undefined) {
|
|
272
271
|
if (updates.useSecretsManager) {
|
|
273
272
|
for (const provider of model.config.providers) {
|
|
274
|
-
|
|
273
|
+
const settingsApiKey = provider.apiKey;
|
|
274
|
+
// If the secrets manager doesn't have the current API key, set the current
|
|
275
275
|
// one from settings.
|
|
276
|
+
// Update the settings value with SECRETS_REPLACEMENT if a key exist in the
|
|
277
|
+
// secrets manager (was already there or a value was set in settings).
|
|
276
278
|
if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
|
|
277
|
-
|
|
279
|
+
if (settingsApiKey !== undefined) {
|
|
280
|
+
setSecretToManager(provider.provider, 'apiKey', settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : '');
|
|
281
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
282
|
+
await model.updateProvider(provider.id, provider);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
287
|
+
await model.updateProvider(provider.id, provider);
|
|
278
288
|
}
|
|
279
|
-
provider.apiKey = SECRETS_REPLACEMENT;
|
|
280
|
-
await model.updateProvider(provider.id, provider);
|
|
281
289
|
}
|
|
282
290
|
}
|
|
283
291
|
else {
|
|
284
292
|
for (const provider of model.config.providers) {
|
|
285
293
|
const apiKey = await getSecretFromManager(provider.provider, 'apiKey');
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
await model.updateProvider(provider.id, provider);
|
|
289
|
-
}
|
|
294
|
+
provider.apiKey = apiKey;
|
|
295
|
+
await model.updateProvider(provider.id, provider);
|
|
290
296
|
}
|
|
291
297
|
}
|
|
292
298
|
}
|
|
@@ -400,7 +406,13 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
400
406
|
const isActiveCompleter = config.useSameProviderForChatAndCompleter
|
|
401
407
|
? isActive
|
|
402
408
|
: config.activeCompleterProvider === provider.id;
|
|
409
|
+
const providerInfo = providerRegistry.getProviderInfo(provider.provider);
|
|
410
|
+
const providerToolCapabilities = providerInfo?.providerToolCapabilities;
|
|
403
411
|
const params = provider.parameters;
|
|
412
|
+
const webSearchEnabled = !!providerToolCapabilities?.webSearch &&
|
|
413
|
+
provider.customSettings?.webSearch?.enabled === true;
|
|
414
|
+
const webFetchEnabled = !!providerToolCapabilities?.webFetch &&
|
|
415
|
+
provider.customSettings?.webFetch?.enabled === true;
|
|
404
416
|
return (React.createElement(ListItem, { key: provider.id, sx: {
|
|
405
417
|
flexDirection: 'column',
|
|
406
418
|
alignItems: 'stretch',
|
|
@@ -429,22 +441,25 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
429
441
|
provider.model,
|
|
430
442
|
provider.description &&
|
|
431
443
|
` • ${provider.description}`),
|
|
432
|
-
params
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
444
|
+
(params?.temperature !== undefined ||
|
|
445
|
+
params?.maxOutputTokens !== undefined ||
|
|
446
|
+
params?.maxTurns !== undefined ||
|
|
447
|
+
webSearchEnabled ||
|
|
448
|
+
webFetchEnabled) && (React.createElement(Box, { sx: {
|
|
436
449
|
display: 'flex',
|
|
437
450
|
flexWrap: 'wrap',
|
|
438
451
|
gap: 1,
|
|
439
452
|
mt: 1
|
|
440
453
|
} },
|
|
441
|
-
params
|
|
442
|
-
params
|
|
443
|
-
params
|
|
454
|
+
params?.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
|
|
455
|
+
params?.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
|
|
456
|
+
params?.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" })),
|
|
457
|
+
webSearchEnabled && (React.createElement(Chip, { label: trans.__('Web Search'), size: "small", variant: "outlined", color: "info" })),
|
|
458
|
+
webFetchEnabled && (React.createElement(Chip, { label: trans.__('Web Fetch'), size: "small", variant: "outlined", color: "info" }))))),
|
|
444
459
|
React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
|
|
445
460
|
React.createElement(MoreVert, null)))));
|
|
446
461
|
}))))),
|
|
447
|
-
|
|
462
|
+
secretsAccess?.isAvailable && (React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSecretsManager, onChange: e => handleConfigUpdate({
|
|
448
463
|
useSecretsManager: e.target.checked
|
|
449
464
|
}), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
|
|
450
465
|
React.createElement("span", null, trans.__('Use the secrets manager to manage API keys')),
|
|
@@ -523,19 +538,17 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
523
538
|
React.createElement(Box, null,
|
|
524
539
|
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Requiring Approval')),
|
|
525
540
|
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Commands that require user approval before AI can execute them')),
|
|
526
|
-
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}, size: "small" },
|
|
538
|
-
React.createElement(Delete, null))))))),
|
|
541
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
542
|
+
const newCommands = [
|
|
543
|
+
...config.commandsRequiringApproval
|
|
544
|
+
];
|
|
545
|
+
newCommands.splice(index, 1);
|
|
546
|
+
handleConfigUpdate({
|
|
547
|
+
commandsRequiringApproval: newCommands
|
|
548
|
+
});
|
|
549
|
+
}, size: "small" },
|
|
550
|
+
React.createElement(Delete, null)) },
|
|
551
|
+
React.createElement(ListItemText, { primary: command }))))),
|
|
539
552
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Add New Command'), placeholder: trans.__('e.g., notebook:run-cell'), onKeyDown: e => {
|
|
540
553
|
if (e.key === 'Enter') {
|
|
541
554
|
const value = e.target.value.trim();
|
|
@@ -551,7 +564,65 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
551
564
|
e.target.value = '';
|
|
552
565
|
}
|
|
553
566
|
}
|
|
554
|
-
}, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') }))
|
|
567
|
+
}, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') })),
|
|
568
|
+
React.createElement(Divider, { sx: { my: 2 } }),
|
|
569
|
+
React.createElement(Box, null,
|
|
570
|
+
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Auto-Rendering MIME Bundles')),
|
|
571
|
+
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Only these execute_command command IDs can auto-render MIME bundle outputs in chat')),
|
|
572
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.commandsAutoRenderMimeBundles ?? []).map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
573
|
+
const newCommands = [
|
|
574
|
+
...(config.commandsAutoRenderMimeBundles ??
|
|
575
|
+
[])
|
|
576
|
+
];
|
|
577
|
+
newCommands.splice(index, 1);
|
|
578
|
+
handleConfigUpdate({
|
|
579
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
580
|
+
});
|
|
581
|
+
}, size: "small" },
|
|
582
|
+
React.createElement(Delete, null)) },
|
|
583
|
+
React.createElement(ListItemText, { primary: command }))))),
|
|
584
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Add Auto-Render Command'), placeholder: trans.__('e.g., jupyterlab-ai-commands:execute-in-kernel'), onKeyDown: e => {
|
|
585
|
+
if (e.key === 'Enter') {
|
|
586
|
+
const value = e.target.value.trim();
|
|
587
|
+
const existingCommands = config.commandsAutoRenderMimeBundles ?? [];
|
|
588
|
+
if (value && !existingCommands.includes(value)) {
|
|
589
|
+
const newCommands = [...existingCommands, value];
|
|
590
|
+
handleConfigUpdate({
|
|
591
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
592
|
+
});
|
|
593
|
+
e.target.value = '';
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}, helperText: trans.__('Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel') })),
|
|
597
|
+
React.createElement(Divider, { sx: { my: 2 } }),
|
|
598
|
+
React.createElement(Box, null,
|
|
599
|
+
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Trusted MIME Types for Auto-Render')),
|
|
600
|
+
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('When auto-rendering command outputs, these MIME types are marked trusted in chat')),
|
|
601
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.trustedMimeTypesForAutoRender ?? []).map((mimeType, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
602
|
+
const newMimeTypes = [
|
|
603
|
+
...(config.trustedMimeTypesForAutoRender ??
|
|
604
|
+
[])
|
|
605
|
+
];
|
|
606
|
+
newMimeTypes.splice(index, 1);
|
|
607
|
+
handleConfigUpdate({
|
|
608
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
609
|
+
});
|
|
610
|
+
}, size: "small" },
|
|
611
|
+
React.createElement(Delete, null)) },
|
|
612
|
+
React.createElement(ListItemText, { primary: mimeType }))))),
|
|
613
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Add Trusted MIME Type'), placeholder: trans.__('e.g., text/html'), onKeyDown: e => {
|
|
614
|
+
if (e.key === 'Enter') {
|
|
615
|
+
const value = e.target.value.trim();
|
|
616
|
+
const existingMimeTypes = config.trustedMimeTypesForAutoRender ?? [];
|
|
617
|
+
if (value && !existingMimeTypes.includes(value)) {
|
|
618
|
+
const newMimeTypes = [...existingMimeTypes, value];
|
|
619
|
+
handleConfigUpdate({
|
|
620
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
621
|
+
});
|
|
622
|
+
e.target.value = '';
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}, helperText: trans.__('Press Enter to add a MIME type. Default: text/html') })))))),
|
|
555
626
|
activeTab === 2 && (React.createElement(Card, { elevation: 2 },
|
|
556
627
|
React.createElement(CardContent, null,
|
|
557
628
|
React.createElement(Box, { sx: {
|
|
@@ -565,7 +636,8 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
565
636
|
React.createElement(Typography, { variant: "h6", component: "h2" }, trans.__('Remote MCP Servers'))),
|
|
566
637
|
React.createElement(Button, { variant: "contained", startIcon: React.createElement(Add, null), onClick: openAddMCPDialog, size: "small" }, trans.__('Add Server'))),
|
|
567
638
|
React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 } }, trans.__("Configure remote Model Context Protocol (MCP) servers to extend the AI's capabilities with external tools and data sources.")),
|
|
568
|
-
config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true },
|
|
639
|
+
config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true, secondaryAction: React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
|
|
640
|
+
React.createElement(MoreVert, null)) },
|
|
569
641
|
React.createElement(ListItemText, { primary: React.createElement(Box, { sx: {
|
|
570
642
|
display: 'flex',
|
|
571
643
|
alignItems: 'center',
|
|
@@ -582,10 +654,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
582
654
|
React.createElement(Typography, { variant: "body2", color: "text.secondary" }, server.url),
|
|
583
655
|
server.enabled && agentManagerFactory && (React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Status: %1', agentManagerFactory.isMCPServerConnected(server.name)
|
|
584
656
|
? trans.__('Connected')
|
|
585
|
-
: trans.__('Connection failed'))))) }),
|
|
586
|
-
React.createElement(ListItemSecondaryAction, null,
|
|
587
|
-
React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
|
|
588
|
-
React.createElement(MoreVert, null))))))))))),
|
|
657
|
+
: trans.__('Connection failed'))))) }))))))))),
|
|
589
658
|
React.createElement(ProviderConfigDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSave: editingProvider ? handleEditProvider : handleAddProvider, initialConfig: editingProvider, mode: editingProvider ? 'edit' : 'add', providerRegistry: providerRegistry, handleSecretField: handleSecretField, trans: trans }),
|
|
590
659
|
React.createElement(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: handleMenuClose },
|
|
591
660
|
React.createElement(MenuItem, { onClick: () => {
|
|
@@ -674,18 +743,3 @@ const MCPServerDialog = ({ open, onClose, onSave, initialConfig, mode, trans })
|
|
|
674
743
|
React.createElement(Button, { onClick: onClose }, trans.__('Cancel')),
|
|
675
744
|
React.createElement(Button, { onClick: handleSave, variant: "contained", disabled: !canSave }, mode === 'add' ? trans.__('Add') : trans.__('Save')))));
|
|
676
745
|
};
|
|
677
|
-
var Private;
|
|
678
|
-
(function (Private) {
|
|
679
|
-
/**
|
|
680
|
-
* The token to use with the secrets manager, setter and getter.
|
|
681
|
-
*/
|
|
682
|
-
let secretsToken;
|
|
683
|
-
function setToken(value) {
|
|
684
|
-
secretsToken = value;
|
|
685
|
-
}
|
|
686
|
-
Private.setToken = setToken;
|
|
687
|
-
function getToken() {
|
|
688
|
-
return secretsToken;
|
|
689
|
-
}
|
|
690
|
-
Private.getToken = getToken;
|
|
691
|
-
})(Private || (Private = {}));
|
|
@@ -21,5 +21,11 @@ export declare class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
|
21
21
|
* Get the model of the chat.
|
|
22
22
|
*/
|
|
23
23
|
get model(): AIChatModel;
|
|
24
|
+
/**
|
|
25
|
+
* Get the area of the chat.
|
|
26
|
+
*/
|
|
27
|
+
get area(): string | undefined;
|
|
28
|
+
private _writersChanged;
|
|
24
29
|
private _approvalButtons;
|
|
30
|
+
private _outputAreaCompat;
|
|
25
31
|
}
|
|
@@ -2,6 +2,7 @@ import { CommandToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { launchIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { ApprovalButtons } from '../approval-buttons';
|
|
4
4
|
import { TokenUsageWidget } from '../components/token-usage-display';
|
|
5
|
+
import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
|
|
5
6
|
import { CommandIds } from '../tokens';
|
|
6
7
|
/**
|
|
7
8
|
* The chat as a main area widget.
|
|
@@ -34,11 +35,19 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
34
35
|
chatPanel: this.content,
|
|
35
36
|
agentManager: this.model.agentManager
|
|
36
37
|
});
|
|
38
|
+
// Temporary compat: keep output-area CSS context for MIME renderers
|
|
39
|
+
// until jupyter-chat provides it natively.
|
|
40
|
+
this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
|
|
41
|
+
chatPanel: this.content
|
|
42
|
+
});
|
|
43
|
+
this.model.writersChanged.connect(this._writersChanged);
|
|
37
44
|
}
|
|
38
45
|
dispose() {
|
|
39
46
|
super.dispose();
|
|
40
47
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
41
48
|
this._approvalButtons.dispose();
|
|
49
|
+
this._outputAreaCompat.dispose();
|
|
50
|
+
this.model.writersChanged.disconnect(this._writersChanged);
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Get the model of the chat.
|
|
@@ -46,5 +55,24 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
46
55
|
get model() {
|
|
47
56
|
return this.content.model;
|
|
48
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the area of the chat.
|
|
60
|
+
*/
|
|
61
|
+
get area() {
|
|
62
|
+
return this.content.area;
|
|
63
|
+
}
|
|
64
|
+
_writersChanged = (_, writers) => {
|
|
65
|
+
// Check if AI is currently writing (streaming)
|
|
66
|
+
const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
|
|
67
|
+
if (aiWriting) {
|
|
68
|
+
this.content.inputToolbarRegistry?.hide('send');
|
|
69
|
+
this.content.inputToolbarRegistry?.show('stop');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.content.inputToolbarRegistry?.hide('stop');
|
|
73
|
+
this.content.inputToolbarRegistry?.show('send');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
49
76
|
_approvalButtons;
|
|
77
|
+
_outputAreaCompat;
|
|
50
78
|
}
|