auto-coder 0.1.353__py3-none-any.whl → 0.1.355__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 (60) hide show
  1. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/RECORD +60 -45
  3. autocoder/agent/agentic_filter.py +1 -1
  4. autocoder/auto_coder.py +8 -0
  5. autocoder/auto_coder_rag.py +37 -1
  6. autocoder/auto_coder_runner.py +58 -77
  7. autocoder/chat/conf_command.py +270 -0
  8. autocoder/chat/models_command.py +485 -0
  9. autocoder/chat_auto_coder.py +29 -24
  10. autocoder/chat_auto_coder_lang.py +26 -2
  11. autocoder/commands/auto_command.py +60 -132
  12. autocoder/commands/auto_web.py +1 -1
  13. autocoder/commands/tools.py +1 -1
  14. autocoder/common/__init__.py +3 -1
  15. autocoder/common/command_completer.py +58 -12
  16. autocoder/common/command_completer_v2.py +576 -0
  17. autocoder/common/conversations/__init__.py +52 -0
  18. autocoder/common/conversations/compatibility.py +303 -0
  19. autocoder/common/conversations/conversation_manager.py +502 -0
  20. autocoder/common/conversations/example.py +152 -0
  21. autocoder/common/file_monitor/__init__.py +5 -0
  22. autocoder/common/file_monitor/monitor.py +383 -0
  23. autocoder/common/global_cancel.py +53 -16
  24. autocoder/common/ignorefiles/__init__.py +4 -0
  25. autocoder/common/ignorefiles/ignore_file_utils.py +103 -0
  26. autocoder/common/ignorefiles/test_ignore_file_utils.py +91 -0
  27. autocoder/common/rulefiles/__init__.py +15 -0
  28. autocoder/common/rulefiles/autocoderrules_utils.py +173 -0
  29. autocoder/common/save_formatted_log.py +54 -0
  30. autocoder/common/v2/agent/agentic_edit.py +10 -39
  31. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +1 -1
  32. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +73 -43
  33. autocoder/common/v2/code_agentic_editblock_manager.py +9 -9
  34. autocoder/common/v2/code_diff_manager.py +2 -2
  35. autocoder/common/v2/code_editblock_manager.py +31 -18
  36. autocoder/common/v2/code_strict_diff_manager.py +3 -2
  37. autocoder/dispacher/actions/action.py +6 -6
  38. autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
  39. autocoder/events/event_manager_singleton.py +1 -1
  40. autocoder/index/index.py +3 -3
  41. autocoder/models.py +22 -9
  42. autocoder/rag/api_server.py +14 -2
  43. autocoder/rag/cache/local_byzer_storage_cache.py +1 -1
  44. autocoder/rag/cache/local_duckdb_storage_cache.py +8 -0
  45. autocoder/rag/cache/simple_cache.py +63 -33
  46. autocoder/rag/loaders/docx_loader.py +1 -1
  47. autocoder/rag/loaders/filter_utils.py +133 -76
  48. autocoder/rag/loaders/image_loader.py +15 -3
  49. autocoder/rag/loaders/pdf_loader.py +2 -2
  50. autocoder/rag/long_context_rag.py +11 -0
  51. autocoder/rag/qa_conversation_strategy.py +5 -31
  52. autocoder/rag/utils.py +21 -2
  53. autocoder/utils/_markitdown.py +66 -25
  54. autocoder/utils/auto_coder_utils/chat_stream_out.py +4 -4
  55. autocoder/utils/thread_utils.py +9 -27
  56. autocoder/version.py +1 -1
  57. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/LICENSE +0 -0
  58. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/WHEEL +0 -0
  59. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/entry_points.txt +0 -0
  60. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,91 @@
