Skip to content

Commit 1375f31

Browse files
Aaaaashensorrow
andauthored
chore: improve chat context (#4407)
* chore: improve chat context * Update packages/ai-native/src/browser/components/ChatInput.tsx Co-authored-by: LouisLv <zheyanglv@qq.com> * chore: update session manager --------- Co-authored-by: LouisLv <zheyanglv@qq.com>
1 parent daa8564 commit 1375f31

23 files changed

Lines changed: 343 additions & 185 deletions

packages/ai-native/__test__/browser/chat/chat-agent.service.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { CancellationToken } from '@opensumi/ide-core-common';
1+
import { CancellationToken, Emitter } from '@opensumi/ide-core-common';
2+
import { ChatFeatureRegistryToken, ChatServiceToken } from '@opensumi/ide-core-common/lib/types/ai-native';
23
import { createBrowserInjector } from '@opensumi/ide-dev-tool/src/injector-helper';
34
import { MockInjector } from '@opensumi/ide-dev-tool/src/mock-injector';
45

56
import { ChatAgentService } from '../../../lib/browser/chat/chat-agent.service';
67
import { IChatAgent, IChatAgentMetadata, IChatAgentRequest, IChatManagerService } from '../../../lib/common';
8+
import { LLMContextServiceToken } from '../../../lib/common/llm-context';
9+
import { ChatAgentPromptProvider } from '../../../lib/common/prompts/context-prompt-provider';
710

811
describe('ChatAgentService', () => {
912
let injector: MockInjector;
@@ -19,9 +22,32 @@ describe('ChatAgentService', () => {
1922
startSession: jest.fn(),
2023
},
2124
},
25+
{
26+
token: ChatAgentPromptProvider,
27+
useValue: {
28+
provideContextPrompt: (val, msg) => msg,
29+
},
30+
},
31+
{
32+
token: ChatServiceToken,
33+
useValue: {
34+
},
35+
},
36+
{
37+
token: LLMContextServiceToken,
38+
useValue: {
39+
onDidContextFilesChangeEvent: new Emitter().event,
40+
serialize: () => { },
41+
},
42+
},
43+
{
44+
token: ChatFeatureRegistryToken,
45+
useValue: {
46+
},
47+
},
2248
]),
2349
);
24-
chatAgentService = new ChatAgentService();
50+
chatAgentService = injector.get(ChatAgentService);
2551
});
2652

