auto-coder 0.1.399__py3-none-any.whl → 1.0.0__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.399.dist-info → auto_coder-1.0.0.dist-info}/METADATA +1 -1
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/RECORD +71 -35
- autocoder/agent/agentic_filter.py +1 -1
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +1 -1
- autocoder/auto_coder_runner.py +121 -26
- autocoder/chat_auto_coder.py +81 -22
- autocoder/commands/auto_command.py +1 -1
- autocoder/common/__init__.py +2 -2
- autocoder/common/ac_style_command_parser/parser.py +27 -12
- autocoder/common/auto_coder_lang.py +78 -0
- autocoder/common/command_completer_v2.py +1 -1
- autocoder/common/file_monitor/test_file_monitor.py +307 -0
- autocoder/common/git_utils.py +7 -2
- autocoder/common/pruner/__init__.py +0 -0
- autocoder/common/pruner/agentic_conversation_pruner.py +197 -0
- autocoder/common/pruner/context_pruner.py +574 -0
- autocoder/common/pruner/conversation_pruner.py +132 -0
- autocoder/common/pruner/test_agentic_conversation_pruner.py +342 -0
- autocoder/common/pruner/test_context_pruner.py +546 -0
- autocoder/common/pull_requests/__init__.py +256 -0
- autocoder/common/pull_requests/base_provider.py +191 -0
- autocoder/common/pull_requests/config.py +66 -0
- autocoder/common/pull_requests/example.py +1 -0
- autocoder/common/pull_requests/exceptions.py +46 -0
- autocoder/common/pull_requests/manager.py +201 -0
- autocoder/common/pull_requests/models.py +164 -0
- autocoder/common/pull_requests/providers/__init__.py +23 -0
- autocoder/common/pull_requests/providers/gitcode_provider.py +19 -0
- autocoder/common/pull_requests/providers/gitee_provider.py +20 -0
- autocoder/common/pull_requests/providers/github_provider.py +214 -0
- autocoder/common/pull_requests/providers/gitlab_provider.py +29 -0
- autocoder/common/pull_requests/test_module.py +1 -0
- autocoder/common/pull_requests/utils.py +344 -0
- autocoder/common/tokens/__init__.py +77 -0
- autocoder/common/tokens/counter.py +231 -0
- autocoder/common/tokens/file_detector.py +105 -0
- autocoder/common/tokens/filters.py +111 -0
- autocoder/common/tokens/models.py +28 -0
- autocoder/common/v2/agent/agentic_edit.py +538 -590
- autocoder/common/v2/agent/agentic_edit_tools/__init__.py +8 -1
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_read_tool_resolver.py +40 -0
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +43 -0
- autocoder/common/v2/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +8 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +33 -88
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +8 -8
- autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +118 -0
- autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +324 -0
- autocoder/common/v2/agent/agentic_edit_types.py +47 -4
- autocoder/common/v2/agent/runner/__init__.py +31 -0
- autocoder/common/v2/agent/runner/base_runner.py +106 -0
- autocoder/common/v2/agent/runner/event_runner.py +216 -0
- autocoder/common/v2/agent/runner/sdk_runner.py +40 -0
- autocoder/common/v2/agent/runner/terminal_runner.py +283 -0
- autocoder/common/v2/agent/runner/tool_display.py +191 -0
- autocoder/index/entry.py +1 -1
- autocoder/plugins/token_helper_plugin.py +107 -7
- autocoder/run_context.py +9 -0
- autocoder/sdk/__init__.py +114 -81
- autocoder/sdk/cli/handlers.py +2 -1
- autocoder/sdk/cli/main.py +9 -2
- autocoder/sdk/cli/options.py +4 -3
- autocoder/sdk/core/auto_coder_core.py +7 -152
- autocoder/sdk/core/bridge.py +5 -4
- autocoder/sdk/models/options.py +8 -6
- autocoder/version.py +1 -1
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,10 @@ from .attempt_completion_tool_resolver import AttemptCompletionToolResolver
|
|
|
12
12
|
from .plan_mode_respond_tool_resolver import PlanModeRespondToolResolver
|
|
13
13
|
from .use_mcp_tool_resolver import UseMcpToolResolver
|
|
14
14
|
from .use_rag_tool_resolver import UseRAGToolResolver
|
|
15
|
-
from .
|
|
15
|
+
from .todo_read_tool_resolver import TodoReadToolResolver
|
|
16
|
+
from .todo_write_tool_resolver import TodoWriteToolResolver
|
|
17
|
+
from .ac_mod_read_tool_resolver import ACModReadToolResolver
|
|
18
|
+
from .ac_mod_write_tool_resolver import ACModWriteToolResolver
|
|
16
19
|
|
|
17
20
|
__all__ = [
|
|
18
21
|
"BaseToolResolver",
|
|
@@ -29,4 +32,8 @@ __all__ = [
|
|
|
29
32
|
"UseMcpToolResolver",
|
|
30
33
|
"UseRAGToolResolver",
|
|
31
34
|
"ListPackageInfoToolResolver",
|
|
35
|
+
"TodoReadToolResolver",
|
|
36
|
+
"TodoWriteToolResolver",
|
|
37
|
+
"ACModReadToolResolver",
|
|
38
|
+
"ACModWriteToolResolver",
|
|
32
39
|
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_types import ACModReadTool, ToolResult
|
|
6
|
+
from loguru import logger
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
11
|
+
|
|
12
|
+
class ACModReadToolResolver(BaseToolResolver):
|
|
13
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ACModReadTool, args):
|
|
14
|
+
super().__init__(agent, tool, args)
|
|
15
|
+
self.tool: ACModReadTool = tool
|
|
16
|
+
|
|
17
|
+
def resolve(self) -> ToolResult:
|
|
18
|
+
source_dir = self.args.source_dir or "."
|
|
19
|
+
input_path = self.tool.path.strip()
|
|
20
|
+
abs_input_path = os.path.abspath(os.path.join(source_dir, input_path)) if not os.path.isabs(input_path) else input_path
|
|
21
|
+
|
|
22
|
+
# # 校验输入目录是否在项目目录内
|
|
23
|
+
# if not abs_input_path.startswith(abs_source_dir):
|
|
24
|
+
# return ToolResult(success=False, message=f"Error: Access denied. Path outside project: {self.tool.path}")
|
|
25
|
+
|
|
26
|
+
# 直接在输入文件夹中查找 .ac.mod.md 文件
|
|
27
|
+
mod_file_path = os.path.join(abs_input_path, ".ac.mod.md")
|
|
28
|
+
|
|
29
|
+
logger.info(f"Looking for package info at: {mod_file_path}")
|
|
30
|
+
|
|
31
|
+
if not os.path.exists(mod_file_path):
|
|
32
|
+
return ToolResult(success=True, message=f"The path {self.tool.path} is NOT AC module.", content="")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with open(mod_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
36
|
+
content = f.read()
|
|
37
|
+
return ToolResult(success=True, message=f"The path {self.tool.path} is AC module.", content=content)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"Error reading package info file: {e}")
|
|
40
|
+
return ToolResult(success=False, message=f"Error reading {self.tool.path}/.ac.mod.md file: {e}")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional, List, Tuple
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_tools.replace_in_file_tool_resolver import ReplaceInFileToolResolver
|
|
6
|
+
from autocoder.common.v2.agent.agentic_edit_types import ACModWriteTool, ToolResult,ReplaceInFileTool
|
|
7
|
+
from loguru import logger
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
12
|
+
|
|
13
|
+
class ACModWriteToolResolver(BaseToolResolver):
|
|
14
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ACModWriteTool, args):
|
|
15
|
+
super().__init__(agent, tool, args)
|
|
16
|
+
self.tool: ACModWriteTool = tool
|
|
17
|
+
|
|
18
|
+
def resolve(self) -> ToolResult:
|
|
19
|
+
source_dir = self.args.source_dir or "."
|
|
20
|
+
input_path = self.tool.path.strip()
|
|
21
|
+
|
|
22
|
+
# Check if the path already contains .ac.mod.md file name
|
|
23
|
+
if input_path.endswith('.ac.mod.md'):
|
|
24
|
+
# Path already includes the filename
|
|
25
|
+
if not os.path.isabs(input_path):
|
|
26
|
+
mod_file_path = os.path.abspath(os.path.join(source_dir, input_path))
|
|
27
|
+
else:
|
|
28
|
+
mod_file_path = input_path
|
|
29
|
+
|
|
30
|
+
# Create the parent directory if it doesn't exist
|
|
31
|
+
parent_dir = os.path.dirname(mod_file_path)
|
|
32
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
33
|
+
else:
|
|
34
|
+
# Path is a directory, need to append .ac.mod.md
|
|
35
|
+
abs_input_path = os.path.abspath(os.path.join(source_dir, input_path)) if not os.path.isabs(input_path) else input_path
|
|
36
|
+
|
|
37
|
+
# Create the directory if it doesn't exist
|
|
38
|
+
os.makedirs(abs_input_path, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
# Path to the .ac.mod.md file
|
|
41
|
+
mod_file_path = os.path.join(abs_input_path, ".ac.mod.md")
|
|
42
|
+
|
|
43
|
+
return ReplaceInFileToolResolver(self.agent, ReplaceInFileTool(path=mod_file_path, diff=self.tool.diff), self.args).resolve()
|
|
@@ -29,6 +29,14 @@ class AskFollowupQuestionToolResolver(BaseToolResolver):
|
|
|
29
29
|
Packages the question and options to be handled by the main loop/UI.
|
|
30
30
|
This resolver doesn't directly ask the user but prepares the data for it.
|
|
31
31
|
"""
|
|
32
|
+
# Check if running in CLI mode, if so return immediately with a message
|
|
33
|
+
# instructing the model to solve the problem on its own
|
|
34
|
+
if get_run_context().is_cli() or self.agent.args.enable_agentic_auto_approve:
|
|
35
|
+
return ToolResult(
|
|
36
|
+
success=False,
|
|
37
|
+
message="Remember, you cannot ask follow-up questions. Please try to solve the problem on your own using the available information. Do not give up and do your best to find a solution."
|
|
38
|
+
)
|
|
39
|
+
|
|
32
40
|
question = self.tool.question
|
|
33
41
|
options = self.tool.options or []
|
|
34
42
|
options_text = "\n".join([f"{i+1}. {option}" for i, option in enumerate(options)])
|
|
@@ -9,7 +9,7 @@ from autocoder.common import shells
|
|
|
9
9
|
from autocoder.common.printer import Printer
|
|
10
10
|
from loguru import logger
|
|
11
11
|
import typing
|
|
12
|
-
from autocoder.common.context_pruner import PruneContext
|
|
12
|
+
from autocoder.common.pruner.context_pruner import PruneContext
|
|
13
13
|
from autocoder.rag.token_counter import count_tokens
|
|
14
14
|
from autocoder.common import SourceCode
|
|
15
15
|
from autocoder.common import AutoCoderArgs
|
|
@@ -3,7 +3,7 @@ from typing import Dict, Any, Optional
|
|
|
3
3
|
from autocoder.common import AutoCoderArgs,SourceCode
|
|
4
4
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
5
|
from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult
|
|
6
|
-
from autocoder.common.context_pruner import PruneContext
|
|
6
|
+
from autocoder.common.pruner.context_pruner import PruneContext
|
|
7
7
|
from autocoder.common import SourceCode
|
|
8
8
|
from autocoder.rag.token_counter import count_tokens
|
|
9
9
|
from loguru import logger
|
|
@@ -3,7 +3,8 @@ import re
|
|
|
3
3
|
import glob
|
|
4
4
|
from typing import Dict, Any, Optional, List, Union
|
|
5
5
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
6
|
-
|
|
6
|
+
# Import ToolResult from types
|
|
7
|
+
from autocoder.common.v2.agent.agentic_edit_types import SearchFilesTool, ToolResult
|
|
7
8
|
from loguru import logger
|
|
8
9
|
from autocoder.common import AutoCoderArgs
|
|
9
10
|
import typing
|
|
@@ -11,7 +12,7 @@ import typing
|
|
|
11
12
|
from autocoder.common.ignorefiles.ignore_file_utils import should_ignore
|
|
12
13
|
|
|
13
14
|
if typing.TYPE_CHECKING:
|
|
14
|
-
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
15
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SearchFilesToolResolver(BaseToolResolver):
|
|
@@ -24,12 +25,13 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
24
25
|
"""Helper function to search in a directory"""
|
|
25
26
|
search_results = []
|
|
26
27
|
search_glob_pattern = os.path.join(base_dir, "**", file_pattern)
|
|
27
|
-
|
|
28
|
-
logger.info(
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
logger.info(
|
|
30
|
+
f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{base_dir}' (shadow: {is_shadow}) with ignore rules applied.")
|
|
31
|
+
|
|
30
32
|
if compiled_regex is None:
|
|
31
33
|
compiled_regex = re.compile(regex_pattern)
|
|
32
|
-
|
|
34
|
+
|
|
33
35
|
for filepath in glob.glob(search_glob_pattern, recursive=True):
|
|
34
36
|
abs_path = os.path.abspath(filepath)
|
|
35
37
|
if should_ignore(abs_path):
|
|
@@ -43,17 +45,22 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
43
45
|
if compiled_regex.search(line):
|
|
44
46
|
context_start = max(0, i - 2)
|
|
45
47
|
context_end = min(len(lines), i + 3)
|
|
46
|
-
context = "".join(
|
|
47
|
-
|
|
48
|
+
context = "".join(
|
|
49
|
+
[f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
|
|
50
|
+
|
|
48
51
|
if is_shadow and self.shadow_manager:
|
|
49
52
|
try:
|
|
50
|
-
abs_project_path = self.shadow_manager.from_shadow_path(
|
|
51
|
-
|
|
53
|
+
abs_project_path = self.shadow_manager.from_shadow_path(
|
|
54
|
+
filepath)
|
|
55
|
+
relative_path = os.path.relpath(
|
|
56
|
+
abs_project_path, source_dir)
|
|
52
57
|
except Exception:
|
|
53
|
-
relative_path = os.path.relpath(
|
|
58
|
+
relative_path = os.path.relpath(
|
|
59
|
+
filepath, source_dir)
|
|
54
60
|
else:
|
|
55
|
-
relative_path = os.path.relpath(
|
|
56
|
-
|
|
61
|
+
relative_path = os.path.relpath(
|
|
62
|
+
filepath, source_dir)
|
|
63
|
+
|
|
57
64
|
search_results.append({
|
|
58
65
|
"path": relative_path,
|
|
59
66
|
"line_number": i + 1,
|
|
@@ -61,73 +68,12 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
61
68
|
"context": context.strip()
|
|
62
69
|
})
|
|
63
70
|
except Exception as e:
|
|
64
|
-
logger.warning(
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"Could not read or process file {filepath}: {e}")
|
|
65
73
|
continue
|
|
66
|
-
|
|
67
|
-
return search_results
|
|
68
|
-
|
|
69
|
-
def search_files_with_shadow(self, search_path_str: str, regex_pattern: str, file_pattern: str, source_dir: str, absolute_source_dir: str, absolute_search_path: str) -> Union[ToolResult, List[Dict[str, Any]]]:
|
|
70
|
-
"""Search files using shadow manager for path translation"""
|
|
71
|
-
# Security check
|
|
72
|
-
if not absolute_search_path.startswith(absolute_source_dir):
|
|
73
|
-
return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
|
|
74
|
-
|
|
75
|
-
# Check if shadow directory exists
|
|
76
|
-
shadow_exists = False
|
|
77
|
-
shadow_dir_path = None
|
|
78
|
-
if self.shadow_manager:
|
|
79
|
-
try:
|
|
80
|
-
shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_search_path)
|
|
81
|
-
if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
|
|
82
|
-
shadow_exists = True
|
|
83
|
-
except Exception as e:
|
|
84
|
-
logger.warning(f"Error checking shadow path for {absolute_search_path}: {e}")
|
|
85
|
-
|
|
86
|
-
# Validate that at least one of the directories exists
|
|
87
|
-
if not os.path.exists(absolute_search_path) and not shadow_exists:
|
|
88
|
-
return ToolResult(success=False, message=f"Error: Search path not found: {search_path_str}")
|
|
89
|
-
if os.path.exists(absolute_search_path) and not os.path.isdir(absolute_search_path):
|
|
90
|
-
return ToolResult(success=False, message=f"Error: Search path is not a directory: {search_path_str}")
|
|
91
|
-
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
92
|
-
return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {shadow_dir_path}")
|
|
93
74
|
|
|
94
|
-
|
|
95
|
-
compiled_regex = re.compile(regex_pattern)
|
|
96
|
-
|
|
97
|
-
# Search in both directories and merge results
|
|
98
|
-
shadow_results = []
|
|
99
|
-
source_results = []
|
|
100
|
-
|
|
101
|
-
if shadow_exists:
|
|
102
|
-
shadow_results = self.search_in_dir(shadow_dir_path, regex_pattern, file_pattern, source_dir, is_shadow=True, compiled_regex=compiled_regex)
|
|
103
|
-
|
|
104
|
-
if os.path.exists(absolute_search_path) and os.path.isdir(absolute_search_path):
|
|
105
|
-
source_results = self.search_in_dir(absolute_search_path, regex_pattern, file_pattern, source_dir, is_shadow=False, compiled_regex=compiled_regex)
|
|
106
|
-
|
|
107
|
-
# Merge results, prioritizing shadow results
|
|
108
|
-
# Create a dictionary for quick lookup
|
|
109
|
-
results_dict = {}
|
|
110
|
-
for result in source_results:
|
|
111
|
-
key = (result["path"], result["line_number"])
|
|
112
|
-
results_dict[key] = result
|
|
113
|
-
|
|
114
|
-
# Override with shadow results
|
|
115
|
-
for result in shadow_results:
|
|
116
|
-
key = (result["path"], result["line_number"])
|
|
117
|
-
results_dict[key] = result
|
|
118
|
-
|
|
119
|
-
# Convert back to list
|
|
120
|
-
merged_results = list(results_dict.values())
|
|
121
|
-
|
|
122
|
-
return merged_results
|
|
75
|
+
return search_results
|
|
123
76
|
|
|
124
|
-
except re.error as e:
|
|
125
|
-
logger.error(f"Invalid regex pattern '{regex_pattern}': {e}")
|
|
126
|
-
return ToolResult(success=False, message=f"Invalid regex pattern: {e}")
|
|
127
|
-
except Exception as e:
|
|
128
|
-
logger.error(f"Error during file search: {str(e)}")
|
|
129
|
-
return ToolResult(success=False, message=f"An unexpected error occurred during search: {str(e)}")
|
|
130
|
-
|
|
131
77
|
def search_files_normal(self, search_path_str: str, regex_pattern: str, file_pattern: str, source_dir: str, absolute_source_dir: str, absolute_search_path: str) -> Union[ToolResult, List[Dict[str, Any]]]:
|
|
132
78
|
"""Search files directly without using shadow manager"""
|
|
133
79
|
# Security check
|
|
@@ -142,10 +88,11 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
142
88
|
|
|
143
89
|
try:
|
|
144
90
|
compiled_regex = re.compile(regex_pattern)
|
|
145
|
-
|
|
91
|
+
|
|
146
92
|
# Search in the directory
|
|
147
|
-
search_results = self.search_in_dir(
|
|
148
|
-
|
|
93
|
+
search_results = self.search_in_dir(
|
|
94
|
+
absolute_search_path, regex_pattern, file_pattern, source_dir, is_shadow=False, compiled_regex=compiled_regex)
|
|
95
|
+
|
|
149
96
|
return search_results
|
|
150
97
|
|
|
151
98
|
except re.error as e:
|
|
@@ -162,21 +109,19 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
162
109
|
file_pattern = self.tool.file_pattern or "*"
|
|
163
110
|
source_dir = self.args.source_dir or "."
|
|
164
111
|
absolute_source_dir = os.path.abspath(source_dir)
|
|
165
|
-
absolute_search_path = os.path.abspath(
|
|
112
|
+
absolute_search_path = os.path.abspath(
|
|
113
|
+
os.path.join(source_dir, search_path_str))
|
|
166
114
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
result = self.search_files_with_shadow(search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
|
|
170
|
-
else:
|
|
171
|
-
result = self.search_files_normal(search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
|
|
115
|
+
result = self.search_files_normal(
|
|
116
|
+
search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
|
|
172
117
|
|
|
173
118
|
# Handle the case where the implementation returns a list instead of a ToolResult
|
|
174
119
|
if isinstance(result, list):
|
|
175
|
-
total_results = len(result)
|
|
120
|
+
total_results = len(result)
|
|
176
121
|
# Limit results to 200 if needed
|
|
177
122
|
if total_results > 200:
|
|
178
123
|
truncated_results = result[:200]
|
|
179
|
-
message = f"Search completed. Found {total_results} matches, showing only the first 200."
|
|
124
|
+
message = f"Search completed. Found {total_results} matches, showing only the first 200."
|
|
180
125
|
logger.info(message)
|
|
181
126
|
return ToolResult(success=True, message=message, content=truncated_results)
|
|
182
127
|
else:
|
|
@@ -110,7 +110,7 @@ def test_create_new_file(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
|
110
110
|
result = resolver.resolve()
|
|
111
111
|
|
|
112
112
|
assert result.success is True
|
|
113
|
-
assert "
|
|
113
|
+
assert "Successfully wrote file" in result.message
|
|
114
114
|
|
|
115
115
|
expected_file_abs_path = os.path.join(temp_test_dir, file_path)
|
|
116
116
|
assert os.path.exists(expected_file_abs_path)
|
|
@@ -135,7 +135,7 @@ def test_overwrite_existing_file(test_args, temp_test_dir, mock_agent_no_shadow)
|
|
|
135
135
|
|
|
136
136
|
assert result.success is True
|
|
137
137
|
assert os.path.exists(abs_file_path)
|
|
138
|
-
with open(abs_file_path, "r", encoding="utf-
|
|
138
|
+
with open(abs_file_path, "r", encoding="utf-8") as f:
|
|
139
139
|
assert f.read() == new_content
|
|
140
140
|
mock_agent_no_shadow.record_file_change.assert_called_once_with(file_path, "modified", content=new_content, diffs=None)
|
|
141
141
|
|
|
@@ -202,7 +202,7 @@ def test_path_outside_project_root_fails(test_args, temp_test_dir, mock_agent_no
|
|
|
202
202
|
result = resolver.resolve()
|
|
203
203
|
|
|
204
204
|
assert result.success is False
|
|
205
|
-
assert "
|
|
205
|
+
assert "Access denied" in result.message
|
|
206
206
|
assert not os.path.exists(outside_abs_path)
|
|
207
207
|
|
|
208
208
|
# shutil.rmtree(another_temp_dir) # Clean up the other temp dir if created by this test
|
|
@@ -226,8 +226,8 @@ def test_linting_not_called_if_disabled(test_args, temp_test_dir, mock_agent_no_
|
|
|
226
226
|
if mock_agent_no_shadow and hasattr(mock_agent_no_shadow, 'shadow_linter') and mock_agent_no_shadow.shadow_linter:
|
|
227
227
|
mock_agent_no_shadow.shadow_linter.lint_shadow_file.assert_not_called()
|
|
228
228
|
|
|
229
|
-
# Check if "
|
|
230
|
-
assert "
|
|
229
|
+
# Check if "Linting is disabled" or "Successfully wrote file" is in message
|
|
230
|
+
assert "Linting is disabled" in result.message or "Successfully wrote file" in result.message
|
|
231
231
|
|
|
232
232
|
|
|
233
233
|
def test_linting_called_if_enabled(test_args, temp_test_dir, mock_agent_with_shadow):
|
|
@@ -251,7 +251,7 @@ def test_linting_called_if_enabled(test_args, temp_test_dir, mock_agent_with_sha
|
|
|
251
251
|
# The actual path passed to lint_shadow_file will be the shadow path
|
|
252
252
|
shadow_path = mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))
|
|
253
253
|
mock_agent_with_shadow.shadow_linter.lint_shadow_file.assert_called_with(shadow_path)
|
|
254
|
-
assert "
|
|
254
|
+
assert "Linting passed" in result.message
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
def test_create_file_with_shadow_manager(test_args, temp_test_dir, mock_agent_with_shadow):
|
|
@@ -311,12 +311,12 @@ def test_linting_error_message_propagation(test_args, temp_test_dir, mock_agent_
|
|
|
311
311
|
|
|
312
312
|
# Temporarily patch _format_lint_issues within the resolver instance for this test
|
|
313
313
|
# to ensure consistent output for assertion.
|
|
314
|
-
formatted_issue_text = f"
|
|
314
|
+
formatted_issue_text = f"File: {mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))}\n - [ERROR] Line 1, Column 0: SyntaxError: Missing parentheses in call to 'print' (Rule: E999)\n"
|
|
315
315
|
with patch.object(resolver, '_format_lint_issues', return_value=formatted_issue_text) as mock_format:
|
|
316
316
|
result = resolver.resolve()
|
|
317
317
|
|
|
318
318
|
assert result.success is True # Write itself is successful
|
|
319
319
|
mock_format.assert_called_once_with(mock_lint_result)
|
|
320
|
-
assert "
|
|
320
|
+
assert "Linting found 1 issue(s)" in result.message
|
|
321
321
|
assert "SyntaxError: Missing parentheses in call to 'print'" in result.message
|
|
322
322
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional, List
|
|
2
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_types import TodoReadTool, ToolResult
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import typing
|
|
6
|
+
from autocoder.common import AutoCoderArgs
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TodoReadToolResolver(BaseToolResolver):
|
|
16
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: TodoReadTool, args: AutoCoderArgs):
|
|
17
|
+
super().__init__(agent, tool, args)
|
|
18
|
+
self.tool: TodoReadTool = tool # For type hinting
|
|
19
|
+
|
|
20
|
+
def _get_todo_file_path(self) -> str:
|
|
21
|
+
"""Get the path to the todo file for this session."""
|
|
22
|
+
source_dir = self.args.source_dir or "."
|
|
23
|
+
todo_dir = os.path.join(source_dir, ".auto-coder", "todos")
|
|
24
|
+
os.makedirs(todo_dir, exist_ok=True)
|
|
25
|
+
return os.path.join(todo_dir, "current_session.json")
|
|
26
|
+
|
|
27
|
+
def _load_todos(self) -> List[Dict[str, Any]]:
|
|
28
|
+
"""Load todos from the session file."""
|
|
29
|
+
todo_file = self._get_todo_file_path()
|
|
30
|
+
if not os.path.exists(todo_file):
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with open(todo_file, 'r', encoding='utf-8') as f:
|
|
35
|
+
data = json.load(f)
|
|
36
|
+
return data.get('todos', [])
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.warning(f"Failed to load todos: {e}")
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
def _format_todo_display(self, todos: List[Dict[str, Any]]) -> str:
|
|
42
|
+
"""Format todos for display."""
|
|
43
|
+
if not todos:
|
|
44
|
+
return "No todos found for this session."
|
|
45
|
+
|
|
46
|
+
output = []
|
|
47
|
+
output.append("=== Current Session Todo List ===\n")
|
|
48
|
+
|
|
49
|
+
# Group by status
|
|
50
|
+
pending = [t for t in todos if t.get('status') == 'pending']
|
|
51
|
+
in_progress = [t for t in todos if t.get('status') == 'in_progress']
|
|
52
|
+
completed = [t for t in todos if t.get('status') == 'completed']
|
|
53
|
+
|
|
54
|
+
if in_progress:
|
|
55
|
+
output.append("🔄 In Progress:")
|
|
56
|
+
for todo in in_progress:
|
|
57
|
+
priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
|
|
58
|
+
output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
|
|
59
|
+
if todo.get('notes'):
|
|
60
|
+
output.append(f" 📝 {todo['notes']}")
|
|
61
|
+
output.append("")
|
|
62
|
+
|
|
63
|
+
if pending:
|
|
64
|
+
output.append("⏳ Pending:")
|
|
65
|
+
for todo in pending:
|
|
66
|
+
priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
|
|
67
|
+
output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
|
|
68
|
+
if todo.get('notes'):
|
|
69
|
+
output.append(f" 📝 {todo['notes']}")
|
|
70
|
+
output.append("")
|
|
71
|
+
|
|
72
|
+
if completed:
|
|
73
|
+
output.append("✅ Completed:")
|
|
74
|
+
for todo in completed:
|
|
75
|
+
priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
|
|
76
|
+
output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
|
|
77
|
+
if todo.get('notes'):
|
|
78
|
+
output.append(f" 📝 {todo['notes']}")
|
|
79
|
+
output.append("")
|
|
80
|
+
|
|
81
|
+
# Add summary
|
|
82
|
+
total = len(todos)
|
|
83
|
+
pending_count = len(pending)
|
|
84
|
+
in_progress_count = len(in_progress)
|
|
85
|
+
completed_count = len(completed)
|
|
86
|
+
|
|
87
|
+
output.append(f"📊 Summary: Total {total} items | Pending {pending_count} | In Progress {in_progress_count} | Completed {completed_count}")
|
|
88
|
+
|
|
89
|
+
return "\n".join(output)
|
|
90
|
+
|
|
91
|
+
def resolve(self) -> ToolResult:
|
|
92
|
+
"""
|
|
93
|
+
Read the current todo list and return it in a formatted display.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
logger.info("Reading current todo list")
|
|
97
|
+
|
|
98
|
+
# Load todos from file
|
|
99
|
+
todos = self._load_todos()
|
|
100
|
+
|
|
101
|
+
# Format for display
|
|
102
|
+
formatted_display = self._format_todo_display(todos)
|
|
103
|
+
|
|
104
|
+
logger.info(f"Found {len(todos)} todos in current session")
|
|
105
|
+
|
|
106
|
+
return ToolResult(
|
|
107
|
+
success=True,
|
|
108
|
+
message="Todo list retrieved successfully.",
|
|
109
|
+
content=formatted_display
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error reading todo list: {e}")
|
|
114
|
+
return ToolResult(
|
|
115
|
+
success=False,
|
|
116
|
+
message=f"Failed to read todo list: {str(e)}",
|
|
117
|
+
content=None
|
|
118
|
+
)
|