1
+
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from src.autocoder.common.ignorefiles import ignore_file_utils
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def cleanup_ignore_manager(monkeypatch):
12
+ """
13
+ 在每个测试前后清理 IgnoreFileManager 的单例状态,保证测试隔离
14
+ """
15
+ # 备份原始实例
16
+ original_instance = ignore_file_utils._ignore_manager
17
+ # 强制重新加载忽略规则
18
+ def reset_ignore_manager():
19
+ ignore_file_utils.IgnoreFileManager._instance = None
20
+ return ignore_file_utils.IgnoreFileManager()
21
+
22
+ monkeypatch.setattr(ignore_file_utils, "_ignore_manager", reset_ignore_manager())
23
+ yield
24
+ # 恢复原始实例
25
+ ignore_file_utils._ignore_manager = original_instance
26
+
27
+
28
+ def test_default_excludes(tmp_path, monkeypatch):
29
+ # 切换当前工作目录
30
+ monkeypatch.chdir(tmp_path)
31
+
32
+ # 不创建任何 .autocoderignore 文件,使用默认排除规则
33
+ # 创建默认排除目录
34
+ for dirname in ignore_file_utils.DEFAULT_EXCLUDES:
35
+ (tmp_path / dirname).mkdir(parents=True, exist_ok=True)
36
+ # 应该被忽略
37
+ assert ignore_file_utils.should_ignore(str(tmp_path / dirname)) is True
38
+
39
+ # 创建不会被忽略的文件
40
+ normal_file = tmp_path / "myfile.txt"
41
+ normal_file.write_text("hello")
42
+ assert ignore_file_utils.should_ignore(str(normal_file)) is False
43
+
44
+
45
+ def test_custom_ignore_file(tmp_path, monkeypatch):
46
+ monkeypatch.chdir(tmp_path)
47
+
48
+ # 创建自定义忽略文件
49
+ ignore_file = tmp_path / ".autocoderignore"
50
+ ignore_file.write_text("data/**\nsecret.txt")
51
+
52
+ # 重新初始化忽略管理器以加载新规则
53
+ ignore_file_utils.IgnoreFileManager._instance = None
54
+ ignore_file_utils._ignore_manager = ignore_file_utils.IgnoreFileManager()
55
+
56
+ # 符合忽略规则的路径
57
+ ignored_dir = tmp_path / "data" / "subdir"
58
+ ignored_dir.mkdir(parents=True)
59
+ ignored_file = tmp_path / "secret.txt"
60
+ ignored_file.write_text("secret")
61
+
62
+ assert ignore_file_utils.should_ignore(str(ignored_dir)) is True
63
+ assert ignore_file_utils.should_ignore(str(ignored_file)) is True
64
+
65
+ # 不应被忽略的文件
66
+ normal_file = tmp_path / "keepme.txt"
67
+ normal_file.write_text("keep me")
68
+ assert ignore_file_utils.should_ignore(str(normal_file)) is False
69
+
70
+
71
+ def test_nested_ignore_file(tmp_path, monkeypatch):
72
+ monkeypatch.chdir(tmp_path)
73
+
74
+ # 没有根目录的.ignore,创建.auto-coder/.autocoderignore
75
+ nested_dir = tmp_path / ".auto-coder"
76
+ nested_dir.mkdir()
77
+
78
+ ignore_file = nested_dir / ".autocoderignore"
79
+ ignore_file.write_text("logs/**")
80
+
81
+ # 重新初始化忽略管理器以加载新规则
82
+ ignore_file_utils.IgnoreFileManager._instance = None
83
+ ignore_file_utils._ignore_manager = ignore_file_utils.IgnoreFileManager()
84
+
85
+ ignored_dir = tmp_path / "logs" / "2024"
86
+ ignored_dir.mkdir(parents=True)
87
+ assert ignore_file_utils.should_ignore(str(ignored_dir)) is True
88
+
89
+ normal_file = tmp_path / "main.py"
90
+ normal_file.write_text("# main")
91
+ assert ignore_file_utils.should_ignore(str(normal_file)) is False
@@ -0,0 +1,15 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ AutoCoder 规则文件管理模块
5
+
6
+ 提供读取、解析和监控 AutoCoder 规则文件的功能。
7
+ """
8
+
9
+ from .autocoderrules_utils import (
10
+ get_rules,
11
+ )
12
+
13
+ __all__ = [
14
+ 'get_rules',
15
+ ]
@@ -0,0 +1,173 @@
1
+ import os
2
+ from pathlib import Path
3
+ from threading import Lock
4
+ import threading
5
+ from typing import Dict, List, Optional
6
+ from loguru import logger
7
+
8
+ # 尝试导入 FileMonitor
9
+ try:
10
+ from autocoder.common.file_monitor.monitor import FileMonitor, Change
11
+ except ImportError:
12
+ # 如果导入失败,提供一个空的实现
13
+ logger.warning("警告: 无法导入 FileMonitor,规则文件变更监控将不可用")
14
+ FileMonitor = None
15
+ Change = None
16
+
17
+
18
+ class AutocoderRulesManager:
19
+ """
20
+ 管理和监控 autocoderrules 目录中的规则文件。
21
+
22
+ 实现单例模式,确保全局只有一个规则管理实例。
23
+ 支持监控规则文件变化,当规则文件变化时自动重新加载。
24
+ """
25
+ _instance = None
26
+ _lock = Lock()
27
+
28
+ def __new__(cls, project_root: Optional[str] = None):
29
+ if not cls._instance:
30
+ with cls._lock:
31
+ if not cls._instance:
32
+ cls._instance = super(AutocoderRulesManager, cls).__new__(cls)
33
+ cls._instance._initialized = False
34
+ return cls._instance
35
+
36
+ def __init__(self, project_root: Optional[str] = None):
37
+ if self._initialized:
38
+ return
39
+ self._initialized = True
40
+
41
+ self._rules: Dict[str, str] = {} # 存储规则文件内容: {file_path: content}
42
+ self._rules_dir: Optional[str] = None # 当前使用的规则目录
43
+ self._file_monitor = None # FileMonitor 实例
44
+ self._monitored_dirs: List[str] = [] # 被监控的目录列表
45
+ self._project_root = project_root if project_root is not None else os.getcwd() # 项目根目录
46
+
47
+ # 加载规则
48
+ self._load_rules()
49
+ # 设置文件监控
50
+ self._setup_file_monitor()
51
+
52
+ def _load_rules(self):
53
+ """
54
+ 按优先级顺序加载规则文件。
55
+ 优先级顺序:
56
+ 1. .autocoderrules/
57
+ 2. .auto-coder/.autocoderrules/
58
+ 3. .auto-coder/autocoderrules/
59
+ """
60
+ self._rules = {}
61
+ project_root = self._project_root
62
+
63
+ # 按优先级顺序定义可能的规则目录
64
+ rules_dirs = [
65
+ os.path.join(project_root, ".autocoderrules"),
66
+ os.path.join(project_root, ".auto-coder", ".autocoderrules"),
67
+ os.path.join(project_root, ".auto-coder", "autocoderrules")
68
+ ]
69
+
70
+ # 按优先级查找第一个存在的目录
71
+ found_dir = None
72
+ for rules_dir in rules_dirs:
73
+ if os.path.isdir(rules_dir):
74
+ found_dir = rules_dir
75
+ break
76
+
77
+ if not found_dir:
78
+ logger.info("未找到规则目录")
79
+ return
80
+
81
+ self._rules_dir = found_dir
82
+ logger.info(f"使用规则目录: {self._rules_dir}")
83
+
84
+ # 加载目录中的所有 .md 文件
85
+ try:
86
+ for fname in os.listdir(self._rules_dir):
87
+ if fname.endswith(".md"):
88
+ fpath = os.path.join(self._rules_dir, fname)
89
+ try:
90
+ with open(fpath, "r", encoding="utf-8") as f:
91
+ content = f.read()
92
+ self._rules[fpath] = content
93
+ logger.info(f"已加载规则文件: {fpath}")
94
+ except Exception as e:
95
+ logger.info(f"加载规则文件 {fpath} 时出错: {e}")
96
+ continue
97
+ except Exception as e:
98
+ logger.info(f"读取规则目录 {self._rules_dir} 时出错: {e}")
99
+
100
+ def _setup_file_monitor(self):
101
+ """设置文件监控,当规则文件或目录变化时重新加载规则"""
102
+ if FileMonitor is None or not self._rules_dir:
103
+ return
104
+
105
+ try:
106
+ # 获取项目根目录
107
+ project_root = self._project_root
108
+
109
+ # 创建 FileMonitor 实例
110
+ self._file_monitor = FileMonitor(root_dir=project_root)
111
+
112
+ # 监控所有可能的规则目录
113
+ self._monitored_dirs = [
114
+ os.path.join(project_root, ".autocoderrules"),
115
+ os.path.join(project_root, ".auto-coder", ".autocoderrules"),
116
+ os.path.join(project_root, ".auto-coder", "autocoderrules")
117
+ ]
118
+
119
+ # 注册目录监控
120
+ for dir_path in self._monitored_dirs:
121
+ # 创建目录(如果不存在)
122
+ os.makedirs(dir_path, exist_ok=True)
123
+ # 注册监控
124
+ self._file_monitor.register(dir_path, self._on_rules_changed)
125
+ logger.info(f"已注册规则目录监控: {dir_path}")
126
+
127
+ # 启动监控
128
+ if not self._file_monitor.is_running():
129
+ self._file_monitor.start()
130
+ logger.info("规则文件监控已启动")
131
+
132
+ except Exception as e:
133
+ logger.warning(f"设置规则文件监控时出错: {e}")
134
+
135
+ def _on_rules_changed(self, change_type: Change, changed_path: str):
136
+ """当规则文件或目录发生变化时的回调函数"""
137
+ # 检查变化是否与规则相关
138
+ is_rule_related = False
139
+
140
+ # 检查是否是 .md 文件
141
+ if changed_path.endswith(".md"):
142
+ # 检查文件是否在监控的目录中
143
+ for dir_path in self._monitored_dirs:
144
+ if os.path.abspath(changed_path).startswith(os.path.abspath(dir_path)):
145
+ is_rule_related = True
146
+ break
147
+ else:
148
+ # 检查是否是监控的目录本身
149
+ for dir_path in self._monitored_dirs:
150
+ if os.path.abspath(changed_path) == os.path.abspath(dir_path):
151
+ is_rule_related = True
152
+ break
153
+
154
+ if is_rule_related:
155
+ logger.info(f"检测到规则相关变化 ({change_type.name}): {changed_path}")
156
+ # 重新加载规则
157
+ self._load_rules()
158
+ logger.info("已重新加载规则")
159
+
160
+ def get_rules(self) -> Dict[str, str]:
161
+ """获取所有规则文件内容"""
162
+ return self._rules.copy()
163
+
164
+
165
+ # 对外提供单例
166
+ _rules_manager = None
167
+
168
+ def get_rules(project_root: Optional[str] = None) -> Dict[str, str]:
169
+ """获取所有规则文件内容,可指定项目根目录"""
170
+ global _rules_manager
171
+ if _rules_manager is None:
172
+ _rules_manager = AutocoderRulesManager(project_root=project_root)
173
+ return _rules_manager.get_rules()
@@ -0,0 +1,54 @@
1
+ import os
2
+ import json
3
+ import uuid
4
+ from datetime import datetime
5
+
6
+ def save_formatted_log(project_root, json_text, suffix):
7
+ """
8
+ Save a JSON log as a formatted markdown file under project_root/.cache/logs.
9
+ Filename: <YYYYmmdd_HHMMSS>_<uuid>_<suffix>.md
10
+ Args:
11
+ project_root (str): The root directory of the project.
12
+ json_text (str): The JSON string to be formatted and saved.
13
+ suffix (str): The suffix for the filename.
14
+ """
15
+ # Parse JSON
16
+ try:
17
+ data = json.loads(json_text)
18
+ except Exception as e:
19
+ raise ValueError(f"Invalid JSON provided: {e}")
20
+
21
+ # Format as markdown with recursive depth
22
+ def to_markdown(obj, level=1):
23
+ lines = []
24
+ if isinstance(obj, dict):
25
+ for key, value in obj.items():
26
+ lines.append(f"{'#' * (level + 1)} {key}\n")
27
+ lines.extend(to_markdown(value, level + 1))
28
+ elif isinstance(obj, list):
29
+ for idx, item in enumerate(obj, 1):
30
+ lines.append(f"{'#' * (level + 1)} Item {idx}\n")
31
+ lines.extend(to_markdown(item, level + 1))
32
+ else:
33
+ lines.append(str(obj) + "\n")
34
+ return lines
35
+
36
+ md_lines = ["# Log Entry\n"]
37
+ md_lines.extend(to_markdown(data, 1))
38
+ md_content = "\n".join(md_lines)
39
+
40
+ # Prepare directory
41
+ logs_dir = os.path.join(project_root, ".cache", "logs")
42
+ os.makedirs(logs_dir, exist_ok=True)
43
+
44
+ # Prepare filename
45
+ now = datetime.now().strftime("%Y%m%d_%H%M%S")
46
+ unique_id = str(uuid.uuid4())
47
+ filename = f"{now}_{unique_id}_{suffix}.md"
48
+ filepath = os.path.join(logs_dir, filename)
49
+
50
+ # Save file
51
+ with open(filepath, "w", encoding="utf-8") as f:
52
+ f.write(md_content)
53
+
54
+ return filepath
@@ -1,6 +1,3 @@
1
- from autocoder.common.v2.agent.agentic_edit_conversation import AgenticConversation
2
- from enum import Enum
3
- from enum import Enum
4
1
  import json
5
2
  import os
6
3
  import time
@@ -55,7 +52,7 @@ from autocoder.common.v2.agent.agentic_edit_tools import ( # Import specific re
55
52
  AttemptCompletionToolResolver, PlanModeRespondToolResolver, UseMcpToolResolver,
56
53
  ListPackageInfoToolResolver
57
54
  )
58
-
55
+ from autocoder.common.rulefiles.autocoderrules_utils import get_rules
59
56
  from autocoder.common.v2.agent.agentic_edit_types import (AgenticEditRequest, ToolResult,
60
57
  MemoryConfig, CommandConfig, BaseTool,
61
58
  ExecuteCommandTool, ReadFileTool,
@@ -119,12 +116,7 @@ class AgenticEdit:
119
116
  self.memory_config = memory_config
120
117
  self.command_config = command_config # Note: command_config might be unused now
121
118
  self.project_type_analyzer = ProjectTypeAnalyzer(
122
- args=args, llm=self.llm)
123
-
124
- self.conversation_manager = AgenticConversation(
125
- args, self.conversation_history, conversation_name=conversation_name)
126
- # 当前不开启历史记录,所以清空
127
- self.conversation_manager.clear_history()
119
+ args=args, llm=self.llm)
128
120
 
129
121
  self.shadow_manager = ShadowManager(
130
122
  args.source_dir, args.event_file, args.ignore_clean_shadows)
@@ -687,21 +679,8 @@ class AgenticEdit:
687
679
  {% endif %}
688
680
  """
