Skip to content

Commit 994f538

Browse files
committed
chore: improve chat context
1 parent d65885d commit 994f538

24 files changed

Lines changed: 330 additions & 220 deletions

File tree

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { CancellationToken } from '@opensumi/ide-core-common';
1+
import { LLMContextServiceToken } from '@opensumi/ide-ai-native/lib/common/llm-context';
2+
import { CancellationToken, Emitter } from '@opensumi/ide-core-common';
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';
6-
import { IChatAgent, IChatAgentMetadata, IChatAgentRequest, IChatManagerService } from '../../../lib/common';
7+
import { IChatAgent, IChatAgentMetadata, IChatAgentRequest, IChatAgentService, IChatManagerService } from '../../../lib/common';
78

89
describe('ChatAgentService', () => {
910
let injector: MockInjector;
@@ -19,6 +20,12 @@ describe('ChatAgentService', () => {
1920
startSession: jest.fn(),
2021
},
2122
},
23+
{
24+
token: LLMContextServiceToken,
25+
useValue: {
26+
onDidContextFilesChangeEvent: new Emitter().event,
27+
},
28+
},
2229
]),
2330
);
2431
chatAgentService = new ChatAgentService();
@@ -42,7 +49,7 @@ describe('ChatAgentService', () => {
4249
id: 'agent1',
4350
metadata: {},
4451
provideSlashCommands: () => Promise.resolve([]),
45-
invoke: () => {},
52+
invoke: () => { },
4653
} as unknown as IChatAgent;
4754
chatAgentService.registerAgent(agent);
4855

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ import { Autowired, Injectable } from '@opensumi/di';
44
import {
55
CancellationToken,
66
ChatFeatureRegistryToken,
7+
ChatMessageRole,
78
ChatServiceToken,
89
Disposable,
910
Emitter,
10-
IChatContent,
1111
IChatProgress,
1212
IDisposable,
1313
ILogger,
1414
toDisposable,
1515
} from '@opensumi/ide-core-common';
16-
import { ChatMessageRole } from '@opensumi/ide-core-common';
1716
import { IChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
1817

1918
import {
@@ -26,6 +25,8 @@ import {
2625
IChatFollowup,
2726
IChatMessageStructure,
2827
} from '../../common';
28+
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
29+
import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider';
2930
import { IChatFeatureRegistry } from '../types';
3031

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

3738
private defaultAgentId: string | undefined;
3839

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

@@ -45,6 +52,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
4552
@Autowired(ILogger)
4653
logger: ILogger;
4754

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

@@ -54,6 +67,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
5467
constructor() {
5568
super();
5669
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+
}));
5776
}
5877

5978
registerAgent(agent: IChatAgent): IDisposable {
@@ -120,17 +139,37 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
120139
if (!data) {
121140
throw new Error(`No agent with id ${id}`);
122141
}
142+
143+
// system
123144
if (data.agent.metadata.systemPrompt) {
124145
history.unshift({
125146
role: ChatMessageRole.System,
126147
content: data.agent.metadata.systemPrompt,
127148
});
128149
}
129150

151+
// 发送第一条消息时携带初始 context
152+
if (!this.initialUserMessageMap.has(id)) {
153+
this.initialUserMessageMap.set(id, request.message);
154+
const rawMessage = request.message;
155+
request.message = this.provideContextMessage(rawMessage);
156+
}
157+
158+
if (this.shouldUpdateContext) {
159+
request.message = this.provideContextMessage(request.message);
160+
this.shouldUpdateContext = false;
161+
}
162+
130163
const result = await data.agent.invoke(request, progress, history, token);
131164
return result;
132165
}
133166

