@jupyterlite/ai 0.9.1 → 0.11.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/README.md +5 -214
- package/lib/agent.d.ts +58 -66
- package/lib/agent.js +291 -310
- package/lib/approval-buttons.d.ts +19 -82
- package/lib/approval-buttons.js +36 -289
- package/lib/chat-model-registry.d.ts +6 -0
- package/lib/chat-model-registry.js +4 -1
- package/lib/chat-model.d.ts +26 -54
- package/lib/chat-model.js +277 -303
- package/lib/components/clear-button.d.ts +6 -1
- package/lib/components/clear-button.js +10 -6
- package/lib/components/completion-status.d.ts +5 -0
- package/lib/components/completion-status.js +5 -4
- package/lib/components/model-select.d.ts +6 -1
- package/lib/components/model-select.js +13 -16
- package/lib/components/stop-button.d.ts +6 -1
- package/lib/components/stop-button.js +12 -8
- package/lib/components/token-usage-display.d.ts +5 -0
- package/lib/components/token-usage-display.js +2 -2
- package/lib/components/tool-select.d.ts +6 -1
- package/lib/components/tool-select.js +10 -9
- package/lib/index.d.ts +1 -0
- package/lib/index.js +61 -81
- package/lib/models/settings-model.d.ts +1 -1
- package/lib/models/settings-model.js +40 -26
- package/lib/providers/built-in-providers.js +38 -19
- package/lib/providers/models.d.ts +3 -3
- package/lib/providers/provider-registry.d.ts +3 -4
- package/lib/providers/provider-registry.js +1 -4
- package/lib/tokens.d.ts +5 -6
- package/lib/tools/commands.d.ts +2 -1
- package/lib/tools/commands.js +36 -49
- package/lib/widgets/ai-settings.d.ts +6 -0
- package/lib/widgets/ai-settings.js +72 -71
- package/lib/widgets/main-area-chat.d.ts +2 -0
- package/lib/widgets/main-area-chat.js +5 -2
- package/lib/widgets/provider-config-dialog.d.ts +2 -0
- package/lib/widgets/provider-config-dialog.js +34 -34
- package/package.json +13 -13
- package/schema/settings-model.json +3 -2
- package/src/agent.ts +360 -372
- package/src/approval-buttons.ts +43 -389
- package/src/chat-model-registry.ts +9 -1
- package/src/chat-model.ts +399 -370
- package/src/completion/completion-provider.ts +2 -3
- package/src/components/clear-button.tsx +18 -6
- package/src/components/completion-status.tsx +18 -4
- package/src/components/model-select.tsx +25 -16
- package/src/components/stop-button.tsx +22 -9
- package/src/components/token-usage-display.tsx +14 -2
- package/src/components/tool-select.tsx +27 -9
- package/src/index.ts +78 -134
- package/src/models/settings-model.ts +41 -27
- package/src/providers/built-in-providers.ts +38 -19
- package/src/providers/models.ts +3 -3
- package/src/providers/provider-registry.ts +4 -8
- package/src/tokens.ts +5 -6
- package/src/tools/commands.ts +40 -53
- package/src/widgets/ai-settings.tsx +153 -84
- package/src/widgets/main-area-chat.ts +8 -2
- package/src/widgets/provider-config-dialog.tsx +54 -41
- package/style/base.css +24 -73
- package/lib/mcp/browser.d.ts +0 -68
- package/lib/mcp/browser.js +0 -138
- package/lib/tools/file.d.ts +0 -36
- package/lib/tools/file.js +0 -351
- package/lib/tools/notebook.d.ts +0 -40
- package/lib/tools/notebook.js +0 -779
- package/src/mcp/browser.ts +0 -220
- package/src/tools/file.ts +0 -438
- package/src/tools/notebook.ts +0 -986
package/src/chat-model.ts
CHANGED
|
@@ -12,6 +12,12 @@ import { PathExt } from '@jupyterlab/coreutils';
|
|
|
12
12
|
|
|
13
13
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
14
14
|
|
|
15
|
+
import { IDocumentWidget } from '@jupyterlab/docregistry';
|
|
16
|
+
|
|
17
|
+
import { INotebookModel, Notebook } from '@jupyterlab/notebook';
|
|
18
|
+
|
|
19
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
20
|
+
|
|
15
21
|
import { UUID } from '@lumino/coreutils';
|
|
16
22
|
|
|
17
23
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
@@ -24,12 +30,58 @@ import { AISettingsModel } from './models/settings-model';
|
|
|
24
30
|
|
|
25
31
|
import { ITokenUsage } from './tokens';
|
|
26
32
|
|
|
33
|
+
import { YNotebook } from '@jupyter/ydoc';
|
|
34
|
+
|
|
27
35
|
import * as nbformat from '@jupyterlab/nbformat';
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
* Tool call status types.
|
|
39
|
+
*/
|
|
40
|
+
type ToolStatus =
|
|
41
|
+
| 'pending'
|
|
42
|
+
| 'awaiting_approval'
|
|
43
|
+
| 'approved'
|
|
44
|
+
| 'rejected'
|
|
45
|
+
| 'completed'
|
|
46
|
+
| 'error';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Context for tracking tool execution state.
|
|
50
|
+
*/
|
|
51
|
+
interface IToolExecutionContext {
|
|
52
|
+
/**
|
|
53
|
+
* The tool call ID from the AI SDK.
|
|
54
|
+
*/
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
/**
|
|
57
|
+
* The chat message ID for UI updates.
|
|
58
|
+
*/
|
|
59
|
+
messageId: string;
|
|
60
|
+
/**
|
|
61
|
+
* The tool name.
|
|
62
|
+
*/
|
|
63
|
+
toolName: string;
|
|
64
|
+
/**
|
|
65
|
+
* The tool input (formatted).
|
|
66
|
+
*/
|
|
67
|
+
input: string;
|
|
68
|
+
/**
|
|
69
|
+
* Optional approval ID if awaiting approval.
|
|
70
|
+
*/
|
|
71
|
+
approvalId?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Current status.
|
|
74
|
+
*/
|
|
75
|
+
status: ToolStatus;
|
|
76
|
+
/**
|
|
77
|
+
* Human-readable summary extracted from tool input for display.
|
|
78
|
+
*/
|
|
79
|
+
summary?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* AI Chat Model implementation that provides chat functionality tool integration,
|
|
84
|
+
* and MCP server support.
|
|
33
85
|
*/
|
|
34
86
|
export class AIChatModel extends AbstractChatModel {
|
|
35
87
|
/**
|
|
@@ -48,6 +100,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
48
100
|
this._settingsModel = options.settingsModel;
|
|
49
101
|
this._user = options.user;
|
|
50
102
|
this._agentManager = options.agentManager;
|
|
103
|
+
this._trans = options.trans;
|
|
51
104
|
|
|
52
105
|
// Listen for agent events
|
|
53
106
|
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
|
|
@@ -123,7 +176,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
123
176
|
*/
|
|
124
177
|
clearMessages = (): void => {
|
|
125
178
|
this.messagesDeleted(0, this.messages.length);
|
|
126
|
-
this.
|
|
179
|
+
this._toolContexts.clear();
|
|
127
180
|
this._agentManager.clearHistory();
|
|
128
181
|
};
|
|
129
182
|
|
|
@@ -165,7 +218,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
165
218
|
const attachmentContents = await this._processAttachments(
|
|
166
219
|
this.input.attachments
|
|
167
220
|
);
|
|
168
|
-
// Clear attachments right after processing
|
|
169
221
|
this.input.clearAttachments();
|
|
170
222
|
|
|
171
223
|
if (attachmentContents.length > 0) {
|
|
@@ -192,78 +244,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
192
244
|
}
|
|
193
245
|
}
|
|
194
246
|
|
|
195
|
-
/**
|
|
196
|
-
* Approves a tool call and updates the UI accordingly.
|
|
197
|
-
* @param interruptionId The interruption ID to approve
|
|
198
|
-
* @param messageId Optional message ID for UI updates
|
|
199
|
-
*/
|
|
200
|
-
async approveToolCall(
|
|
201
|
-
interruptionId: string,
|
|
202
|
-
messageId?: string
|
|
203
|
-
): Promise<void> {
|
|
204
|
-
await this._agentManager.approveToolCall(interruptionId);
|
|
205
|
-
|
|
206
|
-
// Update the tool call box to show "Approved" status
|
|
207
|
-
if (messageId) {
|
|
208
|
-
this._updateToolCallBoxStatus(messageId, 'Approved', true);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Rejects a tool call and updates the UI accordingly.
|
|
214
|
-
* @param interruptionId The interruption ID to reject
|
|
215
|
-
* @param messageId Optional message ID for UI updates
|
|
216
|
-
*/
|
|
217
|
-
async rejectToolCall(
|
|
218
|
-
interruptionId: string,
|
|
219
|
-
messageId?: string
|
|
220
|
-
): Promise<void> {
|
|
221
|
-
await this._agentManager.rejectToolCall(interruptionId);
|
|
222
|
-
|
|
223
|
-
// Update the tool call box to show "Rejected" status
|
|
224
|
-
if (messageId) {
|
|
225
|
-
this._updateToolCallBoxStatus(messageId, 'Rejected', false);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Approves all tools in a group.
|
|
231
|
-
* @param groupId The group ID containing the tool calls
|
|
232
|
-
* @param interruptionIds Array of interruption IDs to approve
|
|
233
|
-
* @param messageId Optional message ID for UI updates
|
|
234
|
-
*/
|
|
235
|
-
async approveGroupedToolCalls(
|
|
236
|
-
groupId: string,
|
|
237
|
-
interruptionIds: string[],
|
|
238
|
-
messageId?: string
|
|
239
|
-
): Promise<void> {
|
|
240
|
-
await this._agentManager.approveGroupedToolCalls(groupId, interruptionIds);
|
|
241
|
-
|
|
242
|
-
// Update the grouped approval message to show approved status
|
|
243
|
-
if (messageId) {
|
|
244
|
-
this._updateGroupedApprovalStatus(messageId, 'Tools approved', true);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Rejects all tools in a group.
|
|
250
|
-
* @param groupId The group ID containing the tool calls
|
|
251
|
-
* @param interruptionIds Array of interruption IDs to reject
|
|
252
|
-
* @param messageId Optional message ID for UI updates
|
|
253
|
-
*/
|
|
254
|
-
async rejectGroupedToolCalls(
|
|
255
|
-
groupId: string,
|
|
256
|
-
interruptionIds: string[],
|
|
257
|
-
messageId?: string
|
|
258
|
-
): Promise<void> {
|
|
259
|
-
await this._agentManager.rejectGroupedToolCalls(groupId, interruptionIds);
|
|
260
|
-
|
|
261
|
-
// Update the grouped approval message to show rejected status
|
|
262
|
-
if (messageId) {
|
|
263
|
-
this._updateGroupedApprovalStatus(messageId, 'Tools rejected', false);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
247
|
/**
|
|
268
248
|
* Gets the AI user information for system messages.
|
|
269
249
|
*/
|
|
@@ -307,11 +287,11 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
307
287
|
case 'tool_call_complete':
|
|
308
288
|
this._handleToolCallCompleteEvent(event);
|
|
309
289
|
break;
|
|
310
|
-
case '
|
|
311
|
-
this.
|
|
290
|
+
case 'tool_approval_request':
|
|
291
|
+
this._handleToolApprovalRequest(event);
|
|
312
292
|
break;
|
|
313
|
-
case '
|
|
314
|
-
this.
|
|
293
|
+
case 'tool_approval_resolved':
|
|
294
|
+
this._handleToolApprovalResolved(event);
|
|
315
295
|
break;
|
|
316
296
|
case 'error':
|
|
317
297
|
this._handleErrorEvent(event);
|
|
@@ -365,6 +345,34 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
365
345
|
}
|
|
366
346
|
}
|
|
367
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Extracts a human-readable summary from tool input for display in the header.
|
|
350
|
+
* @param toolName The name of the tool being called
|
|
351
|
+
* @param input The formatted JSON input string
|
|
352
|
+
* @returns A short summary string or empty string if none available
|
|
353
|
+
*/
|
|
354
|
+
private _extractToolSummary(toolName: string, input: string): string {
|
|
355
|
+
try {
|
|
356
|
+
const parsedInput = JSON.parse(input);
|
|
357
|
+
|
|
358
|
+
switch (toolName) {
|
|
359
|
+
case 'execute_command':
|
|
360
|
+
if (parsedInput.commandId) {
|
|
361
|
+
return parsedInput.commandId;
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
case 'discover_commands':
|
|
365
|
+
if (parsedInput.query) {
|
|
366
|
+
return `query: "${parsedInput.query}"`;
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// If parsing fails, return empty string
|
|
372
|
+
}
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
|
|
368
376
|
/**
|
|
369
377
|
* Handles the start of a tool call execution.
|
|
370
378
|
* @param event Event containing the tool call start data
|
|
@@ -372,204 +380,134 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
372
380
|
private _handleToolCallStartEvent(
|
|
373
381
|
event: IAgentEvent<'tool_call_start'>
|
|
374
382
|
): void {
|
|
375
|
-
const
|
|
383
|
+
const messageId = UUID.uuid4();
|
|
384
|
+
const summary = this._extractToolSummary(
|
|
385
|
+
event.data.toolName,
|
|
386
|
+
event.data.input
|
|
387
|
+
);
|
|
388
|
+
const context: IToolExecutionContext = {
|
|
389
|
+
toolCallId: event.data.callId,
|
|
390
|
+
messageId,
|
|
391
|
+
toolName: event.data.toolName,
|
|
392
|
+
input: event.data.input,
|
|
393
|
+
status: 'pending',
|
|
394
|
+
summary
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
this._toolContexts.set(event.data.callId, context);
|
|
398
|
+
|
|
376
399
|
const toolCallMessage: IChatMessage = {
|
|
377
|
-
body:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<div class="jp-ai-tool-section">
|
|
385
|
-
<div class="jp-ai-tool-label">Input</div>
|
|
386
|
-
<pre class="jp-ai-tool-code"><code>${event.data.input}</code></pre>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
</details>`,
|
|
400
|
+
body: Private.buildToolCallHtml({
|
|
401
|
+
toolName: context.toolName,
|
|
402
|
+
input: context.input,
|
|
403
|
+
status: context.status,
|
|
404
|
+
summary: context.summary,
|
|
405
|
+
trans: this._trans
|
|
406
|
+
}),
|
|
390
407
|
sender: this._getAIUser(),
|
|
391
|
-
id:
|
|
408
|
+
id: messageId,
|
|
392
409
|
time: Date.now() / 1000,
|
|
393
410
|
type: 'msg',
|
|
394
411
|
raw_time: false
|
|
395
412
|
};
|
|
396
413
|
|
|
397
|
-
if (event.data.callId) {
|
|
398
|
-
this._pendingToolCalls.set(event.data.callId, toolCallMessageId);
|
|
399
|
-
}
|
|
400
414
|
this.messageAdded(toolCallMessage);
|
|
401
415
|
}
|
|
402
416
|
|
|
403
417
|
/**
|
|
404
418
|
* Handles the completion of a tool call execution.
|
|
405
|
-
* @param event Event containing the tool call completion data
|
|
406
419
|
*/
|
|
407
420
|
private _handleToolCallCompleteEvent(
|
|
408
421
|
event: IAgentEvent<'tool_call_complete'>
|
|
409
422
|
): void {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
msg => msg.id === messageId
|
|
414
|
-
);
|
|
415
|
-
if (existingMessageIndex !== -1) {
|
|
416
|
-
const existingMessage = this.messages[existingMessageIndex];
|
|
417
|
-
const inputJson =
|
|
418
|
-
existingMessage.body.match(/<code>([\s\S]*?)<\/code>/)?.[1] || '';
|
|
419
|
-
|
|
420
|
-
const statusClass = event.data.isError
|
|
421
|
-
? 'jp-ai-tool-error'
|
|
422
|
-
: 'jp-ai-tool-completed';
|
|
423
|
-
const statusText = event.data.isError ? 'Error' : 'Completed';
|
|
424
|
-
const statusColor = event.data.isError
|
|
425
|
-
? 'jp-ai-tool-status-error'
|
|
426
|
-
: 'jp-ai-tool-status-completed';
|
|
427
|
-
|
|
428
|
-
const updatedMessage: IChatMessage = {
|
|
429
|
-
...existingMessage,
|
|
430
|
-
body: `<details class="jp-ai-tool-call ${statusClass}">
|
|
431
|
-
<summary class="jp-ai-tool-header">
|
|
432
|
-
<div class="jp-ai-tool-icon">⚡</div>
|
|
433
|
-
<div class="jp-ai-tool-title">${event.data.toolName}</div>
|
|
434
|
-
<div class="jp-ai-tool-status ${statusColor}">${statusText}</div>
|
|
435
|
-
</summary>
|
|
436
|
-
<div class="jp-ai-tool-body">
|
|
437
|
-
<div class="jp-ai-tool-section">
|
|
438
|
-
<div class="jp-ai-tool-label">Input</div>
|
|
439
|
-
<pre class="jp-ai-tool-code"><code>${inputJson}</code></pre>
|
|
440
|
-
</div>
|
|
441
|
-
<div class="jp-ai-tool-section">
|
|
442
|
-
<div class="jp-ai-tool-label">${event.data.isError ? 'Error' : 'Result'}</div>
|
|
443
|
-
<pre class="jp-ai-tool-code"><code>${event.data.output}</code></pre>
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
</details>`
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
this.messageAdded(updatedMessage);
|
|
450
|
-
this._pendingToolCalls.delete(event.data.callId);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
423
|
+
const status = event.data.isError ? 'error' : 'completed';
|
|
424
|
+
this._updateToolCallUI(event.data.callId, status, event.data.output);
|
|
425
|
+
this._toolContexts.delete(event.data.callId);
|
|
453
426
|
}
|
|
454
427
|
|
|
455
428
|
/**
|
|
456
|
-
* Handles
|
|
457
|
-
* @param event Event containing the tool approval request data
|
|
429
|
+
* Handles error events from the AI agent.
|
|
458
430
|
*/
|
|
459
|
-
private
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// Handle single tool approval - either update existing tool call message or create new approval message
|
|
463
|
-
if (event.data.callId) {
|
|
464
|
-
const messageId = this._pendingToolCalls.get(event.data.callId);
|
|
465
|
-
if (messageId) {
|
|
466
|
-
const existingMessageIndex = this.messages.findIndex(
|
|
467
|
-
msg => msg.id === messageId
|
|
468
|
-
);
|
|
469
|
-
if (existingMessageIndex !== -1) {
|
|
470
|
-
const existingMessage = this.messages[existingMessageIndex];
|
|
471
|
-
const assistantName = this._getAIUser().display_name;
|
|
472
|
-
|
|
473
|
-
const updatedMessage: IChatMessage = {
|
|
474
|
-
...existingMessage,
|
|
475
|
-
body: `<details class="jp-ai-tool-call jp-ai-tool-pending" open>
|
|
476
|
-
<summary class="jp-ai-tool-header">
|
|
477
|
-
<div class="jp-ai-tool-icon">⚡</div>
|
|
478
|
-
<div class="jp-ai-tool-title">${event.data.toolName}</div>
|
|
479
|
-
<div class="jp-ai-tool-status jp-ai-tool-status-pending">Needs Approval</div>
|
|
480
|
-
</summary>
|
|
481
|
-
<div class="jp-ai-tool-body">
|
|
482
|
-
<div class="jp-ai-tool-section">
|
|
483
|
-
<div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
|
|
484
|
-
<pre class="jp-ai-tool-code"><code>${event.data.toolInput}</code></pre>
|
|
485
|
-
</div>
|
|
486
|
-
[APPROVAL_BUTTONS:${event.data.interruptionId}]
|
|
487
|
-
</div>
|
|
488
|
-
</details>`
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
this.messageAdded(updatedMessage);
|
|
492
|
-
this.updateWriters([]);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Fallback: create separate approval message
|
|
499
|
-
const approvalMessageId = UUID.uuid4();
|
|
500
|
-
const assistantName = this._getAIUser().display_name;
|
|
501
|
-
|
|
502
|
-
const approvalMessage: IChatMessage = {
|
|
503
|
-
body: `**🤖 Tool Approval Required: ${event.data.toolName}**
|
|
504
|
-
|
|
505
|
-
${assistantName} wants to execute this tool. Do you approve?
|
|
506
|
-
|
|
507
|
-
\`\`\`json
|
|
508
|
-
${event.data.toolInput}
|
|
509
|
-
\`\`\`
|
|
510
|
-
|
|
511
|
-
[APPROVAL_BUTTONS:${event.data.interruptionId}]`,
|
|
431
|
+
private _handleErrorEvent(event: IAgentEvent<'error'>): void {
|
|
432
|
+
this.messageAdded({
|
|
433
|
+
body: `Error generating response: ${event.data.error.message}`,
|
|
512
434
|
sender: this._getAIUser(),
|
|
513
|
-
id:
|
|
435
|
+
id: UUID.uuid4(),
|
|
514
436
|
time: Date.now() / 1000,
|
|
515
437
|
type: 'msg',
|
|
516
438
|
raw_time: false
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
this.messageAdded(approvalMessage);
|
|
520
|
-
this.updateWriters([]); // Stop showing "AI is writing"
|
|
439
|
+
});
|
|
521
440
|
}
|
|
522
441
|
|
|
523
442
|
/**
|
|
524
|
-
* Handles
|
|
525
|
-
* @param event Event containing the grouped tool approval request data
|
|
443
|
+
* Handles tool approval request events from the AI agent.
|
|
526
444
|
*/
|
|
527
|
-
private
|
|
528
|
-
event: IAgentEvent<'
|
|
445
|
+
private _handleToolApprovalRequest(
|
|
446
|
+
event: IAgentEvent<'tool_approval_request'>
|
|
529
447
|
): void {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
.join('\n\n');
|
|
539
|
-
|
|
540
|
-
const approvalMessage: IChatMessage = {
|
|
541
|
-
body: `**🤖 Multiple Tool Approvals Required**
|
|
542
|
-
|
|
543
|
-
${assistantName} wants to execute ${event.data.approvals.length} tools. Do you approve?
|
|
448
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
449
|
+
if (!context) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
context.approvalId = event.data.approvalId;
|
|
453
|
+
context.input = JSON.stringify(event.data.args, null, 2);
|
|
454
|
+
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
455
|
+
}
|
|
544
456
|
|
|
545
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Handles tool approval resolved events from the AI agent.
|
|
459
|
+
*/
|
|
460
|
+
private _handleToolApprovalResolved(
|
|
461
|
+
event: IAgentEvent<'tool_approval_resolved'>
|
|
462
|
+
): void {
|
|
463
|
+
const context = Array.from(this._toolContexts.values()).find(
|
|
464
|
+
ctx => ctx.approvalId === event.data.approvalId
|
|
465
|
+
);
|
|
466
|
+
if (!context) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
546
469
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
id: approvalMessageId,
|
|
550
|
-
time: Date.now() / 1000,
|
|
551
|
-
type: 'msg',
|
|
552
|
-
raw_time: false
|
|
553
|
-
};
|
|
470
|
+
const status = event.data.approved ? 'approved' : 'rejected';
|
|
471
|
+
this._updateToolCallUI(context.toolCallId, status);
|
|
554
472
|
|
|
555
|
-
|
|
556
|
-
|
|
473
|
+
if (!event.data.approved) {
|
|
474
|
+
this._toolContexts.delete(context.toolCallId);
|
|
475
|
+
}
|
|
557
476
|
}
|
|
558
477
|
|
|
559
478
|
/**
|
|
560
|
-
*
|
|
561
|
-
* @param event Event containing the error information
|
|
479
|
+
* Updates a tool call's UI with new status and optional output.
|
|
562
480
|
*/
|
|
563
|
-
private
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
481
|
+
private _updateToolCallUI(
|
|
482
|
+
toolCallId: string,
|
|
483
|
+
status: ToolStatus,
|
|
484
|
+
output?: string
|
|
485
|
+
): void {
|
|
486
|
+
const context = this._toolContexts.get(toolCallId);
|
|
487
|
+
if (!context) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const existingMessage = this.messages.find(
|
|
492
|
+
msg => msg.id === context.messageId
|
|
493
|
+
);
|
|
494
|
+
if (!existingMessage) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
context.status = status;
|
|
499
|
+
this.messageAdded({
|
|
500
|
+
...existingMessage,
|
|
501
|
+
body: Private.buildToolCallHtml({
|
|
502
|
+
toolName: context.toolName,
|
|
503
|
+
input: context.input,
|
|
504
|
+
status: context.status,
|
|
505
|
+
summary: context.summary,
|
|
506
|
+
output,
|
|
507
|
+
approvalId: context.approvalId,
|
|
508
|
+
trans: this._trans
|
|
509
|
+
})
|
|
510
|
+
});
|
|
573
511
|
}
|
|
574
512
|
|
|
575
513
|
/**
|
|
@@ -623,23 +561,45 @@ ${toolsList}
|
|
|
623
561
|
}
|
|
624
562
|
|
|
625
563
|
try {
|
|
626
|
-
|
|
564
|
+
// Try reading from live notebook if open
|
|
565
|
+
const widget = this.input.documentManager?.findWidget(
|
|
627
566
|
attachment.value
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
567
|
+
) as IDocumentWidget<Notebook, INotebookModel> | undefined;
|
|
568
|
+
let cellData: nbformat.ICell[];
|
|
569
|
+
let kernelLang = 'text';
|
|
570
|
+
|
|
571
|
+
const ymodel = widget?.context.model.sharedModel as YNotebook;
|
|
632
572
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
573
|
+
if (ymodel) {
|
|
574
|
+
const nb = ymodel.toJSON();
|
|
575
|
+
|
|
576
|
+
cellData = nb.cells;
|
|
577
|
+
|
|
578
|
+
const lang =
|
|
579
|
+
nb.metadata.language_info?.name ||
|
|
580
|
+
nb.metadata.kernelspec?.language ||
|
|
581
|
+
'text';
|
|
582
|
+
|
|
583
|
+
kernelLang = String(lang);
|
|
584
|
+
} else {
|
|
585
|
+
// Fallback: reading from disk
|
|
586
|
+
const model = await this.input.documentManager?.services.contents.get(
|
|
587
|
+
attachment.value
|
|
588
|
+
);
|
|
589
|
+
if (!model || model.type !== 'notebook') {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
cellData = model.content.cells ?? [];
|
|
593
|
+
|
|
594
|
+
kernelLang =
|
|
595
|
+
model.content.metadata.language_info?.name ||
|
|
596
|
+
model.content.metadata.kernelspec?.language ||
|
|
597
|
+
'text';
|
|
598
|
+
}
|
|
637
599
|
|
|
638
600
|
const selectedCells = attachment.cells
|
|
639
601
|
.map(cellInfo => {
|
|
640
|
-
const cell =
|
|
641
|
-
(c: any) => c.id === cellInfo.id
|
|
642
|
-
);
|
|
602
|
+
const cell = cellData.find(c => c.id === cellInfo.id);
|
|
643
603
|
if (!cell) {
|
|
644
604
|
return null;
|
|
645
605
|
}
|
|
@@ -660,7 +620,7 @@ ${toolsList}
|
|
|
660
620
|
'text/plain'
|
|
661
621
|
];
|
|
662
622
|
|
|
663
|
-
function extractDisplay(data:
|
|
623
|
+
function extractDisplay(data: nbformat.IMimeBundle): string {
|
|
664
624
|
for (const mime of DISPLAY_PRIORITY) {
|
|
665
625
|
if (!(mime in data)) {
|
|
666
626
|
continue;
|
|
@@ -673,13 +633,13 @@ ${toolsList}
|
|
|
673
633
|
|
|
674
634
|
switch (mime) {
|
|
675
635
|
case 'application/vnd.jupyter.widget-view+json':
|
|
676
|
-
return `Widget: ${(value as
|
|
636
|
+
return `Widget: ${(value as { model_id?: string }).model_id ?? 'unknown model'}`;
|
|
677
637
|
|
|
678
638
|
case 'image/png':
|
|
679
|
-
return `}...)`;
|
|
639
|
+
return `.slice(0, 100)}...)`;
|
|
680
640
|
|
|
681
641
|
case 'image/jpeg':
|
|
682
|
-
return `}...)`;
|
|
642
|
+
return `.slice(0, 100)}...)`;
|
|
683
643
|
|
|
684
644
|
case 'image/svg+xml':
|
|
685
645
|
return String(value).slice(0, 500) + '...\n[svg truncated]';
|
|
@@ -712,8 +672,9 @@ ${toolsList}
|
|
|
712
672
|
|
|
713
673
|
let outputs = '';
|
|
714
674
|
if (cellType === 'code' && Array.isArray(cell.outputs)) {
|
|
715
|
-
|
|
716
|
-
|
|
675
|
+
const outputsArray = cell.outputs as nbformat.IOutput[];
|
|
676
|
+
outputs = outputsArray
|
|
677
|
+
.map(output => {
|
|
717
678
|
if (output.output_type === 'stream') {
|
|
718
679
|
return (output as nbformat.IStream).text;
|
|
719
680
|
} else if (output.output_type === 'error') {
|
|
@@ -777,36 +738,48 @@ ${toolsList}
|
|
|
777
738
|
}
|
|
778
739
|
|
|
779
740
|
try {
|
|
780
|
-
|
|
741
|
+
// Try reading from an open widget first
|
|
742
|
+
const widget = this.input.documentManager?.findWidget(
|
|
743
|
+
attachment.value
|
|
744
|
+
) as IDocumentWidget<Notebook, INotebookModel> | undefined;
|
|
745
|
+
|
|
746
|
+
if (widget && widget.context && widget.context.model) {
|
|
747
|
+
const model = widget.context.model;
|
|
748
|
+
const ymodel = model.sharedModel as YNotebook;
|
|
749
|
+
|
|
750
|
+
if (typeof ymodel.getSource === 'function') {
|
|
751
|
+
const source = ymodel.getSource();
|
|
752
|
+
return typeof source === 'string'
|
|
753
|
+
? source
|
|
754
|
+
: JSON.stringify(source, null, 2);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// If not open, load from disk
|
|
759
|
+
const diskModel = await this.input.documentManager?.services.contents.get(
|
|
781
760
|
attachment.value
|
|
782
761
|
);
|
|
783
|
-
|
|
762
|
+
|
|
763
|
+
if (!diskModel?.content) {
|
|
784
764
|
return null;
|
|
785
765
|
}
|
|
786
|
-
|
|
766
|
+
|
|
767
|
+
if (diskModel.type === 'file') {
|
|
787
768
|
// Regular file content
|
|
788
|
-
return
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
return cleanCell;
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
const notebookModel = {
|
|
804
|
-
cells,
|
|
805
|
-
metadata: (model as any).metadata || {},
|
|
806
|
-
nbformat: (model as any).nbformat || 4,
|
|
807
|
-
nbformat_minor: (model as any).nbformat_minor || 4
|
|
769
|
+
return diskModel.content;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (diskModel.type === 'notebook') {
|
|
773
|
+
const cleaned = {
|
|
774
|
+
...diskModel,
|
|
775
|
+
cells: diskModel.content.cells.map((cell: nbformat.ICell) => ({
|
|
776
|
+
...cell,
|
|
777
|
+
outputs: [] as nbformat.IOutput[],
|
|
778
|
+
execution_count: null
|
|
779
|
+
}))
|
|
808
780
|
};
|
|
809
|
-
|
|
781
|
+
|
|
782
|
+
return JSON.stringify(cleaned);
|
|
810
783
|
}
|
|
811
784
|
return null;
|
|
812
785
|
} catch (error) {
|
|
@@ -815,107 +788,159 @@ ${toolsList}
|
|
|
815
788
|
}
|
|
816
789
|
}
|
|
817
790
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
private
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
isSuccess: boolean
|
|
828
|
-
): void {
|
|
829
|
-
const existingMessageIndex = this.messages.findIndex(
|
|
830
|
-
msg => msg.id === messageId
|
|
831
|
-
);
|
|
832
|
-
if (existingMessageIndex !== -1) {
|
|
833
|
-
const existingMessage = this.messages[existingMessageIndex];
|
|
834
|
-
|
|
835
|
-
// Extract tool count and names from existing message
|
|
836
|
-
const toolCountMatch = existingMessage.body.match(/execute (\d+) tools/);
|
|
837
|
-
const toolCount = toolCountMatch ? toolCountMatch[1] : 'multiple';
|
|
791
|
+
// Private fields
|
|
792
|
+
private _settingsModel: AISettingsModel;
|
|
793
|
+
private _user: IUser;
|
|
794
|
+
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
795
|
+
private _agentManager: AgentManager;
|
|
796
|
+
private _currentStreamingMessage: IChatMessage | null = null;
|
|
797
|
+
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
798
|
+
private _trans: TranslationBundle;
|
|
799
|
+
}
|
|
838
800
|
|
|
839
|
-
|
|
840
|
-
|
|
801
|
+
namespace Private {
|
|
802
|
+
export function escapeHtml(value: string): string {
|
|
803
|
+
// Prefer the same native escaping approach used in JupyterLab itself
|
|
804
|
+
// (e.g. `@jupyterlab/completer`).
|
|
805
|
+
if (typeof document !== 'undefined') {
|
|
806
|
+
const node = document.createElement('span');
|
|
807
|
+
node.textContent = value;
|
|
808
|
+
return node.innerHTML;
|
|
809
|
+
}
|
|
841
810
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
811
|
+
// Fallback
|
|
812
|
+
return value
|
|
813
|
+
.replace(/&/g, '&')
|
|
814
|
+
.replace(/</g, '<')
|
|
815
|
+
.replace(/>/g, '>')
|
|
816
|
+
.replace(/"/g, '"')
|
|
817
|
+
.replace(/'/g, ''');
|
|
818
|
+
}
|
|
845
819
|
|
|
846
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Configuration for rendering tool call status.
|
|
822
|
+
*/
|
|
823
|
+
interface IStatusConfig {
|
|
824
|
+
cssClass: string;
|
|
825
|
+
statusClass: string;
|
|
826
|
+
open?: boolean;
|
|
827
|
+
}
|
|
847
828
|
|
|
848
|
-
<
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
829
|
+
const STATUS_CONFIG: Record<ToolStatus, IStatusConfig> = {
|
|
830
|
+
pending: {
|
|
831
|
+
cssClass: 'jp-ai-tool-pending',
|
|
832
|
+
statusClass: 'jp-ai-tool-status-pending'
|
|
833
|
+
},
|
|
834
|
+
awaiting_approval: {
|
|
835
|
+
cssClass: 'jp-ai-tool-pending',
|
|
836
|
+
statusClass: 'jp-ai-tool-status-approval',
|
|
837
|
+
open: true
|
|
838
|
+
},
|
|
839
|
+
approved: {
|
|
840
|
+
cssClass: 'jp-ai-tool-pending',
|
|
841
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
842
|
+
},
|
|
843
|
+
rejected: {
|
|
844
|
+
cssClass: 'jp-ai-tool-error',
|
|
845
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
846
|
+
},
|
|
847
|
+
completed: {
|
|
848
|
+
cssClass: 'jp-ai-tool-completed',
|
|
849
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
850
|
+
},
|
|
851
|
+
error: {
|
|
852
|
+
cssClass: 'jp-ai-tool-error',
|
|
853
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
854
|
+
}
|
|
855
|
+
};
|
|
852
856
|
|
|
853
|
-
|
|
857
|
+
/**
|
|
858
|
+
* Returns the translated status text for a given tool status.
|
|
859
|
+
*/
|
|
860
|
+
const getStatusText = (
|
|
861
|
+
status: ToolStatus,
|
|
862
|
+
trans: TranslationBundle
|
|
863
|
+
): string => {
|
|
864
|
+
switch (status) {
|
|
865
|
+
case 'pending':
|
|
866
|
+
return trans.__('Running...');
|
|
867
|
+
case 'awaiting_approval':
|
|
868
|
+
return trans.__('Awaiting Approval');
|
|
869
|
+
case 'approved':
|
|
870
|
+
return trans.__('Approved - Executing...');
|
|
871
|
+
case 'rejected':
|
|
872
|
+
return trans.__('Rejected');
|
|
873
|
+
case 'completed':
|
|
874
|
+
return trans.__('Completed');
|
|
875
|
+
case 'error':
|
|
876
|
+
return trans.__('Error');
|
|
854
877
|
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Options for building tool call HTML.
|
|
882
|
+
*/
|
|
883
|
+
interface IToolCallHtmlOptions {
|
|
884
|
+
toolName: string;
|
|
885
|
+
input: string;
|
|
886
|
+
status: ToolStatus;
|
|
887
|
+
summary?: string;
|
|
888
|
+
output?: string;
|
|
889
|
+
approvalId?: string;
|
|
890
|
+
trans: TranslationBundle;
|
|
855
891
|
}
|
|
856
892
|
|
|
857
893
|
/**
|
|
858
|
-
*
|
|
859
|
-
* @param messageId The message ID to update
|
|
860
|
-
* @param status The status text to display
|
|
861
|
-
* @param isSuccess Whether the action was successful
|
|
894
|
+
* Builds HTML for a tool call display.
|
|
862
895
|
*/
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
896
|
+
export function buildToolCallHtml(options: IToolCallHtmlOptions): string {
|
|
897
|
+
const { toolName, input, status, summary, output, approvalId, trans } =
|
|
898
|
+
options;
|
|
899
|
+
const config = STATUS_CONFIG[status];
|
|
900
|
+
const statusText = getStatusText(status, trans);
|
|
901
|
+
const escapedToolName = escapeHtml(toolName);
|
|
902
|
+
const escapedInput = escapeHtml(input);
|
|
903
|
+
const openAttr = config.open ? ' open' : '';
|
|
904
|
+
const summaryHtml = summary
|
|
905
|
+
? `<span class="jp-ai-tool-summary">${escapeHtml(summary)}</span>`
|
|
906
|
+
: '';
|
|
873
907
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
908
|
+
let bodyContent = `
|
|
909
|
+
<div class="jp-ai-tool-section">
|
|
910
|
+
<div class="jp-ai-tool-label">${trans.__('Input')}</div>
|
|
911
|
+
<pre class="jp-ai-tool-code"><code>${escapedInput}</code></pre>
|
|
912
|
+
</div>`;
|
|
913
|
+
|
|
914
|
+
// Add approval buttons if awaiting approval
|
|
915
|
+
if (status === 'awaiting_approval' && approvalId) {
|
|
916
|
+
bodyContent += `
|
|
917
|
+
<div class="jp-ai-tool-approval-buttons jp-ai-approval-id--${approvalId}">
|
|
918
|
+
<button class="jp-ai-approval-btn jp-ai-approval-approve">${trans.__('Approve')}</button>
|
|
919
|
+
<button class="jp-ai-approval-btn jp-ai-approval-reject">${trans.__('Reject')}</button>
|
|
920
|
+
</div>`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Add output/result section if provided
|
|
924
|
+
if (output !== undefined) {
|
|
925
|
+
const escapedOutput = escapeHtml(output);
|
|
926
|
+
const label = status === 'error' ? trans.__('Error') : trans.__('Result');
|
|
927
|
+
bodyContent += `
|
|
928
|
+
<div class="jp-ai-tool-section">
|
|
929
|
+
<div class="jp-ai-tool-label">${label}</div>
|
|
930
|
+
<pre class="jp-ai-tool-code"><code>${escapedOutput}</code></pre>
|
|
931
|
+
</div>`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return `<details class="jp-ai-tool-call ${config.cssClass}"${openAttr}>
|
|
894
935
|
<summary class="jp-ai-tool-header">
|
|
895
936
|
<div class="jp-ai-tool-icon">⚡</div>
|
|
896
|
-
<div class="jp-ai-tool-title">${
|
|
897
|
-
<div class="jp-ai-tool-status ${
|
|
937
|
+
<div class="jp-ai-tool-title">${escapedToolName}${summaryHtml}</div>
|
|
938
|
+
<div class="jp-ai-tool-status ${config.statusClass}">${statusText}</div>
|
|
898
939
|
</summary>
|
|
899
|
-
<div class="jp-ai-tool-body"
|
|
900
|
-
<div class="jp-ai-tool-section">
|
|
901
|
-
<div class="jp-ai-tool-label">Input</div>
|
|
902
|
-
<pre class="jp-ai-tool-code"><code>${toolInput}</code></pre>
|
|
940
|
+
<div class="jp-ai-tool-body">${bodyContent}
|
|
903
941
|
</div>
|
|
904
|
-
</
|
|
905
|
-
</details>`
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
this.messageAdded(updatedMessage);
|
|
909
|
-
}
|
|
942
|
+
</details>`;
|
|
910
943
|
}
|
|
911
|
-
|
|
912
|
-
// Private fields
|
|
913
|
-
private _settingsModel: AISettingsModel;
|
|
914
|
-
private _user: IUser;
|
|
915
|
-
private _pendingToolCalls: Map<string, string> = new Map();
|
|
916
|
-
private _agentManager: AgentManager;
|
|
917
|
-
private _currentStreamingMessage: IChatMessage | null = null;
|
|
918
|
-
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
919
944
|
}
|
|
920
945
|
|
|
921
946
|
/**
|
|
@@ -946,6 +971,10 @@ export namespace AIChatModel {
|
|
|
946
971
|
* Optional document manager for file operations
|
|
947
972
|
*/
|
|
948
973
|
documentManager?: IDocumentManager;
|
|
974
|
+
/**
|
|
975
|
+
* The application language translation bundle.
|
|
976
|
+
*/
|
|
977
|
+
trans: TranslationBundle;
|
|
949
978
|
}
|
|
950
979
|
|
|
951
980
|
/**
|