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

feat: support edit_file tool #4385

Merged
merged 54 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9bd7924
feat: mcp server client poc
life2015 Jan 21, 2025
65e12be
feat: introduce MCP tools contribution
life2015 Jan 21, 2025
b99e11d
fix: 修复 mcp sdk 引入类型问题
ensorrow Jan 22, 2025
9520f4c
feat: add builtin MCP server
life2015 Jan 22, 2025
c54b20f
fix: mcp types fix
life2015 Jan 22, 2025
83fc2b7
fix: mcp types fix2
life2015 Jan 22, 2025
bafda53
feat: sumi mcp builtin sever
life2015 Jan 23, 2025
301df94
feat: code optimization
life2015 Jan 24, 2025
6d94523
feat: support llm tool call streaming and ui, more mcp tools
life2015 Feb 7, 2025
5de2d5e
feat: enhance language model error handling and streaming
ensorrow Feb 10, 2025
96b3331
feat: mcp tools grouped by clientId, add mcp tools panel
life2015 Feb 10, 2025
c4e719c
feat: add openai compatible api preferences
ensorrow Feb 11, 2025
04e954a
feat: support chat history in language model request
life2015 Feb 11, 2025
fa713fd
feat: add MCP server configuration support via preferences
life2015 Feb 11, 2025
7703231
feat: implement readfile & readdir tools
ensorrow Feb 11, 2025
67c8b2b
fix: tool impl bugs
ensorrow Feb 11, 2025
384c73d
Merge branch 'feat/mcp-server-client-poc-2' of https://github.com/ope…
ensorrow Feb 11, 2025
d56fdce
refactor: use design system variables in ChatToolRender styles
life2015 Feb 11, 2025
e00b72f
refactor: improve logging and revert some unnecessary optimization
life2015 Feb 12, 2025
15e612e
fix: logger not work in node.js
life2015 Feb 12, 2025
e120ad5
fix: mcp tool render fix
life2015 Feb 12, 2025
8aec9b2
feat: add MCP and custom LLM config
life2015 Feb 13, 2025
8ddda06
Merge branch 'main' into feat/mcp-server-client-poc-2
life2015 Feb 13, 2025
830a403
fix: build error fix
life2015 Feb 13, 2025
590818a
fix: lint fix
life2015 Feb 13, 2025
4752a83
fix: lint fix
life2015 Feb 13, 2025
027284e
fix: lint error fix
life2015 Feb 13, 2025
08374f2
feat: format the tool call error message
ensorrow Feb 14, 2025
2a572cc
feat: implement edit-file tool
ensorrow Feb 18, 2025
befdd94
Merge branch 'main' of https://github.com/opensumi/core into feat/edi…
ensorrow Feb 18, 2025
48abd2c
feat: support system prompt & other config passthrough, fix apply
ensorrow Feb 18, 2025
b1220e7
feat: implement apply demo with qwen-turbo
ensorrow Feb 19, 2025
e1d8e7f
feat: implement edit_file tool view
ensorrow Feb 19, 2025
8d1c176
feat: apply status
ensorrow Feb 19, 2025
287b899
feat: cancel all
ensorrow Feb 19, 2025
3664af2
fix: dispose previewer when close
ensorrow Feb 20, 2025
a2c6c36
fix: simplify diff result
ensorrow Feb 20, 2025
e8b21eb
fix: adjust UI styling details in AI native components
ensorrow Feb 20, 2025
f6361ad
fix: fix accept judgement logic
ensorrow Feb 20, 2025
3a732c6
chore: simplify default chat system prompt
ensorrow Feb 20, 2025
315347f
feat: support edit & diagnositc iteration
ensorrow Feb 20, 2025
288307f
Merge branch 'main' into feat/edit-file-tool
Ricbet Feb 20, 2025
4d0eea7
fix: edit tool display
ensorrow Feb 20, 2025
1777600
fix: add key
ensorrow Feb 20, 2025
3135f2d
feat: builtin tool support label
ensorrow Feb 20, 2025
818a29e
fix: cr
ensorrow Feb 20, 2025
c919062
chore: validate
ensorrow Feb 20, 2025
f884f99
refactor: validate schema & transform args before run tool
ensorrow Feb 20, 2025
10f6816
fix: cr
ensorrow Feb 20, 2025
1d3d589
Merge branch 'main' into feat/edit-file-tool
ensorrow Feb 20, 2025
1a6eec1
feat: display instructions before apply
ensorrow Feb 20, 2025
5bdde02
fix: deps & test
ensorrow Feb 20, 2025
6b2b1f1
fix: deps
ensorrow Feb 20, 2025
71e6e70
fix: missing deps
ensorrow Feb 20, 2025
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
26 changes: 17 additions & 9 deletions jest.setup.jsdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,24 @@ Object.defineProperty(window, 'matchMedia', {
Object.defineProperty(window, 'crypto', {
writable: true,
value: {
randomUUID: () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
}),
randomUUID: () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
}),
getRandomValues: (array) => {
if (!(array instanceof Int8Array || array instanceof Uint8Array ||
array instanceof Int16Array || array instanceof Uint16Array ||
array instanceof Int32Array || array instanceof Uint32Array ||
array instanceof Uint8ClampedArray)) {
if (
!(
array instanceof Int8Array ||
array instanceof Uint8Array ||
array instanceof Int16Array ||
array instanceof Uint16Array ||
array instanceof Int32Array ||
array instanceof Uint32Array ||
array instanceof Uint8ClampedArray
)
) {
throw new TypeError('Expected a TypedArray');
}
for (let i = 0; i < array.length; i++) {
Expand Down
4 changes: 4 additions & 0 deletions packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ export class AINativeBrowserContribution
id: AINativeSettingSectionsId.CodeEditsTyping,
localized: 'preference.ai.native.codeEdits.typing',
},
{
id: AINativeSettingSectionsId.SystemPrompt,
localized: 'preference.ai.native.chat.system.prompt',
},
],
});
}
Expand Down
7 changes: 7 additions & 0 deletions packages/ai-native/src/browser/chat/chat-agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ILogger,
toDisposable,
} from '@opensumi/ide-core-common';
import { ChatMessageRole } from '@opensumi/ide-core-common';
import { IChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';

import {
Expand Down Expand Up @@ -119,6 +120,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
if (!data) {
throw new Error(`No agent with id ${id}`);
}
if (data.agent.metadata.systemPrompt) {
history.unshift({
role: ChatMessageRole.System,
content: data.agent.metadata.systemPrompt,
});
}

const result = await data.agent.invoke(request, progress, history, token);
return result;
Expand Down
2 changes: 0 additions & 2 deletions packages/ai-native/src/browser/chat/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export type IChatProgressResponseContent =
| IChatComponent
| IChatToolContent;

@Injectable({ multiple: true })
export class ChatResponseModel extends Disposable {
#responseParts: IChatProgressResponseContent[] = [];
get responseParts() {
Expand Down Expand Up @@ -218,7 +217,6 @@ export class ChatResponseModel extends Disposable {
}
}

@Injectable({ multiple: true })
export class ChatRequestModel implements IChatRequestModel {
#requestId: string;
public get requestId(): string {
Expand Down
14 changes: 12 additions & 2 deletions packages/ai-native/src/browser/chat/chat-proxy.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Autowired, Injectable } from '@opensumi/di';
import { PreferenceService } from '@opensumi/ide-core-browser';
import { AppConfig, PreferenceService } from '@opensumi/ide-core-browser';
import {
AIBackSerivcePath,
CancellationToken,
Expand Down Expand Up @@ -71,6 +71,9 @@ export class ChatProxyService extends Disposable {
@Autowired(IMessageService)
private readonly messageService: IMessageService;

@Autowired(AppConfig)
private readonly appConfig: AppConfig;

private chatDeferred: Deferred<void> = new Deferred<void>();

public registerDefaultAgent() {
Expand All @@ -83,7 +86,14 @@ export class ChatProxyService extends Disposable {
this.addDispose(
this.chatAgentService.registerAgent({
id: ChatProxyService.AGENT_ID,
metadata: {},
metadata: {
systemPrompt:
this.preferenceService.get<string>(
AINativeSettingSectionsId.SystemPrompt,
'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.',
) +
`\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>`,
},
invoke: async (
request: IChatAgentRequest,
progress: (part: IChatProgress) => void,
Expand Down
13 changes: 12 additions & 1 deletion packages/ai-native/src/browser/chat/chat.internal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export class ChatInternalService extends Disposable {
private readonly _onChangeSession = new Emitter<string>();
public readonly onChangeSession: Event<string> = this._onChangeSession.event;

private readonly _onCancelRequest = new Emitter<void>();
public readonly onCancelRequest: Event<void> = this._onCancelRequest.event;

private readonly _onRegenerateRequest = new Emitter<void>();
public readonly onRegenerateRequest: Event<void> = this._onRegenerateRequest.event;

private _latestRequestId: string;
public get latestRequestId(): string {
return this._latestRequestId;
Expand All @@ -52,11 +58,16 @@ export class ChatInternalService extends Disposable {
}

sendRequest(request: ChatRequestModel, regenerate = false) {
return this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
const result = this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
if (regenerate) {
this._onRegenerateRequest.fire();
}
return result;
}

cancelRequest() {
this.chatManagerService.cancelRequest(this.#sessionModel.sessionId);
this._onCancelRequest.fire();
}

clearSessionModel() {
Expand Down
23 changes: 13 additions & 10 deletions packages/ai-native/src/browser/components/ChatEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ interface Props {
language?: string;
agentId?: string;
command?: string;
hideInsert?: boolean;
}
export const CodeEditorWithHighlight = (props: Props) => {
const { input, language, relationId, agentId, command } = props;
const { input, language, relationId, agentId, command, hideInsert } = props;
const ref = React.useRef<HTMLDivElement | null>(null);
const monacoCommandRegistry = useInjectable<MonacoCommandRegistry>(MonacoCommandRegistry);
const clipboardService = useInjectable<IClipboardService>(IClipboardService);
Expand Down Expand Up @@ -101,15 +102,17 @@ export const CodeEditorWithHighlight = (props: Props) => {
return (
<div className={styles.monaco_wrapper}>
<div className={styles.action_toolbar}>
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
<EnhanceIcon
className={getIcon('insert')}
onClick={() => handleInsert()}
tabIndex={0}
role='button'
ariaLabel={localize('aiNative.chat.code.insert')}
/>
</Popover>
{!hideInsert && (
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
<EnhanceIcon
className={getIcon('insert')}
onClick={() => handleInsert()}
tabIndex={0}
role='button'
ariaLabel={localize('aiNative.chat.code.insert')}
/>
</Popover>
)}
<Popover
id={`ai-chat-copy-${useUUID}`}
title={localize(isCoping ? 'aiNative.chat.code.copy.success' : 'aiNative.chat.code.copy')}
Expand Down
4 changes: 3 additions & 1 deletion packages/ai-native/src/browser/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface MarkdownProps {
className?: string;
fillInIncompleteTokens?: boolean; // 补齐不完整的 token,如代码块或表格
markedOptions?: IMarkedOptions;
hideInsert?: boolean;
}

export const ChatMarkdown = (props: MarkdownProps) => {
Expand All @@ -42,13 +43,14 @@ export const ChatMarkdown = (props: MarkdownProps) => {
<div className={styles.code}>
<ConfigProvider value={appConfig}>
<div className={styles.code_block}>
<div className={styles.code_language}>{language}</div>
<div className={cls(styles.code_language, 'langauge-badge')}>{language}</div>
<CodeEditorWithHighlight
input={code as string}
language={language}
relationId={props.relationId || ''}
agentId={props.agentId}
command={props.command}
hideInsert={props.hideInsert}
/>
</div>
</ConfigProvider>
Expand Down
23 changes: 8 additions & 15 deletions packages/ai-native/src/browser/components/ChatReply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,16 @@ const TreeRenderer = (props: { treeData: IChatResponseProgressFileTreeData }) =>
);
};

const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
const { toolCall } = props;
const ToolCallRender = (props: { toolCall: IChatToolContent['content']; messageId?: string }) => {
const { toolCall, messageId } = props;
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
const [node, setNode] = useState<React.JSX.Element | null>(null);

useEffect(() => {
const config = chatAgentViewService.getChatComponent('toolCall');
if (config) {
const { component: Component, initialProps } = config;
setNode(<Component {...initialProps} value={toolCall} />);
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
return;
}
setNode(
Expand All @@ -169,22 +169,22 @@ const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
);
const deferred = chatAgentViewService.getChatComponentDeferred('toolCall')!;
deferred.promise.then(({ component: Component, initialProps }) => {
setNode(<Component {...initialProps} value={toolCall} />);
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
});
}, [toolCall.state]);

return node;
};

const ComponentRender = (props: { component: string; value?: unknown }) => {
const ComponentRender = (props: { component: string; value?: unknown; messageId?: string }) => {
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
const [node, setNode] = useState<React.JSX.Element | null>(null);

useEffect(() => {
const config = chatAgentViewService.getChatComponent(props.component);
if (config) {
const { component: Component, initialProps } = config;
setNode(<Component {...initialProps} value={props.value} />);
setNode(<Component {...initialProps} value={props.value} messageId={props.messageId} />);
return;
}
setNode(
Expand Down Expand Up @@ -224,7 +224,6 @@ export const ChatReply = (props: IChatReplyProps) => {
const chatApiService = useInjectable<ChatService>(ChatServiceToken);
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);

useEffect(() => {
const disposableCollection = new DisposableCollection();

Expand Down Expand Up @@ -298,12 +297,6 @@ export const ChatReply = (props: IChatReplyProps) => {
</div>
);

const renderComponent = (componentId: string, value: unknown) => (
<ComponentRender component={componentId} value={value} />
);

const renderToolCall = (toolCall: IChatToolContent['content']) => <ToolCallRender toolCall={toolCall} />;

const contentNode = React.useMemo(
() =>
request.response.responseContents.map((item, index) => {
Expand All @@ -313,9 +306,9 @@ export const ChatReply = (props: IChatReplyProps) => {
} else if (item.kind === 'treeData') {
node = renderTreeData(item.treeData);
} else if (item.kind === 'component') {
node = renderComponent(item.component, item.value);
node = <ComponentRender component={item.component} value={item.value} messageId={msgId} />;
} else if (item.kind === 'toolCall') {
node = renderToolCall(item.content);
node = <ToolCallRender toolCall={item.content} messageId={msgId} />;
} else {
node = renderMarkdown(item.content);
}
Expand Down
61 changes: 41 additions & 20 deletions packages/ai-native/src/browser/components/ChatToolRender.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import cls from 'classnames';
import React, { useState } from 'react';

import { useInjectable } from '@opensumi/ide-core-browser';
import { Icon } from '@opensumi/ide-core-browser/lib/components';
import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { IChatToolContent, uuid } from '@opensumi/ide-core-common';

import { IMCPServerRegistry, TokenMCPServerRegistry } from '../types';

import { CodeEditorWithHighlight } from './ChatEditor';
import styles from './ChatToolRender.module.less';

export const ChatToolRender = (props: { value: IChatToolContent['content'] }) => {
const { value } = props;
export const ChatToolRender = (props: { value: IChatToolContent['content']; messageId?: string }) => {
const { value, messageId } = props;
const [isExpanded, setIsExpanded] = useState(false);
const mcpServerFeatureRegistry = useInjectable<IMCPServerRegistry>(TokenMCPServerRegistry);

if (!value || !value.function || !value.id) {
return null;
}
const label = mcpServerFeatureRegistry.getMCPTool(value.function.name)?.label || value.function.name;

const ToolComponent = mcpServerFeatureRegistry.getToolComponent(value.function.name);

const getStateInfo = (state?: string): { label: string; icon: React.ReactNode } => {
switch (state) {
case 'streaming-start':
case 'streaming':
return { label: 'Generating', icon: <Loading /> };
case 'complete':
return { label: 'Complete', icon: <Icon iconClass="codicon codicon-check" /> };
return { label: 'Complete', icon: <Icon iconClass='codicon codicon-check' /> };
case 'result':
return { label: 'Result Ready', icon: <Icon iconClass="codicon codicon-check-all" /> };
return { label: 'Result Ready', icon: <Icon iconClass='codicon codicon-check-all' /> };
default:
return { label: state || 'Unknown', icon: <Icon iconClass="codicon codicon-question" /> };
return { label: state || 'Unknown', icon: <Icon iconClass='codicon codicon-question' /> };
}
};
const getParsedArgs = () => {
try {
// TODO: 流式输出中function_call的参数还不完整,需要等待complete状态
if (value.state !== 'complete' && value.state !== 'result') {
return {};
}
return JSON.parse(value.function?.arguments || '{}');
} catch (error) {
return {};
}
};

Expand All @@ -36,12 +54,12 @@ export const ChatToolRender = (props: { value: IChatToolContent['content'] }) =>

const stateInfo = getStateInfo(value.state);

return (
<div className={styles['chat-tool-render']}>
return [
<div className={styles['chat-tool-render']} key='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>
{value?.function?.name}
{label}
</div>
{value.state && (
<div className={styles['tool-state']}>
Expand All @@ -54,24 +72,27 @@ export const ChatToolRender = (props: { value: IChatToolContent['content'] }) =>
{value?.function?.arguments && (
<div className={styles['tool-arguments']}>
<div className={styles['section-label']}>Arguments</div>
<CodeEditorWithHighlight
input={value?.function?.arguments}
language={'json'}
relationId={uuid(4)}
/>
<CodeEditorWithHighlight input={value?.function?.arguments} language={'json'} relationId={uuid(4)} />
</div>
)}
{value?.result && (
<div className={styles['tool-result']}>
<div className={styles['section-label']}>Result</div>
<CodeEditorWithHighlight
input={value.result}
language={'json'}
relationId={uuid(4)}
/>
<CodeEditorWithHighlight input={value.result} language={'json'} relationId={uuid(4)} />
</div>
)}
</div>
</div>
);
</div>,
ToolComponent && (
<ToolComponent
key='tool-component'
state={value.state}
args={getParsedArgs()}
result={value.result}
index={value.index}
messageId={messageId}
toolCallId={value.id}
/>
),
];
};
Loading
Loading