@jupyterlite/ai 0.17.0 → 0.19.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/chat-commands/clear.d.ts +1 -0
- package/lib/chat-commands/index.d.ts +1 -0
- package/lib/chat-commands/skills.d.ts +2 -1
- package/lib/chat-model-handler.d.ts +4 -3
- package/lib/chat-model-handler.js +2 -1
- package/lib/chat-model.d.ts +148 -8
- package/lib/chat-model.js +368 -79
- package/lib/completion/completion-provider.d.ts +3 -1
- package/lib/completion/completion-provider.js +1 -2
- package/lib/completion/index.d.ts +1 -0
- package/lib/components/clear-button.d.ts +1 -0
- package/lib/components/clear-button.js +3 -4
- package/lib/components/completion-status.d.ts +1 -0
- package/lib/components/completion-status.js +5 -4
- package/lib/components/index.d.ts +1 -0
- package/lib/components/model-select.d.ts +1 -0
- package/lib/components/model-select.js +62 -67
- package/lib/components/save-button.d.ts +3 -2
- package/lib/components/save-button.js +4 -5
- package/lib/components/stop-button.d.ts +1 -0
- package/lib/components/stop-button.js +3 -4
- package/lib/components/tool-select.d.ts +3 -1
- package/lib/components/tool-select.js +47 -60
- package/lib/components/usage-display.d.ts +4 -2
- package/lib/components/usage-display.js +50 -61
- package/lib/diff-manager.d.ts +3 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +50 -59
- package/lib/models/settings-model.d.ts +3 -1
- package/lib/models/settings-model.js +1 -0
- package/lib/rendered-message-outputarea.d.ts +1 -0
- package/lib/tokens.d.ts +48 -597
- package/lib/tokens.js +2 -31
- package/lib/widgets/ai-settings.d.ts +3 -1
- package/lib/widgets/ai-settings.js +185 -344
- package/lib/widgets/main-area-chat.d.ts +3 -3
- package/lib/widgets/main-area-chat.js +2 -4
- package/lib/widgets/provider-config-dialog.d.ts +2 -1
- package/lib/widgets/provider-config-dialog.js +102 -167
- package/package.json +111 -258
- package/schema/settings-model.json +6 -0
- package/src/chat-commands/skills.ts +2 -2
- package/src/chat-model-handler.ts +10 -6
- package/src/chat-model.ts +488 -96
- package/src/completion/completion-provider.ts +6 -6
- package/src/components/clear-button.tsx +0 -2
- package/src/components/completion-status.tsx +2 -2
- package/src/components/model-select.tsx +1 -1
- package/src/components/save-button.tsx +3 -3
- package/src/components/stop-button.tsx +0 -2
- package/src/components/tool-select.tsx +10 -9
- package/src/components/usage-display.tsx +4 -2
- package/src/diff-manager.ts +4 -3
- package/src/index.ts +103 -107
- package/src/models/settings-model.ts +7 -6
- package/src/tokens.ts +54 -744
- package/src/widgets/ai-settings.tsx +40 -11
- package/src/widgets/main-area-chat.ts +5 -8
- package/src/widgets/provider-config-dialog.tsx +8 -8
- package/LICENSE +0 -30
- package/README.md +0 -49
- package/lib/agent.d.ts +0 -277
- package/lib/agent.js +0 -1116
- package/lib/icons.d.ts +0 -3
- package/lib/icons.js +0 -8
- package/lib/providers/built-in-providers.d.ts +0 -21
- package/lib/providers/built-in-providers.js +0 -233
- package/lib/providers/generated-context-windows.d.ts +0 -8
- package/lib/providers/generated-context-windows.js +0 -96
- package/lib/providers/model-info.d.ts +0 -3
- package/lib/providers/model-info.js +0 -58
- package/lib/providers/models.d.ts +0 -37
- package/lib/providers/models.js +0 -28
- package/lib/providers/provider-registry.d.ts +0 -49
- package/lib/providers/provider-registry.js +0 -72
- package/lib/providers/provider-tools.d.ts +0 -36
- package/lib/providers/provider-tools.js +0 -93
- package/lib/skills/index.d.ts +0 -4
- package/lib/skills/index.js +0 -7
- package/lib/skills/parse-skill.d.ts +0 -25
- package/lib/skills/parse-skill.js +0 -69
- package/lib/skills/skill-loader.d.ts +0 -25
- package/lib/skills/skill-loader.js +0 -133
- package/lib/skills/skill-registry.d.ts +0 -31
- package/lib/skills/skill-registry.js +0 -100
- package/lib/skills/types.d.ts +0 -29
- package/lib/skills/types.js +0 -5
- package/lib/tools/commands.d.ts +0 -11
- package/lib/tools/commands.js +0 -154
- package/lib/tools/skills.d.ts +0 -9
- package/lib/tools/skills.js +0 -73
- package/lib/tools/tool-registry.d.ts +0 -35
- package/lib/tools/tool-registry.js +0 -55
- package/lib/tools/web.d.ts +0 -8
- package/lib/tools/web.js +0 -196
- package/src/agent.ts +0 -1441
- package/src/icons.ts +0 -11
- package/src/providers/built-in-providers.ts +0 -241
- package/src/providers/generated-context-windows.ts +0 -102
- package/src/providers/model-info.ts +0 -88
- package/src/providers/models.ts +0 -76
- package/src/providers/provider-registry.ts +0 -88
- package/src/providers/provider-tools.ts +0 -179
- package/src/skills/index.ts +0 -14
- package/src/skills/parse-skill.ts +0 -91
- package/src/skills/skill-loader.ts +0 -175
- package/src/skills/skill-registry.ts +0 -137
- package/src/skills/types.ts +0 -37
- package/src/tools/commands.ts +0 -210
- package/src/tools/skills.ts +0 -84
- package/src/tools/tool-registry.ts +0 -63
- package/src/tools/web.ts +0 -238
- package/src/types.d.ts +0 -4
- package/style/icons/jupyternaut-lite.svg +0 -7
package/lib/chat-model.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AbstractChatModel } from '@jupyter/chat';
|
|
2
2
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
3
|
import * as nbformat from '@jupyterlab/nbformat';
|
|
4
|
+
import { AI_AVATAR } from '@jupyternaut/agent';
|
|
4
5
|
import { UUID } from '@lumino/coreutils';
|
|
5
6
|
import { Debouncer } from '@lumino/polling';
|
|
6
7
|
import { Signal } from '@lumino/signaling';
|
|
7
|
-
import {
|
|
8
|
+
import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from '@jupyternaut/agent';
|
|
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,10 @@ 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),
|
|
155
|
+
removeQueuedMessage: (id) => this.removeQueuedMessage(id),
|
|
156
|
+
reorderQueuedMessages: (ids) => this.reorderQueuedMessages(ids),
|
|
157
|
+
editQueuedMessage: (id, body) => this.editQueuedMessage(id, body)
|
|
146
158
|
};
|
|
147
159
|
}
|
|
148
160
|
/**
|
|
@@ -155,14 +167,29 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
155
167
|
* Clears all messages from the chat and resets conversation state.
|
|
156
168
|
*/
|
|
157
169
|
clearMessages = async () => {
|
|
170
|
+
this.stopStreaming();
|
|
171
|
+
this._messageQueue = [];
|
|
172
|
+
this._isBusy = false;
|
|
173
|
+
this._queueMessageId = null;
|
|
174
|
+
this._currentStreamingMessage = null;
|
|
158
175
|
this.messagesDeleted(0, this.messages.length);
|
|
176
|
+
this.title = null;
|
|
159
177
|
this._toolContexts.clear();
|
|
160
178
|
await this._agentManager.clearHistory();
|
|
161
179
|
};
|
|
180
|
+
/**
|
|
181
|
+
* Overrides messageAdded to ensure queued messages stay at the bottom.
|
|
182
|
+
*/
|
|
183
|
+
messageAdded(message) {
|
|
184
|
+
super.messageAdded(message);
|
|
185
|
+
if (this._queueMessageId && message.id !== this._queueMessageId) {
|
|
186
|
+
this._updateQueueUI();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
162
189
|
/**
|
|
163
190
|
* Adds a non-user message to the chat (used by chat commands).
|
|
164
191
|
*/
|
|
165
|
-
|
|
192
|
+
_addSystemMessage(body) {
|
|
166
193
|
const message = {
|
|
167
194
|
body,
|
|
168
195
|
sender: this._getAIUser(),
|
|
@@ -193,9 +220,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
193
220
|
raw_time: false,
|
|
194
221
|
attachments: [...this.input.attachments]
|
|
195
222
|
};
|
|
196
|
-
this.messageAdded(userMessage);
|
|
197
223
|
// Check if we have valid configuration
|
|
198
224
|
if (!this._agentManager.hasValidConfig()) {
|
|
225
|
+
this.messageAdded(userMessage);
|
|
199
226
|
const errorMessage = {
|
|
200
227
|
body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
|
|
201
228
|
sender: this._getAIUser(),
|
|
@@ -207,30 +234,48 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
207
234
|
this.messageAdded(errorMessage);
|
|
208
235
|
return;
|
|
209
236
|
}
|
|
237
|
+
if (this._isBusy) {
|
|
238
|
+
this._messageQueue.push({
|
|
239
|
+
id: UUID.uuid4(),
|
|
240
|
+
body: message.body,
|
|
241
|
+
_originalMsg: userMessage
|
|
242
|
+
});
|
|
243
|
+
this.input.clearAttachments();
|
|
244
|
+
this._updateQueueUI();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this._isBusy = true;
|
|
248
|
+
this.messageAdded(userMessage);
|
|
249
|
+
this.input.clearAttachments();
|
|
250
|
+
await this._processMessage(userMessage);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Internal method to process attachments and send the message to the agent.
|
|
254
|
+
*/
|
|
255
|
+
async _processMessage(userMessage) {
|
|
210
256
|
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
257
|
this.updateWriters([{ user: this._getAIUser() }]);
|
|
258
|
+
let enhancedMessage = userMessage.body;
|
|
259
|
+
if (userMessage.attachments && userMessage.attachments.length > 0) {
|
|
260
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
261
|
+
const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
|
|
262
|
+
const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
|
|
263
|
+
const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
|
|
264
|
+
enhancedMessage = await Private.processAttachments(userMessage.attachments, this.input.documentManager, userMessage.body, supportsImages, supportsPdf, supportsAudio);
|
|
265
|
+
}
|
|
229
266
|
await this._agentManager.generateResponse(enhancedMessage);
|
|
230
267
|
}
|
|
231
268
|
catch (error) {
|
|
232
269
|
const errorMessage = {
|
|
233
|
-
body:
|
|
270
|
+
body: '',
|
|
271
|
+
mime_model: {
|
|
272
|
+
data: {
|
|
273
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
274
|
+
},
|
|
275
|
+
metadata: {
|
|
276
|
+
errorMessage: `Error generating AI response: ${error.message}`
|
|
277
|
+
}
|
|
278
|
+
},
|
|
234
279
|
sender: this._getAIUser(),
|
|
235
280
|
id: UUID.uuid4(),
|
|
236
281
|
time: Date.now() / 1000,
|
|
@@ -240,7 +285,112 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
240
285
|
this.messageAdded(errorMessage);
|
|
241
286
|
}
|
|
242
287
|
finally {
|
|
288
|
+
this._drainQueue();
|
|
289
|
+
if (this._settingsModel.config.autoTitle &&
|
|
290
|
+
(this.messages.filter(msg => msg.sender.username !== 'ai-assistant')
|
|
291
|
+
.length <= 5 ||
|
|
292
|
+
this.title === null)) {
|
|
293
|
+
try {
|
|
294
|
+
this.title = await this.requestTitle();
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
console.warn('Error while generating a title\n', e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Removes the message-queue chat component.
|
|
304
|
+
*/
|
|
305
|
+
_removeQueueUI() {
|
|
306
|
+
if (this._queueMessageId) {
|
|
307
|
+
const existingMsg = this.messages.find(msg => msg.id === this._queueMessageId);
|
|
308
|
+
if (existingMsg) {
|
|
309
|
+
const idx = this.messages.indexOf(existingMsg);
|
|
310
|
+
if (idx !== -1) {
|
|
311
|
+
this.messagesDeleted(idx, 1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
this._queueMessageId = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Creates or updates the message-queue chat component.
|
|
319
|
+
*/
|
|
320
|
+
_updateQueueUI() {
|
|
321
|
+
this._removeQueueUI();
|
|
322
|
+
if (this._messageQueue.length === 0) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const queueBody = {
|
|
326
|
+
data: {
|
|
327
|
+
'application/vnd.jupyter.chat.components': 'message-queue'
|
|
328
|
+
},
|
|
329
|
+
metadata: {
|
|
330
|
+
messages: this._messageQueue.map(m => ({
|
|
331
|
+
id: m.id,
|
|
332
|
+
body: m.body,
|
|
333
|
+
attachments: m._originalMsg.attachments
|
|
334
|
+
})),
|
|
335
|
+
targetId: this.name
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
this._queueMessageId = UUID.uuid4();
|
|
339
|
+
const queueMessage = {
|
|
340
|
+
body: '',
|
|
341
|
+
mime_model: queueBody,
|
|
342
|
+
sender: { username: 'system', display_name: '' },
|
|
343
|
+
id: this._queueMessageId,
|
|
344
|
+
time: Date.now() / 1000,
|
|
345
|
+
type: 'msg',
|
|
346
|
+
raw_time: false
|
|
347
|
+
};
|
|
348
|
+
this.messageAdded(queueMessage);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Processes the next message in the queue, or marks the agent as idle.
|
|
352
|
+
*/
|
|
353
|
+
async _drainQueue() {
|
|
354
|
+
if (this._messageQueue.length === 0) {
|
|
355
|
+
this._isBusy = false;
|
|
243
356
|
this.updateWriters([]);
|
|
357
|
+
this._removeQueueUI();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Dequeue and push to chat
|
|
361
|
+
const next = this._messageQueue.shift();
|
|
362
|
+
next._originalMsg.time = Date.now() / 1000;
|
|
363
|
+
this.messageAdded(next._originalMsg);
|
|
364
|
+
await this._processMessage(next._originalMsg);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Removes a queued message by its ID.
|
|
368
|
+
* @param messageId The ID of the queued message to remove
|
|
369
|
+
*/
|
|
370
|
+
removeQueuedMessage(messageId) {
|
|
371
|
+
this.messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Reorders queued messages by their IDs.
|
|
375
|
+
* @param messageIds Array of message IDs in the desired order
|
|
376
|
+
*/
|
|
377
|
+
reorderQueuedMessages(messageIds) {
|
|
378
|
+
const byId = new Map(this._messageQueue.map(m => [m.id, m]));
|
|
379
|
+
this.messageQueue = messageIds
|
|
380
|
+
.map(id => byId.get(id))
|
|
381
|
+
.filter((m) => m !== undefined);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Edits a queued message by its ID.
|
|
385
|
+
* @param messageId The ID of the queued message to edit
|
|
386
|
+
* @param newBody The new body of the message
|
|
387
|
+
*/
|
|
388
|
+
editQueuedMessage(messageId, newBody) {
|
|
389
|
+
const queue = [...this._messageQueue];
|
|
390
|
+
const index = queue.findIndex(m => m.id === messageId);
|
|
391
|
+
if (index !== -1) {
|
|
392
|
+
queue[index] = { ...queue[index], body: newBody };
|
|
393
|
+
this.messageQueue = queue;
|
|
244
394
|
}
|
|
245
395
|
}
|
|
246
396
|
/**
|
|
@@ -310,7 +460,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
310
460
|
}
|
|
311
461
|
}
|
|
312
462
|
else if (!silent) {
|
|
313
|
-
console.log(`Provider not
|
|
463
|
+
console.log(`Provider not provided when restoring ${filepath}.`);
|
|
314
464
|
}
|
|
315
465
|
const messages = content.messages.map(message => {
|
|
316
466
|
let attachments = [];
|
|
@@ -327,7 +477,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
327
477
|
});
|
|
328
478
|
await this.clearMessages();
|
|
329
479
|
this.messagesInserted(0, messages);
|
|
330
|
-
this.
|
|
480
|
+
await this._rebuildHistory();
|
|
331
481
|
this.autosave = content.metadata?.autosave ?? false;
|
|
332
482
|
this.title = content.metadata?.title ?? null;
|
|
333
483
|
return true;
|
|
@@ -343,7 +493,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
343
493
|
const messages = [
|
|
344
494
|
{
|
|
345
495
|
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'
|
|
496
|
+
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
497
|
},
|
|
348
498
|
{
|
|
349
499
|
role: 'user',
|
|
@@ -362,6 +512,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
362
512
|
const attachmentMap = new Map(); // JSON → index
|
|
363
513
|
const attachmentsList = []; // Actual attachments
|
|
364
514
|
this.messages.forEach(message => {
|
|
515
|
+
if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
365
518
|
let attachmentIndexes = [];
|
|
366
519
|
if (message.attachments) {
|
|
367
520
|
attachmentIndexes = message.attachments.map(attachment => {
|
|
@@ -420,6 +573,49 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
420
573
|
this.config = { ...config, enableCodeToolbar: true };
|
|
421
574
|
// Agent manager handles agent recreation automatically via its own settings listener
|
|
422
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Rebuild history when the active model changes.
|
|
578
|
+
*/
|
|
579
|
+
_onModelChanged() {
|
|
580
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
581
|
+
const modelKey = providerConfig
|
|
582
|
+
? `${providerConfig.provider}:${providerConfig.model}`
|
|
583
|
+
: undefined;
|
|
584
|
+
if (modelKey && modelKey !== this._currentModelKey) {
|
|
585
|
+
this._currentModelKey = modelKey;
|
|
586
|
+
this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Rebuilds the agent history from the current messages.
|
|
591
|
+
* For vision-capable models, re-reads binary attachments from disk.
|
|
592
|
+
* For text-only models, uses message text only.
|
|
593
|
+
*/
|
|
594
|
+
async _rebuildHistory() {
|
|
595
|
+
const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
|
|
596
|
+
const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
|
|
597
|
+
const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
|
|
598
|
+
const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
|
|
599
|
+
const modelMessages = [];
|
|
600
|
+
for (const msg of this.messages) {
|
|
601
|
+
const isAI = msg.sender.username === 'ai-assistant';
|
|
602
|
+
if (!isAI && msg.attachments?.length) {
|
|
603
|
+
const enhancedContent = await Private.processAttachments(msg.attachments, this.input.documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
|
|
604
|
+
modelMessages.push({
|
|
605
|
+
role: 'user',
|
|
606
|
+
content: enhancedContent
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
else if (msg.body) {
|
|
610
|
+
modelMessages.push({
|
|
611
|
+
role: isAI ? 'assistant' : 'user',
|
|
612
|
+
content: msg.body
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// Skip messages with empty body like tool calls
|
|
616
|
+
}
|
|
617
|
+
this._agentManager.setHistory(modelMessages);
|
|
618
|
+
}
|
|
423
619
|
/**
|
|
424
620
|
* Handles events emitted by the agent manager.
|
|
425
621
|
* @param event The event data containing type and payload
|
|
@@ -468,6 +664,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
468
664
|
this.messageAdded(aiMessage);
|
|
469
665
|
this._currentStreamingMessage =
|
|
470
666
|
this.messages.find(message => message.id === aiMessage.id) ?? null;
|
|
667
|
+
this._updateQueueUI();
|
|
471
668
|
}
|
|
472
669
|
/**
|
|
473
670
|
* Handles streaming message chunks from the AI agent.
|
|
@@ -583,13 +780,18 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
583
780
|
body: '',
|
|
584
781
|
mime_model: {
|
|
585
782
|
data: {
|
|
586
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
783
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
587
784
|
},
|
|
588
785
|
metadata: {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
786
|
+
toolCalls: [
|
|
787
|
+
{
|
|
788
|
+
toolCallId: context.toolCallId,
|
|
789
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
790
|
+
kind: context.toolName,
|
|
791
|
+
status: 'in_progress',
|
|
792
|
+
rawInput: context.input
|
|
793
|
+
}
|
|
794
|
+
]
|
|
593
795
|
}
|
|
594
796
|
},
|
|
595
797
|
sender: this._getAIUser(),
|
|
@@ -599,6 +801,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
599
801
|
raw_time: false
|
|
600
802
|
};
|
|
601
803
|
this.messageAdded(toolCallMessage);
|
|
804
|
+
this._updateQueueUI();
|
|
602
805
|
}
|
|
603
806
|
/**
|
|
604
807
|
* Handles the completion of a tool call execution.
|
|
@@ -642,13 +845,22 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
642
845
|
*/
|
|
643
846
|
_handleErrorEvent(event) {
|
|
644
847
|
this.messageAdded({
|
|
645
|
-
body:
|
|
848
|
+
body: '',
|
|
849
|
+
mime_model: {
|
|
850
|
+
data: {
|
|
851
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
852
|
+
},
|
|
853
|
+
metadata: {
|
|
854
|
+
errorMessage: `Error generating response: ${event.data.error.message}`
|
|
855
|
+
}
|
|
856
|
+
},
|
|
646
857
|
sender: this._getAIUser(),
|
|
647
858
|
id: UUID.uuid4(),
|
|
648
859
|
time: Date.now() / 1000,
|
|
649
860
|
type: 'msg',
|
|
650
861
|
raw_time: false
|
|
651
862
|
});
|
|
863
|
+
this._updateQueueUI();
|
|
652
864
|
}
|
|
653
865
|
/**
|
|
654
866
|
* Handles tool approval request events from the AI agent.
|
|
@@ -658,7 +870,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
658
870
|
if (!context) {
|
|
659
871
|
return;
|
|
660
872
|
}
|
|
661
|
-
context.approvalId = event.data.approvalId;
|
|
662
873
|
context.input = JSON.stringify(event.data.args, null, 2);
|
|
663
874
|
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
664
875
|
}
|
|
@@ -666,12 +877,12 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
666
877
|
* Handles tool approval resolved events from the AI agent.
|
|
667
878
|
*/
|
|
668
879
|
_handleToolApprovalResolved(event) {
|
|
669
|
-
const context =
|
|
880
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
670
881
|
if (!context) {
|
|
671
882
|
return;
|
|
672
883
|
}
|
|
673
884
|
const status = event.data.approved ? 'approved' : 'rejected';
|
|
674
|
-
this._updateToolCallUI(
|
|
885
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
675
886
|
if (!event.data.approved) {
|
|
676
887
|
this._toolContexts.delete(context.toolCallId);
|
|
677
888
|
}
|
|
@@ -692,41 +903,79 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
692
903
|
existingMessage.update({
|
|
693
904
|
mime_model: {
|
|
694
905
|
data: {
|
|
695
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
906
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
696
907
|
},
|
|
697
908
|
metadata: {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
909
|
+
toolCalls: [
|
|
910
|
+
{
|
|
911
|
+
toolCallId: context.toolCallId,
|
|
912
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
913
|
+
kind: context.toolName,
|
|
914
|
+
status: context.status,
|
|
915
|
+
rawInput: context.input,
|
|
916
|
+
rawOutput: output,
|
|
917
|
+
sessionId: this.name,
|
|
918
|
+
permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
919
|
+
...(status === 'awaiting_approval' && {
|
|
920
|
+
permissionOptions: [
|
|
921
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
922
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
923
|
+
]
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
]
|
|
705
927
|
}
|
|
706
928
|
}
|
|
707
929
|
});
|
|
708
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* The current message queue
|
|
933
|
+
*/
|
|
934
|
+
get messageQueue() {
|
|
935
|
+
return this._messageQueue;
|
|
936
|
+
}
|
|
937
|
+
set messageQueue(value) {
|
|
938
|
+
this._messageQueue = value;
|
|
939
|
+
this._updateQueueUI();
|
|
940
|
+
if (this._messageQueue.length > 0 && !this._isBusy) {
|
|
941
|
+
this._drainQueue();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Whether the chat is busy
|
|
946
|
+
*/
|
|
947
|
+
get isBusy() {
|
|
948
|
+
return this._isBusy;
|
|
949
|
+
}
|
|
950
|
+
set isBusy(value) {
|
|
951
|
+
this._isBusy = value;
|
|
952
|
+
}
|
|
709
953
|
// Private fields
|
|
710
954
|
_settingsModel;
|
|
711
955
|
_user;
|
|
712
956
|
_toolContexts = new Map();
|
|
713
957
|
_agentManager;
|
|
958
|
+
_providerRegistry;
|
|
959
|
+
_currentModelKey;
|
|
714
960
|
_currentStreamingMessage = null;
|
|
715
961
|
_nameChanged = new Signal(this);
|
|
716
962
|
_contentsManager;
|
|
717
963
|
_autosave = false;
|
|
718
964
|
_autosaveChanged = new Signal(this);
|
|
719
965
|
_autosaveDebouncer;
|
|
966
|
+
_messageQueue = [];
|
|
967
|
+
_isBusy = false;
|
|
968
|
+
_queueMessageId = null;
|
|
720
969
|
_title = null;
|
|
721
970
|
_titleChanged = new Signal(this);
|
|
722
971
|
}
|
|
723
972
|
var Private;
|
|
724
973
|
(function (Private) {
|
|
725
|
-
|
|
974
|
+
Private.isPlainObject = (value) => {
|
|
726
975
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
727
976
|
};
|
|
728
|
-
|
|
729
|
-
if (!isPlainObject(value)) {
|
|
977
|
+
Private.isDisplayOutput = (value) => {
|
|
978
|
+
if (!Private.isPlainObject(value)) {
|
|
730
979
|
return false;
|
|
731
980
|
}
|
|
732
981
|
const output = value;
|
|
@@ -734,14 +983,14 @@ var Private;
|
|
|
734
983
|
nbformat.isDisplayUpdate(output) ||
|
|
735
984
|
nbformat.isExecuteResult(output));
|
|
736
985
|
};
|
|
737
|
-
|
|
986
|
+
Private.toMimeBundle = (value, trustedMimeTypes) => {
|
|
738
987
|
const data = value.data;
|
|
739
|
-
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
988
|
+
if (!Private.isPlainObject(data) || Object.keys(data).length === 0) {
|
|
740
989
|
return null;
|
|
741
990
|
}
|
|
742
991
|
return {
|
|
743
992
|
data: data,
|
|
744
|
-
...(isPlainObject(value.metadata)
|
|
993
|
+
...(Private.isPlainObject(value.metadata)
|
|
745
994
|
? { metadata: value.metadata }
|
|
746
995
|
: {}),
|
|
747
996
|
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
@@ -757,21 +1006,21 @@ var Private;
|
|
|
757
1006
|
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
758
1007
|
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
759
1008
|
*/
|
|
760
|
-
|
|
761
|
-
if (isDisplayOutput(value)) {
|
|
1009
|
+
Private.toDisplayOutputs = (value) => {
|
|
1010
|
+
if (Private.isDisplayOutput(value)) {
|
|
762
1011
|
return [value];
|
|
763
1012
|
}
|
|
764
1013
|
if (Array.isArray(value)) {
|
|
765
|
-
return value.filter(isDisplayOutput);
|
|
1014
|
+
return value.filter(Private.isDisplayOutput);
|
|
766
1015
|
}
|
|
767
|
-
if (!isPlainObject(value)) {
|
|
1016
|
+
if (!Private.isPlainObject(value)) {
|
|
768
1017
|
return [];
|
|
769
1018
|
}
|
|
770
1019
|
if (Array.isArray(value.outputs)) {
|
|
771
|
-
return value.outputs.filter(isDisplayOutput);
|
|
1020
|
+
return value.outputs.filter(Private.isDisplayOutput);
|
|
772
1021
|
}
|
|
773
1022
|
if ('result' in value) {
|
|
774
|
-
return toDisplayOutputs(value.result);
|
|
1023
|
+
return Private.toDisplayOutputs(value.result);
|
|
775
1024
|
}
|
|
776
1025
|
return [];
|
|
777
1026
|
};
|
|
@@ -780,10 +1029,10 @@ var Private;
|
|
|
780
1029
|
*/
|
|
781
1030
|
function extractMimeBundlesFromUnknown(content, options = {}) {
|
|
782
1031
|
const bundles = [];
|
|
783
|
-
const outputs = toDisplayOutputs(content);
|
|
1032
|
+
const outputs = Private.toDisplayOutputs(content);
|
|
784
1033
|
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
785
1034
|
for (const output of outputs) {
|
|
786
|
-
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1035
|
+
const bundle = Private.toMimeBundle(output, trustedMimeTypes);
|
|
787
1036
|
if (bundle) {
|
|
788
1037
|
bundles.push(bundle);
|
|
789
1038
|
}
|
|
@@ -804,16 +1053,21 @@ var Private;
|
|
|
804
1053
|
}
|
|
805
1054
|
Private.formatToolOutput = formatToolOutput;
|
|
806
1055
|
/**
|
|
807
|
-
* Processes file attachments and returns
|
|
1056
|
+
* Processes file attachments and returns the message content with the attachments.
|
|
808
1057
|
* @param attachments Array of file attachments to process
|
|
809
1058
|
* @param documentManager Optional document manager for file operations
|
|
810
|
-
* @
|
|
1059
|
+
* @param body The message body
|
|
1060
|
+
* @param supportsImages Whether the model supports images
|
|
1061
|
+
* @param supportsPdf Whether the model supports pdfs
|
|
1062
|
+
* @param supportsAudio Whether the model supports audio
|
|
1063
|
+
* @returns Enhanced message content
|
|
811
1064
|
*/
|
|
812
|
-
async function processAttachments(attachments, documentManager) {
|
|
1065
|
+
async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
|
|
813
1066
|
const textContents = [];
|
|
814
|
-
const
|
|
1067
|
+
const includedParts = [];
|
|
1068
|
+
const omittedNames = [];
|
|
815
1069
|
if (!documentManager) {
|
|
816
|
-
return
|
|
1070
|
+
return body;
|
|
817
1071
|
}
|
|
818
1072
|
for (const attachment of attachments) {
|
|
819
1073
|
try {
|
|
@@ -837,24 +1091,50 @@ var Private;
|
|
|
837
1091
|
}
|
|
838
1092
|
}
|
|
839
1093
|
if (mimetype?.startsWith('image/')) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1094
|
+
if (supportsImages) {
|
|
1095
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1096
|
+
if (data) {
|
|
1097
|
+
includedParts.push({
|
|
1098
|
+
type: 'image',
|
|
1099
|
+
image: data,
|
|
1100
|
+
mediaType: mimetype
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
847
1106
|
}
|
|
848
1107
|
}
|
|
849
1108
|
else if (mimetype === 'application/pdf') {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1109
|
+
if (supportsPdf) {
|
|
1110
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1111
|
+
if (data) {
|
|
1112
|
+
includedParts.push({
|
|
1113
|
+
type: 'file',
|
|
1114
|
+
data,
|
|
1115
|
+
mediaType: mimetype,
|
|
1116
|
+
filename: PathExt.basename(attachment.value)
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
else if (mimetype?.startsWith('audio/')) {
|
|
1125
|
+
if (supportsAudio) {
|
|
1126
|
+
const data = await readBinaryAttachment(attachment, documentManager);
|
|
1127
|
+
if (data) {
|
|
1128
|
+
includedParts.push({
|
|
1129
|
+
type: 'file',
|
|
1130
|
+
data,
|
|
1131
|
+
mediaType: mimetype,
|
|
1132
|
+
filename: PathExt.basename(attachment.value)
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
858
1138
|
}
|
|
859
1139
|
}
|
|
860
1140
|
else {
|
|
@@ -874,7 +1154,16 @@ var Private;
|
|
|
874
1154
|
textContents.push(`**File: ${attachment.value}** (Could not read file)`);
|
|
875
1155
|
}
|
|
876
1156
|
}
|
|
877
|
-
|
|
1157
|
+
let textPart = body;
|
|
1158
|
+
if (textContents.length > 0) {
|
|
1159
|
+
textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
|
|
1160
|
+
}
|
|
1161
|
+
if (omittedNames.length > 0) {
|
|
1162
|
+
textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
|
|
1163
|
+
}
|
|
1164
|
+
return includedParts.length > 0
|
|
1165
|
+
? [{ type: 'text', text: textPart }, ...includedParts]
|
|
1166
|
+
: textPart;
|
|
878
1167
|
}
|
|
879
1168
|
Private.processAttachments = processAttachments;
|
|
880
1169
|
/**
|