@jupyterlite/ai 0.16.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 +7 -1
- package/lib/agent.js +59 -10
- 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 +254 -130
- package/lib/components/clear-button.d.ts +1 -1
- package/lib/components/clear-button.js +1 -1
- package/lib/index.js +200 -15
- package/lib/tokens.d.ts +13 -3
- package/lib/tokens.js +1 -0
- package/lib/widgets/main-area-chat.d.ts +1 -0
- package/lib/widgets/main-area-chat.js +7 -1
- package/package.json +1 -1
- package/src/agent.ts +72 -14
- package/src/chat-commands/clear.ts +1 -1
- package/src/chat-model-handler.ts +6 -1
- package/src/chat-model.ts +343 -171
- package/src/components/clear-button.tsx +3 -3
- package/src/index.ts +245 -17
- package/src/tokens.ts +13 -3
- package/src/widgets/main-area-chat.ts +9 -1
package/lib/index.js
CHANGED
|
@@ -213,6 +213,22 @@ const chatModelHandler = {
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* The active cell manager plugin, to allow copying code from chat to notebook.
|
|
218
|
+
*/
|
|
219
|
+
const activeCellManager = {
|
|
220
|
+
id: '@jupyterlite/ai:activeCellManager',
|
|
221
|
+
description: 'Add the active cell manager to the model handler',
|
|
222
|
+
autoStart: true,
|
|
223
|
+
requires: [IChatModelHandler, INotebookTracker],
|
|
224
|
+
activate: (app, modelHandler, notebookTracker) => {
|
|
225
|
+
const activeCellManager = new ActiveCellManager({
|
|
226
|
+
tracker: notebookTracker,
|
|
227
|
+
shell: app.shell
|
|
228
|
+
});
|
|
229
|
+
modelHandler.activeCellManager = activeCellManager;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
216
232
|
/**
|
|
217
233
|
* Initialization data for the extension.
|
|
218
234
|
*/
|
|
@@ -232,13 +248,12 @@ const plugin = {
|
|
|
232
248
|
IThemeManager,
|
|
233
249
|
ILayoutRestorer,
|
|
234
250
|
ILabShell,
|
|
235
|
-
INotebookTracker,
|
|
236
251
|
ITranslator,
|
|
237
252
|
IComponentsRendererFactory,
|
|
238
253
|
ICommandPalette,
|
|
239
254
|
IDocumentManager
|
|
240
255
|
],
|
|
241
|
-
activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell,
|
|
256
|
+
activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, translator, chatComponentsFactory, palette, documentManager) => {
|
|
242
257
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
243
258
|
// Create attachment opener registry to handle file attachments
|
|
244
259
|
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
|
|
@@ -253,16 +268,6 @@ const plugin = {
|
|
|
253
268
|
void app.commands.execute(CommandIds.openSettings);
|
|
254
269
|
}
|
|
255
270
|
};
|
|
256
|
-
// Create ActiveCellManager if notebook tracker is available, and add it to the
|
|
257
|
-
// model registry.
|
|
258
|
-
let activeCellManager;
|
|
259
|
-
if (notebookTracker) {
|
|
260
|
-
activeCellManager = new ActiveCellManager({
|
|
261
|
-
tracker: notebookTracker,
|
|
262
|
-
shell: app.shell
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
modelHandler.activeCellManager = activeCellManager;
|
|
266
271
|
// Creating the tracker for the chat widgets
|
|
267
272
|
const namespace = 'ai-chat';
|
|
268
273
|
const tracker = new WidgetTracker({ namespace });
|
|
@@ -346,6 +351,16 @@ const plugin = {
|
|
|
346
351
|
function saveTracker() {
|
|
347
352
|
tracker.save(widget);
|
|
348
353
|
}
|
|
354
|
+
function updateToolbarTitleOverlay() {
|
|
355
|
+
const titleNode = chatPanel.current?.toolbar.node
|
|
356
|
+
.getElementsByClassName('jp-chat-sidepanel-widget-title')
|
|
357
|
+
.item(0);
|
|
358
|
+
if (titleNode) {
|
|
359
|
+
titleNode.setAttribute('title', model.title ?? model.name);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
model.titleChanged.connect(updateToolbarTitleOverlay);
|
|
363
|
+
updateToolbarTitleOverlay();
|
|
349
364
|
// Update the tracker if the model name changed.
|
|
350
365
|
model.nameChanged.connect(saveTracker);
|
|
351
366
|
// Update the tracker if the active provider changed.
|
|
@@ -386,6 +401,7 @@ const plugin = {
|
|
|
386
401
|
chatPanel: widget
|
|
387
402
|
});
|
|
388
403
|
widget.disposed.connect(() => {
|
|
404
|
+
model.titleChanged.disconnect(updateToolbarTitleOverlay);
|
|
389
405
|
model.nameChanged.disconnect(saveTracker);
|
|
390
406
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
391
407
|
model.writersChanged?.disconnect(writersChanged);
|
|
@@ -514,6 +530,49 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
514
530
|
model.nameChanged.disconnect(saveTracker);
|
|
515
531
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
516
532
|
});
|
|
533
|
+
return widget;
|
|
534
|
+
};
|
|
535
|
+
const focusOnChat = (area, widget) => {
|
|
536
|
+
if (area === 'main' && widget) {
|
|
537
|
+
app.shell.activateById(widget.id);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
app.shell.activateById(chatPanel.id);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const applyInputArgs = (model, args) => {
|
|
544
|
+
const input = typeof args.input === 'string' ? args.input : undefined;
|
|
545
|
+
const autoSend = args.autoSend === true;
|
|
546
|
+
const shouldFocus = args.focus !== false;
|
|
547
|
+
if (input !== undefined) {
|
|
548
|
+
model.input.value = input;
|
|
549
|
+
}
|
|
550
|
+
if (autoSend && input !== undefined) {
|
|
551
|
+
model.input.send(model.input.value);
|
|
552
|
+
}
|
|
553
|
+
if (shouldFocus) {
|
|
554
|
+
model.input.focus();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const findChatWidget = (name, provider) => {
|
|
558
|
+
if (!name && !provider) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
return tracker.find(widget => {
|
|
562
|
+
const model = widget.model;
|
|
563
|
+
return ((!name || widget.model.name === name) &&
|
|
564
|
+
(!provider || model.agentManager.activeProvider === provider));
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
const disposeSideChatModel = (model) => {
|
|
568
|
+
const loadedName = chatPanel
|
|
569
|
+
.getLoadedModelNames()
|
|
570
|
+
.find(name => chatPanel.getLoadedModel(name) === model);
|
|
571
|
+
if (!loadedName) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
chatPanel.disposeLoadedModel(loadedName);
|
|
575
|
+
return true;
|
|
517
576
|
};
|
|
518
577
|
commands.addCommand(CommandIds.openChat, {
|
|
519
578
|
label: trans.__('Open a chat'),
|
|
@@ -543,12 +602,18 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
543
602
|
if (!model) {
|
|
544
603
|
return false;
|
|
545
604
|
}
|
|
605
|
+
const shouldFocus = args.focus === true;
|
|
606
|
+
let widget;
|
|
546
607
|
if (area === 'main') {
|
|
547
|
-
openInMain(model);
|
|
608
|
+
widget = openInMain(model);
|
|
548
609
|
}
|
|
549
610
|
else {
|
|
550
|
-
chatPanel.open({ model });
|
|
611
|
+
widget = chatPanel.open({ model });
|
|
612
|
+
}
|
|
613
|
+
if (shouldFocus) {
|
|
614
|
+
focusOnChat(area, widget);
|
|
551
615
|
}
|
|
616
|
+
applyInputArgs(model, { ...args, focus: shouldFocus });
|
|
552
617
|
return true;
|
|
553
618
|
},
|
|
554
619
|
describedBy: {
|
|
@@ -567,6 +632,116 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
567
632
|
provider: {
|
|
568
633
|
type: 'string',
|
|
569
634
|
description: trans.__('The provider/model to use with this chat')
|
|
635
|
+
},
|
|
636
|
+
input: {
|
|
637
|
+
type: 'string',
|
|
638
|
+
description: trans.__('The input text to prefill in the chat')
|
|
639
|
+
},
|
|
640
|
+
focus: {
|
|
641
|
+
type: 'boolean',
|
|
642
|
+
description: trans.__('Whether to focus the chat input after opening it')
|
|
643
|
+
},
|
|
644
|
+
autoSend: {
|
|
645
|
+
type: 'boolean',
|
|
646
|
+
description: trans.__('Whether to auto-send the provided input after opening the chat')
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
commands.addCommand(CommandIds.openOrRevealChat, {
|
|
653
|
+
label: trans.__('Open or reveal the chat panel'),
|
|
654
|
+
execute: async (args) => {
|
|
655
|
+
const area = args.area === 'main' ? 'main' : 'side';
|
|
656
|
+
const provider = args.provider ?? undefined;
|
|
657
|
+
const name = args.name ?? undefined;
|
|
658
|
+
const shouldFocus = args.focus === true;
|
|
659
|
+
let existingWidget = findChatWidget(name, provider);
|
|
660
|
+
if (!existingWidget && !name) {
|
|
661
|
+
const providerConfig = provider
|
|
662
|
+
? settingsModel.getProvider(provider)
|
|
663
|
+
: settingsModel.getDefaultProvider();
|
|
664
|
+
existingWidget = findChatWidget(undefined, providerConfig?.id);
|
|
665
|
+
}
|
|
666
|
+
// If the side chat model is loaded but not currently displayed, reveal it first.
|
|
667
|
+
if (!existingWidget && name) {
|
|
668
|
+
const loadedModel = chatPanel.getLoadedModel(name);
|
|
669
|
+
if (loadedModel) {
|
|
670
|
+
existingWidget = chatPanel.open({ model: loadedModel });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!existingWidget) {
|
|
674
|
+
return commands.execute(CommandIds.openChat, {
|
|
675
|
+
...args,
|
|
676
|
+
focus: shouldFocus
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const currentArea = existingWidget instanceof MainAreaChat ? 'main' : 'side';
|
|
680
|
+
if (currentArea !== area) {
|
|
681
|
+
const targetName = existingWidget.model.name;
|
|
682
|
+
const moved = (await commands.execute(CommandIds.moveChat, {
|
|
683
|
+
name: targetName,
|
|
684
|
+
area
|
|
685
|
+
}));
|
|
686
|
+
if (!moved) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const movedWidget = findChatWidget(targetName);
|
|
690
|
+
if (!movedWidget) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (area === 'side') {
|
|
694
|
+
chatPanel.open({ model: movedWidget.model });
|
|
695
|
+
}
|
|
696
|
+
if (shouldFocus) {
|
|
697
|
+
focusOnChat(area, movedWidget);
|
|
698
|
+
}
|
|
699
|
+
applyInputArgs(movedWidget.model, {
|
|
700
|
+
...args,
|
|
701
|
+
focus: shouldFocus
|
|
702
|
+
});
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
if (area === 'side') {
|
|
706
|
+
chatPanel.open({ model: existingWidget.model });
|
|
707
|
+
}
|
|
708
|
+
if (shouldFocus) {
|
|
709
|
+
focusOnChat(area, existingWidget);
|
|
710
|
+
}
|
|
711
|
+
applyInputArgs(existingWidget.model, {
|
|
712
|
+
...args,
|
|
713
|
+
focus: shouldFocus
|
|
714
|
+
});
|
|
715
|
+
return true;
|
|
716
|
+
},
|
|
717
|
+
describedBy: {
|
|
718
|
+
args: {
|
|
719
|
+
type: 'object',
|
|
720
|
+
properties: {
|
|
721
|
+
area: {
|
|
722
|
+
type: 'string',
|
|
723
|
+
enum: ['main', 'side'],
|
|
724
|
+
description: trans.__('The name of the area to open or reveal the chat in')
|
|
725
|
+
},
|
|
726
|
+
name: {
|
|
727
|
+
type: 'string',
|
|
728
|
+
description: trans.__('The name of the chat')
|
|
729
|
+
},
|
|
730
|
+
provider: {
|
|
731
|
+
type: 'string',
|
|
732
|
+
description: trans.__('The provider/model to use with this chat')
|
|
733
|
+
},
|
|
734
|
+
input: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description: trans.__('The input text to prefill in the chat')
|
|
737
|
+
},
|
|
738
|
+
focus: {
|
|
739
|
+
type: 'boolean',
|
|
740
|
+
description: trans.__('Whether to focus the chat input after opening it')
|
|
741
|
+
},
|
|
742
|
+
autoSend: {
|
|
743
|
+
type: 'boolean',
|
|
744
|
+
description: trans.__('Whether to auto-send the provided input after opening the chat')
|
|
570
745
|
}
|
|
571
746
|
}
|
|
572
747
|
}
|
|
@@ -615,7 +790,8 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
615
790
|
activeProvider: previousModel.agentManager.activeProvider,
|
|
616
791
|
tokenUsage: previousModel.agentManager.tokenUsage,
|
|
617
792
|
messages: previousModel.messages,
|
|
618
|
-
autosave: previousModel.autosave
|
|
793
|
+
autosave: previousModel.autosave,
|
|
794
|
+
title: previousModel.title
|
|
619
795
|
});
|
|
620
796
|
// Wait (with timeout) for the tracker to have updated the previous widget.
|
|
621
797
|
const status = await Promise.any([
|
|
@@ -630,6 +806,14 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
630
806
|
}
|
|
631
807
|
if (area === 'main') {
|
|
632
808
|
openInMain(model);
|
|
809
|
+
if (previousWidget instanceof ChatWidget) {
|
|
810
|
+
// Clean up the side-panel model entry before disposing the previous
|
|
811
|
+
// widget/model state.
|
|
812
|
+
if (!disposeSideChatModel(previousModel)) {
|
|
813
|
+
previousWidget.dispose();
|
|
814
|
+
previousModel.dispose();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
633
817
|
}
|
|
634
818
|
else {
|
|
635
819
|
previousWidget?.dispose();
|
|
@@ -1118,6 +1302,7 @@ export default [
|
|
|
1118
1302
|
skillRegistryPlugin,
|
|
1119
1303
|
skillsCommandPlugin,
|
|
1120
1304
|
chatModelHandler,
|
|
1305
|
+
activeCellManager,
|
|
1121
1306
|
plugin,
|
|
1122
1307
|
toolRegistry,
|
|
1123
1308
|
agentManagerFactory,
|
package/lib/tokens.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { Token } from '@lumino/coreutils';
|
|
5
5
|
import type { IDisposable } from '@lumino/disposable';
|
|
6
6
|
import { ISignal } from '@lumino/signaling';
|
|
7
|
-
import type { Tool, LanguageModel } from 'ai';
|
|
7
|
+
import type { Tool, LanguageModel, UserContent, ModelMessage } from 'ai';
|
|
8
8
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
9
9
|
import type { IModelOptions } from './providers/models';
|
|
10
10
|
import { AIChatModel } from './chat-model';
|
|
@@ -17,6 +17,7 @@ export declare namespace CommandIds {
|
|
|
17
17
|
const openSettings = "@jupyterlite/ai:open-settings";
|
|
18
18
|
const reposition = "@jupyterlite/ai:reposition";
|
|
19
19
|
const openChat = "@jupyterlite/ai:open-chat";
|
|
20
|
+
const openOrRevealChat = "@jupyterlite/ai:open-or-reveal-chat";
|
|
20
21
|
const moveChat = "@jupyterlite/ai:move-chat";
|
|
21
22
|
const refreshSkills = "@jupyterlite/ai:refresh-skills";
|
|
22
23
|
const saveChat = "@jupyterlite/ai:save-chat";
|
|
@@ -465,7 +466,7 @@ export interface IAgentManager {
|
|
|
465
466
|
/**
|
|
466
467
|
* Clears conversation history and resets agent state.
|
|
467
468
|
*/
|
|
468
|
-
clearHistory(): void
|
|
469
|
+
clearHistory(): Promise<void>;
|
|
469
470
|
/**
|
|
470
471
|
* Sets the conversation history with a list of messages from the chat.
|
|
471
472
|
* @param messages The chat messages to set as history
|
|
@@ -492,7 +493,12 @@ export interface IAgentManager {
|
|
|
492
493
|
* Handles the complete execution cycle including tool calls.
|
|
493
494
|
* @param message The user message to respond to (may include processed attachment content)
|
|
494
495
|
*/
|
|
495
|
-
generateResponse(message:
|
|
496
|
+
generateResponse(message: UserContent): Promise<void>;
|
|
497
|
+
/**
|
|
498
|
+
* Create a transient language model to request a text response, which won't be added to history.
|
|
499
|
+
* @param messages - the messages sequence to send to the model.
|
|
500
|
+
*/
|
|
501
|
+
textResponse(messages: ModelMessage[]): Promise<string>;
|
|
496
502
|
/**
|
|
497
503
|
* Initializes the AI agent with current settings and tools.
|
|
498
504
|
* Sets up the agent with model configuration, tools, and MCP tools.
|
|
@@ -561,6 +567,10 @@ export interface ICreateChatOptions {
|
|
|
561
567
|
* Whether the chat is autosaved or not.
|
|
562
568
|
*/
|
|
563
569
|
autosave?: boolean;
|
|
570
|
+
/**
|
|
571
|
+
* An optional title to the chat.
|
|
572
|
+
*/
|
|
573
|
+
title?: string | null;
|
|
564
574
|
}
|
|
565
575
|
/**
|
|
566
576
|
* Token for the chat model handler.
|
package/lib/tokens.js
CHANGED
|
@@ -7,6 +7,7 @@ export var CommandIds;
|
|
|
7
7
|
CommandIds.openSettings = '@jupyterlite/ai:open-settings';
|
|
8
8
|
CommandIds.reposition = '@jupyterlite/ai:reposition';
|
|
9
9
|
CommandIds.openChat = '@jupyterlite/ai:open-chat';
|
|
10
|
+
CommandIds.openOrRevealChat = '@jupyterlite/ai:open-or-reveal-chat';
|
|
10
11
|
CommandIds.moveChat = '@jupyterlite/ai:move-chat';
|
|
11
12
|
CommandIds.refreshSkills = '@jupyterlite/ai:refresh-skills';
|
|
12
13
|
CommandIds.saveChat = '@jupyterlite/ai:save-chat';
|
|
@@ -10,7 +10,8 @@ import { CommandIds } from '../tokens';
|
|
|
10
10
|
export class MainAreaChat extends MainAreaWidget {
|
|
11
11
|
constructor(options) {
|
|
12
12
|
super(options);
|
|
13
|
-
this.title.label = this.
|
|
13
|
+
this.title.label = this.model.name;
|
|
14
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
14
15
|
const { trans } = options;
|
|
15
16
|
// Move to side button.
|
|
16
17
|
this.toolbar.addItem('moveToSide', new CommandToolbarButton({
|
|
@@ -43,12 +44,14 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
43
44
|
chatPanel: this.content
|
|
44
45
|
});
|
|
45
46
|
this.model.writersChanged.connect(this._writersChanged);
|
|
47
|
+
this.model.titleChanged.connect(this._titleChanged);
|
|
46
48
|
}
|
|
47
49
|
dispose() {
|
|
48
50
|
super.dispose();
|
|
49
51
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
50
52
|
this._outputAreaCompat.dispose();
|
|
51
53
|
this.model.writersChanged.disconnect(this._writersChanged);
|
|
54
|
+
this.model.titleChanged.disconnect(this._titleChanged);
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Get the model of the chat.
|
|
@@ -74,5 +77,8 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
74
77
|
this.content.inputToolbarRegistry?.show('send');
|
|
75
78
|
}
|
|
76
79
|
};
|
|
80
|
+
_titleChanged = () => {
|
|
81
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
82
|
+
};
|
|
77
83
|
_outputAreaCompat;
|
|
78
84
|
}
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
5
5
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
6
|
import {
|
|
7
|
+
generateText,
|
|
7
8
|
ToolLoopAgent,
|
|
8
9
|
type ModelMessage,
|
|
9
10
|
type LanguageModel,
|
|
@@ -13,7 +14,9 @@ import {
|
|
|
13
14
|
type TypedToolError,
|
|
14
15
|
type TypedToolOutputDenied,
|
|
15
16
|
type TypedToolResult,
|
|
16
|
-
type
|
|
17
|
+
type UserContent,
|
|
18
|
+
type AssistantModelMessage,
|
|
19
|
+
APICallError
|
|
17
20
|
} from 'ai';
|
|
18
21
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
19
22
|
|
|
@@ -504,12 +507,13 @@ export class AgentManager implements IAgentManager {
|
|
|
504
507
|
this._pendingApprovals.clear();
|
|
505
508
|
|
|
506
509
|
// Convert chat messages to model messages
|
|
507
|
-
const modelMessages = messages.map(msg => {
|
|
508
|
-
const
|
|
510
|
+
const modelMessages: ModelMessage[] = messages.map(msg => {
|
|
511
|
+
const role =
|
|
512
|
+
msg.sender.username === 'ai-assistant' ? 'assistant' : 'user';
|
|
509
513
|
return {
|
|
510
|
-
role
|
|
514
|
+
role,
|
|
511
515
|
content: msg.body
|
|
512
|
-
}
|
|
516
|
+
};
|
|
513
517
|
});
|
|
514
518
|
this._history = Private.sanitizeModelMessages(modelMessages);
|
|
515
519
|
}
|
|
@@ -571,10 +575,17 @@ export class AgentManager implements IAgentManager {
|
|
|
571
575
|
* Handles the complete execution cycle including tool calls.
|
|
572
576
|
* @param message The user message to respond to (may include processed attachment content)
|
|
573
577
|
*/
|
|
574
|
-
async generateResponse(message:
|
|
578
|
+
async generateResponse(message: UserContent): Promise<void> {
|
|
575
579
|
this._streaming = new PromiseDelegate();
|
|
576
580
|
this._controller = new AbortController();
|
|
577
581
|
const responseHistory: ModelMessage[] = [];
|
|
582
|
+
|
|
583
|
+
// Add user message to history
|
|
584
|
+
responseHistory.push({
|
|
585
|
+
role: 'user',
|
|
586
|
+
content: message
|
|
587
|
+
});
|
|
588
|
+
|
|
578
589
|
try {
|
|
579
590
|
// Ensure we have an agent
|
|
580
591
|
if (!this._agent) {
|
|
@@ -585,12 +596,6 @@ export class AgentManager implements IAgentManager {
|
|
|
585
596
|
throw new Error('Failed to initialize agent');
|
|
586
597
|
}
|
|
587
598
|
|
|
588
|
-
// Add user message to history
|
|
589
|
-
responseHistory.push({
|
|
590
|
-
role: 'user',
|
|
591
|
-
content: message
|
|
592
|
-
});
|
|
593
|
-
|
|
594
599
|
let continueLoop = true;
|
|
595
600
|
while (continueLoop) {
|
|
596
601
|
const result = await this._agent.stream({
|
|
@@ -647,9 +652,41 @@ export class AgentManager implements IAgentManager {
|
|
|
647
652
|
this._history.push(...Private.sanitizeModelMessages(responseHistory));
|
|
648
653
|
} catch (error) {
|
|
649
654
|
if ((error as Error).name !== 'AbortError') {
|
|
655
|
+
let helpMessage = `${(error as Error).message}`;
|
|
656
|
+
|
|
657
|
+
// Remove attachments from history on payload rejection errors
|
|
658
|
+
if (
|
|
659
|
+
APICallError.isInstance(error) &&
|
|
660
|
+
(error.statusCode === 400 ||
|
|
661
|
+
error.statusCode === 404 ||
|
|
662
|
+
error.statusCode === 413 ||
|
|
663
|
+
error.statusCode === 415 ||
|
|
664
|
+
error.statusCode === 422)
|
|
665
|
+
) {
|
|
666
|
+
for (const msg of [...this._history, ...responseHistory]) {
|
|
667
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
668
|
+
const hasMedia = msg.content.some(p => p.type !== 'text');
|
|
669
|
+
if (hasMedia) {
|
|
670
|
+
const textContent = msg.content
|
|
671
|
+
.filter(p => p.type === 'text')
|
|
672
|
+
.map(p => (p as { text: string }).text)
|
|
673
|
+
.join('\n');
|
|
674
|
+
msg.content =
|
|
675
|
+
textContent || '_Attachment removed due to error_';
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
helpMessage +=
|
|
680
|
+
'\n\nAttachments have been removed from history. Please send your prompt again.';
|
|
681
|
+
}
|
|
650
682
|
this._agentEvent.emit({
|
|
651
683
|
type: 'error',
|
|
652
|
-
data: { error:
|
|
684
|
+
data: { error: new Error(helpMessage) }
|
|
685
|
+
});
|
|
686
|
+
this._history.push(...Private.sanitizeModelMessages(responseHistory));
|
|
687
|
+
this._history.push({
|
|
688
|
+
role: 'assistant',
|
|
689
|
+
content: helpMessage
|
|
653
690
|
});
|
|
654
691
|
}
|
|
655
692
|
} finally {
|
|
@@ -658,6 +695,24 @@ export class AgentManager implements IAgentManager {
|
|
|
658
695
|
}
|
|
659
696
|
}
|
|
660
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Create a transient language model to request a text response which won't be added to history.
|
|
700
|
+
* @param messages - the messages sequence to send to the model.
|
|
701
|
+
*/
|
|
702
|
+
async textResponse(messages: ModelMessage[]): Promise<string> {
|
|
703
|
+
try {
|
|
704
|
+
const model = await this._createModel();
|
|
705
|
+
const result = await generateText({
|
|
706
|
+
model,
|
|
707
|
+
messages
|
|
708
|
+
});
|
|
709
|
+
this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
|
|
710
|
+
return result.text;
|
|
711
|
+
} catch (e) {
|
|
712
|
+
throw `Error while getting the topic of the chat\n${e}`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
661
716
|
/**
|
|
662
717
|
* Updates cumulative token usage statistics from a completed model step.
|
|
663
718
|
*/
|
|
@@ -932,6 +987,9 @@ ${richOutputWorkflowInstruction}`;
|
|
|
932
987
|
await this._handleApprovalRequest(part, processResult);
|
|
933
988
|
break;
|
|
934
989
|
|
|
990
|
+
case 'error':
|
|
991
|
+
throw part.error;
|
|
992
|
+
|
|
935
993
|
case 'finish-step':
|
|
936
994
|
this._updateTokenUsage(part.usage, part.usage.inputTokens);
|
|
937
995
|
break;
|
|
@@ -940,7 +998,7 @@ ${richOutputWorkflowInstruction}`;
|
|
|
940
998
|
processResult.aborted = true;
|
|
941
999
|
break;
|
|
942
1000
|
|
|
943
|
-
// Ignore: text-start, text-end, finish,
|
|
1001
|
+
// Ignore: text-start, text-end, finish, and others
|
|
944
1002
|
default:
|
|
945
1003
|
break;
|
|
946
1004
|
}
|
|
@@ -29,7 +29,7 @@ export class ClearCommandProvider implements IChatCommandProvider {
|
|
|
29
29
|
const context = inputModel.chatContext as
|
|
30
30
|
| AIChatModel.IAIChatContext
|
|
31
31
|
| undefined;
|
|
32
|
-
context?.clearMessages?.();
|
|
32
|
+
await context?.clearMessages?.();
|
|
33
33
|
|
|
34
34
|
inputModel.value = '';
|
|
35
35
|
inputModel.clearAttachments();
|
|
@@ -29,7 +29,8 @@ export class ChatModelHandler implements IChatModelHandler {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
createModel(options: ICreateChatOptions): AIChatModel {
|
|
32
|
-
const { name, activeProvider, tokenUsage, messages, autosave } =
|
|
32
|
+
const { name, activeProvider, tokenUsage, messages, autosave, title } =
|
|
33
|
+
options;
|
|
33
34
|
|
|
34
35
|
// Create Agent Manager first so it can be shared
|
|
35
36
|
const agentManager = this._agentManagerFactory.createAgent({
|
|
@@ -58,6 +59,10 @@ export class ChatModelHandler implements IChatModelHandler {
|
|
|
58
59
|
|
|
59
60
|
model.name = name;
|
|
60
61
|
|
|
62
|
+
if (title) {
|
|
63
|
+
model.title = title;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
return model;
|
|
62
67
|
}
|
|
63
68
|
|