auto-coder 0.1.374__py3-none-any.whl → 0.1.376__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.374.dist-info → auto_coder-0.1.376.dist-info}/METADATA +2 -2
- {auto_coder-0.1.374.dist-info → auto_coder-0.1.376.dist-info}/RECORD +27 -57
- autocoder/agent/base_agentic/base_agent.py +202 -52
- autocoder/agent/base_agentic/default_tools.py +38 -6
- autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +83 -43
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +88 -25
- autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +171 -62
- autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +101 -56
- autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +5 -0
- autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +5 -0
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +145 -32
- autocoder/auto_coder_rag.py +80 -11
- autocoder/models.py +2 -2
- autocoder/rag/agentic_rag.py +217 -0
- autocoder/rag/cache/local_duckdb_storage_cache.py +63 -33
- autocoder/rag/conversation_to_queries.py +37 -5
- autocoder/rag/long_context_rag.py +161 -41
- autocoder/rag/tools/__init__.py +10 -0
- autocoder/rag/tools/recall_tool.py +163 -0
- autocoder/rag/tools/search_tool.py +126 -0
- autocoder/rag/types.py +36 -0
- autocoder/utils/_markitdown.py +59 -13
- autocoder/version.py +1 -1
- autocoder/agent/agentic_edit.py +0 -833
- autocoder/agent/agentic_edit_tools/__init__.py +0 -28
- autocoder/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +0 -32
- autocoder/agent/agentic_edit_tools/attempt_completion_tool_resolver.py +0 -29
- autocoder/agent/agentic_edit_tools/base_tool_resolver.py +0 -29
- autocoder/agent/agentic_edit_tools/execute_command_tool_resolver.py +0 -84
- autocoder/agent/agentic_edit_tools/list_code_definition_names_tool_resolver.py +0 -75
- autocoder/agent/agentic_edit_tools/list_files_tool_resolver.py +0 -62
- autocoder/agent/agentic_edit_tools/plan_mode_respond_tool_resolver.py +0 -30
- autocoder/agent/agentic_edit_tools/read_file_tool_resolver.py +0 -36
- autocoder/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +0 -95
- autocoder/agent/agentic_edit_tools/search_files_tool_resolver.py +0 -70
- autocoder/agent/agentic_edit_tools/use_mcp_tool_resolver.py +0 -55
- autocoder/agent/agentic_edit_tools/write_to_file_tool_resolver.py +0 -98
- autocoder/agent/agentic_edit_types.py +0 -124
- autocoder/auto_coder_lang.py +0 -60
- autocoder/auto_coder_rag_client_mcp.py +0 -170
- autocoder/auto_coder_rag_mcp.py +0 -193
- autocoder/common/llm_rerank.py +0 -84
- autocoder/common/model_speed_test.py +0 -392
- autocoder/common/v2/agent/agentic_edit_conversation.py +0 -188
- autocoder/common/v2/agent/ignore_utils.py +0 -50
- autocoder/dispacher/actions/plugins/action_translate.py +0 -214
- autocoder/ignorefiles/__init__.py +0 -4
- autocoder/ignorefiles/ignore_file_utils.py +0 -63
- autocoder/ignorefiles/test_ignore_file_utils.py +0 -91
- autocoder/linters/code_linter.py +0 -588
- autocoder/rag/loaders/test_image_loader.py +0 -209
- autocoder/rag/raw_rag.py +0 -96
- autocoder/rag/simple_directory_reader.py +0 -646
- autocoder/rag/simple_rag.py +0 -404
- autocoder/regex_project/__init__.py +0 -162
- autocoder/utils/coder.py +0 -125
- autocoder/utils/tests.py +0 -37
- {auto_coder-0.1.374.dist-info → auto_coder-0.1.376.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.374.dist-info → auto_coder-0.1.376.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.374.dist-info → auto_coder-0.1.376.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.374.dist-info → auto_coder-0.1.376.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Default tools initialization module
|
|
3
3
|
Used to initialize and register default tools
|
|
4
4
|
"""
|
|
5
|
-
from typing import Dict,
|
|
5
|
+
from typing import Dict, Optional, List, Any
|
|
6
6
|
from loguru import logger
|
|
7
7
|
import byzerllm
|
|
8
8
|
from .tool_registry import ToolRegistry
|
|
@@ -502,10 +502,30 @@ def register_default_tools_case_doc(params: Dict[str, Any]):
|
|
|
502
502
|
|
|
503
503
|
logger.info(f"处理了 {len(DEFAULT_TOOLS_CASE_DOC)} 个默认工具用例文档")
|
|
504
504
|
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
def get_default_tool_names():
|
|
506
|
+
return [
|
|
507
|
+
"execute_command",
|
|
508
|
+
"read_file",
|
|
509
|
+
"write_to_file",
|
|
510
|
+
"replace_in_file",
|
|
511
|
+
"search_files",
|
|
512
|
+
"list_files",
|
|
513
|
+
"ask_followup_question",
|
|
514
|
+
"attempt_completion",
|
|
515
|
+
"plan_mode_respond",
|
|
516
|
+
"use_mcp_tool",
|
|
517
|
+
"talk_to",
|
|
518
|
+
"talk_to_group"
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
def register_default_tools(params: Dict[str, Any], default_tools_list: Optional[List[str]] = None):
|
|
507
522
|
"""
|
|
508
|
-
Register
|
|
523
|
+
Register default tools
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
params: Parameters for tool generators
|
|
527
|
+
default_tools_list: Optional list of tool names to register. If provided, only tools in this list will be registered.
|
|
528
|
+
If None, all default tools will be registered.
|
|
509
529
|
"""
|
|
510
530
|
tool_desc_gen = ToolDescGenerators(params)
|
|
511
531
|
tool_examples_gen = ToolExampleGenerators(params)
|
|
@@ -661,12 +681,24 @@ def register_default_tools(params: Dict[str, Any]):
|
|
|
661
681
|
"case_docs": []
|
|
662
682
|
}
|
|
663
683
|
}
|
|
664
|
-
#
|
|
684
|
+
# 先使用统一的工具注册方法注册工具
|
|
685
|
+
registered_count = 0
|
|
665
686
|
for tool_tag, tool_info in DEFAULT_TOOLS.items():
|
|
687
|
+
# attempt_completion 工具是必须注册的
|
|
688
|
+
if tool_tag == "attempt_completion":
|
|
689
|
+
ToolRegistry.register_unified_tool(tool_tag, tool_info)
|
|
690
|
+
registered_count += 1
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
# 如果提供了工具列表,则只注册列表中的工具
|
|
694
|
+
if default_tools_list is not None and tool_tag not in default_tools_list:
|
|
695
|
+
continue
|
|
696
|
+
|
|
666
697
|
ToolRegistry.register_unified_tool(tool_tag, tool_info)
|
|
698
|
+
registered_count += 1
|
|
667
699
|
|
|
668
700
|
logger.info(
|
|
669
|
-
f"Registered {
|
|
701
|
+
f"Registered {registered_count} default tools using unified registration")
|
|
670
702
|
|
|
671
703
|
# 然后注册默认工具用例文档
|
|
672
704
|
# 这样可以确保在注册用例文档时,所有工具已经注册完成
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
3
3
|
from autocoder.agent.base_agentic.types import ListFilesTool, ToolResult # Import ToolResult from types
|
|
4
|
-
from typing import Optional, Dict, Any, List
|
|
4
|
+
from typing import Optional, Dict, Any, List, Set, Union
|
|
5
5
|
import fnmatch
|
|
6
6
|
import re
|
|
7
7
|
import json
|
|
@@ -21,13 +21,40 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
21
21
|
self.tool: ListFilesTool = tool # For type hinting
|
|
22
22
|
self.shadow_manager = self.agent.shadow_manager
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
def list_files_in_dir(self, base_dir: str, recursive: bool, source_dir: str, is_outside_source: bool) -> Set[str]:
|
|
25
|
+
"""Helper function to list files in a directory"""
|
|
26
|
+
result = set()
|
|
27
|
+
try:
|
|
28
|
+
if recursive:
|
|
29
|
+
for root, dirs, files in os.walk(base_dir):
|
|
30
|
+
# Modify dirs in-place to skip ignored dirs early
|
|
31
|
+
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
|
|
32
|
+
for name in files:
|
|
33
|
+
full_path = os.path.join(root, name)
|
|
34
|
+
if should_ignore(full_path):
|
|
35
|
+
continue
|
|
36
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
37
|
+
result.add(display_path)
|
|
38
|
+
for d in dirs:
|
|
39
|
+
full_path = os.path.join(root, d)
|
|
40
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
41
|
+
result.add(display_path + "/")
|
|
42
|
+
else:
|
|
43
|
+
for item in os.listdir(base_dir):
|
|
44
|
+
full_path = os.path.join(base_dir, item)
|
|
45
|
+
if should_ignore(full_path):
|
|
46
|
+
continue
|
|
47
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
48
|
+
if os.path.isdir(full_path):
|
|
49
|
+
result.add(display_path + "/")
|
|
50
|
+
else:
|
|
51
|
+
result.add(display_path)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.warning(f"Error listing files in {base_dir}: {e}")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
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]]:
|
|
57
|
+
"""List files using shadow manager for path translation"""
|
|
31
58
|
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
32
59
|
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
33
60
|
if is_outside_source:
|
|
@@ -52,46 +79,14 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
52
79
|
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
53
80
|
return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
|
|
54
81
|
|
|
55
|
-
# Helper function to list files in a directory
|
|
56
|
-
def list_files_in_dir(base_dir: str) -> set:
|
|
57
|
-
result = set()
|
|
58
|
-
try:
|
|
59
|
-
if recursive:
|
|
60
|
-
for root, dirs, files in os.walk(base_dir):
|
|
61
|
-
# Modify dirs in-place to skip ignored dirs early
|
|
62
|
-
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
|
|
63
|
-
for name in files:
|
|
64
|
-
full_path = os.path.join(root, name)
|
|
65
|
-
if should_ignore(full_path):
|
|
66
|
-
continue
|
|
67
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
68
|
-
result.add(display_path)
|
|
69
|
-
for d in dirs:
|
|
70
|
-
full_path = os.path.join(root, d)
|
|
71
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
72
|
-
result.add(display_path + "/")
|
|
73
|
-
else:
|
|
74
|
-
for item in os.listdir(base_dir):
|
|
75
|
-
full_path = os.path.join(base_dir, item)
|
|
76
|
-
if should_ignore(full_path):
|
|
77
|
-
continue
|
|
78
|
-
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
79
|
-
if os.path.isdir(full_path):
|
|
80
|
-
result.add(display_path + "/")
|
|
81
|
-
else:
|
|
82
|
-
result.add(display_path)
|
|
83
|
-
except Exception as e:
|
|
84
|
-
logger.warning(f"Error listing files in {base_dir}: {e}")
|
|
85
|
-
return result
|
|
86
|
-
|
|
87
82
|
# Collect files from shadow and/or source directory
|
|
88
83
|
shadow_files_set = set()
|
|
89
84
|
if shadow_exists:
|
|
90
|
-
shadow_files_set = list_files_in_dir(shadow_dir_path)
|
|
85
|
+
shadow_files_set = self.list_files_in_dir(shadow_dir_path, recursive, source_dir, is_outside_source)
|
|
91
86
|
|
|
92
87
|
source_files_set = set()
|
|
93
88
|
if os.path.exists(absolute_list_path) and os.path.isdir(absolute_list_path):
|
|
94
|
-
source_files_set = list_files_in_dir(absolute_list_path)
|
|
89
|
+
source_files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
|
|
95
90
|
|
|
96
91
|
# Merge results, prioritizing shadow files if exist
|
|
97
92
|
if shadow_exists:
|
|
@@ -104,7 +99,52 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
104
99
|
try:
|
|
105
100
|
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
|
|
106
101
|
logger.info(message)
|
|
107
|
-
return
|
|
102
|
+
return sorted(merged_files)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
105
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
106
|
+
|
|
107
|
+
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]]:
|
|
108
|
+
"""List files directly without using shadow manager"""
|
|
109
|
+
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
110
|
+
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
111
|
+
if is_outside_source:
|
|
112
|
+
logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
|
|
113
|
+
|
|
114
|
+
# Validate that the directory exists
|
|
115
|
+
if not os.path.exists(absolute_list_path):
|
|
116
|
+
return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
|
|
117
|
+
if not os.path.isdir(absolute_list_path):
|
|
118
|
+
return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
|
|
119
|
+
|
|
120
|
+
# Collect files from the directory
|
|
121
|
+
files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(files_set)} items."
|
|
125
|
+
logger.info(message)
|
|
126
|
+
return sorted(files_set)
|
|
108
127
|
except Exception as e:
|
|
109
128
|
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
110
129
|
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
130
|
+
|
|
131
|
+
def resolve(self) -> ToolResult:
|
|
132
|
+
"""Resolve the list files tool by calling the appropriate implementation"""
|
|
133
|
+
list_path_str = self.tool.path
|
|
134
|
+
recursive = self.tool.recursive or False
|
|
135
|
+
source_dir = self.args.source_dir or "."
|
|
136
|
+
absolute_source_dir = os.path.abspath(source_dir)
|
|
137
|
+
absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
|
|
138
|
+
|
|
139
|
+
# Choose the appropriate implementation based on whether shadow_manager is available
|
|
140
|
+
if self.shadow_manager:
|
|
141
|
+
result = self.list_files_with_shadow(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
|
|
142
|
+
else:
|
|
143
|
+
result = self.list_files_normal(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
|
|
144
|
+
|
|
145
|
+
# Handle the case where the implementation returns a sorted list instead of a ToolResult
|
|
146
|
+
if isinstance(result, list):
|
|
147
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(result)} items."
|
|
148
|
+
return ToolResult(success=True, message=message, content=result)
|
|
149
|
+
else:
|
|
150
|
+
return result
|
|
@@ -5,6 +5,16 @@ from autocoder.agent.base_agentic.types import ReadFileTool, ToolResult # Impor
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
import typing
|
|
7
7
|
from autocoder.common import AutoCoderArgs
|
|
8
|
+
from autocoder.common.context_pruner import PruneContext
|
|
9
|
+
from autocoder.common import SourceCode
|
|
10
|
+
from autocoder.rag.token_counter import count_tokens
|
|
11
|
+
from loguru import logger
|
|
12
|
+
import typing
|
|
13
|
+
from autocoder.rag.loaders import (
|
|
14
|
+
extract_text_from_pdf,
|
|
15
|
+
extract_text_from_docx,
|
|
16
|
+
extract_text_from_ppt
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
if typing.TYPE_CHECKING:
|
|
10
20
|
from ..base_agent import BaseAgent
|
|
@@ -15,40 +25,93 @@ class ReadFileToolResolver(BaseToolResolver):
|
|
|
15
25
|
super().__init__(agent, tool, args)
|
|
16
26
|
self.tool: ReadFileTool = tool # For type hinting
|
|
17
27
|
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
28
|
+
self.context_pruner = PruneContext(
|
|
29
|
+
max_tokens=self.args.context_prune_safe_zone_tokens,
|
|
30
|
+
args=self.args,
|
|
31
|
+
llm=self.agent.context_prune_llm
|
|
32
|
+
)
|
|
18
33
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
def _prune_file_content(self, content: str, file_path: str) -> str:
|
|
35
|
+
"""对文件内容进行剪枝处理"""
|
|
36
|
+
if not self.context_pruner:
|
|
37
|
+
return content
|
|
38
|
+
|
|
39
|
+
# 计算 token 数量
|
|
40
|
+
tokens = count_tokens(content)
|
|
41
|
+
if tokens <= self.args.context_prune_safe_zone_tokens:
|
|
42
|
+
return content
|
|
43
|
+
|
|
44
|
+
# 创建 SourceCode 对象
|
|
45
|
+
source_code = SourceCode(
|
|
46
|
+
module_name=file_path,
|
|
47
|
+
source_code=content,
|
|
48
|
+
tokens=tokens
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# 使用 context_pruner 进行剪枝
|
|
52
|
+
pruned_sources = self.context_pruner.handle_overflow(
|
|
53
|
+
file_sources=[source_code],
|
|
54
|
+
conversations=self.agent.current_conversations if self.agent else [],
|
|
55
|
+
strategy=self.args.context_prune_strategy
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not pruned_sources:
|
|
59
|
+
return content
|
|
60
|
+
|
|
61
|
+
return pruned_sources[0].source_code
|
|
24
62
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
def _read_file_content(self, file_path_to_read: str) -> str:
|
|
64
|
+
content = ""
|
|
65
|
+
ext = os.path.splitext(file_path_to_read)[1].lower()
|
|
28
66
|
|
|
67
|
+
if ext == '.pdf':
|
|
68
|
+
logger.info(f"Extracting text from PDF: {file_path_to_read}")
|
|
69
|
+
content = extract_text_from_pdf(file_path_to_read)
|
|
70
|
+
elif ext == '.docx':
|
|
71
|
+
logger.info(f"Extracting text from DOCX: {file_path_to_read}")
|
|
72
|
+
content = extract_text_from_docx(file_path_to_read)
|
|
73
|
+
elif ext in ('.pptx', '.ppt'):
|
|
74
|
+
logger.info(f"Extracting text from PPT/PPTX: {file_path_to_read}")
|
|
75
|
+
slide_texts = []
|
|
76
|
+
for slide_identifier, slide_text_content in extract_text_from_ppt(file_path_to_read):
|
|
77
|
+
slide_texts.append(f"--- Slide {slide_identifier} ---\n{slide_text_content}")
|
|
78
|
+
content = "\n\n".join(slide_texts) if slide_texts else ""
|
|
79
|
+
else:
|
|
80
|
+
logger.info(f"Reading plain text file: {file_path_to_read}")
|
|
81
|
+
with open(file_path_to_read, 'r', encoding='utf-8', errors='replace') as f:
|
|
82
|
+
content = f.read()
|
|
83
|
+
|
|
84
|
+
# 对内容进行剪枝处理
|
|
85
|
+
return self._prune_file_content(content, file_path_to_read)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_file_normal(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
89
|
+
"""Read file directly without using shadow manager"""
|
|
29
90
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
with open(shadow_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
36
|
-
content = f.read()
|
|
37
|
-
logger.info(f"[Shadow] Successfully read shadow file: {shadow_path}")
|
|
38
|
-
return ToolResult(success=True, message=f"Successfully read file (shadow): {file_path}", content=content)
|
|
39
|
-
except Exception as e:
|
|
40
|
-
pass
|
|
41
|
-
# else fallback to original file
|
|
42
|
-
# Fallback to original file
|
|
91
|
+
# Security check: ensure the path is within the source directory
|
|
92
|
+
if not abs_file_path.startswith(abs_project_dir):
|
|
93
|
+
return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
|
|
94
|
+
|
|
95
|
+
# Check if file exists
|
|
43
96
|
if not os.path.exists(abs_file_path):
|
|
44
|
-
return ToolResult(success=False, message=f"Error: File not found
|
|
97
|
+
return ToolResult(success=False, message=f"Error: File not found: {file_path}")
|
|
45
98
|
if not os.path.isfile(abs_file_path):
|
|
46
|
-
return ToolResult(success=False, message=f"Error:
|
|
99
|
+
return ToolResult(success=False, message=f"Error: Not a file: {file_path}")
|
|
47
100
|
|
|
48
|
-
|
|
49
|
-
|
|
101
|
+
# Read file content
|
|
102
|
+
content = self._read_file_content(abs_file_path)
|
|
50
103
|
logger.info(f"Successfully read file: {file_path}")
|
|
51
104
|
return ToolResult(success=True, message=f"Successfully read file: {file_path}", content=content)
|
|
52
105
|
except Exception as e:
|
|
53
106
|
logger.error(f"Error reading file '{file_path}': {str(e)}")
|
|
54
107
|
return ToolResult(success=False, message=f"An error occurred while reading the file: {str(e)}")
|
|
108
|
+
|
|
109
|
+
def resolve(self) -> ToolResult:
|
|
110
|
+
"""Resolve the read file tool by calling the appropriate implementation"""
|
|
111
|
+
file_path = self.tool.path
|
|
112
|
+
source_dir = self.args.source_dir or "."
|
|
113
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
114
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
115
|
+
|
|
116
|
+
return self.read_file_normal(file_path, source_dir, abs_project_dir, abs_file_path)
|
|
117
|
+
|
|
@@ -10,6 +10,9 @@ from autocoder.common.printer import Printer
|
|
|
10
10
|
from autocoder.common import AutoCoderArgs
|
|
11
11
|
from loguru import logger
|
|
12
12
|
from autocoder.common.auto_coder_lang import get_message_with_format
|
|
13
|
+
from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
|
|
14
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
|
|
15
|
+
from autocoder.linters.models import IssueSeverity, FileLintResult
|
|
13
16
|
if typing.TYPE_CHECKING:
|
|
14
17
|
from ..base_agent import BaseAgent
|
|
15
18
|
|
|
@@ -17,7 +20,9 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
17
20
|
def __init__(self, agent: Optional['BaseAgent'], tool: ReplaceInFileTool, args: AutoCoderArgs):
|
|
18
21
|
super().__init__(agent, tool, args)
|
|
19
22
|
self.tool: ReplaceInFileTool = tool # For type hinting
|
|
23
|
+
self.args = args
|
|
20
24
|
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
25
|
+
self.shadow_linter = self.agent.shadow_linter if self.agent else None
|
|
21
26
|
|
|
22
27
|
def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
|
|
23
28
|
"""
|
|
@@ -61,78 +66,155 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
61
66
|
logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
|
|
62
67
|
return blocks
|
|
63
68
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
def _filter_lint_issues(self, lint_result:FileLintResult, levels: List[IssueSeverity] = [IssueSeverity.ERROR, IssueSeverity.WARNING]):
|
|
70
|
+
"""
|
|
71
|
+
过滤 lint 结果,只保留指定级别的问题
|
|
72
|
+
|
|
73
|
+
参数:
|
|
74
|
+
lint_result: 单个文件的 lint 结果对象
|
|
75
|
+
levels: 要保留的问题级别列表,默认保留 ERROR 和 WARNING 级别
|
|
76
|
+
|
|
77
|
+
返回:
|
|
78
|
+
过滤后的 lint 结果对象(原对象的副本)
|
|
79
|
+
"""
|
|
80
|
+
if not lint_result or not lint_result.issues:
|
|
81
|
+
return lint_result
|
|
82
|
+
|
|
83
|
+
# 创建一个新的 issues 列表,只包含指定级别的问题
|
|
84
|
+
filtered_issues = []
|
|
85
|
+
for issue in lint_result.issues:
|
|
86
|
+
if issue.severity in levels:
|
|
87
|
+
filtered_issues.append(issue)
|
|
88
|
+
|
|
89
|
+
# 更新 lint_result 的副本
|
|
90
|
+
filtered_result = lint_result
|
|
91
|
+
filtered_result.issues = filtered_issues
|
|
92
|
+
|
|
93
|
+
# 更新计数
|
|
94
|
+
filtered_result.error_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.ERROR)
|
|
95
|
+
filtered_result.warning_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.WARNING)
|
|
96
|
+
filtered_result.info_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.INFO)
|
|
97
|
+
|
|
98
|
+
return filtered_result
|
|
99
|
+
|
|
100
|
+
def _format_lint_issues(self, lint_result:FileLintResult):
|
|
101
|
+
"""
|
|
102
|
+
将 lint 结果格式化为可读的文本格式
|
|
103
|
+
|
|
104
|
+
参数:
|
|
105
|
+
lint_result: 单个文件的 lint 结果对象
|
|
106
|
+
|
|
107
|
+
返回:
|
|
108
|
+
str: 格式化的问题描述
|
|
109
|
+
"""
|
|
110
|
+
formatted_issues = []
|
|
111
|
+
|
|
112
|
+
for issue in lint_result.issues:
|
|
113
|
+
severity = "错误" if issue.severity.value == 3 else "警告" if issue.severity.value == 2 else "信息"
|
|
114
|
+
line_info = f"第{issue.position.line}行"
|
|
115
|
+
if issue.position.column:
|
|
116
|
+
line_info += f", 第{issue.position.column}列"
|
|
117
|
+
|
|
118
|
+
formatted_issues.append(
|
|
119
|
+
f" - [{severity}] {line_info}: {issue.message} (规则: {issue.code})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return "\n".join(formatted_issues)
|
|
74
123
|
|
|
75
|
-
|
|
76
|
-
target_path = abs_file_path
|
|
77
|
-
if self.shadow_manager:
|
|
78
|
-
target_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
124
|
+
|
|
79
125
|
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
def replace_in_file_normal(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
|
|
127
|
+
"""Replace content in file directly without using shadow manager"""
|
|
82
128
|
try:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
original_content = f.read()
|
|
86
|
-
elif self.shadow_manager and os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
|
|
87
|
-
# If shadow doesn't exist, but original exists, read original content (create shadow implicitly later)
|
|
88
|
-
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
89
|
-
original_content = f.read()
|
|
90
|
-
# create parent dirs of shadow if needed
|
|
91
|
-
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
92
|
-
# write original content into shadow file as baseline
|
|
93
|
-
with open(target_path, 'w', encoding='utf-8') as f:
|
|
94
|
-
f.write(original_content)
|
|
95
|
-
logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
|
|
96
|
-
else:
|
|
129
|
+
# Read original content
|
|
130
|
+
if not os.path.exists(abs_file_path):
|
|
97
131
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
|
|
132
|
+
if not os.path.isfile(abs_file_path):
|
|
133
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.not_a_file", file_path=file_path))
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
|
|
135
|
+
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
136
|
+
original_content = f.read()
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
parsed_blocks = self.parse_diff(diff_content)
|
|
139
|
+
if not parsed_blocks:
|
|
140
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
current_content = original_content
|
|
143
|
+
applied_count = 0
|
|
144
|
+
errors = []
|
|
113
145
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
|
|
118
|
-
else:
|
|
119
|
-
error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
|
|
120
|
-
logger.warning(error_message)
|
|
121
|
-
context_start = max(0, original_content.find(search_block[:20]) - 100)
|
|
122
|
-
context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
|
|
123
|
-
logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
|
|
124
|
-
errors.append(error_message)
|
|
125
|
-
# continue applying remaining blocks
|
|
146
|
+
# Apply blocks sequentially
|
|
147
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
148
|
+
start_index = current_content.find(search_block)
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
if start_index != -1:
|
|
151
|
+
current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
|
|
152
|
+
applied_count += 1
|
|
153
|
+
logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
|
|
154
|
+
else:
|
|
155
|
+
error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
|
|
156
|
+
logger.warning(error_message)
|
|
157
|
+
context_start = max(0, original_content.find(search_block[:20]) - 100)
|
|
158
|
+
context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
|
|
159
|
+
logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
|
|
160
|
+
errors.append(error_message)
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
if applied_count == 0 and errors:
|
|
163
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
|
|
164
|
+
|
|
165
|
+
# Write the modified content back to file
|
|
166
|
+
if self.agent and self.agent.checkpoint_manager:
|
|
167
|
+
changes = {
|
|
168
|
+
file_path: CheckpointFileChange(
|
|
169
|
+
file_path=file_path,
|
|
170
|
+
content=current_content,
|
|
171
|
+
is_deletion=False,
|
|
172
|
+
is_new=True
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
change_group_id = self.args.event_file
|
|
176
|
+
|
|
177
|
+
self.agent.checkpoint_manager.apply_changes_with_conversation(
|
|
178
|
+
changes=changes,
|
|
179
|
+
conversations=self.agent.current_conversations,
|
|
180
|
+
change_group_id=change_group_id,
|
|
181
|
+
metadata={"event_file": self.args.event_file}
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
with open(abs_file_path, 'w', encoding='utf-8') as f:
|
|
185
|
+
f.write(current_content)
|
|
186
|
+
|
|
134
187
|
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
135
188
|
|
|
189
|
+
# 新增:执行代码质量检查
|
|
190
|
+
lint_results = None
|
|
191
|
+
lint_message = ""
|
|
192
|
+
formatted_issues = ""
|
|
193
|
+
has_lint_issues = False
|
|
194
|
+
|
|
195
|
+
# 检查是否启用了Lint功能
|
|
196
|
+
enable_lint = self.args.enable_auto_fix_lint
|
|
197
|
+
logger.info(f"检查Lint功能状态: enable_lint={enable_lint}")
|
|
198
|
+
|
|
199
|
+
if enable_lint:
|
|
200
|
+
try:
|
|
201
|
+
if self.agent.linter:
|
|
202
|
+
lint_results = self.agent.linter.lint_file(file_path)
|
|
203
|
+
if lint_results and lint_results.issues:
|
|
204
|
+
# 过滤 lint 结果,只保留 ERROR 和 WARNING 级别的问题
|
|
205
|
+
filtered_results = self._filter_lint_issues(lint_results)
|
|
206
|
+
if filtered_results.issues:
|
|
207
|
+
has_lint_issues = True
|
|
208
|
+
# 格式化 lint 问题
|
|
209
|
+
formatted_issues = self._format_lint_issues(filtered_results)
|
|
210
|
+
lint_message = f"\n\n代码质量检查发现 {len(filtered_results.issues)} 个问题"
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Lint 检查失败: {str(e)}")
|
|
213
|
+
lint_message = "\n\n尝试进行代码质量检查时出错。"
|
|
214
|
+
else:
|
|
215
|
+
logger.info("代码质量检查已禁用")
|
|
216
|
+
|
|
217
|
+
# 构建包含 lint 结果的返回消息
|
|
136
218
|
if errors:
|
|
137
219
|
message = get_message_with_format("replace_in_file.apply_success_with_warnings",
|
|
138
220
|
applied=applied_count,
|
|
@@ -143,14 +225,41 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
143
225
|
message = get_message_with_format("replace_in_file.apply_success",
|
|
144
226
|
applied=applied_count,
|
|
145
227
|
total=len(parsed_blocks),
|
|
146
|
-
file_path=file_path)
|
|
228
|
+
file_path=file_path)
|
|
147
229
|
|
|
148
230
|
# 变更跟踪,回调AgenticEdit
|
|
149
231
|
if self.agent:
|
|
150
232
|
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
151
233
|
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
234
|
+
|
|
235
|
+
# 附加 lint 结果到返回内容
|
|
236
|
+
result_content = {
|
|
237
|
+
"content": current_content,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# 只有在启用Lint时才添加Lint结果
|
|
241
|
+
if enable_lint:
|
|
242
|
+
message = message + "\n" + lint_message
|
|
243
|
+
result_content["lint_results"] = {
|
|
244
|
+
"has_issues": has_lint_issues,
|
|
245
|
+
"issues": formatted_issues if has_lint_issues else None
|
|
246
|
+
}
|
|
152
247
|
|
|
153
|
-
return ToolResult(success=True, message=message, content=
|
|
248
|
+
return ToolResult(success=True, message=message, content=result_content)
|
|
154
249
|
except Exception as e:
|
|
155
250
|
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
156
251
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
|
|
252
|
+
|
|
253
|
+
def resolve(self) -> ToolResult:
|
|
254
|
+
"""Resolve the replace in file tool by calling the appropriate implementation"""
|
|
255
|
+
file_path = self.tool.path
|
|
256
|
+
diff_content = self.tool.diff
|
|
257
|
+
source_dir = self.args.source_dir or "."
|
|
258
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
259
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
260
|
+
|
|
261
|
+
# Security check
|
|
262
|
+
if not abs_file_path.startswith(abs_project_dir):
|
|
263
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
|
|
264
|
+
|
|
265
|
+
return self.replace_in_file_normal(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)
|