auto-coder 0.1.361__py3-none-any.whl → 0.1.363__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.361.dist-info → auto_coder-0.1.363.dist-info}/METADATA +2 -1
- {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/RECORD +57 -29
- autocoder/agent/auto_learn.py +249 -262
- autocoder/agent/base_agentic/__init__.py +0 -0
- autocoder/agent/base_agentic/agent_hub.py +169 -0
- autocoder/agent/base_agentic/agentic_lang.py +112 -0
- autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
- autocoder/agent/base_agentic/base_agent.py +1582 -0
- autocoder/agent/base_agentic/default_tools.py +683 -0
- autocoder/agent/base_agentic/test_base_agent.py +82 -0
- autocoder/agent/base_agentic/tool_registry.py +425 -0
- autocoder/agent/base_agentic/tools/__init__.py +12 -0
- autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
- autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
- autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
- autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
- autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
- autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
- autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
- autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
- autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
- autocoder/agent/base_agentic/types.py +189 -0
- autocoder/agent/base_agentic/utils.py +100 -0
- autocoder/auto_coder.py +1 -1
- autocoder/auto_coder_runner.py +36 -14
- autocoder/chat/conf_command.py +11 -10
- autocoder/commands/auto_command.py +227 -159
- autocoder/common/__init__.py +2 -2
- autocoder/common/ignorefiles/ignore_file_utils.py +12 -8
- autocoder/common/result_manager.py +10 -2
- autocoder/common/rulefiles/autocoderrules_utils.py +169 -0
- autocoder/common/save_formatted_log.py +1 -1
- autocoder/common/v2/agent/agentic_edit.py +53 -41
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +15 -12
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +73 -1
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +132 -4
- autocoder/common/v2/agent/agentic_edit_types.py +1 -2
- autocoder/common/v2/agent/agentic_tool_display.py +2 -3
- autocoder/common/v2/code_auto_generate_editblock.py +3 -1
- autocoder/index/index.py +14 -8
- autocoder/privacy/model_filter.py +297 -35
- autocoder/rag/long_context_rag.py +424 -397
- autocoder/rag/test_doc_filter.py +393 -0
- autocoder/rag/test_long_context_rag.py +473 -0
- autocoder/rag/test_token_limiter.py +342 -0
- autocoder/shadows/shadow_manager.py +1 -3
- autocoder/utils/_markitdown.py +22 -3
- autocoder/version.py +1 -1
- {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from autocoder.common import AutoCoderArgs
|
|
3
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
4
|
+
from autocoder.agent.base_agentic.types import BaseTool, ToolResult
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from ..base_agent import BaseAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 首先定义工具模型
|
|
13
|
+
class ExampleTool(BaseTool):
|
|
14
|
+
"""示例工具,只是一个演示"""
|
|
15
|
+
message: str
|
|
16
|
+
times: int = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 然后定义对应的解析器
|
|
20
|
+
class ExampleToolResolver(BaseToolResolver):
|
|
21
|
+
"""示例工具解析器"""
|
|
22
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: ExampleTool, args: AutoCoderArgs):
|
|
23
|
+
super().__init__(agent, tool, args)
|
|
24
|
+
self.tool: ExampleTool = tool # 类型提示
|
|
25
|
+
|
|
26
|
+
def resolve(self) -> ToolResult:
|
|
27
|
+
"""
|
|
28
|
+
执行工具逻辑
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
工具执行结果
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
# 简单示例:重复消息N次
|
|
35
|
+
result = self.tool.message * self.tool.times
|
|
36
|
+
return ToolResult(
|
|
37
|
+
success=True,
|
|
38
|
+
message="成功执行示例工具",
|
|
39
|
+
content=result
|
|
40
|
+
)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return ToolResult(
|
|
43
|
+
success=False,
|
|
44
|
+
message=f"执行示例工具时出错: {str(e)}",
|
|
45
|
+
content=None
|
|
46
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
5
|
+
from autocoder.agent.base_agentic.types import ExecuteCommandTool, ToolResult # Import ToolResult from types
|
|
6
|
+
from autocoder.common import shells
|
|
7
|
+
from autocoder.common.printer import Printer
|
|
8
|
+
from loguru import logger
|
|
9
|
+
import typing
|
|
10
|
+
from autocoder.common import AutoCoderArgs
|
|
11
|
+
from autocoder.events.event_manager_singleton import get_event_manager
|
|
12
|
+
from autocoder.run_context import get_run_context
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from ..base_agent import BaseAgent
|
|
15
|
+
|
|
16
|
+
class ExecuteCommandToolResolver(BaseToolResolver):
|
|
17
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: ExecuteCommandTool, args: AutoCoderArgs):
|
|
18
|
+
super().__init__(agent, tool, args)
|
|
19
|
+
self.tool: ExecuteCommandTool = tool # For type hinting
|
|
20
|
+
|
|
21
|
+
def resolve(self) -> ToolResult:
|
|
22
|
+
printer = Printer()
|
|
23
|
+
command = self.tool.command
|
|
24
|
+
requires_approval = self.tool.requires_approval
|
|
25
|
+
source_dir = self.args.source_dir or "."
|
|
26
|
+
|
|
27
|
+
# Basic security check (can be expanded)
|
|
28
|
+
if ";" in command or "&&" in command or "|" in command or "`" in command:
|
|
29
|
+
# Allow && for cd chaining, but be cautious
|
|
30
|
+
if not command.strip().startswith("cd ") and " && " in command:
|
|
31
|
+
pass # Allow cd chaining like 'cd subdir && command'
|
|
32
|
+
else:
|
|
33
|
+
return ToolResult(success=False, message=f"Command '{command}' contains potentially unsafe characters.")
|
|
34
|
+
|
|
35
|
+
# Approval mechanism (simplified)
|
|
36
|
+
if requires_approval:
|
|
37
|
+
# In a real scenario, this would involve user interaction
|
|
38
|
+
printer.print_str_in_terminal(f"Command requires approval: {command}")
|
|
39
|
+
# For now, let's assume approval is granted in non-interactive mode or handled elsewhere
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
printer.print_str_in_terminal(f"Executing command: {command} in {os.path.abspath(source_dir)}")
|
|
43
|
+
try:
|
|
44
|
+
# 使用封装的run_cmd方法执行命令
|
|
45
|
+
if get_run_context().is_web():
|
|
46
|
+
answer = get_event_manager(
|
|
47
|
+
self.args.event_file).ask_user(prompt=f"Allow to execute the `{command}`?",options=["yes","no"])
|
|
48
|
+
if answer == "yes":
|
|
49
|
+
pass
|
|
50
|
+
else:
|
|
51
|
+
return ToolResult(success=False, message=f"Command '{command}' execution denied by user.")
|
|
52
|
+
|
|
53
|
+
exit_code, output = run_cmd_subprocess(command, verbose=True, cwd=source_dir)
|
|
54
|
+
|
|
55
|
+
logger.info(f"Command executed: {command}")
|
|
56
|
+
logger.info(f"Return Code: {exit_code}")
|
|
57
|
+
if output:
|
|
58
|
+
logger.info(f"Output:\n{output}")
|
|
59
|
+
|
|
60
|
+
if exit_code == 0:
|
|
61
|
+
return ToolResult(success=True, message="Command executed successfully.", content=output)
|
|
62
|
+
else:
|
|
63
|
+
error_message = f"Command failed with return code {exit_code}.\nOutput:\n{output}"
|
|
64
|
+
return ToolResult(success=False, message=error_message, content={"output": output, "returncode": exit_code})
|
|
65
|
+
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return ToolResult(success=False, message=f"Error: The command '{command.split()[0]}' was not found. Please ensure it is installed and in the system's PATH.")
|
|
68
|
+
except PermissionError:
|
|
69
|
+
return ToolResult(success=False, message=f"Error: Permission denied when trying to execute '{command}'.")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Error executing command '{command}': {str(e)}")
|
|
72
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while executing the command: {str(e)}")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
3
|
+
from autocoder.agent.base_agentic.types import ListFilesTool, ToolResult # Import ToolResult from types
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
import fnmatch
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
from loguru import logger
|
|
9
|
+
import typing
|
|
10
|
+
from autocoder.common import AutoCoderArgs
|
|
11
|
+
|
|
12
|
+
from autocoder.common.ignorefiles.ignore_file_utils import should_ignore
|
|
13
|
+
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from ..base_agent import BaseAgent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ListFilesToolResolver(BaseToolResolver):
|
|
19
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: ListFilesTool, args: AutoCoderArgs):
|
|
20
|
+
super().__init__(agent, tool, args)
|
|
21
|
+
self.tool: ListFilesTool = tool # For type hinting
|
|
22
|
+
self.shadow_manager = self.agent.shadow_manager
|
|
23
|
+
|
|
24
|
+
def resolve(self) -> ToolResult:
|
|
25
|
+
list_path_str = self.tool.path
|
|
26
|
+
recursive = self.tool.recursive or False
|
|
27
|
+
source_dir = self.args.source_dir or "."
|
|
28
|
+
absolute_source_dir = os.path.abspath(source_dir)
|
|
29
|
+
absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
|
|
30
|
+
|
|
31
|
+
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
32
|
+
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
33
|
+
if is_outside_source:
|
|
34
|
+
logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
|
|
35
|
+
|
|
36
|
+
# Check if shadow directory exists for this path
|
|
37
|
+
shadow_exists = False
|
|
38
|
+
shadow_dir_path = None
|
|
39
|
+
if self.shadow_manager:
|
|
40
|
+
try:
|
|
41
|
+
shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_list_path)
|
|
42
|
+
if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
|
|
43
|
+
shadow_exists = True
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.warning(f"Error checking shadow path for {absolute_list_path}: {e}")
|
|
46
|
+
|
|
47
|
+
# Validate that at least one of the directories exists
|
|
48
|
+
if not os.path.exists(absolute_list_path) and not shadow_exists:
|
|
49
|
+
return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
|
|
50
|
+
if os.path.exists(absolute_list_path) and not os.path.isdir(absolute_list_path):
|
|
51
|
+
return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
|
|
52
|
+
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
53
|
+
return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
|
|
54
|
+
|
|
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
|
+
# Collect files from shadow and/or source directory
|
|
88
|
+
shadow_files_set = set()
|
|
89
|
+
if shadow_exists:
|
|
90
|
+
shadow_files_set = list_files_in_dir(shadow_dir_path)
|
|
91
|
+
|
|
92
|
+
source_files_set = set()
|
|
93
|
+
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)
|
|
95
|
+
|
|
96
|
+
# Merge results, prioritizing shadow files if exist
|
|
97
|
+
if shadow_exists:
|
|
98
|
+
merged_files = shadow_files_set.union(
|
|
99
|
+
{f for f in source_files_set if f not in shadow_files_set}
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
merged_files = source_files_set
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
|
|
106
|
+
logger.info(message)
|
|
107
|
+
return ToolResult(success=True, message=message, content=sorted(merged_files))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
110
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
import typing
|
|
4
|
+
from autocoder.common import AutoCoderArgs
|
|
5
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
6
|
+
from autocoder.agent.base_agentic.types import PlanModeRespondTool, ToolResult # Import ToolResult from types
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from ..base_agent import BaseAgent
|
|
11
|
+
|
|
12
|
+
class PlanModeRespondToolResolver(BaseToolResolver):
|
|
13
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: PlanModeRespondTool, args: AutoCoderArgs):
|
|
14
|
+
super().__init__(agent, tool, args)
|
|
15
|
+
self.tool: PlanModeRespondTool = tool # For type hinting
|
|
16
|
+
|
|
17
|
+
def resolve(self) -> ToolResult:
|
|
18
|
+
"""
|
|
19
|
+
Packages the response and options for Plan Mode interaction.
|
|
20
|
+
"""
|
|
21
|
+
response_text = self.tool.response
|
|
22
|
+
options = self.tool.options
|
|
23
|
+
logger.info(f"Resolving PlanModeRespondTool: Response='{response_text[:100]}...', Options={options}")
|
|
24
|
+
|
|
25
|
+
if not response_text:
|
|
26
|
+
return ToolResult(success=False, message="Error: Plan mode response cannot be empty.")
|
|
27
|
+
|
|
28
|
+
# The actual presentation happens outside the resolver.
|
|
29
|
+
result_content = {
|
|
30
|
+
"response": response_text,
|
|
31
|
+
"options": options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Indicate success in preparing the plan mode response data
|
|
35
|
+
return ToolResult(success=True, message="Plan mode response prepared.", content=result_content)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
4
|
+
from autocoder.agent.base_agentic.types import ReadFileTool, ToolResult # Import ToolResult from types
|
|
5
|
+
from loguru import logger
|
|
6
|
+
import typing
|
|
7
|
+
from autocoder.common import AutoCoderArgs
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from ..base_agent import BaseAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReadFileToolResolver(BaseToolResolver):
|
|
14
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: ReadFileTool, args: AutoCoderArgs):
|
|
15
|
+
super().__init__(agent, tool, args)
|
|
16
|
+
self.tool: ReadFileTool = tool # For type hinting
|
|
17
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
18
|
+
|
|
19
|
+
def resolve(self) -> ToolResult:
|
|
20
|
+
file_path = self.tool.path
|
|
21
|
+
source_dir = self.args.source_dir or "."
|
|
22
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
23
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
24
|
+
|
|
25
|
+
# # Security check: ensure the path is within the source directory
|
|
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}")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
try:
|
|
31
|
+
if self.shadow_manager:
|
|
32
|
+
shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
33
|
+
# If shadow file exists, read from it
|
|
34
|
+
if os.path.exists(shadow_path) and os.path.isfile(shadow_path):
|
|
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
|
|
43
|
+
if not os.path.exists(abs_file_path):
|
|
44
|
+
return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
|
|
45
|
+
if not os.path.isfile(abs_file_path):
|
|
46
|
+
return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
|
|
47
|
+
|
|
48
|
+
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
49
|
+
content = f.read()
|
|
50
|
+
logger.info(f"Successfully read file: {file_path}")
|
|
51
|
+
return ToolResult(success=True, message=f"Successfully read file: {file_path}", content=content)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Error reading file '{file_path}': {str(e)}")
|
|
54
|
+
return ToolResult(success=False, message=f"An error occurred while reading the file: {str(e)}")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import difflib
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
6
|
+
import typing
|
|
7
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
8
|
+
from autocoder.agent.base_agentic.types import ReplaceInFileTool, ToolResult # Import ToolResult from types
|
|
9
|
+
from autocoder.common.printer import Printer
|
|
10
|
+
from autocoder.common import AutoCoderArgs
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from autocoder.common.auto_coder_lang import get_message_with_format
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from ..base_agent import BaseAgent
|
|
15
|
+
|
|
16
|
+
class ReplaceInFileToolResolver(BaseToolResolver):
|
|
17
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: ReplaceInFileTool, args: AutoCoderArgs):
|
|
18
|
+
super().__init__(agent, tool, args)
|
|
19
|
+
self.tool: ReplaceInFileTool = tool # For type hinting
|
|
20
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
21
|
+
|
|
22
|
+
def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
|
|
23
|
+
"""
|
|
24
|
+
Parses the diff content into a list of (search_block, replace_block) tuples.
|
|
25
|
+
"""
|
|
26
|
+
blocks = []
|
|
27
|
+
lines = diff_content.splitlines(keepends=True)
|
|
28
|
+
i = 0
|
|
29
|
+
n = len(lines)
|
|
30
|
+
|
|
31
|
+
while i < n:
|
|
32
|
+
line = lines[i]
|
|
33
|
+
if line.strip() == "<<<<<<< SEARCH":
|
|
34
|
+
i += 1
|
|
35
|
+
search_lines = []
|
|
36
|
+
# Accumulate search block
|
|
37
|
+
while i < n and lines[i].strip() != "=======":
|
|
38
|
+
search_lines.append(lines[i])
|
|
39
|
+
i += 1
|
|
40
|
+
if i >= n:
|
|
41
|
+
logger.warning("Unterminated SEARCH block found in diff content.")
|
|
42
|
+
break
|
|
43
|
+
i += 1 # skip '======='
|
|
44
|
+
replace_lines = []
|
|
45
|
+
# Accumulate replace block
|
|
46
|
+
while i < n and lines[i].strip() != ">>>>>>> REPLACE":
|
|
47
|
+
replace_lines.append(lines[i])
|
|
48
|
+
i += 1
|
|
49
|
+
if i >= n:
|
|
50
|
+
logger.warning("Unterminated REPLACE block found in diff content.")
|
|
51
|
+
break
|
|
52
|
+
i += 1 # skip '>>>>>>> REPLACE'
|
|
53
|
+
|
|
54
|
+
search_block = ''.join(search_lines)
|
|
55
|
+
replace_block = ''.join(replace_lines)
|
|
56
|
+
blocks.append((search_block, replace_block))
|
|
57
|
+
else:
|
|
58
|
+
i += 1
|
|
59
|
+
|
|
60
|
+
if not blocks and diff_content.strip():
|
|
61
|
+
logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
|
|
62
|
+
return blocks
|
|
63
|
+
|
|
64
|
+
def resolve(self) -> ToolResult:
|
|
65
|
+
file_path = self.tool.path
|
|
66
|
+
diff_content = self.tool.diff
|
|
67
|
+
source_dir = self.args.source_dir or "."
|
|
68
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
69
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
70
|
+
|
|
71
|
+
# Security check
|
|
72
|
+
if not abs_file_path.startswith(abs_project_dir):
|
|
73
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
|
|
74
|
+
|
|
75
|
+
# Determine target path: shadow file if shadow_manager exists
|
|
76
|
+
target_path = abs_file_path
|
|
77
|
+
if self.shadow_manager:
|
|
78
|
+
target_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
79
|
+
|
|
80
|
+
# If shadow file does not exist yet, but original file exists, copy original content into shadow first? No, just treat as normal file.
|
|
81
|
+
# For now, read from shadow if exists, else fallback to original file
|
|
82
|
+
try:
|
|
83
|
+
if os.path.exists(target_path) and os.path.isfile(target_path):
|
|
84
|
+
with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
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:
|
|
97
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
|
|
100
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
|
|
101
|
+
|
|
102
|
+
parsed_blocks = self.parse_diff(diff_content)
|
|
103
|
+
if not parsed_blocks:
|
|
104
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
|
|
105
|
+
|
|
106
|
+
current_content = original_content
|
|
107
|
+
applied_count = 0
|
|
108
|
+
errors = []
|
|
109
|
+
|
|
110
|
+
# Apply blocks sequentially
|
|
111
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
112
|
+
start_index = current_content.find(search_block)
|
|
113
|
+
|
|
114
|
+
if start_index != -1:
|
|
115
|
+
current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
|
|
116
|
+
applied_count += 1
|
|
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
|
|
126
|
+
|
|
127
|
+
if applied_count == 0 and errors:
|
|
128
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
132
|
+
with open(target_path, 'w', encoding='utf-8') as f:
|
|
133
|
+
f.write(current_content)
|
|
134
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
135
|
+
|
|
136
|
+
if errors:
|
|
137
|
+
message = get_message_with_format("replace_in_file.apply_success_with_warnings",
|
|
138
|
+
applied=applied_count,
|
|
139
|
+
total=len(parsed_blocks),
|
|
140
|
+
file_path=file_path,
|
|
141
|
+
errors="\n".join(errors))
|
|
142
|
+
else:
|
|
143
|
+
message = get_message_with_format("replace_in_file.apply_success",
|
|
144
|
+
applied=applied_count,
|
|
145
|
+
total=len(parsed_blocks),
|
|
146
|
+
file_path=file_path)
|
|
147
|
+
|
|
148
|
+
# 变更跟踪,回调AgenticEdit
|
|
149
|
+
if self.agent:
|
|
150
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
151
|
+
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
152
|
+
|
|
153
|
+
return ToolResult(success=True, message=message, content=current_content)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
156
|
+
return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import glob
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
|
|
6
|
+
from autocoder.agent.base_agentic.types import SearchFilesTool, ToolResult # Import ToolResult from types
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from autocoder.common import AutoCoderArgs
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from autocoder.common.ignorefiles.ignore_file_utils import should_ignore
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from ..base_agent import BaseAgent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SearchFilesToolResolver(BaseToolResolver):
|
|
18
|
+
def __init__(self, agent: Optional['BaseAgent'], tool: SearchFilesTool, args: AutoCoderArgs):
|
|
19
|
+
super().__init__(agent, tool, args)
|
|
20
|
+
self.tool: SearchFilesTool = tool
|
|
21
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
22
|
+
|
|
23
|
+
def resolve(self) -> ToolResult:
|
|
24
|
+
search_path_str = self.tool.path
|
|
25
|
+
regex_pattern = self.tool.regex
|
|
26
|
+
file_pattern = self.tool.file_pattern or "*"
|
|
27
|
+
source_dir = self.args.source_dir or "."
|
|
28
|
+
absolute_source_dir = os.path.abspath(source_dir)
|
|
29
|
+
absolute_search_path = os.path.abspath(os.path.join(source_dir, search_path_str))
|
|
30
|
+
|
|
31
|
+
# Security check
|
|
32
|
+
if not absolute_search_path.startswith(absolute_source_dir):
|
|
33
|
+
return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
|
|
34
|
+
|
|
35
|
+
# Check if shadow directory exists
|
|
36
|
+
shadow_exists = False
|
|
37
|
+
shadow_dir_path = None
|
|
38
|
+
if self.shadow_manager:
|
|
39
|
+
try:
|
|
40
|
+
shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_search_path)
|
|
41
|
+
if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
|
|
42
|
+
shadow_exists = True
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Error checking shadow path for {absolute_search_path}: {e}")
|
|
45
|
+
|
|
46
|
+
# Validate that at least one of the directories exists
|
|
47
|
+
if not os.path.exists(absolute_search_path) and not shadow_exists:
|
|
48
|
+
return ToolResult(success=False, message=f"Error: Search path not found: {search_path_str}")
|
|
49
|
+
if os.path.exists(absolute_search_path) and not os.path.isdir(absolute_search_path):
|
|
50
|
+
return ToolResult(success=False, message=f"Error: Search path is not a directory: {search_path_str}")
|
|
51
|
+
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
52
|
+
return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {shadow_dir_path}")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
compiled_regex = re.compile(regex_pattern)
|
|
56
|
+
|
|
57
|
+
# Helper function to search in a directory
|
|
58
|
+
def search_in_dir(base_dir, is_shadow=False):
|
|
59
|
+
search_results = []
|
|
60
|
+
search_glob_pattern = os.path.join(base_dir, "**", file_pattern)
|
|
61
|
+
|
|
62
|
+
logger.info(f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{base_dir}' (shadow: {is_shadow}) with ignore rules applied.")
|
|
63
|
+
|
|
64
|
+
for filepath in glob.glob(search_glob_pattern, recursive=True):
|
|
65
|
+
abs_path = os.path.abspath(filepath)
|
|
66
|
+
if should_ignore(abs_path):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if os.path.isfile(filepath):
|
|
70
|
+
try:
|
|
71
|
+
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
|
72
|
+
lines = f.readlines()
|
|
73
|
+
for i, line in enumerate(lines):
|
|
74
|
+
if compiled_regex.search(line):
|
|
75
|
+
context_start = max(0, i - 2)
|
|
76
|
+
context_end = min(len(lines), i + 3)
|
|
77
|
+
context = "".join([f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
|
|
78
|
+
|
|
79
|
+
if is_shadow and self.shadow_manager:
|
|
80
|
+
try:
|
|
81
|
+
abs_project_path = self.shadow_manager.from_shadow_path(filepath)
|
|
82
|
+
relative_path = os.path.relpath(abs_project_path, source_dir)
|
|
83
|
+
except Exception:
|
|
84
|
+
relative_path = os.path.relpath(filepath, source_dir)
|
|
85
|
+
else:
|
|
86
|
+
relative_path = os.path.relpath(filepath, source_dir)
|
|
87
|
+
|
|
88
|
+
search_results.append({
|
|
89
|
+
"path": relative_path,
|
|
90
|
+
"line_number": i + 1,
|
|
91
|
+
"match_line": line.strip(),
|
|
92
|
+
"context": context.strip()
|
|
93
|
+
})
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Could not read or process file {filepath}: {e}")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
return search_results
|
|
99
|
+
|
|
100
|
+
# Search in both directories and merge results
|
|
101
|
+
shadow_results = []
|
|
102
|
+
source_results = []
|
|
103
|
+
|
|
104
|
+
if shadow_exists:
|
|
105
|
+
shadow_results = search_in_dir(shadow_dir_path, is_shadow=True)
|
|
106
|
+
|
|
107
|
+
if os.path.exists(absolute_search_path) and os.path.isdir(absolute_search_path):
|
|
108
|
+
source_results = search_in_dir(absolute_search_path, is_shadow=False)
|
|
109
|
+
|
|
110
|
+
# Merge results, prioritizing shadow results
|
|
111
|
+
# Create a dictionary for quick lookup
|
|
112
|
+
results_dict = {}
|
|
113
|
+
for result in source_results:
|
|
114
|
+
key = (result["path"], result["line_number"])
|
|
115
|
+
results_dict[key] = result
|
|
116
|
+
|
|
117
|
+
# Override with shadow results
|
|
118
|
+
for result in shadow_results:
|
|
119
|
+
key = (result["path"], result["line_number"])
|
|
120
|
+
results_dict[key] = result
|
|
121
|
+
|
|
122
|
+
# Convert back to list
|
|
123
|
+
merged_results = list(results_dict.values())
|
|
124
|
+
|
|
125
|
+
message = f"Search completed. Found {len(merged_results)} matches."
|
|
126
|
+
logger.info(message)
|
|
127
|
+
return ToolResult(success=True, message=message, content=merged_results)
|
|
128
|
+
|
|
129
|
+
except re.error as e:
|
|
130
|
+
logger.error(f"Invalid regex pattern '{regex_pattern}': {e}")
|
|
131
|
+
return ToolResult(success=False, message=f"Invalid regex pattern: {e}")
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Error during file search: {str(e)}")
|
|
134
|
+
return ToolResult(success=False, message=f"An unexpected error occurred during search: {str(e)}")
|