autocoder-nano 0.1.30__py3-none-any.whl → 0.1.33__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.
Files changed (34) hide show
  1. autocoder_nano/agent/agent_base.py +4 -4
  2. autocoder_nano/agent/agentic_edit.py +1584 -0
  3. autocoder_nano/agent/agentic_edit_tools/__init__.py +28 -0
  4. autocoder_nano/agent/agentic_edit_tools/ask_followup_question_tool.py +51 -0
  5. autocoder_nano/agent/agentic_edit_tools/attempt_completion_tool.py +36 -0
  6. autocoder_nano/agent/agentic_edit_tools/base_tool_resolver.py +31 -0
  7. autocoder_nano/agent/agentic_edit_tools/execute_command_tool.py +65 -0
  8. autocoder_nano/agent/agentic_edit_tools/list_code_definition_names_tool.py +78 -0
  9. autocoder_nano/agent/agentic_edit_tools/list_files_tool.py +123 -0
  10. autocoder_nano/agent/agentic_edit_tools/list_package_info_tool.py +42 -0
  11. autocoder_nano/agent/agentic_edit_tools/plan_mode_respond_tool.py +35 -0
  12. autocoder_nano/agent/agentic_edit_tools/read_file_tool.py +73 -0
  13. autocoder_nano/agent/agentic_edit_tools/replace_in_file_tool.py +148 -0
  14. autocoder_nano/agent/agentic_edit_tools/search_files_tool.py +135 -0
  15. autocoder_nano/agent/agentic_edit_tools/write_to_file_tool.py +57 -0
  16. autocoder_nano/agent/agentic_edit_types.py +151 -0
  17. autocoder_nano/auto_coder_nano.py +145 -91
  18. autocoder_nano/git_utils.py +63 -1
  19. autocoder_nano/llm_client.py +170 -3
  20. autocoder_nano/llm_types.py +53 -14
  21. autocoder_nano/rules/rules_learn.py +221 -0
  22. autocoder_nano/templates.py +1 -1
  23. autocoder_nano/utils/formatted_log_utils.py +128 -0
  24. autocoder_nano/utils/printer_utils.py +5 -4
  25. autocoder_nano/utils/shell_utils.py +85 -0
  26. autocoder_nano/version.py +1 -1
  27. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/METADATA +3 -2
  28. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/RECORD +33 -16
  29. autocoder_nano/agent/new/auto_new_project.py +0 -278
  30. /autocoder_nano/{agent/new → rules}/__init__.py +0 -0
  31. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/LICENSE +0 -0
  32. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/WHEEL +0 -0
  33. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/entry_points.txt +0 -0
  34. {autocoder_nano-0.1.30.dist-info → autocoder_nano-0.1.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,148 @@
1
+ import os
2
+ import typing
3
+ from typing import Tuple
4
+
5
+ from autocoder_nano.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
6
+ from autocoder_nano.agent.agentic_edit_types import *
7
+ from autocoder_nano.llm_types import AutoCoderArgs
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from autocoder_nano.agent.agentic_edit import AgenticEdit
11
+
12
+
13
+ class ReplaceInFileToolResolver(BaseToolResolver):
14
+ def __init__(self, agent: Optional['AgenticEdit'], tool: ReplaceInFileTool, args: AutoCoderArgs):
15
+ super().__init__(agent, tool, args)
16
+ self.tool: ReplaceInFileTool = tool # For type hinting
17
+ self.args = args
18
+
19
+ @staticmethod
20
+ def parse_diff(diff_content: str) -> List[Tuple[str, str]]:
21
+ """
22
+ Parses the diff content into a list of (search_block, replace_block) tuples.
23
+ """
24
+ blocks = []
25
+ lines = diff_content.splitlines(keepends=True)
26
+ i = 0
27
+ n = len(lines)
28
+
29
+ while i < n:
30
+ line = lines[i]
31
+ if line.strip() == "<<<<<<< SEARCH":
32
+ i += 1
33
+ search_lines = []
34
+ # Accumulate search block
35
+ while i < n and lines[i].strip() != "=======":
36
+ search_lines.append(lines[i])
37
+ i += 1
38
+ if i >= n:
39
+ # warning: Unterminated SEARCH block found in diff content.
40
+ break
41
+ i += 1 # skip '======='
42
+ replace_lines = []
43
+ # Accumulate replace block
44
+ while i < n and lines[i].strip() != ">>>>>>> REPLACE":
45
+ replace_lines.append(lines[i])
46
+ i += 1
47
+ if i >= n:
48
+ # warning: Unterminated REPLACE block found in diff content.
49
+ break
50
+ i += 1 # skip '>>>>>>> REPLACE'
51
+
52
+ search_block = ''.join(search_lines)
53
+ replace_block = ''.join(replace_lines)
54
+ blocks.append((search_block, replace_block))
55
+ else:
56
+ i += 1
57
+
58
+ if not blocks and diff_content.strip():
59
+ pass
60
+ # warning: Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}
61
+ return blocks
62
+
63
+ def replace_in_file_normal(
64
+ self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str
65
+ ) -> ToolResult:
66
+ """Replace content in file directly without using shadow manager"""
67
+ try:
68
+ if not os.path.exists(abs_file_path):
69
+ return ToolResult(success=False, message=f"错误:未找到文件路径:{file_path}")
70
+ if not os.path.isfile(abs_file_path):
71
+ return ToolResult(success=False, message=f"错误:该路径不是文件:{file_path}")
72
+
73
+ with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
74
+ original_content = f.read()
75
+
76
+ parsed_blocks = self.parse_diff(diff_content)
77
+ if not parsed_blocks:
78
+ return ToolResult(success=False, message="错误:在提供的diff中未找到有效的SEARCH/REPLACE代码块. ")
79
+
80
+ current_content = original_content
81
+ applied_count = 0
82
+ errors = []
83
+
84
+ # Apply blocks sequentially
85
+ for i, (search_block, replace_block) in enumerate(parsed_blocks):
86
+ start_index = current_content.find(search_block)
87
+
88
+ if start_index != -1:
89
+ current_content = current_content[:start_index] + replace_block + current_content[
90
+ start_index + len(search_block):]
91
+ applied_count += 1
92
+ # f"Applied SEARCH/REPLACE block {i + 1} in file {file_path}"
93
+ else:
94
+ error_message = (f"SEARCH block {i+1} not found in the current file content. Content to "
95
+ f"search:\n---\n{search_block}\n---")
96
+ # logger.warning(error_message)
97
+ context_start = max(0, original_content.find(search_block[:20]) - 100)
98
+ context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
99
+ # warning: f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---"
100
+ errors.append(error_message)
101
+
102
+ return_errors = "\n".join(errors)
103
+ if applied_count == 0 and errors:
104
+ return ToolResult(success=False, message=f"未能应用任何更改, 错误信息: {return_errors}")
105
+
106
+ # todo: 应该是先备份,再写入, 参考 autocoder checkpoint_manager
107
+
108
+ with open(abs_file_path, 'w', encoding='utf-8') as f:
109
+ f.write(current_content)
110
+
111
+ # info: f"已成功将 {applied_count}/{len(parsed_blocks)} 个更改应用到文件:{file_path}"
112
+
113
+ # todo: 写入后执行代码质量检查
114
+
115
+ # 构建包含 lint 结果的返回消息
116
+ if errors:
117
+ message = f"成功应用了 {applied_count}/{len(parsed_blocks)} 个更改到文件:{file_path}. \n警告信息: \n{return_errors}"
118
+ else:
119
+ message = f"成功应用了 {applied_count}/{len(parsed_blocks)} 个更改到文件:{file_path}"
120
+
121
+ # 变更跟踪,回调AgenticEdit
122
+ if self.agent:
123
+ rel_path = os.path.relpath(abs_file_path, abs_project_dir)
124
+ self.agent.record_file_change(
125
+ rel_path, "modified", diff=diff_content, content=current_content)
126
+
127
+ result_content = {"content": current_content}
128
+
129
+ return ToolResult(success=True, message=message, content=result_content)
130
+ except Exception as e:
131
+ return ToolResult(success=False,
132
+ message=f"An error occurred while processing the file '{file_path}': {str(e)}")
133
+
134
+ def resolve(self) -> ToolResult:
135
+ """Resolve the replacement in file tool by calling the appropriate implementation"""
136
+ file_path = self.tool.path
137
+ diff_content = self.tool.diff
138
+ source_dir = self.args.source_dir or "."
139
+ abs_project_dir = os.path.abspath(source_dir)
140
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
141
+
142
+ # 安全检查
143
+ if not abs_file_path.startswith(abs_project_dir):
144
+ return ToolResult(
145
+ success=False,
146
+ message=f"错误: 拒绝访问, 尝试修改项目目录之外的文件:{file_path}")
147
+
148
+ return self.replace_in_file_normal(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)
@@ -0,0 +1,135 @@
1
+ import glob
2
+ import os
3
+ import re
4
+ import typing
5
+ from typing import Optional, List, Dict, Any, Union
6
+
7
+ from autocoder_nano.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
8
+ from autocoder_nano.agent.agentic_edit_types import SearchFilesTool, ToolResult
9
+ from autocoder_nano.llm_types import AutoCoderArgs
10
+ from autocoder_nano.sys_utils import default_exclude_dirs
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from autocoder_nano.agent.agentic_edit import AgenticEdit
14
+
15
+
16
+ class SearchFilesToolResolver(BaseToolResolver):
17
+ def __init__(self, agent: Optional['AgenticEdit'], tool: SearchFilesTool, args: AutoCoderArgs):
18
+ super().__init__(agent, tool, args)
19
+ self.tool: SearchFilesTool = tool
20
+ self.exclude_files = args.exclude_files + default_exclude_dirs
21
+ self.exclude_patterns = self.parse_exclude_files(self.exclude_files)
22
+
23
+ @staticmethod
24
+ def parse_exclude_files(exclude_files):
25
+ if not exclude_files:
26
+ return []
27
+
28
+ if isinstance(exclude_files, str):
29
+ exclude_files = [exclude_files]
30
+
31
+ exclude_patterns = []
32
+ for pattern in exclude_files:
33
+ if pattern.startswith("regex://"):
34
+ pattern = pattern[8:]
35
+ exclude_patterns.append(re.compile(pattern))
36
+ else:
37
+ exclude_patterns.append(re.compile(pattern))
38
+ return exclude_patterns
39
+
40
+ def should_exclude(self, file_path):
41
+ for pattern in self.exclude_patterns:
42
+ if pattern.search(file_path):
43
+ return True
44
+ return False
45
+
46
+ def search_in_dir(
47
+ self, base_dir: str, regex_pattern: str, file_pattern: str, source_dir: str,
48
+ is_shadow: bool = False, compiled_regex: Optional[re.Pattern] = None
49
+ ) -> List[Dict[str, Any]]:
50
+ """Helper function to search in a directory"""
51
+ search_results = []
52
+ search_glob_pattern = os.path.join(base_dir, "**", file_pattern)
53
+
54
+ if compiled_regex is None:
55
+ compiled_regex = re.compile(regex_pattern)
56
+
57
+ for filepath in glob.glob(search_glob_pattern, recursive=True):
58
+ abs_path = os.path.abspath(filepath)
59
+ if self.should_exclude(abs_path):
60
+ continue
61
+ if os.path.isfile(filepath):
62
+ try:
63
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
64
+ lines = f.readlines()
65
+ for i, line in enumerate(lines):
66
+ if compiled_regex.search(line):
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
+
71
+ relative_path = os.path.relpath(filepath, source_dir)
72
+
73
+ search_results.append({
74
+ "path": relative_path,
75
+ "line_number": i + 1,
76
+ "match_line": line.strip(),
77
+ "context": context.strip()
78
+ })
79
+ except Exception as e:
80
+ # logger.warning(f"Could not read or process file {filepath}: {e}")
81
+ continue
82
+
83
+ return search_results
84
+
85
+ def search_files_normal(
86
+ self, search_path_str: str, regex_pattern: str, file_pattern: str, source_dir: str,
87
+ absolute_source_dir: str, absolute_search_path: str
88
+ ) -> Union[ToolResult, List[Dict[str, Any]]]:
89
+ """Search files directly without using shadow manager"""
90
+ # Security check
91
+ if not absolute_search_path.startswith(absolute_source_dir):
92
+ return ToolResult(success=False,
93
+ message=f"错误: 拒绝访问, 尝试搜索项目目录之外的文件: {search_path_str}")
94
+
95
+ # Validate that the directory exists
96
+ if not os.path.exists(absolute_search_path):
97
+ return ToolResult(success=False, message=f"错误: 搜索路径未找到 {search_path_str}")
98
+ if not os.path.isdir(absolute_search_path):
99
+ return ToolResult(success=False, message=f"错误: 搜错路径不是目录 {search_path_str}")
100
+
101
+ try:
102
+ compiled_regex = re.compile(regex_pattern)
103
+ # Search in the directory
104
+ search_results = self.search_in_dir(absolute_search_path, regex_pattern, file_pattern, source_dir,
105
+ is_shadow=False, compiled_regex=compiled_regex)
106
+ return search_results
107
+ except re.error as e:
108
+ return ToolResult(success=False, message=f"无效的正则表达式: {e}")
109
+ except Exception as e:
110
+ return ToolResult(success=False, message=f"搜索过程中出现未知错误: {str(e)}")
111
+
112
+ def resolve(self) -> ToolResult:
113
+ """Resolve the search files tool by calling the appropriate implementation"""
114
+ search_path_str = self.tool.path
115
+ regex_pattern = self.tool.regex
116
+ file_pattern = self.tool.file_pattern or "*"
117
+ source_dir = self.args.source_dir or "."
118
+ absolute_source_dir = os.path.abspath(source_dir)
119
+ absolute_search_path = os.path.abspath(os.path.join(source_dir, search_path_str))
120
+
121
+ result = self.search_files_normal(
122
+ search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path
123
+ )
124
+
125
+ if isinstance(result, list):
126
+ total_results = len(result)
127
+ if total_results > 200:
128
+ truncated_results = result[:200]
129
+ message = f"搜索完成. 总计匹配 {total_results} 条结果, 当前展示前 200 条"
130
+ return ToolResult(success=True, message=message, content=truncated_results)
131
+ else:
132
+ message = f"搜索完成. 总计匹配 {total_results} 条结果."
133
+ return ToolResult(success=True, message=message, content=result)
134
+ else:
135
+ return result
@@ -0,0 +1,57 @@
1
+ import os
2
+ import typing
3
+ from typing import Optional
4
+
5
+ from autocoder_nano.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
6
+ from autocoder_nano.agent.agentic_edit_types import WriteToFileTool, ToolResult
7
+ from autocoder_nano.llm_types import AutoCoderArgs
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from autocoder_nano.agent.agentic_edit import AgenticEdit
11
+
12
+
13
+ class WriteToFileToolResolver(BaseToolResolver):
14
+ def __init__(self, agent: Optional['AgenticEdit'], tool: WriteToFileTool, args: AutoCoderArgs):
15
+ super().__init__(agent, tool, args)
16
+ self.tool: WriteToFileTool = tool # For type hinting
17
+ self.args = args
18
+
19
+ def write_file_normal(self, file_path: str, content: str, source_dir: str, abs_project_dir: str,
20
+ abs_file_path: str) -> ToolResult:
21
+ """Write file directly without using shadow manager"""
22
+ try:
23
+ os.makedirs(os.path.dirname(abs_file_path), exist_ok=True)
24
+
25
+ if self.agent:
26
+ rel_path = os.path.relpath(abs_file_path, abs_project_dir)
27
+ self.agent.record_file_change(rel_path, "added", diff=None, content=content)
28
+
29
+ # todo: 应该是先备份,再写入, 参考 autocoder checkpoint_manager
30
+
31
+ with open(abs_file_path, 'w', encoding='utf-8') as f:
32
+ f.write(content)
33
+
34
+ # todo: 写入后执行代码质量检查
35
+
36
+ message = f"{file_path}"
37
+ result_content = {"content": content}
38
+
39
+ return ToolResult(success=True, message=message, content=result_content)
40
+ except Exception as e:
41
+ return ToolResult(success=False, message=f"An error occurred while writing to the file: {str(e)}")
42
+
43
+ def resolve(self) -> ToolResult:
44
+ """Resolve the write file tool by calling the appropriate implementation"""
45
+ file_path = self.tool.path
46
+ content = self.tool.content
47
+ source_dir = self.args.source_dir or "."
48
+ abs_project_dir = os.path.abspath(source_dir)
49
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
50
+
51
+ # Security check: ensure the path is within the source directory
52
+ if not abs_file_path.startswith(abs_project_dir):
53
+ return ToolResult(
54
+ success=False,
55
+ message=f"错误: 拒绝访问, 尝试修改项目目录之外的文件:{file_path}")
56
+
57
+ return self.write_file_normal(file_path, content, source_dir, abs_project_dir, abs_file_path)
@@ -0,0 +1,151 @@
1
+ from typing import List, Optional, Dict, Type, Any
2
+
3
+ from pydantic import BaseModel, SkipValidation
4
+
5
+
6
+ class FileChangeEntry(BaseModel):
7
+ """ 文件变更条目,用于记录文件的变更信息 """
8
+ type: str # 'added' 或 'modified'
9
+ diffs: List[str] = [] # 使用 replace_in_file 时,记录 diff 内容
10
+ content: Optional[str] = None # 使用 write_to_file 时,记录文件内容
11
+
12
+
13
+ class AgenticEditRequest(BaseModel):
14
+ user_input: str
15
+
16
+
17
+ # 工具的基本Pydantic模型
18
+ class BaseTool(BaseModel):
19
+ """ 代理工具的基类,所有工具类都应继承此类 """
20
+ pass
21
+
22
+
23
+ class ExecuteCommandTool(BaseTool):
24
+ command: str
25
+ requires_approval: bool
26
+
27
+
28
+ class ReadFileTool(BaseTool):
29
+ path: str
30
+
31
+
32
+ class WriteToFileTool(BaseTool):
33
+ path: str
34
+ content: str
35
+
36
+
37
+ class ReplaceInFileTool(BaseTool):
38
+ path: str
39
+ diff: str
40
+
41
+
42
+ class SearchFilesTool(BaseTool):
43
+ path: str
44
+ regex: str
45
+ file_pattern: Optional[str] = None
46
+
47
+
48
+ class ListFilesTool(BaseTool):
49
+ path: str
50
+ recursive: Optional[bool] = False
51
+
52
+
53
+ class ListCodeDefinitionNamesTool(BaseTool):
54
+ path: str
55
+
56
+
57
+ class AskFollowupQuestionTool(BaseTool):
58
+ question: str
59
+ options: Optional[List[str]] = None
60
+
61
+
62
+ class AttemptCompletionTool(BaseTool):
63
+ result: str
64
+ command: Optional[str] = None
65
+
66
+
67
+ class PlanModeRespondTool(BaseTool):
68
+ response: str
69
+ options: Optional[List[str]] = None
70
+
71
+
72
+ class UseRAGTool(BaseTool):
73
+ server_name: str
74
+ query: str
75
+
76
+
77
+ class ListPackageInfoTool(BaseTool):
78
+ path: str # 源码包目录,相对路径或绝对路径
79
+
80
+
81
+ class LLMOutputEvent(BaseModel):
82
+ """Represents plain text output from the LLM."""
83
+ text: str
84
+
85
+
86
+ class LLMThinkingEvent(BaseModel):
87
+ """Represents text within <thinking> tags from the LLM."""
88
+ text: str
89
+
90
+
91
+ class ToolCallEvent(BaseModel):
92
+ """Represents the LLM deciding to call a tool."""
93
+ tool: SkipValidation[BaseTool] # Use SkipValidation as BaseTool itself is complex
94
+ tool_xml: str
95
+
96
+
97
+ # Result class used by Tool Resolvers
98
+ class ToolResult(BaseModel):
99
+ success: bool
100
+ message: str
101
+ content: Any = None # Can store file content, command output, etc.
102
+
103
+
104
+ class ToolResultEvent(BaseModel):
105
+ """Represents the result of executing a tool."""
106
+ tool_name: str
107
+ result: ToolResult
108
+
109
+
110
+ class TokenUsageEvent(BaseModel):
111
+ """Represents the result of executing a tool."""
112
+ usage: Any
113
+
114
+
115
+ class PlanModeRespondEvent(BaseModel):
116
+ """Represents the LLM attempting to complete the task."""
117
+ completion: SkipValidation[PlanModeRespondTool] # Skip validation
118
+ completion_xml: str
119
+
120
+
121
+ class CompletionEvent(BaseModel):
122
+ """Represents the LLM attempting to complete the task."""
123
+ completion: SkipValidation[AttemptCompletionTool] # Skip validation
124
+ completion_xml: str
125
+
126
+
127
+ class ErrorEvent(BaseModel):
128
+ """Represents an error during the process."""
129
+ message: str
130
+
131
+
132
+ class WindowLengthChangeEvent(BaseModel):
133
+ """Represents the token usage in the conversation window."""
134
+ tokens_used: int
135
+
136
+
137
+ # Mapping from tool tag names to Pydantic models
138
+ TOOL_MODEL_MAP: Dict[str, Type[BaseTool]] = {
139
+ "execute_command": ExecuteCommandTool,
140
+ "read_file": ReadFileTool,
141
+ "write_to_file": WriteToFileTool,
142
+ "replace_in_file": ReplaceInFileTool,
143
+ "search_files": SearchFilesTool,
144
+ "list_files": ListFilesTool,
145
+ "list_code_definition_names": ListCodeDefinitionNamesTool,
146
+ "ask_followup_question": AskFollowupQuestionTool,
147
+ "attempt_completion": AttemptCompletionTool,
148
+ "plan_mode_respond": PlanModeRespondTool,
149
+ "use_rag_tool": UseRAGTool,
150
+ "list_package_info": ListPackageInfoTool,
151
+ }