@jupyterlite/ai 0.12.0 → 0.13.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.
Files changed (49) hide show
  1. package/lib/agent.d.ts +24 -2
  2. package/lib/agent.js +161 -24
  3. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  4. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  5. package/lib/chat-model.d.ts +8 -0
  6. package/lib/chat-model.js +156 -8
  7. package/lib/completion/completion-provider.d.ts +1 -1
  8. package/lib/completion/completion-provider.js +14 -2
  9. package/lib/components/model-select.js +4 -4
  10. package/lib/components/tool-select.d.ts +11 -2
  11. package/lib/components/tool-select.js +77 -18
  12. package/lib/index.d.ts +3 -3
  13. package/lib/index.js +128 -66
  14. package/lib/models/settings-model.d.ts +2 -0
  15. package/lib/models/settings-model.js +2 -0
  16. package/lib/providers/built-in-providers.js +7 -0
  17. package/lib/providers/provider-tools.d.ts +36 -0
  18. package/lib/providers/provider-tools.js +93 -0
  19. package/lib/rendered-message-outputarea.d.ts +24 -0
  20. package/lib/rendered-message-outputarea.js +48 -0
  21. package/lib/tokens.d.ts +44 -7
  22. package/lib/tokens.js +1 -1
  23. package/lib/tools/commands.js +4 -2
  24. package/lib/tools/web.d.ts +8 -0
  25. package/lib/tools/web.js +196 -0
  26. package/lib/widgets/ai-settings.d.ts +1 -1
  27. package/lib/widgets/ai-settings.js +125 -38
  28. package/lib/widgets/main-area-chat.d.ts +6 -0
  29. package/lib/widgets/main-area-chat.js +28 -0
  30. package/lib/widgets/provider-config-dialog.js +207 -4
  31. package/package.json +10 -4
  32. package/schema/settings-model.json +89 -1
  33. package/src/agent.ts +220 -42
  34. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  35. package/src/chat-model.ts +223 -14
  36. package/src/completion/completion-provider.ts +26 -12
  37. package/src/components/model-select.tsx +4 -5
  38. package/src/components/tool-select.tsx +110 -7
  39. package/src/index.ts +153 -82
  40. package/src/models/settings-model.ts +6 -0
  41. package/src/providers/built-in-providers.ts +7 -0
  42. package/src/providers/provider-tools.ts +179 -0
  43. package/src/rendered-message-outputarea.ts +62 -0
  44. package/src/tokens.ts +53 -9
  45. package/src/tools/commands.ts +4 -2
  46. package/src/tools/web.ts +238 -0
  47. package/src/widgets/ai-settings.tsx +282 -77
  48. package/src/widgets/main-area-chat.ts +34 -1
  49. package/src/widgets/provider-config-dialog.tsx +496 -3
