auto-coder 0.1.363__py3-none-any.whl → 0.1.365__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/METADATA +2 -2
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/RECORD +39 -23
- autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +1 -1
- autocoder/auto_coder.py +46 -2
- autocoder/auto_coder_runner.py +2 -0
- autocoder/common/__init__.py +5 -0
- autocoder/common/file_checkpoint/__init__.py +21 -0
- autocoder/common/file_checkpoint/backup.py +264 -0
- autocoder/common/file_checkpoint/conversation_checkpoint.py +182 -0
- autocoder/common/file_checkpoint/examples.py +217 -0
- autocoder/common/file_checkpoint/manager.py +611 -0
- autocoder/common/file_checkpoint/models.py +156 -0
- autocoder/common/file_checkpoint/store.py +383 -0
- autocoder/common/file_checkpoint/test_backup.py +242 -0
- autocoder/common/file_checkpoint/test_manager.py +570 -0
- autocoder/common/file_checkpoint/test_models.py +360 -0
- autocoder/common/file_checkpoint/test_store.py +327 -0
- autocoder/common/file_checkpoint/test_utils.py +297 -0
- autocoder/common/file_checkpoint/utils.py +119 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +114 -55
- autocoder/common/save_formatted_log.py +76 -5
- autocoder/common/utils_code_auto_generate.py +2 -1
- autocoder/common/v2/agent/agentic_edit.py +545 -225
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +83 -43
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +116 -29
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +179 -48
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +101 -56
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +173 -132
- autocoder/common/v2/agent/agentic_edit_types.py +4 -0
- autocoder/compilers/normal_compiler.py +64 -0
- autocoder/events/event_manager_singleton.py +133 -4
- autocoder/linters/normal_linter.py +373 -0
- autocoder/linters/python_linter.py +4 -2
- autocoder/version.py +1 -1
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import Dict, Any, Optional
|
|
2
|
+
from typing import Dict, Any, Optional, List, Set, Union
|
|
3
3
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
4
4
|
from autocoder.common.v2.agent.agentic_edit_types import ListFilesTool, ToolResult # Import ToolResult from types
|
|
5
5
|
from loguru import logger
|
|
@@ -18,13 +18,40 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
18
18
|
self.tool: ListFilesTool = tool # For type hinting
|
|
19
19
|
self.shadow_manager = self.agent.shadow_manager
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
def list_files_in_dir(self, base_dir: str, recursive: bool, source_dir: str, is_outside_source: bool) -> Set[str]:
|
|
22
|
+
"""Helper function to list files in a directory"""
|
|
23
|
+
result = set()
|
|
24
|
+
try:
|
|
25
|
+
if recursive:
|
|
26
|
+
for root, dirs, files in os.walk(base_dir):
|
|
27
|
+
# Modify dirs in-place to skip ignored dirs early
|
|
28
|
+
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
|
|
29
|
+
for name in files:
|
|
30
|
+
full_path = os.path.join(root, name)
|
|
31
|
+
if should_ignore(full_path):
|
|
32
|
+
continue
|
|
33
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
34
|
+
result.add(display_path)
|
|
35
|
+
for d in dirs:
|
|
36
|
+
full_path = os.path.join(root, d)
|
|
37
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
38
|
+
result.add(display_path + "/")
|
|
39
|
+
else:
|
|
40
|
+
for item in os.listdir(base_dir):
|
|
41
|
+
full_path = os.path.join(base_dir, item)
|
|
42
|
+
if should_ignore(full_path):
|
|
43
|
+
continue
|
|
44
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
45
|
+
if os.path.isdir(full_path):
|
|
46
|
+
result.add(display_path + "/")
|
|
47
|
+
else:
|
|
48
|
+
result.add(display_path)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"Error listing files in {base_dir}: {e}")
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def list_files_with_shadow(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
|
|
54
|
+
"""List files using shadow manager for path translation"""
|
|
28
55
|
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
29
56
|
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
30
57
|
if is_outside_source:
|
|
@@ -49,46 +76,14 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
49
76
|
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
50
77
|
return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
|
|
51
78
|
|
|
52
|
-
# Helper function to list files in a directory
|
|
53
|
-
def list_files_in_dir(base_dir: str) -> set:
|
|
54
|
-
result = set()
|
|
55
|
-
try:
|
|
56
|
-
if recursive:
|
|
57
|
-
for root, dirs, files in os.walk(base_dir):
|
|
58
|
-
# Modify dirs in-place to skip ignored dirs early
|
|
59
|
-
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
|
|
60
|
-
for name in files:
|
|
61
|
-
full_path = os.path.join(root, name)
|
|
62
|
-
if should_ignore(full_path):
|
|
63
|
-
continue
|
|
64
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
65
|
-
result.add(display_path)
|
|
66
|
-
for d in dirs:
|
|
67
|
-
full_path = os.path.join(root, d)
|
|
68
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
69
|
-
result.add(display_path + "/")
|
|
70
|
-
else:
|
|
71
|
-
for item in os.listdir(base_dir):
|
|
72
|
-
full_path = os.path.join(base_dir, item)
|
|
73
|
-
if should_ignore(full_path):
|
|
74
|
-
continue
|
|
75
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
76
|
-
if os.path.isdir(full_path):
|
|
77
|
-
result.add(display_path + "/")
|
|
78
|
-
else:
|
|
79
|
-
result.add(display_path)
|
|
80
|
-
except Exception as e:
|
|
81
|
-
logger.warning(f"Error listing files in {base_dir}: {e}")
|
|
82
|
-
return result
|
|
83
|
-
|
|
84
79
|
# Collect files from shadow and/or source directory
|
|
85
80
|
shadow_files_set = set()
|
|
86
81
|
if shadow_exists:
|
|
87
|
-
shadow_files_set = list_files_in_dir(shadow_dir_path)
|
|
82
|
+
shadow_files_set = self.list_files_in_dir(shadow_dir_path, recursive, source_dir, is_outside_source)
|
|
88
83
|
|
|
89
84
|
source_files_set = set()
|
|
90
85
|
if os.path.exists(absolute_list_path) and os.path.isdir(absolute_list_path):
|
|
91
|
-
source_files_set = list_files_in_dir(absolute_list_path)
|
|
86
|
+
source_files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
|
|
92
87
|
|
|
93
88
|
# Merge results, prioritizing shadow files if exist
|
|
94
89
|
if shadow_exists:
|
|
@@ -101,7 +96,52 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
101
96
|
try:
|
|
102
97
|
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
|
|
103
98
|
logger.info(message)
|
|
104
|
-
return
|
|
99
|
+
return sorted(merged_files)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
102
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
103
|
+
|
|
104
|
+
def list_files_normal(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
|
|
105
|
+
"""List files directly without using shadow manager"""
|
|
106
|
+
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
107
|
+
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
108
|
+
if is_outside_source:
|
|
109
|
+
logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
|
|
110
|
+
|
|
111
|
+
# Validate that the directory exists
|
|
112
|
+
if not os.path.exists(absolute_list_path):
|
|
113
|
+
return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
|
|
114
|
+
if not os.path.isdir(absolute_list_path):
|
|
115
|
+
return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
|
|
116
|
+
|
|
117
|
+
# Collect files from the directory
|
|
118
|
+
files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(files_set)} items."
|
|
122
|
+
logger.info(message)
|
|
123
|
+
return sorted(files_set)
|
|
105
124
|
except Exception as e:
|
|
106
125
|
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
107
126
|
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
127
|
+
|
|
128
|
+
def resolve(self) -> ToolResult:
|
|
129
|
+
"""Resolve the list files tool by calling the appropriate implementation"""
|
|
130
|
+
list_path_str = self.tool.path
|
|
131
|
+
recursive = self.tool.recursive or False
|
|
132
|
+
source_dir = self.args.source_dir or "."
|
|
133
|
+
absolute_source_dir = os.path.abspath(source_dir)
|
|
134
|
+
absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
|
|
135
|
+
|
|
136
|
+
# Choose the appropriate implementation based on whether shadow_manager is available
|
|
137
|
+
if self.shadow_manager:
|
|
138
|
+
result = self.list_files_with_shadow(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
|
|
139
|
+
else:
|
|
140
|
+
result = self.list_files_normal(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
|
|
141
|
+
|
|
142
|
+
# Handle the case where the implementation returns a sorted list instead of a ToolResult
|
|
143
|
+
if isinstance(result, list):
|
|
144
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(result)} items."
|
|
145
|
+
return ToolResult(success=True, message=message, content=result)
|
|
146
|
+
else:
|
|
147
|
+
return result
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Dict, Any, Optional
|
|
3
|
-
from autocoder.common import AutoCoderArgs
|
|
3
|
+
from autocoder.common import AutoCoderArgs,SourceCode
|
|
4
4
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
|
-
from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult
|
|
6
|
+
from autocoder.common.context_pruner import PruneContext
|
|
7
|
+
from autocoder.common import SourceCode
|
|
8
|
+
from autocoder.rag.token_counter import count_tokens
|
|
6
9
|
from loguru import logger
|
|
7
10
|
import typing
|
|
11
|
+
from autocoder.rag.loaders import (
|
|
12
|
+
extract_text_from_pdf,
|
|
13
|
+
extract_text_from_docx,
|
|
14
|
+
extract_text_from_ppt
|
|
15
|
+
)
|
|
8
16
|
|
|
9
17
|
if typing.TYPE_CHECKING:
|
|
10
18
|
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
@@ -15,40 +23,119 @@ class ReadFileToolResolver(BaseToolResolver):
|
|
|
15
23
|
super().__init__(agent, tool, args)
|
|
16
24
|
self.tool: ReadFileTool = tool # For type hinting
|
|
17
25
|
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
26
|
+
self.context_pruner = PruneContext(
|
|
27
|
+
max_tokens=self.args.context_prune_safe_zone_tokens,
|
|
28
|
+
args=self.args,
|
|
29
|
+
llm=self.agent.context_prune_llm
|
|
30
|
+
)
|
|
18
31
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
def _prune_file_content(self, content: str, file_path: str) -> str:
|
|
33
|
+
"""对文件内容进行剪枝处理"""
|
|
34
|
+
if not self.context_pruner:
|
|
35
|
+
return content
|
|
36
|
+
|
|
37
|
+
# 计算 token 数量
|
|
38
|
+
tokens = count_tokens(content)
|
|
39
|
+
if tokens <= self.args.context_prune_safe_zone_tokens:
|
|
40
|
+
return content
|
|
41
|
+
|
|
42
|
+
# 创建 SourceCode 对象
|
|
43
|
+
source_code = SourceCode(
|
|
44
|
+
module_name=file_path,
|
|
45
|
+
source_code=content,
|
|
46
|
+
tokens=tokens
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 使用 context_pruner 进行剪枝
|
|
50
|
+
pruned_sources = self.context_pruner.handle_overflow(
|
|
51
|
+
file_sources=[source_code],
|
|
52
|
+
conversations=self.agent.current_conversations if self.agent else [],
|
|
53
|
+
strategy=self.args.context_prune_strategy
|
|
54
|
+
)
|
|
24
55
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
|
|
56
|
+
if not pruned_sources:
|
|
57
|
+
return content
|
|
28
58
|
|
|
59
|
+
return pruned_sources[0].source_code
|
|
60
|
+
|
|
61
|
+
def _read_file_content(self, file_path_to_read: str) -> str:
|
|
62
|
+
content = ""
|
|
63
|
+
ext = os.path.splitext(file_path_to_read)[1].lower()
|
|
64
|
+
|
|
65
|
+
if ext == '.pdf':
|
|
66
|
+
logger.info(f"Extracting text from PDF: {file_path_to_read}")
|
|
67
|
+
content = extract_text_from_pdf(file_path_to_read)
|
|
68
|
+
elif ext == '.docx':
|
|
69
|
+
logger.info(f"Extracting text from DOCX: {file_path_to_read}")
|
|
70
|
+
content = extract_text_from_docx(file_path_to_read)
|
|
71
|
+
elif ext in ('.pptx', '.ppt'):
|
|
72
|
+
logger.info(f"Extracting text from PPT/PPTX: {file_path_to_read}")
|
|
73
|
+
slide_texts = []
|
|
74
|
+
for slide_identifier, slide_text_content in extract_text_from_ppt(file_path_to_read):
|
|
75
|
+
slide_texts.append(f"--- Slide {slide_identifier} ---\n{slide_text_content}")
|
|
76
|
+
content = "\n\n".join(slide_texts) if slide_texts else ""
|
|
77
|
+
else:
|
|
78
|
+
logger.info(f"Reading plain text file: {file_path_to_read}")
|
|
79
|
+
with open(file_path_to_read, 'r', encoding='utf-8', errors='replace') as f:
|
|
80
|
+
content = f.read()
|
|
81
|
+
|
|
82
|
+
# 对内容进行剪枝处理
|
|
83
|
+
return self._prune_file_content(content, file_path_to_read)
|
|
84
|
+
|
|
85
|
+
def read_file_with_shadow(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
86
|
+
"""Read file using shadow manager for path translation"""
|
|
29
87
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# else fallback to original file
|
|
42
|
-
# Fallback to original file
|
|
88
|
+
# Check if shadow file exists
|
|
89
|
+
shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
90
|
+
if os.path.exists(shadow_path) and os.path.isfile(shadow_path):
|
|
91
|
+
try:
|
|
92
|
+
content = self._read_file_content(shadow_path)
|
|
93
|
+
logger.info(f"[Shadow] Successfully processed shadow file: {shadow_path}")
|
|
94
|
+
return ToolResult(success=True, message=f"{file_path}", content=content)
|
|
95
|
+
except Exception as e_shadow:
|
|
96
|
+
logger.warning(f"Failed to read shadow file {shadow_path} with _read_file_content: {str(e_shadow)}. Falling back to original file.")
|
|
97
|
+
|
|
98
|
+
# If shadow file doesn't exist or reading failed, try original file
|
|
43
99
|
if not os.path.exists(abs_file_path):
|
|
44
100
|
return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
|
|
45
101
|
if not os.path.isfile(abs_file_path):
|
|
46
102
|
return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
|
|
47
103
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
104
|
+
content = self._read_file_content(abs_file_path)
|
|
105
|
+
logger.info(f"Successfully processed file: {file_path}")
|
|
106
|
+
return ToolResult(success=True, message=f"{file_path}", content=content)
|
|
107
|
+
|
|
52
108
|
except Exception as e:
|
|
53
|
-
logger.
|
|
54
|
-
|
|
109
|
+
logger.warning(f"Error processing file '{file_path}': {str(e)}")
|
|
110
|
+
logger.exception(e)
|
|
111
|
+
return ToolResult(success=False, message=f"An error occurred while processing the file '{file_path}': {str(e)}")
|
|
112
|
+
|
|
113
|
+
def read_file_normal(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
114
|
+
"""Read file directly without using shadow manager"""
|
|
115
|
+
try:
|
|
116
|
+
if not os.path.exists(abs_file_path):
|
|
117
|
+
return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
|
|
118
|
+
if not os.path.isfile(abs_file_path):
|
|
119
|
+
return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
|
|
120
|
+
|
|
121
|
+
content = self._read_file_content(abs_file_path)
|
|
122
|
+
logger.info(f"Successfully processed file: {file_path}")
|
|
123
|
+
return ToolResult(success=True, message=f"{file_path}", content=content)
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.warning(f"Error processing file '{file_path}': {str(e)}")
|
|
127
|
+
logger.exception(e)
|
|
128
|
+
return ToolResult(success=False, message=f"An error occurred while processing the file '{file_path}': {str(e)}")
|
|
129
|
+
|
|
130
|
+
def resolve(self) -> ToolResult:
|
|
131
|
+
"""Resolve the read file tool by calling the appropriate implementation"""
|
|
132
|
+
file_path = self.tool.path
|
|
133
|
+
source_dir = self.args.source_dir or "."
|
|
134
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
135
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
136
|
+
|
|
137
|
+
# Choose the appropriate implementation based on whether shadow_manager is available
|
|
138
|
+
if self.shadow_manager:
|
|
139
|
+
return self.read_file_with_shadow(file_path, source_dir, abs_project_dir, abs_file_path)
|
|
140
|
+
else:
|
|
141
|
+
return self.read_file_normal(file_path, source_dir, abs_project_dir, abs_file_path)
|
|
@@ -4,7 +4,9 @@ from typing import Dict, Any, Optional, List, Tuple
|
|
|
4
4
|
import typing
|
|
5
5
|
from autocoder.common import AutoCoderArgs
|
|
6
6
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
7
|
-
from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult
|
|
7
|
+
from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult
|
|
8
|
+
from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
|
|
9
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
|
|
8
10
|
from loguru import logger
|
|
9
11
|
from autocoder.common.auto_coder_lang import get_message_with_format
|
|
10
12
|
if typing.TYPE_CHECKING:
|
|
@@ -84,30 +86,18 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
84
86
|
|
|
85
87
|
return "\n".join(formatted_issues)
|
|
86
88
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
abs_project_dir = os.path.abspath(source_dir)
|
|
92
|
-
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
93
|
-
|
|
94
|
-
# Security check
|
|
95
|
-
if not abs_file_path.startswith(abs_project_dir):
|
|
96
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
|
|
97
|
-
|
|
98
|
-
# Determine target path: shadow file if shadow_manager exists
|
|
99
|
-
target_path = abs_file_path
|
|
100
|
-
if self.shadow_manager:
|
|
89
|
+
def replace_in_file_with_shadow(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
90
|
+
"""Replace content in file using shadow manager for path translation"""
|
|
91
|
+
try:
|
|
92
|
+
# Determine target path: shadow file
|
|
101
93
|
target_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
# For now, read from shadow if exists, else fallback to original file
|
|
105
|
-
try:
|
|
95
|
+
# Read original content
|
|
106
96
|
if os.path.exists(target_path) and os.path.isfile(target_path):
|
|
107
97
|
with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
108
98
|
original_content = f.read()
|
|
109
|
-
elif
|
|
110
|
-
# If shadow doesn't exist, but original exists, read original content
|
|
99
|
+
elif os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
|
|
100
|
+
# If shadow doesn't exist, but original exists, read original content
|
|
111
101
|
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
112
102
|
original_content = f.read()
|
|
113
103
|
# create parent dirs of shadow if needed
|
|
@@ -118,43 +108,40 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
118
108
|
logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
|
|
119
109
|
else:
|
|
120
110
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
|
|
121
|
-
except Exception as e:
|
|
122
|
-
logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
|
|
123
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
parsed_blocks = self.parse_diff(diff_content)
|
|
113
|
+
if not parsed_blocks:
|
|
114
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
|
|
128
115
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
current_content = original_content
|
|
117
|
+
applied_count = 0
|
|
118
|
+
errors = []
|
|
132
119
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
# Apply blocks sequentially
|
|
121
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
122
|
+
start_index = current_content.find(search_block)
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# continue applying remaining blocks
|
|
124
|
+
if start_index != -1:
|
|
125
|
+
current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
|
|
126
|
+
applied_count += 1
|
|
127
|
+
logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
|
|
128
|
+
else:
|
|
129
|
+
error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
|
|
130
|
+
logger.warning(error_message)
|
|
131
|
+
context_start = max(0, original_content.find(search_block[:20]) - 100)
|
|
132
|
+
context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
|
|
133
|
+
logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
|
|
134
|
+
errors.append(error_message)
|
|
149
135
|
|
|
150
|
-
|
|
151
|
-
|
|
136
|
+
if applied_count == 0 and errors:
|
|
137
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
|
|
152
138
|
|
|
153
|
-
|
|
139
|
+
# Write the modified content back to shadow file
|
|
154
140
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
155
141
|
with open(target_path, 'w', encoding='utf-8') as f:
|
|
156
142
|
f.write(current_content)
|
|
157
|
-
|
|
143
|
+
|
|
144
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to shadow file: {file_path}")
|
|
158
145
|
|
|
159
146
|
# 新增:执行代码质量检查
|
|
160
147
|
lint_results = None
|
|
@@ -212,6 +199,132 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
212
199
|
"content": current_content,
|
|
213
200
|
}
|
|
214
201
|
|
|
202
|
+
# 只有在启用Lint时才添加Lint结果
|
|
203
|
+
if enable_lint:
|
|
204
|
+
result_content["lint_results"] = {
|
|
205
|
+
"has_issues": has_lint_issues,
|
|
206
|
+
"issues": formatted_issues if has_lint_issues else None
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return ToolResult(success=True, message=message, content=result_content)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Error writing replaced content to shadow file '{file_path}': {str(e)}")
|
|
212
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
|
|
213
|
+
|
|
214
|
+
def replace_in_file_normal(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
215
|
+
"""Replace content in file directly without using shadow manager"""
|
|
216
|
+
try:
|
|
217
|
+
# Read original content
|
|
218
|
+
if not os.path.exists(abs_file_path):
|
|
219
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
|
|
220
|
+
if not os.path.isfile(abs_file_path):
|
|
221
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.not_a_file", file_path=file_path))
|
|
222
|
+
|
|
223
|
+
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
224
|
+
original_content = f.read()
|
|
225
|
+
|
|
226
|
+
parsed_blocks = self.parse_diff(diff_content)
|
|
227
|
+
if not parsed_blocks:
|
|
228
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
|
|
229
|
+
|
|
230
|
+
current_content = original_content
|
|
231
|
+
applied_count = 0
|
|
232
|
+
errors = []
|
|
233
|
+
|
|
234
|
+
# Apply blocks sequentially
|
|
235
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
236
|
+
start_index = current_content.find(search_block)
|
|
237
|
+
|
|
238
|
+
if start_index != -1:
|
|
239
|
+
current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
|
|
240
|
+
applied_count += 1
|
|
241
|
+
logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
|
|
242
|
+
else:
|
|
243
|
+
error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
|
|
244
|
+
logger.warning(error_message)
|
|
245
|
+
context_start = max(0, original_content.find(search_block[:20]) - 100)
|
|
246
|
+
context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
|
|
247
|
+
logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
|
|
248
|
+
errors.append(error_message)
|
|
249
|
+
|
|
250
|
+
if applied_count == 0 and errors:
|
|
251
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
|
|
252
|
+
|
|
253
|
+
# Write the modified content back to file
|
|
254
|
+
if self.agent and self.agent.checkpoint_manager:
|
|
255
|
+
changes = {
|
|
256
|
+
file_path: CheckpointFileChange(
|
|
257
|
+
file_path=file_path,
|
|
258
|
+
content=current_content,
|
|
259
|
+
is_deletion=False,
|
|
260
|
+
is_new=True
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
change_group_id = self.args.event_file
|
|
264
|
+
|
|
265
|
+
self.agent.checkpoint_manager.apply_changes_with_conversation(
|
|
266
|
+
changes=changes,
|
|
267
|
+
conversations=self.agent.current_conversations,
|
|
268
|
+
change_group_id=change_group_id,
|
|
269
|
+
metadata={"event_file": self.args.event_file}
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
with open(abs_file_path, 'w', encoding='utf-8') as f:
|
|
273
|
+
f.write(current_content)
|
|
274
|
+
|
|
275
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
276
|
+
|
|
277
|
+
# 新增:执行代码质量检查
|
|
278
|
+
lint_results = None
|
|
279
|
+
lint_message = ""
|
|
280
|
+
formatted_issues = ""
|
|
281
|
+
has_lint_issues = False
|
|
282
|
+
|
|
283
|
+
# 检查是否启用了Lint功能
|
|
284
|
+
enable_lint = self.args.enable_auto_fix_lint
|
|
285
|
+
|
|
286
|
+
if enable_lint:
|
|
287
|
+
try:
|
|
288
|
+
if self.agent.linter:
|
|
289
|
+
lint_results = self.agent.linter.lint_file(file_path)
|
|
290
|
+
if lint_results and lint_results.issues:
|
|
291
|
+
has_lint_issues = True
|
|
292
|
+
# 格式化 lint 问题
|
|
293
|
+
formatted_issues = self._format_lint_issues(lint_results)
|
|
294
|
+
lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Lint 检查失败: {str(e)}")
|
|
297
|
+
lint_message = "\n\n尝试进行代码质量检查时出错。"
|
|
298
|
+
else:
|
|
299
|
+
logger.info("代码质量检查已禁用")
|
|
300
|
+
|
|
301
|
+
# 构建包含 lint 结果的返回消息
|
|
302
|
+
if errors:
|
|
303
|
+
message = get_message_with_format("replace_in_file.apply_success_with_warnings",
|
|
304
|
+
applied=applied_count,
|
|
305
|
+
total=len(parsed_blocks),
|
|
306
|
+
file_path=file_path,
|
|
307
|
+
errors="\n".join(errors))
|
|
308
|
+
else:
|
|
309
|
+
message = get_message_with_format("replace_in_file.apply_success",
|
|
310
|
+
applied=applied_count,
|
|
311
|
+
total=len(parsed_blocks),
|
|
312
|
+
file_path=file_path)
|
|
313
|
+
|
|
314
|
+
# 将 lint 消息添加到结果中,如果启用了Lint
|
|
315
|
+
if enable_lint:
|
|
316
|
+
message += lint_message
|
|
317
|
+
|
|
318
|
+
# 变更跟踪,回调AgenticEdit
|
|
319
|
+
if self.agent:
|
|
320
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
321
|
+
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
322
|
+
|
|
323
|
+
# 附加 lint 结果到返回内容
|
|
324
|
+
result_content = {
|
|
325
|
+
"content": current_content,
|
|
326
|
+
}
|
|
327
|
+
|
|
215
328
|
# 只有在启用Lint时才添加Lint结果
|
|
216
329
|
if enable_lint:
|
|
217
330
|
result_content["lint_results"] = {
|
|
@@ -223,3 +336,21 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
223
336
|
except Exception as e:
|
|
224
337
|
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
225
338
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
|
|
339
|
+
|
|
340
|
+
def resolve(self) -> ToolResult:
|
|
341
|
+
"""Resolve the replace in file tool by calling the appropriate implementation"""
|
|
342
|
+
file_path = self.tool.path
|
|
343
|
+
diff_content = self.tool.diff
|
|
344
|
+
source_dir = self.args.source_dir or "."
|
|
345
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
346
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
347
|
+
|
|
348
|
+
# Security check
|
|
349
|
+
if not abs_file_path.startswith(abs_project_dir):
|
|
350
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
|
|
351
|
+
|
|
352
|
+
# Choose the appropriate implementation based on whether shadow_manager is available
|
|
353
|
+
if self.shadow_manager:
|
|
354
|
+
return self.replace_in_file_with_shadow(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)
|
|
355
|
+
else:
|
|
356
|
+
return self.replace_in_file_normal(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)
|