2753
it('should register an agent', () => {
@@ -42,7 +68,7 @@ describe('ChatAgentService', () => {
4268
id: 'agent1',
4369
metadata: {},
4470
provideSlashCommands: () => Promise.resolve([]),
45-
invoke: () => {},
71+
invoke: () => { },
4672
} as unknown as IChatAgent;
4773
chatAgentService.registerAgent(agent);
4874

packages/ai-native/src/browser/chat/chat-agent.service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Autowired, Injectable } from '@opensumi/di';
44
import {
55
CancellationToken,
66
ChatFeatureRegistryToken,
7+
ChatMessageRole,
78
ChatServiceToken,
89
Disposable,
910
Emitter,
@@ -24,6 +25,8 @@ import {
2425
IChatFollowup,
2526
IChatMessageStructure,
2627
} from '../../common';
28+
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
29+
import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider';
2730
import { IChatFeatureRegistry } from '../types';
2831

2932
import { ChatService } from './chat.api.service';
@@ -34,6 +37,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
3437

3538
private defaultAgentId: string | undefined;
3639

40+
private initialUserMessageMap: Map<string, string> = new Map();
41+
42+
private shouldUpdateContext = false;
43+
44+
private contextVersion: number;
45+
3746
private readonly _onDidChangeAgents = new Emitter<void>();
3847
readonly onDidChangeAgents = this._onDidChangeAgents.event;
3948

@@ -43,6 +52,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
4352
@Autowired(ILogger)
4453
logger: ILogger;
4554

55+
@Autowired(LLMContextServiceToken)
56+
protected readonly contextService: LLMContextService;
57+
58+
@Autowired(ChatAgentPromptProvider)
59+
protected readonly promptProvider: ChatAgentPromptProvider;
60+
4661
@Autowired(ChatServiceToken)
4762
private aiChatService: ChatService;
4863

@@ -52,6 +67,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
5267
constructor() {
5368
super();
5469
this.addDispose(this._onDidChangeAgents);
70+
this.addDispose(this.contextService.onDidContextFilesChangeEvent((event) => {
71+
if (event.version !== this.contextVersion) {
72+
this.contextVersion = event.version;
73+
this.shouldUpdateContext = true;
74+
}
75+
}));
5576
}
5677

5778
registerAgent(agent: IChatAgent): IDisposable {
@@ -119,10 +140,28 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
119140
throw new Error(`No agent with id ${id}`);
120141
}
121142

143+
// 发送第一条消息时携带初始 context
144+
if (!this.initialUserMessageMap.has(request.sessionId)) {
145+
this.initialUserMessageMap.set(request.sessionId, request.message);
146+
const rawMessage = request.message;
147+
request.message = this.provideContextMessage(rawMessage);
148+
}
149+
150+
if (this.shouldUpdateContext) {
151+
request.message = this.provideContextMessage(request.message);
152+
this.shouldUpdateContext = false;
153+
}
154+
122155
const result = await data.agent.invoke(request, progress, history, token);
123156
return result;
124157
}
125158

159+
private provideContextMessage(message: string) {
160+
const context = this.contextService.serialize();
161+
const fullMessage = this.promptProvider.provideContextPrompt(context, message);
162+
return fullMessage;
163+
}
164+
126165
populateChatInput(id: string, message: IChatMessageStructure) {
127166
this.aiChatService.sendMessage({
128167
...message,

packages/ai-native/src/browser/chat/chat-model.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di';
1+
import { Injectable } from '@opensumi/di';
22
import {
33
Disposable,
44
Emitter,
@@ -8,8 +8,6 @@ import {
88
IChatProgress,
99
IChatToolContent,
1010
IChatTreeData,
11-
ILogger,
12-
memoize,
1311
uuid,
1412
} from '@opensumi/ide-core-common';
1513
import { MarkdownString, isMarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent';
@@ -274,13 +272,18 @@ export class ChatRequestModel implements IChatRequestModel {
274272
export class ChatModel extends Disposable implements IChatModel {
275273
private static requestIdPool = 0;
276274

277-
constructor(initParams?: { sessionId?: string; history?: MsgHistoryManager; requests?: ChatRequestModel[] }) {
275+
constructor(initParams?: {
276+
sessionId?: string;
277+
history?: MsgHistoryManager;
278+
requests?: ChatRequestModel[];
279+
}) {
278280
super();
279281
this.#sessionId = initParams?.sessionId ?? uuid();
280282
this.history = initParams?.history ?? new MsgHistoryManager();
281283
if (initParams?.requests) {
282284
this.#requests = new Map(initParams.requests.map((r) => [r.requestId, r]));
283285
}
286+
284287
}
285288

286289
#sessionId: string;
@@ -300,9 +303,11 @@ export class ChatModel extends Disposable implements IChatModel {
300303
readonly history: MsgHistoryManager;
301304

302305
addRequest(message: IChatRequestMessage): ChatRequestModel {
306+
const msg = message;
307+
303308
const requestId = `${this.sessionId}_request_${ChatModel.requestIdPool++}`;
304-
const response = new ChatResponseModel(requestId, this, message.agentId);
305-
const request = new ChatRequestModel(requestId, this, message, response);
309+
const response = new ChatResponseModel(requestId, this, msg.agentId);
310+
const request = new ChatRequestModel(requestId, this, msg, response);
306311

307312
this.#requests.set(requestId, request);
308313
return request;

packages/ai-native/src/browser/chat/chat-proxy.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
IAIReporter,
1313
IApplicationService,
1414
IChatProgress,
15-
uuid,
15+
getOperatingSystemName,
1616
} from '@opensumi/ide-core-common';
1717
import { AINativeSettingSectionsId } from '@opensumi/ide-core-common/lib/settings/ai-native';
1818
import { IChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
@@ -92,7 +92,7 @@ export class ChatProxyService extends Disposable {
9292
AINativeSettingSectionsId.SystemPrompt,
9393
'You are a powerful AI coding assistant working in OpenSumi, a top IDE framework. You collaborate with a USER to solve coding tasks, which may involve creating, modifying, or debugging code, or answering questions. When the USER sends a message, relevant context (e.g., open files, cursor position, edit history, linter errors) may be attached. Use this information as needed.\n\n<tool_calling>\nYou have access to tools to assist with tasks. Follow these rules:\n1. Always adhere to the tool call schema and provide all required parameters.\n2. Only use tools explicitly provided; ignore unavailable ones.\n3. Avoid mentioning tool names to the USER (e.g., say "I will edit your file" instead of "I need to use the edit_file tool").\n4. Only call tools when necessary; respond directly if the task is general or you already know the answer.\n5. Explain to the USER why you’re using a tool before calling it.\n</tool_calling>\n\n<making_code_changes>\nWhen modifying code:\n1. Use code edit tools instead of outputting code unless explicitly requested.\n2. Limit tool calls to one per turn.\n3. Ensure generated code is immediately executable by including necessary imports, dependencies, and endpoints.\n4. For new projects, create a dependency management file (e.g., requirements.txt) and a README.\n5. For web apps, design a modern, user-friendly UI.\n6. Avoid generating non-textual or excessively long code.\n7. Read file contents before editing, unless appending a small change or creating a new file.\n8. Fix introduced linter errors if possible, but stop after 3 attempts and ask the USER for guidance.\n9. Reapply reasonable code edits if they weren’t followed initially.\n</making_code_changes>\n\nUse the appropriate tools to fulfill the USER’s request, ensuring all required parameters are provided or inferred from context.',
9494
) +
95-
`\n\n<user_info>\nThe user's OS version is ${this.applicationService.frontendOS}. The absolute path of the user's workspace is ${this.appConfig.workspaceDir}.\n</user_info>`,
95+
`\n\n<user_info>\nThe user's OS is ${getOperatingSystemName()}. The absolute path of the user's workspace is ${this.appConfig.workspaceDir}.\n</user_info>`,
9696
},
9797
invoke: async (
9898
request: IChatAgentRequest,

packages/ai-native/src/browser/chat/chat.module.less

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@
279279
}
280280
}
281281

282-
283282
.chat_tips_container {
284283
display: flex;
285284
align-items: center;
@@ -290,6 +289,6 @@
290289
}
291290

292291
.chat_history {
293-
width: calc(100% - 60px);
292+
width: calc(100% - 40px);
294293
color: var(--design-text-foreground);
295294
}

packages/ai-native/src/browser/chat/chat.view.tsx

Lines changed: 7 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
AINativeConfigService,
66
CommandService,
77
getIcon,
8-
useEventEffect,
98
useInjectable,
109
useUpdateOnEvent,
1110
} from '@opensumi/ide-core-browser';
@@ -42,8 +41,6 @@ import {
4241
IChatMessageStructure,
4342
TokenMCPServerProxyService,
4443
} from '../../common';
45-
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
46-
import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider';
4744
import { ChatContext } from '../components/ChatContext';
4845
import { CodeBlockWrapperInput } from '../components/ChatEditor';
4946
import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
@@ -82,18 +79,12 @@ export const AIChatView = () => {
8279
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
8380
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
8481
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
85-
const contextService = useInjectable<LLMContextService>(LLMContextServiceToken);
86-
const promptProvider = useInjectable<ChatAgentPromptProvider>(ChatAgentPromptProvider);
87-
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);
8882

8983
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
9084
const msgHistoryManager = aiChatService.sessionModel.history;
9185
const containerRef = React.useRef<HTMLDivElement>(null);
9286
const autoScroll = React.useRef<boolean>(true);
9387
const chatInputRef = React.useRef<{ setInputValue: (v: string) => void } | null>(null);
94-
const dialogService = useInjectable<IDialogService>(IDialogService);
95-
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
96-
const commandService = useInjectable<CommandService>(CommandService);
9788

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

@@ -115,8 +106,6 @@ export const AIChatView = () => {
115106
const [defaultAgentId, setDefaultAgentId] = React.useState<string>('');
116107
const [command, setCommand] = React.useState('');
117108
const [theme, setTheme] = React.useState<string | null>(null);
118-
const [mcpToolsCount, setMcpToolsCount] = React.useState<number>(0);
119-
const [mcpServersCount, setMcpServersCount] = React.useState<number>(0);
120109

121110
React.useEffect(() => {
122111
const featureSlashCommands = chatFeatureRegistry.getAllShortcutSlashCommand();
@@ -537,10 +526,7 @@ export const AIChatView = () => {
537526
const { message, agentId, command, reportExtra } = value;
538527
const { actionType, actionSource } = reportExtra || {};
539528

540-
const context = contextService.serialize();
541-
const fullMessage = await promptProvider.provideContextPrompt(context, message);
542-
543-
const request = aiChatService.createRequest(fullMessage, agentId!, command);
529+
const request = aiChatService.createRequest(message, agentId!, command);
544530
if (!request) {
545531
return;
546532
}
@@ -689,32 +675,6 @@ export const AIChatView = () => {
689675
};
690676
}, [aiChatService.sessionModel]);
691677

692-
useEventEffect(
693-
mcpServerProxyService.onChangeMCPServers,
694-
() => {
695-
mcpServerProxyService.getAllMCPTools().then((tools) => {
696-
setMcpToolsCount(tools.length);
697-
});
698-
mcpServerProxyService.$getServers().then((servers) => {
699-
setMcpServersCount(servers.length);
700-
});
701-
},
702-
[mcpServerProxyService],
703-
);
704-
705-
const handleShowMCPTools = React.useCallback(async () => {
706-
const tools = await mcpServerProxyService.getAllMCPTools();
707-
dialogService.open({
708-
message: <MCPToolsDialog tools={tools} />,
709-
type: MessageType.Empty,
710-
buttons: ['关闭'],
711-
});
712-
}, [mcpServerProxyService, dialogService]);
713-
714-
const handleShowMCPConfig = React.useCallback(() => {
715-
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
716-
}, [commandService]);
717-
718678
return (
719679
<div id={styles.ai_chat_view}>
720680
<div className={styles.header_container}>
@@ -754,18 +714,6 @@ export const AIChatView = () => {
754714
</Popover>
755715
))}
756716
</div>
757-
<div className={styles.header_operate_right}>
758-
{aiNativeConfigService.capabilities.supportsMCP && (
759-
<>
760-
<div className={styles.tag} onClick={handleShowMCPConfig}>
761-
{`MCP Servers: ${mcpServersCount}`}
762-
</div>
763-
<div className={styles.tag} onClick={handleShowMCPTools}>
764-
{`MCP Tools: ${mcpToolsCount}`}
765-
</div>
766-
</>
767-
)}
768-
</div>
769717
</div>
770718
<ChatInputWrapperRender
771719
onSend={(value, agentId, command) =>
@@ -808,6 +756,8 @@ export function DefaultChatViewHeader({
808756
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
809757
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);
810758
const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
759+
const commandService = useInjectable<CommandService>(CommandService);
760+
811761
const [historyList, setHistoryList] = React.useState<IChatHistoryItem[]>([]);
812762
const [currentTitle, setCurrentTitle] = React.useState<string>('');
813763
const handleNewChat = React.useCallback(() => {
@@ -828,6 +778,10 @@ export function DefaultChatViewHeader({
828778
[aiChatService],
829779
);
830780

781+
const handleShowMCPConfig = React.useCallback(() => {
782+
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
783+
}, [commandService]);
784+
831785
const handleShowMCPTools = React.useCallback(async () => {
832786
const tools = await mcpServerProxyService.getAllMCPTools();
833787
dialogService.open({
@@ -916,23 +870,6 @@ export function DefaultChatViewHeader({
916870
ariaLabel={localize('aiNative.operate.clear.title')}
917871
/>
918872
</Popover>
919-
{aiNativeConfigService.capabilities.supportsMCP && (
920-
<Popover
921-
overlayClassName={styles.popover_icon}
922-
id={'ai-chat-header-tools'}
923-
position={PopoverPosition.left}
924-
title={localize('aiNative.operate.tools.title')}
925-
>
926-
<EnhanceIcon
927-
wrapperClassName={styles.action_btn}
928-
className={getIcon('menubar-tool')}
929-
onClick={handleShowMCPTools}
930-
tabIndex={0}
931-
role='button'
932-
ariaLabel={localize('aiNative.operate.tools.title')}
933-
/>
934-
</Popover>
935-
)}
936873
<Popover
937874
overlayClassName={styles.popover_icon}
938875
id={'ai-chat-header-close'}

packages/ai-native/src/browser/components/ChatContext/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const ChatContext = memo(() => {
3939
50,
4040
)((files) => {
4141
if (files) {
42-
updateAddedFiles(files);
42+
updateAddedFiles([...files.attached]);
4343
}
4444
}, contextService);
4545

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

5959
const onDidDeselect = useCallback((uri: URI) => {
60-
contextService.removeFileFromContext(uri);
60+
contextService.removeFileFromContext(uri, true);
6161
}, []);
6262

6363
const onDidClickFile = useCallback((uri: URI) => {

0 commit comments

Comments
 (0)