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.

Files changed (57) hide show
  1. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/METADATA +2 -1
  2. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/RECORD +57 -29
  3. autocoder/agent/auto_learn.py +249 -262
  4. autocoder/agent/base_agentic/__init__.py +0 -0
  5. autocoder/agent/base_agentic/agent_hub.py +169 -0
  6. autocoder/agent/base_agentic/agentic_lang.py +112 -0
  7. autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
  8. autocoder/agent/base_agentic/base_agent.py +1582 -0
  9. autocoder/agent/base_agentic/default_tools.py +683 -0
  10. autocoder/agent/base_agentic/test_base_agent.py +82 -0
  11. autocoder/agent/base_agentic/tool_registry.py +425 -0
  12. autocoder/agent/base_agentic/tools/__init__.py +12 -0
  13. autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
  14. autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
  15. autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
  16. autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
  17. autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
  18. autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
  19. autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
  20. autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
  21. autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
  22. autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
  23. autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
  24. autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
  25. autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
  26. autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
  27. autocoder/agent/base_agentic/types.py +189 -0
  28. autocoder/agent/base_agentic/utils.py +100 -0
  29. autocoder/auto_coder.py +1 -1
  30. autocoder/auto_coder_runner.py +36 -14
  31. autocoder/chat/conf_command.py +11 -10
  32. autocoder/commands/auto_command.py +227 -159
  33. autocoder/common/__init__.py +2 -2
  34. autocoder/common/ignorefiles/ignore_file_utils.py +12 -8
  35. autocoder/common/result_manager.py +10 -2
  36. autocoder/common/rulefiles/autocoderrules_utils.py +169 -0
  37. autocoder/common/save_formatted_log.py +1 -1
  38. autocoder/common/v2/agent/agentic_edit.py +53 -41
  39. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +15 -12
  40. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +73 -1
  41. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +132 -4
  42. autocoder/common/v2/agent/agentic_edit_types.py +1 -2
  43. autocoder/common/v2/agent/agentic_tool_display.py +2 -3
  44. autocoder/common/v2/code_auto_generate_editblock.py +3 -1
  45. autocoder/index/index.py +14 -8
  46. autocoder/privacy/model_filter.py +297 -35
  47. autocoder/rag/long_context_rag.py +424 -397
  48. autocoder/rag/test_doc_filter.py +393 -0
  49. autocoder/rag/test_long_context_rag.py +473 -0
  50. autocoder/rag/test_token_limiter.py +342 -0
  51. autocoder/shadows/shadow_manager.py +1 -3
  52. autocoder/utils/_markitdown.py +22 -3
  53. autocoder/version.py +1 -1
  54. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/LICENSE +0 -0
  55. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/WHEEL +0 -0
  56. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/entry_points.txt +0 -0
  57. {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)}")