689
681
  import os
690
- extra_docs = {}
691
- rules_dir = os.path.join(self.args.source_dir,
692
- ".auto-coder", "autocoderrules")
693
- if os.path.isdir(rules_dir):
694
- for fname in os.listdir(rules_dir):
695
- if fname.endswith(".md"):
696
- fpath = os.path.join(rules_dir, fname)
697
- try:
698
- with open(fpath, "r", encoding="utf-8") as f:
699
- content = f.read()
700
- key = fpath
701
- extra_docs[key] = content
702
- except Exception:
703
- continue
704
-
682
+ extra_docs = get_rules()
683
+
705
684
  env_info = detect_env()
706
685
  shell_type = "bash"
707
686
  if shells.is_running_in_cmd():
@@ -793,12 +772,10 @@ Below are some files the user is focused on, and the content is up to date. Thes
793
772
  "role":"assistant","content":"Ok"
794
773
  })
795
774
 
796
- logger.info("Adding conversation history")
797
- conversations.extend(self.conversation_manager.get_history())
775
+ logger.info("Adding conversation history")
798
776
  conversations.append({
799
777
  "role": "user", "content": request.user_input
800
- })
801
- self.conversation_manager.add_user_message(request.user_input)
778
+ })
802
779
 
803
780
  logger.info(
804
781
  f"Initial conversation history size: {len(conversations)}")
