@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/src/index.ts
CHANGED
|
@@ -357,6 +357,27 @@ const chatModelHandler: JupyterFrontEndPlugin<IChatModelHandler> = {
|
|
|
357
357
|
}
|
|
358
358
|
};
|
|
359
359
|
|
|
360
|
+
/**
|
|
361
|
+
* The active cell manager plugin, to allow copying code from chat to notebook.
|
|
362
|
+
*/
|
|
363
|
+
const activeCellManager: JupyterFrontEndPlugin<void> = {
|
|
364
|
+
id: '@jupyterlite/ai:activeCellManager',
|
|
365
|
+
description: 'Add the active cell manager to the model handler',
|
|
366
|
+
autoStart: true,
|
|
367
|
+
requires: [IChatModelHandler, INotebookTracker],
|
|
368
|
+
activate: (
|
|
369
|
+
app: JupyterFrontEnd,
|
|
370
|
+
modelHandler: IChatModelHandler,
|
|
371
|
+
notebookTracker: INotebookTracker
|
|
372
|
+
) => {
|
|
373
|
+
const activeCellManager = new ActiveCellManager({
|
|
374
|
+
tracker: notebookTracker,
|
|
375
|
+
shell: app.shell
|
|
376
|
+
});
|
|
377
|
+
modelHandler.activeCellManager = activeCellManager;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
360
381
|
/**
|
|
361
382
|
* Initialization data for the extension.
|
|
362
383
|
*/
|
|
@@ -376,7 +397,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
|
376
397
|
IThemeManager,
|
|
377
398
|
ILayoutRestorer,
|
|
378
399
|
ILabShell,
|
|
379
|
-
INotebookTracker,
|
|
380
400
|
ITranslator,
|
|
381
401
|
IComponentsRendererFactory,
|
|
382
402
|
ICommandPalette,
|
|
@@ -392,7 +412,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
|
392
412
|
themeManager?: IThemeManager,
|
|
393
413
|
restorer?: ILayoutRestorer,
|
|
394
414
|
labShell?: ILabShell,
|
|
395
|
-
notebookTracker?: INotebookTracker,
|
|
396
415
|
translator?: ITranslator,
|
|
397
416
|
chatComponentsFactory?: IComponentsRendererFactory,
|
|
398
417
|
palette?: ICommandPalette,
|
|
@@ -416,17 +435,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
|
416
435
|
}
|
|
417
436
|
};
|
|
418
437
|
|
|
419
|
-
// Create ActiveCellManager if notebook tracker is available, and add it to the
|
|
420
|
-
// model registry.
|
|
421
|
-
let activeCellManager: ActiveCellManager | undefined;
|
|
422
|
-
if (notebookTracker) {
|
|
423
|
-
activeCellManager = new ActiveCellManager({
|
|
424
|
-
tracker: notebookTracker,
|
|
425
|
-
shell: app.shell
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
modelHandler.activeCellManager = activeCellManager;
|
|
429
|
-
|
|
430
438
|
// Creating the tracker for the chat widgets
|
|
431
439
|
const namespace = 'ai-chat';
|
|
432
440
|
const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
|
|
@@ -527,6 +535,18 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
|
527
535
|
tracker.save(widget);
|
|
528
536
|
}
|
|
529
537
|
|
|
538
|
+
function updateToolbarTitleOverlay() {
|
|
539
|
+
const titleNode = chatPanel.current?.toolbar.node
|
|
540
|
+
.getElementsByClassName('jp-chat-sidepanel-widget-title')
|
|
541
|
+
.item(0);
|
|
542
|
+
if (titleNode) {
|
|
543
|
+
titleNode.setAttribute('title', model.title ?? model.name);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
model.titleChanged.connect(updateToolbarTitleOverlay);
|
|
548
|
+
updateToolbarTitleOverlay();
|
|
549
|
+
|
|
530
550
|
// Update the tracker if the model name changed.
|
|
531
551
|
model.nameChanged.connect(saveTracker);
|
|
532
552
|
|
|
@@ -582,6 +602,7 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
|
|
|
582
602
|
});
|
|
583
603
|
|
|
584
604
|
widget.disposed.connect(() => {
|
|
605
|
+
model.titleChanged.disconnect(updateToolbarTitleOverlay);
|
|
585
606
|
model.nameChanged.disconnect(saveTracker);
|
|
586
607
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
587
608
|
model.writersChanged?.disconnect(writersChanged);
|
|
@@ -734,7 +755,7 @@ function registerCommands(
|
|
|
734
755
|
}
|
|
735
756
|
});
|
|
736
757
|
|
|
737
|
-
const openInMain = (model: AIChatModel) => {
|
|
758
|
+
const openInMain = (model: AIChatModel): MainAreaChat => {
|
|
738
759
|
const content = new ChatWidget({
|
|
739
760
|
model,
|
|
740
761
|
rmRegistry,
|
|
@@ -767,6 +788,64 @@ function registerCommands(
|
|
|
767
788
|
model.nameChanged.disconnect(saveTracker);
|
|
768
789
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
769
790
|
});
|
|
791
|
+
|
|
792
|
+
return widget;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const focusOnChat = (
|
|
796
|
+
area: 'main' | 'side',
|
|
797
|
+
widget?: ChatWidget | MainAreaChat
|
|
798
|
+
) => {
|
|
799
|
+
if (area === 'main' && widget) {
|
|
800
|
+
app.shell.activateById(widget.id);
|
|
801
|
+
} else {
|
|
802
|
+
app.shell.activateById(chatPanel.id);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const applyInputArgs = (model: IChatModel, args: any) => {
|
|
807
|
+
const input = typeof args.input === 'string' ? args.input : undefined;
|
|
808
|
+
const autoSend = args.autoSend === true;
|
|
809
|
+
const shouldFocus = args.focus !== false;
|
|
810
|
+
|
|
811
|
+
if (input !== undefined) {
|
|
812
|
+
model.input.value = input;
|
|
813
|
+
}
|
|
814
|
+
if (autoSend && input !== undefined) {
|
|
815
|
+
model.input.send(model.input.value);
|
|
816
|
+
}
|
|
817
|
+
if (shouldFocus) {
|
|
818
|
+
model.input.focus();
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const findChatWidget = (
|
|
823
|
+
name?: string,
|
|
824
|
+
provider?: string
|
|
825
|
+
): ChatWidget | MainAreaChat | undefined => {
|
|
826
|
+
if (!name && !provider) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
return tracker.find(widget => {
|
|
830
|
+
const model = widget.model as AIChatModel;
|
|
831
|
+
return (
|
|
832
|
+
(!name || widget.model.name === name) &&
|
|
833
|
+
(!provider || model.agentManager.activeProvider === provider)
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const disposeSideChatModel = (model: IChatModel): boolean => {
|
|
839
|
+
const loadedName = chatPanel
|
|
840
|
+
.getLoadedModelNames()
|
|
841
|
+
.find(name => chatPanel.getLoadedModel(name) === model);
|
|
842
|
+
|
|
843
|
+
if (!loadedName) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
chatPanel.disposeLoadedModel(loadedName);
|
|
848
|
+
return true;
|
|
770
849
|
};
|
|
771
850
|
|
|
772
851
|
commands.addCommand(CommandIds.openChat, {
|
|
@@ -801,11 +880,18 @@ function registerCommands(
|
|
|
801
880
|
return false;
|
|
802
881
|
}
|
|
803
882
|
|
|
883
|
+
const shouldFocus = args.focus === true;
|
|
884
|
+
let widget: ChatWidget | MainAreaChat | undefined;
|
|
804
885
|
if (area === 'main') {
|
|
805
|
-
openInMain(model);
|
|
886
|
+
widget = openInMain(model);
|
|
806
887
|
} else {
|
|
807
|
-
chatPanel.open({ model });
|
|
888
|
+
widget = chatPanel.open({ model });
|
|
889
|
+
}
|
|
890
|
+
if (shouldFocus) {
|
|
891
|
+
focusOnChat(area, widget);
|
|
808
892
|
}
|
|
893
|
+
applyInputArgs(model, { ...args, focus: shouldFocus });
|
|
894
|
+
|
|
809
895
|
return true;
|
|
810
896
|
},
|
|
811
897
|
describedBy: {
|
|
@@ -824,6 +910,137 @@ function registerCommands(
|
|
|
824
910
|
provider: {
|
|
825
911
|
type: 'string',
|
|
826
912
|
description: trans.__('The provider/model to use with this chat')
|
|
913
|
+
},
|
|
914
|
+
input: {
|
|
915
|
+
type: 'string',
|
|
916
|
+
description: trans.__('The input text to prefill in the chat')
|
|
917
|
+
},
|
|
918
|
+
focus: {
|
|
919
|
+
type: 'boolean',
|
|
920
|
+
description: trans.__(
|
|
921
|
+
'Whether to focus the chat input after opening it'
|
|
922
|
+
)
|
|
923
|
+
},
|
|
924
|
+
autoSend: {
|
|
925
|
+
type: 'boolean',
|
|
926
|
+
description: trans.__(
|
|
927
|
+
'Whether to auto-send the provided input after opening the chat'
|
|
928
|
+
)
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
commands.addCommand(CommandIds.openOrRevealChat, {
|
|
936
|
+
label: trans.__('Open or reveal the chat panel'),
|
|
937
|
+
execute: async (args): Promise<boolean> => {
|
|
938
|
+
const area = (args.area as string) === 'main' ? 'main' : 'side';
|
|
939
|
+
const provider = (args.provider as string) ?? undefined;
|
|
940
|
+
const name = (args.name as string) ?? undefined;
|
|
941
|
+
const shouldFocus = args.focus === true;
|
|
942
|
+
|
|
943
|
+
let existingWidget = findChatWidget(name, provider);
|
|
944
|
+
if (!existingWidget && !name) {
|
|
945
|
+
const providerConfig = provider
|
|
946
|
+
? settingsModel.getProvider(provider)
|
|
947
|
+
: settingsModel.getDefaultProvider();
|
|
948
|
+
existingWidget = findChatWidget(undefined, providerConfig?.id);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// If the side chat model is loaded but not currently displayed, reveal it first.
|
|
952
|
+
if (!existingWidget && name) {
|
|
953
|
+
const loadedModel = chatPanel.getLoadedModel(name);
|
|
954
|
+
if (loadedModel) {
|
|
955
|
+
existingWidget = chatPanel.open({ model: loadedModel });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!existingWidget) {
|
|
960
|
+
return commands.execute(CommandIds.openChat, {
|
|
961
|
+
...args,
|
|
962
|
+
focus: shouldFocus
|
|
963
|
+
}) as Promise<boolean>;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const currentArea =
|
|
967
|
+
existingWidget instanceof MainAreaChat ? 'main' : 'side';
|
|
968
|
+
if (currentArea !== area) {
|
|
969
|
+
const targetName = existingWidget.model.name;
|
|
970
|
+
const moved = (await commands.execute(CommandIds.moveChat, {
|
|
971
|
+
name: targetName,
|
|
972
|
+
area
|
|
973
|
+
})) as boolean;
|
|
974
|
+
if (!moved) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const movedWidget = findChatWidget(targetName);
|
|
979
|
+
if (!movedWidget) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (area === 'side') {
|
|
984
|
+
chatPanel.open({ model: movedWidget.model });
|
|
985
|
+
}
|
|
986
|
+
if (shouldFocus) {
|
|
987
|
+
focusOnChat(area, movedWidget);
|
|
988
|
+
}
|
|
989
|
+
applyInputArgs(movedWidget.model, {
|
|
990
|
+
...args,
|
|
991
|
+
focus: shouldFocus
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (area === 'side') {
|
|
998
|
+
chatPanel.open({ model: existingWidget.model });
|
|
999
|
+
}
|
|
1000
|
+
if (shouldFocus) {
|
|
1001
|
+
focusOnChat(area, existingWidget);
|
|
1002
|
+
}
|
|
1003
|
+
applyInputArgs(existingWidget.model, {
|
|
1004
|
+
...args,
|
|
1005
|
+
focus: shouldFocus
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return true;
|
|
1009
|
+
},
|
|
1010
|
+
describedBy: {
|
|
1011
|
+
args: {
|
|
1012
|
+
type: 'object',
|
|
1013
|
+
properties: {
|
|
1014
|
+
area: {
|
|
1015
|
+
type: 'string',
|
|
1016
|
+
enum: ['main', 'side'],
|
|
1017
|
+
description: trans.__(
|
|
1018
|
+
'The name of the area to open or reveal the chat in'
|
|
1019
|
+
)
|
|
1020
|
+
},
|
|
1021
|
+
name: {
|
|
1022
|
+
type: 'string',
|
|
1023
|
+
description: trans.__('The name of the chat')
|
|
1024
|
+
},
|
|
1025
|
+
provider: {
|
|
1026
|
+
type: 'string',
|
|
1027
|
+
description: trans.__('The provider/model to use with this chat')
|
|
1028
|
+
},
|
|
1029
|
+
input: {
|
|
1030
|
+
type: 'string',
|
|
1031
|
+
description: trans.__('The input text to prefill in the chat')
|
|
1032
|
+
},
|
|
1033
|
+
focus: {
|
|
1034
|
+
type: 'boolean',
|
|
1035
|
+
description: trans.__(
|
|
1036
|
+
'Whether to focus the chat input after opening it'
|
|
1037
|
+
)
|
|
1038
|
+
},
|
|
1039
|
+
autoSend: {
|
|
1040
|
+
type: 'boolean',
|
|
1041
|
+
description: trans.__(
|
|
1042
|
+
'Whether to auto-send the provided input after opening the chat'
|
|
1043
|
+
)
|
|
827
1044
|
}
|
|
828
1045
|
}
|
|
829
1046
|
}
|
|
@@ -883,7 +1100,8 @@ function registerCommands(
|
|
|
883
1100
|
activeProvider: previousModel.agentManager.activeProvider,
|
|
884
1101
|
tokenUsage: previousModel.agentManager.tokenUsage,
|
|
885
1102
|
messages: previousModel.messages,
|
|
886
|
-
autosave: previousModel.autosave
|
|
1103
|
+
autosave: previousModel.autosave,
|
|
1104
|
+
title: previousModel.title
|
|
887
1105
|
});
|
|
888
1106
|
|
|
889
1107
|
// Wait (with timeout) for the tracker to have updated the previous widget.
|
|
@@ -903,6 +1121,15 @@ function registerCommands(
|
|
|
903
1121
|
|
|
904
1122
|
if (area === 'main') {
|
|
905
1123
|
openInMain(model);
|
|
1124
|
+
|
|
1125
|
+
if (previousWidget instanceof ChatWidget) {
|
|
1126
|
+
// Clean up the side-panel model entry before disposing the previous
|
|
1127
|
+
// widget/model state.
|
|
1128
|
+
if (!disposeSideChatModel(previousModel)) {
|
|
1129
|
+
previousWidget.dispose();
|
|
1130
|
+
previousModel.dispose();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
906
1133
|
} else {
|
|
907
1134
|
previousWidget?.dispose();
|
|
908
1135
|
previousModel.dispose();
|
|
@@ -1512,6 +1739,7 @@ export default [
|
|
|
1512
1739
|
skillRegistryPlugin,
|
|
1513
1740
|
skillsCommandPlugin,
|
|
1514
1741
|
chatModelHandler,
|
|
1742
|
+
activeCellManager,
|
|
1515
1743
|
plugin,
|
|
1516
1744
|
toolRegistry,
|
|
1517
1745
|
agentManagerFactory,
|
package/src/tokens.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
|
|
|
10
10
|
import type { IModelOptions } from './providers/models';
|
|
@@ -30,6 +30,7 @@ export namespace CommandIds {
|
|
|
30
30
|
export const openSettings = '@jupyterlite/ai:open-settings';
|
|
31
31
|
export const reposition = '@jupyterlite/ai:reposition';
|
|
32
32
|
export const openChat = '@jupyterlite/ai:open-chat';
|
|
33
|
+
export const openOrRevealChat = '@jupyterlite/ai:open-or-reveal-chat';
|
|
33
34
|
export const moveChat = '@jupyterlite/ai:move-chat';
|
|
34
35
|
export const refreshSkills = '@jupyterlite/ai:refresh-skills';
|
|
35
36
|
export const saveChat = '@jupyterlite/ai:save-chat';
|
|
@@ -584,7 +585,7 @@ export interface IAgentManager {
|
|
|
584
585
|
/**
|
|
585
586
|
* Clears conversation history and resets agent state.
|
|
586
587
|
*/
|
|
587
|
-
clearHistory(): void
|
|
588
|
+
clearHistory(): Promise<void>;
|
|
588
589
|
/**
|
|
589
590
|
* Sets the conversation history with a list of messages from the chat.
|
|
590
591
|
* @param messages The chat messages to set as history
|
|
@@ -611,7 +612,12 @@ export interface IAgentManager {
|
|
|
611
612
|
* Handles the complete execution cycle including tool calls.
|
|
612
613
|
* @param message The user message to respond to (may include processed attachment content)
|
|
613
614
|
*/
|
|
614
|
-
generateResponse(message:
|
|
615
|
+
generateResponse(message: UserContent): Promise<void>;
|
|
616
|
+
/**
|
|
617
|
+
* Create a transient language model to request a text response, which won't be added to history.
|
|
618
|
+
* @param messages - the messages sequence to send to the model.
|
|
619
|
+
*/
|
|
620
|
+
textResponse(messages: ModelMessage[]): Promise<string>;
|
|
615
621
|
/**
|
|
616
622
|
* Initializes the AI agent with current settings and tools.
|
|
617
623
|
* Sets up the agent with model configuration, tools, and MCP tools.
|
|
@@ -695,6 +701,10 @@ export interface ICreateChatOptions {
|
|
|
695
701
|
* Whether the chat is autosaved or not.
|
|
696
702
|
*/
|
|
697
703
|
autosave?: boolean;
|
|
704
|
+
/**
|
|
705
|
+
* An optional title to the chat.
|
|
706
|
+
*/
|
|
707
|
+
title?: string | null;
|
|
698
708
|
}
|
|
699
709
|
/**
|
|
700
710
|
* Token for the chat model handler.
|
|
@@ -24,7 +24,8 @@ export namespace MainAreaChat {
|
|
|
24
24
|
export class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
25
25
|
constructor(options: MainAreaChat.IOptions) {
|
|
26
26
|
super(options);
|
|
27
|
-
this.title.label = this.
|
|
27
|
+
this.title.label = this.model.name;
|
|
28
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
28
29
|
|
|
29
30
|
const { trans } = options;
|
|
30
31
|
|
|
@@ -69,6 +70,8 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
this.model.writersChanged.connect(this._writersChanged);
|
|
73
|
+
|
|
74
|
+
this.model.titleChanged.connect(this._titleChanged);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
dispose(): void {
|
|
@@ -76,6 +79,7 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
|
76
79
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
77
80
|
this._outputAreaCompat.dispose();
|
|
78
81
|
this.model.writersChanged.disconnect(this._writersChanged);
|
|
82
|
+
this.model.titleChanged.disconnect(this._titleChanged);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
@@ -107,5 +111,9 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
|
107
111
|
}
|
|
108
112
|
};
|
|
109
113
|
|
|
114
|
+
private _titleChanged = () => {
|
|
115
|
+
this.title.caption = this.model.title ?? this.model.name;
|
|
116
|
+
};
|
|
117
|
+
|
|
110
118
|
private _outputAreaCompat: RenderedMessageOutputAreaCompat;
|
|
111
119
|
}
|