auto-coder 0.1.334__py3-none-any.whl → 0.1.336__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.334.dist-info → auto_coder-0.1.336.dist-info}/METADATA +2 -2
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.336.dist-info}/RECORD +67 -32
- autocoder/agent/agentic_edit.py +833 -0
- autocoder/agent/agentic_edit_tools/__init__.py +28 -0
- autocoder/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +32 -0
- autocoder/agent/agentic_edit_tools/attempt_completion_tool_resolver.py +29 -0
- autocoder/agent/agentic_edit_tools/base_tool_resolver.py +29 -0
- autocoder/agent/agentic_edit_tools/execute_command_tool_resolver.py +84 -0
- autocoder/agent/agentic_edit_tools/list_code_definition_names_tool_resolver.py +75 -0
- autocoder/agent/agentic_edit_tools/list_files_tool_resolver.py +62 -0
- autocoder/agent/agentic_edit_tools/plan_mode_respond_tool_resolver.py +30 -0
- autocoder/agent/agentic_edit_tools/read_file_tool_resolver.py +36 -0
- autocoder/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +95 -0
- autocoder/agent/agentic_edit_tools/search_files_tool_resolver.py +70 -0
- autocoder/agent/agentic_edit_tools/use_mcp_tool_resolver.py +55 -0
- autocoder/agent/agentic_edit_tools/write_to_file_tool_resolver.py +98 -0
- autocoder/agent/agentic_edit_types.py +124 -0
- autocoder/auto_coder.py +39 -18
- autocoder/auto_coder_rag.py +18 -9
- autocoder/auto_coder_runner.py +50 -5
- autocoder/chat_auto_coder_lang.py +18 -2
- autocoder/commands/tools.py +5 -1
- autocoder/common/__init__.py +2 -0
- autocoder/common/auto_coder_lang.py +40 -8
- autocoder/common/code_auto_generate_diff.py +1 -1
- autocoder/common/code_auto_generate_editblock.py +1 -1
- autocoder/common/code_auto_generate_strict_diff.py +1 -1
- autocoder/common/mcp_hub.py +185 -2
- autocoder/common/mcp_server.py +243 -306
- autocoder/common/mcp_server_install.py +269 -0
- autocoder/common/mcp_server_types.py +169 -0
- autocoder/common/stream_out_type.py +3 -0
- autocoder/common/v2/agent/__init__.py +0 -0
- autocoder/common/v2/agent/agentic_edit.py +1302 -0
- autocoder/common/v2/agent/agentic_edit_tools/__init__.py +28 -0
- autocoder/common/v2/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +70 -0
- autocoder/common/v2/agent/agentic_edit_tools/attempt_completion_tool_resolver.py +35 -0
- autocoder/common/v2/agent/agentic_edit_tools/base_tool_resolver.py +33 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +88 -0
- autocoder/common/v2/agent/agentic_edit_tools/list_code_definition_names_tool_resolver.py +80 -0
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +105 -0
- autocoder/common/v2/agent/agentic_edit_tools/plan_mode_respond_tool_resolver.py +35 -0
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +51 -0
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +144 -0
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +99 -0
- autocoder/common/v2/agent/agentic_edit_tools/use_mcp_tool_resolver.py +46 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +58 -0
- autocoder/common/v2/agent/agentic_edit_types.py +162 -0
- autocoder/common/v2/agent/agentic_tool_display.py +184 -0
- autocoder/common/v2/code_agentic_editblock_manager.py +812 -0
- autocoder/common/v2/code_auto_generate.py +1 -1
- autocoder/common/v2/code_auto_generate_diff.py +1 -1
- autocoder/common/v2/code_auto_generate_editblock.py +1 -1
- autocoder/common/v2/code_auto_generate_strict_diff.py +1 -1
- autocoder/common/v2/code_editblock_manager.py +151 -178
- autocoder/compilers/provided_compiler.py +3 -2
- autocoder/events/event_manager.py +4 -4
- autocoder/events/event_types.py +1 -0
- autocoder/memory/active_context_manager.py +2 -29
- autocoder/models.py +10 -2
- autocoder/shadows/shadow_manager.py +1 -1
- autocoder/utils/llms.py +4 -2
- autocoder/version.py +1 -1
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.336.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.336.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.336.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.336.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
4
|
+
import typing
|
|
5
|
+
from autocoder.common import AutoCoderArgs
|
|
6
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
7
|
+
from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult # Import ToolResult from types
|
|
8
|
+
from loguru import logger
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
11
|
+
|
|
12
|
+
class ReplaceInFileToolResolver(BaseToolResolver):
|
|
13
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ReplaceInFileTool, args: AutoCoderArgs):
|
|
14
|
+
super().__init__(agent, tool, args)
|
|
15
|
+
self.tool: ReplaceInFileTool = tool # For type hinting
|
|
16
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
17
|
+
|
|
18
|
+
def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
|
|
19
|
+
"""
|
|
20
|
+
Parses the diff content into a list of (search_block, replace_block) tuples.
|
|
21
|
+
"""
|
|
22
|
+
blocks = []
|
|
23
|
+
lines = diff_content.splitlines(keepends=True)
|
|
24
|
+
i = 0
|
|
25
|
+
n = len(lines)
|
|
26
|
+
|
|
27
|
+
while i < n:
|
|
28
|
+
line = lines[i]
|
|
29
|
+
if line.strip() == "<<<<<<< SEARCH":
|
|
30
|
+
i += 1
|
|
31
|
+
search_lines = []
|
|
32
|
+
# Accumulate search block
|
|
33
|
+
while i < n and lines[i].strip() != "=======":
|
|
34
|
+
search_lines.append(lines[i])
|
|
35
|
+
i += 1
|
|
36
|
+
if i >= n:
|
|
37
|
+
logger.warning("Unterminated SEARCH block found in diff content.")
|
|
38
|
+
break
|
|
39
|
+
i += 1 # skip '======='
|
|
40
|
+
replace_lines = []
|
|
41
|
+
# Accumulate replace block
|
|
42
|
+
while i < n and lines[i].strip() != ">>>>>>> REPLACE":
|
|
43
|
+
replace_lines.append(lines[i])
|
|
44
|
+
i += 1
|
|
45
|
+
if i >= n:
|
|
46
|
+
logger.warning("Unterminated REPLACE block found in diff content.")
|
|
47
|
+
break
|
|
48
|
+
i += 1 # skip '>>>>>>> REPLACE'
|
|
49
|
+
|
|
50
|
+
search_block = ''.join(search_lines)
|
|
51
|
+
replace_block = ''.join(replace_lines)
|
|
52
|
+
blocks.append((search_block, replace_block))
|
|
53
|
+
else:
|
|
54
|
+
i += 1
|
|
55
|
+
|
|
56
|
+
if not blocks and diff_content.strip():
|
|
57
|
+
logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
|
|
58
|
+
return blocks
|
|
59
|
+
|
|
60
|
+
def resolve(self) -> ToolResult:
|
|
61
|
+
file_path = self.tool.path
|
|
62
|
+
diff_content = self.tool.diff
|
|
63
|
+
source_dir = self.args.source_dir or "."
|
|
64
|
+
abs_project_dir = os.path.abspath(source_dir)
|
|
65
|
+
abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
|
|
66
|
+
|
|
67
|
+
# Security check
|
|
68
|
+
if not abs_file_path.startswith(abs_project_dir):
|
|
69
|
+
return ToolResult(success=False, message=f"Error: Access denied. Attempted to modify file outside the project directory: {file_path}")
|
|
70
|
+
|
|
71
|
+
# Determine target path: shadow file if shadow_manager exists
|
|
72
|
+
target_path = abs_file_path
|
|
73
|
+
if self.shadow_manager:
|
|
74
|
+
target_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
75
|
+
|
|
76
|
+
# If shadow file does not exist yet, but original file exists, copy original content into shadow first? No, just treat as normal file.
|
|
77
|
+
# For now, read from shadow if exists, else fallback to original file
|
|
78
|
+
try:
|
|
79
|
+
if os.path.exists(target_path) and os.path.isfile(target_path):
|
|
80
|
+
with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
81
|
+
original_content = f.read()
|
|
82
|
+
elif self.shadow_manager and os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
|
|
83
|
+
# If shadow doesn't exist, but original exists, read original content (create shadow implicitly later)
|
|
84
|
+
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
85
|
+
original_content = f.read()
|
|
86
|
+
# create parent dirs of shadow if needed
|
|
87
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
88
|
+
# write original content into shadow file as baseline
|
|
89
|
+
with open(target_path, 'w', encoding='utf-8') as f:
|
|
90
|
+
f.write(original_content)
|
|
91
|
+
logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
|
|
92
|
+
else:
|
|
93
|
+
return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
|
|
96
|
+
return ToolResult(success=False, message=f"An error occurred while reading the file for replacement: {str(e)}")
|
|
97
|
+
|
|
98
|
+
parsed_blocks = self.parse_diff(diff_content)
|
|
99
|
+
if not parsed_blocks:
|
|
100
|
+
return ToolResult(success=False, message="Error: No valid SEARCH/REPLACE blocks found in the provided diff.")
|
|
101
|
+
|
|
102
|
+
current_content = original_content
|
|
103
|
+
applied_count = 0
|
|
104
|
+
errors = []
|
|
105
|
+
|
|
106
|
+
# Apply blocks sequentially
|
|
107
|
+
for i, (search_block, replace_block) in enumerate(parsed_blocks):
|
|
108
|
+
start_index = current_content.find(search_block)
|
|
109
|
+
|
|
110
|
+
if start_index != -1:
|
|
111
|
+
current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
|
|
112
|
+
applied_count += 1
|
|
113
|
+
logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
|
|
114
|
+
else:
|
|
115
|
+
error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
|
|
116
|
+
logger.warning(error_message)
|
|
117
|
+
context_start = max(0, original_content.find(search_block[:20]) - 100)
|
|
118
|
+
context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
|
|
119
|
+
logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
|
|
120
|
+
errors.append(error_message)
|
|
121
|
+
# continue applying remaining blocks
|
|
122
|
+
|
|
123
|
+
if applied_count == 0 and errors:
|
|
124
|
+
return ToolResult(success=False, message=f"Failed to apply any changes. Errors:\n" + "\n".join(errors))
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
128
|
+
with open(target_path, 'w', encoding='utf-8') as f:
|
|
129
|
+
f.write(current_content)
|
|
130
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
131
|
+
|
|
132
|
+
message = f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}."
|
|
133
|
+
if errors:
|
|
134
|
+
message += "\nWarnings:\n" + "\n".join(errors)
|
|
135
|
+
|
|
136
|
+
# 变更跟踪,回调AgenticEdit
|
|
137
|
+
if self.agent:
|
|
138
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
139
|
+
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
140
|
+
|
|
141
|
+
return ToolResult(success=True, message=message, content=current_content)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
144
|
+
return ToolResult(success=False, message=f"An error occurred while writing the modified file: {str(e)}")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import glob
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
6
|
+
from autocoder.common.v2.agent.agentic_edit_types import SearchFilesTool, ToolResult # Import ToolResult from types
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from autocoder.common import AutoCoderArgs
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SearchFilesToolResolver(BaseToolResolver):
|
|
16
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: SearchFilesTool, args: AutoCoderArgs):
|
|
17
|
+
super().__init__(agent, tool, args)
|
|
18
|
+
self.tool: SearchFilesTool = tool # For type hinting
|
|
19
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
20
|
+
|
|
21
|
+
def resolve(self) -> ToolResult:
|
|
22
|
+
search_path_str = self.tool.path
|
|
23
|
+
regex_pattern = self.tool.regex
|
|
24
|
+
file_pattern = self.tool.file_pattern or "*" # Default to all files
|
|
25
|
+
source_dir = self.args.source_dir or "."
|
|
26
|
+
absolute_search_path = os.path.abspath(os.path.join(source_dir, search_path_str))
|
|
27
|
+
|
|
28
|
+
# Security check
|
|
29
|
+
if not absolute_search_path.startswith(os.path.abspath(source_dir)):
|
|
30
|
+
return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
|
|
31
|
+
|
|
32
|
+
# Determine search base directory: prefer shadow if exists
|
|
33
|
+
search_base_path = absolute_search_path
|
|
34
|
+
shadow_exists = False
|
|
35
|
+
if self.shadow_manager:
|
|
36
|
+
try:
|
|
37
|
+
shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_search_path)
|
|
38
|
+
if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
|
|
39
|
+
search_base_path = shadow_dir_path
|
|
40
|
+
shadow_exists = True
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.warning(f"Error checking shadow path for {absolute_search_path}: {e}")
|
|
43
|
+
|
|
44
|
+
# Validate that at least one of the directories exists
|
|
45
|
+
if not os.path.exists(absolute_search_path) and not shadow_exists:
|
|
46
|
+
return ToolResult(success=False, message=f"Error: Search path not found: {search_path_str}")
|
|
47
|
+
if os.path.exists(absolute_search_path) and not os.path.isdir(absolute_search_path):
|
|
48
|
+
return ToolResult(success=False, message=f"Error: Search path is not a directory: {search_path_str}")
|
|
49
|
+
if shadow_exists and not os.path.isdir(search_base_path):
|
|
50
|
+
return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {search_base_path}")
|
|
51
|
+
|
|
52
|
+
results = []
|
|
53
|
+
try:
|
|
54
|
+
compiled_regex = re.compile(regex_pattern)
|
|
55
|
+
search_glob_pattern = os.path.join(search_base_path, "**", file_pattern)
|
|
56
|
+
|
|
57
|
+
logger.info(f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{search_base_path}' (shadow: {shadow_exists})")
|
|
58
|
+
|
|
59
|
+
for filepath in glob.glob(search_glob_pattern, recursive=True):
|
|
60
|
+
if os.path.isfile(filepath):
|
|
61
|
+
try:
|
|
62
|
+
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
|
63
|
+
lines = f.readlines()
|
|
64
|
+
for i, line in enumerate(lines):
|
|
65
|
+
if compiled_regex.search(line):
|
|
66
|
+
# Provide context (e.g., line number and surrounding lines)
|
|
67
|
+
context_start = max(0, i - 2)
|
|
68
|
+
context_end = min(len(lines), i + 3)
|
|
69
|
+
context = "".join([f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
|
|
70
|
+
# For shadow files, convert to project-relative path
|
|
71
|
+
if shadow_exists and self.shadow_manager:
|
|
72
|
+
try:
|
|
73
|
+
abs_project_path = self.shadow_manager.from_shadow_path(filepath)
|
|
74
|
+
relative_path = os.path.relpath(abs_project_path, source_dir)
|
|
75
|
+
except Exception:
|
|
76
|
+
relative_path = os.path.relpath(filepath, source_dir)
|
|
77
|
+
else:
|
|
78
|
+
relative_path = os.path.relpath(filepath, source_dir)
|
|
79
|
+
results.append({
|
|
80
|
+
"path": relative_path,
|
|
81
|
+
"line_number": i + 1,
|
|
82
|
+
"match_line": line.strip(),
|
|
83
|
+
"context": context.strip()
|
|
84
|
+
})
|
|
85
|
+
# Limit results per file? Or overall? For now, collect all.
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Could not read or process file {filepath}: {e}")
|
|
88
|
+
continue # Skip files that can't be read
|
|
89
|
+
|
|
90
|
+
message = f"Search completed. Found {len(results)} matches."
|
|
91
|
+
logger.info(message)
|
|
92
|
+
return ToolResult(success=True, message=message, content=results)
|
|
93
|
+
|
|
94
|
+
except re.error as e:
|
|
95
|
+
logger.error(f"Invalid regex pattern '{regex_pattern}': {e}")
|
|
96
|
+
return ToolResult(success=False, message=f"Invalid regex pattern: {e}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error during file search: {str(e)}")
|
|
99
|
+
return ToolResult(success=False, message=f"An unexpected error occurred during search: {str(e)}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
import typing
|
|
3
|
+
from autocoder.common import AutoCoderArgs
|
|
4
|
+
from autocoder.common.mcp_server_types import McpRequest
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
6
|
+
from autocoder.common.v2.agent.agentic_edit_types import UseMcpTool, ToolResult # Import ToolResult from types
|
|
7
|
+
from autocoder.common.mcp_server import get_mcp_server
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UseMcpToolResolver(BaseToolResolver):
|
|
15
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: UseMcpTool, args: AutoCoderArgs):
|
|
16
|
+
super().__init__(agent, tool, args)
|
|
17
|
+
self.tool: UseMcpTool = tool # For type hinting
|
|
18
|
+
|
|
19
|
+
def resolve(self) -> ToolResult:
|
|
20
|
+
"""
|
|
21
|
+
Executes a tool via the Model Context Protocol (MCP) server.
|
|
22
|
+
"""
|
|
23
|
+
final_query = ""
|
|
24
|
+
server_name = self.tool.server_name
|
|
25
|
+
tool_name = self.tool.tool_name
|
|
26
|
+
|
|
27
|
+
if server_name:
|
|
28
|
+
final_query += f"{server_name}\n"
|
|
29
|
+
|
|
30
|
+
if tool_name:
|
|
31
|
+
final_query += f"{tool_name} is recommended for the following query:\n"
|
|
32
|
+
|
|
33
|
+
final_query += f"{self.tool.query}"
|
|
34
|
+
|
|
35
|
+
logger.info(f"Resolving UseMcpTool: Server='{server_name}', Tool='{tool_name}', Query='{final_query}'")
|
|
36
|
+
|
|
37
|
+
mcp_server = get_mcp_server()
|
|
38
|
+
response = mcp_server.send_request(
|
|
39
|
+
McpRequest(
|
|
40
|
+
query=final_query,
|
|
41
|
+
model=self.args.inference_model or self.args.model,
|
|
42
|
+
product_mode=self.args.product_mode
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return ToolResult(success=True, message=response.result)
|
|
46
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_types import WriteToFileTool, ToolResult # Import ToolResult from types
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from autocoder.common import AutoCoderArgs
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
11
|
+
|
|
12
|
+
class WriteToFileToolResolver(BaseToolResolver):
|
|
13
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: WriteToFileTool, args: AutoCoderArgs):
|
|
14
|
+
super().__init__(agent, tool, args)
|
|
15
|
+
self.tool: WriteToFileTool = tool # For type hinting
|
|
16
|
+
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
17
|
+
|
|
18
|
+
def resolve(self) -> ToolResult:
|
|
19
|
+
file_path = self.tool.path
|
|
20
|
+
content = self.tool.content
|
|
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 write file outside the project directory: {file_path}")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
if self.shadow_manager:
|
|
31
|
+
shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
|
|
32
|
+
# Ensure shadow directory exists
|
|
33
|
+
os.makedirs(os.path.dirname(shadow_path), exist_ok=True)
|
|
34
|
+
with open(shadow_path, 'w', encoding='utf-8') as f:
|
|
35
|
+
f.write(content)
|
|
36
|
+
logger.info(f"[Shadow] Successfully wrote shadow file: {shadow_path}")
|
|
37
|
+
|
|
38
|
+
# 回调AgenticEdit,记录变更
|
|
39
|
+
if self.agent:
|
|
40
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
41
|
+
self.agent.record_file_change(rel_path, "added", diff=None, content=content)
|
|
42
|
+
|
|
43
|
+
return ToolResult(success=True, message=f"Successfully wrote to file (shadow): {file_path}", content=content)
|
|
44
|
+
else:
|
|
45
|
+
# No shadow manager fallback to original file
|
|
46
|
+
os.makedirs(os.path.dirname(abs_file_path), exist_ok=True)
|
|
47
|
+
with open(abs_file_path, 'w', encoding='utf-8') as f:
|
|
48
|
+
f.write(content)
|
|
49
|
+
logger.info(f"Successfully wrote to file: {file_path}")
|
|
50
|
+
|
|
51
|
+
if self.agent:
|
|
52
|
+
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
53
|
+
self.agent.record_file_change(rel_path, "added", diff=None, content=content)
|
|
54
|
+
|
|
55
|
+
return ToolResult(success=True, message=f"Successfully wrote to file: {file_path}", content=content)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Error writing to file '{file_path}': {str(e)}")
|
|
58
|
+
return ToolResult(success=False, message=f"An error occurred while writing to the file: {str(e)}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import List, Dict, Any, Callable, Optional, Type
|
|
3
|
+
from pydantic import SkipValidation
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Result class used by Tool Resolvers
|
|
7
|
+
class ToolResult(BaseModel):
|
|
8
|
+
success: bool
|
|
9
|
+
message: str
|
|
10
|
+
content: Any = None # Can store file content, command output, etc.
|
|
11
|
+
|
|
12
|
+
# Pydantic Models for Tools
|
|
13
|
+
class BaseTool(BaseModel):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class ExecuteCommandTool(BaseTool):
|
|
17
|
+
command: str
|
|
18
|
+
requires_approval: bool
|
|
19
|
+
|
|
20
|
+
class ReadFileTool(BaseTool):
|
|
21
|
+
path: str
|
|
22
|
+
|
|
23
|
+
class WriteToFileTool(BaseTool):
|
|
24
|
+
path: str
|
|
25
|
+
content: str
|
|
26
|
+
|
|
27
|
+
class ReplaceInFileTool(BaseTool):
|
|
28
|
+
path: str
|
|
29
|
+
diff: str
|
|
30
|
+
|
|
31
|
+
class SearchFilesTool(BaseTool):
|
|
32
|
+
path: str
|
|
33
|
+
regex: str
|
|
34
|
+
file_pattern: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
class ListFilesTool(BaseTool):
|
|
37
|
+
path: str
|
|
38
|
+
recursive: Optional[bool] = False
|
|
39
|
+
|
|
40
|
+
class ListCodeDefinitionNamesTool(BaseTool):
|
|
41
|
+
path: str
|
|
42
|
+
|
|
43
|
+
class AskFollowupQuestionTool(BaseTool):
|
|
44
|
+
question: str
|
|
45
|
+
options: Optional[List[str]] = None
|
|
46
|
+
|
|
47
|
+
class AttemptCompletionTool(BaseTool):
|
|
48
|
+
result: str
|
|
49
|
+
command: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
class PlanModeRespondTool(BaseTool):
|
|
52
|
+
response: str
|
|
53
|
+
options: Optional[List[str]] = None
|
|
54
|
+
|
|
55
|
+
class UseMcpTool(BaseTool):
|
|
56
|
+
server_name: str
|
|
57
|
+
tool_name: str
|
|
58
|
+
query:str
|
|
59
|
+
|
|
60
|
+
# Event Types for Rich Output Streaming
|
|
61
|
+
class LLMOutputEvent(BaseModel):
|
|
62
|
+
"""Represents plain text output from the LLM."""
|
|
63
|
+
text: str
|
|
64
|
+
|
|
65
|
+
class LLMThinkingEvent(BaseModel):
|
|
66
|
+
"""Represents text within <thinking> tags from the LLM."""
|
|
67
|
+
text: str
|
|
68
|
+
|
|
69
|
+
class ToolCallEvent(BaseModel):
|
|
70
|
+
"""Represents the LLM deciding to call a tool."""
|
|
71
|
+
tool: SkipValidation[BaseTool] # Use SkipValidation as BaseTool itself is complex
|
|
72
|
+
tool_xml: str
|
|
73
|
+
|
|
74
|
+
class ToolResultEvent(BaseModel):
|
|
75
|
+
"""Represents the result of executing a tool."""
|
|
76
|
+
tool_name: str
|
|
77
|
+
result: ToolResult
|
|
78
|
+
|
|
79
|
+
class TokenUsageEvent(BaseModel):
|
|
80
|
+
"""Represents the result of executing a tool."""
|
|
81
|
+
usage: Any
|
|
82
|
+
|
|
83
|
+
class CompletionEvent(BaseModel):
|
|
84
|
+
"""Represents the LLM attempting to complete the task."""
|
|
85
|
+
completion: SkipValidation[AttemptCompletionTool] # Skip validation
|
|
86
|
+
completion_xml: str
|
|
87
|
+
|
|
88
|
+
class ErrorEvent(BaseModel):
|
|
89
|
+
"""Represents an error during the process."""
|
|
90
|
+
message: str
|
|
91
|
+
|
|
92
|
+
# Deprecated: Will be replaced by specific Event types
|
|
93
|
+
# class PlainTextOutput(BaseModel):
|
|
94
|
+
# text: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Mapping from tool tag names to Pydantic models
|
|
98
|
+
TOOL_MODEL_MAP: Dict[str, Type[BaseTool]] = {
|
|
99
|
+
"execute_command": ExecuteCommandTool,
|
|
100
|
+
"read_file": ReadFileTool,
|
|
101
|
+
"write_to_file": WriteToFileTool,
|
|
102
|
+
"replace_in_file": ReplaceInFileTool,
|
|
103
|
+
"search_files": SearchFilesTool,
|
|
104
|
+
"list_files": ListFilesTool,
|
|
105
|
+
"list_code_definition_names": ListCodeDefinitionNamesTool,
|
|
106
|
+
"ask_followup_question": AskFollowupQuestionTool,
|
|
107
|
+
"attempt_completion": AttemptCompletionTool,
|
|
108
|
+
"plan_mode_respond": PlanModeRespondTool,
|
|
109
|
+
"use_mcp_tool": UseMcpTool,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class FileChangeEntry(BaseModel):
|
|
113
|
+
type: str # 'added' or 'modified'
|
|
114
|
+
diffs: List[str] = []
|
|
115
|
+
content: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AgenticEditRequest(BaseModel):
|
|
119
|
+
user_input: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FileOperation(BaseModel):
|
|
123
|
+
path: str
|
|
124
|
+
operation: str # e.g., "MODIFY", "REFERENCE", "ADD", "REMOVE"
|
|
125
|
+
class MemoryConfig(BaseModel):
|
|
126
|
+
"""
|
|
127
|
+
A model to encapsulate memory configuration and operations.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
memory: Dict[str, Any]
|
|
131
|
+
save_memory_func: SkipValidation[Callable]
|
|
132
|
+
|
|
133
|
+
class Config:
|
|
134
|
+
arbitrary_types_allowed = True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CommandConfig(BaseModel):
|
|
138
|
+
coding: SkipValidation[Callable]
|
|
139
|
+
chat: SkipValidation[Callable]
|
|
140
|
+
add_files: SkipValidation[Callable]
|
|
141
|
+
remove_files: SkipValidation[Callable]
|
|
142
|
+
index_build: SkipValidation[Callable]
|
|
143
|
+
index_query: SkipValidation[Callable]
|
|
144
|
+
list_files: SkipValidation[Callable]
|
|
145
|
+
ask: SkipValidation[Callable]
|
|
146
|
+
revert: SkipValidation[Callable]
|
|
147
|
+
commit: SkipValidation[Callable]
|
|
148
|
+
help: SkipValidation[Callable]
|
|
149
|
+
exclude_dirs: SkipValidation[Callable]
|
|
150
|
+
summon: SkipValidation[Callable]
|
|
151
|
+
design: SkipValidation[Callable]
|
|
152
|
+
mcp: SkipValidation[Callable]
|
|
153
|
+
models: SkipValidation[Callable]
|
|
154
|
+
lib: SkipValidation[Callable]
|
|
155
|
+
execute_shell_command: SkipValidation[Callable]
|
|
156
|
+
generate_shell_command: SkipValidation[Callable]
|
|
157
|
+
conf_export: SkipValidation[Callable]
|
|
158
|
+
conf_import: SkipValidation[Callable]
|
|
159
|
+
index_export: SkipValidation[Callable]
|
|
160
|
+
index_import: SkipValidation[Callable]
|
|
161
|
+
exclude_files: SkipValidation[Callable]
|
|
162
|
+
|