Merge pull request #128 from devchat-ai/auto_apply_fix_issue

feat: Enhance issue fixing and add aider integration
This commit is contained in:
kagami 2024-07-19 04:42:19 +00:00 committed by GitHub
commit a4005b6326
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1213 additions and 37 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ custom/*
!custom/config.yml.example
user_settings.yml
.aider*

View File

@ -117,7 +117,7 @@ class IDEService:
return self._result
@rpc_method
def diff_apply(self, filepath, content) -> bool:
def diff_apply(self, filepath, content, autoedit: bool = False) -> bool:
"""
Applies a given diff to a file.
@ -182,3 +182,25 @@ class IDEService:
The extension tools path.
"""
return self._result
@rpc_method
def select_range(
self, fileName: str, startLine: int, startColumn: int, endLine: int, endColumn: int
) -> bool:
"""
Selects a range of text in the specified file.
Args:
fileName: The name of the file.
startLine: The starting line of the selection (0-based).
startColumn: The starting column of the selection (0-based).
endLine: The ending line of the selection (0-based).
endColumn: The ending column of the selection (0-based).
Returns:
A boolean indicating whether the selection was successful.
Note:
If startLine is -1, it cancels the current selection.
"""
return self._result

View File

@ -10,12 +10,7 @@ def run_code(code: str):
@rpc_call
def diff_apply(filepath, content):
pass
@rpc_call
def get_symbol_defines_in_selected_code():
def diff_apply(filepath, content, autoedit=False):
pass

19
merico/aider/README.md Normal file
View File

@ -0,0 +1,19 @@
### 操作指南
aider工作流命令使用步骤如下
1. 确保已经使用 `/aider.files.add` 命令添加了需要处理的文件。
2. 输入 `/aider <message>` 命令,其中 `<message>` 是你想要aider执行的任务描述。
3. 等待aider生成建议的更改。
4. 系统会自动显示每个文件的Diff View你可以选择是否接受修改。
5. 对于多个文件的更改,系统会在每个文件之后询问是否继续查看下一个文件的更改。
注意事项:
- 如果没有添加任何文件到aider命令将会提示你先使用 'aider.files.add' 命令添加文件。
- 你可以使用 `aider.files.remove` 命令从aider中移除文件。
- 所有的更改都会在IDE中以Diff View的形式展示你可以在查看后决定是否应用这些更改。
使用示例:
/aider 重构这段代码以提高性能
这个命令会让aider分析当前添加的文件并提供重构建议以提高代码性能。

214
merico/aider/command.py Normal file
View File

@ -0,0 +1,214 @@
import json
import os
import subprocess
import sys
from devchat.ide import IDEService
from lib.chatmark import Button
GLOBAL_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".chat", ".workflow_config.json")
def save_config(config_path, item, value):
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
else:
config = {}
config[item] = value
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4)
def write_python_path_to_config():
"""
Write the current system Python path to the configuration.
"""
python_path = sys.executable
save_config(GLOBAL_CONFIG_PATH, "aider_python", python_path)
print(f"Python path '{python_path}' has been written to the configuration.")
def get_aider_files():
"""
.chat/.aider_files文件中读取aider文件列表
"""
aider_files_path = os.path.join(".chat", ".aider_files")
if not os.path.exists(aider_files_path):
return []
with open(aider_files_path, "r") as f:
return [line.strip() for line in f if line.strip()]
def run_aider(message, files):
"""
运行aider命令
"""
python = sys.executable
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
cmd = [
python,
"-m",
"aider",
"--model",
f"openai/{model}",
"--yes",
"--no-auto-commits",
"--dry-run",
"--no-pretty",
"--message",
message,
] + files
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
has_started = False
aider_output = ""
for line in process.stdout:
if "run with --help" in line or 'run "aider --help"' in line:
has_started = True
continue
if has_started:
aider_output += line
print(line, end="", flush=True)
return_code = process.wait()
if return_code != 0:
for line in process.stderr:
print(f"Error: {line.strip()}", file=sys.stderr)
sys.exit(return_code)
return aider_output
def apply_changes(changes, files):
"""
应用aider生成的更改
"""
changes_file = ".chat/changes.txt"
os.makedirs(os.path.dirname(changes_file), exist_ok=True)
with open(changes_file, "w") as f:
f.write(changes)
python = sys.executable
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
cmd = [
python,
"-m",
"aider",
"--model",
f"openai/{model}",
"--yes",
"--no-auto-commits",
"--apply",
changes_file,
] + files
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
has_started = False
for line in process.stdout:
if "Model:" in line:
has_started = True
continue
if has_started:
print(line, end="", flush=True)
return_code = process.wait()
if return_code != 0:
for line in process.stderr:
print(f"Error: {line.strip()}", file=sys.stderr)
sys.exit(return_code)
os.remove(changes_file)
def main():
"""
Main function to run the aider command.
This function performs the following tasks:
1. Checks for correct command-line usage
2. Writes the current Python path to the configuration
3. Retrieves the list of files to be processed
4. Runs the aider command with the given message
5. Applies the suggested changes
6. Displays the differences in the IDE
Usage: python command.py <message>
"""
if len(sys.argv) < 2:
print("Usage: python command.py <message>", file=sys.stderr)
sys.exit(1)
write_python_path_to_config()
message = sys.argv[1]
files = get_aider_files()
if not files:
print(
"No files added to aider. Please add files using 'aider.files.add' command.",
file=sys.stderr,
)
sys.exit(1)
print("Running aider...\n", flush=True)
changes = run_aider(message, files)
if not changes:
print("No changes suggested by aider.")
sys.exit(0)
print("\nApplying changes...\n", flush=True)
# 保存原始文件内容
original_contents = {}
for file in files:
with open(file, "r") as f:
original_contents[file] = f.read()
# 应用更改
apply_changes(changes, files)
# 读取更新后的文件内容
updated_contents = {}
for file in files:
with open(file, "r") as f:
updated_contents[file] = f.read()
# 还原原始文件内容
for file in files:
with open(file, "w") as f:
f.write(original_contents[file])
# 使用 IDEService 展示差异
ide_service = IDEService()
for index, file in enumerate(files):
ide_service.diff_apply(file, updated_contents[file])
if index < len(files) - 1:
# 等待用户确认
button = Button(
["Show Next Changes", "Cancel"],
)
button.render()
idx = button.clicked
print("click button:", idx)
if idx == 0:
continue
else:
break
print("Changes have been displayed in the IDE.")
if __name__ == "__main__":
main()

9
merico/aider/command.yml Normal file
View File

@ -0,0 +1,9 @@
description: "aider command"
workflow_python:
env_name: devchat-aider-env
version: 3.11.0
dependencies: requirements.txt
input: required
help: README.md
steps:
- run: $workflow_python $command_path/command.py "$input"

View File

@ -0,0 +1,20 @@
### aider.files.add
添加文件到aider处理列表中。
用法:
/aider.files.add <file_path>
参数:
- <file_path>: 要添加的文件路径(必需)
描述:
这个命令将指定的文件添加到aider的处理列表中。添加后该文件将被包含在后续的aider操作中。
注意:
- 文件路径必须是有效的格式。
- 如果文件已经在列表中,它不会被重复添加。
- 添加成功后会显示当前aider文件列表。
示例:
/aider.files.add src/main.py

View File

@ -0,0 +1,66 @@
import os
import sys
def is_valid_path(path):
"""
检查路径是否为有效的文件路径形式
"""
try:
# 尝试规范化路径
normalized_path = os.path.normpath(path)
# 检查路径是否是绝对路径或相对路径
return (
os.path.isabs(normalized_path)
or not os.path.dirname(normalized_path) == normalized_path
)
except Exception:
return False
def add_file(file_path):
# 1. 检查是否为有效的文件路径形式
if not is_valid_path(file_path):
print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr)
sys.exit(1)
# 获取绝对路径
abs_file_path = file_path.strip()
# 2. 将新增文件路径存储到.chat/.aider_files文件中
aider_files_path = os.path.join(".chat", ".aider_files")
# 确保.chat目录存在
os.makedirs(os.path.dirname(aider_files_path), exist_ok=True)
# 读取现有文件列表
existing_files = set()
if os.path.exists(aider_files_path):
with open(aider_files_path, "r") as f:
existing_files = set(line.strip() for line in f)
# 添加新文件
existing_files.add(abs_file_path)
# 写入更新后的文件列表
with open(aider_files_path, "w") as f:
for file in sorted(existing_files):
f.write(f"{file}\n")
print(f"Added '{abs_file_path}' to aider files.")
print("\nCurrent aider files:")
for file in sorted(existing_files):
print(f"- {file}")
def main():
if len(sys.argv) != 2 or sys.argv[1].strip() == "":
print("Usage: /aider.files.add <file_path>", file=sys.stderr)
sys.exit(1)
file_path = sys.argv[1]
add_file(file_path)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
description: "add files to aider"
input: required
help: README.md
steps:
- run: $devchat_python $command_path/command.py "$input"

View File

@ -0,0 +1,16 @@
### aider.files.list
列出当前在aider处理列表中的所有文件。
用法:
/aider.files.list
描述:
这个命令会显示所有已添加到aider处理列表中的文件。它提供了一个当前aider正在处理的文件的概览。
注意:
- 如果没有文件被添加到aider会显示相应的消息。
- 文件按字母顺序排序显示。
示例:
/aider.files.list

View File

@ -0,0 +1,31 @@
import os
import sys
def list_files():
aider_files_path = os.path.join(".chat", ".aider_files")
# 确保.chat/.aider_files文件存在
if not os.path.exists(aider_files_path):
print("No files have been added to aider yet.")
sys.exit(0)
# 读取文件列表
with open(aider_files_path, "r") as f:
files = [line.strip() for line in f]
# 打印文件列表
if files:
print("Aider files:")
for file in sorted(files):
print(f"- {file}")
else:
print("No files found in aider.")
def main():
list_files()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,4 @@
description: "list files in aider"
help: README.md
steps:
- run: $devchat_python $command_path/command.py

View File

@ -0,0 +1,20 @@
### aider.files.remove
从aider处理列表中移除指定的文件。
用法:
/aider.files.remove <file_path>
参数:
- <file_path>: 要移除的文件路径(必需)
描述:
这个命令从aider的处理列表中移除指定的文件。移除后该文件将不再被包含在后续的aider操作中。
注意:
- 文件路径必须是有效的格式。
- 如果指定的文件不在列表中,会显示相应的消息。
- 移除成功后会显示更新后的aider文件列表。
示例:
/aider.files.remove src/main.py

View File

@ -0,0 +1,72 @@
import os
import sys
def is_valid_path(path):
"""
检查路径是否为有效的文件路径形式
"""
try:
# 尝试规范化路径
normalized_path = os.path.normpath(path)
# 检查路径是否是绝对路径或相对路径
return (
os.path.isabs(normalized_path)
or not os.path.dirname(normalized_path) == normalized_path
)
except Exception:
return False
def remove_file(file_path):
# 1. 检查是否为有效的文件路径形式
if not is_valid_path(file_path):
print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr)
sys.exit(1)
# 获取绝对路径
abs_file_path = file_path.strip()
# 2. 从.chat/.aider_files文件中移除指定文件路径
aider_files_path = os.path.join(".chat", ".aider_files")
# 确保.chat目录存在
if not os.path.exists(aider_files_path):
print(f"Error: '{aider_files_path}' does not exist.", file=sys.stderr)
sys.exit(1)
# 读取现有文件列表
existing_files = set()
with open(aider_files_path, "r") as f:
existing_files = set(line.strip() for line in f)
# 检查文件是否在列表中
if abs_file_path not in existing_files:
print(f"'{abs_file_path}' is not in aider files.")
sys.exit(0)
# 移除文件
existing_files.remove(abs_file_path)
# 写入更新后的文件列表
with open(aider_files_path, "w") as f:
for file in sorted(existing_files):
f.write(f"{file}\n")
print(f"Removed '{abs_file_path}' from aider files.")
print("\nCurrent aider files:")
for file in sorted(existing_files):
print(f"- {file}")
def main():
if len(sys.argv) != 2 or sys.argv[1].strip() == "":
print("Usage: /aider.files.remove <file_path>", file=sys.stderr)
sys.exit(1)
file_path = sys.argv[1]
remove_file(file_path)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
description: "remove files from aider"
input: required
help: README.md
steps:
- run: $devchat_python $command_path/command.py "$input"

View File

@ -0,0 +1,2 @@
git+https://gitee.com/imlaji/aider.git@main
git+https://gitee.com/devchat-ai/devchat.git@aider

View File

@ -0,0 +1,21 @@
### ask_issue
自动修复代码中的lint错误。
用法:
/ask_issue
描述:
这个命令帮助开发者自动修复代码中的lint错误。它使用AI分析选中的代码行识别lint问题并提供修复建议。
步骤:
1. 在IDE中选择包含lint错误的代码行。
2. 运行 /ask_issue 命令。
3. 命令会自动获取选中的代码、相关的lint诊断信息并调用AI生成修复方案。
4. AI会提供问题解释和修复后的代码片段。
注意事项:
- 确保在运行命令前已选择包含lint错误的代码行。
- 命令会优先处理SonarLint诊断的问题。
- AI生成的修复方案会包含问题解释和修改后的代码。
- 修改后的代码会以Markdown格式展示包含足够的上下文信息。

View File

@ -0,0 +1,4 @@
description: Automatically fix lint errors.
help: README.md
steps:
- run: $devchat_python $command_path/main.py

286
merico/ask_issue/main.py Normal file
View File

@ -0,0 +1,286 @@
import os
import re
import sys
from devchat.llm import chat
from devchat.memory import FixSizeChatMemory
from lib.ide_service import IDEService
def extract_edits_block(text):
"""
Extracts the first Markdown code block from the given text without the language specifier.
:param text: A string containing Markdown text
:return: The content of the first Markdown code block, or None if not found
"""
index = text.find("```edits")
if index == -1:
return None
else:
start = index + len("```edits")
end = text.find("```", start)
if end == -1:
return None
else:
return text[start:end]
def extract_markdown_block(text):
"""
Extracts the first Markdown code block from the given text without the language specifier.
:param text: A string containing Markdown text
:return: The content of the first Markdown code block, or None if not found
"""
edit_code = extract_edits_block(text)
if edit_code:
return edit_code
pattern = r"```(?:\w+)?\s*\n(.*?)\n```"
match = re.search(pattern, text, re.DOTALL)
if match:
block_content = match.group(1)
return block_content
else:
# whether exist ```language?
if text.find("```"):
return None
return text
# step 1 : get selected code
def get_selected_code():
selected_data = IDEService().get_selected_range().dict()
if selected_data["range"]["start"] == -1:
return None, None, None
if selected_data["range"]["start"]["line"] != selected_data["range"]["end"]["line"]:
print("Please select the line code of issue reported.\n\n", file=sys.stderr)
sys.exit(1)
return selected_data["abspath"], selected_data["text"], selected_data["range"]["start"]["line"]
# step 2 : input issue descriptions
def input_issue_descriptions(file_path, issue_line_num):
diagnostics = IDEService().get_diagnostics_in_range(file_path, issue_line_num, issue_line_num)
if not diagnostics:
return None
# select first sonarlint diagnostic
for diagnostic in diagnostics:
if diagnostic.find("<sonar") > 0:
return diagnostic
return diagnostics[0]
# step 3 : call llm to generate fix solutions
SYSTEM_ROLE_DIFF = """
You are a code refactoring assistant.
Your task is to refactor the user's code to fix lint diagnostics.
You will be provided with a code snippet and a list of diagnostics. \
Your response should include two parts:
1. An explanation of the reason for the diagnostics and how to fix them.
2. The edited code snippet with the diagnostics fixed, using markdown format for clarity.
The markdown block for edits should look like this:
```edits
def hello():
print("Call hello():")
+ print("hello")
...
- hello(20)
+ hello()
```
Or like this, if a variable is not defined:
```edits
...
+ cur_file = __file__
print(cur_file)
```
Please note the following important points:
1. The new code should maintain the correct indentation. \
The "+ " sign is followed by two spaces for indentation, \
which should be included in the edited code.
2. In addition to outputting key editing information, \
sufficient context (i.e., key information before and after editing) \
should also be provided to help locate the specific position of the edited line.
3. Don't output all file lines, if some lines are unchanged, \
please use "..." to indicate the ignored lines.
4. Use "+ " and "- " at start of the line to indicate the addition and deletion of lines.
Here are some examples of incorrect responses:
Incorrect example 1, where the indentation is not correct:
```edits
def hello():
print("Call hello():")
+ print("hello")
```
In this case, if the "+ " sign and the extra space are removed, \
the print("hello") statement will lack the necessary two spaces for correct indentation.
Incorrect example 2, where no other code lines are provided:
```edits
+ print("hello")
```
This is an incorrect example because without additional context, \
it's unclear where the new print("hello") statement should be inserted.
"""
SYSTEM_ROLE_CODEBLOCK = """
你是一个重构工程师你需要根据错误描述对代码进行问题修正只需要关注描述的问题不需要关注代码中的其他问题
输出的修正代码中如果修改了多个代码段中间没有修改的代码段请使用...表示
每一个被修改的代码段应该包含前后至少3行未修改的代码作为修改代码段的边界表示
输出一个代码块中例如
```edits
def hello():
msg = "hello"
print(msg)
...
if __name__ == "__main__":
hello()
```
"""
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
if LLM_MODEL in [
"qwen2-72b-instruct",
"qwen-long",
"qwen-turbo",
"Yi-34B-Chat",
"deepseek-coder",
"xinghuo-3.5",
]:
SYSTEM_ROLE = SYSTEM_ROLE_CODEBLOCK
else:
SYSTEM_ROLE = SYSTEM_ROLE_DIFF
MESSAGES_A = [
{
"role": "system",
"content": SYSTEM_ROLE,
},
]
# step 3 : call llm to generate fix solutions
PROMPT = """
Here is the code file:
{file_content}
There is an issue in the following code:
{issue_line_code}
{issue_description}
Here is the rule description:
{rule_description}
Please focus only on the error described in the prompt. \
Other errors in the code should be disregarded.
"""
memory = FixSizeChatMemory(max_size=20, messages=MESSAGES_A)
@chat(prompt=PROMPT, stream_out=True, memory=memory)
def call_llm_to_generate_fix_solutions(
file_content, issue_line_code, issue_description, rule_description
):
pass
# current file content
def get_current_file_content(file_path, issue_line_num):
try:
return IDEService().get_collapsed_code(file_path, issue_line_num, issue_line_num)
except Exception:
print("Error reading file:", file=sys.stderr)
return None
# get issue description
def get_rule_description(issue_description):
def parse_source_code(text):
pattern = r"<(\w+):(.+?)>"
match = re.search(pattern, text)
if match:
source = match.group(1)
code = match.group(2)
return source, code
else:
return None, None
issue_source, issue_code = parse_source_code(issue_description)
if issue_source.find("sonar") == -1:
return issue_description
issue_id = issue_code.split(":")[-1]
issue_language = issue_code.split(":")[0]
tools_path = IDEService().get_extension_tools_path()
rules_path = "sonar-rspec"
rule_path = os.path.join(tools_path, rules_path, "rules", issue_id, issue_language, "rule.adoc")
if os.path.exists(rule_path):
with open(rule_path, "r", encoding="utf-8") as file:
return file.read()
return issue_description
def main():
print("start fix issue ...\n\n")
file_path, issue_line, issue_line_num = get_selected_code()
if not file_path or not issue_line:
print("No code selected. Please select the code line you want to fix.", file=sys.stderr)
sys.exit(1)
issue_description = input_issue_descriptions(file_path, issue_line_num)
if not issue_description:
print(
"There are no issues to resolve on the current line. "
"Please select the line where an issue needs to be resolved."
)
sys.exit(0)
print("make llm prompt ...\n\n")
current_file_content = get_current_file_content(file_path, issue_line_num)
rule_description = get_rule_description(issue_description)
# print("Rule description:\n\n", rule_description, end="\n\n")
print("call llm to fix issue ...\n\n")
fix_solutions = call_llm_to_generate_fix_solutions(
file_content=current_file_content,
issue_line_code=issue_line,
issue_description=issue_description,
rule_description=rule_description,
)
if not fix_solutions:
sys.exit(1)
print("\n\n", flush=True)
if __name__ == "__main__":
main()

View File

@ -1,3 +1,23 @@
### fix_issue
### 操作指南
自动修复代码中的lint错误。
用法:
/fix_issue
描述:
这个命令帮助开发者自动修复代码中的lint错误。它使用AI分析选中的代码行识别lint问题并提供修复建议。然后它会自动应用这些修复建议并在IDE中显示更改。
步骤:
1. 在IDE中选择包含lint错误的代码行。
2. 运行 /fix_issue 命令。
3. 命令会自动获取选中的代码、相关的lint诊断信息并调用AI生成修复方案。
4. AI会提供问题解释和修复后的代码。
5. 命令会自动应用这些修复并在IDE中显示更改。
注意事项:
- 确保在运行命令前已选择包含lint错误的代码行。
- 命令会优先处理SonarLint诊断的问题。
- 如果安装了aider Python命令会使用aider来执行AI访问和应用更改。
- 如果没有安装aider Python命令会使用默认实现来生成和应用修复。
- 所有的更改都会在IDE中以Diff View的形式展示你可以在查看后决定是否接受这些更改。

View File

@ -1,12 +1,34 @@
import json
import os
import re
import subprocess
import sys
from devchat.llm import chat
from devchat.memory import FixSizeChatMemory
from lib.ide_service import IDEService
def extract_edits_block(text):
"""
Extracts the first Markdown code block from the given text without the language specifier.
:param text: A string containing Markdown text
:return: The content of the first Markdown code block, or None if not found
"""
index = text.find("```edits")
if index == -1:
return None
else:
start = index + len("```edits")
end = text.find("```", start)
if end == -1:
return None
else:
return text[start:end]
def extract_markdown_block(text):
"""
Extracts the first Markdown code block from the given text without the language specifier.
@ -14,6 +36,10 @@ def extract_markdown_block(text):
:param text: A string containing Markdown text
:return: The content of the first Markdown code block, or None if not found
"""
edit_code = extract_edits_block(text)
if edit_code:
return edit_code
pattern = r"```(?:\w+)?\s*\n(.*?)\n```"
match = re.search(pattern, text, re.DOTALL)
@ -55,29 +81,164 @@ def input_issue_descriptions(file_path, issue_line_num):
# step 3 : call llm to generate fix solutions
PROMPT = """
You are a code refactoring assistant.
This is my code file:
{file_content}
SYSTEM_ROLE_DIFF = """
You are a code refactoring assistant. \
Your task is to refactor the user's code to fix lint diagnostics. \
You will be provided with a code snippet and a list of diagnostics. \
Your response should include two parts:
There is a issue in the following code:
{issue_line_code}
{issue_description}
1. An explanation of the reason for the diagnostics and how to fix them.
2. The edited code snippet with the diagnostics fixed, using markdown format for clarity.
Here is the rule description:
{rule_description}
The markdown block for edits should look like this:
Please provide me refactor code to fix this issue.
```edits
def hello():
print("Call hello():")
+ print("hello")
...
- hello(20)
+ hello()
```
Or like this, if a variable is not defined:
```edits
...
+ cur_file = __file__
print(cur_file)
```
Please note the following important points:
1. The new code should maintain the correct indentation. \
The "+ " sign is followed by two spaces for indentation, \
which should be included in the edited code.
2. In addition to outputting key editing information, \
sufficient context (i.e., key information before and after editing) \
should also be provided to help locate the specific position of the edited line.
3. Don't output all file lines, if some lines are unchanged, \
please use "..." to indicate the ignored lines.
4. Use "+ " and "- " at start of the line to indicate the addition and deletion of lines.
Here are some examples of incorrect responses:
Incorrect example 1, where the indentation is not correct:
```edits
def hello():
print("Call hello():")
+ print("hello")
```
In this case, if the "+ " sign and the extra space are removed, \
the print("hello") statement will lack the necessary two spaces for correct indentation.
Incorrect example 2, where no other code lines are provided:
```edits
+ print("hello")
```
This is an incorrect example because without additional context, \
it's unclear where the new print("hello") statement should be inserted.
"""
SYSTEM_ROLE_CODEBLOCK = """
你是一个重构工程师你需要根据错误描述对代码进行问题修正只需要关注描述的问题不需要关注代码中的其他问题
输出的修正代码中如果修改了多个代码段中间没有修改的代码段请使用...表示
每一个被修改的代码段应该包含前后至少3行未修改的代码作为修改代码段的边界表示
输出一个代码块中例如
```edits
def hello():
msg = "hello"
print(msg)
...
if __name__ == "__main__":
hello()
```
"""
@chat(prompt=PROMPT, stream_out=True)
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
if LLM_MODEL in [
"qwen2-72b-instruct",
"qwen-long",
"qwen-turbo",
"Yi-34B-Chat",
"deepseek-coder",
"xinghuo-3.5",
]:
SYSTEM_ROLE = SYSTEM_ROLE_CODEBLOCK
else:
SYSTEM_ROLE = SYSTEM_ROLE_DIFF
MESSAGES_A = [
{
"role": "system",
"content": SYSTEM_ROLE,
},
]
# step 3 : call llm to generate fix solutions
PROMPT = """
Here is the code file:
{file_content}
There is an issue in the following code:
{issue_line_code}
{issue_description}
Here is the rule description:
{rule_description}
Please focus only on the error described in the prompt. \
Other errors in the code should be disregarded.
"""
memory = FixSizeChatMemory(max_size=20, messages=MESSAGES_A)
@chat(prompt=PROMPT, stream_out=True, memory=memory)
def call_llm_to_generate_fix_solutions(
file_content, issue_line_code, issue_description, rule_description
):
pass
APPLY_SYSTEM_PROMPT = """
Your task is apply the fix solution to the code, \
output the whole new code in markdown code block format.
Here is the code file:
{file_content}
Here is the fix solution:
{fix_solution}
Some rules for output code:
1. Focus on the fix solution, don't focus on other errors in the code.
2. Don't change the indentation of the code.
3. Don't change lines which are not metioned in fix solution, for example, \
don't remove empty lines in code.
Please output only the whole new code which is the result of \
applying the fix solution, and output the whole code.
"""
@chat(prompt=APPLY_SYSTEM_PROMPT, stream_out=True, model="deepseek-coder")
def apply_fix_solution(file_content, fix_solution):
pass
# current file content
def get_current_file_content(file_path, issue_line_num):
try:
@ -117,18 +278,150 @@ def get_rule_description(issue_description):
return issue_description
# step 4 : apply fix solutions to code
def apply_fix_solutions_to_code():
pass
def get_file_content(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
return file.read()
except Exception:
print("Error reading file:", file=sys.stderr)
return None
# step 0: try parse user input
def try_parse_user_input():
pass
GLOBAL_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".chat", ".workflow_config.json")
def get_aider_python_path():
"""
Retrieves the path to the Aider Python executable from the global configuration file.
Returns:
str or None: The path to the Aider Python executable if found in the configuration,
or None if the configuration file doesn't exist or the path is not set.
"""
if os.path.exists(GLOBAL_CONFIG_PATH):
with open(GLOBAL_CONFIG_PATH, "r", encoding="utf-8") as f:
config = json.load(f)
return config.get("aider_python2")
return None
def run_aider(message, file_path):
"""
Run the Aider tool to apply changes to a file based on a given message.
This function executes the Aider tool with specific parameters to apply changes
to the specified file. It captures and returns the output from Aider.
Args:
message (str): The message describing the changes to be made.
file_path (str): The path to the file that needs to be modified.
Returns:
str: The output from the Aider tool, containing information about the changes made.
Raises:
SystemExit: If the Aider process returns a non-zero exit code, indicating an error.
"""
python = get_aider_python_path()
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
cmd = [
python,
"-m",
"aider",
"--model",
f"openai/{model}",
"--yes",
"--no-auto-commits",
"--dry-run",
"--no-pretty",
"--message",
message,
file_path,
]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
has_started = False
aider_output = ""
for line in process.stdout:
if "run with --help" in line or 'run "aider --help"' in line:
has_started = True
continue
if has_started:
aider_output += line
print(line, end="", flush=True)
return_code = process.wait()
if return_code != 0:
for line in process.stderr:
print(f"Error: {line.strip()}", file=sys.stderr)
sys.exit(return_code)
return aider_output
def apply_changes(changes, file_path):
"""
Apply the changes to the specified file using aider.
Args:
changes (str): The changes to be applied to the file.
file_path (str): The path to the file where changes will be applied.
This function creates a temporary file with the changes, then uses aider to apply
these changes to the specified file. It handles the execution of aider and manages
the output and potential errors.
"""
changes_file = ".chat/changes.txt"
os.makedirs(os.path.dirname(changes_file), exist_ok=True)
with open(changes_file, "w", encoding="utf-8") as f:
f.write(changes)
python = get_aider_python_path()
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
cmd = [
python,
"-m",
"aider",
"--model",
f"openai/{model}",
"--yes",
"--no-auto-commits",
"--apply",
changes_file,
file_path,
]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
has_started = False
for line in process.stdout:
if "Model:" in line:
has_started = True
continue
if has_started:
print(line, end="", flush=True)
return_code = process.wait()
if return_code != 0:
for line in process.stderr:
print(f"Error: {line.strip()}", file=sys.stderr)
sys.exit(return_code)
os.remove(changes_file)
def main():
print("start fix issue ...\n\n")
"""
Main function to fix issues in the selected code.
It retrieves the selected code, gets issue descriptions,
generates fix solutions using LLM, and applies the changes.
"""
print("start fix issue ...\n\n", flush=True)
file_path, issue_line, issue_line_num = get_selected_code()
if not file_path or not issue_line:
print("No code selected. Please select the code line you want to fix.", file=sys.stderr)
@ -141,20 +434,71 @@ def main():
)
sys.exit(0)
print("make llm prompt ...\n\n")
print("make llm prompt ...\n\n", flush=True)
current_file_content = get_current_file_content(file_path, issue_line_num)
rule_description = get_rule_description(issue_description)
print("--->>:", rule_description)
# print("Rule description:\n\n", rule_description, end="\n\n")
print("call llm to fix issue ...\n\n")
fix_solutions = call_llm_to_generate_fix_solutions(
file_content=current_file_content,
issue_line_code=issue_line,
issue_description=issue_description,
rule_description=rule_description,
)
if not fix_solutions:
sys.exit(1)
print("call llm to fix issue ...\n\n", flush=True)
# ===> 如果aider python已经安装则直接调用aider来执行AI访问
aider_python = get_aider_python_path()
if aider_python and os.path.exists(aider_python):
python_path = os.environ.get("PYTHONPATH", "")
if python_path:
# remove PYTHONPATH
os.environ.pop("PYTHONPATH")
# Use aider-based implementation
message = f"""
Fix issue: {issue_description}
Which is reported at line: {issue_line}
Rule description: {rule_description}
"""
changes = run_aider(message, file_path)
if not changes:
print("No changes suggested by aider.")
sys.exit(0)
print("\nApplying changes...\n", flush=True)
with open(file_path, "r", encoding="utf-8") as f:
original_content = f.read()
apply_changes(changes, file_path)
with open(file_path, "r", encoding="utf-8") as f:
updated_content = f.read()
with open(file_path, "w", encoding="utf-8") as f:
f.write(original_content)
os.environ["PYTHONPATH"] = python_path
# Display changes in IDE
IDEService().select_range(file_path, -1, -1, -1, -1)
IDEService().diff_apply("", updated_content, False)
else:
print("No aider python found, using default implementation.", end="\n\n")
fix_solutions = call_llm_to_generate_fix_solutions(
file_content=current_file_content,
issue_line_code=issue_line,
issue_description=issue_description,
rule_description=rule_description,
)
if not fix_solutions:
sys.exit(1)
print("\n\n", flush=True)
print("apply fix solution ...\n\n")
updated_content = extract_markdown_block(fix_solutions)
if updated_content:
# Display changes in IDE
IDEService().diff_apply("", updated_content, True)
print("Changes have been displayed in the IDE.")
if __name__ == "__main__":