@jupyterlite/ai 0.16.0 → 0.18.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 +19 -10
- package/lib/agent.js +82 -46
- package/lib/chat-commands/clear.js +1 -1
- package/lib/chat-model-handler.d.ts +2 -3
- package/lib/chat-model-handler.js +6 -2
- package/lib/chat-model.d.ts +129 -26
- package/lib/chat-model.js +543 -160
- package/lib/components/clear-button.d.ts +1 -1
- package/lib/components/clear-button.js +1 -1
- package/lib/components/save-button.d.ts +2 -2
- package/lib/index.js +224 -59
- package/lib/models/settings-model.js +1 -0
- package/lib/providers/built-in-providers.js +1 -1
- package/lib/providers/{generated-context-windows.d.ts → generated-model-info.d.ts} +2 -2
- package/lib/providers/generated-model-info.js +502 -0
- package/lib/providers/model-info.d.ts +3 -0
- package/lib/providers/model-info.js +33 -0
- package/lib/tokens.d.ts +98 -15
- package/lib/tokens.js +1 -0
- package/lib/widgets/ai-settings.js +5 -0
- package/lib/widgets/main-area-chat.d.ts +3 -3
- package/lib/widgets/main-area-chat.js +9 -5
- package/package.json +3 -3
- package/schema/settings-model.json +6 -0
- package/src/agent.ts +100 -52
- package/src/chat-commands/clear.ts +1 -1
- package/src/chat-model-handler.ts +10 -3
- package/src/chat-model.ts +727 -210
- package/src/components/clear-button.tsx +3 -3
- package/src/components/save-button.tsx +3 -3
- package/src/index.ts +289 -83
- package/src/models/settings-model.ts +1 -0
- package/src/providers/built-in-providers.ts +1 -1
- package/src/providers/generated-model-info.ts +508 -0
- package/src/providers/model-info.ts +57 -0
- package/src/tokens.ts +100 -15
- package/src/widgets/ai-settings.tsx +26 -0
- package/src/widgets/main-area-chat.ts +14 -9
- package/lib/providers/generated-context-windows.js +0 -96
- package/src/providers/generated-context-windows.ts +0 -102
package/lib/chat-model.js
CHANGED
|
@@ -5,6 +5,7 @@ import { UUID } from '@lumino/coreutils';
|
|
|
5
5
|
import { Debouncer } from '@lumino/polling';
|
|
6
6
|
import { Signal } from '@lumino/signaling';
|
|
7
7
|
import { AI_AVATAR } from './icons';
|
|
8
|
+
import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from './providers/model-info';
|
|
8
9
|
/**
|
|
9
10
|
* AI Chat Model implementation that provides chat functionality tool integration,
|
|
10
11
|
* and MCP server support.
|
|
@@ -27,10 +28,14 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
27
28
|
this._user = options.user;
|
|
28
29
|
this._agentManager = options.agentManager;
|
|
29
30
|
this._contentsManager = options.contentsManager;
|
|
31
|
+
this._providerRegistry = options.providerRegistry;
|
|
30
32
|
// Listen for agent events
|
|
31
33
|
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
|
|
32
34
|
// Listen for settings changes to update chat behavior
|
|
33
35
|
this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
|
|
36
|
+
// Rebuild history when the model changes
|
|
37
|
+
this._agentManager.activeProviderChanged.connect(this._onModelChanged, this);
|
|
38
|
+
this._settingsModel.stateChanged.connect(this._onModelChanged, this);
|
|
34
39
|
this._autosaveDebouncer = new Debouncer(this.save, 3000);
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
@@ -49,6 +54,31 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
49
54
|
}
|
|
50
55
|
this.setReady();
|
|
51
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A signal emitting when the chat name has changed.
|
|
59
|
+
*/
|
|
60
|
+
get nameChanged() {
|
|
61
|
+
return this._nameChanged;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The title of the chat.
|
|
65
|
+
*/
|
|
66
|
+
get title() {
|
|
67
|
+
return this._title;
|
|
68
|
+
}
|
|
69
|
+
set title(value) {
|
|
70
|
+
this._title = value;
|
|
71
|
+
if (this.autosave) {
|
|
72
|
+
this._autosaveDebouncer.invoke();
|
|
73
|
+
}
|
|
74
|
+
this._titleChanged.emit(this._title);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A signal emitting when the chat title has changed.
|
|
78
|
+
*/
|
|
79
|
+
get titleChanged() {
|
|
80
|
+
return this._titleChanged;
|
|
81
|
+
}
|
|
52
82
|
/**
|
|
53
83
|
* Whether to save the chat automatically.
|
|
54
84
|
*/
|
|
@@ -56,17 +86,20 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
56
86
|
return this._autosave;
|
|
57
87
|
}
|
|
58
88
|
set autosave(value) {
|
|
89
|
+
if (value === this._autosave) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
59
92
|
this._autosave = value;
|
|
60
93
|
this._autosaveChanged.emit(value);
|
|
61
94
|
if (value) {
|
|
62
95
|
this.messagesUpdated.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
63
96
|
this.messageChanged.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
64
|
-
this._autosaveDebouncer.invoke();
|
|
65
97
|
}
|
|
66
98
|
else {
|
|
67
99
|
this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
68
100
|
this.messageChanged.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
69
101
|
}
|
|
102
|
+
this._autosaveDebouncer.invoke();
|
|
70
103
|
}
|
|
71
104
|
/**
|
|
72
105
|
* A signal emitting when the autosave flag changed.
|
|
@@ -74,12 +107,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
74
107
|
get autosaveChanged() {
|
|
75
108
|
return this._autosaveChanged;
|
|
76
109
|
}
|
|
77
|
-
/**
|
|
78
|
-
* A signal emitting when the chat name has changed.
|
|
79
|
-
*/
|
|
80
|
-
get nameChanged() {
|
|
81
|
-
return this._nameChanged;
|
|
82
|
-
}
|
|
83
110
|
/**
|
|
84
111
|
* Gets the current user information.
|
|
85
112
|
*/
|
|
@@ -93,7 +120,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
93
120
|
return this._agentManager.tokenUsageChanged;
|
|
94
121
|
}
|
|
95
122
|
/**
|
|
96
|
-
*
|
|
123
|
+
* The agent manager used in the model.
|
|
97
124
|
*/
|
|
98
125
|
get agentManager() {
|
|
99
126
|
return this._agentManager;
|
|
@@ -108,6 +135,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
108
135
|
* Dispose of the model.
|
|
109
136
|
*/
|
|
110
137
|
dispose() {
|
|
138
|
+
this.stopStreaming();
|
|
111
139
|
this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
112
140
|
super.dispose();
|
|
113
141
|
}
|
|
@@ -123,7 +151,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
123
151
|
stopStreaming: () => this.stopStreaming(),
|
|
124
152
|
clearMessages: () => this.clearMessages(),
|
|
125
153
|
agentManager: this._agentManager,
|
|
126
|
-
addSystemMessage: (body) => this.
|
|
154
|
+
addSystemMessage: (body) => this._addSystemMessage(body)
|
|
127
155
|
};
|
|
128
156
|
}
|
|
129
157
|
/**
|
|
@@ -135,15 +163,30 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
135
163
|
/**
|
|
136
164
|
* Clears all messages from the chat and resets conversation state.
|
|
137
165
|
*/
|
|
138
|
-
clearMessages = () => {
|
|
166
|
+
clearMessages = async () => {
|
|
167
|
+
this.stopStreaming();
|
|
168
|
+
this._messageQueue = [];
|
|
169
|
+
this._isBusy = false;
|
|
170
|
+
this._queueMessageId = null;
|
|
171
|
+
this._currentStreamingMessage = null;
|
|
139
172
|
this.messagesDeleted(0, this.messages.length);
|
|
173
|
+
this.title = null;
|
|
140
174
|
this._toolContexts.clear();
|
|
141
|
-
this._agentManager.clearHistory();
|
|
175
|
+
await this._agentManager.clearHistory();
|
|
142
176
|
};
|
|
177
|
+
/**
|
|
178
|
+
* Overrides messageAdded to ensure queued messages stay at the bottom.
|
|
179
|
+
*/
|
|
180
|
+
messageAdded(message) {
|
|
181
|
+
super.messageAdded(message);
|
|
182
|
+
if (this._queueMessageId && message.id !== this._queueMessageId) {
|
|
183
|
+
this._updateQueueUI();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
143
186
|
/**
|
|
144
187
|
* Adds a non-user message to the chat (used by chat commands).
|
|
145
188
|
*/
|
|
146
|
-
|
|
189
|
+
_addSystemMessage(body) {
|
|
147
190
|
const message = {
|
|
148
191
|
body,
|
|
149
192
|
sender: this._getAIUser(),
|
|
@@ -174,9 +217,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
174
217
|
raw_time: false,
|
|
175
218
|
attachments: [...this.input.attachments]
|
|
176
219
|
};
|
|
177
|
-
this.messageAdded(userMessage);
|
|
178
220
|
// Check if we have valid configuration
|
|
179
221
|
if (!this._agentManager.hasValidConfig()) {
|
|
222
|
+
this.messageAdded(userMessage);
|
|
180
223
|
const errorMessage = {
|
|
181
224
|
body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
|
|
182
225
|
sender: this._getAIUser(),
|
|
@@ -188,23 +231,48 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
188
231
|
this.messageAdded(errorMessage);
|
|
189
232
|
return;
|
|
190
233
|
}
|
|
234
|
+
if (this._isBusy) {
|
|
235
|
+
this._messageQueue.push({
|
|
236
|
+
id: UUID.uuid4(),
|
|
237
|
+
body: message.body,
|
|
238
|
+
_originalMsg: userMessage
|
|
239
|
+
});
|
|
240
|
+
this.input.clearAttachments();
|
|
241
|
+
this._updateQueueUI();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this._isBusy = true;
|
|
245
|
+
this.messageAdded(userMessage);
|
|
246
|
+
this.input.clearAttachments();
|
|
247
|
+
await this._processMessage(userMessage);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Internal method to process attachments and send the message to the agent.
|
|
251
|
+
*/
|
|
252
|
+
async _processMessage(userMessage) {
|
|
191
253
|
try {
|
|
192
|
-
// Process attachments and add their content to the message
|
|
193
|
-
let enhancedMessage = message.body;
|
|
194
|
-
if (this.input.attachments.length > 0) {
|
|
195
|
-
const attachmentContents = await this._processAttachments(this.input.attachments);
|
|
196
|
-
this.input.clearAttachments();
|
|
197
|
-
if (attachmentContents.length > 0) {
|
|
198
|
-
enhancedMessage +=
|
|
199
|
-
'\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
254
|
this.updateWriters([{ user: this._getAIUser() }]);
|
|
255
|
+
let enhancedMessage = userMessage.body;
|
|
256
|
+
if (userMessage.attachments && userMessage.attachments.length > 0) {
|
|
257
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
258
|
+
const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
|
|
259
|
+
const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
|
|
260
|
+
const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
|
|
261
|
+
enhancedMessage = await Private.processAttachments(userMessage.attachments, this.input.documentManager, userMessage.body, supportsImages, supportsPdf, supportsAudio);
|
|
262
|
+
}
|
|
203
263
|
await this._agentManager.generateResponse(enhancedMessage);
|
|
204
264
|
}
|
|
205
265
|
catch (error) {
|
|
206
266
|
const errorMessage = {
|
|
207
|
-
body:
|
|
267
|
+
body: '',
|
|
268
|
+
mime_model: {
|
|
269
|
+
data: {
|
|
270
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
271
|
+
},
|
|
272
|
+
metadata: {
|
|
273
|
+
errorMessage: `Error generating AI response: ${error.message}`
|
|
274
|
+
}
|
|
275
|
+
},
|
|
208
276
|
sender: this._getAIUser(),
|
|
209
277
|
id: UUID.uuid4(),
|
|
210
278
|
time: Date.now() / 1000,
|
|
@@ -214,8 +282,89 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
214
282
|
this.messageAdded(errorMessage);
|
|
215
283
|
}
|
|
216
284
|
finally {
|
|
285
|
+
this._drainQueue();
|
|
286
|
+
if (this._settingsModel.config.autoTitle &&
|
|
287
|
+
(this.messages.length <= 5 || this.title === null)) {
|
|
288
|
+
try {
|
|
289
|
+
this.title = await this.requestTitle();
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
console.warn('Error while generating a title\n', e);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Removes the message-queue chat component.
|
|
299
|
+
*/
|
|
300
|
+
_removeQueueUI() {
|
|
301
|
+
if (this._queueMessageId) {
|
|
302
|
+
const existingMsg = this.messages.find(msg => msg.id === this._queueMessageId);
|
|
303
|
+
if (existingMsg) {
|
|
304
|
+
const idx = this.messages.indexOf(existingMsg);
|
|
305
|
+
if (idx !== -1) {
|
|
306
|
+
this.messagesDeleted(idx, 1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this._queueMessageId = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Creates or updates the message-queue chat component.
|
|
314
|
+
*/
|
|
315
|
+
_updateQueueUI() {
|
|
316
|
+
this._removeQueueUI();
|
|
317
|
+
if (this._messageQueue.length === 0) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const queueBody = {
|
|
321
|
+
data: {
|
|
322
|
+
'application/vnd.jupyter.chat.components': 'message-queue'
|
|
323
|
+
},
|
|
324
|
+
metadata: {
|
|
325
|
+
messages: this._messageQueue.map(m => ({
|
|
326
|
+
id: m.id,
|
|
327
|
+
body: m.body,
|
|
328
|
+
attachments: m._originalMsg.attachments
|
|
329
|
+
})),
|
|
330
|
+
targetId: this.name
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
this._queueMessageId = UUID.uuid4();
|
|
334
|
+
const queueMessage = {
|
|
335
|
+
body: '',
|
|
336
|
+
mime_model: queueBody,
|
|
337
|
+
sender: { username: 'system', display_name: '' },
|
|
338
|
+
id: this._queueMessageId,
|
|
339
|
+
time: Date.now() / 1000,
|
|
340
|
+
type: 'msg',
|
|
341
|
+
raw_time: false
|
|
342
|
+
};
|
|
343
|
+
this.messageAdded(queueMessage);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Processes the next message in the queue, or marks the agent as idle.
|
|
347
|
+
*/
|
|
348
|
+
async _drainQueue() {
|
|
349
|
+
if (this._messageQueue.length === 0) {
|
|
350
|
+
this._isBusy = false;
|
|
217
351
|
this.updateWriters([]);
|
|
352
|
+
this._removeQueueUI();
|
|
353
|
+
return;
|
|
218
354
|
}
|
|
355
|
+
// Dequeue and push to chat
|
|
356
|
+
const next = this._messageQueue.shift();
|
|
357
|
+
next._originalMsg.time = Date.now() / 1000;
|
|
358
|
+
this.messageAdded(next._originalMsg);
|
|
359
|
+
await this._processMessage(next._originalMsg);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Removes a queued message by its ID.
|
|
363
|
+
* @param messageId The ID of the queued message to remove
|
|
364
|
+
*/
|
|
365
|
+
removeQueuedMessage(messageId) {
|
|
366
|
+
this._messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
|
|
367
|
+
this._updateQueueUI();
|
|
219
368
|
}
|
|
220
369
|
/**
|
|
221
370
|
* Save the chat as json file.
|
|
@@ -299,12 +448,33 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
299
448
|
attachments
|
|
300
449
|
};
|
|
301
450
|
});
|
|
302
|
-
this.clearMessages();
|
|
451
|
+
await this.clearMessages();
|
|
303
452
|
this.messagesInserted(0, messages);
|
|
304
|
-
this.
|
|
453
|
+
await this._rebuildHistory();
|
|
305
454
|
this.autosave = content.metadata?.autosave ?? false;
|
|
455
|
+
this.title = content.metadata?.title ?? null;
|
|
306
456
|
return true;
|
|
307
457
|
};
|
|
458
|
+
/**
|
|
459
|
+
* Request a title to this chat, regarding the message history.
|
|
460
|
+
*/
|
|
461
|
+
async requestTitle() {
|
|
462
|
+
const history = this.messages
|
|
463
|
+
.filter(msg => msg.body !== '')
|
|
464
|
+
.map(msg => `${msg.sender.username === 'ai-assistant' ? 'assistant' : 'user'}: ${msg.body}`)
|
|
465
|
+
.join('\n');
|
|
466
|
+
const messages = [
|
|
467
|
+
{
|
|
468
|
+
role: 'system',
|
|
469
|
+
content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting, quotes, or punctuation. Focus on the subject matter and specific content the user is working on, not on the actions taken (e.g. prefer 'Pandas DataFrame filtering' over 'Opening a notebook'). The title should be a noun phrase describing the topic."
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
role: 'user',
|
|
473
|
+
content: history
|
|
474
|
+
}
|
|
475
|
+
];
|
|
476
|
+
return this.agentManager.textResponse(messages);
|
|
477
|
+
}
|
|
308
478
|
/**
|
|
309
479
|
* Serialize the model for backup
|
|
310
480
|
*/
|
|
@@ -315,6 +485,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
315
485
|
const attachmentMap = new Map(); // JSON → index
|
|
316
486
|
const attachmentsList = []; // Actual attachments
|
|
317
487
|
this.messages.forEach(message => {
|
|
488
|
+
if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
318
491
|
let attachmentIndexes = [];
|
|
319
492
|
if (message.attachments) {
|
|
320
493
|
attachmentIndexes = message.attachments.map(attachment => {
|
|
@@ -348,7 +521,8 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
348
521
|
attachments,
|
|
349
522
|
metadata: {
|
|
350
523
|
provider,
|
|
351
|
-
autosave: this.autosave
|
|
524
|
+
autosave: this.autosave,
|
|
525
|
+
...(this.title ? { title: this.title } : {})
|
|
352
526
|
}
|
|
353
527
|
};
|
|
354
528
|
}
|
|
@@ -372,6 +546,49 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
372
546
|
this.config = { ...config, enableCodeToolbar: true };
|
|
373
547
|
// Agent manager handles agent recreation automatically via its own settings listener
|
|
374
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Rebuild history when the active model changes.
|
|
551
|
+
*/
|
|
552
|
+
_onModelChanged() {
|
|
553
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
554
|
+
const modelKey = providerConfig
|
|
555
|
+
? `${providerConfig.provider}:${providerConfig.model}`
|
|
556
|
+
: undefined;
|
|
557
|
+
if (modelKey && modelKey !== this._currentModelKey) {
|
|
558
|
+
this._currentModelKey = modelKey;
|
|
559
|
+
this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Rebuilds the agent history from the current messages.
|
|
564
|
+
* For vision-capable models, re-reads binary attachments from disk.
|
|
565
|
+
* For text-only models, uses message text only.
|
|
566
|
+
*/
|
|
567
|
+
async _rebuildHistory() {
|
|
568
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
569
|
+
const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
|
|
570
|
+
const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
|
|
571
|
+
const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
|
|
572
|
+
const modelMessages = [];
|
|
573
|
+
for (const msg of this.messages) {
|
|
574
|
+
const isAI = msg.sender.username === 'ai-assistant';
|
|
575
|
+
if (!isAI && msg.attachments?.length) {
|
|
576
|
+
const enhancedContent = await Private.processAttachments(msg.attachments, this.input.documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
|
|
577
|
+
modelMessages.push({
|
|
578
|
+
role: 'user',
|
|
579
|
+
content: enhancedContent
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else if (msg.body) {
|
|
583
|
+
modelMessages.push({
|
|
584
|
+
role: isAI ? 'assistant' : 'user',
|
|
585
|
+
content: msg.body
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// Skip messages with empty body like tool calls
|
|
589
|
+
}
|
|
590
|
+
this._agentManager.setHistory(modelMessages);
|
|
591
|
+
}
|
|
375
592
|
/**
|
|
376
593
|
* Handles events emitted by the agent manager.
|
|
377
594
|
* @param event The event data containing type and payload
|
|
@@ -535,13 +752,18 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
535
752
|
body: '',
|
|
536
753
|
mime_model: {
|
|
537
754
|
data: {
|
|
538
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
755
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
539
756
|
},
|
|
540
757
|
metadata: {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
758
|
+
toolCalls: [
|
|
759
|
+
{
|
|
760
|
+
toolCallId: context.toolCallId,
|
|
761
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
762
|
+
kind: context.toolName,
|
|
763
|
+
status: 'in_progress',
|
|
764
|
+
rawInput: context.input
|
|
765
|
+
}
|
|
766
|
+
]
|
|
545
767
|
}
|
|
546
768
|
},
|
|
547
769
|
sender: this._getAIUser(),
|
|
@@ -594,7 +816,15 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
594
816
|
*/
|
|
595
817
|
_handleErrorEvent(event) {
|
|
596
818
|
this.messageAdded({
|
|
597
|
-
body:
|
|
819
|
+
body: '',
|
|
820
|
+
mime_model: {
|
|
821
|
+
data: {
|
|
822
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
823
|
+
},
|
|
824
|
+
metadata: {
|
|
825
|
+
errorMessage: `Error generating response: ${event.data.error.message}`
|
|
826
|
+
}
|
|
827
|
+
},
|
|
598
828
|
sender: this._getAIUser(),
|
|
599
829
|
id: UUID.uuid4(),
|
|
600
830
|
time: Date.now() / 1000,
|
|
@@ -610,7 +840,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
610
840
|
if (!context) {
|
|
611
841
|
return;
|
|
612
842
|
}
|
|
613
|
-
context.approvalId = event.data.approvalId;
|
|
614
843
|
context.input = JSON.stringify(event.data.args, null, 2);
|
|
615
844
|
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
616
845
|
}
|
|
@@ -618,12 +847,12 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
618
847
|
* Handles tool approval resolved events from the AI agent.
|
|
619
848
|
*/
|
|
620
849
|
_handleToolApprovalResolved(event) {
|
|
621
|
-
const context =
|
|
850
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
622
851
|
if (!context) {
|
|
623
852
|
return;
|
|
624
853
|
}
|
|
625
854
|
const status = event.data.approved ? 'approved' : 'rejected';
|
|
626
|
-
this._updateToolCallUI(
|
|
855
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
627
856
|
if (!event.data.approved) {
|
|
628
857
|
this._toolContexts.delete(context.toolCallId);
|
|
629
858
|
}
|
|
@@ -644,63 +873,308 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
644
873
|
existingMessage.update({
|
|
645
874
|
mime_model: {
|
|
646
875
|
data: {
|
|
647
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
876
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
648
877
|
},
|
|
649
878
|
metadata: {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
879
|
+
toolCalls: [
|
|
880
|
+
{
|
|
881
|
+
toolCallId: context.toolCallId,
|
|
882
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
883
|
+
kind: context.toolName,
|
|
884
|
+
status: context.status,
|
|
885
|
+
rawInput: context.input,
|
|
886
|
+
rawOutput: output,
|
|
887
|
+
sessionId: this.name,
|
|
888
|
+
permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
889
|
+
...(status === 'awaiting_approval' && {
|
|
890
|
+
permissionOptions: [
|
|
891
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
892
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
893
|
+
]
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
]
|
|
657
897
|
}
|
|
658
898
|
}
|
|
659
899
|
});
|
|
660
900
|
}
|
|
661
901
|
/**
|
|
662
|
-
*
|
|
902
|
+
* The current message queue
|
|
903
|
+
*/
|
|
904
|
+
get messageQueue() {
|
|
905
|
+
return this._messageQueue;
|
|
906
|
+
}
|
|
907
|
+
set messageQueue(value) {
|
|
908
|
+
this._messageQueue = value;
|
|
909
|
+
this._updateQueueUI();
|
|
910
|
+
if (this._messageQueue.length > 0 && !this._isBusy) {
|
|
911
|
+
this._drainQueue();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Whether the chat is busy
|
|
916
|
+
*/
|
|
917
|
+
get isBusy() {
|
|
918
|
+
return this._isBusy;
|
|
919
|
+
}
|
|
920
|
+
set isBusy(value) {
|
|
921
|
+
this._isBusy = value;
|
|
922
|
+
}
|
|
923
|
+
// Private fields
|
|
924
|
+
_settingsModel;
|
|
925
|
+
_user;
|
|
926
|
+
_toolContexts = new Map();
|
|
927
|
+
_agentManager;
|
|
928
|
+
_providerRegistry;
|
|
929
|
+
_currentModelKey;
|
|
930
|
+
_currentStreamingMessage = null;
|
|
931
|
+
_nameChanged = new Signal(this);
|
|
932
|
+
_contentsManager;
|
|
933
|
+
_autosave = false;
|
|
934
|
+
_autosaveChanged = new Signal(this);
|
|
935
|
+
_autosaveDebouncer;
|
|
936
|
+
_messageQueue = [];
|
|
937
|
+
_isBusy = false;
|
|
938
|
+
_queueMessageId = null;
|
|
939
|
+
_title = null;
|
|
940
|
+
_titleChanged = new Signal(this);
|
|
941
|
+
}
|
|
942
|
+
var Private;
|
|
943
|
+
(function (Private) {
|
|
944
|
+
const isPlainObject = (value) => {
|
|
945
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
946
|
+
};
|
|
947
|
+
const isDisplayOutput = (value) => {
|
|
948
|
+
if (!isPlainObject(value)) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
const output = value;
|
|
952
|
+
return (nbformat.isDisplayData(output) ||
|
|
953
|
+
nbformat.isDisplayUpdate(output) ||
|
|
954
|
+
nbformat.isExecuteResult(output));
|
|
955
|
+
};
|
|
956
|
+
const toMimeBundle = (value, trustedMimeTypes) => {
|
|
957
|
+
const data = value.data;
|
|
958
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
data: data,
|
|
963
|
+
...(isPlainObject(value.metadata)
|
|
964
|
+
? { metadata: value.metadata }
|
|
965
|
+
: {}),
|
|
966
|
+
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
967
|
+
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
968
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
969
|
+
? { trusted: true }
|
|
970
|
+
: {})
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
/**
|
|
974
|
+
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
975
|
+
*
|
|
976
|
+
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
977
|
+
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
978
|
+
*/
|
|
979
|
+
const toDisplayOutputs = (value) => {
|
|
980
|
+
if (isDisplayOutput(value)) {
|
|
981
|
+
return [value];
|
|
982
|
+
}
|
|
983
|
+
if (Array.isArray(value)) {
|
|
984
|
+
return value.filter(isDisplayOutput);
|
|
985
|
+
}
|
|
986
|
+
if (!isPlainObject(value)) {
|
|
987
|
+
return [];
|
|
988
|
+
}
|
|
989
|
+
if (Array.isArray(value.outputs)) {
|
|
990
|
+
return value.outputs.filter(isDisplayOutput);
|
|
991
|
+
}
|
|
992
|
+
if ('result' in value) {
|
|
993
|
+
return toDisplayOutputs(value.result);
|
|
994
|
+
}
|
|
995
|
+
return [];
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
999
|
+
*/
|
|
1000
|
+
function extractMimeBundlesFromUnknown(content, options = {}) {
|
|
1001
|
+
const bundles = [];
|
|
1002
|
+
const outputs = toDisplayOutputs(content);
|
|
1003
|
+
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1004
|
+
for (const output of outputs) {
|
|
1005
|
+
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1006
|
+
if (bundle) {
|
|
1007
|
+
bundles.push(bundle);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return bundles;
|
|
1011
|
+
}
|
|
1012
|
+
Private.extractMimeBundlesFromUnknown = extractMimeBundlesFromUnknown;
|
|
1013
|
+
function formatToolOutput(outputData) {
|
|
1014
|
+
if (typeof outputData === 'string') {
|
|
1015
|
+
return outputData;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
return JSON.stringify(outputData, null, 2);
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
return '[Complex object - cannot serialize]';
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
Private.formatToolOutput = formatToolOutput;
|
|
1025
|
+
/**
|
|
1026
|
+
* Processes file attachments and returns the message content with the attachments.
|
|
663
1027
|
* @param attachments Array of file attachments to process
|
|
664
|
-
* @
|
|
1028
|
+
* @param documentManager Optional document manager for file operations
|
|
1029
|
+
* @param body The message body
|
|
1030
|
+
* @param supportsImages Whether the model supports images
|
|
1031
|
+
* @param supportsPdf Whether the model supports pdfs
|
|
1032
|
+
* @param supportsAudio Whether the model supports audio
|
|
1033
|
+
* @returns Enhanced message content
|
|
665
1034
|
*/
|
|
666
|
-
async
|
|
667
|
-
const
|
|
1035
|
+
async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
|
|
1036
|
+
const textContents = [];
|
|
1037
|
+
const includedParts = [];
|
|
1038
|
+
const omittedNames = [];
|
|
1039
|
+
if (!documentManager) {
|
|
1040
|
+
return body;
|
|
1041
|
+
}
|
|
668
1042
|
for (const attachment of attachments) {
|
|
669
1043
|
try {
|
|
670
1044
|
if (attachment.type === 'notebook' && attachment.cells?.length) {
|
|
671
|
-
const cellContents = await
|
|
1045
|
+
const cellContents = await readNotebookCells(attachment, documentManager);
|
|
672
1046
|
if (cellContents) {
|
|
673
|
-
|
|
1047
|
+
textContents.push(cellContents);
|
|
674
1048
|
}
|
|
675
1049
|
}
|
|
676
1050
|
else {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1051
|
+
let mimetype = attachment.mimetype;
|
|
1052
|
+
const fileExtension = PathExt.extname(attachment.value).toLowerCase();
|
|
1053
|
+
// Fetch mimetype from server metadata if not provided
|
|
1054
|
+
if (!mimetype) {
|
|
1055
|
+
try {
|
|
1056
|
+
const diskModel = await documentManager.services.contents.get(attachment.value, { content: false });
|
|
1057
|
+
mimetype = diskModel?.mimetype;
|
|
1058
|
+
}
|
|
1059
|
+
catch (e) {
|
|
1060
|
+
console.warn(`Failed to fetch metadata for ${attachment.value}:`, e);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (mimetype?.startsWith('image/')) {
|
|
1064
|
+
if (supportsImages) {
|
|
1065
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1066
|
+
if (data) {
|
|
1067
|
+
includedParts.push({
|
|
1068
|
+
type: 'image',
|
|
1069
|
+
image: data,
|
|
1070
|
+
mediaType: mimetype
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else if (mimetype === 'application/pdf') {
|
|
1079
|
+
if (supportsPdf) {
|
|
1080
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1081
|
+
if (data) {
|
|
1082
|
+
includedParts.push({
|
|
1083
|
+
type: 'file',
|
|
1084
|
+
data,
|
|
1085
|
+
mediaType: mimetype,
|
|
1086
|
+
filename: PathExt.basename(attachment.value)
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else if (mimetype?.startsWith('audio/')) {
|
|
1095
|
+
if (supportsAudio) {
|
|
1096
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1097
|
+
if (data) {
|
|
1098
|
+
includedParts.push({
|
|
1099
|
+
type: 'file',
|
|
1100
|
+
data,
|
|
1101
|
+
mediaType: mimetype,
|
|
1102
|
+
filename: PathExt.basename(attachment.value)
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
const fileContent = await readFileAttachment(attachment, documentManager);
|
|
1112
|
+
if (fileContent) {
|
|
1113
|
+
const language = fileExtension === '.ipynb' ||
|
|
1114
|
+
mimetype === 'application/x-ipynb+json'
|
|
1115
|
+
? 'json'
|
|
1116
|
+
: '';
|
|
1117
|
+
textContents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
|
|
1118
|
+
}
|
|
682
1119
|
}
|
|
683
1120
|
}
|
|
684
1121
|
}
|
|
685
1122
|
catch (error) {
|
|
686
1123
|
console.warn(`Failed to read attachment ${attachment.value}:`, error);
|
|
687
|
-
|
|
1124
|
+
textContents.push(`**File: ${attachment.value}** (Could not read file)`);
|
|
688
1125
|
}
|
|
689
1126
|
}
|
|
690
|
-
|
|
1127
|
+
let textPart = body;
|
|
1128
|
+
if (textContents.length > 0) {
|
|
1129
|
+
textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
|
|
1130
|
+
}
|
|
1131
|
+
if (omittedNames.length > 0) {
|
|
1132
|
+
textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
|
|
1133
|
+
}
|
|
1134
|
+
return includedParts.length > 0
|
|
1135
|
+
? [{ type: 'text', text: textPart }, ...includedParts]
|
|
1136
|
+
: textPart;
|
|
691
1137
|
}
|
|
1138
|
+
Private.processAttachments = processAttachments;
|
|
1139
|
+
/**
|
|
1140
|
+
* Reads a binary attachment and returns its base64-encoded content.
|
|
1141
|
+
* @param attachment The attachment to read
|
|
1142
|
+
* @param documentManager Optional document manager for file operations
|
|
1143
|
+
* @returns Base64 string or null if unable to read
|
|
1144
|
+
*/
|
|
1145
|
+
async function readBinaryAttachment(attachment, documentManager) {
|
|
1146
|
+
if (!documentManager) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const diskModel = await documentManager.services.contents.get(attachment.value, { content: true });
|
|
1151
|
+
if (diskModel?.content && diskModel.format === 'base64') {
|
|
1152
|
+
// Strip whitespace/newlines
|
|
1153
|
+
return diskModel.content.replace(/\s/g, '');
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
catch (error) {
|
|
1158
|
+
console.warn(`Failed to read binary attachment ${attachment.value}:`, error);
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
Private.readBinaryAttachment = readBinaryAttachment;
|
|
692
1163
|
/**
|
|
693
1164
|
* Reads the content of a notebook cell.
|
|
694
1165
|
* @param attachment The notebook attachment to read
|
|
1166
|
+
* @param documentManager Optional document manager for file operations
|
|
695
1167
|
* @returns Cell content as string or null if unable to read
|
|
696
1168
|
*/
|
|
697
|
-
async
|
|
698
|
-
if (attachment.type !== 'notebook' ||
|
|
1169
|
+
async function readNotebookCells(attachment, documentManager) {
|
|
1170
|
+
if (attachment.type !== 'notebook' ||
|
|
1171
|
+
!attachment.cells ||
|
|
1172
|
+
!documentManager) {
|
|
699
1173
|
return null;
|
|
700
1174
|
}
|
|
701
1175
|
try {
|
|
702
1176
|
// Try reading from live notebook if open
|
|
703
|
-
const widget =
|
|
1177
|
+
const widget = documentManager.findWidget(attachment.value);
|
|
704
1178
|
let cellData;
|
|
705
1179
|
let kernelLang = 'text';
|
|
706
1180
|
const ymodel = widget?.context.model.sharedModel;
|
|
@@ -714,7 +1188,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
714
1188
|
}
|
|
715
1189
|
else {
|
|
716
1190
|
// Fallback: reading from disk
|
|
717
|
-
const model = await
|
|
1191
|
+
const model = await documentManager.services.contents.get(attachment.value);
|
|
718
1192
|
if (!model || model.type !== 'notebook') {
|
|
719
1193
|
return null;
|
|
720
1194
|
}
|
|
@@ -829,19 +1303,22 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
829
1303
|
return null;
|
|
830
1304
|
}
|
|
831
1305
|
}
|
|
1306
|
+
Private.readNotebookCells = readNotebookCells;
|
|
832
1307
|
/**
|
|
833
1308
|
* Reads the content of a file attachment.
|
|
834
1309
|
* @param attachment The file attachment to read
|
|
1310
|
+
* @param documentManager Optional document manager for file operations
|
|
835
1311
|
* @returns File content as string or null if unable to read
|
|
836
1312
|
*/
|
|
837
|
-
async
|
|
1313
|
+
async function readFileAttachment(attachment, documentManager) {
|
|
838
1314
|
// Handle both 'file' and 'notebook' types since both have a 'value' path
|
|
839
|
-
if (attachment.type !== 'file' && attachment.type !== 'notebook')
|
|
1315
|
+
if ((attachment.type !== 'file' && attachment.type !== 'notebook') ||
|
|
1316
|
+
!documentManager) {
|
|
840
1317
|
return null;
|
|
841
1318
|
}
|
|
842
1319
|
try {
|
|
843
1320
|
// Try reading from an open widget first
|
|
844
|
-
const widget =
|
|
1321
|
+
const widget = documentManager.findWidget(attachment.value);
|
|
845
1322
|
if (widget && widget.context && widget.context.model) {
|
|
846
1323
|
const model = widget.context.model;
|
|
847
1324
|
const ymodel = model.sharedModel;
|
|
@@ -853,7 +1330,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
853
1330
|
}
|
|
854
1331
|
}
|
|
855
1332
|
// If not open, load from disk
|
|
856
|
-
const diskModel = await
|
|
1333
|
+
const diskModel = await documentManager.services.contents.get(attachment.value);
|
|
857
1334
|
if (!diskModel?.content) {
|
|
858
1335
|
return null;
|
|
859
1336
|
}
|
|
@@ -879,99 +1356,5 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
879
1356
|
return null;
|
|
880
1357
|
}
|
|
881
1358
|
}
|
|
882
|
-
|
|
883
|
-
_settingsModel;
|
|
884
|
-
_user;
|
|
885
|
-
_toolContexts = new Map();
|
|
886
|
-
_agentManager;
|
|
887
|
-
_currentStreamingMessage = null;
|
|
888
|
-
_nameChanged = new Signal(this);
|
|
889
|
-
_contentsManager;
|
|
890
|
-
_autosave = false;
|
|
891
|
-
_autosaveChanged = new Signal(this);
|
|
892
|
-
_autosaveDebouncer;
|
|
893
|
-
}
|
|
894
|
-
var Private;
|
|
895
|
-
(function (Private) {
|
|
896
|
-
const isPlainObject = (value) => {
|
|
897
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
898
|
-
};
|
|
899
|
-
const isDisplayOutput = (value) => {
|
|
900
|
-
if (!isPlainObject(value)) {
|
|
901
|
-
return false;
|
|
902
|
-
}
|
|
903
|
-
const output = value;
|
|
904
|
-
return (nbformat.isDisplayData(output) ||
|
|
905
|
-
nbformat.isDisplayUpdate(output) ||
|
|
906
|
-
nbformat.isExecuteResult(output));
|
|
907
|
-
};
|
|
908
|
-
const toMimeBundle = (value, trustedMimeTypes) => {
|
|
909
|
-
const data = value.data;
|
|
910
|
-
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
data: data,
|
|
915
|
-
...(isPlainObject(value.metadata)
|
|
916
|
-
? { metadata: value.metadata }
|
|
917
|
-
: {}),
|
|
918
|
-
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
919
|
-
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
920
|
-
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
921
|
-
? { trusted: true }
|
|
922
|
-
: {})
|
|
923
|
-
};
|
|
924
|
-
};
|
|
925
|
-
/**
|
|
926
|
-
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
927
|
-
*
|
|
928
|
-
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
929
|
-
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
930
|
-
*/
|
|
931
|
-
const toDisplayOutputs = (value) => {
|
|
932
|
-
if (isDisplayOutput(value)) {
|
|
933
|
-
return [value];
|
|
934
|
-
}
|
|
935
|
-
if (Array.isArray(value)) {
|
|
936
|
-
return value.filter(isDisplayOutput);
|
|
937
|
-
}
|
|
938
|
-
if (!isPlainObject(value)) {
|
|
939
|
-
return [];
|
|
940
|
-
}
|
|
941
|
-
if (Array.isArray(value.outputs)) {
|
|
942
|
-
return value.outputs.filter(isDisplayOutput);
|
|
943
|
-
}
|
|
944
|
-
if ('result' in value) {
|
|
945
|
-
return toDisplayOutputs(value.result);
|
|
946
|
-
}
|
|
947
|
-
return [];
|
|
948
|
-
};
|
|
949
|
-
/**
|
|
950
|
-
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
951
|
-
*/
|
|
952
|
-
function extractMimeBundlesFromUnknown(content, options = {}) {
|
|
953
|
-
const bundles = [];
|
|
954
|
-
const outputs = toDisplayOutputs(content);
|
|
955
|
-
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
956
|
-
for (const output of outputs) {
|
|
957
|
-
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
958
|
-
if (bundle) {
|
|
959
|
-
bundles.push(bundle);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
return bundles;
|
|
963
|
-
}
|
|
964
|
-
Private.extractMimeBundlesFromUnknown = extractMimeBundlesFromUnknown;
|
|
965
|
-
function formatToolOutput(outputData) {
|
|
966
|
-
if (typeof outputData === 'string') {
|
|
967
|
-
return outputData;
|
|
968
|
-
}
|
|
969
|
-
try {
|
|
970
|
-
return JSON.stringify(outputData, null, 2);
|
|
971
|
-
}
|
|
972
|
-
catch {
|
|
973
|
-
return '[Complex object - cannot serialize]';
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
Private.formatToolOutput = formatToolOutput;
|
|
1359
|
+
Private.readFileAttachment = readFileAttachment;
|
|
977
1360
|
})(Private || (Private = {}));
|