@@ -31,7 +31,6 @@ import {
31
31
  InputLabel,
32
32
  List,
33
33
  ListItem,
34
- ListItemSecondaryAction,
35
34
  ListItemText,
36
35
  Menu,
37
36
  MenuItem,
@@ -100,6 +99,12 @@ export class AISettingsWidget extends ReactWidget {
100
99
  this.title.label = this._trans.__('AI Settings');
101
100
  this.title.caption = this._trans.__('Configure AI providers and behavior');
102
101
  this.title.closable = true;
102
+
103
+ // Disable the secrets manager if the token is empty.
104
+ if (!options.token) {
105
+ this._settingsModel.updateConfig({ useSecretsManager: false });
106
+ this._secretsManager = undefined;
107
+ }
103
108
  }
104
109
 
105
110
  /**
@@ -282,8 +287,12 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
282
287
  provider: string,
283
288
  fieldName: string
284
289
  ): Promise<string | undefined> => {
290
+ const token = Private.getToken();
291
+ if (!token) {
292
+ return;
293
+ }
285
294
  const secret = await secretsManager?.get(
286
- Private.getToken(),
295
+ token,
287
296
  SECRETS_NAMESPACE,
288
297
  `${provider}:${fieldName}`
289
298
  );
@@ -295,8 +304,12 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
295
304
  fieldName: string,
296
305
  value: string
297
306
  ): Promise<void> => {
307
+ const token = Private.getToken();
308
+ if (!token) {
309
+ return;
310
+ }
298
311
  await secretsManager?.set(
299
- Private.getToken(),
312
+ token,
300
313
  SECRETS_NAMESPACE,
301
314
  `${provider}:${fieldName}`,
302
315
  {
@@ -321,8 +334,12 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
321
334
  if (!(model.config.useSecretsManager && secretsManager)) {
322
335
  return;
323
336
  }
337
+ const token = Private.getToken();
338
+ if (!token) {
339
+ return;
340
+ }
324
341
  await secretsManager?.attach(
325
- Private.getToken(),
342
+ token,
326
343
  SECRETS_NAMESPACE,
327
344
  `${provider}:${fieldName}`,
328
345
  input
@@ -427,17 +444,25 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
427
444
  if (updates.useSecretsManager !== undefined) {
428
445
  if (updates.useSecretsManager) {
429
446
  for (const provider of model.config.providers) {
430
- // if the secrets manager doesn't have the current API key, copy the current
447
+ const settingsApiKey = provider.apiKey;
448
+ // If the secrets manager doesn't have the current API key, set the current
431
449
  // one from settings.
450
+ // Update the settings value with SECRETS_REPLACEMENT if a key exist in the
451
+ // secrets manager (was already there or a value was set in settings).
432
452
  if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
433
- setSecretToManager(
434
- provider.provider,
435
- 'apiKey',
436
- provider.apiKey ?? ''
437
- );
453
+ if (settingsApiKey !== undefined) {
454
+ setSecretToManager(
455
+ provider.provider,
456
+ 'apiKey',
457
+ settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : ''
458
+ );
459
+ provider.apiKey = SECRETS_REPLACEMENT;
460
+ await model.updateProvider(provider.id, provider);
461
+ }
462
+ } else {
463
+ provider.apiKey = SECRETS_REPLACEMENT;
464
+ await model.updateProvider(provider.id, provider);
438
465
  }
439
- provider.apiKey = SECRETS_REPLACEMENT;
440
- await model.updateProvider(provider.id, provider);
441
466
  }
442
467
  } else {
443
468
  for (const provider of model.config.providers) {
@@ -445,10 +470,8 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
445
470
  provider.provider,
446
471
  'apiKey'
447
472
  );
448
- if (apiKey) {
449
- provider.apiKey = apiKey;
450
- await model.updateProvider(provider.id, provider);
451
- }
473
+ provider.apiKey = apiKey;
474
+ await model.updateProvider(provider.id, provider);
452
475
  }
453
476
  }
454
477
  }
@@ -676,7 +699,18 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
676
699
  config.useSameProviderForChatAndCompleter
677
700
  ? isActive
678
701
  : config.activeCompleterProvider === provider.id;
702
+ const providerInfo = providerRegistry.getProviderInfo(
703
+ provider.provider
704
+ );
705
+ const providerToolCapabilities =
706
+ providerInfo?.providerToolCapabilities;
679
707
  const params = provider.parameters;
708
+ const webSearchEnabled =
709
+ !!providerToolCapabilities?.webSearch &&
710
+ provider.customSettings?.webSearch?.enabled === true;
711
+ const webFetchEnabled =
712
+ !!providerToolCapabilities?.webFetch &&
713
+ provider.customSettings?.webFetch?.enabled === true;
680
714
 
681
715
  return (
682
716
  <ListItem
@@ -739,50 +773,67 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
739
773
  </Typography>
740
774
 
741
775
  {/* Display parameters if set */}
