@jupyterlite/ai 0.9.1 → 0.10.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 +274 -300
- 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 +19 -54
- package/lib/chat-model.js +243 -303
- package/lib/components/clear-button.d.ts +6 -1
- package/lib/components/clear-button.js +8 -3
- 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 +9 -8
- package/lib/components/stop-button.d.ts +6 -1
- package/lib/components/stop-button.js +8 -3
- 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 +6 -5
- package/lib/index.js +58 -38
- package/lib/models/settings-model.d.ts +1 -1
- 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 +37 -46
- package/lib/tools/file.js +49 -73
- package/lib/tools/notebook.js +370 -445
- 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 +12 -12
- package/src/agent.ts +342 -361
- package/src/approval-buttons.ts +43 -389
- package/src/chat-model-registry.ts +9 -1
- package/src/chat-model.ts +355 -370
- package/src/completion/completion-provider.ts +2 -3
- package/src/components/clear-button.tsx +16 -3
- package/src/components/completion-status.tsx +18 -4
- package/src/components/model-select.tsx +21 -8
- package/src/components/stop-button.tsx +16 -3
- package/src/components/token-usage-display.tsx +14 -2
- package/src/components/tool-select.tsx +23 -5
- package/src/index.ts +75 -36
- package/src/models/settings-model.ts +1 -1
- 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 +39 -50
- package/src/tools/file.ts +49 -75
- package/src/tools/notebook.ts +451 -510
- 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 +13 -73
- package/lib/mcp/browser.d.ts +0 -68
- package/lib/mcp/browser.js +0 -138
- package/src/mcp/browser.ts +0 -220
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,54 @@ 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
|
+
|
|
78
|
+
/**
|
|
79
|
+
* AI Chat Model implementation that provides chat functionality tool integration,
|
|
80
|
+
* and MCP server support.
|
|
33
81
|
*/
|
|
34
82
|
export class AIChatModel extends AbstractChatModel {
|
|
35
83
|
/**
|
|
@@ -48,6 +96,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
48
96
|
this._settingsModel = options.settingsModel;
|
|
49
97
|
this._user = options.user;
|
|
50
98
|
this._agentManager = options.agentManager;
|
|
99
|
+
this._trans = options.trans;
|
|
51
100
|
|
|
52
101
|
// Listen for agent events
|
|
53
102
|
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
|
|
@@ -123,7 +172,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
123
172
|
*/
|
|
124
173
|
clearMessages = (): void => {
|
|
125
174
|
this.messagesDeleted(0, this.messages.length);
|
|
126
|
-
this.
|
|
175
|
+
this._toolContexts.clear();
|
|
127
176
|
this._agentManager.clearHistory();
|
|
128
177
|
};
|
|
129
178
|
|
|
@@ -165,7 +214,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
165
214
|
const attachmentContents = await this._processAttachments(
|
|
166
215
|
this.input.attachments
|
|
167
216
|
);
|
|
168
|
-
// Clear attachments right after processing
|
|
169
217
|
this.input.clearAttachments();
|
|
170
218
|
|
|
171
219
|
if (attachmentContents.length > 0) {
|
|
@@ -192,78 +240,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
192
240
|
}
|
|
193
241
|
}
|
|
194
242
|
|
|
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
243
|
/**
|
|
268
244
|
* Gets the AI user information for system messages.
|
|
269
245
|
*/
|
|
@@ -307,11 +283,11 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
307
283
|
case 'tool_call_complete':
|
|
308
284
|
this._handleToolCallCompleteEvent(event);
|
|
309
285
|
break;
|
|
310
|
-
case '
|
|
311
|
-
this.
|
|
286
|
+
case 'tool_approval_request':
|
|
287
|
+
this._handleToolApprovalRequest(event);
|
|
312
288
|
break;
|
|
313
|
-
case '
|
|
314
|
-
this.
|
|
289
|
+
case 'tool_approval_resolved':
|
|
290
|
+
this._handleToolApprovalResolved(event);
|
|
315
291
|
break;
|
|
316
292
|
case 'error':
|
|
317
293
|
this._handleErrorEvent(event);
|
|
@@ -372,204 +348,127 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
372
348
|
private _handleToolCallStartEvent(
|
|
373
349
|
event: IAgentEvent<'tool_call_start'>
|
|
374
350
|
): void {
|
|
375
|
-
const
|
|
351
|
+
const messageId = UUID.uuid4();
|
|
352
|
+
const context: IToolExecutionContext = {
|
|
353
|
+
toolCallId: event.data.callId,
|
|
354
|
+
messageId,
|
|
355
|
+
toolName: event.data.toolName,
|
|
356
|
+
input: event.data.input,
|
|
357
|
+
status: 'pending'
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
this._toolContexts.set(event.data.callId, context);
|
|
361
|
+
|
|
376
362
|
const toolCallMessage: IChatMessage = {
|
|
377
|
-
body:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<div class="jp-ai-tool-body">
|
|
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>`,
|
|
363
|
+
body: Private.buildToolCallHtml({
|
|
364
|
+
toolName: context.toolName,
|
|
365
|
+
input: context.input,
|
|
366
|
+
status: context.status,
|
|
367
|
+
trans: this._trans
|
|
368
|
+
}),
|
|
390
369
|
sender: this._getAIUser(),
|
|
391
|
-
id:
|
|
370
|
+
id: messageId,
|
|
392
371
|
time: Date.now() / 1000,
|
|
393
372
|
type: 'msg',
|
|
394
373
|
raw_time: false
|
|
395
374
|
};
|
|
396
375
|
|
|
397
|
-
if (event.data.callId) {
|
|
398
|
-
this._pendingToolCalls.set(event.data.callId, toolCallMessageId);
|
|
399
|
-
}
|
|
400
376
|
this.messageAdded(toolCallMessage);
|
|
401
377
|
}
|
|
402
378
|
|
|
403
379
|
/**
|
|
404
380
|
* Handles the completion of a tool call execution.
|
|
405
|
-
* @param event Event containing the tool call completion data
|
|
406
381
|
*/
|
|
407
382
|
private _handleToolCallCompleteEvent(
|
|
408
383
|
event: IAgentEvent<'tool_call_complete'>
|
|
409
384
|
): 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
|
-
}
|
|
385
|
+
const status = event.data.isError ? 'error' : 'completed';
|
|
386
|
+
this._updateToolCallUI(event.data.callId, status, event.data.output);
|
|
387
|
+
this._toolContexts.delete(event.data.callId);
|
|
453
388
|
}
|
|
454
389
|
|
|
455
390
|
/**
|
|
456
|
-
* Handles
|
|
457
|
-
* @param event Event containing the tool approval request data
|
|
391
|
+
* Handles error events from the AI agent.
|
|
458
392
|
*/
|
|
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}]`,
|
|
393
|
+
private _handleErrorEvent(event: IAgentEvent<'error'>): void {
|
|
394
|
+
this.messageAdded({
|
|
395
|
+
body: `Error generating response: ${event.data.error.message}`,
|
|
512
396
|
sender: this._getAIUser(),
|
|
513
|
-
id:
|
|
397
|
+
id: UUID.uuid4(),
|
|
514
398
|
time: Date.now() / 1000,
|
|
515
399
|
type: 'msg',
|
|
516
400
|
raw_time: false
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
this.messageAdded(approvalMessage);
|
|
520
|
-
this.updateWriters([]); // Stop showing "AI is writing"
|
|
401
|
+
});
|
|
521
402
|
}
|
|
522
403
|
|
|
523
404
|
/**
|
|
524
|
-
* Handles
|
|
525
|
-
* @param event Event containing the grouped tool approval request data
|
|
405
|
+
* Handles tool approval request events from the AI agent.
|
|
526
406
|
*/
|
|
527
|
-
private
|
|
528
|
-
event: IAgentEvent<'
|
|
407
|
+
private _handleToolApprovalRequest(
|
|
408
|
+
event: IAgentEvent<'tool_approval_request'>
|
|
529
409
|
): 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?
|
|
410
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
411
|
+
if (!context) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
context.approvalId = event.data.approvalId;
|
|
415
|
+
context.input = JSON.stringify(event.data.args, null, 2);
|
|
416
|
+
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
417
|
+
}
|
|
544
418
|
|
|
545
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Handles tool approval resolved events from the AI agent.
|
|
421
|
+
*/
|
|
422
|
+
private _handleToolApprovalResolved(
|
|
423
|
+
event: IAgentEvent<'tool_approval_resolved'>
|
|
424
|
+
): void {
|
|
425
|
+
const context = Array.from(this._toolContexts.values()).find(
|
|
426
|
+
ctx => ctx.approvalId === event.data.approvalId
|
|
427
|
+
);
|
|
428
|
+
if (!context) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
546
431
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
id: approvalMessageId,
|
|
550
|
-
time: Date.now() / 1000,
|
|
551
|
-
type: 'msg',
|
|
552
|
-
raw_time: false
|
|
553
|
-
};
|
|
432
|
+
const status = event.data.approved ? 'approved' : 'rejected';
|
|
433
|
+
this._updateToolCallUI(context.toolCallId, status);
|
|
554
434
|
|
|
555
|
-
|
|
556
|
-
|
|
435
|
+
if (!event.data.approved) {
|
|
436
|
+
this._toolContexts.delete(context.toolCallId);
|
|
437
|
+
}
|
|
557
438
|
}
|
|
558
439
|
|
|
559
440
|
/**
|
|
560
|
-
*
|
|
561
|
-
* @param event Event containing the error information
|
|
441
|
+
* Updates a tool call's UI with new status and optional output.
|
|
562
442
|
*/
|
|
563
|
-
private
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
443
|
+
private _updateToolCallUI(
|
|
444
|
+
toolCallId: string,
|
|
445
|
+
status: ToolStatus,
|
|
446
|
+
output?: string
|
|
447
|
+
): void {
|
|
448
|
+
const context = this._toolContexts.get(toolCallId);
|
|
449
|
+
if (!context) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const existingMessage = this.messages.find(
|
|
454
|
+
msg => msg.id === context.messageId
|
|
455
|
+
);
|
|
456
|
+
if (!existingMessage) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
context.status = status;
|
|
461
|
+
this.messageAdded({
|
|
462
|
+
...existingMessage,
|
|
463
|
+
body: Private.buildToolCallHtml({
|
|
464
|
+
toolName: context.toolName,
|
|
465
|
+
input: context.input,
|
|
466
|
+
status: context.status,
|
|
467
|
+
output,
|
|
468
|
+
approvalId: context.approvalId,
|
|
469
|
+
trans: this._trans
|
|
470
|
+
})
|
|
471
|
+
});
|
|
573
472
|
}
|
|
574
473
|
|
|
575
474
|
/**
|
|
@@ -623,23 +522,45 @@ ${toolsList}
|
|
|
623
522
|
}
|
|
624
523
|
|
|
625
524
|
try {
|
|
626
|
-
|
|
525
|
+
// Try reading from live notebook if open
|
|
526
|
+
const widget = this.input.documentManager?.findWidget(
|
|
627
527
|
attachment.value
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
528
|
+
) as IDocumentWidget<Notebook, INotebookModel> | undefined;
|
|
529
|
+
let cellData: nbformat.ICell[];
|
|
530
|
+
let kernelLang = 'text';
|
|
531
|
+
|
|
532
|
+
const ymodel = widget?.context.model.sharedModel as YNotebook;
|
|
533
|
+
|
|
534
|
+
if (ymodel) {
|
|
535
|
+
const nb = ymodel.toJSON();
|
|
632
536
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
537
|
+
cellData = nb.cells;
|
|
538
|
+
|
|
539
|
+
const lang =
|
|
540
|
+
nb.metadata.language_info?.name ||
|
|
541
|
+
nb.metadata.kernelspec?.language ||
|
|
542
|
+
'text';
|
|
543
|
+
|
|
544
|
+
kernelLang = String(lang);
|
|
545
|
+
} else {
|
|
546
|
+
// Fallback: reading from disk
|
|
547
|
+
const model = await this.input.documentManager?.services.contents.get(
|
|
548
|
+
attachment.value
|
|
549
|
+
);
|
|
550
|
+
if (!model || model.type !== 'notebook') {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
cellData = model.content.cells ?? [];
|
|
554
|
+
|
|
555
|
+
kernelLang =
|
|
556
|
+
model.content.metadata.language_info?.name ||
|
|
557
|
+
model.content.metadata.kernelspec?.language ||
|
|
558
|
+
'text';
|
|
559
|
+
}
|
|
637
560
|
|
|
638
561
|
const selectedCells = attachment.cells
|
|
639
562
|
.map(cellInfo => {
|
|
640
|
-
const cell =
|
|
641
|
-
(c: any) => c.id === cellInfo.id
|
|
642
|
-
);
|
|
563
|
+
const cell = cellData.find(c => c.id === cellInfo.id);
|
|
643
564
|
if (!cell) {
|
|
644
565
|
return null;
|
|
645
566
|
}
|
|
@@ -660,7 +581,7 @@ ${toolsList}
|
|
|
660
581
|
'text/plain'
|
|
661
582
|
];
|
|
662
583
|
|
|
663
|
-
function extractDisplay(data:
|
|
584
|
+
function extractDisplay(data: nbformat.IMimeBundle): string {
|
|
664
585
|
for (const mime of DISPLAY_PRIORITY) {
|
|
665
586
|
if (!(mime in data)) {
|
|
666
587
|
continue;
|
|
@@ -673,13 +594,13 @@ ${toolsList}
|
|
|
673
594
|
|
|
674
595
|
switch (mime) {
|
|
675
596
|
case 'application/vnd.jupyter.widget-view+json':
|
|
676
|
-
return `Widget: ${(value as
|
|
597
|
+
return `Widget: ${(value as { model_id?: string }).model_id ?? 'unknown model'}`;
|
|
677
598
|
|
|
678
599
|
case 'image/png':
|
|
679
|
-
return `}...)`;
|
|
600
|
+
return `.slice(0, 100)}...)`;
|
|
680
601
|
|
|
681
602
|
case 'image/jpeg':
|
|
682
|
-
return `}...)`;
|
|
603
|
+
return `.slice(0, 100)}...)`;
|
|
683
604
|
|
|
684
605
|
case 'image/svg+xml':
|
|
685
606
|
return String(value).slice(0, 500) + '...\n[svg truncated]';
|
|
@@ -712,8 +633,9 @@ ${toolsList}
|
|
|
712
633
|
|
|
713
634
|
let outputs = '';
|
|
714
635
|
if (cellType === 'code' && Array.isArray(cell.outputs)) {
|
|
715
|
-
|
|
716
|
-
|
|
636
|
+
const outputsArray = cell.outputs as nbformat.IOutput[];
|
|
637
|
+
outputs = outputsArray
|
|
638
|
+
.map(output => {
|
|
717
639
|
if (output.output_type === 'stream') {
|
|
718
640
|
return (output as nbformat.IStream).text;
|
|
719
641
|
} else if (output.output_type === 'error') {
|
|
@@ -777,36 +699,48 @@ ${toolsList}
|
|
|
777
699
|
}
|
|
778
700
|
|
|
779
701
|
try {
|
|
780
|
-
|
|
702
|
+
// Try reading from an open widget first
|
|
703
|
+
const widget = this.input.documentManager?.findWidget(
|
|
704
|
+
attachment.value
|
|
705
|
+
) as IDocumentWidget<Notebook, INotebookModel> | undefined;
|
|
706
|
+
|
|
707
|
+
if (widget && widget.context && widget.context.model) {
|
|
708
|
+
const model = widget.context.model;
|
|
709
|
+
const ymodel = model.sharedModel as YNotebook;
|
|
710
|
+
|
|
711
|
+
if (typeof ymodel.getSource === 'function') {
|
|
712
|
+
const source = ymodel.getSource();
|
|
713
|
+
return typeof source === 'string'
|
|
714
|
+
? source
|
|
715
|
+
: JSON.stringify(source, null, 2);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// If not open, load from disk
|
|
720
|
+
const diskModel = await this.input.documentManager?.services.contents.get(
|
|
781
721
|
attachment.value
|
|
782
722
|
);
|
|
783
|
-
|
|
723
|
+
|
|
724
|
+
if (!diskModel?.content) {
|
|
784
725
|
return null;
|
|
785
726
|
}
|
|
786
|
-
|
|
727
|
+
|
|
728
|
+
if (diskModel.type === 'file') {
|
|
787
729
|
// 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
|
|
730
|
+
return diskModel.content;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (diskModel.type === 'notebook') {
|
|
734
|
+
const cleaned = {
|
|
735
|
+
...diskModel,
|
|
736
|
+
cells: diskModel.content.cells.map((cell: nbformat.ICell) => ({
|
|
737
|
+
...cell,
|
|
738
|
+
outputs: [] as nbformat.IOutput[],
|
|
739
|
+
execution_count: null
|
|
740
|
+
}))
|
|
808
741
|
};
|
|
809
|
-
|
|
742
|
+
|
|
743
|
+
return JSON.stringify(cleaned);
|
|
810
744
|
}
|
|
811
745
|
return null;
|
|
812
746
|
} catch (error) {
|
|
@@ -815,107 +749,154 @@ ${toolsList}
|
|
|
815
749
|
}
|
|
816
750
|
}
|
|
817
751
|
|
|
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';
|
|
752
|
+
// Private fields
|
|
753
|
+
private _settingsModel: AISettingsModel;
|
|
754
|
+
private _user: IUser;
|
|
755
|
+
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
756
|
+
private _agentManager: AgentManager;
|
|
757
|
+
private _currentStreamingMessage: IChatMessage | null = null;
|
|
758
|
+
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
759
|
+
private _trans: TranslationBundle;
|
|
760
|
+
}
|
|
838
761
|
|
|
839
|
-
|
|
840
|
-
|
|
762
|
+
namespace Private {
|
|
763
|
+
export function escapeHtml(value: string): string {
|
|
764
|
+
// Prefer the same native escaping approach used in JupyterLab itself
|
|
765
|
+
// (e.g. `@jupyterlab/completer`).
|
|
766
|
+
if (typeof document !== 'undefined') {
|
|
767
|
+
const node = document.createElement('span');
|
|
768
|
+
node.textContent = value;
|
|
769
|
+
return node.innerHTML;
|
|
770
|
+
}
|
|
841
771
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
772
|
+
// Fallback
|
|
773
|
+
return value
|
|
774
|
+
.replace(/&/g, '&')
|
|
775
|
+
.replace(/</g, '<')
|
|
776
|
+
.replace(/>/g, '>')
|
|
777
|
+
.replace(/"/g, '"')
|
|
778
|
+
.replace(/'/g, ''');
|
|
779
|
+
}
|
|
845
780
|
|
|
846
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Configuration for rendering tool call status.
|
|
783
|
+
*/
|
|
784
|
+
interface IStatusConfig {
|
|
785
|
+
cssClass: string;
|
|
786
|
+
statusClass: string;
|
|
787
|
+
open?: boolean;
|
|
788
|
+
}
|
|
847
789
|
|
|
848
|
-
<
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
790
|
+
const STATUS_CONFIG: Record<ToolStatus, IStatusConfig> = {
|
|
791
|
+
pending: {
|
|
792
|
+
cssClass: 'jp-ai-tool-pending',
|
|
793
|
+
statusClass: 'jp-ai-tool-status-pending'
|
|
794
|
+
},
|
|
795
|
+
awaiting_approval: {
|
|
796
|
+
cssClass: 'jp-ai-tool-pending',
|
|
797
|
+
statusClass: 'jp-ai-tool-status-approval',
|
|
798
|
+
open: true
|
|
799
|
+
},
|
|
800
|
+
approved: {
|
|
801
|
+
cssClass: 'jp-ai-tool-pending',
|
|
802
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
803
|
+
},
|
|
804
|
+
rejected: {
|
|
805
|
+
cssClass: 'jp-ai-tool-error',
|
|
806
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
807
|
+
},
|
|
808
|
+
completed: {
|
|
809
|
+
cssClass: 'jp-ai-tool-completed',
|
|
810
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
811
|
+
},
|
|
812
|
+
error: {
|
|
813
|
+
cssClass: 'jp-ai-tool-error',
|
|
814
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
815
|
+
}
|
|
816
|
+
};
|
|
852
817
|
|
|
853
|
-
|
|
818
|
+
/**
|
|
819
|
+
* Returns the translated status text for a given tool status.
|
|
820
|
+
*/
|
|
821
|
+
const getStatusText = (
|
|
822
|
+
status: ToolStatus,
|
|
823
|
+
trans: TranslationBundle
|
|
824
|
+
): string => {
|
|
825
|
+
switch (status) {
|
|
826
|
+
case 'pending':
|
|
827
|
+
return trans.__('Running...');
|
|
828
|
+
case 'awaiting_approval':
|
|
829
|
+
return trans.__('Awaiting Approval');
|
|
830
|
+
case 'approved':
|
|
831
|
+
return trans.__('Approved - Executing...');
|
|
832
|
+
case 'rejected':
|
|
833
|
+
return trans.__('Rejected');
|
|
834
|
+
case 'completed':
|
|
835
|
+
return trans.__('Completed');
|
|
836
|
+
case 'error':
|
|
837
|
+
return trans.__('Error');
|
|
854
838
|
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Options for building tool call HTML.
|
|
843
|
+
*/
|
|
844
|
+
interface IToolCallHtmlOptions {
|
|
845
|
+
toolName: string;
|
|
846
|
+
input: string;
|
|
847
|
+
status: ToolStatus;
|
|
848
|
+
output?: string;
|
|
849
|
+
approvalId?: string;
|
|
850
|
+
trans: TranslationBundle;
|
|
855
851
|
}
|
|
856
852
|
|
|
857
853
|
/**
|
|
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
|
|
854
|
+
* Builds HTML for a tool call display.
|
|
862
855
|
*/
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
status
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
);
|
|
871
|
-
if (existingMessageIndex !== -1) {
|
|
872
|
-
const existingMessage = this.messages[existingMessageIndex];
|
|
856
|
+
export function buildToolCallHtml(options: IToolCallHtmlOptions): string {
|
|
857
|
+
const { toolName, input, status, output, approvalId, trans } = options;
|
|
858
|
+
const config = STATUS_CONFIG[status];
|
|
859
|
+
const statusText = getStatusText(status, trans);
|
|
860
|
+
const escapedToolName = escapeHtml(toolName);
|
|
861
|
+
const escapedInput = escapeHtml(input);
|
|
862
|
+
const openAttr = config.open ? ' open' : '';
|
|
873
863
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
864
|
+
let bodyContent = `
|
|
865
|
+
<div class="jp-ai-tool-section">
|
|
866
|
+
<div class="jp-ai-tool-label">${trans.__('Input')}</div>
|
|
867
|
+
<pre class="jp-ai-tool-code"><code>${escapedInput}</code></pre>
|
|
868
|
+
</div>`;
|
|
869
|
+
|
|
870
|
+
// Add approval buttons if awaiting approval
|
|
871
|
+
if (status === 'awaiting_approval' && approvalId) {
|
|
872
|
+
bodyContent += `
|
|
873
|
+
<div class="jp-ai-tool-approval-buttons jp-ai-approval-id--${approvalId}">
|
|
874
|
+
<button class="jp-ai-approval-btn jp-ai-approval-approve">${trans.__('Approve')}</button>
|
|
875
|
+
<button class="jp-ai-approval-btn jp-ai-approval-reject">${trans.__('Reject')}</button>
|
|
876
|
+
</div>`;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Add output/result section if provided
|
|
880
|
+
if (output !== undefined) {
|
|
881
|
+
const escapedOutput = escapeHtml(output);
|
|
882
|
+
const label = status === 'error' ? trans.__('Error') : trans.__('Result');
|
|
883
|
+
bodyContent += `
|
|
884
|
+
<div class="jp-ai-tool-section">
|
|
885
|
+
<div class="jp-ai-tool-label">${label}</div>
|
|
886
|
+
<pre class="jp-ai-tool-code"><code>${escapedOutput}</code></pre>
|
|
887
|
+
</div>`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return `<details class="jp-ai-tool-call ${config.cssClass}"${openAttr}>
|
|
894
891
|
<summary class="jp-ai-tool-header">
|
|
895
892
|
<div class="jp-ai-tool-icon">⚡</div>
|
|
896
|
-
<div class="jp-ai-tool-title">${
|
|
897
|
-
<div class="jp-ai-tool-status ${
|
|
893
|
+
<div class="jp-ai-tool-title">${escapedToolName}</div>
|
|
894
|
+
<div class="jp-ai-tool-status ${config.statusClass}">${statusText}</div>
|
|
898
895
|
</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>
|
|
903
|
-
</div>
|
|
896
|
+
<div class="jp-ai-tool-body">${bodyContent}
|
|
904
897
|
</div>
|
|
905
|
-
</details
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
this.messageAdded(updatedMessage);
|
|
909
|
-
}
|
|
898
|
+
</details>`;
|
|
910
899
|
}
|
|
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
900
|
}
|
|
920
901
|
|
|
921
902
|
/**
|
|
@@ -946,6 +927,10 @@ export namespace AIChatModel {
|
|
|
946
927
|
* Optional document manager for file operations
|
|
947
928
|
*/
|
|
948
929
|
documentManager?: IDocumentManager;
|
|
930
|
+
/**
|
|
931
|
+
* The application language translation bundle.
|
|
932
|
+
*/
|
|
933
|
+
trans: TranslationBundle;
|
|
949
934
|
}
|
|
950
935
|
|
|
951
936
|
/**
|