@@ -808,7 +785,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
808
785
  while True:
809
786
  iteration_count += 1
810
787
  logger.info(f"Starting LLM interaction cycle #{iteration_count}")
811
- global_cancel.check_and_raise()
788
+ global_cancel.check_and_raise(token=self.args.event_file)
812
789
  last_message = conversations[-1]
813
790
  if last_message["role"] == "assistant":
814
791
  logger.info(f"Last message is assistant, skipping LLM interaction cycle")
@@ -838,7 +815,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
838
815
  for event in parsed_events:
839
816
  event_count += 1
840
817
  logger.info(f"Processing event #{event_count}: {type(event).__name__}")
841
- global_cancel.check_and_raise()
818
+ global_cancel.check_and_raise(token=self.args.event_file)
842
819
  if isinstance(event, (LLMOutputEvent, LLMThinkingEvent)):
843
820
  assistant_buffer += event.text
844
821
  logger.debug(f"Accumulated {len(assistant_buffer)} chars in assistant buffer")
@@ -856,9 +833,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
856
833
  conversations.append({
857
834
  "role": "assistant",
858
835
  "content": assistant_buffer + tool_xml
859
- })
860
- self.conversation_manager.add_assistant_message(
861
- assistant_buffer + tool_xml)
836
+ })
862
837
  assistant_buffer = "" # Reset buffer after tool call