742
- {params &&
743
- (params.temperature !== undefined ||
744
- params.maxOutputTokens !== undefined ||
745
- params.maxTurns !== undefined) && (
746
- <Box
747
- sx={{
748
- display: 'flex',
749
- flexWrap: 'wrap',
750
- gap: 1,
751
- mt: 1
752
- }}
753
- >
754
- {params.temperature !== undefined && (
755
- <Chip
756
- label={trans.__(
757
- 'Temp: %1',
758
- params.temperature
759
- )}
760
- size="small"
761
- variant="outlined"
762
- />
763
- )}
764
- {params.maxOutputTokens !== undefined && (
765
- <Chip
766
- label={trans.__(
767
- 'Tokens: %1',
768
- params.maxOutputTokens
769
- )}
770
- size="small"
771
- variant="outlined"
772
- />
773
- )}
774
- {params.maxTurns !== undefined && (
775
- <Chip
776
- label={trans.__(
777
- 'Turns: %1',
778
- params.maxTurns
779
- )}
780
- size="small"
781
- variant="outlined"
782
- />
783
- )}
784
- </Box>
785
- )}
776
+ {(params?.temperature !== undefined ||
777
+ params?.maxOutputTokens !== undefined ||
778
+ params?.maxTurns !== undefined ||
779
+ webSearchEnabled ||
780
+ webFetchEnabled) && (
781
+ <Box
782
+ sx={{
783
+ display: 'flex',
784
+ flexWrap: 'wrap',
785
+ gap: 1,
786
+ mt: 1
787
+ }}
788
+ >
789
+ {params?.temperature !== undefined && (
790
+ <Chip
791
+ label={trans.__(
792
+ 'Temp: %1',
793
+ params.temperature
794
+ )}
795
+ size="small"
796
+ variant="outlined"
797
+ />
798
+ )}
799
+ {params?.maxOutputTokens !== undefined && (
800
+ <Chip
801
+ label={trans.__(
802
+ 'Tokens: %1',
803
+ params.maxOutputTokens
804
+ )}
805
+ size="small"
806
+ variant="outlined"
807
+ />
808
+ )}
809
+ {params?.maxTurns !== undefined && (
810
+ <Chip
811
+ label={trans.__(
812
+ 'Turns: %1',
813
+ params.maxTurns
814
+ )}
815
+ size="small"
816
+ variant="outlined"
817
+ />
818
+ )}
819
+ {webSearchEnabled && (
820
+ <Chip
821
+ label={trans.__('Web Search')}
822
+ size="small"
823
+ variant="outlined"
824
+ color="info"
825
+ />
826
+ )}
827
+ {webFetchEnabled && (
828
+ <Chip
829
+ label={trans.__('Web Fetch')}
830
+ size="small"
831
+ variant="outlined"
832
+ color="info"
833
+ />
834
+ )}
835
+ </Box>
836
+ )}
786
837
  </Box>
787
838
  <IconButton
788
839
  onClick={e => handleMenuClick(e, provider.id)}
@@ -1117,9 +1168,10 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1117
1168
 
1118
1169
  <List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
