auto-coder 0.1.347__py3-none-any.whl → 0.1.349__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.347.dist-info → auto_coder-0.1.349.dist-info}/METADATA +1 -1
- {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/RECORD +37 -27
- autocoder/auto_coder_runner.py +19 -14
- autocoder/chat_auto_coder_lang.py +5 -3
- autocoder/common/auto_coder_lang.py +3 -3
- autocoder/common/model_speed_tester.py +392 -0
- autocoder/common/printer.py +7 -8
- autocoder/common/run_cmd.py +247 -0
- autocoder/common/test_run_cmd.py +110 -0
- autocoder/common/v2/agent/agentic_edit.py +82 -29
- autocoder/common/v2/agent/agentic_edit_conversation.py +9 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +21 -36
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +4 -7
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +2 -5
- autocoder/helper/rag_doc_creator.py +141 -0
- autocoder/ignorefiles/__init__.py +4 -0
- autocoder/ignorefiles/ignore_file_utils.py +63 -0
- autocoder/ignorefiles/test_ignore_file_utils.py +91 -0
- autocoder/models.py +49 -9
- autocoder/plugins/__init__.py +20 -0
- autocoder/rag/cache/byzer_storage_cache.py +10 -4
- autocoder/rag/cache/file_monitor_cache.py +27 -24
- autocoder/rag/cache/local_byzer_storage_cache.py +11 -5
- autocoder/rag/cache/local_duckdb_storage_cache.py +203 -128
- autocoder/rag/cache/simple_cache.py +56 -37
- autocoder/rag/loaders/filter_utils.py +106 -0
- autocoder/rag/loaders/image_loader.py +573 -0
- autocoder/rag/loaders/pdf_loader.py +3 -3
- autocoder/rag/loaders/test_image_loader.py +209 -0
- autocoder/rag/qa_conversation_strategy.py +3 -5
- autocoder/rag/utils.py +20 -9
- autocoder/utils/_markitdown.py +35 -0
- autocoder/version.py +1 -1
- {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import platform
|
|
4
|
+
from unittest import mock
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from autocoder.common.run_cmd import (
|
|
10
|
+
run_cmd,
|
|
11
|
+
run_cmd_subprocess,
|
|
12
|
+
run_cmd_subprocess_generator,
|
|
13
|
+
run_cmd_pexpect,
|
|
14
|
+
get_windows_parent_process_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def test_run_cmd_basic():
|
|
18
|
+
"""
|
|
19
|
+
测试run_cmd函数,确保其正确执行命令并返回预期结果。
|
|
20
|
+
"""
|
|
21
|
+
cmd = "echo hello"
|
|
22
|
+
exit_code, output = run_cmd(cmd)
|
|
23
|
+
assert exit_code == 0, f"命令退出码非零: {exit_code}"
|
|
24
|
+
assert "hello" in output, f"输出不包含hello: {output}"
|
|
25
|
+
|
|
26
|
+
def test_run_cmd_subprocess_normal():
|
|
27
|
+
"""
|
|
28
|
+
测试run_cmd_subprocess正常执行命令,逐步输出。
|
|
29
|
+
"""
|
|
30
|
+
cmd = "echo hello_subprocess"
|
|
31
|
+
gen = run_cmd_subprocess_generator(cmd)
|
|
32
|
+
output = ""
|
|
33
|
+
try:
|
|
34
|
+
for chunk in gen:
|
|
35
|
+
output += chunk
|
|
36
|
+
except Exception as e:
|
|
37
|
+
pytest.fail(f"run_cmd_subprocess异常: {e}")
|
|
38
|
+
assert "hello_subprocess" in output
|
|
39
|
+
|
|
40
|
+
def test_run_cmd_subprocess_error():
|
|
41
|
+
"""
|
|
42
|
+
测试run_cmd_subprocess执行错误命令时能否正确返回异常信息。
|
|
43
|
+
"""
|
|
44
|
+
cmd = "non_existing_command_xyz"
|
|
45
|
+
gen = run_cmd_subprocess_generator(cmd)
|
|
46
|
+
output = ""
|
|
47
|
+
for chunk in gen:
|
|
48
|
+
output += chunk
|
|
49
|
+
# 应该包含错误提示
|
|
50
|
+
assert "[run_cmd_subprocess error]" in output or "not found" in output or "无法" in output or "未找到" in output
|
|
51
|
+
|
|
52
|
+
def test_run_cmd_pexpect_mock():
|
|
53
|
+
"""
|
|
54
|
+
测试run_cmd_pexpect函数,mock pexpect交互行为。
|
|
55
|
+
"""
|
|
56
|
+
with mock.patch("pexpect.spawn") as mock_spawn:
|
|
57
|
+
mock_child = mock.MagicMock()
|
|
58
|
+
mock_child.exitstatus = 0
|
|
59
|
+
mock_child.interact.side_effect = lambda output_filter=None: output_filter(b"mock output\n")
|
|
60
|
+
mock_child.close.return_value = None
|
|
61
|
+
mock_child.exitstatus = 0
|
|
62
|
+
mock_child.getvalue = lambda: b"mock output\n"
|
|
63
|
+
mock_spawn.return_value = mock_child
|
|
64
|
+
|
|
65
|
+
# 由于run_cmd_pexpect内部会decode BytesIO内容
|
|
66
|
+
exit_code, output = run_cmd_pexpect("echo hello", verbose=False)
|
|
67
|
+
assert exit_code == 0
|
|
68
|
+
assert "mock output" in output
|
|
69
|
+
|
|
70
|
+
def test_get_windows_parent_process_name_mocked():
|
|
71
|
+
"""
|
|
72
|
+
测试get_windows_parent_process_name,模拟不同父进程
|
|
73
|
+
"""
|
|
74
|
+
if platform.system() != "Windows":
|
|
75
|
+
# 非Windows系统跳过
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# 构造mock进程树
|
|
79
|
+
class FakeProcess:
|
|
80
|
+
def __init__(self, name, parent=None):
|
|
81
|
+
self._name = name
|
|
82
|
+
self._parent = parent
|
|
83
|
+
|
|
84
|
+
def name(self):
|
|
85
|
+
return self._name
|
|
86
|
+
|
|
87
|
+
def parent(self):
|
|
88
|
+
return self._parent
|
|
89
|
+
|
|
90
|
+
powershell_proc = FakeProcess("powershell.exe")
|
|
91
|
+
cmd_proc = FakeProcess("cmd.exe")
|
|
92
|
+
other_proc = FakeProcess("python.exe")
|
|
93
|
+
|
|
94
|
+
# 模拟powershell父进程
|
|
95
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
96
|
+
MockProcess.return_value = FakeProcess("python.exe", powershell_proc)
|
|
97
|
+
assert get_windows_parent_process_name() == "powershell.exe"
|
|
98
|
+
|
|
99
|
+
# 模拟cmd父进程
|
|
100
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
101
|
+
MockProcess.return_value = FakeProcess("python.exe", cmd_proc)
|
|
102
|
+
assert get_windows_parent_process_name() == "cmd.exe"
|
|
103
|
+
|
|
104
|
+
# 模拟无匹配父进程
|
|
105
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
106
|
+
MockProcess.return_value = FakeProcess("python.exe", other_proc)
|
|
107
|
+
assert get_windows_parent_process_name() is None
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(pytest.main([__file__]))
|
|
@@ -212,18 +212,7 @@ class AgenticEdit:
|
|
|
212
212
|
@byzerllm.prompt()
|
|
213
213
|
def _analyze(self, request: AgenticEditRequest) -> str:
|
|
214
214
|
"""
|
|
215
|
-
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
|
216
|
-
|
|
217
|
-
====
|
|
218
|
-
|
|
219
|
-
FILES CONTEXT
|
|
220
|
-
|
|
221
|
-
The following files are provided to you as context for the user's task. You can use these files to understand the project structure and codebase, and to make informed decisions about which files to modify.
|
|
222
|
-
If you need to read more files, you can use the tools to find and read more files.
|
|
223
|
-
|
|
224
|
-
<files>
|
|
225
|
-
{{files}}
|
|
226
|
-
</files>
|
|
215
|
+
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
|
227
216
|
|
|
228
217
|
====
|
|
229
218
|
|
|
@@ -690,8 +679,10 @@ class AgenticEdit:
|
|
|
690
679
|
The following rules are provided by the user, and you must follow them strictly.
|
|
691
680
|
|
|
692
681
|
{% for key, value in extra_docs.items() %}
|
|
693
|
-
|
|
682
|
+
<user_rule>
|
|
683
|
+
##File: {{ key }}
|
|
694
684
|
{{ value }}
|
|
685
|
+
</user_rule>
|
|
695
686
|
{% endfor %}
|
|
696
687
|
{% endif %}
|
|
697
688
|
"""
|
|
@@ -706,7 +697,7 @@ class AgenticEdit:
|
|
|
706
697
|
try:
|
|
707
698
|
with open(fpath, "r", encoding="utf-8") as f:
|
|
708
699
|
content = f.read()
|
|
709
|
-
key =
|
|
700
|
+
key = fpath
|
|
710
701
|
extra_docs[key] = content
|
|
711
702
|
except Exception:
|
|
712
703
|
continue
|
|
@@ -780,26 +771,57 @@ class AgenticEdit:
|
|
|
780
771
|
Analyzes the user request, interacts with the LLM, parses responses,
|
|
781
772
|
executes tools, and yields structured events for visualization until completion or error.
|
|
782
773
|
"""
|
|
774
|
+
logger.info(f"Starting analyze method with user input: {request.user_input[:50]}...")
|
|
783
775
|
system_prompt = self._analyze.prompt(request)
|
|
776
|
+
logger.info(f"Generated system prompt with length: {len(system_prompt)}")
|
|
777
|
+
|
|
784
778
|
# print(system_prompt)
|
|
785
779
|
conversations = [
|
|
786
780
|
{"role": "system", "content": system_prompt},
|
|
787
|
-
]
|
|
781
|
+
]
|
|
782
|
+
|
|
783
|
+
logger.info("Adding initial files context to conversation")
|
|
784
|
+
conversations.append({
|
|
785
|
+
"role":"user","content":f'''
|
|
786
|
+
Below are some files the user is focused on, and the content is up to date. These entries show the file paths along with their full text content, which can help you better understand the user's needs. If the information is insufficient, you can use tools such as read_file to retrieve more details.
|
|
787
|
+
<files>
|
|
788
|
+
{self.files.to_str()}
|
|
789
|
+
</files>'''
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
conversations.append({
|
|
793
|
+
"role":"assistant","content":"Ok"
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
logger.info("Adding conversation history")
|
|
797
|
+
conversations.extend(self.conversation_manager.get_history())
|
|
788
798
|
conversations.append({
|
|
789
799
|
"role": "user", "content": request.user_input
|
|
790
800
|
})
|
|
791
801
|
self.conversation_manager.add_user_message(request.user_input)
|
|
792
|
-
|
|
802
|
+
|
|
803
|
+
logger.info(
|
|
793
804
|
f"Initial conversation history size: {len(conversations)}")
|
|
794
805
|
|
|
806
|
+
iteration_count = 0
|
|
795
807
|
tool_executed = False
|
|
796
808
|
while True:
|
|
809
|
+
iteration_count += 1
|
|
810
|
+
logger.info(f"Starting LLM interaction cycle #{iteration_count}")
|
|
797
811
|
global_cancel.check_and_raise()
|
|
812
|
+
last_message = conversations[-1]
|
|
813
|
+
if last_message["role"] == "assistant":
|
|
814
|
+
logger.info(f"Last message is assistant, skipping LLM interaction cycle")
|
|
815
|
+
yield CompletionEvent(completion=AttemptCompletionTool(
|
|
816
|
+
result=last_message["content"],
|
|
817
|
+
command=""
|
|
818
|
+
), completion_xml="")
|
|
819
|
+
break
|
|
798
820
|
logger.info(
|
|
799
821
|
f"Starting LLM interaction cycle. History size: {len(conversations)}")
|
|
800
822
|
|
|
801
823
|
assistant_buffer = ""
|
|
802
|
-
|
|
824
|
+
logger.info("Initializing stream chat with LLM")
|
|
803
825
|
llm_response_gen = stream_chat_with_continue(
|
|
804
826
|
llm=self.llm,
|
|
805
827
|
conversations=conversations,
|
|
@@ -808,21 +830,29 @@ class AgenticEdit:
|
|
|
808
830
|
)
|
|
809
831
|
|
|
810
832
|
meta_holder = byzerllm.MetaHolder()
|
|
833
|
+
logger.info("Starting to parse LLM response stream")
|
|
811
834
|
parsed_events = self.stream_and_parse_llm_response(
|
|
812
835
|
llm_response_gen, meta_holder)
|
|
813
836
|
|
|
837
|
+
event_count = 0
|
|
814
838
|
for event in parsed_events:
|
|
839
|
+
event_count += 1
|
|
840
|
+
logger.info(f"Processing event #{event_count}: {type(event).__name__}")
|
|
815
841
|
global_cancel.check_and_raise()
|
|
816
842
|
if isinstance(event, (LLMOutputEvent, LLMThinkingEvent)):
|
|
817
843
|
assistant_buffer += event.text
|
|
818
|
-
|
|
844
|
+
logger.debug(f"Accumulated {len(assistant_buffer)} chars in assistant buffer")
|
|
845
|
+
yield event # Yield text/thinking immediately for display
|
|
819
846
|
|
|
820
847
|
elif isinstance(event, ToolCallEvent):
|
|
821
848
|
tool_executed = True
|
|
822
849
|
tool_obj = event.tool
|
|
850
|
+
tool_name = type(tool_obj).__name__
|
|
851
|
+
logger.info(f"Tool call detected: {tool_name}")
|
|
823
852
|
tool_xml = event.tool_xml # Already reconstructed by parser
|
|
824
853
|
|
|
825
854
|
# Append assistant's thoughts and the tool call to history
|
|
855
|
+
logger.info(f"Adding assistant message with tool call to conversation history")
|
|
826
856
|
conversations.append({
|
|
827
857
|
"role": "assistant",
|
|
828
858
|
"content": assistant_buffer + tool_xml
|
|
@@ -832,11 +862,13 @@ class AgenticEdit:
|
|
|
832
862
|
assistant_buffer = "" # Reset buffer after tool call
|
|
833
863
|
|
|
834
864
|
yield event # Yield the ToolCallEvent for display
|
|
865
|
+
logger.info("Yielded ToolCallEvent")
|
|
835
866
|
|
|
836
867
|
# Handle AttemptCompletion separately as it ends the loop
|
|
837
868
|
if isinstance(tool_obj, AttemptCompletionTool):
|
|
838
869
|
logger.info(
|
|
839
870
|
"AttemptCompletionTool received. Finalizing session.")
|
|
871
|
+
logger.info(f"Completion result: {tool_obj.result[:50]}...")
|
|
840
872
|
yield CompletionEvent(completion=tool_obj, completion_xml=tool_xml)
|
|
841
873
|
logger.info(
|
|
842
874
|
"AgenticEdit analyze loop finished due to AttemptCompletion.")
|
|
@@ -845,6 +877,7 @@ class AgenticEdit:
|
|
|
845
877
|
if isinstance(tool_obj, PlanModeRespondTool):
|
|
846
878
|
logger.info(
|
|
847
879
|
"PlanModeRespondTool received. Finalizing session.")
|
|
880
|
+
logger.info(f"Plan mode response: {tool_obj.response[:50]}...")
|
|
848
881
|
yield PlanModeRespondEvent(completion=tool_obj, completion_xml=tool_xml)
|
|
849
882
|
logger.info(
|
|
850
883
|
"AgenticEdit analyze loop finished due to PlanModeRespond.")
|
|
@@ -862,6 +895,7 @@ class AgenticEdit:
|
|
|
862
895
|
error_xml = f"<tool_result tool_name='{type(tool_obj).__name__}' success='false'><message>Error: Tool resolver not implemented.</message><content></content></tool_result>"
|
|
863
896
|
else:
|
|
864
897
|
try:
|
|
898
|
+
logger.info(f"Creating resolver for tool: {tool_name}")
|
|
865
899
|
resolver = resolver_cls(
|
|
866
900
|
agent=self, tool=tool_obj, args=self.args)
|
|
867
901
|
logger.info(
|
|
@@ -873,6 +907,7 @@ class AgenticEdit:
|
|
|
873
907
|
tool_obj).__name__, result=tool_result)
|
|
874
908
|
|
|
875
909
|
# Prepare XML for conversation history
|
|
910
|
+
logger.info("Preparing XML for conversation history")
|
|
876
911
|
escaped_message = xml.sax.saxutils.escape(
|
|
877
912
|
tool_result.message)
|
|
878
913
|
content_str = str(
|
|
@@ -897,40 +932,55 @@ class AgenticEdit:
|
|
|
897
932
|
error_message)
|
|
898
933
|
error_xml = f"<tool_result tool_name='{type(tool_obj).__name__}' success='false'><message>{escaped_error}</message><content></content></tool_result>"
|
|
899
934
|
|
|
900
|
-
yield result_event # Yield the ToolResultEvent for display
|
|
935
|
+
yield result_event # Yield the ToolResultEvent for display
|
|
936
|
+
logger.info("Yielded ToolResultEvent")
|
|
901
937
|
|
|
902
938
|
# Append the tool result (as user message) to history
|
|
939
|
+
logger.info("Adding tool result to conversation history")
|
|
903
940
|
conversations.append({
|
|
904
941
|
"role": "user", # Simulating the user providing the tool result
|
|
905
942
|
"content": error_xml
|
|
906
943
|
})
|
|
907
944
|
self.conversation_manager.add_user_message(error_xml)
|
|
908
|
-
logger.
|
|
945
|
+
logger.info(
|
|
909
946
|
f"Added tool result to conversations for tool {type(tool_obj).__name__}")
|
|
947
|
+
logger.info(f"Breaking LLM cycle after executing tool: {tool_name}")
|
|
910
948
|
break # After tool execution and result, break to start a new LLM cycle
|
|
911
949
|
|
|
912
950
|
elif isinstance(event, ErrorEvent):
|
|
951
|
+
logger.error(f"Error event occurred: {event.message}")
|
|
913
952
|
yield event # Pass through errors
|
|
914
953
|
# Optionally stop the process on parsing errors
|
|
915
954
|
# logger.error("Stopping analyze loop due to parsing error.")
|
|
916
955
|
# return
|
|
917
956
|
|
|
957
|
+
logger.info("Yielding token usage event")
|
|
918
958
|
yield TokenUsageEvent(usage=meta_holder.meta)
|
|
959
|
+
|
|
919
960
|
if not tool_executed:
|
|
920
961
|
# No tool executed in this LLM response cycle
|
|
921
|
-
logger.info("LLM response finished without executing a tool.")
|
|
962
|
+
logger.info("LLM response finished without executing a tool.")
|
|
922
963
|
# Append any remaining assistant buffer to history if it wasn't followed by a tool
|
|
923
964
|
if assistant_buffer:
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
965
|
+
logger.info(f"Appending assistant buffer to history: {len(assistant_buffer)} chars")
|
|
966
|
+
last_message = conversations[-1]
|
|
967
|
+
if last_message["role"] != "assistant":
|
|
968
|
+
logger.info("Adding new assistant message")
|
|
969
|
+
conversations.append(
|
|
970
|
+
{"role": "assistant", "content": assistant_buffer})
|
|
971
|
+
self.conversation_manager.add_assistant_message(
|
|
972
|
+
assistant_buffer)
|
|
973
|
+
elif last_message["role"] == "assistant":
|
|
974
|
+
logger.info("Appending to existing assistant message")
|
|
975
|
+
last_message["content"] += assistant_buffer
|
|
976
|
+
self.conversation_manager.append_to_last_message(assistant_buffer)
|
|
928
977
|
# If the loop ends without AttemptCompletion, it means the LLM finished talking
|
|
929
978
|
# without signaling completion. We might just stop or yield a final message.
|
|
930
979
|
# Let's assume it stops here.
|
|
980
|
+
logger.info("No tool executed and LLM finished. Breaking out of main loop.")
|
|
931
981
|
break
|
|
932
982
|
|
|
933
|
-
logger.info("AgenticEdit analyze loop finished.")
|
|
983
|
+
logger.info(f"AgenticEdit analyze loop finished after {iteration_count} iterations.")
|
|
934
984
|
|
|
935
985
|
def stream_and_parse_llm_response(
|
|
936
986
|
self, generator: Generator[Tuple[str, Any], None, None], meta_holder: byzerllm.MetaHolder
|
|
@@ -1013,9 +1063,7 @@ class AgenticEdit:
|
|
|
1013
1063
|
|
|
1014
1064
|
for content_chunk, metadata in generator:
|
|
1015
1065
|
global_cancel.check_and_raise()
|
|
1016
|
-
|
|
1017
|
-
meta_holder.meta = metadata
|
|
1018
|
-
logger.info(f"metadata: {metadata.input_tokens_count}")
|
|
1066
|
+
meta_holder.meta = metadata
|
|
1019
1067
|
if not content_chunk:
|
|
1020
1068
|
continue
|
|
1021
1069
|
buffer += content_chunk
|
|
@@ -1320,6 +1368,11 @@ class AgenticEdit:
|
|
|
1320
1368
|
Apply all tracked file changes to the original project directory.
|
|
1321
1369
|
"""
|
|
1322
1370
|
for (file_path, change) in self.get_all_file_changes().items():
|
|
1371
|
+
# Ensure the directory exists before writing the file
|
|
1372
|
+
dir_path = os.path.dirname(file_path)
|
|
1373
|
+
if dir_path: # Ensure dir_path is not empty (for files in root)
|
|
1374
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
1375
|
+
|
|
1323
1376
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
1324
1377
|
f.write(change.content)
|
|
1325
1378
|
|
|
@@ -68,6 +68,15 @@ class AgenticConversation:
|
|
|
68
68
|
"""Adds an assistant message (potentially containing text response)."""
|
|
69
69
|
self.add_message(role="assistant", content=content)
|
|
70
70
|
|
|
71
|
+
def append_to_last_message(self, content: str, role: str = "assistant"):
|
|
72
|
+
"""Appends content to the last message."""
|
|
73
|
+
if self._history:
|
|
74
|
+
last_message = self._history[-1]
|
|
75
|
+
if role and last_message["role"] == role:
|
|
76
|
+
last_message["content"] += content
|
|
77
|
+
elif not role:
|
|
78
|
+
last_message["content"] += content
|
|
79
|
+
|
|
71
80
|
def add_assistant_tool_call_message(self, tool_calls: List[Dict[str, Any]], content: Optional[str] = None):
|
|
72
81
|
"""
|
|
73
82
|
Adds a message representing one or more tool calls from the assistant.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import os
|
|
3
3
|
from typing import Dict, Any, Optional
|
|
4
|
+
from autocoder.common.run_cmd import run_cmd_subprocess
|
|
4
5
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
5
6
|
from autocoder.common.v2.agent.agentic_edit_types import ExecuteCommandTool, ToolResult # Import ToolResult from types
|
|
6
7
|
from autocoder.common import shells
|
|
@@ -8,7 +9,8 @@ from autocoder.common.printer import Printer
|
|
|
8
9
|
from loguru import logger
|
|
9
10
|
import typing
|
|
10
11
|
from autocoder.common import AutoCoderArgs
|
|
11
|
-
|
|
12
|
+
from autocoder.events.event_manager_singleton import get_event_manager
|
|
13
|
+
from autocoder.run_context import get_run_context
|
|
12
14
|
if typing.TYPE_CHECKING:
|
|
13
15
|
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
14
16
|
|
|
@@ -39,45 +41,28 @@ class ExecuteCommandToolResolver(BaseToolResolver):
|
|
|
39
41
|
pass
|
|
40
42
|
|
|
41
43
|
printer.print_str_in_terminal(f"Executing command: {command} in {os.path.abspath(source_dir)}")
|
|
42
|
-
try:
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
44
|
+
try:
|
|
45
|
+
# 使用封装的run_cmd方法执行命令
|
|
46
|
+
if get_run_context().is_web():
|
|
47
|
+
answer = get_event_manager(
|
|
48
|
+
self.args.event_file).ask_user(prompt=f"Allow to execute the `{command}`?",options=["yes","no"])
|
|
49
|
+
if answer == "yes":
|
|
50
|
+
pass
|
|
51
|
+
else:
|
|
52
|
+
return ToolResult(success=False, message=f"Command '{command}' execution denied by user.")
|
|
53
|
+
|
|
54
|
+
exit_code, output = run_cmd_subprocess(command, verbose=True, cwd=source_dir)
|
|
67
55
|
|
|
68
56
|
logger.info(f"Command executed: {command}")
|
|
69
|
-
logger.info(f"Return Code: {
|
|
70
|
-
if
|
|
71
|
-
logger.info(f"
|
|
72
|
-
if stderr:
|
|
73
|
-
logger.info(f"stderr:\n{stderr}")
|
|
74
|
-
|
|
57
|
+
logger.info(f"Return Code: {exit_code}")
|
|
58
|
+
if output:
|
|
59
|
+
logger.info(f"Output:\n{output}")
|
|
75
60
|
|
|
76
|
-
if
|
|
77
|
-
return ToolResult(success=True, message="Command executed successfully.", content=
|
|
61
|
+
if exit_code == 0:
|
|
62
|
+
return ToolResult(success=True, message="Command executed successfully.", content=output)
|
|
78
63
|
else:
|
|
79
|
-
error_message = f"Command failed with return code {
|
|
80
|
-
return ToolResult(success=False, message=error_message, content={"
|
|
64
|
+
error_message = f"Command failed with return code {exit_code}.\nOutput:\n{output}"
|
|
65
|
+
return ToolResult(success=False, message=error_message, content={"output": output, "returncode": exit_code})
|
|
81
66
|
|
|
82
67
|
except FileNotFoundError:
|
|
83
68
|
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.")
|
|
@@ -6,7 +6,7 @@ from loguru import logger
|
|
|
6
6
|
import typing
|
|
7
7
|
from autocoder.common import AutoCoderArgs
|
|
8
8
|
|
|
9
|
-
from autocoder.
|
|
9
|
+
from autocoder.ignorefiles.ignore_file_utils import should_ignore
|
|
10
10
|
|
|
11
11
|
if typing.TYPE_CHECKING:
|
|
12
12
|
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
@@ -25,9 +25,6 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
25
25
|
absolute_source_dir = os.path.abspath(source_dir)
|
|
26
26
|
absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
|
|
27
27
|
|
|
28
|
-
# Load ignore spec from .autocoderignore if exists
|
|
29
|
-
ignore_spec = load_ignore_spec(absolute_source_dir)
|
|
30
|
-
|
|
31
28
|
# Security check: Allow listing outside source_dir IF the original path is outside?
|
|
32
29
|
is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
|
|
33
30
|
if is_outside_source:
|
|
@@ -59,10 +56,10 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
59
56
|
if recursive:
|
|
60
57
|
for root, dirs, files in os.walk(base_dir):
|
|
61
58
|
# Modify dirs in-place to skip ignored dirs early
|
|
62
|
-
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d)
|
|
59
|
+
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
|
|
63
60
|
for name in files:
|
|
64
61
|
full_path = os.path.join(root, name)
|
|
65
|
-
if should_ignore(full_path
|
|
62
|
+
if should_ignore(full_path):
|
|
66
63
|
continue
|
|
67
64
|
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
68
65
|
result.add(display_path)
|
|
@@ -73,7 +70,7 @@ class ListFilesToolResolver(BaseToolResolver):
|
|
|
73
70
|
else:
|
|
74
71
|
for item in os.listdir(base_dir):
|
|
75
72
|
full_path = os.path.join(base_dir, item)
|
|
76
|
-
if should_ignore(full_path
|
|
73
|
+
if should_ignore(full_path):
|
|
77
74
|
continue
|
|
78
75
|
display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
|
|
79
76
|
if os.path.isdir(full_path):
|
|
@@ -9,7 +9,7 @@ from loguru import logger
|
|
|
9
9
|
from autocoder.common import AutoCoderArgs
|
|
10
10
|
import typing
|
|
11
11
|
|
|
12
|
-
from autocoder.
|
|
12
|
+
from autocoder.ignorefiles.ignore_file_utils import should_ignore
|
|
13
13
|
|
|
14
14
|
if typing.TYPE_CHECKING:
|
|
15
15
|
from autocoder.common.v2.agent.agentic_edit import AgenticEdit
|
|
@@ -29,9 +29,6 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
29
29
|
absolute_source_dir = os.path.abspath(source_dir)
|
|
30
30
|
absolute_search_path = os.path.abspath(os.path.join(source_dir, search_path_str))
|
|
31
31
|
|
|
32
|
-
# Load ignore spec from .autocoderignore if exists
|
|
33
|
-
ignore_spec = load_ignore_spec(absolute_source_dir)
|
|
34
|
-
|
|
35
32
|
# Security check
|
|
36
33
|
if not absolute_search_path.startswith(absolute_source_dir):
|
|
37
34
|
return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
|
|
@@ -65,7 +62,7 @@ class SearchFilesToolResolver(BaseToolResolver):
|
|
|
65
62
|
|
|
66
63
|
for filepath in glob.glob(search_glob_pattern, recursive=True):
|
|
67
64
|
abs_path = os.path.abspath(filepath)
|
|
68
|
-
if should_ignore(abs_path
|
|
65
|
+
if should_ignore(abs_path):
|
|
69
66
|
continue
|
|
70
67
|
|
|
71
68
|
if os.path.isfile(filepath):
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
def create_sample_files(base_dir: str):
|
|
6
|
+
"""创建示例代码文件用于演示"""
|
|
7
|
+
os.makedirs(base_dir, exist_ok=True)
|
|
8
|
+
|
|
9
|
+
calculator_content = """
|
|
10
|
+
class Calculator:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.history = []
|
|
13
|
+
|
|
14
|
+
def add(self, a: int, b: int) -> int:
|
|
15
|
+
'''加法函数'''
|
|
16
|
+
result = a + b
|
|
17
|
+
self.history.append(f"{a} + {b} = {result}")
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
def subtract(self, a: int, b: int) -> int:
|
|
21
|
+
'''减法函数'''
|
|
22
|
+
result = a - b
|
|
23
|
+
self.history.append(f"{a} - {b} = {result}")
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def multiply(self, a: int, b: int) -> int:
|
|
27
|
+
'''乘法函数'''
|
|
28
|
+
result = a * b
|
|
29
|
+
self.history.append(f"{a} * {b} = {result}")
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
def divide(self, a: int, b: int) -> float:
|
|
33
|
+
'''除法函数'''
|
|
34
|
+
if b == 0:
|
|
35
|
+
raise ValueError("Cannot divide by zero")
|
|
36
|
+
result = a / b
|
|
37
|
+
self.history.append(f"{a} / {b} = {result}")
|
|
38
|
+
return result
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
string_processor_content = """
|
|
42
|
+
class StringProcessor:
|
|
43
|
+
@staticmethod
|
|
44
|
+
def reverse(text: str) -> str:
|
|
45
|
+
'''反转字符串'''
|
|
46
|
+
return text[::-1]
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def capitalize(text: str) -> str:
|
|
50
|
+
'''首字母大写'''
|
|
51
|
+
return text.capitalize()
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def count_words(text: str) -> int:
|
|
55
|
+
'''计算单词数量'''
|
|
56
|
+
return len(text.split())
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def format_name(first_name: str, last_name: str) -> str:
|
|
60
|
+
'''格式化姓名'''
|
|
61
|
+
return f"{last_name}, {first_name}"
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
data_processor_content = """
|
|
65
|
+
import statistics
|
|
66
|
+
from typing import List, Dict, Any
|
|
67
|
+
|
|
68
|
+
class DataProcessor:
|
|
69
|
+
@staticmethod
|
|
70
|
+
def calculate_average(numbers: List[float]) -> float:
|
|
71
|
+
'''计算平均值'''
|
|
72
|
+
return sum(numbers) / len(numbers)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def find_median(numbers: List[float]) -> float:
|
|
76
|
+
'''计算中位数'''
|
|
77
|
+
sorted_numbers = sorted(numbers)
|
|
78
|
+
n = len(sorted_numbers)
|
|
79
|
+
if n % 2 == 0:
|
|
80
|
+
return (sorted_numbers[n//2-1] + sorted_numbers[n//2]) / 2
|
|
81
|
+
else:
|
|
82
|
+
return sorted_numbers[n//2]
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def find_mode(numbers: List[float]) -> List[float]:
|
|
86
|
+
'''查找众数'''
|
|
87
|
+
try:
|
|
88
|
+
return statistics.mode(numbers)
|
|
89
|
+
except statistics.StatisticsError:
|
|
90
|
+
# 没有唯一众数的情况
|
|
91
|
+
count_dict = {}
|
|
92
|
+
for num in numbers:
|
|
93
|
+
if num in count_dict:
|
|
94
|
+
count_dict[num] += 1
|
|
95
|
+
else:
|
|
96
|
+
count_dict[num] = 1
|
|
97
|
+
max_count = max(count_dict.values())
|
|
98
|
+
return [num for num, count in count_dict.items() if count == max_count]
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
with open(os.path.join(base_dir, "calculator.py"), "w", encoding="utf-8") as f:
|
|
102
|
+
f.write(calculator_content)
|
|
103
|
+
|
|
104
|
+
with open(os.path.join(base_dir, "string_processor.py"), "w", encoding="utf-8") as f:
|
|
105
|
+
f.write(string_processor_content)
|
|
106
|
+
|
|
107
|
+
with open(os.path.join(base_dir, "data_processor.py"), "w", encoding="utf-8") as f:
|
|
108
|
+
f.write(data_processor_content)
|
|
109
|
+
|
|
110
|
+
logger.info(f"示例代码文件已创建在: {base_dir}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def update_sample_file(base_dir: str, filename: str, content: str):
|
|
114
|
+
"""更新指定示例文件内容"""
|
|
115
|
+
file_path = os.path.join(base_dir, filename)
|
|
116
|
+
if not os.path.exists(file_path):
|
|
117
|
+
logger.warning(f"文件 {file_path} 不存在,无法更新")
|
|
118
|
+
return
|
|
119
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
120
|
+
f.write(content)
|
|
121
|
+
logger.info(f"已更新文件: {file_path}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def add_sample_file(base_dir: str, filename: str, content: str):
|
|
125
|
+
"""新增示例文件,若存在则覆盖"""
|
|
126
|
+
file_path = os.path.join(base_dir, filename)
|
|
127
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
128
|
+
f.write(content)
|
|
129
|
+
logger.info(f"已新增文件: {file_path}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def delete_sample_file(base_dir: str, filename: str):
|
|
133
|
+
"""删除指定示例文件"""
|
|
134
|
+
file_path = os.path.join(base_dir, filename)
|
|
135
|
+
try:
|
|
136
|
+
os.remove(file_path)
|
|
137
|
+
logger.info(f"已删除文件: {file_path}")
|
|
138
|
+
except FileNotFoundError:
|
|
139
|
+
logger.warning(f"文件 {file_path} 不存在,无法删除")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"删除文件 {file_path} 失败: {e}")
|