863
838
 
864
839
  yield event # Yield the ToolCallEvent for display
@@ -941,7 +916,6 @@ Below are some files the user is focused on, and the content is up to date. Thes
941
916
  "role": "user", # Simulating the user providing the tool result
942
917
  "content": error_xml
943
918
  })
944
- self.conversation_manager.add_user_message(error_xml)
945
919
  logger.info(
946
920
  f"Added tool result to conversations for tool {type(tool_obj).__name__}")
947
921
  logger.info(f"Breaking LLM cycle after executing tool: {tool_name}")
@@ -968,12 +942,9 @@ Below are some files the user is focused on, and the content is up to date. Thes
968
942
  logger.info("Adding new assistant message")
969
943
  conversations.append(
970
944
  {"role": "assistant", "content": assistant_buffer})
971
- self.conversation_manager.add_assistant_message(
972
- assistant_buffer)
973
945
  elif last_message["role"] == "assistant":
974
946
  logger.info("Appending to existing assistant message")
975
947
  last_message["content"] += assistant_buffer
976
- self.conversation_manager.append_to_last_message(assistant_buffer)
977
948
  # If the loop ends without AttemptCompletion, it means the LLM finished talking
978
949
  # without signaling completion. We might just stop or yield a final message.
979
950
  # Let's assume it stops here.
