@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.
- package/lib/agent.d.ts +24 -2
- package/lib/agent.js +161 -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 +128 -66
- package/lib/models/settings-model.d.ts +2 -0
- package/lib/models/settings-model.js +2 -0
- package/lib/providers/built-in-providers.js +7 -0
- 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 +44 -7
- package/lib/tokens.js +1 -1
- package/lib/tools/commands.js +4 -2
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +1 -1
- package/lib/widgets/ai-settings.js +125 -38
- 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 +207 -4
- package/package.json +10 -4
- package/schema/settings-model.json +89 -1
- package/src/agent.ts +220 -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 +153 -82
- package/src/models/settings-model.ts +6 -0
- package/src/providers/built-in-providers.ts +7 -0
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/tokens.ts +53 -9
- package/src/tools/commands.ts +4 -2
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +282 -77
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +496 -3
package/src/chat-model.ts
CHANGED
|
@@ -3,19 +3,26 @@ import {
|
|
|
3
3
|
IActiveCellManager,
|
|
4
4
|
IAttachment,
|
|
5
5
|
IChatContext,
|
|
6
|
+
IMessage,
|
|
6
7
|
IMessageContent,
|
|
7
8
|
INewMessage,
|
|
8
9
|
IUser
|
|
9
10
|
} from '@jupyter/chat';
|
|
10
11
|
|
|
12
|
+
import { YNotebook } from '@jupyter/ydoc';
|
|
13
|
+
|
|
11
14
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
12
15
|
|
|
13
16
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
14
17
|
|
|
15
18
|
import { IDocumentWidget } from '@jupyterlab/docregistry';
|
|
16
19
|
|
|
20
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
21
|
+
|
|
17
22
|
import { INotebookModel, Notebook } from '@jupyterlab/notebook';
|
|
18
23
|
|
|
24
|
+
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
25
|
+
|
|
19
26
|
import { TranslationBundle } from '@jupyterlab/translation';
|
|
20
27
|
|
|
21
28
|
import { UUID } from '@lumino/coreutils';
|
|
@@ -30,10 +37,6 @@ import { AISettingsModel } from './models/settings-model';
|
|
|
30
37
|
|
|
31
38
|
import { ITokenUsage } from './tokens';
|
|
32
39
|
|
|
33
|
-
import { YNotebook } from '@jupyter/ydoc';
|
|
34
|
-
|
|
35
|
-
import * as nbformat from '@jupyterlab/nbformat';
|
|
36
|
-
|
|
37
40
|
/**
|
|
38
41
|
* Tool call status types.
|
|
39
42
|
*/
|
|
@@ -77,6 +80,10 @@ interface IToolExecutionContext {
|
|
|
77
80
|
* Human-readable summary extracted from tool input for display.
|
|
78
81
|
*/
|
|
79
82
|
summary?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Whether this tool call should auto-render trusted MIME bundles on completion.
|
|
85
|
+
*/
|
|
86
|
+
shouldAutoRenderMimeBundles?: boolean;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
|
@@ -334,8 +341,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
334
341
|
type: 'msg',
|
|
335
342
|
raw_time: false
|
|
336
343
|
};
|
|
337
|
-
this._currentStreamingMessage = aiMessage;
|
|
338
344
|
this.messageAdded(aiMessage);
|
|
345
|
+
this._currentStreamingMessage =
|
|
346
|
+
this.messages.find(message => message.id === aiMessage.id) ?? null;
|
|
339
347
|
}
|
|
340
348
|
|
|
341
349
|
/**
|
|
@@ -347,8 +355,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
347
355
|
this._currentStreamingMessage &&
|
|
348
356
|
this._currentStreamingMessage.id === event.data.messageId
|
|
349
357
|
) {
|
|
350
|
-
this._currentStreamingMessage.body
|
|
351
|
-
this.messageAdded(this._currentStreamingMessage);
|
|
358
|
+
this._currentStreamingMessage.update({ body: event.data.fullContent });
|
|
352
359
|
}
|
|
353
360
|
}
|
|
354
361
|
|
|
@@ -361,8 +368,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
361
368
|
this._currentStreamingMessage &&
|
|
362
369
|
this._currentStreamingMessage.id === event.data.messageId
|
|
363
370
|
) {
|
|
364
|
-
this._currentStreamingMessage.body
|
|
365
|
-
this.messageAdded(this._currentStreamingMessage);
|
|
371
|
+
this._currentStreamingMessage.update({ body: event.data.content });
|
|
366
372
|
this._currentStreamingMessage = null;
|
|
367
373
|
}
|
|
368
374
|
}
|
|
@@ -401,6 +407,21 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
401
407
|
return parsedInput.name;
|
|
402
408
|
}
|
|
403
409
|
break;
|
|
410
|
+
case 'browser_fetch':
|
|
411
|
+
if (parsedInput.url) {
|
|
412
|
+
return parsedInput.url;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case 'web_fetch':
|
|
416
|
+
if (parsedInput.url) {
|
|
417
|
+
return parsedInput.url;
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case 'web_search':
|
|
421
|
+
if (parsedInput.query) {
|
|
422
|
+
return `query: "${parsedInput.query}"`;
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
404
425
|
}
|
|
405
426
|
} catch {
|
|
406
427
|
// If parsing fails, return empty string
|
|
@@ -408,6 +429,30 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
408
429
|
return '';
|
|
409
430
|
}
|
|
410
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Determine whether this tool call should auto-render trusted MIME bundles.
|
|
434
|
+
*/
|
|
435
|
+
private _computeShouldAutoRenderMimeBundles(
|
|
436
|
+
toolName: string,
|
|
437
|
+
input: string
|
|
438
|
+
): boolean {
|
|
439
|
+
if (toolName !== 'execute_command') {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const parsedInput = JSON.parse(input);
|
|
445
|
+
return (
|
|
446
|
+
typeof parsedInput.commandId === 'string' &&
|
|
447
|
+
this._settingsModel.config.commandsAutoRenderMimeBundles.includes(
|
|
448
|
+
parsedInput.commandId
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
411
456
|
/**
|
|
412
457
|
* Handles the start of a tool call execution.
|
|
413
458
|
* @param event Event containing the tool call start data
|
|
@@ -420,13 +465,19 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
420
465
|
event.data.toolName,
|
|
421
466
|
event.data.input
|
|
422
467
|
);
|
|
468
|
+
const shouldAutoRenderMimeBundles =
|
|
469
|
+
this._computeShouldAutoRenderMimeBundles(
|
|
470
|
+
event.data.toolName,
|
|
471
|
+
event.data.input
|
|
472
|
+
);
|
|
423
473
|
const context: IToolExecutionContext = {
|
|
424
474
|
toolCallId: event.data.callId,
|
|
425
475
|
messageId,
|
|
426
476
|
toolName: event.data.toolName,
|
|
427
477
|
input: event.data.input,
|
|
428
478
|
status: 'pending',
|
|
429
|
-
summary
|
|
479
|
+
summary,
|
|
480
|
+
shouldAutoRenderMimeBundles
|
|
430
481
|
};
|
|
431
482
|
|
|
432
483
|
this._toolContexts.set(event.data.callId, context);
|
|
@@ -455,11 +506,53 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
455
506
|
private _handleToolCallCompleteEvent(
|
|
456
507
|
event: IAgentEvent<'tool_call_complete'>
|
|
457
508
|
): void {
|
|
509
|
+
const context = this._toolContexts.get(event.data.callId);
|
|
458
510
|
const status = event.data.isError ? 'error' : 'completed';
|
|
459
|
-
this._updateToolCallUI(
|
|
511
|
+
this._updateToolCallUI(
|
|
512
|
+
event.data.callId,
|
|
513
|
+
status,
|
|
514
|
+
Private.formatToolOutput(event.data.outputData)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (!event.data.isError && this._shouldAutoRenderMimeBundles(context)) {
|
|
518
|
+
// Tool results are arbitrary command payloads (often wrapped in
|
|
519
|
+
// { success, result, outputs, ... }), so extract display outputs
|
|
520
|
+
// defensively instead of assuming a raw kernel message shape.
|
|
521
|
+
const mimeBundles = Private.extractMimeBundlesFromUnknown(
|
|
522
|
+
event.data.outputData,
|
|
523
|
+
{
|
|
524
|
+
trustedMimeTypes:
|
|
525
|
+
this._settingsModel.config.trustedMimeTypesForAutoRender
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
for (const bundle of mimeBundles) {
|
|
529
|
+
this.messageAdded({
|
|
530
|
+
body: bundle,
|
|
531
|
+
sender: this._getAIUser(),
|
|
532
|
+
id: UUID.uuid4(),
|
|
533
|
+
time: Date.now() / 1000,
|
|
534
|
+
type: 'msg',
|
|
535
|
+
raw_time: false
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
460
540
|
this._toolContexts.delete(event.data.callId);
|
|
461
541
|
}
|
|
462
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Determine whether a tool call output should auto-render MIME bundles.
|
|
545
|
+
*/
|
|
546
|
+
private _shouldAutoRenderMimeBundles(
|
|
547
|
+
context: IToolExecutionContext | undefined
|
|
548
|
+
): boolean {
|
|
549
|
+
if (!context) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return !!context.shouldAutoRenderMimeBundles;
|
|
554
|
+
}
|
|
555
|
+
|
|
463
556
|
/**
|
|
464
557
|
* Handles error events from the AI agent.
|
|
465
558
|
*/
|
|
@@ -827,12 +920,119 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
827
920
|
private _user: IUser;
|
|
828
921
|
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
829
922
|
private _agentManager: AgentManager;
|
|
830
|
-
private _currentStreamingMessage:
|
|
923
|
+
private _currentStreamingMessage: IMessage | null = null;
|
|
831
924
|
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
832
925
|
private _trans: TranslationBundle;
|
|
833
926
|
}
|
|
834
927
|
|
|
835
928
|
namespace Private {
|
|
929
|
+
type IMimeBody = Partial<IRenderMime.IMimeModel> &
|
|
930
|
+
Pick<IRenderMime.IMimeModel, 'data'>;
|
|
931
|
+
type IDisplayOutput =
|
|
932
|
+
| nbformat.IDisplayData
|
|
933
|
+
| nbformat.IDisplayUpdate
|
|
934
|
+
| nbformat.IExecuteResult;
|
|
935
|
+
|
|
936
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
937
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
|
|
941
|
+
if (!isPlainObject(value)) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const output = value as nbformat.IOutput;
|
|
946
|
+
return (
|
|
947
|
+
nbformat.isDisplayData(output) ||
|
|
948
|
+
nbformat.isDisplayUpdate(output) ||
|
|
949
|
+
nbformat.isExecuteResult(output)
|
|
950
|
+
);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const toMimeBundle = (
|
|
954
|
+
value: IDisplayOutput,
|
|
955
|
+
trustedMimeTypes: ReadonlySet<string>
|
|
956
|
+
): IMimeBody | null => {
|
|
957
|
+
const data = value.data;
|
|
958
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return {
|
|
963
|
+
data: data as IRenderMime.IMimeModel['data'],
|
|
964
|
+
...(isPlainObject(value.metadata)
|
|
965
|
+
? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
|
|
966
|
+
: {}),
|
|
967
|
+
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
968
|
+
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
969
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
970
|
+
? { trusted: true }
|
|
971
|
+
: {})
|
|
972
|
+
};
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
977
|
+
*
|
|
978
|
+
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
979
|
+
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
980
|
+
*/
|
|
981
|
+
const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
|
|
982
|
+
if (isDisplayOutput(value)) {
|
|
983
|
+
return [value];
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (Array.isArray(value)) {
|
|
987
|
+
return value.filter(isDisplayOutput);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (!isPlainObject(value)) {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (Array.isArray(value.outputs)) {
|
|
995
|
+
return value.outputs.filter(isDisplayOutput);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if ('result' in value) {
|
|
999
|
+
return toDisplayOutputs(value.result);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return [];
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
1007
|
+
*/
|
|
1008
|
+
export function extractMimeBundlesFromUnknown(
|
|
1009
|
+
content: unknown,
|
|
1010
|
+
options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
|
|
1011
|
+
): IMimeBody[] {
|
|
1012
|
+
const bundles: IMimeBody[] = [];
|
|
1013
|
+
const outputs = toDisplayOutputs(content);
|
|
1014
|
+
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1015
|
+
for (const output of outputs) {
|
|
1016
|
+
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1017
|
+
if (bundle) {
|
|
1018
|
+
bundles.push(bundle);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return bundles;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export function formatToolOutput(outputData: unknown): string {
|
|
1025
|
+
if (typeof outputData === 'string') {
|
|
1026
|
+
return outputData;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
return JSON.stringify(outputData, null, 2);
|
|
1031
|
+
} catch {
|
|
1032
|
+
return '[Complex object - cannot serialize]';
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
836
1036
|
export function escapeHtml(value: string): string {
|
|
837
1037
|
// Prefer the same native escaping approach used in JupyterLab itself
|
|
838
1038
|
// (e.g. `@jupyterlab/completer`).
|
|
@@ -927,7 +1127,9 @@ namespace Private {
|
|
|
927
1127
|
/**
|
|
928
1128
|
* Builds HTML for a tool call display.
|
|
929
1129
|
*/
|
|
930
|
-
export function buildToolCallHtml(
|
|
1130
|
+
export function buildToolCallHtml(
|
|
1131
|
+
options: IToolCallHtmlOptions
|
|
1132
|
+
): Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'> {
|
|
931
1133
|
const { toolName, input, status, summary, output, approvalId, trans } =
|
|
932
1134
|
options;
|
|
933
1135
|
const config = STATUS_CONFIG[status];
|
|
@@ -965,7 +1167,7 @@ namespace Private {
|
|
|
965
1167
|
</div>`;
|
|
966
1168
|
}
|
|
967
1169
|
|
|
968
|
-
|
|
1170
|
+
const HTMLContent = `<details class="jp-ai-tool-call ${config.cssClass}"${openAttr}>
|
|
969
1171
|
<summary class="jp-ai-tool-header">
|
|
970
1172
|
<div class="jp-ai-tool-icon">⚡</div>
|
|
971
1173
|
<div class="jp-ai-tool-title">${escapedToolName}${summaryHtml}</div>
|
|
@@ -974,6 +1176,13 @@ namespace Private {
|
|
|
974
1176
|
<div class="jp-ai-tool-body">${bodyContent}
|
|
975
1177
|
</div>
|
|
976
1178
|
</details>`;
|
|
1179
|
+
|
|
1180
|
+
return {
|
|
1181
|
+
trusted: true,
|
|
1182
|
+
data: {
|
|
1183
|
+
'text/html': HTMLContent
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
977
1186
|
}
|
|
978
1187
|
}
|
|
979
1188
|
|
|
@@ -53,6 +53,11 @@ export class AICompletionProvider implements IInlineCompletionProvider {
|
|
|
53
53
|
this._updateModel();
|
|
54
54
|
});
|
|
55
55
|
this._updateModel();
|
|
56
|
+
|
|
57
|
+
// Disable the secrets manager if the token is empty.
|
|
58
|
+
if (!options.token) {
|
|
59
|
+
this._secretsManager = undefined;
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
@@ -163,14 +168,23 @@ export class AICompletionProvider implements IInlineCompletionProvider {
|
|
|
163
168
|
|
|
164
169
|
let apiKey: string;
|
|
165
170
|
if (this._secretsManager && this._settingsModel.config.useSecretsManager) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
const token = Private.getToken();
|
|
172
|
+
if (!token) {
|
|
173
|
+
// This should never happen, the secrets manager should be disabled.
|
|
174
|
+
console.error(
|
|
175
|
+
'@jupyterlite/ai::AICompletionProvider error: the settings manager token is not set.\nYou should disable the secrets manager from the AI settings.'
|
|
176
|
+
);
|
|
177
|
+
apiKey = '';
|
|
178
|
+
} else {
|
|
179
|
+
apiKey =
|
|
180
|
+
(
|
|
181
|
+
await this._secretsManager.get(
|
|
182
|
+
token,
|
|
183
|
+
SECRETS_NAMESPACE,
|
|
184
|
+
`${provider}:apiKey`
|
|
185
|
+
)
|
|
186
|
+
)?.value ?? '';
|
|
187
|
+
}
|
|
174
188
|
} else {
|
|
175
189
|
apiKey = this._settingsModel.getApiKey(activeProvider.id);
|
|
176
190
|
}
|
|
@@ -316,7 +330,7 @@ export namespace AICompletionProvider {
|
|
|
316
330
|
/**
|
|
317
331
|
* The token used to request the secrets manager.
|
|
318
332
|
*/
|
|
319
|
-
token: symbol;
|
|
333
|
+
token: symbol | null;
|
|
320
334
|
}
|
|
321
335
|
}
|
|
322
336
|
|
|
@@ -324,11 +338,11 @@ namespace Private {
|
|
|
324
338
|
/**
|
|
325
339
|
* The token to use with the secrets manager, setter and getter.
|
|
326
340
|
*/
|
|
327
|
-
let secretsToken: symbol;
|
|
328
|
-
export function setToken(value: symbol): void {
|
|
341
|
+
let secretsToken: symbol | null;
|
|
342
|
+
export function setToken(value: symbol | null): void {
|
|
329
343
|
secretsToken = value;
|
|
330
344
|
}
|
|
331
|
-
export function getToken(): symbol {
|
|
345
|
+
export function getToken(): symbol | null {
|
|
332
346
|
return secretsToken;
|
|
333
347
|
}
|
|
334
348
|
}
|
|
@@ -5,7 +5,6 @@ import { Menu, MenuItem, Typography } from '@mui/material';
|
|
|
5
5
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { AIChatModel } from '../chat-model';
|
|
7
7
|
import { AISettingsModel } from '../models/settings-model';
|
|
8
|
-
|
|
9
8
|
/**
|
|
10
9
|
* Properties for the model select component.
|
|
11
10
|
*/
|
|
@@ -199,11 +198,11 @@ export function ModelSelect(props: IModelSelectProps): JSX.Element {
|
|
|
199
198
|
}}
|
|
200
199
|
sx={{
|
|
201
200
|
backgroundColor: isSelected
|
|
202
|
-
? 'var(--
|
|
201
|
+
? 'var(--mui-palette-primary-main)'
|
|
203
202
|
: 'transparent',
|
|
204
203
|
'&:hover': {
|
|
205
204
|
backgroundColor: isSelected
|
|
206
|
-
? 'var(--
|
|
205
|
+
? 'var(--mui-palette-primary-main)'
|
|
207
206
|
: 'var(--jp-layout-color1)'
|
|
208
207
|
},
|
|
209
208
|
display: 'flex',
|
|
@@ -214,7 +213,7 @@ export function ModelSelect(props: IModelSelectProps): JSX.Element {
|
|
|
214
213
|
{isSelected ? (
|
|
215
214
|
<CheckIcon
|
|
216
215
|
sx={{
|
|
217
|
-
color: 'var(--jp-
|
|
216
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
218
217
|
fontSize: 16
|
|
219
218
|
}}
|
|
220
219
|
/>
|
|
@@ -227,7 +226,7 @@ export function ModelSelect(props: IModelSelectProps): JSX.Element {
|
|
|
227
226
|
sx={{
|
|
228
227
|
fontWeight: isSelected ? 600 : 400,
|
|
229
228
|
color: isSelected
|
|
230
|
-
? 'var(--jp-
|
|
229
|
+
? 'var(--jp-ui-inverse-font-color1)'
|
|
231
230
|
: 'inherit'
|
|
232
231
|
}}
|
|
233
232
|
>
|
|
@@ -6,12 +6,14 @@ import BuildIcon from '@mui/icons-material/Build';
|
|
|
6
6
|
|
|
7
7
|
import CheckIcon from '@mui/icons-material/Check';
|
|
8
8
|
|
|
9
|
-
import { Menu, MenuItem, Tooltip, Typography } from '@mui/material';
|
|
9
|
+
import { Divider, Menu, MenuItem, Tooltip, Typography } from '@mui/material';
|
|
10
10
|
|
|
11
11
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
12
12
|
|
|
13
|
-
import { INamedTool, IToolRegistry } from '../tokens';
|
|
13
|
+
import { INamedTool, IProviderRegistry, IToolRegistry } from '../tokens';
|
|
14
14
|
import { AIChatModel } from '../chat-model';
|
|
15
|
+
import { AISettingsModel } from '../models/settings-model';
|
|
16
|
+
import { createProviderTools } from '../providers/provider-tools';
|
|
15
17
|
|
|
16
18
|
const SELECT_ITEM_CLASS = 'jp-AIToolSelect-item';
|
|
17
19
|
|
|
@@ -35,6 +37,16 @@ export interface IToolSelectProps
|
|
|
35
37
|
*/
|
|
36
38
|
onToolSelectionChange: (selectedToolNames: string[]) => void;
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The settings model to compute provider-level web tools.
|
|
42
|
+
*/
|
|
43
|
+
settingsModel: AISettingsModel;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registry for provider metadata used to resolve provider tool capabilities.
|
|
47
|
+
*/
|
|
48
|
+
providerRegistry: IProviderRegistry;
|
|
49
|
+
|
|
38
50
|
/**
|
|
39
51
|
* The application language translator.
|
|
40
52
|
*/
|
|
@@ -49,13 +61,19 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
49
61
|
toolRegistry,
|
|
50
62
|
onToolSelectionChange,
|
|
51
63
|
toolsEnabled,
|
|
64
|
+
settingsModel,
|
|
65
|
+
providerRegistry,
|
|
66
|
+
model,
|
|
52
67
|
translator: trans
|
|
53
68
|
} = props;
|
|
69
|
+
const chatContext = model.chatContext as AIChatModel.IAIChatContext;
|
|
70
|
+
const agentManager = chatContext.agentManager;
|
|
54
71
|
|
|
55
72
|
const [selectedToolNames, setSelectedToolNames] = useState<string[]>([]);
|
|
56
73
|
const [tools, setTools] = useState<INamedTool[]>(
|
|
57
74
|
toolRegistry?.namedTools || []
|
|
58
75
|
);
|
|
76
|
+
const [providerToolNames, setProviderToolNames] = useState<string[]>([]);
|
|
59
77
|
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
|
|
60
78
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
61
79
|
|
|
@@ -103,6 +121,48 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
103
121
|
}
|
|
104
122
|
}, [toolRegistry]);
|
|
105
123
|
|
|
124
|
+
// Track provider-level tools (e.g. web_search/web_fetch).
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!agentManager || !toolsEnabled) {
|
|
127
|
+
setProviderToolNames([]);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const updateProviderTools = () => {
|
|
132
|
+
const activeProviderId = agentManager.activeProvider;
|
|
133
|
+
const providerConfig = settingsModel.getProvider(activeProviderId);
|
|
134
|
+
if (!providerConfig) {
|
|
135
|
+
setProviderToolNames([]);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const providerInfo = providerRegistry.getProviderInfo(
|
|
140
|
+
providerConfig.provider
|
|
141
|
+
);
|
|
142
|
+
const providerTools = createProviderTools({
|
|
143
|
+
providerInfo,
|
|
144
|
+
customSettings: providerConfig.customSettings,
|
|
145
|
+
hasFunctionTools: selectedToolNames.length > 0
|
|
146
|
+
});
|
|
147
|
+
setProviderToolNames(Object.keys(providerTools));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
updateProviderTools();
|
|
151
|
+
settingsModel.stateChanged.connect(updateProviderTools);
|
|
152
|
+
agentManager.activeProviderChanged.connect(updateProviderTools);
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
settingsModel.stateChanged.disconnect(updateProviderTools);
|
|
156
|
+
agentManager.activeProviderChanged.disconnect(updateProviderTools);
|
|
157
|
+
};
|
|
158
|
+
}, [
|
|
159
|
+
settingsModel,
|
|
160
|
+
providerRegistry,
|
|
161
|
+
agentManager,
|
|
162
|
+
selectedToolNames.length,
|
|
163
|
+
toolsEnabled
|
|
164
|
+
]);
|
|
165
|
+
|
|
106
166
|
// Initialize selected tools to all tools by default
|
|
107
167
|
useEffect(() => {
|
|
108
168
|
if (tools.length > 0 && selectedToolNames.length === 0) {
|
|
@@ -113,10 +173,13 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
113
173
|
}, [tools, selectedToolNames.length, onToolSelectionChange]);
|
|
114
174
|
|
|
115
175
|
// Don't render if tools are disabled or no tools available
|
|
116
|
-
if (!toolsEnabled || tools.length === 0) {
|
|
176
|
+
if (!toolsEnabled || (tools.length === 0 && providerToolNames.length === 0)) {
|
|
117
177
|
return <></>;
|
|
118
178
|
}
|
|
119
179
|
|
|
180
|
+
const selectedCount = selectedToolNames.length + providerToolNames.length;
|
|
181
|
+
const totalCount = tools.length + providerToolNames.length;
|
|
182
|
+
|
|
120
183
|
return (
|
|
121
184
|
<>
|
|
122
185
|
<TooltippedButton
|
|
@@ -125,11 +188,11 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
125
188
|
}}
|
|
126
189
|
tooltip={trans.__(
|
|
127
190
|
'Tools (%1/%2 selected)',
|
|
128
|
-
|
|
129
|
-
|
|
191
|
+
selectedCount.toString(),
|
|
192
|
+
totalCount.toString()
|
|
130
193
|
)}
|
|
131
194
|
buttonProps={{
|
|
132
|
-
...(
|
|
195
|
+
...(selectedCount === 0 && {
|
|
133
196
|
variant: 'outlined'
|
|
134
197
|
}),
|
|
135
198
|
title: trans.__('Select AI Tools'),
|
|
@@ -143,7 +206,7 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
143
206
|
}
|
|
144
207
|
}}
|
|
145
208
|
sx={
|
|
146
|
-
|
|
209
|
+
selectedCount === 0
|
|
147
210
|
? { backgroundColor: 'var(--jp-layout-color3)' }
|
|
148
211
|
: {}
|
|
149
212
|
}
|
|
@@ -198,6 +261,42 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
198
261
|
</MenuItem>
|
|
199
262
|
</Tooltip>
|
|
200
263
|
))}
|
|
264
|
+
|
|
265
|
+
{providerToolNames.length > 0 && tools.length > 0 && <Divider />}
|
|
266
|
+
|
|
267
|
+
{providerToolNames.length > 0 && (
|
|
268
|
+
<MenuItem disabled>
|
|
269
|
+
<Typography variant="caption">
|
|
270
|
+
{trans.__('Provider Tools')}
|
|
271
|
+
</Typography>
|
|
272
|
+
</MenuItem>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{providerToolNames.map(toolName => {
|
|
276
|
+
return (
|
|
277
|
+
<Tooltip
|
|
278
|
+
key={toolName}
|
|
279
|
+
title={trans.__('Enabled via provider settings.')}
|
|
280
|
+
placement="left"
|
|
281
|
+
>
|
|
282
|
+
<MenuItem
|
|
283
|
+
className={SELECT_ITEM_CLASS}
|
|
284
|
+
onClick={e => {
|
|
285
|
+
// Keep provider-managed tools read-only from this menu.
|
|
286
|
+
e.stopPropagation();
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<CheckIcon
|
|
290
|
+
sx={{
|
|
291
|
+
marginRight: '8px',
|
|
292
|
+
color: 'text.disabled'
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
<Typography variant="body2">{toolName}</Typography>
|
|
296
|
+
</MenuItem>
|
|
297
|
+
</Tooltip>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
201
300
|
</Menu>
|
|
202
301
|
</>
|
|
203
302
|
);
|
|
@@ -208,6 +307,8 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
|
|
|
208
307
|
*/
|
|
209
308
|
export function createToolSelectItem(
|
|
210
309
|
toolRegistry: IToolRegistry,
|
|
310
|
+
settingsModel: AISettingsModel,
|
|
311
|
+
providerRegistry: IProviderRegistry,
|
|
211
312
|
toolsEnabled: boolean = true,
|
|
212
313
|
translator: TranslationBundle
|
|
213
314
|
): InputToolbarRegistry.IToolbarItem {
|
|
@@ -225,6 +326,8 @@ export function createToolSelectItem(
|
|
|
225
326
|
const toolSelectProps: IToolSelectProps = {
|
|
226
327
|
...props,
|
|
227
328
|
toolRegistry,
|
|
329
|
+
settingsModel,
|
|
330
|
+
providerRegistry,
|
|
228
331
|
onToolSelectionChange,
|
|
229
332
|
toolsEnabled,
|
|
230
333
|
translator
|