@jupyterlite/ai 0.17.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 +12 -9
- package/lib/agent.js +38 -51
- package/lib/chat-model-handler.d.ts +2 -3
- package/lib/chat-model-handler.js +2 -1
- package/lib/chat-model.d.ts +110 -8
- package/lib/chat-model.js +322 -63
- package/lib/components/save-button.d.ts +2 -2
- package/lib/index.js +29 -49
- 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 +85 -12
- package/lib/widgets/ai-settings.js +5 -0
- package/lib/widgets/main-area-chat.d.ts +2 -3
- package/lib/widgets/main-area-chat.js +2 -4
- package/package.json +3 -3
- package/schema/settings-model.json +6 -0
- package/src/agent.ts +46 -56
- package/src/chat-model-handler.ts +4 -2
- package/src/chat-model.ts +435 -90
- package/src/components/save-button.tsx +3 -3
- package/src/index.ts +52 -74
- 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 +87 -12
- package/src/widgets/ai-settings.tsx +26 -0
- package/src/widgets/main-area-chat.ts +5 -8
- 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
|
/**
|
|
@@ -81,17 +86,20 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
81
86
|
return this._autosave;
|
|
82
87
|
}
|
|
83
88
|
set autosave(value) {
|
|
89
|
+
if (value === this._autosave) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
84
92
|
this._autosave = value;
|
|
85
93
|
this._autosaveChanged.emit(value);
|
|
86
94
|
if (value) {
|
|
87
95
|
this.messagesUpdated.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
88
96
|
this.messageChanged.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
89
|
-
this._autosaveDebouncer.invoke();
|
|
90
97
|
}
|
|
91
98
|
else {
|
|
92
99
|
this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
93
100
|
this.messageChanged.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
94
101
|
}
|
|
102
|
+
this._autosaveDebouncer.invoke();
|
|
95
103
|
}
|
|
96
104
|
/**
|
|
97
105
|
* A signal emitting when the autosave flag changed.
|
|
@@ -112,7 +120,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
112
120
|
return this._agentManager.tokenUsageChanged;
|
|
113
121
|
}
|
|
114
122
|
/**
|
|
115
|
-
*
|
|
123
|
+
* The agent manager used in the model.
|
|
116
124
|
*/
|
|
117
125
|
get agentManager() {
|
|
118
126
|
return this._agentManager;
|
|
@@ -127,6 +135,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
127
135
|
* Dispose of the model.
|
|
128
136
|
*/
|
|
129
137
|
dispose() {
|
|
138
|
+
this.stopStreaming();
|
|
130
139
|
this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
|
|
131
140
|
super.dispose();
|
|
132
141
|
}
|
|
@@ -142,7 +151,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
142
151
|
stopStreaming: () => this.stopStreaming(),
|
|
143
152
|
clearMessages: () => this.clearMessages(),
|
|
144
153
|
agentManager: this._agentManager,
|
|
145
|
-
addSystemMessage: (body) => this.
|
|
154
|
+
addSystemMessage: (body) => this._addSystemMessage(body)
|
|
146
155
|
};
|
|
147
156
|
}
|
|
148
157
|
/**
|
|
@@ -155,14 +164,29 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
155
164
|
* Clears all messages from the chat and resets conversation state.
|
|
156
165
|
*/
|
|
157
166
|
clearMessages = async () => {
|
|
167
|
+
this.stopStreaming();
|
|
168
|
+
this._messageQueue = [];
|
|
169
|
+
this._isBusy = false;
|
|
170
|
+
this._queueMessageId = null;
|
|
171
|
+
this._currentStreamingMessage = null;
|
|
158
172
|
this.messagesDeleted(0, this.messages.length);
|
|
173
|
+
this.title = null;
|
|
159
174
|
this._toolContexts.clear();
|
|
160
175
|
await this._agentManager.clearHistory();
|
|
161
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
|
+
}
|
|
162
186
|
/**
|
|
163
187
|
* Adds a non-user message to the chat (used by chat commands).
|
|
164
188
|
*/
|
|
165
|
-
|
|
189
|
+
_addSystemMessage(body) {
|
|
166
190
|
const message = {
|
|
167
191
|
body,
|
|
168
192
|
sender: this._getAIUser(),
|
|
@@ -193,9 +217,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
193
217
|
raw_time: false,
|
|
194
218
|
attachments: [...this.input.attachments]
|
|
195
219
|
};
|
|
196
|
-
this.messageAdded(userMessage);
|
|
197
220
|
// Check if we have valid configuration
|
|
198
221
|
if (!this._agentManager.hasValidConfig()) {
|
|
222
|
+
this.messageAdded(userMessage);
|
|
199
223
|
const errorMessage = {
|
|
200
224
|
body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
|
|
201
225
|
sender: this._getAIUser(),
|
|
@@ -207,30 +231,48 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
207
231
|
this.messageAdded(errorMessage);
|
|
208
232
|
return;
|
|
209
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) {
|
|
210
253
|
try {
|
|
211
|
-
// Process attachments and add their content to the message
|
|
212
|
-
let enhancedMessage = message.body;
|
|
213
|
-
if (this.input.attachments.length > 0) {
|
|
214
|
-
const { textContents, binaryParts } = await Private.processAttachments(this.input.attachments, this.input.documentManager);
|
|
215
|
-
this.input.clearAttachments();
|
|
216
|
-
let textPart = message.body;
|
|
217
|
-
if (textContents.length > 0) {
|
|
218
|
-
textPart +=
|
|
219
|
-
'\n\n--- Attached Files ---\n' + textContents.join('\n\n');
|
|
220
|
-
}
|
|
221
|
-
if (binaryParts.length > 0) {
|
|
222
|
-
enhancedMessage = [{ type: 'text', text: textPart }, ...binaryParts];
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
enhancedMessage = textPart;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
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
|
+
}
|
|
229
263
|
await this._agentManager.generateResponse(enhancedMessage);
|
|
230
264
|
}
|
|
231
265
|
catch (error) {
|
|
232
266
|
const errorMessage = {
|
|
233
|
-
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
|
+
},
|
|
234
276
|
sender: this._getAIUser(),
|
|
235
277
|
id: UUID.uuid4(),
|
|
236
278
|
time: Date.now() / 1000,
|
|
@@ -240,8 +282,89 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
240
282
|
this.messageAdded(errorMessage);
|
|
241
283
|
}
|
|
242
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;
|
|
243
351
|
this.updateWriters([]);
|
|
352
|
+
this._removeQueueUI();
|
|
353
|
+
return;
|
|
244
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();
|
|
245
368
|
}
|
|
246
369
|
/**
|
|
247
370
|
* Save the chat as json file.
|
|
@@ -327,7 +450,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
327
450
|
});
|
|
328
451
|
await this.clearMessages();
|
|
329
452
|
this.messagesInserted(0, messages);
|
|
330
|
-
this.
|
|
453
|
+
await this._rebuildHistory();
|
|
331
454
|
this.autosave = content.metadata?.autosave ?? false;
|
|
332
455
|
this.title = content.metadata?.title ?? null;
|
|
333
456
|
return true;
|
|
@@ -343,7 +466,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
343
466
|
const messages = [
|
|
344
467
|
{
|
|
345
468
|
role: 'system',
|
|
346
|
-
content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting. Focus on the user'
|
|
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."
|
|
347
470
|
},
|
|
348
471
|
{
|
|
349
472
|
role: 'user',
|
|
@@ -362,6 +485,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
362
485
|
const attachmentMap = new Map(); // JSON → index
|
|
363
486
|
const attachmentsList = []; // Actual attachments
|
|
364
487
|
this.messages.forEach(message => {
|
|
488
|
+
if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
365
491
|
let attachmentIndexes = [];
|
|
366
492
|
if (message.attachments) {
|
|
367
493
|
attachmentIndexes = message.attachments.map(attachment => {
|
|
@@ -420,6 +546,49 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
420
546
|
this.config = { ...config, enableCodeToolbar: true };
|
|
421
547
|
// Agent manager handles agent recreation automatically via its own settings listener
|
|
422
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
|
+
}
|
|
423
592
|
/**
|
|
424
593
|
* Handles events emitted by the agent manager.
|
|
425
594
|
* @param event The event data containing type and payload
|
|
@@ -583,13 +752,18 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
583
752
|
body: '',
|
|
584
753
|
mime_model: {
|
|
585
754
|
data: {
|
|
586
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
755
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
587
756
|
},
|
|
588
757
|
metadata: {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
+
]
|
|
593
767
|
}
|
|
594
768
|
},
|
|
595
769
|
sender: this._getAIUser(),
|
|
@@ -642,7 +816,15 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
642
816
|
*/
|
|
643
817
|
_handleErrorEvent(event) {
|
|
644
818
|
this.messageAdded({
|
|
645
|
-
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
|
+
},
|
|
646
828
|
sender: this._getAIUser(),
|
|
647
829
|
id: UUID.uuid4(),
|
|
648
830
|
time: Date.now() / 1000,
|
|
@@ -658,7 +840,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
658
840
|
if (!context) {
|
|
659
841
|
return;
|
|
660
842
|
}
|
|
661
|
-
context.approvalId = event.data.approvalId;
|
|
662
843
|
context.input = JSON.stringify(event.data.args, null, 2);
|
|
663
844
|
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
664
845
|
}
|
|
@@ -666,12 +847,12 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
666
847
|
* Handles tool approval resolved events from the AI agent.
|
|
667
848
|
*/
|
|
668
849
|
_handleToolApprovalResolved(event) {
|
|
669
|
-
const context =
|
|
850
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
670
851
|
if (!context) {
|
|
671
852
|
return;
|
|
672
853
|
}
|
|
673
854
|
const status = event.data.approved ? 'approved' : 'rejected';
|
|
674
|
-
this._updateToolCallUI(
|
|
855
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
675
856
|
if (!event.data.approved) {
|
|
676
857
|
this._toolContexts.delete(context.toolCallId);
|
|
677
858
|
}
|
|
@@ -692,31 +873,69 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
692
873
|
existingMessage.update({
|
|
693
874
|
mime_model: {
|
|
694
875
|
data: {
|
|
695
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
876
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
696
877
|
},
|
|
697
878
|
metadata: {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
+
]
|
|
705
897
|
}
|
|
706
898
|
}
|
|
707
899
|
});
|
|
708
900
|
}
|
|
901
|
+
/**
|
|
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
|
+
}
|
|
709
923
|
// Private fields
|
|
710
924
|
_settingsModel;
|
|
711
925
|
_user;
|
|
712
926
|
_toolContexts = new Map();
|
|
713
927
|
_agentManager;
|
|
928
|
+
_providerRegistry;
|
|
929
|
+
_currentModelKey;
|
|
714
930
|
_currentStreamingMessage = null;
|
|
715
931
|
_nameChanged = new Signal(this);
|
|
716
932
|
_contentsManager;
|
|
717
933
|
_autosave = false;
|
|
718
934
|
_autosaveChanged = new Signal(this);
|
|
719
935
|
_autosaveDebouncer;
|
|
936
|
+
_messageQueue = [];
|
|
937
|
+
_isBusy = false;
|
|
938
|
+
_queueMessageId = null;
|
|
720
939
|
_title = null;
|
|
721
940
|
_titleChanged = new Signal(this);
|
|
722
941
|
}
|
|
@@ -804,16 +1023,21 @@ var Private;
|
|
|
804
1023
|
}
|
|
805
1024
|
Private.formatToolOutput = formatToolOutput;
|
|
806
1025
|
/**
|
|
807
|
-
* Processes file attachments and returns
|
|
1026
|
+
* Processes file attachments and returns the message content with the attachments.
|
|
808
1027
|
* @param attachments Array of file attachments to process
|
|
809
1028
|
* @param documentManager Optional document manager for file operations
|
|
810
|
-
* @
|
|
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
|
|
811
1034
|
*/
|
|
812
|
-
async function processAttachments(attachments, documentManager) {
|
|
1035
|
+
async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
|
|
813
1036
|
const textContents = [];
|
|
814
|
-
const
|
|
1037
|
+
const includedParts = [];
|
|
1038
|
+
const omittedNames = [];
|
|
815
1039
|
if (!documentManager) {
|
|
816
|
-
return
|
|
1040
|
+
return body;
|
|
817
1041
|
}
|
|
818
1042
|
for (const attachment of attachments) {
|
|
819
1043
|
try {
|
|
@@ -837,24 +1061,50 @@ var Private;
|
|
|
837
1061
|
}
|
|
838
1062
|
}
|
|
839
1063
|
if (mimetype?.startsWith('image/')) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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));
|
|
847
1076
|
}
|
|
848
1077
|
}
|
|
849
1078
|
else if (mimetype === 'application/pdf') {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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));
|
|
858
1108
|
}
|
|
859
1109
|
}
|
|
860
1110
|
else {
|
|
@@ -874,7 +1124,16 @@ var Private;
|
|
|
874
1124
|
textContents.push(`**File: ${attachment.value}** (Could not read file)`);
|
|
875
1125
|
}
|
|
876
1126
|
}
|
|
877
|
-
|
|
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;
|
|
878
1137
|
}
|
|
879
1138
|
Private.processAttachments = processAttachments;
|
|
880
1139
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
2
2
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { IAIChatModel } from '../tokens';
|
|
5
5
|
/**
|
|
6
6
|
* Properties for the SaveButton component.
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ export interface ISaveButtonProps {
|
|
|
9
9
|
/**
|
|
10
10
|
* The chat model, used to listen for message changes for auto-save.
|
|
11
11
|
*/
|
|
12
|
-
model:
|
|
12
|
+
model: IAIChatModel;
|
|
13
13
|
/**
|
|
14
14
|
* The application language translator.
|
|
15
15
|
*/
|