@@ -1062,7 +1033,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
1062
1033
  return None
1063
1034
 
1064
1035
  for content_chunk, metadata in generator:
1065
- global_cancel.check_and_raise()
1036
+ global_cancel.check_and_raise(token=self.args.event_file)
1066
1037
  meta_holder.meta = metadata
1067
1038
  if not content_chunk:
1068
1039
  continue
@@ -6,7 +6,7 @@ from loguru import logger
6
6
  import typing
7
7
  from autocoder.common import AutoCoderArgs
8
8
 
9
- from autocoder.ignorefiles.ignore_file_utils import should_ignore
9
+ from autocoder.common.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
@@ -1,4 +1,3 @@
1
-
2
1
  import os
3
2
  import re
4
3
  import glob
@@ -9,7 +8,7 @@ from loguru import logger
9
8
  from autocoder.common import AutoCoderArgs
10
9
  import typing
11
10
 
12
- from autocoder.ignorefiles.ignore_file_utils import should_ignore
11
+ from autocoder.common.ignorefiles.ignore_file_utils import should_ignore
13
12
 
14
13
  if typing.TYPE_CHECKING:
15
14
  from autocoder.common.v2.agent.agentic_edit import AgenticEdit
@@ -33,14 +32,13 @@ class SearchFilesToolResolver(BaseToolResolver):
33
32
  if not absolute_search_path.startswith(absolute_source_dir):
34
33
  return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
35
34
 
36
- # Determine search base directory: prefer shadow if exists
37
- search_base_path = absolute_search_path
35
+ # Check if shadow directory exists
38
36
  shadow_exists = False
37
+ shadow_dir_path = None
39
38
  if self.shadow_manager:
40
39
  try:
41
40
  shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_search_path)
42
41
  if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
43
- search_base_path = shadow_dir_path
44
42
  shadow_exists = True
45
43
  except Exception as e:
46
44
  logger.warning(f"Error checking shadow path for {absolute_search_path}: {e}")
@@ -50,51 +48,83 @@ class SearchFilesToolResolver(BaseToolResolver):
50
48
  return ToolResult(success=False, message=f"Error: Search path not found: {search_path_str}")
51
49
  if os.path.exists(absolute_search_path) and not os.path.isdir(absolute_search_path):
52
50
  return ToolResult(success=False, message=f"Error: Search path is not a directory: {search_path_str}")
53
- if shadow_exists and not os.path.isdir(search_base_path):
54
- return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {search_base_path}")
51
+ if shadow_exists and not os.path.isdir(shadow_dir_path):
52
+ return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {shadow_dir_path}")
55
53
 
56
- results = []
57
54
  try:
58
55
  compiled_regex = re.compile(regex_pattern)
