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.
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/METADATA +1 -1
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/RECORD +60 -45
- autocoder/agent/agentic_filter.py +1 -1
- autocoder/auto_coder.py +8 -0
- autocoder/auto_coder_rag.py +37 -1
- autocoder/auto_coder_runner.py +58 -77
- autocoder/chat/conf_command.py +270 -0
- autocoder/chat/models_command.py +485 -0
- autocoder/chat_auto_coder.py +29 -24
- autocoder/chat_auto_coder_lang.py +26 -2
- autocoder/commands/auto_command.py +60 -132
- autocoder/commands/auto_web.py +1 -1
- autocoder/commands/tools.py +1 -1
- autocoder/common/__init__.py +3 -1
- autocoder/common/command_completer.py +58 -12
- autocoder/common/command_completer_v2.py +576 -0
- autocoder/common/conversations/__init__.py +52 -0
- autocoder/common/conversations/compatibility.py +303 -0
- autocoder/common/conversations/conversation_manager.py +502 -0
- autocoder/common/conversations/example.py +152 -0
- autocoder/common/file_monitor/__init__.py +5 -0
- autocoder/common/file_monitor/monitor.py +383 -0
- autocoder/common/global_cancel.py +53 -16
- autocoder/common/ignorefiles/__init__.py +4 -0
- autocoder/common/ignorefiles/ignore_file_utils.py +103 -0
- autocoder/common/ignorefiles/test_ignore_file_utils.py +91 -0
- autocoder/common/rulefiles/__init__.py +15 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +173 -0
- autocoder/common/save_formatted_log.py +54 -0
- autocoder/common/v2/agent/agentic_edit.py +10 -39
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +73 -43
- autocoder/common/v2/code_agentic_editblock_manager.py +9 -9
- autocoder/common/v2/code_diff_manager.py +2 -2
- autocoder/common/v2/code_editblock_manager.py +31 -18
- autocoder/common/v2/code_strict_diff_manager.py +3 -2
- autocoder/dispacher/actions/action.py +6 -6
- autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
- autocoder/events/event_manager_singleton.py +1 -1
- autocoder/index/index.py +3 -3
- autocoder/models.py +22 -9
- autocoder/rag/api_server.py +14 -2
- autocoder/rag/cache/local_byzer_storage_cache.py +1 -1
- autocoder/rag/cache/local_duckdb_storage_cache.py +8 -0
- autocoder/rag/cache/simple_cache.py +63 -33
- autocoder/rag/loaders/docx_loader.py +1 -1
- autocoder/rag/loaders/filter_utils.py +133 -76
- autocoder/rag/loaders/image_loader.py +15 -3
- autocoder/rag/loaders/pdf_loader.py +2 -2
- autocoder/rag/long_context_rag.py +11 -0
- autocoder/rag/qa_conversation_strategy.py +5 -31
- autocoder/rag/utils.py +21 -2
- autocoder/utils/_markitdown.py +66 -25
- autocoder/utils/auto_coder_utils/chat_stream_out.py +4 -4
- autocoder/utils/thread_utils.py +9 -27
- autocoder/version.py +1 -1
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/entry_points.txt +0 -0
- {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,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
|
-
|
|
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
|
-
#
|
|
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(
|
|
54
|
-
return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
125
|
+
message = f"Search completed. Found {len(merged_results)} matches."
|
|
96
126
|
logger.info(message)
|
|
97
|
-
return ToolResult(success=True, message=message, content=
|
|
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}")
|