auto-coder 0.1.362__py3-none-any.whl → 0.1.364__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.362.dist-info → auto_coder-0.1.364.dist-info}/METADATA +2 -2
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/RECORD +65 -22
- autocoder/agent/base_agentic/__init__.py +0 -0
- autocoder/agent/base_agentic/agent_hub.py +169 -0
- autocoder/agent/base_agentic/agentic_lang.py +112 -0
- autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
- autocoder/agent/base_agentic/base_agent.py +1582 -0
- autocoder/agent/base_agentic/default_tools.py +683 -0
- autocoder/agent/base_agentic/test_base_agent.py +82 -0
- autocoder/agent/base_agentic/tool_registry.py +425 -0
- autocoder/agent/base_agentic/tools/__init__.py +12 -0
- autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
- autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
- autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
- autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
- autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
- autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
- autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
- autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
- autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
- autocoder/agent/base_agentic/types.py +189 -0
- autocoder/agent/base_agentic/utils.py +100 -0
- autocoder/auto_coder_runner.py +6 -4
- autocoder/chat/conf_command.py +11 -10
- autocoder/common/__init__.py +2 -0
- autocoder/common/file_checkpoint/__init__.py +21 -0
- autocoder/common/file_checkpoint/backup.py +264 -0
- autocoder/common/file_checkpoint/examples.py +217 -0
- autocoder/common/file_checkpoint/manager.py +404 -0
- autocoder/common/file_checkpoint/models.py +156 -0
- autocoder/common/file_checkpoint/store.py +383 -0
- autocoder/common/file_checkpoint/test_backup.py +242 -0
- autocoder/common/file_checkpoint/test_manager.py +570 -0
- autocoder/common/file_checkpoint/test_models.py +360 -0
- autocoder/common/file_checkpoint/test_store.py +327 -0
- autocoder/common/file_checkpoint/test_utils.py +297 -0
- autocoder/common/file_checkpoint/utils.py +119 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +138 -55
- autocoder/common/save_formatted_log.py +76 -5
- autocoder/common/v2/agent/agentic_edit.py +339 -216
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +100 -5
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +160 -10
- autocoder/common/v2/agent/agentic_edit_types.py +1 -2
- autocoder/common/v2/agent/agentic_tool_display.py +2 -3
- autocoder/compilers/normal_compiler.py +64 -0
- autocoder/events/event_manager_singleton.py +133 -4
- autocoder/linters/normal_linter.py +373 -0
- autocoder/linters/python_linter.py +4 -2
- autocoder/rag/long_context_rag.py +424 -397
- autocoder/rag/test_doc_filter.py +393 -0
- autocoder/rag/test_long_context_rag.py +473 -0
- autocoder/rag/test_token_limiter.py +342 -0
- autocoder/shadows/shadow_manager.py +1 -3
- autocoder/version.py +1 -1
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import shutil
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
|
|
9
|
+
from autocoder.common.file_checkpoint.utils import (
|
|
10
|
+
apply_shadow_changes, undo_last_changes, get_change_history
|
|
11
|
+
)
|
|
12
|
+
from autocoder.common.file_checkpoint.models import FileChange
|
|
13
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def temp_test_dir():
|
|
17
|
+
"""提供一个临时的测试目录"""
|
|
18
|
+
temp_dir = tempfile.mkdtemp()
|
|
19
|
+
yield temp_dir
|
|
20
|
+
shutil.rmtree(temp_dir)
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def sample_file(temp_test_dir):
|
|
24
|
+
"""创建一个用于测试的样例文件"""
|
|
25
|
+
file_path = os.path.join(temp_test_dir, "sample.txt")
|
|
26
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
27
|
+
f.write("这是一个测试文件的内容")
|
|
28
|
+
return file_path
|
|
29
|
+
|
|
30
|
+
class TestApplyShadowChanges:
|
|
31
|
+
"""apply_shadow_changes函数的单元测试"""
|
|
32
|
+
|
|
33
|
+
def test_apply_new_file(self, temp_test_dir):
|
|
34
|
+
"""测试应用新文件变更"""
|
|
35
|
+
# 准备影子系统变更
|
|
36
|
+
changes = {
|
|
37
|
+
"new_file.txt": {
|
|
38
|
+
"content": "这是新文件内容"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# 应用变更
|
|
43
|
+
result = apply_shadow_changes(temp_test_dir, changes)
|
|
44
|
+
|
|
45
|
+
# 检查结果
|
|
46
|
+
assert result['success'] is True
|
|
47
|
+
assert len(result['change_ids']) == 1
|
|
48
|
+
assert not result['errors']
|
|
49
|
+
assert "new_file.txt" in result['changed_files']
|
|
50
|
+
|
|
51
|
+
# 检查文件是否被创建
|
|
52
|
+
new_file_path = os.path.join(temp_test_dir, "new_file.txt")
|
|
53
|
+
assert os.path.exists(new_file_path)
|
|
54
|
+
|
|
55
|
+
# 检查文件内容
|
|
56
|
+
with open(new_file_path, 'r', encoding='utf-8') as f:
|
|
57
|
+
content = f.read()
|
|
58
|
+
assert content == "这是新文件内容"
|
|
59
|
+
|
|
60
|
+
def test_apply_modify_file(self, temp_test_dir, sample_file):
|
|
61
|
+
"""测试应用修改文件变更"""
|
|
62
|
+
# 获取相对路径
|
|
63
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
64
|
+
|
|
65
|
+
# 准备影子系统变更
|
|
66
|
+
changes = {
|
|
67
|
+
rel_path: {
|
|
68
|
+
"content": "修改后的内容"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 应用变更
|
|
73
|
+
result = apply_shadow_changes(temp_test_dir, changes)
|
|
74
|
+
|
|
75
|
+
# 检查结果
|
|
76
|
+
assert result['success'] is True
|
|
77
|
+
assert len(result['change_ids']) == 1
|
|
78
|
+
assert not result['errors']
|
|
79
|
+
assert rel_path in result['changed_files']
|
|
80
|
+
|
|
81
|
+
# 检查文件内容
|
|
82
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
83
|
+
content = f.read()
|
|
84
|
+
assert content == "修改后的内容"
|
|
85
|
+
|
|
86
|
+
def test_apply_delete_file(self, temp_test_dir, sample_file):
|
|
87
|
+
"""测试应用删除文件变更"""
|
|
88
|
+
# 获取相对路径
|
|
89
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
90
|
+
|
|
91
|
+
# 准备影子系统变更
|
|
92
|
+
changes = {
|
|
93
|
+
rel_path: {
|
|
94
|
+
"content": "",
|
|
95
|
+
"is_deletion": True
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# 应用变更
|
|
100
|
+
result = apply_shadow_changes(temp_test_dir, changes)
|
|
101
|
+
|
|
102
|
+
# 检查结果
|
|
103
|
+
assert result['success'] is True
|
|
104
|
+
assert len(result['change_ids']) == 1
|
|
105
|
+
assert not result['errors']
|
|
106
|
+
assert rel_path in result['changed_files']
|
|
107
|
+
|
|
108
|
+
# 检查文件是否被删除
|
|
109
|
+
assert not os.path.exists(sample_file)
|
|
110
|
+
|
|
111
|
+
def test_apply_multiple_changes(self, temp_test_dir, sample_file):
|
|
112
|
+
"""测试应用多个文件变更"""
|
|
113
|
+
# 获取相对路径
|
|
114
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
115
|
+
|
|
116
|
+
# 准备影子系统变更
|
|
117
|
+
changes = {
|
|
118
|
+
rel_path: {
|
|
119
|
+
"content": "修改后的内容"
|
|
120
|
+
},
|
|
121
|
+
"new_file.txt": {
|
|
122
|
+
"content": "新文件内容"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# 应用变更
|
|
127
|
+
result = apply_shadow_changes(temp_test_dir, changes)
|
|
128
|
+
|
|
129
|
+
# 检查结果
|
|
130
|
+
assert result['success'] is True
|
|
131
|
+
assert len(result['change_ids']) == 2
|
|
132
|
+
assert not result['errors']
|
|
133
|
+
assert rel_path in result['changed_files']
|
|
134
|
+
assert "new_file.txt" in result['changed_files']
|
|
135
|
+
|
|
136
|
+
# 检查文件内容
|
|
137
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
138
|
+
content = f.read()
|
|
139
|
+
assert content == "修改后的内容"
|
|
140
|
+
|
|
141
|
+
new_file_path = os.path.join(temp_test_dir, "new_file.txt")
|
|
142
|
+
assert os.path.exists(new_file_path)
|
|
143
|
+
with open(new_file_path, 'r', encoding='utf-8') as f:
|
|
144
|
+
content = f.read()
|
|
145
|
+
assert content == "新文件内容"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestUndoLastChanges:
|
|
149
|
+
"""undo_last_changes函数的单元测试"""
|
|
150
|
+
|
|
151
|
+
def test_undo_single_change(self, temp_test_dir, sample_file):
|
|
152
|
+
"""测试撤销单个变更"""
|
|
153
|
+
# 获取相对路径
|
|
154
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
155
|
+
|
|
156
|
+
# 先应用变更
|
|
157
|
+
changes = {
|
|
158
|
+
rel_path: {
|
|
159
|
+
"content": "修改后的内容"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
apply_result = apply_shadow_changes(temp_test_dir, changes)
|
|
163
|
+
assert apply_result['success'] is True
|
|
164
|
+
|
|
165
|
+
# 检查文件已被修改
|
|
166
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
167
|
+
content = f.read()
|
|
168
|
+
assert content == "修改后的内容"
|
|
169
|
+
|
|
170
|
+
# 撤销变更
|
|
171
|
+
undo_result = undo_last_changes(temp_test_dir)
|
|
172
|
+
|
|
173
|
+
# 检查撤销结果
|
|
174
|
+
assert undo_result['success'] is True
|
|
175
|
+
assert len(undo_result['restored_files']) == 1
|
|
176
|
+
assert rel_path in undo_result['restored_files']
|
|
177
|
+
|
|
178
|
+
# 检查文件内容是否被恢复
|
|
179
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
180
|
+
content = f.read()
|
|
181
|
+
assert content == "这是一个测试文件的内容"
|
|
182
|
+
|
|
183
|
+
def test_undo_multiple_steps(self, temp_test_dir, sample_file):
|
|
184
|
+
"""测试撤销多个步骤"""
|
|
185
|
+
# 获取相对路径
|
|
186
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
187
|
+
|
|
188
|
+
# 应用第一个变更
|
|
189
|
+
changes1 = {
|
|
190
|
+
rel_path: {
|
|
191
|
+
"content": "第一次修改"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
apply_result1 = apply_shadow_changes(temp_test_dir, changes1)
|
|
195
|
+
assert apply_result1['success'] is True
|
|
196
|
+
|
|
197
|
+
# 应用第二个变更
|
|
198
|
+
changes2 = {
|
|
199
|
+
rel_path: {
|
|
200
|
+
"content": "第二次修改"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
apply_result2 = apply_shadow_changes(temp_test_dir, changes2)
|
|
204
|
+
assert apply_result2['success'] is True
|
|
205
|
+
|
|
206
|
+
# 检查文件内容
|
|
207
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
208
|
+
content = f.read()
|
|
209
|
+
assert content == "第二次修改"
|
|
210
|
+
|
|
211
|
+
# 撤销两个步骤
|
|
212
|
+
undo_result = undo_last_changes(temp_test_dir, steps=2)
|
|
213
|
+
|
|
214
|
+
# 检查撤销结果
|
|
215
|
+
assert undo_result['success'] is True
|
|
216
|
+
assert len(undo_result['restored_files']) == 2
|
|
217
|
+
|
|
218
|
+
# 检查文件内容是否被恢复到原始状态
|
|
219
|
+
with open(sample_file, 'r', encoding='utf-8') as f:
|
|
220
|
+
content = f.read()
|
|
221
|
+
assert content == "这是一个测试文件的内容"
|
|
222
|
+
|
|
223
|
+
def test_undo_with_error(self, temp_test_dir):
|
|
224
|
+
"""测试撤销出错的情况"""
|
|
225
|
+
# 模拟FileChangeManager的undo_last_change方法返回错误
|
|
226
|
+
with patch.object(FileChangeManager, 'undo_last_change') as mock_undo:
|
|
227
|
+
from autocoder.common.file_checkpoint.models import UndoResult
|
|
228
|
+
error_result = UndoResult(success=False)
|
|
229
|
+
error_result.add_error("test.txt", "文件不存在")
|
|
230
|
+
mock_undo.return_value = error_result
|
|
231
|
+
|
|
232
|
+
# 尝试撤销变更
|
|
233
|
+
undo_result = undo_last_changes(temp_test_dir)
|
|
234
|
+
|
|
235
|
+
# 检查撤销结果
|
|
236
|
+
assert undo_result['success'] is False
|
|
237
|
+
assert "test.txt" in undo_result['errors']
|
|
238
|
+
assert undo_result['errors']["test.txt"] == "文件不存在"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestGetChangeHistory:
|
|
242
|
+
"""get_change_history函数的单元测试"""
|
|
243
|
+
|
|
244
|
+
def test_get_history(self, temp_test_dir, sample_file):
|
|
245
|
+
"""测试获取变更历史"""
|
|
246
|
+
# 获取相对路径
|
|
247
|
+
rel_path = os.path.relpath(sample_file, temp_test_dir)
|
|
248
|
+
|
|
249
|
+
# 应用两个变更
|
|
250
|
+
changes1 = {
|
|
251
|
+
rel_path: {
|
|
252
|
+
"content": "第一次修改"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
changes2 = {
|
|
256
|
+
"new_file.txt": {
|
|
257
|
+
"content": "新文件内容"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
apply_shadow_changes(temp_test_dir, changes1)
|
|
262
|
+
apply_shadow_changes(temp_test_dir, changes2)
|
|
263
|
+
|
|
264
|
+
# 获取变更历史
|
|
265
|
+
history = get_change_history(temp_test_dir)
|
|
266
|
+
|
|
267
|
+
# 检查历史记录
|
|
268
|
+
assert len(history) >= 2
|
|
269
|
+
|
|
270
|
+
# 检查第一条记录(最新的,是new_file.txt)
|
|
271
|
+
assert history[0]['file_path'] == "new_file.txt"
|
|
272
|
+
assert history[0]['is_new'] is True
|
|
273
|
+
|
|
274
|
+
# 检查第二条记录(较早的,是修改的sample_file)
|
|
275
|
+
assert history[1]['file_path'] == rel_path
|
|
276
|
+
assert history[1]['is_new'] is False
|
|
277
|
+
|
|
278
|
+
def test_get_history_with_limit(self, temp_test_dir):
|
|
279
|
+
"""测试限制历史记录数量"""
|
|
280
|
+
# 使用模拟的FileChangeManager
|
|
281
|
+
with patch.object(FileChangeManager, 'get_change_history') as mock_get_history:
|
|
282
|
+
# 创建模拟的返回值
|
|
283
|
+
from autocoder.common.file_checkpoint.models import ChangeRecord
|
|
284
|
+
mock_records = [
|
|
285
|
+
ChangeRecord.create(file_path=f"file{i}.txt", backup_id=f"backup{i}")
|
|
286
|
+
for i in range(5)
|
|
287
|
+
]
|
|
288
|
+
mock_get_history.return_value = mock_records
|
|
289
|
+
|
|
290
|
+
# 请求限制为3条记录
|
|
291
|
+
history = get_change_history(temp_test_dir, limit=3)
|
|
292
|
+
|
|
293
|
+
# 检查参数调用
|
|
294
|
+
mock_get_history.assert_called_once_with(3)
|
|
295
|
+
|
|
296
|
+
# 检查结果
|
|
297
|
+
assert len(history) == 5 # 模拟返回了5条记录
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件变更管理模块的工具函数
|
|
3
|
+
|
|
4
|
+
提供一些常用的工具函数,简化文件变更管理模块的使用。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
|
|
11
|
+
from autocoder.common.file_checkpoint.models import FileChange
|
|
12
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_shadow_changes(source_dir: str, changes: Dict[str, Any]) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
将影子系统的变更应用到用户项目中
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
source_dir: 用户项目的根目录
|
|
21
|
+
changes: 影子系统的变更字典,格式为 {file_path: {content: str}}
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dict: 应用结果,包含成功和失败的信息
|
|
25
|
+
"""
|
|
26
|
+
# 创建文件变更管理器
|
|
27
|
+
manager = FileChangeManager(source_dir)
|
|
28
|
+
|
|
29
|
+
# 将影子系统的变更转换为 FileChange 对象
|
|
30
|
+
file_changes = {}
|
|
31
|
+
for file_path, change in changes.items():
|
|
32
|
+
# 确定文件是否是新文件
|
|
33
|
+
abs_file_path = os.path.join(source_dir, file_path)
|
|
34
|
+
is_new = not os.path.exists(abs_file_path)
|
|
35
|
+
|
|
36
|
+
file_changes[file_path] = FileChange(
|
|
37
|
+
file_path=file_path,
|
|
38
|
+
content=change.get('content', ''),
|
|
39
|
+
is_new=is_new,
|
|
40
|
+
is_deletion=change.get('is_deletion', False)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 应用变更
|
|
44
|
+
result = manager.apply_changes(file_changes)
|
|
45
|
+
|
|
46
|
+
# 返回结果
|
|
47
|
+
return {
|
|
48
|
+
'success': result.success,
|
|
49
|
+
'change_ids': result.change_ids,
|
|
50
|
+
'errors': result.errors,
|
|
51
|
+
'changed_files': list(file_changes.keys())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def undo_last_changes(source_dir: str, steps: int = 1) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
撤销最近的变更
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
source_dir: 用户项目的根目录
|
|
61
|
+
steps: 要撤销的步骤数
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict: 撤销结果,包含成功和失败的信息
|
|
65
|
+
"""
|
|
66
|
+
# 创建文件变更管理器
|
|
67
|
+
manager = FileChangeManager(source_dir)
|
|
68
|
+
|
|
69
|
+
# 执行多步撤销
|
|
70
|
+
all_restored_files = []
|
|
71
|
+
all_errors = {}
|
|
72
|
+
success = True
|
|
73
|
+
|
|
74
|
+
for _ in range(steps):
|
|
75
|
+
result = manager.undo_last_change()
|
|
76
|
+
if not result.success:
|
|
77
|
+
success = False
|
|
78
|
+
all_errors.update(result.errors)
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
all_restored_files.extend(result.restored_files)
|
|
82
|
+
|
|
83
|
+
# 返回结果
|
|
84
|
+
return {
|
|
85
|
+
'success': success,
|
|
86
|
+
'restored_files': all_restored_files,
|
|
87
|
+
'errors': all_errors
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_change_history(source_dir: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
获取变更历史记录
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
source_dir: 用户项目的根目录
|
|
97
|
+
limit: 返回的历史记录数量限制
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List[Dict]: 变更记录列表
|
|
101
|
+
"""
|
|
102
|
+
# 创建文件变更管理器
|
|
103
|
+
manager = FileChangeManager(source_dir)
|
|
104
|
+
|
|
105
|
+
# 获取变更历史
|
|
106
|
+
changes = manager.get_change_history(limit)
|
|
107
|
+
|
|
108
|
+
# 转换为字典列表
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
'change_id': change.change_id,
|
|
112
|
+
'timestamp': change.timestamp,
|
|
113
|
+
'file_path': change.file_path,
|
|
114
|
+
'is_new': change.is_new,
|
|
115
|
+
'is_deletion': change.is_deletion,
|
|
116
|
+
'group_id': change.group_id
|
|
117
|
+
}
|
|
118
|
+
for change in changes
|
|
119
|
+
]
|
|
@@ -10,6 +10,7 @@ import byzerllm # Added import
|
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
11
11
|
from typing import List, Dict, Optional, Any # Added Any
|
|
12
12
|
from autocoder.common import AutoCoderArgs
|
|
13
|
+
import concurrent.futures # 添加线程池导入
|
|
13
14
|
|
|
14
15
|
# 尝试导入 FileMonitor
|
|
15
16
|
try:
|
|
@@ -25,7 +26,7 @@ class RuleFile(BaseModel):
|
|
|
25
26
|
"""规则文件的Pydantic模型"""
|
|
26
27
|
description: str = Field(default="", description="规则的描述")
|
|
27
28
|
globs: List[str] = Field(default_factory=list, description="文件匹配模式列表")
|
|
28
|
-
always_apply: bool = Field(default=False,
|
|
29
|
+
always_apply: bool = Field(default=False, description="是否总是应用规则")
|
|
29
30
|
content: str = Field(default="", description="规则文件的正文内容")
|
|
30
31
|
file_path: str = Field(default="", description="规则文件的路径")
|
|
31
32
|
|
|
@@ -206,15 +207,14 @@ class AutocoderRulesManager:
|
|
|
206
207
|
except Exception as e:
|
|
207
208
|
logger.warning(f"解析规则文件YAML头部时出错: {e}")
|
|
208
209
|
|
|
209
|
-
# 创建并返回Pydantic模型
|
|
210
|
+
# 创建并返回Pydantic模型
|
|
210
211
|
rule = RuleFile(
|
|
211
212
|
description=metadata.get('description', ''),
|
|
212
213
|
globs=metadata.get('globs', []),
|
|
213
214
|
always_apply=metadata.get('alwaysApply', False),
|
|
214
215
|
content=markdown_content.strip(),
|
|
215
216
|
file_path=file_path
|
|
216
|
-
)
|
|
217
|
-
|
|
217
|
+
)
|
|
218
218
|
return rule
|
|
219
219
|
|
|
220
220
|
except Exception as e:
|
|
@@ -233,6 +233,25 @@ class AutocoderRulesManager:
|
|
|
233
233
|
parsed_rules.append(parsed_rule)
|
|
234
234
|
return parsed_rules
|
|
235
235
|
|
|
236
|
+
@classmethod
|
|
237
|
+
def reset_instance(cls):
|
|
238
|
+
"""
|
|
239
|
+
重置单例实例。
|
|
240
|
+
如果当前实例正在运行,则先取消注册监控的目录,然后重置实例。
|
|
241
|
+
"""
|
|
242
|
+
with cls._lock:
|
|
243
|
+
if cls._instance is not None:
|
|
244
|
+
# 取消注册监控的目录
|
|
245
|
+
if cls._instance._file_monitor:
|
|
246
|
+
for dir_path in cls._instance._monitored_dirs:
|
|
247
|
+
try:
|
|
248
|
+
cls._instance._file_monitor.unregister(dir_path)
|
|
249
|
+
logger.info(f"已取消注册目录监控: {dir_path}")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning(f"取消注册目录 {dir_path} 时出错: {e}")
|
|
252
|
+
cls._instance = None
|
|
253
|
+
logger.info("AutocoderRulesManager单例已被重置")
|
|
254
|
+
|
|
236
255
|
|
|
237
256
|
# 对外提供单例
|
|
238
257
|
_rules_manager = None
|
|
@@ -258,6 +277,11 @@ def parse_rule_file(file_path: str, project_root: Optional[str] = None) -> RuleF
|
|
|
258
277
|
_rules_manager = AutocoderRulesManager(project_root=project_root)
|
|
259
278
|
return _rules_manager.parse_rule_file(file_path)
|
|
260
279
|
|
|
280
|
+
def reset_rules_manager():
|
|
281
|
+
"""重置AutocoderRulesManager单例实例"""
|
|
282
|
+
AutocoderRulesManager.reset_instance()
|
|
283
|
+
global _rules_manager
|
|
284
|
+
_rules_manager = None
|
|
261
285
|
|
|
262
286
|
# 添加用于返回类型的Pydantic模型
|
|
263
287
|
class RuleRelevance(BaseModel):
|
|
@@ -313,10 +337,49 @@ class RuleSelector:
|
|
|
313
337
|
"rule": rule,
|
|
314
338
|
"context": context
|
|
315
339
|
}
|
|
340
|
+
|
|
341
|
+
def _evaluate_rule(self, rule: RuleFile, context: str) -> tuple[RuleFile, bool, Optional[str]]:
|
|
342
|
+
"""
|
|
343
|
+
评估单个规则是否适用于当前上下文。
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
rule: 要评估的规则
|
|
347
|
+
context: 上下文信息
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
tuple: (规则, 是否选中, 理由)
|
|
351
|
+
"""
|
|
352
|
+
# 如果规则设置为总是应用,直接返回选中
|
|
353
|
+
if rule.always_apply:
|
|
354
|
+
return (rule, True, "规则设置为总是应用")
|
|
355
|
+
|
|
356
|
+
# 如果没有LLM,无法评估non-always规则
|
|
357
|
+
if self.llm is None:
|
|
358
|
+
return (rule, False, "未提供LLM,无法评估non-always规则")
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
prompt = self._build_selection_prompt.prompt(rule=rule, context=context)
|
|
362
|
+
logger.debug(f"为规则 '{os.path.basename(rule.file_path)}' 生成的判断 Prompt (片段): {prompt[:200]}...")
|
|
363
|
+
|
|
364
|
+
result = None
|
|
365
|
+
try:
|
|
366
|
+
# 使用with_return_type方法获取结构化结果
|
|
367
|
+
result = self._build_selection_prompt.with_llm(self.llm).with_return_type(RuleRelevance).run(rule=rule, context=context)
|
|
368
|
+
if result and result.is_relevant:
|
|
369
|
+
return (rule, True, result.reason)
|
|
370
|
+
else:
|
|
371
|
+
return (rule, False, result.reason if result else "未提供理由")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.warning(f"LLM 未能为规则 '{os.path.basename(rule.file_path)}' 提供有效响应: {e}")
|
|
374
|
+
return (rule, False, f"LLM评估出错: {str(e)}")
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logger.error(f"评估规则 '{os.path.basename(rule.file_path)}' 时出错: {e}", exc_info=True)
|
|
378
|
+
return (rule, False, f"评估过程出错: {str(e)}")
|
|
316
379
|
|
|
317
|
-
def select_rules(self, context: str
|
|
380
|
+
def select_rules(self, context: str) -> List[RuleFile]:
|
|
318
381
|
"""
|
|
319
|
-
|
|
382
|
+
选择适用于当前上下文的规则。使用线程池并发评估规则。
|
|
320
383
|
|
|
321
384
|
Args:
|
|
322
385
|
context: 可选的字典,包含用于规则选择的上下文信息 (例如,用户指令、目标文件等)。
|
|
@@ -324,56 +387,47 @@ class RuleSelector:
|
|
|
324
387
|
Returns:
|
|
325
388
|
List[RuleFile]: 选定的规则列表。
|
|
326
389
|
"""
|
|
390
|
+
rules = get_parsed_rules()
|
|
327
391
|
selected_rules: List[RuleFile] = []
|
|
328
392
|
logger.info(f"开始选择规则,总规则数: {len(rules)}")
|
|
329
|
-
|
|
393
|
+
|
|
394
|
+
# 预先分类处理always_apply规则
|
|
395
|
+
always_apply_rules = []
|
|
396
|
+
need_llm_rules = []
|
|
397
|
+
|
|
330
398
|
for rule in rules:
|
|
331
399
|
if rule.always_apply:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
# **** 模拟LLM调用 (用于测试/开发) ****
|
|
366
|
-
# 注释掉模拟部分,使用上面的实际调用
|
|
367
|
-
# simulated_response = "yes" if "always" in rule.description.lower() or "index" in rule.description.lower() else "no"
|
|
368
|
-
# logger.warning(f"模拟LLM判断规则 '{os.path.basename(rule.file_path)}': {simulated_response}")
|
|
369
|
-
# response_text = simulated_response
|
|
370
|
-
# **** 结束模拟 ****
|
|
371
|
-
|
|
372
|
-
except Exception as e:
|
|
373
|
-
logger.error(f"使用 LLM 判断规则 '{os.path.basename(rule.file_path)}' 时出错: {e}", exc_info=True)
|
|
374
|
-
# 根据策略决定是否包含出错的规则,这里选择跳过
|
|
375
|
-
continue
|
|
376
|
-
|
|
400
|
+
always_apply_rules.append(rule)
|
|
401
|
+
elif self.llm is not None:
|
|
402
|
+
need_llm_rules.append(rule)
|
|
403
|
+
|
|
404
|
+
# 添加always_apply规则
|
|
405
|
+
for rule in always_apply_rules:
|
|
406
|
+
selected_rules.append(rule)
|
|
407
|
+
logger.debug(f"规则 '{os.path.basename(rule.file_path)}' (AlwaysApply=True) 已自动选择。")
|
|
408
|
+
|
|
409
|
+
# 如果没有需要LLM评估的规则,直接返回结果
|
|
410
|
+
if not need_llm_rules:
|
|
411
|
+
logger.info(f"规则选择完成,选中规则数: {len(selected_rules)}")
|
|
412
|
+
return selected_rules
|
|
413
|
+
|
|
414
|
+
# 使用线程池并发评估规则
|
|
415
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
416
|
+
# 提交所有评估任务
|
|
417
|
+
future_to_rule = {
|
|
418
|
+
executor.submit(self._evaluate_rule, rule, context): rule
|
|
419
|
+
for rule in need_llm_rules
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# 收集评估结果
|
|
423
|
+
for future in concurrent.futures.as_completed(future_to_rule):
|
|
424
|
+
rule, is_selected, reason = future.result()
|
|
425
|
+
if is_selected:
|
|
426
|
+
selected_rules.append(rule)
|
|
427
|
+
logger.info(f"规则 '{os.path.basename(rule.file_path)}' (AlwaysApply=False) 已被 LLM 选择,原因: {reason}")
|
|
428
|
+
else:
|
|
429
|
+
logger.debug(f"规则 '{os.path.basename(rule.file_path)}' (AlwaysApply=False) 未被 LLM 选择,原因: {reason}")
|
|
430
|
+
|
|
377
431
|
logger.info(f"规则选择完成,选中规则数: {len(selected_rules)}")
|
|
378
432
|
return selected_rules
|
|
379
433
|
|
|
@@ -393,9 +447,38 @@ class RuleSelector:
|
|
|
393
447
|
# 保持 file_path 作为 key
|
|
394
448
|
return {rule.file_path: rule.content for rule in selected_rules}
|
|
395
449
|
|
|
396
|
-
def auto_select_rules(context: str,
|
|
450
|
+
def auto_select_rules(context: str, llm: Optional[byzerllm.ByzerLLM] = None,args:Optional[AutoCoderArgs] = None) -> List[Dict[str, str]]:
|
|
397
451
|
"""
|
|
398
452
|
根据LLM的判断和规则元数据选择适用的规则。
|
|
399
453
|
"""
|
|
400
|
-
selector = RuleSelector(llm=llm, args=args)
|
|
401
|
-
return selector.
|
|
454
|
+
selector = RuleSelector(llm=llm, args=args)
|
|
455
|
+
return selector.get_selected_rules_content(context=context)
|
|
456
|
+
|
|
457
|
+
def get_required_and_index_rules() -> Dict[str, str]:
|
|
458
|
+
"""
|
|
459
|
+
获取所有必须应用的规则文件(always_apply=True)和Index.md文件。
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
project_root: 可选的项目根目录路径,用于初始化规则管理器。
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Dict[str, str]: 包含必须应用的规则和Index.md文件的{file_path: content}字典。
|
|
466
|
+
"""
|
|
467
|
+
# 获取所有解析后的规则文件
|
|
468
|
+
parsed_rules = get_parsed_rules()
|
|
469
|
+
result: Dict[str, str] = {}
|
|
470
|
+
logger.info(f"获取所有解析后的规则文件完成,总数: {len(parsed_rules)}")
|
|
471
|
+
|
|
472
|
+
for rule in parsed_rules:
|
|
473
|
+
# 检查是否是always_apply=True的规则
|
|
474
|
+
if rule.always_apply:
|
|
475
|
+
result[rule.file_path] = rule.content
|
|
476
|
+
logger.info(f"添加必须应用的规则: {os.path.basename(rule.file_path)}")
|
|
477
|
+
|
|
478
|
+
# 检查是否是Index.md文件
|
|
479
|
+
if os.path.basename(rule.file_path).lower() == "index.md":
|
|
480
|
+
result[rule.file_path] = rule.content
|
|
481
|
+
logger.info(f"添加Index.md文件: {rule.file_path}")
|
|
482
|
+
|
|
483
|
+
logger.info(f"获取必须应用的规则和Index.md文件完成,总数: {len(result)}")
|
|
484
|
+
return result
|