auto-coder 0.1.334__py3-none-any.whl → 0.1.340__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.340.dist-info}/METADATA +2 -2
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.340.dist-info}/RECORD +70 -34
- 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/agent/agentic_filter.py +14 -7
- autocoder/auto_coder.py +39 -18
- autocoder/auto_coder_rag.py +18 -9
- autocoder/auto_coder_runner.py +107 -8
- autocoder/chat_auto_coder.py +1 -2
- 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 +84 -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 +1433 -0
- autocoder/common/v2/agent/agentic_edit_conversation.py +179 -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 +153 -0
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +104 -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 +167 -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.340.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.340.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.340.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.334.dist-info → auto_coder-0.1.340.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# src/autocoder/common/v2/agent/agentic_edit_conversation.py
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
from autocoder.common import AutoCoderArgs
|
|
7
|
+
|
|
8
|
+
# Define a type alias for a message dictionary
|
|
9
|
+
MessageType = Dict[str, Any]
|
|
10
|
+
|
|
11
|
+
class AgenticConversation:
|
|
12
|
+
"""
|
|
13
|
+
Manages the conversation history for an agentic editing process.
|
|
14
|
+
|
|
15
|
+
Handles adding messages (user, assistant, tool calls, tool results)
|
|
16
|
+
and retrieving the history.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, args: AutoCoderArgs, initial_history: Optional[List[MessageType]] = None, conversation_name: Optional[str] = None):
|
|
20
|
+
"""
|
|
21
|
+
Initializes the conversation history.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
initial_history: An optional list of messages to start with.
|
|
25
|
+
conversation_name: Optional conversation identifier. If provided, history is saved/loaded from a file named after it.
|
|
26
|
+
"""
|
|
27
|
+
self.project_path = args.source_dir
|
|
28
|
+
self._history: List[MessageType] = initial_history if initial_history is not None else []
|
|
29
|
+
|
|
30
|
+
# Determine the memory directory
|
|
31
|
+
memory_dir = os.path.join(self.project_path, ".auto-coder", "memory", "agentic_edit_memory")
|
|
32
|
+
os.makedirs(memory_dir, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Determine conversation file path
|
|
35
|
+
if conversation_name:
|
|
36
|
+
filename = f"{conversation_name}.json"
|
|
37
|
+
else:
|
|
38
|
+
conversation_name = str(uuid.uuid4())
|
|
39
|
+
filename = f"{conversation_name}.json"
|
|
40
|
+
|
|
41
|
+
self.conversation_name = conversation_name
|
|
42
|
+
self.memory_file_path = os.path.join(memory_dir, filename)
|
|
43
|
+
|
|
44
|
+
# Load existing history if file exists
|
|
45
|
+
self._load_memory()
|
|
46
|
+
|
|
47
|
+
def add_message(self, role: str, content: Any, **kwargs):
|
|
48
|
+
"""
|
|
49
|
+
Adds a message to the conversation history.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
role: The role of the message sender (e.g., "user", "assistant", "tool").
|
|
53
|
+
content: The content of the message. Can be None for messages like tool calls.
|
|
54
|
+
**kwargs: Additional key-value pairs to include in the message dictionary (e.g., tool_calls, tool_call_id).
|
|
55
|
+
"""
|
|
56
|
+
message: MessageType = {"role": role}
|
|
57
|
+
if content is not None:
|
|
58
|
+
message["content"] = content
|
|
59
|
+
message.update(kwargs)
|
|
60
|
+
self._history.append(message)
|
|
61
|
+
self._save_memory()
|
|
62
|
+
|
|
63
|
+
def add_user_message(self, content: str):
|
|
64
|
+
"""Adds a user message."""
|
|
65
|
+
self.add_message(role="user", content=content)
|
|
66
|
+
|
|
67
|
+
def add_assistant_message(self, content: str):
|
|
68
|
+
"""Adds an assistant message (potentially containing text response)."""
|
|
69
|
+
self.add_message(role="assistant", content=content)
|
|
70
|
+
|
|
71
|
+
def add_assistant_tool_call_message(self, tool_calls: List[Dict[str, Any]], content: Optional[str] = None):
|
|
72
|
+
"""
|
|
73
|
+
Adds a message representing one or more tool calls from the assistant.
|
|
74
|
+
Optionally includes assistant's textual reasoning/content alongside the calls.
|
|
75
|
+
"""
|
|
76
|
+
self.add_message(role="assistant", content=content, tool_calls=tool_calls)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def add_tool_result_message(self, tool_call_id: str, content: Any):
|
|
80
|
+
"""Adds a message representing the result of a specific tool call."""
|
|
81
|
+
# The content here is typically the output/result from the tool execution.
|
|
82
|
+
self.add_message(role="tool", content=content, tool_call_id=tool_call_id)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_history(self) -> List[MessageType]:
|
|
86
|
+
"""
|
|
87
|
+
Returns the latest 20 pairs of (user, assistant) conversation history.
|
|
88
|
+
Merges adjacent same-role messages into one, concatenated by newline.
|
|
89
|
+
Ensures that each user message is paired with the subsequent assistant response,
|
|
90
|
+
skips other roles, and that the last message is always assistant (drops trailing user if unpaired).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A list of message dictionaries, ordered chronologically.
|
|
94
|
+
"""
|
|
95
|
+
paired_history = []
|
|
96
|
+
pair_count = 0
|
|
97
|
+
pending_assistant = None
|
|
98
|
+
pending_user = None
|
|
99
|
+
|
|
100
|
+
# Traverse history in reverse to collect latest pairs with merging
|
|
101
|
+
for msg in reversed(self._history):
|
|
102
|
+
role = msg.get("role")
|
|
103
|
+
if role == "assistant":
|
|
104
|
+
if pending_assistant is None:
|
|
105
|
+
pending_assistant = dict(msg)
|
|
106
|
+
else:
|
|
107
|
+
# Merge with previous assistant
|
|
108
|
+
prev_content = pending_assistant.get("content", "")
|
|
109
|
+
curr_content = msg.get("content", "")
|
|
110
|
+
merged_content = (curr_content.strip() + "\n" + prev_content.strip()).strip()
|
|
111
|
+
pending_assistant["content"] = merged_content
|
|
112
|
+
elif role == "user":
|
|
113
|
+
if pending_user is None:
|
|
114
|
+
pending_user = dict(msg)
|
|
115
|
+
else:
|
|
116
|
+
# Merge with previous user
|
|
117
|
+
prev_content = pending_user.get("content", "")
|
|
118
|
+
curr_content = msg.get("content", "")
|
|
119
|
+
merged_content = (curr_content.strip() + "\n" + prev_content.strip()).strip()
|
|
120
|
+
pending_user["content"] = merged_content
|
|
121
|
+
|
|
122
|
+
if pending_assistant is not None:
|
|
123
|
+
# Have a full pair, insert in order
|
|
124
|
+
paired_history.insert(0, pending_user)
|
|
125
|
+
paired_history.insert(1, pending_assistant)
|
|
126
|
+
pair_count += 1
|
|
127
|
+
pending_assistant = None
|
|
128
|
+
pending_user = None
|
|
129
|
+
if pair_count >= 20:
|
|
130
|
+
break
|
|
131
|
+
else:
|
|
132
|
+
# User without assistant yet, continue accumulating
|
|
133
|
+
continue
|
|
134
|
+
else:
|
|
135
|
+
# Ignore other roles
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Ensure last message is assistant, drop trailing user if unpaired
|
|
139
|
+
if paired_history and paired_history[-1].get("role") == "user":
|
|
140
|
+
paired_history.pop()
|
|
141
|
+
|
|
142
|
+
return paired_history
|
|
143
|
+
|
|
144
|
+
def clear_history(self):
|
|
145
|
+
"""Clears the conversation history."""
|
|
146
|
+
self._history = []
|
|
147
|
+
self._save_memory()
|
|
148
|
+
|
|
149
|
+
def __len__(self) -> int:
|
|
150
|
+
"""Returns the number of messages in the history."""
|
|
151
|
+
return len(self._history)
|
|
152
|
+
|
|
153
|
+
def __str__(self) -> str:
|
|
154
|
+
"""Returns a string representation of the conversation history."""
|
|
155
|
+
# Consider a more readable format if needed for debugging
|
|
156
|
+
return str(self._history)
|
|
157
|
+
|
|
158
|
+
# Potential future enhancements:
|
|
159
|
+
# - Method to limit history size (by tokens or message count)
|
|
160
|
+
# - Method to format history specifically for different LLM APIs
|
|
161
|
+
# - Serialization/deserialization methods
|
|
162
|
+
|
|
163
|
+
def _save_memory(self):
|
|
164
|
+
try:
|
|
165
|
+
os.makedirs(os.path.dirname(self.memory_file_path), exist_ok=True)
|
|
166
|
+
with open(self.memory_file_path, "w", encoding="utf-8") as f:
|
|
167
|
+
json.dump(self._history, f, ensure_ascii=False, indent=2)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
# Optionally log or ignore
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def _load_memory(self):
|
|
173
|
+
try:
|
|
174
|
+
if os.path.exists(self.memory_file_path):
|
|
175
|
+
with open(self.memory_file_path, "r", encoding="utf-8") as f:
|
|
176
|
+
self._history = json.load(f)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
# Ignore loading errors, start fresh
|
|
179
|
+
pass
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# flake8: noqa
|
|
2
|
+
from .base_tool_resolver import BaseToolResolver
|
|
3
|
+
from .execute_command_tool_resolver import ExecuteCommandToolResolver
|
|
4
|
+
from .read_file_tool_resolver import ReadFileToolResolver
|
|
5
|
+
from .write_to_file_tool_resolver import WriteToFileToolResolver
|
|
6
|
+
from .replace_in_file_tool_resolver import ReplaceInFileToolResolver
|
|
7
|
+
from .search_files_tool_resolver import SearchFilesToolResolver
|
|
8
|
+
from .list_files_tool_resolver import ListFilesToolResolver
|
|
9
|
+
from .list_code_definition_names_tool_resolver import ListCodeDefinitionNamesToolResolver
|
|
10
|
+
from .ask_followup_question_tool_resolver import AskFollowupQuestionToolResolver
|
|
11
|
+
from .attempt_completion_tool_resolver import AttemptCompletionToolResolver
|
|
12
|
+
from .plan_mode_respond_tool_resolver import PlanModeRespondToolResolver
|
|
13
|
+
from .use_mcp_tool_resolver import UseMcpToolResolver
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseToolResolver",
|
|
17
|
+
"ExecuteCommandToolResolver",
|
|
18
|
+
"ReadFileToolResolver",
|
|
19
|
+
"WriteToFileToolResolver",
|
|
20
|
+
"ReplaceInFileToolResolver",
|
|
21
|
+
"SearchFilesToolResolver",
|
|
22
|
+
"ListFilesToolResolver",
|
|
23
|
+
"ListCodeDefinitionNamesToolResolver",
|
|
24
|
+
"AskFollowupQuestionToolResolver",
|
|
25
|
+
"AttemptCompletionToolResolver",
|
|
26
|
+
"PlanModeRespondToolResolver",
|
|
27
|
+
"UseMcpToolResolver",
|
|
28
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_types import AskFollowupQuestionTool, ToolResult # Import ToolResult from types
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import typing
|
|
6
|
+
from autocoder.common import AutoCoderArgs
|
|
7
|
+
from autocoder.run_context import get_run_context
|
|
8
|
+
from autocoder.events.event_manager_singleton import get_event_manager
|
|
9
|
+
from autocoder.events import event_content as EventContentCreator
|
|
10
|
+
from prompt_toolkit import PromptSession
|
|
11
|
+
from prompt_toolkit.styles import Style
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
if typing.TYPE_CHECKING:
|
|
17
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
18
|
+
|
|
19
|
+
class AskFollowupQuestionToolResolver(BaseToolResolver):
|
|
20
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: AskFollowupQuestionTool, args: AutoCoderArgs):
|
|
21
|
+
super().__init__(agent, tool, args)
|
|
22
|
+
self.tool: AskFollowupQuestionTool = tool # For type hinting
|
|
23
|
+
|
|
24
|
+
def resolve(self) -> ToolResult:
|
|
25
|
+
"""
|
|
26
|
+
Packages the question and options to be handled by the main loop/UI.
|
|
27
|
+
This resolver doesn't directly ask the user but prepares the data for it.
|
|
28
|
+
"""
|
|
29
|
+
question = self.tool.question
|
|
30
|
+
options = self.tool.options
|
|
31
|
+
|
|
32
|
+
if get_run_context().is_web():
|
|
33
|
+
answer = get_event_manager(
|
|
34
|
+
self.args.event_file).ask_user(prompt=question)
|
|
35
|
+
self.result_manager.append(content=answer, meta={
|
|
36
|
+
"action": "ask_user",
|
|
37
|
+
"input": {
|
|
38
|
+
"question": question
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
return answer
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
|
|
45
|
+
# 创建一个醒目的问题面板
|
|
46
|
+
question_text = Text(question, style="bold cyan")
|
|
47
|
+
question_panel = Panel(
|
|
48
|
+
question_text,
|
|
49
|
+
title="[bold yellow]auto-coder.chat's Question[/bold yellow]",
|
|
50
|
+
border_style="blue",
|
|
51
|
+
expand=False
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 显示问题面板
|
|
55
|
+
console.print(question_panel)
|
|
56
|
+
|
|
57
|
+
session = PromptSession(
|
|
58
|
+
message=self.printer.get_message_from_key('tool_ask_user'))
|
|
59
|
+
try:
|
|
60
|
+
answer = session.prompt()
|
|
61
|
+
except KeyboardInterrupt:
|
|
62
|
+
answer = ""
|
|
63
|
+
|
|
64
|
+
# The actual asking logic resides outside the resolver, typically in the agent's main loop
|
|
65
|
+
# or UI interaction layer. The resolver's job is to validate and package the request.
|
|
66
|
+
if not answer:
|
|
67
|
+
return ToolResult(success=False, message="Error: Question not answered.")
|
|
68
|
+
|
|
69
|
+
# Indicate success in preparing the question data
|
|
70
|
+
return ToolResult(success=True, message="Follow-up question prepared.", content=answer)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_types import AttemptCompletionTool, ToolResult # Import ToolResult from types
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import typing
|
|
6
|
+
from autocoder.common import AutoCoderArgs
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
10
|
+
|
|
11
|
+
class AttemptCompletionToolResolver(BaseToolResolver):
|
|
12
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: AttemptCompletionTool, args: AutoCoderArgs):
|
|
13
|
+
super().__init__(agent, tool, args)
|
|
14
|
+
self.tool: AttemptCompletionTool = tool # For type hinting
|
|
15
|
+
|
|
16
|
+
def resolve(self) -> ToolResult:
|
|
17
|
+
"""
|
|
18
|
+
Packages the completion result and optional command to signal task completion.
|
|
19
|
+
"""
|
|
20
|
+
result_text = self.tool.result
|
|
21
|
+
command = self.tool.command
|
|
22
|
+
|
|
23
|
+
logger.info(f"Resolving AttemptCompletionTool: Result='{result_text[:100]}...', Command='{command}'")
|
|
24
|
+
|
|
25
|
+
if not result_text:
|
|
26
|
+
return ToolResult(success=False, message="Error: Completion result cannot be empty.")
|
|
27
|
+
|
|
28
|
+
# The actual presentation of the result happens outside the resolver.
|
|
29
|
+
result_content = {
|
|
30
|
+
"result": result_text,
|
|
31
|
+
"command": command
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Indicate success in preparing the completion data
|
|
35
|
+
return ToolResult(success=True, message="Task completion attempted.", content=result_content)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_types import BaseTool, ToolResult # Import ToolResult from types
|
|
4
|
+
from autocoder.common import AutoCoderArgs
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseToolResolver(ABC):
|
|
12
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: BaseTool, args: AutoCoderArgs):
|
|
13
|
+
"""
|
|
14
|
+
Initializes the resolver.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
agent: The AutoCoder agent instance.
|
|
18
|
+
tool: The Pydantic model instance representing the tool call.
|
|
19
|
+
args: Additional arguments needed for execution (e.g., source_dir).
|
|
20
|
+
"""
|
|
21
|
+
self.agent = agent
|
|
22
|
+
self.tool = tool
|
|
23
|
+
self.args = args
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def resolve(self) -> ToolResult:
|
|
27
|
+
"""
|
|
28
|
+
Executes the tool's logic.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A ToolResult object indicating success or failure and a message.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, Any, 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 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
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
14
|
+
|
|
15
|
+
class ExecuteCommandToolResolver(BaseToolResolver):
|
|
16
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ExecuteCommandTool, args: AutoCoderArgs):
|
|
17
|
+
super().__init__(agent, tool, args)
|
|
18
|
+
self.tool: ExecuteCommandTool = tool # For type hinting
|
|
19
|
+
|
|
20
|
+
def resolve(self) -> ToolResult:
|
|
21
|
+
printer = Printer()
|
|
22
|
+
command = self.tool.command
|
|
23
|
+
requires_approval = self.tool.requires_approval
|
|
24
|
+
source_dir = self.args.source_dir or "."
|
|
25
|
+
|
|
26
|
+
# Basic security check (can be expanded)
|
|
27
|
+
if ";" in command or "&&" in command or "|" in command or "`" in command:
|
|
28
|
+
# Allow && for cd chaining, but be cautious
|
|
29
|
+
if not command.strip().startswith("cd ") and " && " in command:
|
|
30
|
+
pass # Allow cd chaining like 'cd subdir && command'
|
|
31
|
+
else:
|
|
32
|
+
return ToolResult(success=False, message=f"Command '{command}' contains potentially unsafe characters.")
|
|
33
|
+
|
|
34
|
+
# Approval mechanism (simplified)
|
|
35
|
+
if requires_approval:
|
|
36
|
+
# In a real scenario, this would involve user interaction
|
|
37
|
+
printer.print_str_in_terminal(f"Command requires approval: {command}")
|
|
38
|
+
# For now, let's assume approval is granted in non-interactive mode or handled elsewhere
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
printer.print_str_in_terminal(f"Executing command: {command} in {os.path.abspath(source_dir)}")
|
|
42
|
+
try:
|
|
43
|
+
# Determine shell based on OS
|
|
44
|
+
shell = True
|
|
45
|
+
executable = None
|
|
46
|
+
if shells.is_windows():
|
|
47
|
+
# Decide between cmd and powershell if needed, default is cmd
|
|
48
|
+
pass # shell=True uses default shell
|
|
49
|
+
else:
|
|
50
|
+
# Use bash or zsh? Default is usually fine.
|
|
51
|
+
pass # shell=True uses default shell
|
|
52
|
+
|
|
53
|
+
# Execute the command
|
|
54
|
+
process = subprocess.Popen(
|
|
55
|
+
command,
|
|
56
|
+
shell=True,
|
|
57
|
+
stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
cwd=source_dir,
|
|
60
|
+
text=True,
|
|
61
|
+
encoding=shells.get_terminal_encoding(),
|
|
62
|
+
errors='replace' # Handle potential decoding errors
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
stdout, stderr = process.communicate()
|
|
66
|
+
returncode = process.returncode
|
|
67
|
+
|
|
68
|
+
logger.info(f"Command executed: {command}")
|
|
69
|
+
logger.info(f"Return Code: {returncode}")
|
|
70
|
+
if stdout:
|
|
71
|
+
logger.info(f"stdout:\n{stdout}")
|
|
72
|
+
if stderr:
|
|
73
|
+
logger.info(f"stderr:\n{stderr}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if returncode == 0:
|
|
77
|
+
return ToolResult(success=True, message="Command executed successfully.", content=stdout)
|
|
78
|
+
else:
|
|
79
|
+
error_message = f"Command failed with return code {returncode}.\nStderr:\n{stderr}\nStdout:\n{stdout}"
|
|
80
|
+
return ToolResult(success=False, message=error_message, content={"stdout": stdout, "stderr": stderr, "returncode": returncode})
|
|
81
|
+
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
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.")
|
|
84
|
+
except PermissionError:
|
|
85
|
+
return ToolResult(success=False, message=f"Error: Permission denied when trying to execute '{command}'.")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Error executing command '{command}': {str(e)}")
|
|
88
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while executing the command: {str(e)}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_types import ListCodeDefinitionNamesTool, ToolResult # Import ToolResult from types
|
|
5
|
+
import json
|
|
6
|
+
from autocoder.index.index import IndexManager
|
|
7
|
+
from loguru import logger
|
|
8
|
+
import traceback
|
|
9
|
+
from autocoder.index.symbols_utils import (
|
|
10
|
+
extract_symbols,
|
|
11
|
+
SymbolType,
|
|
12
|
+
symbols_info_to_str,
|
|
13
|
+
)
|
|
14
|
+
import typing
|
|
15
|
+
from autocoder.common import AutoCoderArgs
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ListCodeDefinitionNamesToolResolver(BaseToolResolver):
|
|
22
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ListCodeDefinitionNamesTool, args: AutoCoderArgs):
|
|
23
|
+
super().__init__(agent, tool, args)
|
|
24
|
+
self.tool: ListCodeDefinitionNamesTool = tool # For type hinting
|
|
25
|
+
self.llm = self.agent.llm
|
|
26
|
+
|
|
27
|
+
def _get_index(self):
|
|
28
|
+
index_manager = IndexManager(
|
|
29
|
+
llm=self.llm, sources=[], args=self.args)
|
|
30
|
+
return index_manager
|
|
31
|
+
|
|
32
|
+
def resolve(self) -> ToolResult:
|
|
33
|
+
|
|
34
|
+
index_items = self._get_index().read_index()
|
|
35
|
+
index_data = {item.module_name: item for item in index_items}
|
|
36
|
+
|
|
37
|
+
target_path_str = self.tool.path
|
|
38
|
+
source_dir = self.args.source_dir or "."
|
|
39
|
+
absolute_target_path = os.path.abspath(os.path.join(source_dir, target_path_str))
|
|
40
|
+
|
|
41
|
+
# Security check
|
|
42
|
+
if not absolute_target_path.startswith(os.path.abspath(source_dir)):
|
|
43
|
+
return ToolResult(success=False, message=f"Error: Access denied. Attempted to analyze code outside the project directory: {target_path_str}")
|
|
44
|
+
|
|
45
|
+
if not os.path.exists(absolute_target_path):
|
|
46
|
+
return ToolResult(success=False, message=f"Error: Path not found: {target_path_str}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Use RepoParser or a similar mechanism to extract definitions
|
|
50
|
+
# RepoParser might need adjustments or a specific method for this tool's purpose.
|
|
51
|
+
# This is a placeholder implementation. A real implementation needs robust code parsing.
|
|
52
|
+
logger.info(f"Analyzing definitions in: {absolute_target_path}")
|
|
53
|
+
all_symbols = []
|
|
54
|
+
|
|
55
|
+
if os.path.isfile(absolute_target_path):
|
|
56
|
+
file_paths = [absolute_target_path]
|
|
57
|
+
else:
|
|
58
|
+
return ToolResult(success=False, message=f"Error: Path is neither a file nor a directory: {target_path_str}")
|
|
59
|
+
|
|
60
|
+
for file_path in file_paths:
|
|
61
|
+
try:
|
|
62
|
+
item = index_data[file_path]
|
|
63
|
+
symbols_str = item.symbols
|
|
64
|
+
symbols = extract_symbols(symbols_str)
|
|
65
|
+
if symbols:
|
|
66
|
+
all_symbols.append({
|
|
67
|
+
"path": file_path,
|
|
68
|
+
"definitions": [{"name": s, "type": "function"} for s in symbols.functions] + [{"name": s, "type": "variable"} for s in symbols.variables] + [{"name": s, "type": "class"} for s in symbols.classes]
|
|
69
|
+
})
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.warning(f"Could not parse symbols from {file_path}: {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
message = f"Successfully extracted {sum(len(s['definitions']) for s in all_symbols)} definitions from {len(all_symbols)} files in '{target_path_str}'."
|
|
75
|
+
logger.info(message)
|
|
76
|
+
return ToolResult(success=True, message=message, content=all_symbols)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error extracting code definitions from '{target_path_str}': {str(e)}")
|
|
80
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while extracting code definitions: {str(e)}")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_types import ListFilesTool, 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 autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ListFilesToolResolver(BaseToolResolver):
|
|
14
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: ListFilesTool, args: AutoCoderArgs):
|
|
15
|
+
super().__init__(agent, tool, args)
|
|
16
|
+
self.tool: ListFilesTool = tool # For type hinting
|
|
17
|
+
self.shadow_manager = self.agent.shadow_manager
|
|
18
|
+
|
|
19
|
+
def resolve(self) -> ToolResult:
|
|
20
|
+
list_path_str = self.tool.path
|
|
21
|
+
recursive = self.tool.recursive or False
|
|
22
|
+
source_dir = self.args.source_dir or "."
|
|
23
|
+
absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
|
|
24
|
+
|
|
25
|
+
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
26
|
+
# For now, let's restrict to source_dir for safety, unless path explicitly starts absolute
|
|
27
|
+
# This needs careful consideration based on security requirements.
|
|
28
|
+
# Let's allow listing anywhere for now, but log a warning if outside source_dir.
|
|
29
|
+
is_outside_source = not absolute_list_path.startswith(os.path.abspath(source_dir))
|
|
30
|
+
if is_outside_source:
|
|
31
|
+
logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
|
|
32
|
+
# Add more checks if needed, e.g., prevent listing sensitive system dirs
|
|
33
|
+
|
|
34
|
+
# Check if shadow directory exists for this path
|
|
35
|
+
shadow_paths = []
|
|
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_list_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_list_path}: {e}")
|
|
45
|
+
|
|
46
|
+
# Validate that at least one of the directories exists
|
|
47
|
+
if not os.path.exists(absolute_list_path) and not shadow_exists:
|
|
48
|
+
return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
|
|
49
|
+
if os.path.exists(absolute_list_path) and not os.path.isdir(absolute_list_path):
|
|
50
|
+
return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
|
|
51
|
+
if shadow_exists and not os.path.isdir(shadow_dir_path):
|
|
52
|
+
return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
|
|
53
|
+
|
|
54
|
+
# Helper function to list files in a directory
|
|
55
|
+
def list_files_in_dir(base_dir: str) -> set:
|
|
56
|
+
result = set()
|
|
57
|
+
try:
|
|
58
|
+
if recursive:
|
|
59
|
+
for root, dirs, files in os.walk(base_dir):
|
|
60
|
+
for name in files:
|
|
61
|
+
full_path = os.path.join(root, name)
|
|
62
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
63
|
+
result.add(display_path)
|
|
64
|
+
for name in dirs:
|
|
65
|
+
full_path = os.path.join(root, name)
|
|
66
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
67
|
+
result.add(display_path + "/")
|
|
68
|
+
else:
|
|
69
|
+
for item in os.listdir(base_dir):
|
|
70
|
+
full_path = os.path.join(base_dir, item)
|
|
71
|
+
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
72
|
+
if os.path.isdir(full_path):
|
|
73
|
+
result.add(display_path + "/")
|
|
74
|
+
else:
|
|
75
|
+
result.add(display_path)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"Error listing files in {base_dir}: {e}")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
# Collect files from shadow and/or source directory
|
|
81
|
+
shadow_files_set = set()
|
|
82
|
+
if shadow_exists:
|
|
83
|
+
shadow_files_set = list_files_in_dir(shadow_dir_path)
|
|
84
|
+
|
|
85
|
+
source_files_set = set()
|
|
86
|
+
if os.path.exists(absolute_list_path) and os.path.isdir(absolute_list_path):
|
|
87
|
+
source_files_set = list_files_in_dir(absolute_list_path)
|
|
88
|
+
|
|
89
|
+
# Merge results, prioritizing shadow files if exist
|
|
90
|
+
merged_files = set()
|
|
91
|
+
if shadow_exists:
|
|
92
|
+
# Use shadow files + source files that are NOT shadowed
|
|
93
|
+
merged_files = shadow_files_set.union(
|
|
94
|
+
{f for f in source_files_set if f not in shadow_files_set}
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
merged_files = source_files_set
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
|
|
101
|
+
logger.info(message)
|
|
102
|
+
return ToolResult(success=True, message=message, content=sorted(merged_files))
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
|
|
105
|
+
return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
import typing
|
|
3
|
+
from autocoder.common import AutoCoderArgs
|
|
4
|
+
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
|
+
from autocoder.common.v2.agent.agentic_edit_types import PlanModeRespondTool, ToolResult # Import ToolResult from types
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
10
|
+
|
|
11
|
+
class PlanModeRespondToolResolver(BaseToolResolver):
|
|
12
|
+
def __init__(self, agent: Optional['AgenticEdit'], tool: PlanModeRespondTool, args: AutoCoderArgs):
|
|
13
|
+
super().__init__(agent, tool, args)
|
|
14
|
+
self.tool: PlanModeRespondTool = tool # For type hinting
|
|
15
|
+
|
|
16
|
+
def resolve(self) -> ToolResult:
|
|
17
|
+
"""
|
|
18
|
+
Packages the response and options for Plan Mode interaction.
|
|
19
|
+
"""
|
|
20
|
+
response_text = self.tool.response
|
|
21
|
+
options = self.tool.options
|
|
22
|
+
|
|
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)
|