Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export class AgentManager implements IAgentManager {
this._providerRegistry = options.providerRegistry;
this._skillRegistry = options.skillRegistry;
this._secretsManager = options.secretsManager;
this._toolsEnabled = options.toolsEnabled;
this._selectedToolNames = [];
this._agent = null;
this._history = [];
Expand Down Expand Up @@ -782,7 +783,8 @@ export class AgentManager implements IAgentManager {
const model = await this._createModel();

const supportsToolCalling = this._supportsToolCalling();
const canUseTools = config.toolsEnabled && supportsToolCalling;
const canUseTools =
(this._toolsEnabled ?? config.toolsEnabled) && supportsToolCalling;
const hasFunctionToolRegistry = !!(
this._toolRegistry && Object.keys(this._toolRegistry.tools).length > 0
);
Expand Down Expand Up @@ -1281,6 +1283,7 @@ WEB RETRIEVAL POLICY:
private _providerRegistry?: IProviderRegistry;
private _skillRegistry?: ISkillRegistry;
private _secretsManager?: ISecretsManager;
private _toolsEnabled?: boolean;
private _selectedToolNames: string[];
private _agent: ToolLoopAgent<never, ToolMap> | null;
private _history: ModelMessage[];
Expand Down
22 changes: 18 additions & 4 deletions src/chat-model-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ export class ChatModelHandler implements IChatModelHandler {
}

createModel(options: ICreateChatOptions): IAIChatModel {
const { name, activeProvider, tokenUsage, messages, autosave, title } =
options;
const {
name,
activeProvider,
tokenUsage,
messages,
contextMessages,
autosave,
title,
restore,
toolsEnabled,
enableCodeToolbar
} = options;

// Create Agent Manager first so it can be shared
const agentManager = this._agentManagerFactory.createAgent({
Expand All @@ -40,7 +50,8 @@ export class ChatModelHandler implements IChatModelHandler {
providerRegistry: this._providerRegistry,
activeProvider,
tokenUsage,
renderMimeRegistry: this._rmRegistry
renderMimeRegistry: this._rmRegistry,
toolsEnabled
});

// Create AI chat model
Expand All @@ -51,7 +62,10 @@ export class ChatModelHandler implements IChatModelHandler {
activeCellManager: this._activeCellManager,
documentManager: this._docManager,
contentsManager: this._contentsManager,
providerRegistry: this._providerRegistry
providerRegistry: this._providerRegistry,
contextMessages,
restore,
enableCodeToolbar
});

messages?.forEach(message => {
Expand Down
37 changes: 29 additions & 8 deletions src/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
activeCellManager: options.activeCellManager,
documentManager: options.documentManager,
config: {
enableCodeToolbar: true,
enableCodeToolbar: options.enableCodeToolbar ?? true,
sendWithShiftEnter: options.settingsModel.config.sendWithShiftEnter
}
});
Expand All @@ -118,6 +118,12 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
this._agentManager = options.agentManager;
this._contentsManager = options.contentsManager;
this._providerRegistry = options.providerRegistry;
this._contextMessages = (options.contextMessages ?? []).map(message => ({
...message.content,
attachments: message.attachments ? [...message.attachments] : undefined
}));
this._restore = options.restore ?? true;
this._enableCodeToolbar = options.enableCodeToolbar ?? true;

// Listen for agent events
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
Expand All @@ -144,7 +150,7 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
set name(value: string) {
super.name = value;
this._nameChanged.emit(value);
if (!this.messages.length) {
if (this._restore && !this.messages.length) {
const directory = this._settingsModel.config.chatBackupDirectory;
const filepath = PathExt.join(directory, `${this.name}.chat`);
this.restore(filepath, true);
Expand Down Expand Up @@ -301,6 +307,7 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
this.title = null;
this._toolContexts.clear();
await this._agentManager.clearHistory();
await this.rebuildHistory();
};

/**
Expand Down Expand Up @@ -648,7 +655,7 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
});
await this.clearMessages();
this.messagesInserted(0, messages);
await this._rebuildHistory();
await this.rebuildHistory();
this.autosave = content.metadata?.autosave ?? false;
this.title = content.metadata?.title ?? null;
return true;
Expand Down Expand Up @@ -758,7 +765,10 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
*/
private _onSettingsChanged(): void {
const config = this._settingsModel.config;
this.config = { ...config, enableCodeToolbar: true };
this.config = {
...config,
enableCodeToolbar: this._enableCodeToolbar
};
// Agent manager handles agent recreation automatically via its own settings listener
}

Expand All @@ -774,18 +784,18 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
: undefined;
if (modelKey && modelKey !== this._currentModelKey) {
this._currentModelKey = modelKey;
this._rebuildHistory().catch(e =>
this.rebuildHistory().catch(e =>
console.warn('Failed to rebuild history on model change:', e)
);
}
}

/**
* Rebuilds the agent history from the current messages.
* Rebuilds the agent history from the hidden context and current messages.
* For vision-capable models, re-reads binary attachments from disk.
* For text-only models, uses message text only.
*/
private async _rebuildHistory(): Promise<void> {
async rebuildHistory(): Promise<void> {
const providerConfig = this._settingsModel.getProvider(
this._agentManager.activeProvider
);
Expand All @@ -803,7 +813,7 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {
);

const modelMessages: ModelMessage[] = [];
for (const msg of this.messages) {
for (const msg of [...this._contextMessages, ...this.messages]) {
const isAI = msg.sender.username === 'ai-assistant';
if (!isAI && msg.attachments?.length) {
const enhancedContent = await Private.processAttachments(
Expand Down Expand Up @@ -1244,6 +1254,9 @@ export class AIChatModel extends AbstractChatModel implements IAIChatModel {

// Private fields
private _settingsModel: IAISettingsModel;
private _contextMessages: IMessageContent[];
private _restore: boolean;
private _enableCodeToolbar: boolean;
private _user: IUser;
private _toolContexts: Map<string, IToolExecutionContext> = new Map();
private _agentManager: IAgentManager;
Expand Down Expand Up @@ -1842,10 +1855,18 @@ export namespace AIChatModel {
* Optional provider registry for model capability lookups.
*/
providerRegistry?: IProviderRegistry;
/**
* Messages provided to the agent as hidden conversation context.
*/
contextMessages?: IMessage[];
/**
* Whether to restore or not the message (default to true)
*/
restore?: boolean;
/**
* Whether code blocks can modify the active notebook cell.
*/
enableCodeToolbar?: boolean;
}

/**
Expand Down
157 changes: 157 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ import {
} from '@jupyterlab/translation';

import {
closeIcon,
fileUploadIcon,
PanelWithToolbar,
saveIcon,
settingsIcon,
Toolbar,
Expand Down Expand Up @@ -137,6 +139,9 @@ import { AISettingsWidget } from './widgets/ai-settings';

import { MainAreaChat } from './widgets/main-area-chat';

const SIDE_CHAT_ID = '@jupyterlite/ai:side-chat';
const SIDE_CHAT_MODEL_NAME = '@jupyterlite/ai:side-chat-model';

namespace Private {
let aiSecretsToken: symbol | null = null;

Expand Down Expand Up @@ -552,6 +557,20 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
// Update the tracker if the active provider changed.
model.agentManager.activeProviderChanged.connect(saveTracker);

chatPanel.current?.toolbar.insertBefore(
'close',
'sideChat',
new ToolbarButton({
icon: chatIcon,
onClick: () => {
void app.commands.execute(CommandIds.openSideChat, {
name: model.name
});
},
tooltip: trans.__('Open side chat')
})
);

// Update the token usage widget.
usageWidget?.dispose();

Expand Down Expand Up @@ -887,6 +906,144 @@ function registerCommands(
return true;
};

const createSideChatInputToolbar = () => {
const registry = InputToolbarRegistry.defaultToolbarRegistry();
registry.addItem('stop', stopItem(trans));
registry.addItem('clear', clearItem(trans));
registry.addItem('model', createModelSelectItem(settingsModel, trans));
return registry;
};

let sideChatWidget: PanelWithToolbar | null = null;
let sideChatContent: ChatWidget | null = null;
let sideChatSource: IAIChatModel | null = null;

commands.addCommand(CommandIds.openSideChat, {
label: trans.__('Open side chat'),
caption: trans.__('Open a side chat for this conversation'),
icon: chatIcon,
execute: async (args): Promise<boolean> => {
const sourceName =
typeof args.name === 'string' ? args.name : undefined;
const sourceWidget = findChatWidget(sourceName);
const sourceModel = sourceWidget?.model as IAIChatModel | undefined;
if (!sourceModel) {
return false;
}

if (sideChatWidget && sideChatSource === sourceModel) {
app.shell.activateById(sideChatWidget.id);
sideChatContent?.model.input.focus();
return true;
}

sideChatWidget?.dispose();

const activeProvider = sourceModel.agentManager.activeProvider;
if (!activeProvider) {
showErrorMessage(
trans.__('Error creating side chat'),
trans.__('Please set up a provider')
);
if (commands.hasCommand(CommandIds.openSettings)) {
void commands.execute(CommandIds.openSettings);
}
return false;
}

const model = modelRegistry.createModel({
name: SIDE_CHAT_MODEL_NAME,
activeProvider,
contextMessages: sourceModel.messages,
restore: false,
toolsEnabled: false,
enableCodeToolbar: false
});
await model.rebuildHistory();

const content = new ChatWidget({
model,
rmRegistry,
themeManager: themeManager ?? null,
inputToolbarRegistry: createSideChatInputToolbar(),
attachmentOpenerRegistry,
chatCommandRegistry,
area: 'sidebar'
});
const widget = new PanelWithToolbar();
widget.id = SIDE_CHAT_ID;
widget.addClass('jp-ai-side-chat');
widget.title.icon = chatIcon;
widget.title.label = trans.__('Side Chat');
widget.title.caption = trans.__(
'Side chat for %1',
sourceModel.title ?? sourceModel.name
);
widget.title.closable = true;
widget.toolbar.addItem('spacer', Toolbar.createSpacerItem());
widget.toolbar.addItem(
'close',
new ToolbarButton({
icon: closeIcon,
onClick: () => {
widget.dispose();
},
tooltip: trans.__('Close side chat')
})
);
widget.addWidget(widget.toolbar);
widget.addWidget(content);

const writersChanged = (
_: IChatModel,
writers: IChatModel.IWriter[]
) => {
const aiWriting = writers.some(
writer => writer.user.username === 'ai-assistant'
);
if (aiWriting) {
content.inputToolbarRegistry?.show('stop');
} else {
content.inputToolbarRegistry?.hide('stop');
}
};
model.writersChanged?.connect(writersChanged);

const outputAreaCompat = new RenderedMessageOutputAreaCompat({
chatPanel: content
});
widget.disposed.connect(() => {
model.writersChanged?.disconnect(writersChanged);
outputAreaCompat.dispose();
model.dispose();
if (sideChatWidget === widget) {
sideChatWidget = null;
sideChatContent = null;
sideChatSource = null;
}
});

sideChatWidget = widget;
sideChatContent = content;
sideChatSource = sourceModel;
app.shell.add(widget, 'right', { rank: 1000 });
app.shell.activateById(widget.id);
model.input.focus();
return true;
},
describedBy: {
args: {
type: 'object',
properties: {
name: {
type: 'string',
description: trans.__('The source chat name')
}
}
}
}
});

commands.addCommand(CommandIds.openChat, {
label: trans.__('Open a chat'),
execute: async (args): Promise<boolean> => {
Expand Down
Loading
Loading