auto-coder 0.1.361__py3-none-any.whl → 0.1.363__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 (57) hide show
  1. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/METADATA +2 -1
  2. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/RECORD +57 -29
  3. autocoder/agent/auto_learn.py +249 -262
  4. autocoder/agent/base_agentic/__init__.py +0 -0
  5. autocoder/agent/base_agentic/agent_hub.py +169 -0
  6. autocoder/agent/base_agentic/agentic_lang.py +112 -0
  7. autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
  8. autocoder/agent/base_agentic/base_agent.py +1582 -0
  9. autocoder/agent/base_agentic/default_tools.py +683 -0
  10. autocoder/agent/base_agentic/test_base_agent.py +82 -0
  11. autocoder/agent/base_agentic/tool_registry.py +425 -0
  12. autocoder/agent/base_agentic/tools/__init__.py +12 -0
  13. autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
  14. autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
  15. autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
  16. autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
  17. autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
  18. autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
  19. autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
  20. autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
  21. autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
  22. autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
  23. autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
  24. autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
  25. autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
  26. autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
  27. autocoder/agent/base_agentic/types.py +189 -0
  28. autocoder/agent/base_agentic/utils.py +100 -0
  29. autocoder/auto_coder.py +1 -1
  30. autocoder/auto_coder_runner.py +36 -14
  31. autocoder/chat/conf_command.py +11 -10
  32. autocoder/commands/auto_command.py +227 -159
  33. autocoder/common/__init__.py +2 -2
  34. autocoder/common/ignorefiles/ignore_file_utils.py +12 -8
  35. autocoder/common/result_manager.py +10 -2
  36. autocoder/common/rulefiles/autocoderrules_utils.py +169 -0
  37. autocoder/common/save_formatted_log.py +1 -1
  38. autocoder/common/v2/agent/agentic_edit.py +53 -41
  39. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +15 -12
  40. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +73 -1
  41. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +132 -4
  42. autocoder/common/v2/agent/agentic_edit_types.py +1 -2
  43. autocoder/common/v2/agent/agentic_tool_display.py +2 -3
  44. autocoder/common/v2/code_auto_generate_editblock.py +3 -1
  45. autocoder/index/index.py +14 -8
  46. autocoder/privacy/model_filter.py +297 -35
  47. autocoder/rag/long_context_rag.py +424 -397
  48. autocoder/rag/test_doc_filter.py +393 -0
  49. autocoder/rag/test_long_context_rag.py +473 -0
  50. autocoder/rag/test_token_limiter.py +342 -0
  51. autocoder/shadows/shadow_manager.py +1 -3
  52. autocoder/utils/_markitdown.py +22 -3
  53. autocoder/version.py +1 -1
  54. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/LICENSE +0 -0
  55. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/WHEEL +0 -0
  56. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/entry_points.txt +0 -0
  57. {auto_coder-0.1.361.dist-info → auto_coder-0.1.363.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,473 @@
1
+ import pytest
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ from loguru import logger
6
+ from pathlib import Path
7
+ import byzerllm
8
+ from typing import Dict, Any
9
+
10
+ # 导入被测模块
11
+ from autocoder.rag.long_context_rag import LongContextRAG
12
+ from autocoder.common import AutoCoderArgs
13
+
14
+ # 1. 初始化FileMonitor(必须最先进行)
15
+ @pytest.fixture(scope="function")
16
+ def setup_file_monitor(temp_test_dir):
17
+ """初始化FileMonitor,必须最先执行"""
18
+ try:
19
+ from autocoder.common.file_monitor.monitor import FileMonitor
20
+ monitor = FileMonitor(temp_test_dir)
21
+ monitor.reset_instance()
22
+ if not monitor.is_running():
23
+ monitor.start()
24
+ logger.info(f"文件监控已启动: {temp_test_dir}")
25
+ else:
26
+ logger.info(f"文件监控已在运行中: {monitor.root_dir}")
27
+ except Exception as e:
28
+ logger.error(f"初始化文件监控出错: {e}")
29
+
30
+ # 2. 加载规则文件
31
+ try:
32
+ from autocoder.common.rulefiles.autocoderrules_utils import get_rules, reset_rules_manager
33
+ reset_rules_manager()
34
+ rules = get_rules(temp_test_dir)
35
+ logger.info(f"已加载规则: {len(rules)} 条")
36
+ except Exception as e:
37
+ logger.error(f"加载规则出错: {e}")
38
+
39
+ return temp_test_dir
40
+
41
+ # Pytest Fixture: 临时测试目录
42
+ @pytest.fixture(scope="function")
43
+ def temp_test_dir():
44
+ """提供一个临时的、测试后自动清理的目录"""
45
+ temp_dir = tempfile.mkdtemp()
46
+ logger.info(f"创建测试临时目录: {temp_dir}")
47
+ yield temp_dir
48
+ logger.info(f"清理测试临时目录: {temp_dir}")
49
+ shutil.rmtree(temp_dir)
50
+
51
+ # Pytest Fixture: 测试文件结构
52
+ @pytest.fixture(scope="function")
53
+ def test_files(temp_test_dir):
54
+ """创建测试所需的文件/目录结构"""
55
+ # 创建示例文件
56
+ file_structure = {
57
+ "docs/guide.md": "# RAG 使用指南\n使用LongContextRAG可以处理大规模文档检索和问答。",
58
+ "docs/api.md": "# API说明\n## 初始化\n```python\nrag = LongContextRAG(llm, args, path)\n```",
59
+ "src/example.py": "def add(a, b):\n return a + b\n\ndef subtract(a, b):\n return a - b",
60
+ "src/utils/helpers.py": "def format_text(text):\n return text.strip()\n\ndef count_words(text):\n return len(text.split())",
61
+ ".gitignore": "*.log\n__pycache__/\n.cache/",
62
+ ".autocoderignore": "*.log\n__pycache__/\n.cache/"
63
+ }
64
+
65
+ for file_path, content in file_structure.items():
66
+ full_path = os.path.join(temp_test_dir, file_path)
67
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
68
+ with open(full_path, 'w', encoding='utf-8') as f:
69
+ f.write(content)
70
+
71
+ return temp_test_dir
72
+
73
+ # Pytest Fixture: 配置参数
74
+ @pytest.fixture
75
+ def test_args():
76
+ """创建测试用配置参数"""
77
+ return AutoCoderArgs(
78
+ source_dir=".",
79
+ context_prune=True,
80
+ context_prune_strategy="extract",
81
+ conversation_prune_safe_zone_tokens=400,
82
+ context_prune_sliding_window_size=10,
83
+ context_prune_sliding_window_overlap=2,
84
+ rag_context_window_limit=8000,
85
+ rag_doc_filter_relevance=3,
86
+ full_text_ratio=0.7,
87
+ segment_ratio=0.2,
88
+ index_filter_workers=1,
89
+ required_exts=".py,.md",
90
+ monitor_mode=False,
91
+ enable_hybrid_index=False
92
+ )
93
+
94
+ # 3. 加载tokenizer (必须在FileMonitor和rules初始化之后)
95
+ @pytest.fixture
96
+ def load_tokenizer_fixture(setup_file_monitor):
97
+ """加载tokenizer,必须在FileMonitor和rules初始化之后"""
98
+ from autocoder.auto_coder_runner import load_tokenizer
99
+ load_tokenizer()
100
+ logger.info("Tokenizer加载完成")
101
+ return True
102
+
103
+ # 4. 初始化LLM
104
+ @pytest.fixture
105
+ def real_llm(load_tokenizer_fixture):
106
+ """创建真实的LLM对象,必须在tokenizer加载之后"""
107
+ from autocoder.utils.llms import get_single_llm
108
+ llm = get_single_llm("v3_chat", product_mode="lite")
109
+ logger.info(f"LLM初始化完成: {llm.default_model_name}")
110
+ return llm
111
+
112
+ # 5. LongContextRAG实例
113
+ @pytest.fixture
114
+ def rag_instance(real_llm, test_args, test_files, setup_file_monitor, load_tokenizer_fixture):
115
+ """创建LongContextRAG实例,必须在前面所有步骤之后"""
116
+ # 创建实例
117
+ instance = LongContextRAG(
118
+ llm=real_llm,
119
+ args=test_args,
120
+ path=test_files,
121
+ tokenizer_path=None
122
+ )
123
+ logger.info("RAG组件初始化完成")
124
+ return instance
125
+
126
+ # 用于构建RAG测试查询的辅助类
127
+ class RAGQueryBuilder:
128
+ def __init__(self, feature_name: str):
129
+ self.feature_name = feature_name
130
+
131
+ @byzerllm.prompt()
132
+ def build_test_query(self, specific_aspect: str = None) -> Dict[str, Any]:
133
+ """
134
+ 我需要了解有关{{ feature_name }}的信息。
135
+ {% if specific_aspect %}
136
+ 特别是关于{{ specific_aspect }}的部分,请详细说明其工作原理。
137
+ {% else %}
138
+ 请提供其基本用法和主要特性。
139
+ {% endif %}
140
+ 例如,如何在代码中实现和使用它?
141
+ """
142
+ return {
143
+ "feature_name": self.feature_name,
144
+ "specific_aspect": specific_aspect
145
+ }
146
+
147
+ # --- 测试用例 ---
148
+
149
+ def test_search(rag_instance):
150
+ """测试文档搜索功能"""
151
+ # 搜索查询
152
+ query = "如何使用RAG进行文档检索?"
153
+ results = rag_instance.search(query)
154
+
155
+ # 验证结果
156
+ assert len(results) >= 1
157
+ # 检查是否找到了相关文档
158
+ relevant_docs = [doc for doc in results if "RAG" in doc.source_code]
159
+ assert len(relevant_docs) > 0
160
+
161
+ def test_stream_chat_oai(rag_instance):
162
+ """测试流式聊天功能"""
163
+ # 使用byzerllm.prompt装饰器构建更有结构的测试查询
164
+ query_builder = RAGQueryBuilder(feature_name="RAG检索增强生成")
165
+ test_query = query_builder.build_test_query.prompt(specific_aspect="文档检索原理")
166
+
167
+ # 构建对话
168
+ conversations = [{"role": "user", "content": test_query}]
169
+
170
+ # 执行流式聊天
171
+ generator, context = rag_instance.stream_chat_oai(conversations)
172
+
173
+ # 收集所有响应片段
174
+ response_chunks = []
175
+ tokens_metadata = []
176
+
177
+ for content, metadata in generator:
178
+ if content:
179
+ response_chunks.append(content)
180
+ if metadata:
181
+ tokens_metadata.append(metadata)
182
+
183
+ # 合并响应内容
184
+ full_response = "".join(response_chunks)
185
+
186
+ # 验证响应
187
+ assert len(response_chunks) > 0, "应该产生至少一个响应片段"
188
+ assert full_response, "响应内容不应为空"
189
+
190
+ # 验证响应内容中是否包含关键概念
191
+ keywords = ["RAG", "检索", "文档", "生成"]
192
+ found_keywords = [keyword for keyword in keywords if keyword in full_response]
193
+ assert len(found_keywords) > 0, f"响应应包含至少一个关键词: {keywords}"
194
+
195
+ # 验证元数据
196
+ if tokens_metadata:
197
+ assert any(hasattr(meta, 'input_tokens_count') for meta in tokens_metadata), "元数据应包含输入token计数"
198
+ assert any(hasattr(meta, 'generated_tokens_count') for meta in tokens_metadata), "元数据应包含生成token计数"
199
+
200
+ # 验证上下文
201
+ assert context is not None, "应返回上下文信息"
202
+ if isinstance(context, list) and context:
203
+ # 检查返回的上下文文件是否包含相关文档
204
+ # 由于在合并文档的情况下,文件名可能是Merged_开头,不一定包含RAG字样
205
+ # 只需检查是否有内容返回即可,不必严格要求文件名包含RAG
206
+ assert len(context) > 0, "上下文应包含至少一个文档"
207
+
208
+ # 打印测试结果详情
209
+ logger.info("="*80)
210
+ logger.info("流式聊天测试结果:")
211
+ logger.info("-"*80)
212
+ logger.info(f"测试查询: {test_query}")
213
+ logger.info("-"*80)
214
+ logger.info(f"响应片段数量: {len(response_chunks)}")
215
+ logger.info(f"完整响应内容:\n{full_response}")
216
+ logger.info("-"*80)
217
+ logger.info(f"找到的关键词: {found_keywords}")
218
+ logger.info("-"*80)
219
+
220
+ # 打印token统计
221
+ if tokens_metadata:
222
+ input_tokens = sum(getattr(meta, 'input_tokens_count', 0) for meta in tokens_metadata if hasattr(meta, 'input_tokens_count'))
223
+ generated_tokens = sum(getattr(meta, 'generated_tokens_count', 0) for meta in tokens_metadata if hasattr(meta, 'generated_tokens_count'))
224
+ logger.info(f"输入Token总数: {input_tokens}")
225
+ logger.info(f"生成Token总数: {generated_tokens}")
226
+ logger.info(f"Token总消耗: {input_tokens + generated_tokens}")
227
+ else:
228
+ logger.info("未收集到Token元数据")
229
+ logger.info("-"*80)
230
+
231
+ # 打印上下文信息
232
+ logger.info(f"上下文文件数量: {len(context) if isinstance(context, list) else '未知'}")
233
+ if isinstance(context, list) and context:
234
+ for idx, ctx in enumerate(context):
235
+ logger.info(f"上下文[{idx}]: {ctx}")
236
+ logger.info("="*80)
237
+
238
+ def test_process_document_retrieval(rag_instance):
239
+ """测试文档召回和过滤阶段"""
240
+ # 构建测试对话
241
+ query = "如何使用RAG进行文档检索?"
242
+ conversations = [{"role": "user", "content": query}]
243
+
244
+ # 准备RAG统计数据
245
+ from autocoder.rag.long_context_rag import RAGStat, RecallStat, ChunkStat, AnswerStat
246
+ rag_stat = RAGStat(
247
+ recall_stat=RecallStat(
248
+ total_input_tokens=0,
249
+ total_generated_tokens=0,
250
+ model_name=rag_instance.recall_llm.default_model_name,
251
+ ),
252
+ chunk_stat=ChunkStat(
253
+ total_input_tokens=0,
254
+ total_generated_tokens=0,
255
+ model_name=rag_instance.chunk_llm.default_model_name,
256
+ ),
257
+ answer_stat=AnswerStat(
258
+ total_input_tokens=0,
259
+ total_generated_tokens=0,
260
+ model_name=rag_instance.qa_llm.default_model_name,
261
+ ),
262
+ )
263
+
264
+ # 调用方法获取生成器
265
+ generator = rag_instance._process_document_retrieval(
266
+ conversations=conversations,
267
+ query=query,
268
+ rag_stat=rag_stat
269
+ )
270
+
271
+ # 收集生成器的输出
272
+ items = []
273
+ for item in generator:
274
+ items.append(item)
275
+
276
+ # 验证生成器产生了输出
277
+ assert len(items) > 0, "文档召回和过滤阶段应该产生输出"
278
+
279
+ # 验证最后一个输出是包含结果的字典
280
+ assert isinstance(items[-1], dict), "最后一个输出应该是包含结果的字典"
281
+ assert "result" in items[-1], "结果字典应包含'result'键"
282
+
283
+ # 验证统计数据被更新
284
+ assert rag_stat.recall_stat.total_input_tokens > 0, "输入token计数应该增加"
285
+ assert rag_stat.recall_stat.total_generated_tokens > 0, "生成token计数应该增加"
286
+
287
+ # 打印测试结果详情
288
+ logger.info("="*80)
289
+ logger.info("文档召回和过滤阶段测试结果:")
290
+ logger.info("-"*80)
291
+ logger.info(f"生成的项目数: {len(items)}")
292
+ logger.info(f"相关文档数: {len(items[-1]['result']) if 'result' in items[-1] else 0}")
293
+ logger.info(f"输入tokens: {rag_stat.recall_stat.total_input_tokens}")
294
+ logger.info(f"输出tokens: {rag_stat.recall_stat.total_generated_tokens}")
295
+ logger.info("="*80)
296
+
297
+ def test_process_document_chunking(rag_instance):
298
+ """测试文档分块和重排序阶段"""
299
+ # 首先获取文档
300
+ query = "如何使用RAG进行文档检索?"
301
+ conversations = [{"role": "user", "content": query}]
302
+
303
+ # 准备RAG统计数据
304
+ from autocoder.rag.long_context_rag import RAGStat, RecallStat, ChunkStat, AnswerStat
305
+ rag_stat = RAGStat(
306
+ recall_stat=RecallStat(
307
+ total_input_tokens=10, # 假设已有一些token
308
+ total_generated_tokens=5,
309
+ model_name=rag_instance.recall_llm.default_model_name,
310
+ ),
311
+ chunk_stat=ChunkStat(
312
+ total_input_tokens=0,
313
+ total_generated_tokens=0,
314
+ model_name=rag_instance.chunk_llm.default_model_name,
315
+ ),
316
+ answer_stat=AnswerStat(
317
+ total_input_tokens=0,
318
+ total_generated_tokens=0,
319
+ model_name=rag_instance.qa_llm.default_model_name,
320
+ ),
321
+ )
322
+
323
+ # 获取测试文档
324
+ recall_generator = rag_instance._process_document_retrieval(
325
+ conversations=conversations,
326
+ query=query,
327
+ rag_stat=rag_stat
328
+ )
329
+
330
+ # 找到包含文档的结果
331
+ relevant_docs = None
332
+ for item in recall_generator:
333
+ if isinstance(item, dict) and "result" in item:
334
+ relevant_docs = item["result"]
335
+
336
+ # 确保我们有文档可以测试
337
+ assert relevant_docs is not None, "应该获取到一些相关文档"
338
+ assert len(relevant_docs) > 0, "相关文档数量应该大于0"
339
+
340
+ # 获取文档的source_code属性
341
+ source_docs = [doc.source_code for doc in relevant_docs]
342
+
343
+ # 调用文档分块方法
344
+ filter_time = 1.0 # 模拟过滤耗时
345
+ chunking_generator = rag_instance._process_document_chunking(
346
+ relevant_docs=source_docs,
347
+ conversations=conversations,
348
+ rag_stat=rag_stat,
349
+ filter_time=filter_time
350
+ )
351
+
352
+ # 收集分块结果
353
+ chunking_items = []
354
+ for item in chunking_generator:
355
+ chunking_items.append(item)
356
+
357
+ # 验证生成器产生了输出
358
+ assert len(chunking_items) > 0, "文档分块阶段应该产生输出"
359
+
360
+ # 验证最后一个输出是包含处理结果的字典
361
+ assert isinstance(chunking_items[-1], dict), "最后一个输出应该是包含结果的字典"
362
+ assert "result" in chunking_items[-1], "结果字典应包含'result'键"
363
+
364
+ # 验证统计数据
365
+ if rag_instance.tokenizer is not None:
366
+ # 如果有tokenizer,应该会更新chunk_stat
367
+ assert rag_stat.chunk_stat.total_input_tokens >= 0, "分块输入token计数应该被记录"
368
+
369
+ # 打印测试结果详情
370
+ logger.info("="*80)
371
+ logger.info("文档分块和重排序阶段测试结果:")
372
+ logger.info("-"*80)
373
+ logger.info(f"生成的项目数: {len(chunking_items)}")
374
+ logger.info(f"处理后的文档数: {len(chunking_items[-1]['result']) if 'result' in chunking_items[-1] else 0}")
375
+ logger.info(f"全文区文档数: {len(chunking_items[-1].get('first_round_full_docs', [])) if isinstance(chunking_items[-1], dict) else 0}")
376
+ logger.info(f"分段区文档数: {len(chunking_items[-1].get('second_round_extracted_docs', [])) if isinstance(chunking_items[-1], dict) else 0}")
377
+ logger.info(f"分块处理时间: {chunking_items[-1].get('sencond_round_time', 0) if isinstance(chunking_items[-1], dict) else 0}")
378
+ logger.info("="*80)
379
+
380
+ def test_process_qa_generation(rag_instance):
381
+ """测试QA生成阶段"""
382
+ # 构建测试对话
383
+ query = "如何使用RAG进行文档检索?"
384
+ conversations = [{"role": "user", "content": query}]
385
+
386
+ # 准备RAG统计数据
387
+ from autocoder.rag.long_context_rag import RAGStat, RecallStat, ChunkStat, AnswerStat
388
+ rag_stat = RAGStat(
389
+ recall_stat=RecallStat(
390
+ total_input_tokens=100, # 假设前面阶段已有一些token
391
+ total_generated_tokens=20,
392
+ model_name=rag_instance.recall_llm.default_model_name,
393
+ ),
394
+ chunk_stat=ChunkStat(
395
+ total_input_tokens=50,
396
+ total_generated_tokens=10,
397
+ model_name=rag_instance.chunk_llm.default_model_name,
398
+ ),
399
+ answer_stat=AnswerStat(
400
+ total_input_tokens=0,
401
+ total_generated_tokens=0,
402
+ model_name=rag_instance.qa_llm.default_model_name,
403
+ ),
404
+ )
405
+
406
+ # 获取测试文档(从召回到分块的完整流程)
407
+ doc_retrieval_generator = rag_instance._process_document_retrieval(
408
+ conversations=conversations,
409
+ query=query,
410
+ rag_stat=rag_stat
411
+ )
412
+
413
+ relevant_docs = None
414
+ for item in doc_retrieval_generator:
415
+ if isinstance(item, dict) and "result" in item:
416
+ relevant_docs = item["result"]
417
+
418
+ source_docs = [doc.source_code for doc in relevant_docs]
419
+
420
+ doc_chunking_generator = rag_instance._process_document_chunking(
421
+ relevant_docs=source_docs,
422
+ conversations=conversations,
423
+ rag_stat=rag_stat,
424
+ filter_time=1.0
425
+ )
426
+
427
+ processed_docs = None
428
+ for item in doc_chunking_generator:
429
+ if isinstance(item, dict) and "result" in item:
430
+ processed_docs = item["result"]
431
+
432
+ # 确保我们有处理好的文档可以测试
433
+ assert processed_docs is not None, "应该获取到处理后的文档"
434
+
435
+ # 调用QA生成方法
436
+ qa_generator = rag_instance._process_qa_generation(
437
+ relevant_docs=processed_docs,
438
+ conversations=conversations,
439
+ target_llm=rag_instance.qa_llm,
440
+ rag_stat=rag_stat
441
+ )
442
+
443
+ # 收集QA生成结果
444
+ qa_items = []
445
+ for item in qa_generator:
446
+ qa_items.append(item)
447
+
448
+ # 验证生成器产生了输出
449
+ assert len(qa_items) > 0, "QA生成阶段应该产生输出"
450
+
451
+ # 验证有内容生成
452
+ content_items = [item[0] for item in qa_items if isinstance(item, tuple) and len(item) == 2 and item[0]]
453
+ assert len(content_items) > 0, "QA生成应该产生内容"
454
+
455
+ # 验证统计数据被更新
456
+ assert rag_stat.answer_stat.total_input_tokens > 0, "QA生成输入token计数应该增加"
457
+ assert rag_stat.answer_stat.total_generated_tokens > 0, "QA生成的输出token计数应该增加"
458
+
459
+ # 打印测试结果详情
460
+ logger.info("="*80)
461
+ logger.info("QA生成阶段测试结果:")
462
+ logger.info("-"*80)
463
+ logger.info(f"生成的项目数: {len(qa_items)}")
464
+ logger.info(f"生成的内容片段数: {len(content_items)}")
465
+ logger.info(f"内容样例: {content_items[0] if content_items else 'None'}")
466
+ logger.info(f"QA输入tokens: {rag_stat.answer_stat.total_input_tokens}")
467
+ logger.info(f"QA输出tokens: {rag_stat.answer_stat.total_generated_tokens}")
468
+ logger.info(f"总输入tokens: {rag_stat.recall_stat.total_input_tokens + rag_stat.chunk_stat.total_input_tokens + rag_stat.answer_stat.total_input_tokens}")
469
+ logger.info(f"总输出tokens: {rag_stat.recall_stat.total_generated_tokens + rag_stat.chunk_stat.total_generated_tokens + rag_stat.answer_stat.total_generated_tokens}")
470
+ logger.info("="*80)
471
+
472
+ if __name__ == "__main__":
473
+ pytest.main(["-xvs", "test_long_context_rag.py"])