167+
private provideContextMessage(message: string) {
168+
const context = this.contextService.serialize();
169+
const fullMessage = this.promptProvider.provideContextPrompt(context, message);
170+
return fullMessage;
171+
}
172+
134173
populateChatInput(id: string, message: IChatMessageStructure) {
135174
this.aiChatService.sendMessage({
136175
...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,17 +79,11 @@ 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 chatInputRef = React.useRef<{ setInputValue: (v: string) => void } | null>(null);
93-
const dialogService = useInjectable<IDialogService>(IDialogService);
94-
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
95-
const commandService = useInjectable<CommandService>(CommandService);
9687

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

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

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

518-
const context = contextService.serialize();
519-
const fullMessage = await promptProvider.provideContextPrompt(context, message);
520-
521-
const request = aiChatService.createRequest(fullMessage, agentId!, command);
507+
const request = aiChatService.createRequest(message, agentId!, command);
522508
if (!request) {
523509
return;
524510
}
@@ -667,32 +653,6 @@ export const AIChatView = () => {
667653
};
668654
}, [aiChatService.sessionModel]);
669655

670-
useEventEffect(
671-
mcpServerProxyService.onChangeMCPServers,
672-
() => {
673-
mcpServerProxyService.getAllMCPTools().then((tools) => {
674-
setMcpToolsCount(tools.length);
675-
});
676-
mcpServerProxyService.$getServers().then((servers) => {
677-
setMcpServersCount(servers.length);
678-
});
679-
},
680-
[mcpServerProxyService],
681-
);
682-
683-
const handleShowMCPTools = React.useCallback(async () => {
684-
const tools = await mcpServerProxyService.getAllMCPTools();
685-
dialogService.open({
686-
message: <MCPToolsDialog tools={tools} />,
687-
type: MessageType.Empty,
688-
buttons: ['关闭'],
689-
});
690-
}, [mcpServerProxyService, dialogService]);
691-
692-
const handleShowMCPConfig = React.useCallback(() => {
693-
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
694-
}, [commandService]);
695-
696656
return (
697657
<div id={styles.ai_chat_view}>
698658
<div className={styles.header_container}>
@@ -732,18 +692,6 @@ export const AIChatView = () => {
732692
</Popover>
733693
))}
734694
</div>
735-
<div className={styles.header_operate_right}>
736-
{aiNativeConfigService.capabilities.supportsMCP && (
737-
<>
738-
<div className={styles.tag} onClick={handleShowMCPConfig}>
739-
{`MCP Servers: ${mcpServersCount}`}
740-
</div>
741-
<div className={styles.tag} onClick={handleShowMCPTools}>
742-
{`MCP Tools: ${mcpToolsCount}`}
743-
</div>
744-
</>
745-
)}
746-
</div>
747695
</div>
748696
<ChatInputWrapperRender
749697
onSend={(value, agentId, command) =>
@@ -786,6 +734,8 @@ export function DefaultChatViewHeader({
786734
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
787735
const mcpServerProxyService = useInjectable<MCPServerProxyService>(TokenMCPServerProxyService);
788736
const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
737+
const commandService = useInjectable<CommandService>(CommandService);
738+
789739
const [historyList, setHistoryList] = React.useState<IChatHistoryItem[]>([]);
790740
const [currentTitle, setCurrentTitle] = React.useState<string>('');
791741
const handleNewChat = React.useCallback(() => {
@@ -806,6 +756,10 @@ export function DefaultChatViewHeader({
806756
[aiChatService],
807757
);
808758

759+
const handleShowMCPConfig = React.useCallback(() => {
760+
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
761+
}, [commandService]);
762+
809763
const handleShowMCPTools = React.useCallback(async () => {
810764
const tools = await mcpServerProxyService.getAllMCPTools();
811765
dialogService.open({
@@ -894,23 +848,6 @@ export function DefaultChatViewHeader({
894848
ariaLabel={localize('aiNative.operate.clear.title')}
895849
/>
896850
</Popover>
897-
{aiNativeConfigService.capabilities.supportsMCP && (
898-
<Popover
899-
overlayClassName={styles.popover_icon}
900-
id={'ai-chat-header-tools'}
901-
position={PopoverPosition.left}
902-
title={localize('aiNative.operate.tools.title')}
903-
>
904-
<EnhanceIcon
905-
wrapperClassName={styles.action_btn}
906-
className={getIcon('menubar-tool')}
907-
onClick={handleShowMCPTools}
908-
tabIndex={0}
909-
role='button'
910-
ariaLabel={localize('aiNative.operate.tools.title')}
911-
/>
912-
</Popover>
913-
)}
914851
<Popover
915852
overlayClassName={styles.popover_icon}
916853
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)