1119
1170
  {config.commandsRequiringApproval.map((command, index) => (
1120
- <ListItem key={index} divider>
1121
- <ListItemText primary={command} />
1122
- <ListItemSecondaryAction>
1171
+ <ListItem
1172
+ key={index}
1173
+ divider
1174
+ secondaryAction={
1123
1175
  <IconButton
1124
1176
  onClick={() => {
1125
1177
  const newCommands = [
@@ -1134,7 +1186,9 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1134
1186
  >
1135
1187
  <Delete />
1136
1188
  </IconButton>
1137
- </ListItemSecondaryAction>
1189
+ }
1190
+ >
1191
+ <ListItemText primary={command} />
1138
1192
  </ListItem>
1139
1193
  ))}
1140
1194
  </List>
@@ -1168,6 +1222,154 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1168
1222
  )}
1169
1223
  />
1170
1224
  </Box>
1225
+
1226
+ <Divider sx={{ my: 2 }} />
1227
+
1228
+ <Box>
1229
+ <Typography variant="body1" gutterBottom>
1230
+ {trans.__('Commands Auto-Rendering MIME Bundles')}
1231
+ </Typography>
1232
+ <Typography
1233
+ variant="caption"
1234
+ color="text.secondary"
1235
+ gutterBottom
1236
+ sx={{ display: 'block' }}
1237
+ >
1238
+ {trans.__(
1239
+ 'Only these execute_command command IDs can auto-render MIME bundle outputs in chat'
1240
+ )}
1241
+ </Typography>
1242
+
1243
+ <List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
1244
+ {(config.commandsAutoRenderMimeBundles ?? []).map(
1245
+ (command, index) => (
1246
+ <ListItem
1247
+ key={index}
1248
+ divider
1249
+ secondaryAction={
1250
+ <IconButton
1251
+ onClick={() => {
1252
+ const newCommands = [
1253
+ ...(config.commandsAutoRenderMimeBundles ??
1254
+ [])
1255
+ ];
1256
+ newCommands.splice(index, 1);
1257
+ handleConfigUpdate({
1258
+ commandsAutoRenderMimeBundles: newCommands
1259
+ });
1260
+ }}
1261
+ size="small"
1262
+ >
1263
+ <Delete />
1264
+ </IconButton>
1265
+ }
1266
+ >
1267
+ <ListItemText primary={command} />
1268
+ </ListItem>
1269
+ )
1270
+ )}
1271
+ </List>
1272
+
1273
+ <TextField
1274
+ fullWidth
1275
+ label={trans.__('Add Auto-Render Command')}
1276
+ placeholder={trans.__(
1277
+ 'e.g., jupyterlab-ai-commands:execute-in-kernel'
1278
+ )}
1279
+ onKeyDown={e => {
1280
+ if (e.key === 'Enter') {
1281
+ const value = (
1282
+ e.target as HTMLInputElement
1283
+ ).value.trim();
1284
+ const existingCommands =
1285
+ config.commandsAutoRenderMimeBundles ?? [];
1286
+ if (value && !existingCommands.includes(value)) {
1287
+ const newCommands = [...existingCommands, value];
1288
+ handleConfigUpdate({
1289
+ commandsAutoRenderMimeBundles: newCommands
1290
+ });
1291
+ (e.target as HTMLInputElement).value = '';
1292
+ }
1293
+ }
1294
+ }}
1295
+ helperText={trans.__(
1296
+ 'Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel'
1297
+ )}
1298
+ />
1299
+ </Box>
1300
+
1301
+ <Divider sx={{ my: 2 }} />
1302
+
1303
+ <Box>
1304
+ <Typography variant="body1" gutterBottom>
1305
+ {trans.__('Trusted MIME Types for Auto-Render')}
1306
+ </Typography>
1307
+ <Typography
1308
+ variant="caption"
1309
+ color="text.secondary"
1310
+ gutterBottom
1311
+ sx={{ display: 'block' }}
1312
+ >
1313
+ {trans.__(
1314
+ 'When auto-rendering command outputs, these MIME types are marked trusted in chat'
1315
+ )}
1316
+ </Typography>
1317
+
1318
+ <List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
1319
+ {(config.trustedMimeTypesForAutoRender ?? []).map(
1320
+ (mimeType, index) => (
1321
+ <ListItem
1322
+ key={index}
1323
+ divider
1324
+ secondaryAction={
1325
+ <IconButton
1326
+ onClick={() => {
1327
+ const newMimeTypes = [
1328
+ ...(config.trustedMimeTypesForAutoRender ??
1329
+ [])
1330
+ ];
1331
+ newMimeTypes.splice(index, 1);
1332
+ handleConfigUpdate({
1333
+ trustedMimeTypesForAutoRender: newMimeTypes
1334
+ });
1335
+ }}
1336
+ size="small"
1337
+ >
1338
+ <Delete />
1339
+ </IconButton>
1340
+ }
1341
+ >
1342
+ <ListItemText primary={mimeType} />
1343
+ </ListItem>
1344
+ )
1345
+ )}
1346
+ </List>
1347
+
1348
+ <TextField
1349
+ fullWidth
1350
+ label={trans.__('Add Trusted MIME Type')}
1351
+ placeholder={trans.__('e.g., text/html')}
1352
+ onKeyDown={e => {
1353
+ if (e.key === 'Enter') {
1354
+ const value = (
1355
+ e.target as HTMLInputElement
1356
+ ).value.trim();
1357
+ const existingMimeTypes =
1358
+ config.trustedMimeTypesForAutoRender ?? [];
1359
+ if (value && !existingMimeTypes.includes(value)) {
1360
+ const newMimeTypes = [...existingMimeTypes, value];
1361
+ handleConfigUpdate({
1362
+ trustedMimeTypesForAutoRender: newMimeTypes
1363
+ });
1364
+ (e.target as HTMLInputElement).value = '';
1365
+ }
1366
+ }
1367
+ }}
1368
+ helperText={trans.__(
1369
+ 'Press Enter to add a MIME type. Default: text/html'
1370
+ )}
1371
+ />
1372
+ </Box>
1171
1373
  </Box>
1172
1374
  </CardContent>
1173
1375
  </Card>
@@ -1215,7 +1417,18 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1215
1417
  ) : (
1216
1418
  <List>
1217
1419
  {config.mcpServers.map(server => (
1218
- <ListItem key={server.id} divider>
1420
+ <ListItem
1421
+ key={server.id}
1422
+ divider
1423
+ secondaryAction={
1424
+ <IconButton
1425
+ onClick={e => handleMCPMenuClick(e, server.id)}
1426
+ size="small"
1427
+ >
1428
+ <MoreVert />
1429
+ </IconButton>
1430
+ }
1431
+ >
1219
1432
  <ListItemText
1220
1433
  primary={
1221
1434
  <Box
@@ -1279,14 +1492,6 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1279
1492
  </Box>
1280
1493
  }
1281
1494
  />
1282
- <ListItemSecondaryAction>
1283
- <IconButton
1284
- onClick={e => handleMCPMenuClick(e, server.id)}
1285
- size="small"
1286
- >
1287
- <MoreVert />
1288
- </IconButton>
1289
- </ListItemSecondaryAction>
1290
1495
  </ListItem>
1291
1496
  ))}
1292
1497
  </List>
@@ -1517,7 +1722,7 @@ export namespace AISettingsWidget {
1517
1722
  /**
1518
1723
  * The token used to request the secrets manager.
1519
1724
  */
1520
- token: symbol;
1725
+ token: symbol | null;
1521
1726
  /**
1522
1727
  * The application language translation bundle.
1523
1728
  */
@@ -1529,11 +1734,11 @@ namespace Private {
1529
1734
  /**
1530
1735
  * The token to use with the secrets manager, setter and getter.
1531
1736
  */
1532
- let secretsToken: symbol;
1533
- export function setToken(value: symbol): void {
1737
+ let secretsToken: symbol | null;
1738
+ export function setToken(value: symbol | null): void {
1534
1739
  secretsToken = value;
1535
1740
  }
1536
- export function getToken(): symbol {
1741
+ export function getToken(): symbol | null {
1537
1742
  return secretsToken;
1538
1743
  }
1539
1744
  }
@@ -1,4 +1,4 @@
1
- import { ChatWidget } from '@jupyter/chat';
1
+ import { ChatWidget, IChatModel } from '@jupyter/chat';
2
2
  import { CommandToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
3
3
  import { launchIcon } from '@jupyterlab/ui-components';
4
4
  import type { TranslationBundle } from '@jupyterlab/translation';
@@ -8,6 +8,7 @@ import { ApprovalButtons } from '../approval-buttons';
8
8
  import { AIChatModel } from '../chat-model';
9
9
  import { TokenUsageWidget } from '../components/token-usage-display';
10
10
  import { AISettingsModel } from '../models/settings-model';
11
+ import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
11
12
  import { CommandIds } from '../tokens';
12
13
 
13
14
  export namespace MainAreaChat {
@@ -56,12 +57,21 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
56
57
  chatPanel: this.content,
57
58
  agentManager: this.model.agentManager
58
59
  });
60
+ // Temporary compat: keep output-area CSS context for MIME renderers
61
+ // until jupyter-chat provides it natively.
62
+ this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
63
+ chatPanel: this.content
64
+ });
65
+
66
+ this.model.writersChanged.connect(this._writersChanged);
59
67
  }
60
68
 
61
69
  dispose(): void {
62
70
  super.dispose();
63
71
  // Dispose of the approval buttons widget when the chat is disposed.
64
72
  this._approvalButtons.dispose();
73
+ this._outputAreaCompat.dispose();
74
+ this.model.writersChanged.disconnect(this._writersChanged);
65
75
  }
66
76
 
67
77
  /**
@@ -71,5 +81,28 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
71
81
  return this.content.model as AIChatModel;
72
82
  }
73
83
 
84
+ /**
85
+ * Get the area of the chat.
86
+ */
87
+ get area(): string | undefined {
88
+ return this.content.area;
89
+ }
90
+
91
+ private _writersChanged = (_: IChatModel, writers: IChatModel.IWriter[]) => {
92
+ // Check if AI is currently writing (streaming)
93
+ const aiWriting = writers.some(
94
+ writer => writer.user.username === 'ai-assistant'
95
+ );
96
+
97
+ if (aiWriting) {
98
+ this.content.inputToolbarRegistry?.hide('send');
99
+ this.content.inputToolbarRegistry?.show('stop');
100
+ } else {
101
+ this.content.inputToolbarRegistry?.hide('stop');
102
+ this.content.inputToolbarRegistry?.show('send');
103
+ }
104
+ };
105
+
74
106
  private _approvalButtons: ApprovalButtons;
107
+ private _outputAreaCompat: RenderedMessageOutputAreaCompat;
75
108
  }