59
- search_glob_pattern = os.path.join(search_base_path, "**", file_pattern)
60
-
61
- logger.info(f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{search_base_path}' (shadow: {shadow_exists}) with ignore rules applied.")
62
-
63
- for filepath in glob.glob(search_glob_pattern, recursive=True):
64
- abs_path = os.path.abspath(filepath)
65
- if should_ignore(abs_path):
66
- continue
56
+
57
+ # Helper function to search in a directory
58
+ def search_in_dir(base_dir, is_shadow=False):
59
+ search_results = []
60
+ search_glob_pattern = os.path.join(base_dir, "**", file_pattern)
61
+
62
+ logger.info(f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{base_dir}' (shadow: {is_shadow}) with ignore rules applied.")
63
+
64
+ for filepath in glob.glob(search_glob_pattern, recursive=True):
65
+ abs_path = os.path.abspath(filepath)
66
+ if should_ignore(abs_path):
67
+ continue
67
68
 
68
- if os.path.isfile(filepath):
69
- try:
70
- with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
71
- lines = f.readlines()
72
- for i, line in enumerate(lines):
73
- if compiled_regex.search(line):
74
- context_start = max(0, i - 2)
75
- context_end = min(len(lines), i + 3)
76
- context = "".join([f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
77
- if shadow_exists and self.shadow_manager:
78
- try:
79
- abs_project_path = self.shadow_manager.from_shadow_path(filepath)
80
- relative_path = os.path.relpath(abs_project_path, source_dir)
81
- except Exception:
69
+ if os.path.isfile(filepath):
70
+ try:
71
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
72
+ lines = f.readlines()
73
+ for i, line in enumerate(lines):
74
+ if compiled_regex.search(line):
75
+ context_start = max(0, i - 2)
76
+ context_end = min(len(lines), i + 3)
77
+ context = "".join([f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
78
+
79
+ if is_shadow and self.shadow_manager:
80
+ try:
81
+ abs_project_path = self.shadow_manager.from_shadow_path(filepath)
82
+ relative_path = os.path.relpath(abs_project_path, source_dir)
83
+ except Exception:
84
+ relative_path = os.path.relpath(filepath, source_dir)
85
+ else:
82
86
  relative_path = os.path.relpath(filepath, source_dir)
83
- else:
84
- relative_path = os.path.relpath(filepath, source_dir)
85
- results.append({
86
- "path": relative_path,
87
- "line_number": i + 1,
88
- "match_line": line.strip(),
89
- "context": context.strip()
90
- })
91
- except Exception as e:
92
- logger.warning(f"Could not read or process file {filepath}: {e}")
93
- continue
87
+
88
+ search_results.append({
89
+ "path": relative_path,
90
+ "line_number": i + 1,
91
+ "match_line": line.strip(),
92
+ "context": context.strip()
93
+ })
94
+ except Exception as e:
95
+ logger.warning(f"Could not read or process file {filepath}: {e}")
96
+ continue
97
+
98
+ return search_results
99
+
100
+ # Search in both directories and merge results
101
+ shadow_results = []
102
+ source_results = []
103
+
104
+ if shadow_exists:
105
+ shadow_results = search_in_dir(shadow_dir_path, is_shadow=True)
106
+
107
+ if os.path.exists(absolute_search_path) and os.path.isdir(absolute_search_path):
108
+ source_results = search_in_dir(absolute_search_path, is_shadow=False)
109
+
110
+ # Merge results, prioritizing shadow results
111
+ # Create a dictionary for quick lookup
112
+ results_dict = {}
113
+ for result in source_results:
114
+ key = (result["path"], result["line_number"])
115
+ results_dict[key] = result
116
+
117
+ # Override with shadow results
118
+ for result in shadow_results:
119
+ key = (result["path"], result["line_number"])
120
+ results_dict[key] = result
121
+
122
+ # Convert back to list
123
+ merged_results = list(results_dict.values())
94
124
 
95
- message = f"Search completed. Found {len(results)} matches."
125
+ message = f"Search completed. Found {len(merged_results)} matches."
96
126
  logger.info(message)
97
- return ToolResult(success=True, message=message, content=results)
127
+ return ToolResult(success=True, message=message, content=merged_results)
98
128
 
99
129
  except re.error as e:
100
130
  logger.error(f"Invalid regex pattern '{regex_pattern}': {e}")