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.

Files changed (37) hide show
  1. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/RECORD +37 -27
  3. autocoder/auto_coder_runner.py +19 -14
  4. autocoder/chat_auto_coder_lang.py +5 -3
  5. autocoder/common/auto_coder_lang.py +3 -3
  6. autocoder/common/model_speed_tester.py +392 -0
  7. autocoder/common/printer.py +7 -8
  8. autocoder/common/run_cmd.py +247 -0
  9. autocoder/common/test_run_cmd.py +110 -0
  10. autocoder/common/v2/agent/agentic_edit.py +82 -29
  11. autocoder/common/v2/agent/agentic_edit_conversation.py +9 -0
  12. autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +21 -36
  13. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +4 -7
  14. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +2 -5
  15. autocoder/helper/rag_doc_creator.py +141 -0
  16. autocoder/ignorefiles/__init__.py +4 -0
  17. autocoder/ignorefiles/ignore_file_utils.py +63 -0
  18. autocoder/ignorefiles/test_ignore_file_utils.py +91 -0
  19. autocoder/models.py +49 -9
  20. autocoder/plugins/__init__.py +20 -0
  21. autocoder/rag/cache/byzer_storage_cache.py +10 -4
  22. autocoder/rag/cache/file_monitor_cache.py +27 -24
  23. autocoder/rag/cache/local_byzer_storage_cache.py +11 -5
  24. autocoder/rag/cache/local_duckdb_storage_cache.py +203 -128
  25. autocoder/rag/cache/simple_cache.py +56 -37
  26. autocoder/rag/loaders/filter_utils.py +106 -0
  27. autocoder/rag/loaders/image_loader.py +573 -0
  28. autocoder/rag/loaders/pdf_loader.py +3 -3
  29. autocoder/rag/loaders/test_image_loader.py +209 -0
  30. autocoder/rag/qa_conversation_strategy.py +3 -5
  31. autocoder/rag/utils.py +20 -9
  32. autocoder/utils/_markitdown.py +35 -0
  33. autocoder/version.py +1 -1
  34. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/LICENSE +0 -0
  35. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/WHEEL +0 -0
  36. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/entry_points.txt +0 -0
  37. {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
- ### {{ key }}
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 = os.path.splitext(fname)[0]
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
- ] + self.conversation_manager.get_history()
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
- logger.debug(
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
- yield event # Yield text/thinking immediately for display
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.debug(
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
- conversations.append(
925
- {"role": "assistant", "content": assistant_buffer})
926
- self.conversation_manager.add_assistant_message(
927
- assistant_buffer)
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
- # 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
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: {returncode}")
70
- if stdout:
71
- logger.info(f"stdout:\n{stdout}")
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 returncode == 0:
77
- return ToolResult(success=True, message="Command executed successfully.", content=stdout)
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 {returncode}.\nStderr:\n{stderr}\nStdout:\n{stdout}"
80
- return ToolResult(success=False, message=error_message, content={"stdout": stdout, "stderr": stderr, "returncode": returncode})
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.common.v2.agent.ignore_utils import load_ignore_spec, should_ignore, DEFAULT_IGNORED_DIRS
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), ignore_spec, DEFAULT_IGNORED_DIRS, absolute_source_dir)]
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, ignore_spec, DEFAULT_IGNORED_DIRS, absolute_source_dir):
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, ignore_spec, DEFAULT_IGNORED_DIRS, absolute_source_dir):
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.common.v2.agent.ignore_utils import load_ignore_spec, should_ignore, DEFAULT_IGNORED_DIRS
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, ignore_spec, DEFAULT_IGNORED_DIRS, absolute_source_dir):
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}")