@jupyterlite/ai 0.15.0 → 0.17.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 -2
- package/lib/agent.js +112 -17
- package/lib/chat-commands/clear.js +1 -1
- package/lib/chat-model-handler.js +4 -1
- package/lib/chat-model.d.ts +25 -24
- package/lib/chat-model.js +262 -132
- package/lib/components/clear-button.d.ts +1 -1
- package/lib/components/clear-button.js +1 -1
- package/lib/components/index.d.ts +1 -1
- package/lib/components/index.js +1 -1
- package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
- package/lib/components/usage-display.js +109 -0
- package/lib/index.js +205 -20
- package/lib/models/settings-model.js +1 -0
- package/lib/providers/built-in-providers.js +5 -0
- package/lib/providers/generated-context-windows.d.ts +8 -0
- package/lib/providers/generated-context-windows.js +96 -0
- package/lib/providers/model-info.d.ts +3 -0
- package/lib/providers/model-info.js +58 -0
- package/lib/tokens.d.ts +34 -3
- package/lib/tokens.js +8 -7
- package/lib/widgets/ai-settings.js +9 -0
- package/lib/widgets/main-area-chat.d.ts +1 -0
- package/lib/widgets/main-area-chat.js +10 -4
- package/lib/widgets/provider-config-dialog.js +18 -5
- package/package.json +3 -2
- package/schema/settings-model.json +11 -0
- package/src/agent.ts +151 -21
- package/src/chat-commands/clear.ts +1 -1
- package/src/chat-model-handler.ts +6 -1
- package/src/chat-model.ts +350 -175
- package/src/components/clear-button.tsx +3 -3
- package/src/components/index.ts +1 -1
- package/src/components/usage-display.tsx +208 -0
- package/src/index.ts +250 -26
- package/src/models/settings-model.ts +1 -0
- package/src/providers/built-in-providers.ts +5 -0
- package/src/providers/generated-context-windows.ts +102 -0
- package/src/providers/model-info.ts +88 -0
- package/src/tokens.ts +46 -10
- package/src/widgets/ai-settings.tsx +42 -0
- package/src/widgets/main-area-chat.ts +12 -4
- package/src/widgets/provider-config-dialog.tsx +45 -5
- package/lib/components/token-usage-display.js +0 -72
- package/src/components/token-usage-display.tsx +0 -137
package/lib/tokens.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { Token } from '@lumino/coreutils';
|
|
5
5
|
import type { IDisposable } from '@lumino/disposable';
|
|
6
6
|
import { ISignal } from '@lumino/signaling';
|
|
7
|
-
import type { Tool, LanguageModel } from 'ai';
|
|
7
|
+
import type { Tool, LanguageModel, UserContent, ModelMessage } from 'ai';
|
|
8
8
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
9
9
|
import type { IModelOptions } from './providers/models';
|
|
10
10
|
import { AIChatModel } from './chat-model';
|
|
@@ -17,6 +17,7 @@ export declare namespace CommandIds {
|
|
|
17
17
|
const openSettings = "@jupyterlite/ai:open-settings";
|
|
18
18
|
const reposition = "@jupyterlite/ai:reposition";
|
|
19
19
|
const openChat = "@jupyterlite/ai:open-chat";
|
|
20
|
+
const openOrRevealChat = "@jupyterlite/ai:open-or-reveal-chat";
|
|
20
21
|
const moveChat = "@jupyterlite/ai:move-chat";
|
|
21
22
|
const refreshSkills = "@jupyterlite/ai:refresh-skills";
|
|
22
23
|
const saveChat = "@jupyterlite/ai:save-chat";
|
|
@@ -153,6 +154,12 @@ export interface IProviderToolCapabilities {
|
|
|
153
154
|
/**
|
|
154
155
|
* Provider information
|
|
155
156
|
*/
|
|
157
|
+
export interface IProviderModelInfo {
|
|
158
|
+
/**
|
|
159
|
+
* Default context window for the model in tokens.
|
|
160
|
+
*/
|
|
161
|
+
contextWindow?: number;
|
|
162
|
+
}
|
|
156
163
|
export interface IProviderInfo {
|
|
157
164
|
/**
|
|
158
165
|
* Unique identifier for the provider
|
|
@@ -173,6 +180,10 @@ export interface IProviderInfo {
|
|
|
173
180
|
* Default model names for this provider
|
|
174
181
|
*/
|
|
175
182
|
defaultModels: string[];
|
|
183
|
+
/**
|
|
184
|
+
* Optional per-model metadata keyed by model ID.
|
|
185
|
+
*/
|
|
186
|
+
modelInfo?: Record<string, IProviderModelInfo>;
|
|
176
187
|
/**
|
|
177
188
|
* Whether this provider supports custom base URLs
|
|
178
189
|
*/
|
|
@@ -246,6 +257,7 @@ export interface IProviderParameters {
|
|
|
246
257
|
temperature?: number;
|
|
247
258
|
maxOutputTokens?: number;
|
|
248
259
|
maxTurns?: number;
|
|
260
|
+
contextWindow?: number;
|
|
249
261
|
supportsFillInMiddle?: boolean;
|
|
250
262
|
useFilterText?: boolean;
|
|
251
263
|
}
|
|
@@ -282,6 +294,7 @@ export interface IAIConfig {
|
|
|
282
294
|
toolsEnabled: boolean;
|
|
283
295
|
sendWithShiftEnter: boolean;
|
|
284
296
|
showTokenUsage: boolean;
|
|
297
|
+
showContextUsage: boolean;
|
|
285
298
|
commandsRequiringApproval: string[];
|
|
286
299
|
commandsAutoRenderMimeBundles: string[];
|
|
287
300
|
trustedMimeTypesForAutoRender: string[];
|
|
@@ -453,7 +466,7 @@ export interface IAgentManager {
|
|
|
453
466
|
/**
|
|
454
467
|
* Clears conversation history and resets agent state.
|
|
455
468
|
*/
|
|
456
|
-
clearHistory(): void
|
|
469
|
+
clearHistory(): Promise<void>;
|
|
457
470
|
/**
|
|
458
471
|
* Sets the conversation history with a list of messages from the chat.
|
|
459
472
|
* @param messages The chat messages to set as history
|
|
@@ -480,7 +493,12 @@ export interface IAgentManager {
|
|
|
480
493
|
* Handles the complete execution cycle including tool calls.
|
|
481
494
|
* @param message The user message to respond to (may include processed attachment content)
|
|
482
495
|
*/
|
|
483
|
-
generateResponse(message:
|
|
496
|
+
generateResponse(message: UserContent): Promise<void>;
|
|
497
|
+
/**
|
|
498
|
+
* Create a transient language model to request a text response, which won't be added to history.
|
|
499
|
+
* @param messages - the messages sequence to send to the model.
|
|
500
|
+
*/
|
|
501
|
+
textResponse(messages: ModelMessage[]): Promise<string>;
|
|
484
502
|
/**
|
|
485
503
|
* Initializes the AI agent with current settings and tools.
|
|
486
504
|
* Sets up the agent with model configuration, tools, and MCP tools.
|
|
@@ -549,6 +567,10 @@ export interface ICreateChatOptions {
|
|
|
549
567
|
* Whether the chat is autosaved or not.
|
|
550
568
|
*/
|
|
551
569
|
autosave?: boolean;
|
|
570
|
+
/**
|
|
571
|
+
* An optional title to the chat.
|
|
572
|
+
*/
|
|
573
|
+
title?: string | null;
|
|
552
574
|
}
|
|
553
575
|
/**
|
|
554
576
|
* Token for the chat model handler.
|
|
@@ -633,6 +655,15 @@ export interface ITokenUsage {
|
|
|
633
655
|
* Number of output tokens generated (completion tokens)
|
|
634
656
|
*/
|
|
635
657
|
outputTokens: number;
|
|
658
|
+
/**
|
|
659
|
+
* Estimated prompt tokens used by the most recent model request.
|
|
660
|
+
* This is based on the final step of the latest request.
|
|
661
|
+
*/
|
|
662
|
+
lastRequestInputTokens?: number;
|
|
663
|
+
/**
|
|
664
|
+
* Configured context window size for the active provider/model.
|
|
665
|
+
*/
|
|
666
|
+
contextWindow?: number;
|
|
636
667
|
}
|
|
637
668
|
/**
|
|
638
669
|
* The string that replaces a secret key in settings.
|
package/lib/tokens.js
CHANGED
|
@@ -7,6 +7,7 @@ export var CommandIds;
|
|
|
7
7
|
CommandIds.openSettings = '@jupyterlite/ai:open-settings';
|
|
8
8
|
CommandIds.reposition = '@jupyterlite/ai:reposition';
|
|
9
9
|
CommandIds.openChat = '@jupyterlite/ai:open-chat';
|
|
10
|
+
CommandIds.openOrRevealChat = '@jupyterlite/ai:open-or-reveal-chat';
|
|
10
11
|
CommandIds.moveChat = '@jupyterlite/ai:move-chat';
|
|
11
12
|
CommandIds.refreshSkills = '@jupyterlite/ai:refresh-skills';
|
|
12
13
|
CommandIds.saveChat = '@jupyterlite/ai:save-chat';
|
|
@@ -15,15 +16,15 @@ export var CommandIds;
|
|
|
15
16
|
/**
|
|
16
17
|
* The tool registry token.
|
|
17
18
|
*/
|
|
18
|
-
export const IToolRegistry = new Token('@jupyterlite/ai:
|
|
19
|
+
export const IToolRegistry = new Token('@jupyterlite/ai:IToolRegistry', 'Tool registry for AI agent functionality');
|
|
19
20
|
/**
|
|
20
21
|
* The skill registry token.
|
|
21
22
|
*/
|
|
22
|
-
export const ISkillRegistry = new Token('@jupyterlite/ai:
|
|
23
|
+
export const ISkillRegistry = new Token('@jupyterlite/ai:ISkillRegistry', 'Skill registry for AI agent functionality');
|
|
23
24
|
/**
|
|
24
25
|
* Token for the provider registry.
|
|
25
26
|
*/
|
|
26
|
-
export const IProviderRegistry = new Token('@jupyterlite/ai:
|
|
27
|
+
export const IProviderRegistry = new Token('@jupyterlite/ai:IProviderRegistry', 'Registry for AI providers');
|
|
27
28
|
/**
|
|
28
29
|
* Token for the AI settings model.
|
|
29
30
|
*/
|
|
@@ -31,19 +32,19 @@ export const IAISettingsModel = new Token('@jupyterlite/ai:IAISettingsModel');
|
|
|
31
32
|
/**
|
|
32
33
|
* Token for the agent manager.
|
|
33
34
|
*/
|
|
34
|
-
export const IAgentManager = new Token('@jupyterlite/ai:
|
|
35
|
+
export const IAgentManager = new Token('@jupyterlite/ai:IAgentManager');
|
|
35
36
|
/*
|
|
36
37
|
* Token for the agent manager factory.
|
|
37
38
|
*/
|
|
38
|
-
export const IAgentManagerFactory = new Token('@jupyterlite/ai:
|
|
39
|
+
export const IAgentManagerFactory = new Token('@jupyterlite/ai:IAgentManagerFactory');
|
|
39
40
|
/**
|
|
40
41
|
* Token for the chat model handler.
|
|
41
42
|
*/
|
|
42
|
-
export const IChatModelHandler = new Token('@jupyterlite/ai:
|
|
43
|
+
export const IChatModelHandler = new Token('@jupyterlite/ai:IChatModelHandler');
|
|
43
44
|
/**
|
|
44
45
|
* Token for the diff manager.
|
|
45
46
|
*/
|
|
46
|
-
export const IDiffManager = new Token('@jupyterlite/ai:
|
|
47
|
+
export const IDiffManager = new Token('@jupyterlite/ai:IDiffManager');
|
|
47
48
|
/**
|
|
48
49
|
* The string that replaces a secret key in settings.
|
|
49
50
|
*/
|
|
@@ -13,6 +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 { getEffectiveContextWindow } from '../providers/model-info';
|
|
16
17
|
import { SECRETS_REPLACEMENT } from '../tokens';
|
|
17
18
|
import { ProviderConfigDialog } from './provider-config-dialog';
|
|
18
19
|
/**
|
|
@@ -409,6 +410,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
409
410
|
const providerInfo = providerRegistry.getProviderInfo(provider.provider);
|
|
410
411
|
const providerToolCapabilities = providerInfo?.providerToolCapabilities;
|
|
411
412
|
const params = provider.parameters;
|
|
413
|
+
const effectiveContextWindow = getEffectiveContextWindow(provider, providerRegistry);
|
|
412
414
|
const webSearchEnabled = !!providerToolCapabilities?.webSearch &&
|
|
413
415
|
provider.customSettings?.webSearch?.enabled === true;
|
|
414
416
|
const webFetchEnabled = !!providerToolCapabilities?.webFetch &&
|
|
@@ -444,6 +446,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
444
446
|
(params?.temperature !== undefined ||
|
|
445
447
|
params?.maxOutputTokens !== undefined ||
|
|
446
448
|
params?.maxTurns !== undefined ||
|
|
449
|
+
effectiveContextWindow !== undefined ||
|
|
447
450
|
webSearchEnabled ||
|
|
448
451
|
webFetchEnabled) && (React.createElement(Box, { sx: {
|
|
449
452
|
display: 'flex',
|
|
@@ -454,6 +457,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
454
457
|
params?.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
|
|
455
458
|
params?.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
|
|
456
459
|
params?.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" })),
|
|
460
|
+
effectiveContextWindow !== undefined && (React.createElement(Chip, { label: trans.__('Context: %1', effectiveContextWindow), size: "small", variant: "outlined" })),
|
|
457
461
|
webSearchEnabled && (React.createElement(Chip, { label: trans.__('Web Search'), size: "small", variant: "outlined", color: "info" })),
|
|
458
462
|
webFetchEnabled && (React.createElement(Chip, { label: trans.__('Web Fetch'), size: "small", variant: "outlined", color: "info" }))))),
|
|
459
463
|
React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
|
|
@@ -483,6 +487,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
483
487
|
}), color: "primary" }), label: React.createElement(Box, null,
|
|
484
488
|
React.createElement(Typography, { variant: "body1" }, trans.__('Show Token Usage')),
|
|
485
489
|
React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Display token usage information in the chat toolbar'))) }),
|
|
490
|
+
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showContextUsage, onChange: e => handleConfigUpdate({
|
|
491
|
+
showContextUsage: e.target.checked
|
|
492
|
+
}), color: "primary" }), label: React.createElement(Box, null,
|
|
493
|
+
React.createElement(Typography, { variant: "body1" }, trans.__('Show Context Usage')),
|
|
494
|
+
React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Display estimated context usage in the chat toolbar'))) }),
|
|
486
495
|
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showCellDiff, onChange: e => handleConfigUpdate({
|
|
487
496
|
showCellDiff: e.target.checked
|
|
488
497
|
}), color: "primary" }), label: React.createElement(Box, null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommandToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
|
|
2
2
|
import { launchIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { SaveComponentWidget } from '../components/save-button';
|
|
4
|
-
import {
|
|
4
|
+
import { UsageWidget } from '../components/usage-display';
|
|
5
5
|
import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
|
|
6
6
|
import { CommandIds } from '../tokens';
|
|
7
7
|
/**
|
|
@@ -10,7 +10,8 @@ import { CommandIds } from '../tokens';
|
|
|
10
10
|
export class MainAreaChat extends MainAreaWidget {
|
|
11
11
|
constructor(options) {
|
|
12
12
|
super(options);
|
|
13
|
-
this.title.label = this.
|
|
13
|
+
this.title.label = this.model.name;
|
|
14
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
14
15
|
const { trans } = options;
|
|
15
16
|
// Move to side button.
|
|
16
17
|
this.toolbar.addItem('moveToSide', new CommandToolbarButton({
|
|
@@ -30,25 +31,27 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
30
31
|
}));
|
|
31
32
|
}
|
|
32
33
|
// Add the token usage button.
|
|
33
|
-
const
|
|
34
|
+
const usageWidget = new UsageWidget({
|
|
34
35
|
tokenUsageChanged: this.model.tokenUsageChanged,
|
|
35
36
|
settingsModel: options.settingsModel,
|
|
36
37
|
initialTokenUsage: this.model.agentManager.tokenUsage,
|
|
37
38
|
translator: trans
|
|
38
39
|
});
|
|
39
|
-
this.toolbar.addItem('
|
|
40
|
+
this.toolbar.addItem('usage', usageWidget);
|
|
40
41
|
// Temporary compat: keep output-area CSS context for MIME renderers
|
|
41
42
|
// until jupyter-chat provides it natively.
|
|
42
43
|
this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
|
|
43
44
|
chatPanel: this.content
|
|
44
45
|
});
|
|
45
46
|
this.model.writersChanged.connect(this._writersChanged);
|
|
47
|
+
this.model.titleChanged.connect(this._titleChanged);
|
|
46
48
|
}
|
|
47
49
|
dispose() {
|
|
48
50
|
super.dispose();
|
|
49
51
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
50
52
|
this._outputAreaCompat.dispose();
|
|
51
53
|
this.model.writersChanged.disconnect(this._writersChanged);
|
|
54
|
+
this.model.titleChanged.disconnect(this._titleChanged);
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Get the model of the chat.
|
|
@@ -74,5 +77,8 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
74
77
|
this.content.inputToolbarRegistry?.show('send');
|
|
75
78
|
}
|
|
76
79
|
};
|
|
80
|
+
_titleChanged = () => {
|
|
81
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
82
|
+
};
|
|
77
83
|
_outputAreaCompat;
|
|
78
84
|
}
|
|
@@ -4,6 +4,7 @@ import Visibility from '@mui/icons-material/Visibility';
|
|
|
4
4
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
5
5
|
import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, List, ListItem, ListItemText, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import { getProviderModelInfo } from '../providers/model-info';
|
|
7
8
|
/**
|
|
8
9
|
* Default parameter values for provider configuration
|
|
9
10
|
*/
|
|
@@ -80,6 +81,7 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
80
81
|
const [expandedAdvanced, setExpandedAdvanced] = React.useState(false);
|
|
81
82
|
const selectedProviderInfo = React.useMemo(() => providerRegistry.getProviderInfo(provider), [providerRegistry, provider]);
|
|
82
83
|
const providerToolCapabilities = selectedProviderInfo?.providerToolCapabilities;
|
|
84
|
+
const selectedModelInfo = React.useMemo(() => getProviderModelInfo(selectedProviderInfo, model), [selectedProviderInfo, model]);
|
|
83
85
|
const webSearchImplementation = providerToolCapabilities?.webSearch?.implementation;
|
|
84
86
|
const supportsWebSearch = !!providerToolCapabilities?.webSearch;
|
|
85
87
|
const supportsWebFetch = !!providerToolCapabilities?.webFetch;
|
|
@@ -300,13 +302,24 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
300
302
|
maxOutputTokens: e.target.value
|
|
301
303
|
? Number(e.target.value)
|
|
302
304
|
: undefined
|
|
303
|
-
}), placeholder: trans.__('Leave empty for provider default'), helperText: trans.__('Maximum length of AI responses'),
|
|
305
|
+
}), placeholder: trans.__('Leave empty for provider default'), helperText: trans.__('Maximum length of AI responses'), slotProps: { htmlInput: { min: 1 } } }),
|
|
304
306
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Max Turns (Optional)'), type: "number", value: parameters.maxTurns ?? '', onChange: e => setParameters({
|
|
305
307
|
...parameters,
|
|
306
308
|
maxTurns: e.target.value
|
|
307
309
|
? Number(e.target.value)
|
|
308
310
|
: undefined
|
|
309
|
-
}), placeholder: trans.__('Default: %1', DEFAULT_MAX_TURNS), helperText: trans.__('Maximum number of tool execution turns'),
|
|
311
|
+
}), placeholder: trans.__('Default: %1', DEFAULT_MAX_TURNS), helperText: trans.__('Maximum number of tool execution turns'), slotProps: { htmlInput: { min: 1, max: 100 } } }),
|
|
312
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Context Window (Optional)'), type: "number", value: parameters.contextWindow ?? '', onChange: e => setParameters({
|
|
313
|
+
...parameters,
|
|
314
|
+
contextWindow: e.target.value
|
|
315
|
+
? Number(e.target.value)
|
|
316
|
+
: undefined
|
|
317
|
+
}), placeholder: selectedModelInfo?.contextWindow !== undefined
|
|
318
|
+
? trans.__('Default: %1', selectedModelInfo.contextWindow.toLocaleString())
|
|
319
|
+
: trans.__('e.g., 128000'), helperText: selectedModelInfo?.contextWindow !== undefined &&
|
|
320
|
+
parameters.contextWindow === undefined
|
|
321
|
+
? trans.__('Using provider metadata default of %1 tokens for this model unless you override it here.', selectedModelInfo.contextWindow.toLocaleString())
|
|
322
|
+
: trans.__('Model context window size in tokens (used for context usage estimation)'), slotProps: { htmlInput: { min: 1 } } }),
|
|
310
323
|
React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 2, mb: 1 } }, trans.__('Completion Options')),
|
|
311
324
|
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: parameters.supportsFillInMiddle ?? false, onChange: e => setParameters({
|
|
312
325
|
...parameters,
|
|
@@ -344,7 +357,7 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
344
357
|
webSearchImplementation === 'anthropic' && (React.createElement(React.Fragment, null,
|
|
345
358
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Search Max Uses'), type: "number", value: webSearchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webSearch', 'maxUses', e.target.value
|
|
346
359
|
? Number(e.target.value)
|
|
347
|
-
: undefined),
|
|
360
|
+
: undefined), slotProps: { htmlInput: { min: 1 } } }),
|
|
348
361
|
renderDomainList('webSearch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webSearchSettings.blockedDomains))))))),
|
|
349
362
|
supportsWebFetch && (React.createElement(React.Fragment, null,
|
|
350
363
|
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.enabled === true, onChange: e => updateCustomSetting('webFetch', 'enabled', e.target.checked) }), label: trans.__('Enable Web Fetch') }),
|
|
@@ -358,10 +371,10 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
358
371
|
} },
|
|
359
372
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Uses'), type: "number", value: webFetchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webFetch', 'maxUses', e.target.value
|
|
360
373
|
? Number(e.target.value)
|
|
361
|
-
: undefined),
|
|
374
|
+
: undefined), slotProps: { htmlInput: { min: 1 } } }),
|
|
362
375
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Content Tokens'), type: "number", value: webFetchSettings.maxContentTokens ?? '', onChange: e => updateCustomSetting('webFetch', 'maxContentTokens', e.target.value
|
|
363
376
|
? Number(e.target.value)
|
|
364
|
-
: undefined),
|
|
377
|
+
: undefined), slotProps: { htmlInput: { min: 1 } } }),
|
|
365
378
|
renderDomainList('webFetch.allowedDomains', trans.__('Allowed Domains'), trans.__('docs.example.com'), webFetchSettings.allowedDomains),
|
|
366
379
|
renderDomainList('webFetch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webFetchSettings.blockedDomains),
|
|
367
380
|
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.citationsEnabled === true, onChange: e => updateCustomSetting('webFetch', 'citationsEnabled', e.target.checked) }), label: trans.__('Enable Citations') })))))))))))),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlite/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "AI code completions and chat for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"watch:src": "tsc -w --sourceMap",
|
|
54
54
|
"watch:labextension": "jupyter labextension watch .",
|
|
55
55
|
"docs": "jupyter book start",
|
|
56
|
-
"docs:build": "sed -e 's/\\[@/[/g' -e 's/@/\\@/g' CHANGELOG.md > docs/_changelog_content.md && jupyter book build --html"
|
|
56
|
+
"docs:build": "sed -e 's/\\[@/[/g' -e 's/@/\\@/g' CHANGELOG.md > docs/_changelog_content.md && jupyter book build --html",
|
|
57
|
+
"sync:model-context-windows": "node scripts/sync-model-context-windows.mjs && prettier --write src/providers/generated-context-windows.ts && eslint --fix src/providers/generated-context-windows.ts"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"@ai-sdk/anthropic": "^3.0.58",
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
"maximum": 100,
|
|
55
55
|
"default": 25
|
|
56
56
|
},
|
|
57
|
+
"contextWindow": {
|
|
58
|
+
"type": "number",
|
|
59
|
+
"description": "Model context window size in tokens (used for context usage estimation)",
|
|
60
|
+
"minimum": 1
|
|
61
|
+
},
|
|
57
62
|
"supportsFillInMiddle": {
|
|
58
63
|
"type": "boolean",
|
|
59
64
|
"description": "Whether the model supports fill-in-middle completion"
|
|
@@ -211,6 +216,12 @@
|
|
|
211
216
|
"type": "boolean",
|
|
212
217
|
"default": false
|
|
213
218
|
},
|
|
219
|
+
"showContextUsage": {
|
|
220
|
+
"title": "Show Context Usage",
|
|
221
|
+
"description": "Display estimated context usage percentage in the chat toolbar",
|
|
222
|
+
"type": "boolean",
|
|
223
|
+
"default": false
|
|
224
|
+
},
|
|
214
225
|
"commandsRequiringApproval": {
|
|
215
226
|
"title": "Commands Requiring Approval",
|
|
216
227
|
"description": "List of commands that require user approval before AI can execute them",
|
package/src/agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
5
5
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
6
|
import {
|
|
7
|
+
generateText,
|
|
7
8
|
ToolLoopAgent,
|
|
8
9
|
type ModelMessage,
|
|
9
10
|
type LanguageModel,
|
|
@@ -13,11 +14,14 @@ import {
|
|
|
13
14
|
type TypedToolError,
|
|
14
15
|
type TypedToolOutputDenied,
|
|
15
16
|
type TypedToolResult,
|
|
16
|
-
type
|
|
17
|
+
type UserContent,
|
|
18
|
+
type AssistantModelMessage,
|
|
19
|
+
APICallError
|
|
17
20
|
} from 'ai';
|
|
18
21
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
19
22
|
|
|
20
23
|
import { createModel } from './providers/models';
|
|
24
|
+
import { getEffectiveContextWindow } from './providers/model-info';
|
|
21
25
|
import {
|
|
22
26
|
createProviderTools,
|
|
23
27
|
type IProviderCustomSettings
|
|
@@ -53,6 +57,10 @@ interface IStreamProcessResult {
|
|
|
53
57
|
* Whether an approval request was encountered and processed.
|
|
54
58
|
*/
|
|
55
59
|
approvalProcessed: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the stream was aborted before completion.
|
|
62
|
+
*/
|
|
63
|
+
aborted: boolean;
|
|
56
64
|
/**
|
|
57
65
|
* The approval response message to add to history (if approval was processed).
|
|
58
66
|
*/
|
|
@@ -387,7 +395,17 @@ export class AgentManager implements IAgentManager {
|
|
|
387
395
|
return this._activeProvider;
|
|
388
396
|
}
|
|
389
397
|
set activeProvider(value: string) {
|
|
398
|
+
const previousProvider = this._activeProvider;
|
|
390
399
|
this._activeProvider = value;
|
|
400
|
+
|
|
401
|
+
// Reset request-level context estimate only when switching between providers.
|
|
402
|
+
if (previousProvider && previousProvider !== value) {
|
|
403
|
+
this._tokenUsage.lastRequestInputTokens = undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this._tokenUsage.contextWindow = this._getActiveContextWindow();
|
|
407
|
+
|
|
408
|
+
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
391
409
|
this.initializeAgent();
|
|
392
410
|
this._activeProviderChanged.emit(this._activeProvider);
|
|
393
411
|
}
|
|
@@ -463,7 +481,11 @@ export class AgentManager implements IAgentManager {
|
|
|
463
481
|
|
|
464
482
|
// Clear history and token usage
|
|
465
483
|
this._history = [];
|
|
466
|
-
this._tokenUsage = {
|
|
484
|
+
this._tokenUsage = {
|
|
485
|
+
inputTokens: 0,
|
|
486
|
+
outputTokens: 0,
|
|
487
|
+
contextWindow: this._getActiveContextWindow()
|
|
488
|
+
};
|
|
467
489
|
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
468
490
|
}
|
|
469
491
|
|
|
@@ -485,12 +507,13 @@ export class AgentManager implements IAgentManager {
|
|
|
485
507
|
this._pendingApprovals.clear();
|
|
486
508
|
|
|
487
509
|
// Convert chat messages to model messages
|
|
488
|
-
const modelMessages = messages.map(msg => {
|
|
489
|
-
const
|
|
510
|
+
const modelMessages: ModelMessage[] = messages.map(msg => {
|
|
511
|
+
const role =
|
|
512
|
+
msg.sender.username === 'ai-assistant' ? 'assistant' : 'user';
|
|
490
513
|
return {
|
|
491
|
-
role
|
|
514
|
+
role,
|
|
492
515
|
content: msg.body
|
|
493
|
-
}
|
|
516
|
+
};
|
|
494
517
|
});
|
|
495
518
|
this._history = Private.sanitizeModelMessages(modelMessages);
|
|
496
519
|
}
|
|
@@ -552,10 +575,17 @@ export class AgentManager implements IAgentManager {
|
|
|
552
575
|
* Handles the complete execution cycle including tool calls.
|
|
553
576
|
* @param message The user message to respond to (may include processed attachment content)
|
|
554
577
|
*/
|
|
555
|
-
async generateResponse(message:
|
|
578
|
+
async generateResponse(message: UserContent): Promise<void> {
|
|
556
579
|
this._streaming = new PromiseDelegate();
|
|
557
580
|
this._controller = new AbortController();
|
|
558
581
|
const responseHistory: ModelMessage[] = [];
|
|
582
|
+
|
|
583
|
+
// Add user message to history
|
|
584
|
+
responseHistory.push({
|
|
585
|
+
role: 'user',
|
|
586
|
+
content: message
|
|
587
|
+
});
|
|
588
|
+
|
|
559
589
|
try {
|
|
560
590
|
// Ensure we have an agent
|
|
561
591
|
if (!this._agent) {
|
|
@@ -566,12 +596,6 @@ export class AgentManager implements IAgentManager {
|
|
|
566
596
|
throw new Error('Failed to initialize agent');
|
|
567
597
|
}
|
|
568
598
|
|
|
569
|
-
// Add user message to history
|
|
570
|
-
responseHistory.push({
|
|
571
|
-
role: 'user',
|
|
572
|
-
content: message
|
|
573
|
-
});
|
|
574
|
-
|
|
575
599
|
let continueLoop = true;
|
|
576
600
|
while (continueLoop) {
|
|
577
601
|
const result = await this._agent.stream({
|
|
@@ -581,9 +605,22 @@ export class AgentManager implements IAgentManager {
|
|
|
581
605
|
|
|
582
606
|
const streamResult = await this._processStreamResult(result);
|
|
583
607
|
|
|
584
|
-
|
|
608
|
+
if (streamResult.aborted) {
|
|
609
|
+
try {
|
|
610
|
+
const responseMessages = await result.response;
|
|
611
|
+
if (responseMessages.messages?.length) {
|
|
612
|
+
this._history.push(
|
|
613
|
+
...Private.sanitizeModelMessages(responseMessages.messages)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
// Aborting before a step finishes leaves no completed response to persist.
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Get response messages for completed steps.
|
|
585
623
|
const responseMessages = await result.response;
|
|
586
|
-
this._updateTokenUsage(await result.usage);
|
|
587
624
|
|
|
588
625
|
// Add response messages to history
|
|
589
626
|
if (responseMessages.messages?.length) {
|
|
@@ -615,9 +652,41 @@ export class AgentManager implements IAgentManager {
|
|
|
615
652
|
this._history.push(...Private.sanitizeModelMessages(responseHistory));
|
|
616
653
|
} catch (error) {
|
|
617
654
|
if ((error as Error).name !== 'AbortError') {
|
|
655
|
+
let helpMessage = `${(error as Error).message}`;
|
|
656
|
+
|
|
657
|
+
// Remove attachments from history on payload rejection errors
|
|
658
|
+
if (
|
|
659
|
+
APICallError.isInstance(error) &&
|
|
660
|
+
(error.statusCode === 400 ||
|
|
661
|
+
error.statusCode === 404 ||
|
|
662
|
+
error.statusCode === 413 ||
|
|
663
|
+
error.statusCode === 415 ||
|
|
664
|
+
error.statusCode === 422)
|
|
665
|
+
) {
|
|
666
|
+
for (const msg of [...this._history, ...responseHistory]) {
|
|
667
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
668
|
+
const hasMedia = msg.content.some(p => p.type !== 'text');
|
|
669
|
+
if (hasMedia) {
|
|
670
|
+
const textContent = msg.content
|
|
671
|
+
.filter(p => p.type === 'text')
|
|
672
|
+
.map(p => (p as { text: string }).text)
|
|
673
|
+
.join('\n');
|
|
674
|
+
msg.content =
|
|
675
|
+
textContent || '_Attachment removed due to error_';
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
helpMessage +=
|
|
680
|
+
'\n\nAttachments have been removed from history. Please send your prompt again.';
|
|
681
|
+
}
|
|
618
682
|
this._agentEvent.emit({
|
|
619
683
|
type: 'error',
|
|
620
|
-
data: { error:
|
|
684
|
+
data: { error: new Error(helpMessage) }
|
|
685
|
+
});
|
|
686
|
+
this._history.push(...Private.sanitizeModelMessages(responseHistory));
|
|
687
|
+
this._history.push({
|
|
688
|
+
role: 'assistant',
|
|
689
|
+
content: helpMessage
|
|
621
690
|
});
|
|
622
691
|
}
|
|
623
692
|
} finally {
|
|
@@ -627,16 +696,56 @@ export class AgentManager implements IAgentManager {
|
|
|
627
696
|
}
|
|
628
697
|
|
|
629
698
|
/**
|
|
630
|
-
*
|
|
699
|
+
* Create a transient language model to request a text response which won't be added to history.
|
|
700
|
+
* @param messages - the messages sequence to send to the model.
|
|
701
|
+
*/
|
|
702
|
+
async textResponse(messages: ModelMessage[]): Promise<string> {
|
|
703
|
+
try {
|
|
704
|
+
const model = await this._createModel();
|
|
705
|
+
const result = await generateText({
|
|
706
|
+
model,
|
|
707
|
+
messages
|
|
708
|
+
});
|
|
709
|
+
this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
|
|
710
|
+
return result.text;
|
|
711
|
+
} catch (e) {
|
|
712
|
+
throw `Error while getting the topic of the chat\n${e}`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Updates cumulative token usage statistics from a completed model step.
|
|
631
718
|
*/
|
|
632
719
|
private _updateTokenUsage(
|
|
633
|
-
usage: { inputTokens?: number; outputTokens?: number } | undefined
|
|
720
|
+
usage: { inputTokens?: number; outputTokens?: number } | undefined,
|
|
721
|
+
lastRequestInputTokens?: number
|
|
634
722
|
): void {
|
|
723
|
+
const contextWindow = this._getActiveContextWindow();
|
|
724
|
+
const estimatedRequestInputTokens =
|
|
725
|
+
lastRequestInputTokens ?? usage?.inputTokens;
|
|
726
|
+
|
|
635
727
|
if (usage) {
|
|
636
728
|
this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
|
|
637
729
|
this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
|
|
638
|
-
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
639
730
|
}
|
|
731
|
+
|
|
732
|
+
this._tokenUsage.lastRequestInputTokens = estimatedRequestInputTokens;
|
|
733
|
+
this._tokenUsage.contextWindow = contextWindow;
|
|
734
|
+
|
|
735
|
+
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Gets the configured context window for the active provider.
|
|
740
|
+
*/
|
|
741
|
+
private _getActiveContextWindow(): number | undefined {
|
|
742
|
+
const activeProviderConfig = this._settingsModel.getProvider(
|
|
743
|
+
this._activeProvider
|
|
744
|
+
);
|
|
745
|
+
return getEffectiveContextWindow(
|
|
746
|
+
activeProviderConfig,
|
|
747
|
+
this._providerRegistry
|
|
748
|
+
);
|
|
640
749
|
}
|
|
641
750
|
|
|
642
751
|
/**
|
|
@@ -699,6 +808,13 @@ export class AgentManager implements IAgentManager {
|
|
|
699
808
|
activeProviderConfig && this._providerRegistry
|
|
700
809
|
? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
|
|
701
810
|
: null;
|
|
811
|
+
const contextWindow = getEffectiveContextWindow(
|
|
812
|
+
activeProviderConfig,
|
|
813
|
+
this._providerRegistry
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
this._tokenUsage.contextWindow = contextWindow;
|
|
817
|
+
this._tokenUsageChanged.emit(this._tokenUsage);
|
|
702
818
|
|
|
703
819
|
const temperature =
|
|
704
820
|
activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
|
|
@@ -806,7 +922,10 @@ ${richOutputWorkflowInstruction}`;
|
|
|
806
922
|
): Promise<IStreamProcessResult> {
|
|
807
923
|
let fullResponse = '';
|
|
808
924
|
let currentMessageId: string | null = null;
|
|
809
|
-
const processResult: IStreamProcessResult = {
|
|
925
|
+
const processResult: IStreamProcessResult = {
|
|
926
|
+
approvalProcessed: false,
|
|
927
|
+
aborted: false
|
|
928
|
+
};
|
|
810
929
|
|
|
811
930
|
for await (const part of result.fullStream) {
|
|
812
931
|
switch (part.type) {
|
|
@@ -868,7 +987,18 @@ ${richOutputWorkflowInstruction}`;
|
|
|
868
987
|
await this._handleApprovalRequest(part, processResult);
|
|
869
988
|
break;
|
|
870
989
|
|
|
871
|
-
|
|
990
|
+
case 'error':
|
|
991
|
+
throw part.error;
|
|
992
|
+
|
|
993
|
+
case 'finish-step':
|
|
994
|
+
this._updateTokenUsage(part.usage, part.usage.inputTokens);
|
|
995
|
+
break;
|
|
996
|
+
|
|
997
|
+
case 'abort':
|
|
998
|
+
processResult.aborted = true;
|
|
999
|
+
break;
|
|
1000
|
+
|
|
1001
|
+
// Ignore: text-start, text-end, finish, and others
|
|
872
1002
|
default:
|
|
873
1003
|
break;
|
|
874
1004
|
}
|