auto-coder 0.1.364__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.364.dist-info → auto_coder-0.1.365.dist-info}/METADATA +1 -1
- {auto_coder-0.1.364.dist-info → auto_coder-0.1.365.dist-info}/RECORD +19 -18
- autocoder/auto_coder.py +46 -2
- autocoder/common/__init__.py +3 -0
- autocoder/common/file_checkpoint/conversation_checkpoint.py +182 -0
- autocoder/common/file_checkpoint/manager.py +208 -1
- autocoder/common/utils_code_auto_generate.py +2 -1
- autocoder/common/v2/agent/agentic_edit.py +291 -92
- 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 +115 -28
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +169 -61
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +101 -56
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +95 -76
- autocoder/common/v2/agent/agentic_edit_types.py +4 -0
- autocoder/version.py +1 -1
- {auto_coder-0.1.364.dist-info → auto_coder-0.1.365.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.364.dist-info → auto_coder-0.1.365.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.364.dist-info → auto_coder-0.1.365.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.364.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
|
+
)
|
|
55
|
+
|
|
56
|
+
if not pruned_sources:
|
|
57
|
+
return content
|
|
24
58
|
|
|
25
|
-
|
|
26
|
-
# if not abs_file_path.startswith(abs_project_dir):
|
|
27
|
-
# return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
|
|
59
|
+
return pruned_sources[0].source_code
|
|
28
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
|
-
logger.info(f"Successfully read file: {file_path}")
|
|
104
|
+
content = self._read_file_content(abs_file_path)
|
|
105
|
+
logger.info(f"Successfully processed file: {file_path}")
|
|
51
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,7 @@ 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
8
|
from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
|
|
9
9
|
from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
|
|
10
10
|
from loguru import logger
|
|
@@ -86,30 +86,18 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
86
86
|
|
|
87
87
|
return "\n".join(formatted_issues)
|
|
88
88
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
abs_project_dir = os.path.abspath(source_dir)
|
|
94
|
-
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
95
|
-
|
|
96
|
-
# Security check
|
|
97
|
-
if not abs_file_path.startswith(abs_project_dir):
|
|
98
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
|
|
99
|
-
|
|
100
|
-
# Determine target path: shadow file if shadow_manager exists
|
|
101
|
-
target_path = abs_file_path
|
|
102
|
-
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
|
|
103
93
|
target_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
104
94
|
|
|
105
|
-
|
|
106
|
-
# For now, read from shadow if exists, else fallback to original file
|
|
107
|
-
try:
|
|
95
|
+
# Read original content
|
|
108
96
|
if os.path.exists(target_path) and os.path.isfile(target_path):
|
|
109
97
|
with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
110
98
|
original_content = f.read()
|
|
111
|
-
elif
|
|
112
|
-
# 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
|
|
113
101
|
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
114
102
|
original_content = f.read()
|
|
115
103
|
# create parent dirs of shadow if needed
|
|
@@ -120,41 +108,149 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
120
108
|
logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
|
|
121
109
|
else:
|
|
122
110
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
|
|
125
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
|
|
126
111
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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"))
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
current_content = original_content
|
|
117
|
+
applied_count = 0
|
|
118
|
+
errors = []
|
|
134
119
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
# Apply blocks sequentially
|
|
121
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
122
|
+
start_index = current_content.find(search_block)
|
|
123
|
+
|
|
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)
|
|
135
|
+
|
|
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)))
|
|
138
|
+
|
|
139
|
+
# Write the modified content back to shadow file
|
|
140
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
141
|
+
with open(target_path, 'w', encoding='utf-8') as f:
|
|
142
|
+
f.write(current_content)
|
|
143
|
+
|
|
144
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to shadow file: {file_path}")
|
|
145
|
+
|
|
146
|
+
# 新增:执行代码质量检查
|
|
147
|
+
lint_results = None
|
|
148
|
+
lint_message = ""
|
|
149
|
+
formatted_issues = ""
|
|
150
|
+
has_lint_issues = False
|
|
151
|
+
|
|
152
|
+
# 检查是否启用了Lint功能
|
|
153
|
+
enable_lint = self.args.enable_auto_fix_lint
|
|
154
|
+
|
|
155
|
+
if enable_lint:
|
|
156
|
+
try:
|
|
157
|
+
if self.shadow_linter and self.shadow_manager:
|
|
158
|
+
# 对修改后的文件进行 lint 检查
|
|
159
|
+
shadow_path = target_path # 已经是影子路径
|
|
160
|
+
lint_results = self.shadow_linter.lint_shadow_file(shadow_path)
|
|
161
|
+
|
|
162
|
+
if lint_results and lint_results.issues:
|
|
163
|
+
has_lint_issues = True
|
|
164
|
+
# 格式化 lint 问题
|
|
165
|
+
formatted_issues = self._format_lint_issues(lint_results)
|
|
166
|
+
lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
|
|
167
|
+
else:
|
|
168
|
+
lint_message = "\n\n代码质量检查通过,未发现问题。"
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Lint 检查失败: {str(e)}")
|
|
171
|
+
lint_message = "\n\n尝试进行代码质量检查时出错。"
|
|
172
|
+
else:
|
|
173
|
+
logger.info("代码质量检查已禁用")
|
|
138
174
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
175
|
+
# 构建包含 lint 结果的返回消息
|
|
176
|
+
if errors:
|
|
177
|
+
message = get_message_with_format("replace_in_file.apply_success_with_warnings",
|
|
178
|
+
applied=applied_count,
|
|
179
|
+
total=len(parsed_blocks),
|
|
180
|
+
file_path=file_path,
|
|
181
|
+
errors="\n".join(errors))
|
|
143
182
|
else:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
message = get_message_with_format("replace_in_file.apply_success",
|
|
184
|
+
applied=applied_count,
|
|
185
|
+
total=len(parsed_blocks),
|
|
186
|
+
file_path=file_path)
|
|
187
|
+
|
|
188
|
+
# 将 lint 消息添加到结果中,如果启用了Lint
|
|
189
|
+
if enable_lint:
|
|
190
|
+
message += lint_message
|
|
151
191
|
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
# 变更跟踪,回调AgenticEdit
|
|
193
|
+
if self.agent:
|
|
194
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
195
|
+
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
154
196
|
|
|
197
|
+
# 附加 lint 结果到返回内容
|
|
198
|
+
result_content = {
|
|
199
|
+
"content": current_content,
|
|
200
|
+
}
|
|
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"""
|
|
155
216
|
try:
|
|
156
|
-
|
|
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 = []
|
|
157
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
|
|
158
254
|
if self.agent and self.agent.checkpoint_manager:
|
|
159
255
|
changes = {
|
|
160
256
|
file_path: CheckpointFileChange(
|
|
@@ -165,12 +261,18 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
165
261
|
)
|
|
166
262
|
}
|
|
167
263
|
change_group_id = self.args.event_file
|
|
168
|
-
|
|
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
|
+
)
|
|
169
271
|
else:
|
|
170
|
-
with open(
|
|
272
|
+
with open(abs_file_path, 'w', encoding='utf-8') as f:
|
|
171
273
|
f.write(current_content)
|
|
172
274
|
|
|
173
|
-
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
275
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
174
276
|
|
|
175
277
|
# 新增:执行代码质量检查
|
|
176
278
|
lint_results = None
|
|
@@ -183,18 +285,6 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
183
285
|
|
|
184
286
|
if enable_lint:
|
|
185
287
|
try:
|
|
186
|
-
if self.shadow_linter and self.shadow_manager:
|
|
187
|
-
# 对修改后的文件进行 lint 检查
|
|
188
|
-
shadow_path = target_path # 已经是影子路径
|
|
189
|
-
lint_results = self.shadow_linter.lint_shadow_file(shadow_path)
|
|
190
|
-
|
|
191
|
-
if lint_results and lint_results.issues:
|
|
192
|
-
has_lint_issues = True
|
|
193
|
-
# 格式化 lint 问题
|
|
194
|
-
formatted_issues = self._format_lint_issues(lint_results)
|
|
195
|
-
lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
|
|
196
|
-
else:
|
|
197
|
-
lint_message = "\n\n代码质量检查通过,未发现问题。"
|
|
198
288
|
if self.agent.linter:
|
|
199
289
|
lint_results = self.agent.linter.lint_file(file_path)
|
|
200
290
|
if lint_results and lint_results.issues:
|
|
@@ -246,3 +336,21 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
246
336
|
except Exception as e:
|
|
247
337
|
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
248
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)
|