Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: improve chat context #4407

Open
wants to merge 11 commits into
base: v3.8
Choose a base branch
from
18 changes: 17 additions & 1 deletion packages/ai-native/src/browser/chat/chat-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import { ChatMessageRole, IChatMessage, IHistoryChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';

import { IChatAgentService, IChatFollowup, IChatRequestMessage, IChatResponseErrorDetails } from '../../common';
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider';
import { MsgHistoryManager } from '../model/msg-history-manager';

import { ChatModel, ChatRequestModel, ChatResponseModel, IChatProgressResponseContent } from './chat-model';
Expand Down Expand Up @@ -49,6 +51,12 @@ export class ChatManagerService extends Disposable {
@Autowired(StorageProvider)
private storageProvider: StorageProvider;

@Autowired(ChatAgentPromptProvider)
protected readonly promptProvider: ChatAgentPromptProvider;

@Autowired(LLMContextServiceToken)
protected readonly contextService: LLMContextService;

private _chatStorage: IStorage;

protected fromJSON(data: ISessionModel[]) {
Expand Down Expand Up @@ -98,11 +106,19 @@ export class ChatManagerService extends Disposable {
}

startSession() {
const model = new ChatModel();
const model = new ChatModel({
provideContext: this.provideContextPrompt.bind(this),
});
this.#sessionModels.set(model.sessionId, model);
return model;
}

private provideContextPrompt(message: string) {
const context = this.contextService.serialize();
const fullMessage = this.promptProvider.provideContextPrompt(context, message);
return fullMessage;
}

getSession(sessionId: string): ChatModel | undefined {
return this.#sessionModels.get(sessionId);
}
Expand Down
21 changes: 18 additions & 3 deletions packages/ai-native/src/browser/chat/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,22 @@ export class ChatRequestModel implements IChatRequestModel {
export class ChatModel extends Disposable implements IChatModel {
private static requestIdPool = 0;

constructor(initParams?: { sessionId?: string; history?: MsgHistoryManager; requests?: ChatRequestModel[] }) {
private provideContextPrompt?: (string) => string;

constructor(initParams?: {
sessionId?: string;
history?: MsgHistoryManager;
requests?: ChatRequestModel[];
provideContext?: (msg: string) => string;
}) {
super();
this.#sessionId = initParams?.sessionId ?? uuid();
this.history = initParams?.history ?? new MsgHistoryManager();
if (initParams?.requests) {
this.#requests = new Map(initParams.requests.map((r) => [r.requestId, r]));
}

this.provideContextPrompt = initParams?.provideContext;
}

#sessionId: string;
Expand All @@ -300,9 +309,15 @@ export class ChatModel extends Disposable implements IChatModel {
readonly history: MsgHistoryManager;

addRequest(message: IChatRequestMessage): ChatRequestModel {
const msg = message;
// first msg
if (ChatModel.requestIdPool === 0 && this.provideContextPrompt) {
msg.prompt = this.provideContextPrompt(msg.prompt);
}

const requestId = `${this.sessionId}_request_${ChatModel.requestIdPool++}`;
const response = new ChatResponseModel(requestId, this, message.agentId);
const request = new ChatRequestModel(requestId, this, message, response);
const response = new ChatResponseModel(requestId, this, msg.agentId);
const request = new ChatRequestModel(requestId, this, msg, response);

this.#requests.set(requestId, request);
return request;
Expand Down
3 changes: 1 addition & 2 deletions packages/ai-native/src/browser/chat/chat.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@
}
}


.chat_tips_container {
display: flex;
align-items: center;
Expand All @@ -290,6 +289,6 @@
}

.chat_history {
width: calc(100% - 60px);
width: calc(100% - 40px);
color: var(--design-text-foreground);
}
77 changes: 7 additions & 70 deletions packages/ai-native/src/browser/chat/chat.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
AINativeConfigService,
CommandService,
getIcon,
useEventEffect,
useInjectable,
useUpdateOnEvent,
} from '@opensumi/ide-core-browser';
Expand Down Expand Up @@ -42,8 +41,6 @@ import {
IChatMessageStructure,
TokenMCPServerProxyService,
} from '../../common';
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider';
import { ChatContext } from '../components/ChatContext';
import { CodeBlockWrapperInput } from '../components/ChatEditor';
import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
Expand Down Expand Up @@ -82,17 +79,11 @@ export const AIChatView = () => {
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
const contextService = useInjectable<LLMContextService>(LLMContextServiceToken);
const promptProvider = useInjectable<ChatAgentPromptProvider>(ChatAgentPromptProvider);
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);

const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
const msgHistoryManager = aiChatService.sessionModel.history;
const containerRef = React.useRef<HTMLDivElement>(null);
const chatInputRef = React.useRef<{ setInputValue: (v: string) => void } | null>(null);
const dialogService = useInjectable<IDialogService>(IDialogService);
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
const commandService = useInjectable<CommandService>(CommandService);

const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);

Expand All @@ -114,8 +105,6 @@ export const AIChatView = () => {
const [defaultAgentId, setDefaultAgentId] = React.useState<string>('');
const [command, setCommand] = React.useState('');
const [theme, setTheme] = React.useState<string | null>(null);
const [mcpToolsCount, setMcpToolsCount] = React.useState<number>(0);
const [mcpServersCount, setMcpServersCount] = React.useState<number>(0);

React.useEffect(() => {
const featureSlashCommands = chatFeatureRegistry.getAllShortcutSlashCommand();
Expand Down Expand Up @@ -515,10 +504,7 @@ export const AIChatView = () => {
const { message, agentId, command, reportExtra } = value;
const { actionType, actionSource } = reportExtra || {};

const context = contextService.serialize();
const fullMessage = await promptProvider.provideContextPrompt(context, message);

const request = aiChatService.createRequest(fullMessage, agentId!, command);
const request = aiChatService.createRequest(message, agentId!, command);
if (!request) {
return;
}
Expand Down Expand Up @@ -667,32 +653,6 @@ export const AIChatView = () => {
};
}, [aiChatService.sessionModel]);

useEventEffect(
mcpServerProxyService.onChangeMCPServers,
() => {
mcpServerProxyService.getAllMCPTools().then((tools) => {
setMcpToolsCount(tools.length);
});
mcpServerProxyService.$getServers().then((servers) => {
setMcpServersCount(servers.length);
});
},
[mcpServerProxyService],
);

const handleShowMCPTools = React.useCallback(async () => {
const tools = await mcpServerProxyService.getAllMCPTools();
dialogService.open({
message: <MCPToolsDialog tools={tools} />,
type: MessageType.Empty,
buttons: ['ๅ…ณ้—ญ'],
});
}, [mcpServerProxyService, dialogService]);

const handleShowMCPConfig = React.useCallback(() => {
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
}, [commandService]);

return (
<div id={styles.ai_chat_view}>
<div className={styles.header_container}>
Expand Down Expand Up @@ -732,18 +692,6 @@ export const AIChatView = () => {
</Popover>
))}
</div>
<div className={styles.header_operate_right}>
{aiNativeConfigService.capabilities.supportsMCP && (
<>
<div className={styles.tag} onClick={handleShowMCPConfig}>
{`MCP Servers: ${mcpServersCount}`}
</div>
<div className={styles.tag} onClick={handleShowMCPTools}>
{`MCP Tools: ${mcpToolsCount}`}
</div>
</>
)}
</div>
</div>
<ChatInputWrapperRender
onSend={(value, agentId, command) =>
Expand Down Expand Up @@ -786,6 +734,8 @@ export function DefaultChatViewHeader({
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);
const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
const commandService = useInjectable<CommandService>(CommandService);

const [historyList, setHistoryList] = React.useState<IChatHistoryItem[]>([]);
const [currentTitle, setCurrentTitle] = React.useState<string>('');
const handleNewChat = React.useCallback(() => {
Expand All @@ -806,6 +756,10 @@ export function DefaultChatViewHeader({
[aiChatService],
);

const handleShowMCPConfig = React.useCallback(() => {
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
}, [commandService]);

const handleShowMCPTools = React.useCallback(async () => {
const tools = await mcpServerProxyService.getAllMCPTools();
dialogService.open({
Expand Down Expand Up @@ -894,23 +848,6 @@ export function DefaultChatViewHeader({
ariaLabel={localize('aiNative.operate.clear.title')}
/>
</Popover>
{aiNativeConfigService.capabilities.supportsMCP && (
<Popover
overlayClassName={styles.popover_icon}
id={'ai-chat-header-tools'}
position={PopoverPosition.left}
title={localize('aiNative.operate.tools.title')}
>
<EnhanceIcon
wrapperClassName={styles.action_btn}
className={getIcon('menubar-tool')}
onClick={handleShowMCPTools}
tabIndex={0}
role='button'
ariaLabel={localize('aiNative.operate.tools.title')}
/>
</Popover>
)}
<Popover
overlayClassName={styles.popover_icon}
id={'ai-chat-header-close'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const ChatContext = memo(() => {
50,
)((files) => {
if (files) {
updateAddedFiles(files);
updateAddedFiles([...files.attached]);
}
}, contextService);

Expand All @@ -57,7 +57,7 @@ export const ChatContext = memo(() => {
}, []);

const onDidDeselect = useCallback((uri: URI) => {
contextService.removeFileFromContext(uri);
contextService.removeFileFromContext(uri, true);
}, []);

const onDidClickFile = useCallback((uri: URI) => {
Expand Down
71 changes: 67 additions & 4 deletions packages/ai-native/src/browser/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import cls from 'classnames';
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';

import { useInjectable, useLatest } from '@opensumi/ide-core-browser';
import { AINativeConfigService, useInjectable, useLatest } from '@opensumi/ide-core-browser';
import { Icon, Popover, PopoverPosition, getIcon } from '@opensumi/ide-core-browser/lib/components';
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { InteractiveInput } from '@opensumi/ide-core-browser/lib/components/ai-native/interactive-input/index';
import { ChatAgentViewServiceToken, ChatFeatureRegistryToken, localize, runWhenIdle } from '@opensumi/ide-core-common';
import {
ChatAgentViewServiceToken,
ChatFeatureRegistryToken,
MessageType,
localize,
runWhenIdle,
} from '@opensumi/ide-core-common';
import { CommandService } from '@opensumi/ide-core-common/lib/command';
import { MonacoCommandRegistry } from '@opensumi/ide-editor/lib/browser/monaco-contrib/command/command.service';
import { IDialogService } from '@opensumi/ide-overlay';

import { AT_SIGN_SYMBOL, IChatAgentService, SLASH_SYMBOL } from '../../common';
import { AT_SIGN_SYMBOL, IChatAgentService, SLASH_SYMBOL, TokenMCPServerProxyService } from '../../common';
import { ChatAgentViewService } from '../chat/chat-agent.view.service';
import { ChatSlashCommandItemModel } from '../chat/chat-model';
import { ChatProxyService } from '../chat/chat-proxy.service';
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
import { MCPServerProxyService } from '../mcp/mcp-server-proxy.service';
import { MCPToolsDialog } from '../mcp/mcp-tools-dialog.view';
import { IChatSlashCommandItem } from '../types';

import { ChatContext } from './ChatContext';
import styles from './components.module.less';

const INSTRUCTION_BOTTOM = 8;
Expand Down Expand Up @@ -195,12 +205,29 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
const [isExpand, setIsExpand] = useState(false);
const [placeholder, setPlaceHolder] = useState(localize('aiNative.chat.input.placeholder.default'));

const dialogService = useInjectable<IDialogService>(IDialogService);
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);
const monacoCommandRegistry = useInjectable<MonacoCommandRegistry>(MonacoCommandRegistry);
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
const commandService = useInjectable<CommandService>(CommandService);

const currentAgentIdRef = useLatest(agentId);

const handleShowMCPConfig = React.useCallback(() => {
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
}, [commandService]);

const handleShowMCPTools = React.useCallback(async () => {
const tools = await mcpServerProxyService.getAllMCPTools();
dialogService.open({
message: <MCPToolsDialog tools={tools} />,
type: MessageType.Empty,
buttons: ['ๅ…ณ้—ญ'],
});
}, [mcpServerProxyService, dialogService]);

useImperativeHandle(ref, () => ({
setInputValue: (v: string) => {
setValue(v);
Expand Down Expand Up @@ -462,6 +489,42 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
height={inputHeight}
popoverPosition={PopoverPosition.left}
/>
<div className={styles.chat_input_footer}>
{aiNativeConfigService.capabilities.supportsMCP && (
<div className={styles.mcp_desc}>
<Popover
overlayClassName={styles.popover_icon}
id={'ai-chat-header-mcp-server'}
position={PopoverPosition.left}
title={'MCP Server'}
>
<EnhanceIcon
wrapperClassName={styles.action_btn}
className={'codicon codicon-server'}
onClick={handleShowMCPConfig}
tabIndex={0}
role='button'
ariaLabel={'MCP Server'}
/>
</Popover>
<Popover
overlayClassName={styles.popover_icon}
id={'ai-chat-header-tools'}
position={PopoverPosition.left}
title={localize('aiNative.operate.tools.title')}
>
<EnhanceIcon
wrapperClassName={styles.action_btn}
className={getIcon('menubar-tool')}
onClick={handleShowMCPTools}
tabIndex={0}
role='button'
ariaLabel={localize('aiNative.operate.tools.title')}
/>
</Popover>
</div>
)}
</div>
</div>
);
});
3 changes: 1 addition & 2 deletions packages/ai-native/src/browser/components/ChatToolRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ export const ChatToolRender = (props: { value: IChatToolContent['content']; mess
<div className={styles['chat-tool-render']}>
<div className={styles['tool-header']} onClick={toggleExpand}>
<div className={styles['tool-name']}>
<span className={cls(styles['expand-icon'], { [styles.expanded]: isExpanded })}>โ–ถ</span>
<Icon iconClass={`codicon codicon-triangle-${isExpanded ? 'down' : 'right'}`} />
{label}
</div>
{value.state && (
<div className={styles['tool-state']}>
<span className={styles['state-icon']}>{stateInfo.icon}</span>
<span className={styles['state-label']}>{stateInfo.label}</span>
</div>
)}
</div>
Expand Down
Loading
Loading