@jupyterlite/ai 0.15.0 → 0.17.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 -2
- package/lib/agent.js +112 -17
- package/lib/chat-commands/clear.js +1 -1
- package/lib/chat-model-handler.js +4 -1
- package/lib/chat-model.d.ts +25 -24
- package/lib/chat-model.js +262 -132
- package/lib/components/clear-button.d.ts +1 -1
- package/lib/components/clear-button.js +1 -1
- package/lib/components/index.d.ts +1 -1
- package/lib/components/index.js +1 -1
- package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
- package/lib/components/usage-display.js +109 -0
- package/lib/index.js +205 -20
- package/lib/models/settings-model.js +1 -0
- package/lib/providers/built-in-providers.js +5 -0
- package/lib/providers/generated-context-windows.d.ts +8 -0
- package/lib/providers/generated-context-windows.js +96 -0
- package/lib/providers/model-info.d.ts +3 -0
- package/lib/providers/model-info.js +58 -0
- package/lib/tokens.d.ts +34 -3
- package/lib/tokens.js +8 -7
- package/lib/widgets/ai-settings.js +9 -0
- package/lib/widgets/main-area-chat.d.ts +1 -0
- package/lib/widgets/main-area-chat.js +10 -4
- package/lib/widgets/provider-config-dialog.js +18 -5
- package/package.json +3 -2
- package/schema/settings-model.json +11 -0
- package/src/agent.ts +151 -21
- package/src/chat-commands/clear.ts +1 -1
- package/src/chat-model-handler.ts +6 -1
- package/src/chat-model.ts +350 -175
- package/src/components/clear-button.tsx +3 -3
- package/src/components/index.ts +1 -1
- package/src/components/usage-display.tsx +208 -0
- package/src/index.ts +250 -26
- package/src/models/settings-model.ts +1 -0
- package/src/providers/built-in-providers.ts +5 -0
- package/src/providers/generated-context-windows.ts +102 -0
- package/src/providers/model-info.ts +88 -0
- package/src/tokens.ts +46 -10
- package/src/widgets/ai-settings.tsx +42 -0
- package/src/widgets/main-area-chat.ts +12 -4
- package/src/widgets/provider-config-dialog.tsx +45 -5
- package/lib/components/token-usage-display.js +0 -72
- package/src/components/token-usage-display.tsx +0 -137
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,6 +32,8 @@ 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
39
|
import type { IAgentManager, IAISettingsModel, ITokenUsage } from './tokens';
|
|
@@ -133,6 +136,34 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
133
136
|
this.setReady();
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
/**
|
|
140
|
+
* A signal emitting when the chat name has changed.
|
|
141
|
+
*/
|
|
142
|
+
get nameChanged(): ISignal<AIChatModel, string> {
|
|
143
|
+
return this._nameChanged;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The title of the chat.
|
|
148
|
+
*/
|
|
149
|
+
get title(): string | null {
|
|
150
|
+
return this._title;
|
|
151
|
+
}
|
|
152
|
+
set title(value: string | null) {
|
|
153
|
+
this._title = value;
|
|
154
|
+
if (this.autosave) {
|
|
155
|
+
this._autosaveDebouncer.invoke();
|
|
156
|
+
}
|
|
157
|
+
this._titleChanged.emit(this._title);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A signal emitting when the chat title has changed.
|
|
162
|
+
*/
|
|
163
|
+
get titleChanged(): ISignal<AIChatModel, string | null> {
|
|
164
|
+
return this._titleChanged;
|
|
165
|
+
}
|
|
166
|
+
|
|
136
167
|
/**
|
|
137
168
|
* Whether to save the chat automatically.
|
|
138
169
|
*/
|
|
@@ -171,13 +202,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
171
202
|
return this._autosaveChanged;
|
|
172
203
|
}
|
|
173
204
|
|
|
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
205
|
/**
|
|
182
206
|
* Gets the current user information.
|
|
183
207
|
*/
|
|
@@ -243,10 +267,10 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
243
267
|
/**
|
|
244
268
|
* Clears all messages from the chat and resets conversation state.
|
|
245
269
|
*/
|
|
246
|
-
clearMessages = (): void => {
|
|
270
|
+
clearMessages = async (): Promise<void> => {
|
|
247
271
|
this.messagesDeleted(0, this.messages.length);
|
|
248
272
|
this._toolContexts.clear();
|
|
249
|
-
this._agentManager.clearHistory();
|
|
273
|
+
await this._agentManager.clearHistory();
|
|
250
274
|
};
|
|
251
275
|
|
|
252
276
|
/**
|
|
@@ -303,16 +327,24 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
303
327
|
|
|
304
328
|
try {
|
|
305
329
|
// Process attachments and add their content to the message
|
|
306
|
-
let enhancedMessage = message.body;
|
|
330
|
+
let enhancedMessage: UserContent = message.body;
|
|
307
331
|
if (this.input.attachments.length > 0) {
|
|
308
|
-
const
|
|
309
|
-
this.input.attachments
|
|
332
|
+
const { textContents, binaryParts } = await Private.processAttachments(
|
|
333
|
+
this.input.attachments,
|
|
334
|
+
this.input.documentManager
|
|
310
335
|
);
|
|
311
336
|
this.input.clearAttachments();
|
|
312
337
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
338
|
+
let textPart = message.body;
|
|
339
|
+
if (textContents.length > 0) {
|
|
340
|
+
textPart +=
|
|
341
|
+
'\n\n--- Attached Files ---\n' + textContents.join('\n\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (binaryParts.length > 0) {
|
|
345
|
+
enhancedMessage = [{ type: 'text', text: textPart }, ...binaryParts];
|
|
346
|
+
} else {
|
|
347
|
+
enhancedMessage = textPart;
|
|
316
348
|
}
|
|
317
349
|
}
|
|
318
350
|
|
|
@@ -376,7 +408,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
376
408
|
return false;
|
|
377
409
|
}
|
|
378
410
|
const contentModel = await this._contentsManager
|
|
379
|
-
.get(filepath, { content: true })
|
|
411
|
+
.get(filepath, { content: true, type: 'file', format: 'text' })
|
|
380
412
|
.catch(() => {
|
|
381
413
|
if (!silent) {
|
|
382
414
|
console.log(`There is no backup for chat '${this.name}'`);
|
|
@@ -386,9 +418,12 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
386
418
|
if (!contentModel) {
|
|
387
419
|
return false;
|
|
388
420
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
421
|
+
let content: AIChatModel.ExportedChat;
|
|
422
|
+
try {
|
|
423
|
+
content = JSON.parse(contentModel.content);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
throw `Error when parsing the chat ${filepath}\n${e}`;
|
|
426
|
+
}
|
|
392
427
|
|
|
393
428
|
if (content.metadata?.provider) {
|
|
394
429
|
if (this._settingsModel.getProvider(content.metadata.provider)) {
|
|
@@ -415,13 +450,39 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
415
450
|
attachments
|
|
416
451
|
};
|
|
417
452
|
});
|
|
418
|
-
this.clearMessages();
|
|
453
|
+
await this.clearMessages();
|
|
419
454
|
this.messagesInserted(0, messages);
|
|
420
455
|
this._agentManager.setHistory(messages);
|
|
421
456
|
this.autosave = content.metadata?.autosave ?? false;
|
|
457
|
+
this.title = content.metadata?.title ?? null;
|
|
422
458
|
return true;
|
|
423
459
|
};
|
|
424
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Request a title to this chat, regarding the message history.
|
|
463
|
+
*/
|
|
464
|
+
async requestTitle(): Promise<string> {
|
|
465
|
+
const history = this.messages
|
|
466
|
+
.filter(msg => msg.body !== '')
|
|
467
|
+
.map(
|
|
468
|
+
msg =>
|
|
469
|
+
`${msg.sender.username === 'ai-assistant' ? 'assistant' : 'user'}: ${msg.body}`
|
|
470
|
+
)
|
|
471
|
+
.join('\n');
|
|
472
|
+
const messages: ModelMessage[] = [
|
|
473
|
+
{
|
|
474
|
+
role: 'system',
|
|
475
|
+
content:
|
|
476
|
+
"Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting. Focus on the user's main intent."
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
role: 'user',
|
|
480
|
+
content: history
|
|
481
|
+
}
|
|
482
|
+
];
|
|
483
|
+
return this.agentManager.textResponse(messages);
|
|
484
|
+
}
|
|
485
|
+
|
|
425
486
|
/**
|
|
426
487
|
* Serialize the model for backup
|
|
427
488
|
*/
|
|
@@ -471,7 +532,8 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
471
532
|
attachments,
|
|
472
533
|
metadata: {
|
|
473
534
|
provider,
|
|
474
|
-
autosave: this.autosave
|
|
535
|
+
autosave: this.autosave,
|
|
536
|
+
...(this.title ? { title: this.title } : {})
|
|
475
537
|
}
|
|
476
538
|
};
|
|
477
539
|
}
|
|
@@ -859,61 +921,286 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
859
921
|
});
|
|
860
922
|
}
|
|
861
923
|
|
|
924
|
+
// Private fields
|
|
925
|
+
private _settingsModel: IAISettingsModel;
|
|
926
|
+
private _user: IUser;
|
|
927
|
+
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
928
|
+
private _agentManager: IAgentManager;
|
|
929
|
+
private _currentStreamingMessage: IMessage | null = null;
|
|
930
|
+
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
931
|
+
private _contentsManager?: Contents.IManager;
|
|
932
|
+
private _autosave: boolean = false;
|
|
933
|
+
private _autosaveChanged = new Signal<AIChatModel, boolean>(this);
|
|
934
|
+
private _autosaveDebouncer: Debouncer;
|
|
935
|
+
private _title: string | null = null;
|
|
936
|
+
private _titleChanged = new Signal<AIChatModel, string | null>(this);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
namespace Private {
|
|
940
|
+
type IDisplayOutput =
|
|
941
|
+
| nbformat.IDisplayData
|
|
942
|
+
| nbformat.IDisplayUpdate
|
|
943
|
+
| nbformat.IExecuteResult;
|
|
944
|
+
|
|
945
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
946
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
|
|
950
|
+
if (!isPlainObject(value)) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const output = value as nbformat.IOutput;
|
|
955
|
+
return (
|
|
956
|
+
nbformat.isDisplayData(output) ||
|
|
957
|
+
nbformat.isDisplayUpdate(output) ||
|
|
958
|
+
nbformat.isExecuteResult(output)
|
|
959
|
+
);
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const toMimeBundle = (
|
|
963
|
+
value: IDisplayOutput,
|
|
964
|
+
trustedMimeTypes: ReadonlySet<string>
|
|
965
|
+
): IMimeModelBody | null => {
|
|
966
|
+
const data = value.data;
|
|
967
|
+
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
data: data as IRenderMime.IMimeModel['data'],
|
|
973
|
+
...(isPlainObject(value.metadata)
|
|
974
|
+
? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
|
|
975
|
+
: {}),
|
|
976
|
+
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
977
|
+
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
978
|
+
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
979
|
+
? { trusted: true }
|
|
980
|
+
: {})
|
|
981
|
+
};
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
986
|
+
*
|
|
987
|
+
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
988
|
+
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
989
|
+
*/
|
|
990
|
+
const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
|
|
991
|
+
if (isDisplayOutput(value)) {
|
|
992
|
+
return [value];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (Array.isArray(value)) {
|
|
996
|
+
return value.filter(isDisplayOutput);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (!isPlainObject(value)) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (Array.isArray(value.outputs)) {
|
|
1004
|
+
return value.outputs.filter(isDisplayOutput);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if ('result' in value) {
|
|
1008
|
+
return toDisplayOutputs(value.result);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return [];
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
1016
|
+
*/
|
|
1017
|
+
export function extractMimeBundlesFromUnknown(
|
|
1018
|
+
content: unknown,
|
|
1019
|
+
options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
|
|
1020
|
+
): IMimeModelBody[] {
|
|
1021
|
+
const bundles: IMimeModelBody[] = [];
|
|
1022
|
+
const outputs = toDisplayOutputs(content);
|
|
1023
|
+
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1024
|
+
for (const output of outputs) {
|
|
1025
|
+
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1026
|
+
if (bundle) {
|
|
1027
|
+
bundles.push(bundle);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return bundles;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export function formatToolOutput(outputData: unknown): string {
|
|
1034
|
+
if (typeof outputData === 'string') {
|
|
1035
|
+
return outputData;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
return JSON.stringify(outputData, null, 2);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return '[Complex object - cannot serialize]';
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
862
1045
|
/**
|
|
863
|
-
* Processes file attachments and returns
|
|
1046
|
+
* Processes file attachments and returns text contents and binary parts separately.
|
|
864
1047
|
* @param attachments Array of file attachments to process
|
|
865
|
-
* @
|
|
1048
|
+
* @param documentManager Optional document manager for file operations
|
|
1049
|
+
* @returns Text contents and binary parts
|
|
866
1050
|
*/
|
|
867
|
-
|
|
868
|
-
attachments: IAttachment[]
|
|
869
|
-
|
|
870
|
-
|
|
1051
|
+
export async function processAttachments(
|
|
1052
|
+
attachments: IAttachment[],
|
|
1053
|
+
documentManager: IDocumentManager | null | undefined
|
|
1054
|
+
): Promise<{
|
|
1055
|
+
textContents: string[];
|
|
1056
|
+
binaryParts: Array<ImagePart | FilePart>;
|
|
1057
|
+
}> {
|
|
1058
|
+
const textContents: string[] = [];
|
|
1059
|
+
const binaryParts: Array<ImagePart | FilePart> = [];
|
|
1060
|
+
|
|
1061
|
+
if (!documentManager) {
|
|
1062
|
+
return { textContents, binaryParts };
|
|
1063
|
+
}
|
|
871
1064
|
|
|
872
1065
|
for (const attachment of attachments) {
|
|
873
1066
|
try {
|
|
874
1067
|
if (attachment.type === 'notebook' && attachment.cells?.length) {
|
|
875
|
-
const cellContents = await
|
|
1068
|
+
const cellContents = await readNotebookCells(
|
|
1069
|
+
attachment,
|
|
1070
|
+
documentManager
|
|
1071
|
+
);
|
|
876
1072
|
if (cellContents) {
|
|
877
|
-
|
|
1073
|
+
textContents.push(cellContents);
|
|
878
1074
|
}
|
|
879
1075
|
} else {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1076
|
+
let mimetype = attachment.mimetype;
|
|
1077
|
+
const fileExtension = PathExt.extname(attachment.value).toLowerCase();
|
|
1078
|
+
|
|
1079
|
+
// Fetch mimetype from server metadata if not provided
|
|
1080
|
+
if (!mimetype) {
|
|
1081
|
+
try {
|
|
1082
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1083
|
+
attachment.value,
|
|
1084
|
+
{ content: false }
|
|
1085
|
+
);
|
|
1086
|
+
mimetype = diskModel?.mimetype;
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
console.warn(
|
|
1089
|
+
`Failed to fetch metadata for ${attachment.value}:`,
|
|
1090
|
+
e
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (mimetype?.startsWith('image/')) {
|
|
1096
|
+
const data = await readBinaryAttachment(
|
|
1097
|
+
attachment,
|
|
1098
|
+
documentManager
|
|
1099
|
+
);
|
|
1100
|
+
if (data) {
|
|
1101
|
+
binaryParts.push({
|
|
1102
|
+
type: 'image',
|
|
1103
|
+
image: data,
|
|
1104
|
+
mediaType: mimetype
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
} else if (mimetype === 'application/pdf') {
|
|
1108
|
+
const data = await readBinaryAttachment(
|
|
1109
|
+
attachment,
|
|
1110
|
+
documentManager
|
|
888
1111
|
);
|
|
1112
|
+
if (data) {
|
|
1113
|
+
binaryParts.push({
|
|
1114
|
+
type: 'file',
|
|
1115
|
+
data,
|
|
1116
|
+
mediaType: mimetype,
|
|
1117
|
+
filename: PathExt.basename(attachment.value)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
const fileContent = await readFileAttachment(
|
|
1122
|
+
attachment,
|
|
1123
|
+
documentManager
|
|
1124
|
+
);
|
|
1125
|
+
if (fileContent) {
|
|
1126
|
+
const language =
|
|
1127
|
+
fileExtension === '.ipynb' ||
|
|
1128
|
+
mimetype === 'application/x-ipynb+json'
|
|
1129
|
+
? 'json'
|
|
1130
|
+
: '';
|
|
1131
|
+
textContents.push(
|
|
1132
|
+
`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
889
1135
|
}
|
|
890
1136
|
}
|
|
891
1137
|
} catch (error) {
|
|
892
1138
|
console.warn(`Failed to read attachment ${attachment.value}:`, error);
|
|
893
|
-
|
|
1139
|
+
textContents.push(
|
|
1140
|
+
`**File: ${attachment.value}** (Could not read file)`
|
|
1141
|
+
);
|
|
894
1142
|
}
|
|
895
1143
|
}
|
|
896
1144
|
|
|
897
|
-
return
|
|
1145
|
+
return { textContents, binaryParts };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Reads a binary attachment and returns its base64-encoded content.
|
|
1150
|
+
* @param attachment The attachment to read
|
|
1151
|
+
* @param documentManager Optional document manager for file operations
|
|
1152
|
+
* @returns Base64 string or null if unable to read
|
|
1153
|
+
*/
|
|
1154
|
+
export async function readBinaryAttachment(
|
|
1155
|
+
attachment: IAttachment,
|
|
1156
|
+
documentManager: IDocumentManager | null | undefined
|
|
1157
|
+
): Promise<string | null> {
|
|
1158
|
+
if (!documentManager) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
try {
|
|
1163
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1164
|
+
attachment.value,
|
|
1165
|
+
{ content: true }
|
|
1166
|
+
);
|
|
1167
|
+
if (diskModel?.content && diskModel.format === 'base64') {
|
|
1168
|
+
// Strip whitespace/newlines
|
|
1169
|
+
return (diskModel.content as string).replace(/\s/g, '');
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
console.warn(
|
|
1174
|
+
`Failed to read binary attachment ${attachment.value}:`,
|
|
1175
|
+
error
|
|
1176
|
+
);
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
898
1179
|
}
|
|
899
1180
|
|
|
900
1181
|
/**
|
|
901
1182
|
* Reads the content of a notebook cell.
|
|
902
1183
|
* @param attachment The notebook attachment to read
|
|
1184
|
+
* @param documentManager Optional document manager for file operations
|
|
903
1185
|
* @returns Cell content as string or null if unable to read
|
|
904
1186
|
*/
|
|
905
|
-
|
|
906
|
-
attachment: IAttachment
|
|
1187
|
+
export async function readNotebookCells(
|
|
1188
|
+
attachment: IAttachment,
|
|
1189
|
+
documentManager: IDocumentManager | null | undefined
|
|
907
1190
|
): Promise<string | null> {
|
|
908
|
-
if (
|
|
1191
|
+
if (
|
|
1192
|
+
attachment.type !== 'notebook' ||
|
|
1193
|
+
!attachment.cells ||
|
|
1194
|
+
!documentManager
|
|
1195
|
+
) {
|
|
909
1196
|
return null;
|
|
910
1197
|
}
|
|
911
1198
|
|
|
912
1199
|
try {
|
|
913
1200
|
// Try reading from live notebook if open
|
|
914
|
-
const widget =
|
|
915
|
-
|
|
916
|
-
|
|
1201
|
+
const widget = documentManager.findWidget(attachment.value) as
|
|
1202
|
+
| IDocumentWidget<Notebook, INotebookModel>
|
|
1203
|
+
| undefined;
|
|
917
1204
|
let cellData: nbformat.ICell[];
|
|
918
1205
|
let kernelLang = 'text';
|
|
919
1206
|
|
|
@@ -932,7 +1219,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
932
1219
|
kernelLang = String(lang);
|
|
933
1220
|
} else {
|
|
934
1221
|
// Fallback: reading from disk
|
|
935
|
-
const model = await
|
|
1222
|
+
const model = await documentManager.services.contents.get(
|
|
936
1223
|
attachment.value
|
|
937
1224
|
);
|
|
938
1225
|
if (!model || model.type !== 'notebook') {
|
|
@@ -1076,21 +1363,26 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1076
1363
|
/**
|
|
1077
1364
|
* Reads the content of a file attachment.
|
|
1078
1365
|
* @param attachment The file attachment to read
|
|
1366
|
+
* @param documentManager Optional document manager for file operations
|
|
1079
1367
|
* @returns File content as string or null if unable to read
|
|
1080
1368
|
*/
|
|
1081
|
-
|
|
1082
|
-
attachment: IAttachment
|
|
1369
|
+
export async function readFileAttachment(
|
|
1370
|
+
attachment: IAttachment,
|
|
1371
|
+
documentManager: IDocumentManager | null | undefined
|
|
1083
1372
|
): Promise<string | null> {
|
|
1084
1373
|
// Handle both 'file' and 'notebook' types since both have a 'value' path
|
|
1085
|
-
if (
|
|
1374
|
+
if (
|
|
1375
|
+
(attachment.type !== 'file' && attachment.type !== 'notebook') ||
|
|
1376
|
+
!documentManager
|
|
1377
|
+
) {
|
|
1086
1378
|
return null;
|
|
1087
1379
|
}
|
|
1088
1380
|
|
|
1089
1381
|
try {
|
|
1090
1382
|
// Try reading from an open widget first
|
|
1091
|
-
const widget =
|
|
1092
|
-
|
|
1093
|
-
|
|
1383
|
+
const widget = documentManager.findWidget(attachment.value) as
|
|
1384
|
+
| IDocumentWidget<Notebook, INotebookModel>
|
|
1385
|
+
| undefined;
|
|
1094
1386
|
|
|
1095
1387
|
if (widget && widget.context && widget.context.model) {
|
|
1096
1388
|
const model = widget.context.model;
|
|
@@ -1105,7 +1397,7 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1105
1397
|
}
|
|
1106
1398
|
|
|
1107
1399
|
// If not open, load from disk
|
|
1108
|
-
const diskModel = await
|
|
1400
|
+
const diskModel = await documentManager.services.contents.get(
|
|
1109
1401
|
attachment.value
|
|
1110
1402
|
);
|
|
1111
1403
|
|
|
@@ -1136,127 +1428,6 @@ export class AIChatModel extends AbstractChatModel {
|
|
|
1136
1428
|
return null;
|
|
1137
1429
|
}
|
|
1138
1430
|
}
|
|
1139
|
-
|
|
1140
|
-
// Private fields
|
|
1141
|
-
private _settingsModel: IAISettingsModel;
|
|
1142
|
-
private _user: IUser;
|
|
1143
|
-
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
|
|
1144
|
-
private _agentManager: IAgentManager;
|
|
1145
|
-
private _currentStreamingMessage: IMessage | null = null;
|
|
1146
|
-
private _nameChanged = new Signal<AIChatModel, string>(this);
|
|
1147
|
-
private _contentsManager?: Contents.IManager;
|
|
1148
|
-
private _autosave: boolean = false;
|
|
1149
|
-
private _autosaveChanged = new Signal<AIChatModel, boolean>(this);
|
|
1150
|
-
private _autosaveDebouncer: Debouncer;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
namespace Private {
|
|
1154
|
-
type IMimeBody = Partial<IRenderMime.IMimeModel> &
|
|
1155
|
-
Pick<IRenderMime.IMimeModel, 'data'>;
|
|
1156
|
-
type IDisplayOutput =
|
|
1157
|
-
| nbformat.IDisplayData
|
|
1158
|
-
| nbformat.IDisplayUpdate
|
|
1159
|
-
| nbformat.IExecuteResult;
|
|
1160
|
-
|
|
1161
|
-
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
1162
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
const isDisplayOutput = (value: unknown): value is IDisplayOutput => {
|
|
1166
|
-
if (!isPlainObject(value)) {
|
|
1167
|
-
return false;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const output = value as nbformat.IOutput;
|
|
1171
|
-
return (
|
|
1172
|
-
nbformat.isDisplayData(output) ||
|
|
1173
|
-
nbformat.isDisplayUpdate(output) ||
|
|
1174
|
-
nbformat.isExecuteResult(output)
|
|
1175
|
-
);
|
|
1176
|
-
};
|
|
1177
|
-
|
|
1178
|
-
const toMimeBundle = (
|
|
1179
|
-
value: IDisplayOutput,
|
|
1180
|
-
trustedMimeTypes: ReadonlySet<string>
|
|
1181
|
-
): IMimeBody | null => {
|
|
1182
|
-
const data = value.data;
|
|
1183
|
-
if (!isPlainObject(data) || Object.keys(data).length === 0) {
|
|
1184
|
-
return null;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
return {
|
|
1188
|
-
data: data as IRenderMime.IMimeModel['data'],
|
|
1189
|
-
...(isPlainObject(value.metadata)
|
|
1190
|
-
? { metadata: value.metadata as IRenderMime.IMimeModel['metadata'] }
|
|
1191
|
-
: {}),
|
|
1192
|
-
// MIME auto-rendering only runs for explicitly configured command IDs.
|
|
1193
|
-
// Trust handling is configurable to keep risky MIME execution opt-in.
|
|
1194
|
-
...(Object.keys(data).some(m => trustedMimeTypes.has(m))
|
|
1195
|
-
? { trusted: true }
|
|
1196
|
-
: {})
|
|
1197
|
-
};
|
|
1198
|
-
};
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Normalize arbitrary tool payloads into canonical display outputs.
|
|
1202
|
-
*
|
|
1203
|
-
* Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
|
|
1204
|
-
* often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
|
|
1205
|
-
*/
|
|
1206
|
-
const toDisplayOutputs = (value: unknown): IDisplayOutput[] => {
|
|
1207
|
-
if (isDisplayOutput(value)) {
|
|
1208
|
-
return [value];
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (Array.isArray(value)) {
|
|
1212
|
-
return value.filter(isDisplayOutput);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (!isPlainObject(value)) {
|
|
1216
|
-
return [];
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
if (Array.isArray(value.outputs)) {
|
|
1220
|
-
return value.outputs.filter(isDisplayOutput);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
if ('result' in value) {
|
|
1224
|
-
return toDisplayOutputs(value.result);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
return [];
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Extract rendermime-ready mime bundles from arbitrary tool results.
|
|
1232
|
-
*/
|
|
1233
|
-
export function extractMimeBundlesFromUnknown(
|
|
1234
|
-
content: unknown,
|
|
1235
|
-
options: { trustedMimeTypes?: ReadonlyArray<string> } = {}
|
|
1236
|
-
): IMimeBody[] {
|
|
1237
|
-
const bundles: IMimeBody[] = [];
|
|
1238
|
-
const outputs = toDisplayOutputs(content);
|
|
1239
|
-
const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
|
|
1240
|
-
for (const output of outputs) {
|
|
1241
|
-
const bundle = toMimeBundle(output, trustedMimeTypes);
|
|
1242
|
-
if (bundle) {
|
|
1243
|
-
bundles.push(bundle);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return bundles;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
export function formatToolOutput(outputData: unknown): string {
|
|
1250
|
-
if (typeof outputData === 'string') {
|
|
1251
|
-
return outputData;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
try {
|
|
1255
|
-
return JSON.stringify(outputData, null, 2);
|
|
1256
|
-
} catch {
|
|
1257
|
-
return '[Complex object - cannot serialize]';
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
1431
|
}
|
|
1261
1432
|
|
|
1262
1433
|
/**
|
|
@@ -1308,7 +1479,7 @@ export namespace AIChatModel {
|
|
|
1308
1479
|
/**
|
|
1309
1480
|
* The clear messages callback.
|
|
1310
1481
|
*/
|
|
1311
|
-
clearMessages: () => void
|
|
1482
|
+
clearMessages: () => Promise<void>;
|
|
1312
1483
|
/**
|
|
1313
1484
|
* Adds an assistant/system message to the chat.
|
|
1314
1485
|
*/
|
|
@@ -1347,6 +1518,10 @@ export namespace AIChatModel {
|
|
|
1347
1518
|
* Whether the chat is automatically saved.
|
|
1348
1519
|
*/
|
|
1349
1520
|
autosave?: boolean;
|
|
1521
|
+
/**
|
|
1522
|
+
* An optional title of the chat.
|
|
1523
|
+
*/
|
|
1524
|
+
title?: string;
|
|
1350
1525
|
};
|
|
1351
1526
|
};
|
|
1352
1527
|
}
|