@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/src/chat-model.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
IChatContext,
|
|
6
6
|
IMessage,
|
|
7
7
|
IMessageContent,
|
|
8
|
+
IMimeModelBody,
|
|
8
9
|
INewMessage,
|
|
9
10
|
IUser
|
|
10
11
|
} from '@jupyter/chat';
|
|
@@ -31,9 +32,23 @@ import { Debouncer } from '@lumino/polling';
|
|
|
31
32
|
|
|
32
33
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
33
34
|
|
|
35
|
+
import type { UserContent, ImagePart, FilePart, ModelMessage } from 'ai';
|
|
36
|
+
|
|
34
37
|
import { AI_AVATAR } from './icons';
|
|
35
38
|
|
|
36
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
IAgentManager,
|
|
41
|
+
IAIChatModel,
|
|
42
|
+
IAISettingsModel,
|
|
43
|
+
IProviderRegistry,
|
|
44
|
+
ITokenUsage
|
|
45
|
+
} from './tokens';
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
modelSupportsAudio,
|
|
49
|
+
modelSupportsImages,
|
|
50
|
+
modelSupportsPdf
|
|
51
|
+
} from './providers/model-info';
|
|
37
52
|
|
|
38
53
|
/**
|
|
39
54
|
* Tool call status types.
|
|
@@ -66,10 +81,6 @@ interface IToolExecutionContext {
|
|
|
66
81
|
* The tool input (formatted).
|
|
67
82
|
*/
|
|
68
83
|
input: string;
|
|
69
|
-
/**
|
|
70
|
-
* Optional approval ID if awaiting approval.
|
|
71
|
-
*/
|
|
72
|
-
approvalId?: string;
|
|
73
84
|
/**
|
|
74
85
|
* Current status.
|
|
75
86
|
*/
|
|
@@ -88,7 +99,7 @@ interface IToolExecutionContext {
|
|
|
88
99
|
* AI Chat Model implementation that provides chat functionality tool integration,
|
|
89
100
|
* and MCP server support.
|
|
90
101
|
*/
|
|
91
|
-
export class AIChatModel extends AbstractChatModel {
|
|
102
|
+
export class AIChatModel extends AbstractChatModel implements IAIChatModel {
|
|
92
103
|
/**
|
|
93
104
|
* Constructs a new AIChatModel instance.
|
|
94
105
|
* @param options Configuration options for the chat model
|
|
@@ -106,6 +117,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
106
117
|
this._user = options.user;
|
|
107
118
|
this._agentManager = options.agentManager;
|
|
108
119
|
this._contentsManager = options.contentsManager;
|
|
120
|
+
this._providerRegistry = options.providerRegistry;
|
|
109
121
|
|
|
110
122
|
// Listen for agent events
|
|
111
123
|
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
|
|
@@ -113,6 +125,13 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
113
125
|
// Listen for settings changes to update chat behavior
|
|
114
126
|
this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
|
|
115
127
|
|
|
128
|
+
// Rebuild history when the model changes
|
|
129
|
+
this._agentManager.activeProviderChanged.connect(
|
|
130
|
+
this._onModelChanged,
|
|
131
|
+
this
|
|
132
|
+
);
|
|
133
|
+
this._settingsModel.stateChanged.connect(this._onModelChanged, this);
|
|
134
|
+
|
|
116
135
|
this._autosaveDebouncer = new Debouncer(this.save, 3000);
|
|
117
136
|
}
|
|
118
137
|
|
|
@@ -133,6 +152,34 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
133
152
|
this.setReady();
|
|
134
153
|
}
|
|
135
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A signal emitting when the chat name has changed.
|
|
157
|
+
*/
|
|
158
|
+
get nameChanged(): ISignal<IAIChatModel, string> {
|
|
159
|
+
return this._nameChanged;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The title of the chat.
|
|
164
|
+
*/
|
|
165
|
+
get title(): string | null {
|
|
166
|
+
return this._title;
|
|
167
|
+
}
|
|
168
|
+
set title(value: string | null) {
|
|
169
|
+
this._title = value;
|
|
170
|
+
if (this.autosave) {
|
|
171
|
+
this._autosaveDebouncer.invoke();
|
|
172
|
+
}
|
|
173
|
+
this._titleChanged.emit(this._title);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A signal emitting when the chat title has changed.
|
|
178
|
+
*/
|
|
179
|
+
get titleChanged(): ISignal<IAIChatModel, string | null> {
|
|
180
|
+
return this._titleChanged;
|
|
181
|
+
}
|
|
182
|
+
|
|
136
183
|
/**
|
|
137
184
|
* Whether to save the chat automatically.
|
|
138
185
|
*/
|
|
@@ -140,6 +187,9 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
140
187
|
return this._autosave;
|
|
141
188
|
}
|
|
142
189
|
set autosave(value: boolean) {
|
|
190
|
+
if (value === this._autosave) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
143
193
|
this._autosave = value;
|
|
144
194
|
this._autosaveChanged.emit(value);
|
|
145
195
|
if (value) {
|
|
@@ -151,7 +201,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
151
201
|
this._autosaveDebouncer.invoke,
|
|
152
202
|
this._autosaveDebouncer
|
|
153
203
|
);
|
|
154
|
-
this._autosaveDebouncer.invoke();
|
|
155
204
|
} else {
|
|
156
205
|
this.messagesUpdated.disconnect(
|
|
157
206
|
this._autosaveDebouncer.invoke,
|
|
@@ -162,22 +211,16 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
162
211
|
this._autosaveDebouncer
|
|
163
212
|
);
|
|
164
213
|
}
|
|
214
|
+
this._autosaveDebouncer.invoke();
|
|
165
215
|
}
|
|
166
216
|
|
|
167
217
|
/**
|
|
168
218
|
* A signal emitting when the autosave flag changed.
|
|
169
219
|
*/
|
|
170
|
-
get autosaveChanged(): ISignal<
|
|
220
|
+
get autosaveChanged(): ISignal<IAIChatModel, boolean> {
|
|
171
221
|
return this._autosaveChanged;
|
|
172
222
|
}
|
|
173
223
|
|
|
174
|
-
/**
|
|
175
|
-
* A signal emitting when the chat name has changed.
|
|
176
|
-
*/
|
|
177
|
-
get nameChanged(): ISignal<AIChatModel, string> {
|
|
178
|
-
return this._nameChanged;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
224
|
/**
|
|
182
225
|
* Gets the current user information.
|
|
183
226
|
*/
|
|
@@ -193,7 +236,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
193
236
|
}
|
|
194
237
|
|
|
195
238
|
/**
|
|
196
|
-
*
|
|
239
|
+
* The agent manager used in the model.
|
|
197
240
|
*/
|
|
198
241
|
get agentManager(): IAgentManager {
|
|
199
242
|
return this._agentManager;
|
|
@@ -210,6 +253,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
210
253
|
* Dispose of the model.
|
|
211
254
|
*/
|
|
212
255
|
dispose(): void {
|
|
256
|
+
this.stopStreaming();
|
|
213
257
|
this.messagesUpdated.disconnect(
|
|
214
258
|
this._autosaveDebouncer.invoke,
|
|
215
259
|
this._autosaveDebouncer
|
|
@@ -229,7 +273,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
229
273
|
stopStreaming: () => this.stopStreaming(),
|
|
230
274
|
clearMessages: () => this.clearMessages(),
|
|
231
275
|
agentManager: this._agentManager,
|
|
232
|
-
addSystemMessage: (body: string) => this.
|
|
276
|
+
addSystemMessage: (body: string) => this._addSystemMessage(body)
|
|
233
277
|
};
|
|
234
278
|
}
|
|
235
279
|
|
|
@@ -243,16 +287,32 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
243
287
|
/**
|
|
244
288
|
* Clears all messages from the chat and resets conversation state.
|
|
245
289
|
*/
|
|
246
|
-
clearMessages = (): void => {
|
|
290
|
+
clearMessages = async (): Promise<void> => {
|
|
291
|
+
this.stopStreaming();
|
|
292
|
+
this._messageQueue = [];
|
|
293
|
+
this._isBusy = false;
|
|
294
|
+
this._queueMessageId = null;
|
|
295
|
+
this._currentStreamingMessage = null;
|
|
247
296
|
this.messagesDeleted(0, this.messages.length);
|
|
297
|
+
this.title = null;
|
|
248
298
|
this._toolContexts.clear();
|
|
249
|
-
this._agentManager.clearHistory();
|
|
299
|
+
await this._agentManager.clearHistory();
|
|
250
300
|
};
|
|
251
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Overrides messageAdded to ensure queued messages stay at the bottom.
|
|
304
|
+
*/
|
|
305
|
+
override messageAdded(message: IMessageContent): void {
|
|
306
|
+
super.messageAdded(message);
|
|
307
|
+
if (this._queueMessageId && message.id !== this._queueMessageId) {
|
|
308
|
+
this._updateQueueUI();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
252
312
|
/**
|
|
253
313
|
* Adds a non-user message to the chat (used by chat commands).
|
|
254
314
|
*/
|
|
255
|
-
|
|
315
|
+
private _addSystemMessage(body: string): void {
|
|
256
316
|
const message: IMessageContent = {
|
|
257
317
|
body,
|
|
258
318
|
sender: this._getAIUser(),
|
|
@@ -285,10 +345,10 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
285
345
|
raw_time: false,
|
|
286
346
|
attachments: [...this.input.attachments]
|
|
287
347
|
};
|
|
288
|
-
this.messageAdded(userMessage);
|
|
289
348
|
|
|
290
349
|
// Check if we have valid configuration
|
|
291
350
|
if (!this._agentManager.hasValidConfig()) {
|
|
351
|
+
this.messageAdded(userMessage);
|
|
292
352
|
const errorMessage: IMessageContent = {
|
|
293
353
|
body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
|
|
294
354
|
sender: this._getAIUser(),
|
|
@@ -301,27 +361,71 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
301
361
|
return;
|
|
302
362
|
}
|
|
303
363
|
|
|
364
|
+
if (this._isBusy) {
|
|
365
|
+
this._messageQueue.push({
|
|
366
|
+
id: UUID.uuid4(),
|
|
367
|
+
body: message.body,
|
|
368
|
+
_originalMsg: userMessage
|
|
369
|
+
});
|
|
370
|
+
this.input.clearAttachments();
|
|
371
|
+
this._updateQueueUI();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this._isBusy = true;
|
|
376
|
+
this.messageAdded(userMessage);
|
|
377
|
+
this.input.clearAttachments();
|
|
378
|
+
|
|
379
|
+
await this._processMessage(userMessage);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Internal method to process attachments and send the message to the agent.
|
|
384
|
+
*/
|
|
385
|
+
private async _processMessage(userMessage: IMessageContent): Promise<void> {
|
|
304
386
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
387
|
+
this.updateWriters([{ user: this._getAIUser() }]);
|
|
388
|
+
|
|
389
|
+
let enhancedMessage: UserContent = userMessage.body;
|
|
390
|
+
if (userMessage.attachments && userMessage.attachments.length > 0) {
|
|
391
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
392
|
+
this._agentManager.activeProvider
|
|
393
|
+
);
|
|
394
|
+
const supportsImages = modelSupportsImages(
|
|
395
|
+
providerConfig,
|
|
396
|
+
this._providerRegistry
|
|
397
|
+
);
|
|
398
|
+
const supportsPdf = modelSupportsPdf(
|
|
399
|
+
providerConfig,
|
|
400
|
+
this._providerRegistry
|
|
401
|
+
);
|
|
402
|
+
const supportsAudio = modelSupportsAudio(
|
|
403
|
+
providerConfig,
|
|
404
|
+
this._providerRegistry
|
|
310
405
|
);
|
|
311
|
-
this.input.clearAttachments();
|
|
312
406
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
407
|
+
enhancedMessage = await Private.processAttachments(
|
|
408
|
+
userMessage.attachments,
|
|
409
|
+
this.input.documentManager,
|
|
410
|
+
userMessage.body,
|
|
411
|
+
supportsImages,
|
|
412
|
+
supportsPdf,
|
|
413
|
+
supportsAudio
|
|
414
|
+
);
|
|
317
415
|
}
|
|
318
416
|
|
|
319
|
-
this.updateWriters([{ user: this._getAIUser() }]);
|
|
320
|
-
|
|
321
417
|
await this._agentManager.generateResponse(enhancedMessage);
|
|
322
418
|
} catch (error) {
|
|
323
419
|
const errorMessage: IMessageContent = {
|
|
324
|
-
body:
|
|
420
|
+
body: '',
|
|
421
|
+
mime_model: {
|
|
422
|
+
data: {
|
|
423
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
424
|
+
},
|
|
425
|
+
metadata: {
|
|
426
|
+
errorMessage: `Error generating AI response: ${(error as Error).message}`
|
|
427
|
+
}
|
|
428
|
+
},
|
|
325
429
|
sender: this._getAIUser(),
|
|
326
430
|
id: UUID.uuid4(),
|
|
327
431
|
time: Date.now() / 1000,
|
|
@@ -330,8 +434,102 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
330
434
|
};
|
|
331
435
|
this.messageAdded(errorMessage);
|
|
332
436
|
} finally {
|
|
437
|
+
this._drainQueue();
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
this._settingsModel.config.autoTitle &&
|
|
441
|
+
(this.messages.length <= 5 || this.title === null)
|
|
442
|
+
) {
|
|
443
|
+
try {
|
|
444
|
+
this.title = await this.requestTitle();
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.warn('Error while generating a title\n', e);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Removes the message-queue chat component.
|
|
454
|
+
*/
|
|
455
|
+
private _removeQueueUI(): void {
|
|
456
|
+
if (this._queueMessageId) {
|
|
457
|
+
const existingMsg = this.messages.find(
|
|
458
|
+
msg => msg.id === this._queueMessageId
|
|
459
|
+
);
|
|
460
|
+
if (existingMsg) {
|
|
461
|
+
const idx = this.messages.indexOf(existingMsg);
|
|
462
|
+
if (idx !== -1) {
|
|
463
|
+
this.messagesDeleted(idx, 1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
this._queueMessageId = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Creates or updates the message-queue chat component.
|
|
472
|
+
*/
|
|
473
|
+
private _updateQueueUI(): void {
|
|
474
|
+
this._removeQueueUI();
|
|
475
|
+
|
|
476
|
+
if (this._messageQueue.length === 0) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const queueBody = {
|
|
481
|
+
data: {
|
|
482
|
+
'application/vnd.jupyter.chat.components': 'message-queue'
|
|
483
|
+
},
|
|
484
|
+
metadata: {
|
|
485
|
+
messages: this._messageQueue.map(m => ({
|
|
486
|
+
id: m.id,
|
|
487
|
+
body: m.body,
|
|
488
|
+
attachments: m._originalMsg.attachments
|
|
489
|
+
})),
|
|
490
|
+
targetId: this.name
|
|
491
|
+
}
|
|
492
|
+
} as IMimeModelBody;
|
|
493
|
+
|
|
494
|
+
this._queueMessageId = UUID.uuid4();
|
|
495
|
+
const queueMessage: IMessageContent = {
|
|
496
|
+
body: '',
|
|
497
|
+
mime_model: queueBody,
|
|
498
|
+
sender: { username: 'system', display_name: '' },
|
|
499
|
+
id: this._queueMessageId,
|
|
500
|
+
time: Date.now() / 1000,
|
|
501
|
+
type: 'msg',
|
|
502
|
+
raw_time: false
|
|
503
|
+
};
|
|
504
|
+
this.messageAdded(queueMessage);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Processes the next message in the queue, or marks the agent as idle.
|
|
509
|
+
*/
|
|
510
|
+
private async _drainQueue(): Promise<void> {
|
|
511
|
+
if (this._messageQueue.length === 0) {
|
|
512
|
+
this._isBusy = false;
|
|
333
513
|
this.updateWriters([]);
|
|
514
|
+
this._removeQueueUI();
|
|
515
|
+
return;
|
|
334
516
|
}
|
|
517
|
+
|
|
518
|
+
// Dequeue and push to chat
|
|
519
|
+
const next = this._messageQueue.shift()!;
|
|
520
|
+
next._originalMsg.time = Date.now() / 1000;
|
|
521
|
+
this.messageAdded(next._originalMsg);
|
|
522
|
+
|
|
523
|
+
await this._processMessage(next._originalMsg);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Removes a queued message by its ID.
|
|
528
|
+
* @param messageId The ID of the queued message to remove
|
|
529
|
+
*/
|
|
530
|
+
removeQueuedMessage(messageId: string): void {
|
|
531
|
+
this._messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
|
|
532
|
+
this._updateQueueUI();
|
|
335
533
|
}
|
|
336
534
|
|
|
337
535
|
/**
|
|
@@ -418,13 +616,39 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
418
616
|
attachments
|
|
419
617
|
};
|
|
420
618
|
});
|
|
421
|
-
this.clearMessages();
|
|
619
|
+
await this.clearMessages();
|
|
422
620
|
this.messagesInserted(0, messages);
|
|
423
|
-
this.
|
|
621
|
+
await this._rebuildHistory();
|
|
424
622
|
this.autosave = content.metadata?.autosave ?? false;
|
|
623
|
+
this.title = content.metadata?.title ?? null;
|
|
425
624
|
return true;
|
|
426
625
|
};
|
|
427
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Request a title to this chat, regarding the message history.
|
|
629
|
+
*/
|
|
630
|
+
async requestTitle(): Promise<string> {
|
|
631
|
+
const history = this.messages
|
|
632
|
+
.filter(msg => msg.body !== '')
|
|
633
|
+
.map(
|
|
634
|
+
msg =>
|
|
635
|
+
`${msg.sender.username === 'ai-assistant' ? 'assistant' : 'user'}: ${msg.body}`
|
|
636
|
+
)
|
|
637
|
+
.join('\n');
|
|
638
|
+
const messages: ModelMessage[] = [
|
|
639
|
+
{
|
|
640
|
+
role: 'system',
|
|
641
|
+
content:
|
|
642
|
+
"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."
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
role: 'user',
|
|
646
|
+
content: history
|
|
647
|
+
}
|
|
648
|
+
];
|
|
649
|
+
return this.agentManager.textResponse(messages);
|
|
650
|
+
}
|
|
651
|
+
|
|
428
652
|
/**
|
|
429
653
|
* Serialize the model for backup
|
|
430
654
|
*/
|
|
@@ -436,6 +660,13 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
436
660
|
const attachmentsList: IAttachment[] = []; // Actual attachments
|
|
437
661
|
|
|
438
662
|
this.messages.forEach(message => {
|
|
663
|
+
if (
|
|
664
|
+
message.content?.mime_model?.data?.[
|
|
665
|
+
'application/vnd.jupyter.chat.components'
|
|
666
|
+
] === 'message-queue'
|
|
667
|
+
) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
439
670
|
let attachmentIndexes: string[] = [];
|
|
440
671
|
if (message.attachments) {
|
|
441
672
|
attachmentIndexes = message.attachments.map(attachment => {
|
|
@@ -474,7 +705,8 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
474
705
|
attachments,
|
|
475
706
|
metadata: {
|
|
476
707
|
provider,
|
|
477
|
-
autosave: this.autosave
|
|
708
|
+
autosave: this.autosave,
|
|
709
|
+
...(this.title ? { title: this.title } : {})
|
|
478
710
|
}
|
|
479
711
|
};
|
|
480
712
|
}
|
|
@@ -500,6 +732,75 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
500
732
|
// Agent manager handles agent recreation automatically via its own settings listener
|
|
501
733
|
}
|
|
502
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Rebuild history when the active model changes.
|
|
737
|
+
*/
|
|
738
|
+
private _onModelChanged(): void {
|
|
739
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
740
|
+
this._agentManager.activeProvider
|
|
741
|
+
);
|
|
742
|
+
const modelKey = providerConfig
|
|
743
|
+
? `${providerConfig.provider}:${providerConfig.model}`
|
|
744
|
+
: undefined;
|
|
745
|
+
if (modelKey && modelKey !== this._currentModelKey) {
|
|
746
|
+
this._currentModelKey = modelKey;
|
|
747
|
+
this._rebuildHistory().catch(e =>
|
|
748
|
+
console.warn('Failed to rebuild history on model change:', e)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Rebuilds the agent history from the current messages.
|
|
755
|
+
* For vision-capable models, re-reads binary attachments from disk.
|
|
756
|
+
* For text-only models, uses message text only.
|
|
757
|
+
*/
|
|
758
|
+
private async _rebuildHistory(): Promise<void> {
|
|
759
|
+
const providerConfig = this._settingsModel.getProvider(
|
|
760
|
+
this._agentManager.activeProvider
|
|
761
|
+
);
|
|
762
|
+
const supportsImages = modelSupportsImages(
|
|
763
|
+
providerConfig,
|
|
764
|
+
this._providerRegistry
|
|
765
|
+
);
|
|
766
|
+
const supportsPdf = modelSupportsPdf(
|
|
767
|
+
providerConfig,
|
|
768
|
+
this._providerRegistry
|
|
769
|
+
);
|
|
770
|
+
const supportsAudio = modelSupportsAudio(
|
|
771
|
+
providerConfig,
|
|
772
|
+
this._providerRegistry
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
const modelMessages: ModelMessage[] = [];
|
|
776
|
+
for (const msg of this.messages) {
|
|
777
|
+
const isAI = msg.sender.username === 'ai-assistant';
|
|
778
|
+
if (!isAI && msg.attachments?.length) {
|
|
779
|
+
const enhancedContent = await Private.processAttachments(
|
|
780
|
+
msg.attachments,
|
|
781
|
+
this.input.documentManager,
|
|
782
|
+
msg.body,
|
|
783
|
+
supportsImages,
|
|
784
|
+
supportsPdf,
|
|
785
|
+
supportsAudio
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
modelMessages.push({
|
|
789
|
+
role: 'user',
|
|
790
|
+
content: enhancedContent
|
|
791
|
+
} as ModelMessage);
|
|
792
|
+
} else if (msg.body) {
|
|
793
|
+
modelMessages.push({
|
|
794
|
+
role: isAI ? 'assistant' : 'user',
|
|
795
|
+
content: msg.body
|
|
796
|
+
} as ModelMessage);
|
|
797
|
+
}
|
|
798
|
+
// Skip messages with empty body like tool calls
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
this._agentManager.setHistory(modelMessages);
|
|
802
|
+
}
|
|
803
|
+
|
|
503
804
|
/**
|
|
504
805
|
* Handles events emitted by the agent manager.
|
|
505
806
|
* @param event The event data containing type and payload
|
|
@@ -700,13 +1001,18 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
700
1001
|
body: '',
|
|
701
1002
|
mime_model: {
|
|
702
1003
|
data: {
|
|
703
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
1004
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
704
1005
|
},
|
|
705
1006
|
metadata: {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1007
|
+
toolCalls: [
|
|
1008
|
+
{
|
|
1009
|
+
toolCallId: context.toolCallId,
|
|
1010
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
1011
|
+
kind: context.toolName,
|
|
1012
|
+
status: 'in_progress',
|
|
1013
|
+
rawInput: context.input
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
710
1016
|
}
|
|
711
1017
|
},
|
|
712
1018
|
sender: this._getAIUser(),
|
|
@@ -778,7 +1084,15 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
778
1084
|
*/
|
|
779
1085
|
private _handleErrorEvent(event: IAgentManager.IAgentEvent<'error'>): void {
|
|
780
1086
|
this.messageAdded({
|
|
781
|
-
body:
|
|
1087
|
+
body: '',
|
|
1088
|
+
mime_model: {
|
|
1089
|
+
data: {
|
|
1090
|
+
'application/vnd.jupyter.chat.components': 'error'
|
|
1091
|
+
},
|
|
1092
|
+
metadata: {
|
|
1093
|
+
errorMessage: `Error generating response: ${event.data.error.message}`
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
782
1096
|
sender: this._getAIUser(),
|
|
783
1097
|
id: UUID.uuid4(),
|
|
784
1098
|
time: Date.now() / 1000,
|
|
@@ -797,7 +1111,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
797
1111
|
if (!context) {
|
|
798
1112
|
return;
|
|
799
1113
|
}
|
|
800
|
-
context.approvalId = event.data.approvalId;
|
|
801
1114
|
context.input = JSON.stringify(event.data.args, null, 2);
|
|
802
1115
|
this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
|
|
803
1116
|
}
|
|
@@ -808,15 +1121,13 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
808
1121
|
private _handleToolApprovalResolved(
|
|
809
1122
|
event: IAgentManager.IAgentEvent<'tool_approval_resolved'>
|
|
810
1123
|
): void {
|
|
811
|
-
const context =
|
|
812
|
-
ctx => ctx.approvalId === event.data.approvalId
|
|
813
|
-
);
|
|
1124
|
+
const context = this._toolContexts.get(event.data.toolCallId);
|
|
814
1125
|
if (!context) {
|
|
815
1126
|
return;
|
|
816
1127
|
}
|
|
817
1128
|
|
|
818
1129
|
const status = event.data.approved ? 'approved' : 'rejected';
|
|
819
|
-
this._updateToolCallUI(
|
|
1130
|
+
this._updateToolCallUI(event.data.toolCallId, status);
|
|
820
1131
|
|
|
821
1132
|
if (!event.data.approved) {
|
|
822
1133
|
this._toolContexts.delete(context.toolCallId);
|
|
@@ -847,76 +1158,390 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
847
1158
|
existingMessage.update({
|
|
848
1159
|
mime_model: {
|
|
849
1160
|
data: {
|
|
850
|
-
'application/vnd.jupyter.chat.components': 'tool-
|
|
1161
|
+
'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
|
|
851
1162
|
},
|
|
852
1163
|
metadata: {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1164
|
+
toolCalls: [
|
|
1165
|
+
{
|
|
1166
|
+
toolCallId: context.toolCallId,
|
|
1167
|
+
title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
|
|
1168
|
+
kind: context.toolName,
|
|
1169
|
+
status: context.status,
|
|
1170
|
+
rawInput: context.input,
|
|
1171
|
+
rawOutput: output,
|
|
1172
|
+
sessionId: this.name,
|
|
1173
|
+
permissionStatus:
|
|
1174
|
+
status === 'awaiting_approval' ? 'pending' : 'resolved',
|
|
1175
|
+
...(status === 'awaiting_approval' && {
|
|
1176
|
+
permissionOptions: [
|
|
1177
|
+
{ optionId: 'approve', name: 'Approve', kind: 'allow_once' },
|
|
1178
|
+
{ optionId: 'reject', name: 'Reject', kind: 'reject_once' }
|
|
1179
|
+
]
|
|
1180
|
+
})
|
|
1181
|
+
}
|
|
1182
|
+
]
|
|
860
1183
|
}
|
|
861
1184
|
}
|
|
862
1185
|
});
|
|
863
1186
|
}
|
|
864
1187
|
|
|
865
1188
|
/**
|
|
866
|
-
*
|
|
1189
|
+
* The current message queue
|
|
1190
|
+
*/
|
|
1191
|
+
get messageQueue(): Private.IQueuedItem[] {
|
|
1192
|
+
return this._messageQueue;
|
|
1193
|
+
}
|
|
1194
|
+
set messageQueue(value: Private.IQueuedItem[]) {
|
|
1195
|
+
this._messageQueue = value;
|
|
1196
|
+
this._updateQueueUI();
|
|
1197
|
+
if (this._messageQueue.length > 0 && !this._isBusy) {
|
|
1198
|
+
this._drainQueue();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Whether the chat is busy
|
|
1204
|
+
*/
|
|
1205
|
+
get isBusy(): boolean {
|
|
1206
|
+
return this._isBusy;
|
|
1207
|
+
}
|
|
1208
|
+
set isBusy(value: boolean) {
|
|
1209
|
+
this._isBusy = value;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Private fields
|
|
1213
|
+
private _settingsModel: IAISettingsModel;
|
|
1214
|
+
private _user: IUser;
|
|
1215
|
+
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
1216
|
+
private _agentManager: IAgentManager;
|
|
1217
|
+
private _providerRegistry?: IProviderRegistry;
|
|
1218
|
+
private _currentModelKey: string | undefined;
|
|
1219
|
+
private _currentStreamingMessage: IMessage | null = null;
|
|
1220
|
+
private _nameChanged = new Signal<IAIChatModel, string>(this);
|
|
1221
|
+
private _contentsManager?: Contents.IManager;
|
|
1222
|
+
private _autosave: boolean = false;
|
|
1223
|
+
private _autosaveChanged = new Signal<IAIChatModel, boolean>(this);
|
|
1224
|
+
private _autosaveDebouncer: Debouncer;
|
|
1225
|
+
private _messageQueue: Private.IQueuedItem[] = [];
|
|
1226
|
+
private _isBusy: boolean = false;
|
|
1227
|
+
private _queueMessageId: string | null = null;
|
|
1228
|
+
private _title: string | null = null;
|
|
1229
|
+
private _titleChanged = new Signal<IAIChatModel, string | null>(this);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
namespace Private {
|
|
1233
|
+
export interface IQueuedItem {
|
|
1234
|
+
id: string;
|
|
1235
|
+
body: string;
|
|
1236
|
+
_originalMsg: IMessageContent;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
type IDisplayOutput =
|
|
1240
|
+
| nbformat.IDisplayData
|
|
1241
|
+
| nbformat.IDisplayUpdate
|
|
1242
|
+
| nbformat.IExecuteResult;
|
|
1243
|
+
|
|
1244
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
1245
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
|
|
1249
|
+
if (!isPlainObject(value)) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const output = value as nbformat.IOutput;
|
|
1254
|
+
return (
|
|
1255
|
+
nbformat.isDisplayData(output) ||
|
|
1256
|
+
nbformat.isDisplayUpdate(output) ||
|
|
1257
|
+
nbformat.isExecuteResult(output)
|
|
1258
|
+
);
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const toMimeBundle = (
|
|
1262
|
+
value: IDisplayOutput,
|
|
1263
|
+
trustedMimeTypes: ReadonlySet<string>
|
|
1264
|
+
): IMimeModelBody | null => {
|
|
1265
|
+
const data = value.data;
|
|
1266
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
data: data as IRenderMime.IMimeModel['data'],
|
|
1272
|
+
...(isPlainObject(value.metadata)
|
|
1273
|
+
? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
|
|
1274
|
+
: {}),
|
|
1275
|
+
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
1276
|
+
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
1277
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
1278
|
+
? { trusted: true }
|
|
1279
|
+
: {})
|
|
1280
|
+
};
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
1285
|
+
*
|
|
1286
|
+
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
1287
|
+
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
1288
|
+
*/
|
|
1289
|
+
const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
|
|
1290
|
+
if (isDisplayOutput(value)) {
|
|
1291
|
+
return [value];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (Array.isArray(value)) {
|
|
1295
|
+
return value.filter(isDisplayOutput);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (!isPlainObject(value)) {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (Array.isArray(value.outputs)) {
|
|
1303
|
+
return value.outputs.filter(isDisplayOutput);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if ('result' in value) {
|
|
1307
|
+
return toDisplayOutputs(value.result);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return [];
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
1315
|
+
*/
|
|
1316
|
+
export function extractMimeBundlesFromUnknown(
|
|
1317
|
+
content: unknown,
|
|
1318
|
+
options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
|
|
1319
|
+
): IMimeModelBody[] {
|
|
1320
|
+
const bundles: IMimeModelBody[] = [];
|
|
1321
|
+
const outputs = toDisplayOutputs(content);
|
|
1322
|
+
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1323
|
+
for (const output of outputs) {
|
|
1324
|
+
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1325
|
+
if (bundle) {
|
|
1326
|
+
bundles.push(bundle);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return bundles;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export function formatToolOutput(outputData: unknown): string {
|
|
1333
|
+
if (typeof outputData === 'string') {
|
|
1334
|
+
return outputData;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
try {
|
|
1338
|
+
return JSON.stringify(outputData, null, 2);
|
|
1339
|
+
} catch {
|
|
1340
|
+
return '[Complex object - cannot serialize]';
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Processes file attachments and returns the message content with the attachments.
|
|
867
1346
|
* @param attachments Array of file attachments to process
|
|
868
|
-
* @
|
|
1347
|
+
* @param documentManager Optional document manager for file operations
|
|
1348
|
+
* @param body The message body
|
|
1349
|
+
* @param supportsImages Whether the model supports images
|
|
1350
|
+
* @param supportsPdf Whether the model supports pdfs
|
|
1351
|
+
* @param supportsAudio Whether the model supports audio
|
|
1352
|
+
* @returns Enhanced message content
|
|
869
1353
|
*/
|
|
870
|
-
|
|
871
|
-
attachments: IAttachment[]
|
|
872
|
-
|
|
873
|
-
|
|
1354
|
+
export async function processAttachments(
|
|
1355
|
+
attachments: IAttachment[],
|
|
1356
|
+
documentManager: IDocumentManager | null | undefined,
|
|
1357
|
+
body: string,
|
|
1358
|
+
supportsImages: boolean,
|
|
1359
|
+
supportsPdf: boolean,
|
|
1360
|
+
supportsAudio: boolean
|
|
1361
|
+
): Promise<UserContent> {
|
|
1362
|
+
const textContents: string[] = [];
|
|
1363
|
+
const includedParts: Array<ImagePart | FilePart> = [];
|
|
1364
|
+
const omittedNames: string[] = [];
|
|
1365
|
+
|
|
1366
|
+
if (!documentManager) {
|
|
1367
|
+
return body;
|
|
1368
|
+
}
|
|
874
1369
|
|
|
875
1370
|
for (const attachment of attachments) {
|
|
876
1371
|
try {
|
|
877
1372
|
if (attachment.type === 'notebook' && attachment.cells?.length) {
|
|
878
|
-
const cellContents = await
|
|
1373
|
+
const cellContents = await readNotebookCells(
|
|
1374
|
+
attachment,
|
|
1375
|
+
documentManager
|
|
1376
|
+
);
|
|
879
1377
|
if (cellContents) {
|
|
880
|
-
|
|
1378
|
+
textContents.push(cellContents);
|
|
881
1379
|
}
|
|
882
1380
|
} else {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1381
|
+
let mimetype = attachment.mimetype;
|
|
1382
|
+
const fileExtension = PathExt.extname(attachment.value).toLowerCase();
|
|
1383
|
+
|
|
1384
|
+
// Fetch mimetype from server metadata if not provided
|
|
1385
|
+
if (!mimetype) {
|
|
1386
|
+
try {
|
|
1387
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1388
|
+
attachment.value,
|
|
1389
|
+
{ content: false }
|
|
1390
|
+
);
|
|
1391
|
+
mimetype = diskModel?.mimetype;
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
console.warn(
|
|
1394
|
+
`Failed to fetch metadata for ${attachment.value}:`,
|
|
1395
|
+
e
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (mimetype?.startsWith('image/')) {
|
|
1401
|
+
if (supportsImages) {
|
|
1402
|
+
const data = await readBinaryAttachment(
|
|
1403
|
+
attachment,
|
|
1404
|
+
documentManager
|
|
1405
|
+
);
|
|
1406
|
+
if (data) {
|
|
1407
|
+
includedParts.push({
|
|
1408
|
+
type: 'image',
|
|
1409
|
+
image: data,
|
|
1410
|
+
mediaType: mimetype
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1415
|
+
}
|
|
1416
|
+
} else if (mimetype === 'application/pdf') {
|
|
1417
|
+
if (supportsPdf) {
|
|
1418
|
+
const data = await readBinaryAttachment(
|
|
1419
|
+
attachment,
|
|
1420
|
+
documentManager
|
|
1421
|
+
);
|
|
1422
|
+
if (data) {
|
|
1423
|
+
includedParts.push({
|
|
1424
|
+
type: 'file',
|
|
1425
|
+
data,
|
|
1426
|
+
mediaType: mimetype,
|
|
1427
|
+
filename: PathExt.basename(attachment.value)
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
} else {
|
|
1431
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1432
|
+
}
|
|
1433
|
+
} else if (mimetype?.startsWith('audio/')) {
|
|
1434
|
+
if (supportsAudio) {
|
|
1435
|
+
const data = await readBinaryAttachment(
|
|
1436
|
+
attachment,
|
|
1437
|
+
documentManager
|
|
1438
|
+
);
|
|
1439
|
+
if (data) {
|
|
1440
|
+
includedParts.push({
|
|
1441
|
+
type: 'file',
|
|
1442
|
+
data,
|
|
1443
|
+
mediaType: mimetype,
|
|
1444
|
+
filename: PathExt.basename(attachment.value)
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
omittedNames.push(PathExt.basename(attachment.value));
|
|
1449
|
+
}
|
|
1450
|
+
} else {
|
|
1451
|
+
const fileContent = await readFileAttachment(
|
|
1452
|
+
attachment,
|
|
1453
|
+
documentManager
|
|
891
1454
|
);
|
|
1455
|
+
if (fileContent) {
|
|
1456
|
+
const language =
|
|
1457
|
+
fileExtension === '.ipynb' ||
|
|
1458
|
+
mimetype === 'application/x-ipynb+json'
|
|
1459
|
+
? 'json'
|
|
1460
|
+
: '';
|
|
1461
|
+
textContents.push(
|
|
1462
|
+
`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
892
1465
|
}
|
|
893
1466
|
}
|
|
894
1467
|
} catch (error) {
|
|
895
1468
|
console.warn(`Failed to read attachment ${attachment.value}:`, error);
|
|
896
|
-
|
|
1469
|
+
textContents.push(
|
|
1470
|
+
`**File: ${attachment.value}** (Could not read file)`
|
|
1471
|
+
);
|
|
897
1472
|
}
|
|
898
1473
|
}
|
|
899
1474
|
|
|
900
|
-
|
|
1475
|
+
let textPart = body;
|
|
1476
|
+
if (textContents.length > 0) {
|
|
1477
|
+
textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (omittedNames.length > 0) {
|
|
1481
|
+
textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return includedParts.length > 0
|
|
1485
|
+
? [{ type: 'text', text: textPart }, ...includedParts]
|
|
1486
|
+
: textPart;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Reads a binary attachment and returns its base64-encoded content.
|
|
1491
|
+
* @param attachment The attachment to read
|
|
1492
|
+
* @param documentManager Optional document manager for file operations
|
|
1493
|
+
* @returns Base64 string or null if unable to read
|
|
1494
|
+
*/
|
|
1495
|
+
export async function readBinaryAttachment(
|
|
1496
|
+
attachment: IAttachment,
|
|
1497
|
+
documentManager: IDocumentManager | null | undefined
|
|
1498
|
+
): Promise<string | null> {
|
|
1499
|
+
if (!documentManager) {
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
try {
|
|
1504
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1505
|
+
attachment.value,
|
|
1506
|
+
{ content: true }
|
|
1507
|
+
);
|
|
1508
|
+
if (diskModel?.content && diskModel.format === 'base64') {
|
|
1509
|
+
// Strip whitespace/newlines
|
|
1510
|
+
return (diskModel.content as string).replace(/\s/g, '');
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
console.warn(
|
|
1515
|
+
`Failed to read binary attachment ${attachment.value}:`,
|
|
1516
|
+
error
|
|
1517
|
+
);
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
901
1520
|
}
|
|
902
1521
|
|
|
903
1522
|
/**
|
|
904
1523
|
* Reads the content of a notebook cell.
|
|
905
1524
|
* @param attachment The notebook attachment to read
|
|
1525
|
+
* @param documentManager Optional document manager for file operations
|
|
906
1526
|
* @returns Cell content as string or null if unable to read
|
|
907
1527
|
*/
|
|
908
|
-
|
|
909
|
-
attachment: IAttachment
|
|
1528
|
+
export async function readNotebookCells(
|
|
1529
|
+
attachment: IAttachment,
|
|
1530
|
+
documentManager: IDocumentManager | null | undefined
|
|
910
1531
|
): Promise<string | null> {
|
|
911
|
-
if (
|
|
1532
|
+
if (
|
|
1533
|
+
attachment.type !== 'notebook' ||
|
|
1534
|
+
!attachment.cells ||
|
|
1535
|
+
!documentManager
|
|
1536
|
+
) {
|
|
912
1537
|
return null;
|
|
913
1538
|
}
|
|
914
1539
|
|
|
915
1540
|
try {
|
|
916
1541
|
// Try reading from live notebook if open
|
|
917
|
-
const widget =
|
|
918
|
-
|
|
919
|
-
|
|
1542
|
+
const widget = documentManager.findWidget(attachment.value) as
|
|
1543
|
+
| IDocumentWidget<Notebook, INotebookModel>
|
|
1544
|
+
| undefined;
|
|
920
1545
|
let cellData: nbformat.ICell[];
|
|
921
1546
|
let kernelLang = 'text';
|
|
922
1547
|
|
|
@@ -935,7 +1560,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
935
1560
|
kernelLang = String(lang);
|
|
936
1561
|
} else {
|
|
937
1562
|
// Fallback: reading from disk
|
|
938
|
-
const model = await
|
|
1563
|
+
const model = await documentManager.services.contents.get(
|
|
939
1564
|
attachment.value
|
|
940
1565
|
);
|
|
941
1566
|
if (!model || model.type !== 'notebook') {
|
|
@@ -1079,21 +1704,26 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1079
1704
|
/**
|
|
1080
1705
|
* Reads the content of a file attachment.
|
|
1081
1706
|
* @param attachment The file attachment to read
|
|
1707
|
+
* @param documentManager Optional document manager for file operations
|
|
1082
1708
|
* @returns File content as string or null if unable to read
|
|
1083
1709
|
*/
|
|
1084
|
-
|
|
1085
|
-
attachment: IAttachment
|
|
1710
|
+
export async function readFileAttachment(
|
|
1711
|
+
attachment: IAttachment,
|
|
1712
|
+
documentManager: IDocumentManager | null | undefined
|
|
1086
1713
|
): Promise<string | null> {
|
|
1087
1714
|
// Handle both 'file' and 'notebook' types since both have a 'value' path
|
|
1088
|
-
if (
|
|
1715
|
+
if (
|
|
1716
|
+
(attachment.type !== 'file' && attachment.type !== 'notebook') ||
|
|
1717
|
+
!documentManager
|
|
1718
|
+
) {
|
|
1089
1719
|
return null;
|
|
1090
1720
|
}
|
|
1091
1721
|
|
|
1092
1722
|
try {
|
|
1093
1723
|
// Try reading from an open widget first
|
|
1094
|
-
const widget =
|
|
1095
|
-
|
|
1096
|
-
|
|
1724
|
+
const widget = documentManager.findWidget(attachment.value) as
|
|
1725
|
+
| IDocumentWidget<Notebook, INotebookModel>
|
|
1726
|
+
| undefined;
|
|
1097
1727
|
|
|
1098
1728
|
if (widget && widget.context && widget.context.model) {
|
|
1099
1729
|
const model = widget.context.model;
|
|
@@ -1108,7 +1738,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1108
1738
|
}
|
|
1109
1739
|
|
|
1110
1740
|
// If not open, load from disk
|
|
1111
|
-
const diskModel = await
|
|
1741
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1112
1742
|
attachment.value
|
|
1113
1743
|
);
|
|
1114
1744
|
|
|
@@ -1139,127 +1769,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1139
1769
|
return null;
|
|
1140
1770
|
}
|
|
1141
1771
|
}
|
|
1142
|
-
|
|
1143
|
-
// Private fields
|
|
1144
|
-
private _settingsModel: IAISettingsModel;
|
|
1145
|
-
private _user: IUser;
|
|
1146
|
-
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
1147
|
-
private _agentManager: IAgentManager;
|
|
1148
|
-
private _currentStreamingMessage: IMessage | null = null;
|
|
1149
|
-
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
1150
|
-
private _contentsManager?: Contents.IManager;
|
|
1151
|
-
private _autosave: boolean = false;
|
|
1152
|
-
private _autosaveChanged = new Signal<AIChatModel, boolean>(this);
|
|
1153
|
-
private _autosaveDebouncer: Debouncer;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
namespace Private {
|
|
1157
|
-
type IMimeBody = Partial<IRenderMime.IMimeModel> &
|
|
1158
|
-
Pick<IRenderMime.IMimeModel, 'data'>;
|
|
1159
|
-
type IDisplayOutput =
|
|
1160
|
-
| nbformat.IDisplayData
|
|
1161
|
-
| nbformat.IDisplayUpdate
|
|
1162
|
-
| nbformat.IExecuteResult;
|
|
1163
|
-
|
|
1164
|
-
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
1165
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1166
|
-
};
|
|
1167
|
-
|
|
1168
|
-
const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
|
|
1169
|
-
if (!isPlainObject(value)) {
|
|
1170
|
-
return false;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const output = value as nbformat.IOutput;
|
|
1174
|
-
return (
|
|
1175
|
-
nbformat.isDisplayData(output) ||
|
|
1176
|
-
nbformat.isDisplayUpdate(output) ||
|
|
1177
|
-
nbformat.isExecuteResult(output)
|
|
1178
|
-
);
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
const toMimeBundle = (
|
|
1182
|
-
value: IDisplayOutput,
|
|
1183
|
-
trustedMimeTypes: ReadonlySet<string>
|
|
1184
|
-
): IMimeBody | null => {
|
|
1185
|
-
const data = value.data;
|
|
1186
|
-
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
return {
|
|
1191
|
-
data: data as IRenderMime.IMimeModel['data'],
|
|
1192
|
-
...(isPlainObject(value.metadata)
|
|
1193
|
-
? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
|
|
1194
|
-
: {}),
|
|
1195
|
-
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
1196
|
-
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
1197
|
-
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
1198
|
-
? { trusted: true }
|
|
1199
|
-
: {})
|
|
1200
|
-
};
|
|
1201
|
-
};
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
1205
|
-
*
|
|
1206
|
-
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
1207
|
-
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
1208
|
-
*/
|
|
1209
|
-
const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
|
|
1210
|
-
if (isDisplayOutput(value)) {
|
|
1211
|
-
return [value];
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (Array.isArray(value)) {
|
|
1215
|
-
return value.filter(isDisplayOutput);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if (!isPlainObject(value)) {
|
|
1219
|
-
return [];
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
if (Array.isArray(value.outputs)) {
|
|
1223
|
-
return value.outputs.filter(isDisplayOutput);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
if ('result' in value) {
|
|
1227
|
-
return toDisplayOutputs(value.result);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
return [];
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
1235
|
-
*/
|
|
1236
|
-
export function extractMimeBundlesFromUnknown(
|
|
1237
|
-
content: unknown,
|
|
1238
|
-
options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
|
|
1239
|
-
): IMimeBody[] {
|
|
1240
|
-
const bundles: IMimeBody[] = [];
|
|
1241
|
-
const outputs = toDisplayOutputs(content);
|
|
1242
|
-
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1243
|
-
for (const output of outputs) {
|
|
1244
|
-
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1245
|
-
if (bundle) {
|
|
1246
|
-
bundles.push(bundle);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
return bundles;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
export function formatToolOutput(outputData: unknown): string {
|
|
1253
|
-
if (typeof outputData === 'string') {
|
|
1254
|
-
return outputData;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
try {
|
|
1258
|
-
return JSON.stringify(outputData, null, 2);
|
|
1259
|
-
} catch {
|
|
1260
|
-
return '[Complex object - cannot serialize]';
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
1772
|
}
|
|
1264
1773
|
|
|
1265
1774
|
/**
|
|
@@ -1294,6 +1803,10 @@ export namespace AIChatModel {
|
|
|
1294
1803
|
* The contents manager.
|
|
1295
1804
|
*/
|
|
1296
1805
|
contentsManager?: Contents.IManager;
|
|
1806
|
+
/**
|
|
1807
|
+
* Optional provider registry for model capability lookups.
|
|
1808
|
+
*/
|
|
1809
|
+
providerRegistry?: IProviderRegistry;
|
|
1297
1810
|
/**
|
|
1298
1811
|
* Whether to restore or not the message (default to true)
|
|
1299
1812
|
*/
|
|
@@ -1311,7 +1824,7 @@ export namespace AIChatModel {
|
|
|
1311
1824
|
/**
|
|
1312
1825
|
* The clear messages callback.
|
|
1313
1826
|
*/
|
|
1314
|
-
clearMessages: () => void
|
|
1827
|
+
clearMessages: () => Promise<void>;
|
|
1315
1828
|
/**
|
|
1316
1829
|
* Adds an assistant/system message to the chat.
|
|
1317
1830
|
*/
|
|
@@ -1350,6 +1863,10 @@ export namespace AIChatModel {
|
|
|
1350
1863
|
* Whether the chat is automatically saved.
|
|
1351
1864
|
*/
|
|
1352
1865
|
autosave?: boolean;
|
|
1866
|
+
/**
|
|
1867
|
+
* An optional title of the chat.
|
|
1868
|
+
*/
|
|
1869
|
+
title?: string;
|
|
1353
1870
|
};
|
|
1354
1871
|
};
|
|
1355
1872
|
}
|