Files
ai-contract-platform/backend/tests/test_rag_service.py
T
2026-04-24 13:44:17 +08:00

1645 lines
71 KiB
Python

import unittest
from unittest.mock import Mock, MagicMock
from datetime import datetime, timezone
from app.services.rag_service import RAGService
from app.models.knowledge_base_document import KnowledgeBaseDocument
class TestKnowledgeBaseDocument:
"""模拟KnowledgeBaseDocument类"""
def __init__(self, id, knowledge_base_id, tenant_id, file_name, content_excerpt, created_at=None):
self.id = id
self.knowledge_base_id = knowledge_base_id
self.tenant_id = tenant_id
self.file_name = file_name
self.content_excerpt = content_excerpt
self.created_at = created_at or datetime.now(timezone.utc)
class RAGServiceTestCase(unittest.TestCase):
def setUp(self):
self.mock_db = Mock()
self.service = RAGService(self.mock_db)
def test_extract_keywords(self):
"""测试关键词提取"""
text = "这是一个关于合同违约金的测试文档"
keywords = self.service._extract_keywords(text)
# 当前简单分词实现会将中文按字符分割
# 应该提取出有意义的单字关键词(过滤停用词后)
self.assertIn("合", keywords)
self.assertIn("同", keywords)
self.assertIn("违", keywords)
self.assertIn("约", keywords)
self.assertIn("金", keywords)
self.assertIn("测", keywords)
self.assertIn("试", keywords)
self.assertIn("文", keywords)
self.assertIn("档", keywords)
# 不应该包含停用词
self.assertNotIn("这", keywords)
self.assertNotIn("是", keywords)
self.assertNotIn("一", keywords)
self.assertNotIn("个", keywords)
self.assertNotIn("关", keywords)
self.assertNotIn("于", keywords)
def test_calculate_similarity(self):
"""测试相似度计算"""
query_keywords = ["合同", "违约金", "条款"]
document_text = "本合同包含违约金条款,需要仔细审查违约责任部分。"
score = self.service._calculate_similarity(query_keywords, document_text)
# 应该有正分数
self.assertGreater(score, 0)
# 测试无匹配的情况
no_match_keywords = ["发票", "付款"]
no_match_score = self.service._calculate_similarity(no_match_keywords, document_text)
self.assertEqual(no_match_score, 0)
def test_build_context_from_results(self):
"""测试上下文构建"""
search_results = [
{
"id": "doc-1",
"knowledge_base_id": "kb-1",
"file_name": "合同范本.docx",
"content": "本合同违约金条款约定,违约方需支付合同总金额的20%作为违约金。",
"score": 0.8,
"created_at": "2026-04-16T10:00:00+00:00"
},
{
"id": "doc-2",
"knowledge_base_id": "kb-1",
"file_name": "法律条款.pdf",
"content": "根据民法典规定,违约金不得超过实际损失的30%。",
"score": 0.6,
"created_at": "2026-04-16T11:00:00+00:00"
}
]
context, sources = self.service.build_context_from_results(search_results)
# 应该构建出上下文
self.assertIsNotNone(context)
self.assertIn("[文档1]", context)
self.assertIn("[文档2]", context)
# 应该包含来源
self.assertEqual(len(sources), 2)
self.assertEqual(sources[0]["title"], "文档: 合同范本.docx")
self.assertEqual(sources[1]["title"], "文档: 法律条款.pdf")
def test_generate_answer_with_context(self):
"""测试回答生成"""
query = "违约金的比例应该是多少?"
context = "[文档1] 本合同违约金条款约定,违约方需支付合同总金额的20%作为违约金。\n\n[文档2] 根据民法典规定,违约金不得超过实际损失的30%。"
answer = self.service.generate_answer_with_context(query, context)
# 应该生成包含上下文的回答
self.assertIsNotNone(answer)
self.assertIn("根据知识库中的相关文档", answer)
self.assertIn("[文档1]", answer)
self.assertIn("[文档2]", answer)
def test_generate_answer_without_context(self):
"""测试无上下文时的回答生成"""
query = "违约金的比例应该是多少?"
context = ""
answer = self.service.generate_answer_with_context(query, context)
# 应该返回默认回答
self.assertIn("根据现有知识库,我暂时无法回答这个问题", answer)
def test_search_relevant_documents_no_documents(self):
"""测试无文档时的搜索"""
# 模拟repository返回空列表
self.service.document_repository.list_all_for_tenant = Mock(return_value=[])
results = self.service.search_relevant_documents("tenant-1", "测试查询")
self.assertEqual(len(results), 0)
def test_search_relevant_documents_with_matching(self):
"""测试有匹配文档时的搜索"""
mock_doc1 = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="合同文档.docx",
content_excerpt="本合同包含违约金条款和免责条款。付款节点为验收后十个工作日。"
)
mock_doc2 = TestKnowledgeBaseDocument(
id="doc-2",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="法律指南.pdf",
content_excerpt="关于发票和付款的相关规定。"
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc1, mock_doc2])
results = self.service.search_relevant_documents("tenant-1", "违约金条款")
self.assertGreaterEqual(len(results), 1)
if len(results) > 0:
self.assertEqual(results[0]["id"], "doc-1")
self.assertGreater(results[0]["score"], 0)
if len(results) > 1:
self.assertLess(results[1]["score"], results[0]["score"])
def test_build_chat_payload_with_results(self):
search_results = [
{
"id": "doc-1",
"knowledge_base_id": "kb-1",
"file_name": "合同范本.docx",
"content": "本合同违约金条款约定,违约方需支付合同总金额的20%作为违约金。",
"score": 1.2,
"created_at": "2026-04-16T10:00:00+00:00",
}
]
payload = self.service.build_chat_payload("违约金怎么约定", search_results)
self.assertGreaterEqual(len(payload["chunks"]), 3)
self.assertIn("审查结论", payload["chunks"][0])
self.assertIn("重点摘要", payload["chunks"][1])
self.assertIn("建议核查点", payload["chunks"][2])
self.assertIn("优先处理项", payload["chunks"][3])
self.assertIn("修改建议", payload["chunks"][4])
self.assertIn("谈判建议", payload["chunks"][5])
self.assertEqual(payload["sources"][0]["title"], "文档: 合同范本.docx")
self.assertIn("可参考来源", payload["sources"][0]["content"])
def test_build_chat_payload_without_results(self):
payload = self.service.build_chat_payload("续约条款怎么写", [])
self.assertGreaterEqual(len(payload["chunks"]), 4)
self.assertIn("风险提示", payload["chunks"][0])
self.assertIn("审查结论", payload["chunks"][1])
self.assertIn("重点摘要", payload["chunks"][2])
self.assertIn("建议核查点", payload["chunks"][3])
self.assertIn("优先处理项", payload["chunks"][4])
self.assertIn("修改建议", payload["chunks"][5])
self.assertIn("谈判建议", payload["chunks"][6])
self.assertEqual(payload["sources"][0]["title"], "审查要点:续约机制")
self.assertEqual(payload["sources"][1]["title"], "续约风险提示")
def test_search_relevant_documents_splits_long_content(self):
long_content = "\n".join(["违约金条款需要重点核查。" for _ in range(80)])
mock_doc = TestKnowledgeBaseDocument(
id="doc-long",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="long.docx",
content_excerpt=long_content,
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "违约金条款", limit=5)
self.assertGreaterEqual(len(results), 1)
self.assertTrue(all(len(item["content"]) <= 480 for item in results))
self.assertTrue(all(item["file_name"] == "long.docx" for item in results))
def test_generate_answer_with_context_includes_topic_hint(self):
answer = self.service.generate_answer_with_context(
"违约条款要看什么",
"[文档1] 违约责任需明确。",
)
self.assertIn("建议重点关注", answer)
self.assertIn("违约责任", answer)
def test_format_default_sources_contains_platform_hint(self):
sources = self.service.format_default_sources()
self.assertEqual(sources[0]["title"], "平台提示")
self.assertIsNone(sources[0]["score"])
def test_format_answer_chunks_appends_summary(self):
chunks = self.service.format_answer_chunks("回答正文", ["关注违约责任", "关注付款节点"])
self.assertEqual(chunks[0], "回答正文")
self.assertIn("重点摘要", chunks[1])
self.assertIn("关注付款节点", chunks[1])
def test_format_default_chunks_includes_topic_hint(self):
chunks = self.service.format_default_chunks("保密条款怎么审")
self.assertIn("已收到你的问题", chunks[0])
self.assertIn("建议关注", chunks[-1])
self.assertIn("保密范围", chunks[-1])
def test_build_contract_review_payload_adds_contract_sections(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 违约责任需明确。",
"sources": [{"title": "文档: 合同.docx", "content": "违约责任需明确。", "score": 1.3}],
"summary": ["违约责任需明确"],
"review_topics": ["违约"],
}
payload = self.service.build_contract_review_payload("违约条款怎么审", summary)
self.assertIn("审查结论", payload["chunks"][0])
self.assertIn("重点摘要", payload["chunks"][1])
self.assertIn("建议核查点", payload["chunks"][2])
self.assertIn("优先处理项", payload["chunks"][3])
self.assertIn("修改建议", payload["chunks"][4])
self.assertIn("谈判建议", payload["chunks"][5])
self.assertIn("合同审查辅助建议", payload["chunks"][-1])
def test_build_contract_review_fallback_payload_uses_topic_sources(self):
payload = self.service.build_contract_review_fallback_payload("付款条款怎么审")
self.assertEqual(payload["sources"][0]["title"], "审查要点:付款安排")
self.assertIn("风险提示", payload["chunks"][0])
self.assertIn("建议核查点", payload["chunks"][3])
def test_detect_review_topics_defaults_to_general(self):
topics = self.service._detect_review_topics("随机聊天", [])
self.assertEqual(topics, ["general"])
def test_build_focus_label_uses_general_when_needed(self):
label = self.service._build_focus_label(["general"])
self.assertEqual(label, "合同审查建议")
def test_rewrite_answer_for_contract_review_changes_tone(self):
rewritten = self.service._rewrite_answer_for_contract_review(
"违约条款怎么审",
"根据知识库中的相关文档,我为您提供以下信息:\n\n请优先核对上述文档原文,再结合具体合同条款作出判断。",
["违约"],
)
self.assertIn("当前更应从违约责任与违约金角度开展审查", rewritten)
self.assertIn("结合合同原文逐项比对", rewritten)
def test_build_backup_sources_returns_default_when_unknown(self):
sources = self.service._build_backup_sources(["general"])
self.assertEqual(sources[0]["title"], "平台提示")
def test_score_to_risk_label(self):
self.assertEqual(self.service._score_to_risk_label(None), "中风险")
self.assertEqual(self.service._score_to_risk_label(1.2), "高风险")
self.assertEqual(self.service._score_to_risk_label(0.7), "中风险")
self.assertEqual(self.service._score_to_risk_label(0.2), "低风险")
def test_build_source_note_contains_focus_and_risk(self):
note = self.service._build_source_note({"score": 1.1}, ["违约"])
self.assertIn("违约责任与违约金", note)
self.assertIn("高风险", note)
def test_truncate_source_content(self):
content = self.service._truncate_source_content("a" * 150)
self.assertTrue(content.endswith("..."))
def test_should_use_contract_review_style_for_contract_question(self):
self.assertTrue(self.service._should_use_contract_review_style("这个合同条款怎么审", []))
def test_should_use_contract_review_style_for_legal_result(self):
self.assertTrue(self.service._should_use_contract_review_style("普通问题", [{"content": "建议重点核查风险条款"}]))
def test_decorate_sources_appends_backup_sources(self):
sources = self.service._decorate_sources(
[{"title": "文档: 合同.docx", "content": "违约责任需明确。", "score": 1.2}],
["违约"],
)
self.assertGreaterEqual(len(sources), 2)
self.assertIn("可参考来源", sources[0]["content"])
def test_merge_review_summary_uses_general_fallback(self):
summary = self.service._merge_review_summary([], ["general"])
self.assertIn("先确认问题是否对应具体条款", summary[0])
def test_build_review_checklist_uses_general_fallback(self):
checklist = self.service._build_review_checklist(["general"])
self.assertIn("优先上传合同原文", "\n".join(checklist))
def test_normalize_analysis_sentence_truncates(self):
normalized = self.service._normalize_analysis_sentence("a" * 120)
self.assertTrue(normalized.endswith("..."))
def test_build_contract_headline(self):
headline = self.service._build_contract_headline("违约条款怎么审", ["违约"])
self.assertIn("违约责任与违约金", headline)
def test_build_contract_headline_prefers_review_context_action(self):
headline = self.service._build_contract_headline(
"请继续解释",
["general"],
{"metadata": {"focusLabel": "自动续约", "actionLabel": "解释风险"}},
)
self.assertIn("自动续约", headline)
self.assertIn("解释风险", headline)
def test_build_contract_review_payload_uses_review_context(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 自动续约需明确退出机制。",
"sources": [{"title": "文档: 合同.docx", "content": "自动续约需明确退出机制。", "score": 1.1}],
"summary": ["自动续约需明确退出机制"],
"review_topics": ["general"],
}
payload = self.service.build_contract_review_payload(
"请继续解释这个风险项",
summary,
review_context={
"metadata": {
"focusLabel": "自动续约",
"actionLabel": "解释风险",
"focusType": "risk_item",
"overallRisk": "high",
}
},
)
self.assertIn("自动续约", payload["chunks"][0])
self.assertIn("解释风险", payload["chunks"][0])
self.assertIn("总体风险偏高", payload["chunks"][1])
self.assertIn("自动续约", payload["sources"][0]["content"])
self.assertIn("聚焦类型:风险项", payload["sources"][0]["content"])
self.assertIn("为什么会触发当前风险判断", "\n".join(payload["chunks"]))
def test_build_context_action_sections_prefers_revision_action(self):
sections = self.service._build_context_action_sections(
["general"],
{
"metadata": {
"focusLabel": "付款节点",
"actionLabel": "给修改建议",
"focusType": "clause",
}
},
)
self.assertIn("修订清单", sections["修改建议"][0])
self.assertIn("付款节点对应的合同条款", sections["修改建议"][1])
def test_build_context_action_sections_prefers_negotiation_action(self):
sections = self.service._build_context_action_sections(
["general"],
{
"metadata": {
"focusLabel": "免责条款",
"actionLabel": "给谈判建议",
"focusType": "clause",
}
},
)
self.assertIn("谈判顺序", sections["谈判建议"][0])
self.assertIn("免责条款对应的合同条款", sections["谈判建议"][1])
def test_build_topic_findings_for_general(self):
findings = self.service._build_topic_findings(["general"])
self.assertTrue(findings)
def test_build_chat_payload_non_contract_question_uses_review_style_due_to_terms(self):
payload = self.service.build_chat_payload(
"风险怎么判断",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "建议重点核查违约责任。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("审查结论", payload["chunks"][0])
def test_build_backup_sources_limits_count(self):
sources = self.service._build_backup_sources(["违约", "付款", "续约"])
self.assertLessEqual(len(sources), 2)
def test_decorated_source_content_keeps_original_excerpt(self):
sources = self.service._decorate_sources(
[{"title": "文档: 合同.docx", "content": "付款节点需明确。", "score": 0.8}],
["付款"],
)
self.assertIn("付款节点需明确", sources[0]["content"])
def test_build_chat_payload_appends_disclaimer(self):
payload = self.service.build_chat_payload(
"违约条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "违约责任需明确。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("不替代正式法律意见", payload["chunks"][-1])
def test_build_contract_review_fallback_payload_appends_disclaimer(self):
payload = self.service.build_contract_review_fallback_payload("免责条款")
self.assertIn("不替代正式法律意见", payload["chunks"][-1])
def test_build_source_note_for_none_score(self):
note = self.service._build_source_note({"score": None}, ["general"])
self.assertIn("中风险", note)
def test_focus_label_dedupes_topics(self):
label = self.service._build_focus_label(["违约", "违约"])
self.assertEqual(label, "违约责任与违约金")
def test_merge_review_summary_dedupes_items(self):
summary = self.service._merge_review_summary(["违约触发条件是否明确"], ["违约"])
self.assertEqual(len(summary), len(set(summary)))
def test_build_review_checklist_dedupes_items(self):
checklist = self.service._build_review_checklist(["违约", "违约"])
self.assertEqual(len(checklist), len(set(checklist)))
def test_should_use_contract_review_style_false_for_plain_text(self):
self.assertFalse(self.service._should_use_contract_review_style("hello world", [{"content": "plain text"}]))
def test_decorate_sources_respects_snippet_limit(self):
long_content = "a" * 200
sources = self.service._decorate_sources(
[{"title": "文档: 合同.docx", "content": long_content, "score": 1.0}],
["general"],
)
self.assertIn("...", sources[0]["content"])
def test_build_contract_review_payload_uses_general_topic(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 一般内容。",
"sources": [{"title": "文档: 合同.docx", "content": "一般内容。", "score": 0.4}],
"summary": ["一般内容"],
"review_topics": ["general"],
}
payload = self.service.build_contract_review_payload("随机问题", summary)
self.assertIn("合同审查建议", payload["chunks"][0])
def test_build_contract_review_fallback_payload_uses_default_sources_for_general(self):
payload = self.service.build_contract_review_fallback_payload("随机问题")
self.assertEqual(payload["sources"][0]["title"], "平台提示")
def test_build_topic_findings_uses_first_checkpoint(self):
findings = self.service._build_topic_findings(["付款"])
self.assertIn("付款节点是否和验收挂钩", findings[0])
def test_rewrite_answer_for_contract_review_truncates_long_answer(self):
rewritten = self.service._rewrite_answer_for_contract_review("付款条款", "a" * 200, ["付款"])
self.assertIn("...", rewritten)
def test_build_source_note_uses_general_focus(self):
note = self.service._build_source_note({"score": 0.3}, ["general"])
self.assertIn("合同审查建议", note)
def test_build_chat_payload_review_style_includes_backup_sources(self):
payload = self.service.build_chat_payload(
"保密条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "保密义务需明确。", "score": 0.8, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertGreaterEqual(len(payload["sources"]), 2)
def test_build_contract_review_fallback_payload_uses_general_checklist_when_needed(self):
payload = self.service.build_contract_review_fallback_payload("random text")
self.assertIn("先确认问题是否对应具体条款", "\n".join(payload["chunks"]))
def test_detect_review_topics_uses_search_results(self):
topics = self.service._detect_review_topics("随机问题", [{"content": "付款节点需要明确"}])
self.assertEqual(topics, ["付款"])
def test_decorated_sources_preserve_score(self):
sources = self.service._decorate_sources(
[{"title": "文档: 合同.docx", "content": "违约责任需明确。", "score": 1.2}],
["违约"],
)
self.assertEqual(sources[0]["score"], 1.2)
def test_build_contract_review_payload_summary_limit(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a", "b", "c", "d", "e"],
"review_topics": ["违约"],
}
payload = self.service.build_contract_review_payload("违约条款", summary)
bullet_lines = [line for line in payload["chunks"][1].splitlines() if line.startswith("-")]
self.assertLessEqual(len(bullet_lines), 4)
def test_contract_review_payload_checklist_limit(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["违约", "付款", "续约"],
}
payload = self.service.build_contract_review_payload("综合条款", summary)
bullet_lines = [line for line in payload["chunks"][2].splitlines() if line.startswith("-")]
self.assertLessEqual(len(bullet_lines), 6)
def test_build_contract_review_fallback_payload_summary_limit(self):
payload = self.service.build_contract_review_fallback_payload("违约付款续约免责保密")
bullet_lines = [line for line in payload["chunks"][2].splitlines() if line.startswith("-")]
self.assertLessEqual(len(bullet_lines), 4)
def test_build_contract_review_fallback_payload_sources_limit(self):
payload = self.service.build_contract_review_fallback_payload("违约付款续约免责保密")
self.assertLessEqual(len(payload["sources"]), 2)
def test_contract_review_payload_source_note_contains_risk_label(self):
payload = self.service.build_chat_payload(
"违约条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "违约责任需明确。", "score": 1.2, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("高风险", payload["sources"][0]["content"])
def test_contract_review_payload_source_note_contains_focus(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.8, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("付款与结算安排", payload["sources"][0]["content"])
def test_build_contract_review_payload_has_disclaimer_last(self):
payload = self.service.build_chat_payload(
"保密条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "保密义务需明确。", "score": 0.8, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertEqual(payload["chunks"][-1], "以下意见仅作为合同审查辅助建议,不替代正式法律意见。")
def test_build_contract_review_fallback_payload_has_disclaimer_last(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertEqual(payload["chunks"][-1], "以下意见仅作为合同审查辅助建议,不替代正式法律意见。")
def test_rewrite_answer_for_contract_review_keeps_query(self):
rewritten = self.service._rewrite_answer_for_contract_review("付款条款", "根据知识库中的相关文档,我为您提供以下信息:", ["付款"])
self.assertIn("付款条款", rewritten)
def test_build_focus_label_multiple_topics(self):
label = self.service._build_focus_label(["违约", "付款"])
self.assertIn("违约责任与违约金", label)
self.assertIn("付款与结算安排", label)
def test_build_backup_sources_for_known_topic(self):
sources = self.service._build_backup_sources(["违约"])
self.assertEqual(sources[0]["title"], "审查要点:违约责任")
def test_contract_review_payload_without_review_style_uses_default_formatter(self):
payload = self.service.build_chat_payload(
"plain question",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "plain answer", "score": 0.8, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(payload["chunks"])
def test_build_contract_review_fallback_payload_general_sources(self):
payload = self.service.build_contract_review_fallback_payload("hello world")
self.assertEqual(payload["sources"][0]["title"], "平台提示")
def test_generate_answer_with_context_still_contains_context(self):
answer = self.service.generate_answer_with_context("违约条款", "[文档1] 违约责任需明确。")
self.assertIn("[文档1]", answer)
def test_build_contract_review_payload_sources_limit(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [
{"title": "文档: A", "content": "A", "score": 1.0},
{"title": "文档: B", "content": "B", "score": 0.8},
{"title": "文档: C", "content": "C", "score": 0.7},
],
"summary": ["a"],
"review_topics": ["违约"],
}
payload = self.service.build_contract_review_payload("违约条款", summary)
self.assertLessEqual(len(payload["sources"]), 5)
def test_decorated_sources_add_backup_when_missing(self):
sources = self.service._decorate_sources([], ["付款"])
self.assertTrue(sources)
def test_contract_review_payload_for_general_topic_uses_general_focus(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["general"],
}
payload = self.service.build_contract_review_payload("random", summary)
self.assertIn("合同审查建议", payload["chunks"][0])
def test_build_source_note_for_low_score(self):
note = self.service._build_source_note({"score": 0.1}, ["付款"])
self.assertIn("低风险", note)
def test_build_source_note_for_medium_score(self):
note = self.service._build_source_note({"score": 0.8}, ["付款"])
self.assertIn("中风险", note)
def test_build_source_note_for_high_score(self):
note = self.service._build_source_note({"score": 1.5}, ["付款"])
self.assertIn("高风险", note)
def test_build_contract_review_payload_appends_backup_sources_even_with_one_source(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertGreaterEqual(len(payload["sources"]), 2)
def test_should_use_contract_review_style_for_contract_keyword_only(self):
self.assertTrue(self.service._should_use_contract_review_style("合同", []))
def test_should_use_contract_review_style_for_empty_inputs(self):
self.assertFalse(self.service._should_use_contract_review_style("", []))
def test_detect_review_topics_multiple_topics(self):
topics = self.service._detect_review_topics("违约和付款条款", [])
self.assertEqual(topics, ["违约", "付款"])
def test_build_review_checklist_for_multiple_topics(self):
checklist = self.service._build_review_checklist(["违约", "付款"])
self.assertTrue(any("违约" in item for item in checklist))
self.assertTrue(any("付款" in item for item in checklist))
def test_merge_review_summary_for_multiple_topics(self):
summary = self.service._merge_review_summary([], ["违约", "付款"])
self.assertTrue(any("违约" in item for item in summary))
self.assertTrue(any("付款" in item for item in summary))
def test_build_contract_review_payload_with_empty_summary_points(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": [],
"review_topics": ["违约"],
}
payload = self.service.build_contract_review_payload("违约条款", summary)
self.assertIn("违约触发条件是否明确", payload["chunks"][1])
def test_build_contract_review_fallback_payload_with_general_topic(self):
payload = self.service.build_contract_review_fallback_payload("plain text")
self.assertIn("合同审查辅助建议", payload["chunks"][-1])
def test_format_default_sources_shape(self):
source = self.service.format_default_sources()[0]
self.assertIn("title", source)
self.assertIn("content", source)
self.assertIn("score", source)
def test_build_contract_review_payload_source_content_contains_note(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "付款节点需明确。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertIn("可参考来源", payload["sources"][0]["content"])
def test_build_contract_review_payload_source_content_is_truncated(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "a" * 200, "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertIn("...", payload["sources"][0]["content"])
def test_build_contract_review_payload_preserves_source_score(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 0.9}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertEqual(payload["sources"][0]["score"], 0.9)
def test_generate_answer_with_context_contains_attention_section_when_hint_exists(self):
answer = self.service.generate_answer_with_context("付款条款", "[文档1] 付款节点需明确。")
self.assertIn("建议重点关注", answer)
def test_generate_answer_with_context_no_attention_section_when_no_hint(self):
answer = self.service.generate_answer_with_context("hello", "[文档1] 普通内容。")
self.assertNotIn("建议重点关注", answer)
def test_build_backup_sources_returns_topic_specific_content(self):
sources = self.service._build_backup_sources(["保密"])
self.assertIn("保密", sources[0]["title"])
def test_build_source_note_uses_focus_label(self):
note = self.service._build_source_note({"score": 1.0}, ["保密"])
self.assertIn("保密义务", note)
def test_build_review_checklist_general_limit(self):
checklist = self.service._build_review_checklist(["general"])
self.assertLessEqual(len(checklist), 6)
def test_build_contract_review_payload_general_sources_limit(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [],
"summary": ["a"],
"review_topics": ["general"],
}
payload = self.service.build_contract_review_payload("random", summary)
self.assertLessEqual(len(payload["sources"]), 2)
def test_build_contract_review_payload_summary_contains_topic_points(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertIn("付款节点是否和验收挂钩", payload["chunks"][1])
def test_build_contract_review_payload_checklist_contains_topic_points(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertIn("逾期付款责任是否清晰", payload["chunks"][2])
def test_build_contract_review_fallback_payload_summary_contains_topic_points(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertIn("付款节点是否和验收挂钩", payload["chunks"][2])
def test_build_contract_review_fallback_payload_checklist_contains_topic_points(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertIn("逾期付款责任是否清晰", payload["chunks"][3])
def test_build_contract_review_payload_answer_contains_topic_findings(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
self.assertIn("付款与结算安排", payload["chunks"][0])
def test_build_contract_review_fallback_payload_answer_contains_general_focus(self):
payload = self.service.build_contract_review_fallback_payload("random")
self.assertIn("审查结论", payload["chunks"][1])
def test_build_contract_review_payload_source_note_general(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": None}],
"summary": ["a"],
"review_topics": ["general"],
}
payload = self.service.build_contract_review_payload("random", summary)
self.assertIn("合同审查建议", payload["sources"][0]["content"])
def test_build_contract_review_payload_backup_sources_for_topic(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [],
"summary": ["a"],
"review_topics": ["违约"],
}
payload = self.service.build_contract_review_payload("违约条款", summary)
self.assertEqual(payload["sources"][0]["title"], "审查要点:违约责任")
def test_build_contract_review_fallback_payload_general_sources(self):
payload = self.service.build_contract_review_fallback_payload("hello")
self.assertEqual(payload["sources"][0]["title"], "平台提示")
def test_build_contract_review_payload_summary_deduped(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["付款节点是否和验收挂钩"],
"review_topics": ["付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
bullet_lines = [line for line in payload["chunks"][1].splitlines() if line.startswith("-")]
self.assertEqual(len(bullet_lines), len(set(bullet_lines)))
def test_build_contract_review_payload_checklist_deduped(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "内容。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款", "付款"],
}
payload = self.service.build_contract_review_payload("付款条款", summary)
bullet_lines = [line for line in payload["chunks"][2].splitlines() if line.startswith("-")]
self.assertEqual(len(bullet_lines), len(set(bullet_lines)))
def test_build_contract_review_fallback_payload_general_summary_deduped(self):
payload = self.service.build_contract_review_fallback_payload("random")
bullet_lines = [line for line in payload["chunks"][2].splitlines() if line.startswith("-")]
self.assertEqual(len(bullet_lines), len(set(bullet_lines)))
def test_build_contract_review_fallback_payload_general_checklist_deduped(self):
payload = self.service.build_contract_review_fallback_payload("random")
bullet_lines = [line for line in payload["chunks"][3].splitlines() if line.startswith("-")]
self.assertEqual(len(bullet_lines), len(set(bullet_lines)))
def test_build_source_note_high_risk_for_high_score(self):
note = self.service._build_source_note({"score": 2.0}, ["违约"])
self.assertIn("高风险", note)
def test_build_source_note_medium_risk_for_mid_score(self):
note = self.service._build_source_note({"score": 0.7}, ["违约"])
self.assertIn("中风险", note)
def test_build_source_note_low_risk_for_low_score(self):
note = self.service._build_source_note({"score": 0.1}, ["违约"])
self.assertIn("低风险", note)
def test_build_source_note_medium_risk_for_none_score(self):
note = self.service._build_source_note({"score": None}, ["违约"])
self.assertIn("中风险", note)
def test_build_contract_review_payload_source_note_appended(self):
payload = self.service.build_chat_payload(
"违约条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "违约责任需明确。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("可参考来源", payload["sources"][0]["content"])
def test_build_contract_review_payload_for_general_query_with_legal_cue(self):
payload = self.service.build_chat_payload(
"hello",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "建议核查合同风险。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("审查结论", payload["chunks"][0])
def test_build_contract_review_payload_for_non_contract_query_without_legal_cue(self):
payload = self.service.build_chat_payload(
"hello",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "plain answer", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertEqual(len(payload["chunks"]), 2)
def test_build_contract_review_fallback_payload_topics_from_query(self):
payload = self.service.build_contract_review_fallback_payload("违约付款")
self.assertIn("违约触发条件是否明确", payload["chunks"][2])
self.assertIn("付款节点是否和验收挂钩", payload["chunks"][2])
def test_build_contract_review_payload_topics_from_summary(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "付款节点需明确。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款", "违约"],
}
payload = self.service.build_contract_review_payload("综合审查", summary)
self.assertIn("付款与结算安排", payload["chunks"][0])
self.assertIn("违约责任与违约金", payload["chunks"][0])
def test_build_contract_review_payload_sources_include_backup_for_each_topic(self):
summary = {
"answer": "根据知识库中的相关文档,我为您提供以下信息:\n\n[文档1] 内容。",
"sources": [{"title": "文档: 合同.docx", "content": "付款节点需明确。", "score": 1.0}],
"summary": ["a"],
"review_topics": ["付款", "违约"],
}
payload = self.service.build_contract_review_payload("综合审查", summary)
self.assertLessEqual(len(payload["sources"]), 5)
def test_build_contract_review_fallback_payload_source_titles(self):
payload = self.service.build_contract_review_fallback_payload("保密条款")
self.assertTrue(all("title" in item for item in payload["sources"]))
def test_build_contract_review_payload_source_titles(self):
payload = self.service.build_chat_payload(
"保密条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "保密义务需明确。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(all("title" in item for item in payload["sources"]))
def test_build_contract_review_payload_source_scores_preserved(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertEqual(payload["sources"][0]["score"], 0.95)
def test_build_contract_review_payload_source_backup_score_none(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIsNone(payload["sources"][-1]["score"])
def test_build_contract_review_payload_chunks_are_strings(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(all(isinstance(item, str) for item in payload["chunks"]))
def test_build_contract_review_payload_sources_are_dicts(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(all(isinstance(item, dict) for item in payload["sources"]))
def test_build_contract_review_payload_summary_headline(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(payload["chunks"][1].startswith("重点摘要"))
def test_build_contract_review_payload_checklist_headline(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(payload["chunks"][2].startswith("建议核查点"))
def test_build_contract_review_fallback_payload_summary_headline(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertTrue(payload["chunks"][2].startswith("重点摘要"))
def test_build_contract_review_fallback_payload_checklist_headline(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertTrue(payload["chunks"][3].startswith("建议核查点"))
def test_build_contract_review_payload_analysis_headline(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(payload["chunks"][0].startswith("审查结论"))
def test_build_contract_review_fallback_payload_risk_headline(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertTrue(payload["chunks"][0].startswith("风险提示"))
def test_build_contract_review_fallback_payload_analysis_headline(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertTrue(payload["chunks"][1].startswith("审查结论"))
def test_build_contract_review_payload_source_note_contains_source_headline(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("可参考来源", payload["sources"][0]["content"])
def test_build_contract_review_payload_appends_contract_disclaimer(self):
payload = self.service.build_chat_payload(
"付款条款怎么审",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确。", "score": 0.95, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertEqual(payload["chunks"][-1], "以下意见仅作为合同审查辅助建议,不替代正式法律意见。")
def test_build_contract_review_fallback_payload_appends_contract_disclaimer(self):
payload = self.service.build_contract_review_fallback_payload("付款条款")
self.assertEqual(payload["chunks"][-1], "以下意见仅作为合同审查辅助建议,不替代正式法律意见。")
def test_extract_keywords_includes_cjk_terms(self):
keywords = self.service._extract_keywords("自动续约条款与付款发票")
self.assertIn("自动续约", keywords)
self.assertIn("付款", keywords)
self.assertIn("发票", keywords)
def test_build_summary_points_dedupes_hints(self):
search_results = [
{
"id": "doc-1",
"knowledge_base_id": "kb-1",
"file_name": "contract.docx",
"content": "违约责任应明确违约金和触发条件。",
"score": 1.0,
"created_at": "2026-04-16T10:00:00+00:00",
}
]
points = self.service._build_summary_points("违约条款怎么审", search_results)
self.assertGreaterEqual(len(points), 1)
self.assertEqual(len(points), len(set(points)))
def test_dedupe_results_keeps_top_order(self):
results = [
{"file_name": "a.docx", "content": "x", "score": 3},
{"file_name": "a.docx", "content": "x", "score": 2},
{"file_name": "b.docx", "content": "y", "score": 1},
]
deduped = self.service._dedupe_results(results, 5)
self.assertEqual(len(deduped), 2)
self.assertEqual(deduped[0]["score"], 3)
self.assertEqual(deduped[1]["file_name"], "b.docx")
def test_calculate_keyword_coverage(self):
coverage = self.service._calculate_keyword_coverage(["违约", "条款"], ["违约", "责任", "条款"])
self.assertGreater(coverage, 0.9)
def test_calculate_domain_bonus(self):
bonus = self.service._calculate_domain_bonus("违约条款", "本合同的违约责任需要重点核查")
self.assertGreater(bonus, 0)
def test_build_context_limits_result_count(self):
search_results = [
{"id": f"doc-{index}", "knowledge_base_id": "kb-1", "file_name": f"doc-{index}.txt", "content": f"内容{index}", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
for index in range(5)
]
context, sources = self.service.build_context_from_results(search_results)
self.assertEqual(len(sources), 3)
self.assertIn("[文档3]", context)
self.assertNotIn("[文档4]", context)
def test_summarize_results_returns_answer_sources_and_summary(self):
search_results = [
{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点应明确。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
]
summary = self.service.summarize_results("付款条款", search_results)
self.assertIn("answer", summary)
self.assertIn("sources", summary)
self.assertIn("summary", summary)
self.assertTrue(summary["summary"])
def test_search_relevant_documents_respects_limit(self):
docs = [
TestKnowledgeBaseDocument(
id=f"doc-{index}",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name=f"doc-{index}.txt",
content_excerpt="违约条款需要重点核查。付款节点也要明确。",
)
for index in range(10)
]
self.service.document_repository.list_all_for_tenant = Mock(return_value=docs)
results = self.service.search_relevant_documents("tenant-1", "违约条款", limit=2)
self.assertLessEqual(len(results), 2)
def test_build_chat_payload_preserves_source_score(self):
payload = self.service.build_chat_payload(
"违约条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "违约责任需明确。", "score": 2.5, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertEqual(payload["sources"][0]["score"], 2.5)
def test_format_default_chunks_without_topic_hint(self):
chunks = self.service.format_default_chunks("随机问题")
self.assertEqual(len(chunks), 2)
def test_generate_answer_without_context(self):
answer = self.service.generate_answer_with_context("付款条款", "")
self.assertIn("暂时无法回答", answer)
def test_build_context_truncates_long_content(self):
search_results = [
{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "a" * 600, "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
]
context, sources = self.service.build_context_from_results(search_results)
self.assertTrue(sources[0]["content"].endswith("..."))
self.assertIn("[文档1]", context)
def test_search_relevant_documents_returns_empty_for_blank_keywords(self):
self.service.document_repository.list_all_for_tenant = Mock(return_value=[])
results = self.service.search_relevant_documents("tenant-1", " ")
self.assertEqual(results, [])
def test_format_answer_chunks_without_summary(self):
chunks = self.service.format_answer_chunks("回答正文", [])
self.assertEqual(chunks, ["回答正文"])
def test_calculate_domain_bonus_without_term_match(self):
bonus = self.service._calculate_domain_bonus("天气如何", "今天阳光很好")
self.assertEqual(bonus, 0.0)
def test_keyword_coverage_without_overlap(self):
coverage = self.service._calculate_keyword_coverage(["违约"], ["付款"])
self.assertEqual(coverage, 0.0)
def test_search_relevant_documents_dedupes_same_chunk(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="dup.docx",
content_excerpt="违约条款。违约条款。违约条款。",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "违约条款", limit=5)
unique_pairs = {(item["file_name"], item["content"]) for item in results}
self.assertEqual(len(unique_pairs), len(results))
def test_format_default_sources_message(self):
sources = self.service.format_default_sources()
self.assertIn("未检索到", sources[0]["content"])
def test_build_summary_points_respects_max_count(self):
search_results = [
{"id": f"doc-{index}", "knowledge_base_id": "kb-1", "file_name": f"doc-{index}.txt", "content": f"付款条款内容{index}", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
for index in range(6)
]
points = self.service._build_summary_points("付款条款", search_results)
self.assertLessEqual(len(points), 4)
def test_generate_answer_with_multiple_hints(self):
answer = self.service.generate_answer_with_context("违约和付款条款", "[文档1] 内容")
self.assertIn("建议重点关注", answer)
self.assertIn("付款节点", answer)
self.assertIn("违约责任", answer)
def test_extract_keywords_handles_ascii_tokens(self):
keywords = self.service._extract_keywords("invoice payment clause")
self.assertIn("invoice", keywords)
self.assertIn("payment", keywords)
self.assertIn("clause", keywords)
def test_search_relevant_documents_with_ascii_query(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="invoice.txt",
content_excerpt="invoice payment clause for delivery",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "payment clause", limit=3)
self.assertEqual(results[0]["id"], "doc-1")
self.assertGreater(results[0]["score"], 0)
def test_build_chat_payload_without_results_uses_platform_source(self):
payload = self.service.build_chat_payload("免责条款", [])
self.assertEqual(payload["sources"][0]["title"], "审查要点:免责条款")
self.assertIn("免责条款是否覆盖核心义务", "\n".join(payload["chunks"]))
def test_generate_answer_with_context_preserves_query(self):
answer = self.service.generate_answer_with_context("付款条款有哪些风险", "[文档1] 付款节点应明确")
self.assertIn("付款条款有哪些风险", answer)
def test_build_context_returns_empty_for_no_results(self):
context, sources = self.service.build_context_from_results([])
self.assertEqual(context, "")
self.assertEqual(sources, [])
def test_summarize_results_summary_uses_excerpt(self):
summary = self.service.summarize_results(
"付款条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点应明确,发票要求需写清。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(any("付款节点" in item for item in summary["summary"]))
def test_split_long_content_respects_chunk_overlap_behavior(self):
text = "。".join(["违约条款需要重点核查" for _ in range(50)])
chunks = self.service.search_relevant_documents
self.assertTrue(callable(chunks))
def test_dedupe_results_respects_limit(self):
results = [
{"file_name": f"doc-{index}.txt", "content": str(index), "score": index}
for index in range(10, 0, -1)
]
deduped = self.service._dedupe_results(results, 3)
self.assertEqual(len(deduped), 3)
def test_default_chunks_without_hints_have_two_sections(self):
chunks = self.service.format_default_chunks("hello world")
self.assertEqual(len(chunks), 2)
def test_context_sources_include_score(self):
context, sources = self.service.build_context_from_results([
{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "合同条款", "score": 2.3, "created_at": "2026-04-16T10:00:00+00:00"}
])
self.assertIn("[文档1]", context)
self.assertEqual(sources[0]["score"], 2.3)
def test_answer_chunks_support_summary_points(self):
chunks = self.service.format_answer_chunks("正文", ["A", "B", "C"])
self.assertEqual(len(chunks), 2)
self.assertIn("A", chunks[1])
def test_keyword_extraction_removes_stop_words(self):
keywords = self.service._extract_keywords("这是一个付款条款")
self.assertNotIn("这", keywords)
self.assertIn("付款", keywords)
def test_search_relevant_documents_returns_empty_when_no_chunk_matches(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="doc.txt",
content_excerpt="天气晴朗适合出门。",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "违约责任", limit=5)
self.assertEqual(results, [])
def test_domain_bonus_accumulates_terms(self):
bonus = self.service._calculate_domain_bonus("违约付款条款", "合同中违约和付款条款均需关注")
self.assertGreaterEqual(bonus, 0.3)
def test_summary_points_prioritize_topic_hints(self):
points = self.service._build_summary_points(
"违约条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "违约责任应明确。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertIn("违约责任", points[0])
def test_search_relevant_documents_can_match_cjk_bigram(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="doc.txt",
content_excerpt="自动续约需要设置通知期。",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "续约通知", limit=5)
self.assertGreaterEqual(len(results), 1)
def test_generate_answer_with_context_contains_context(self):
answer = self.service.generate_answer_with_context("合同条款", "[文档1] 合同应明确付款节点")
self.assertIn("[文档1]", answer)
def test_default_sources_shape(self):
source = self.service.format_default_sources()[0]
self.assertIn("title", source)
self.assertIn("content", source)
self.assertIn("score", source)
def test_build_chat_payload_result_chunk_order(self):
payload = self.service.build_chat_payload(
"付款条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点应明确", "score": 2.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(payload["chunks"][0].startswith("审查结论"))
self.assertTrue(payload["chunks"][1].startswith("重点摘要"))
def test_search_relevant_documents_orders_by_score_desc(self):
docs = [
TestKnowledgeBaseDocument("doc-1", "kb-1", "tenant-1", "a.txt", "违约责任和付款节点都要关注。"),
TestKnowledgeBaseDocument("doc-2", "kb-1", "tenant-1", "b.txt", "付款节点。"),
]
self.service.document_repository.list_all_for_tenant = Mock(return_value=docs)
results = self.service.search_relevant_documents("tenant-1", "违约付款", limit=5)
self.assertGreaterEqual(results[0]["score"], results[-1]["score"])
def test_extract_topic_hints_applies_known_terms(self):
payload = self.service.build_chat_payload("保密条款", [])
self.assertIn("保密信息范围是否明确", "\n".join(payload["chunks"]))
def test_build_context_source_titles_use_filename(self):
_context, sources = self.service.build_context_from_results([
{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "rules.pdf", "content": "付款规则", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
])
self.assertEqual(sources[0]["title"], "文档: rules.pdf")
def test_answer_with_context_has_final_guidance(self):
answer = self.service.generate_answer_with_context("续约条款", "[文档1] 应明确通知窗口")
self.assertIn("优先核对上述文档原文", answer)
def test_default_chunks_keep_query_text(self):
query = "这个付款条款怎么审"
chunks = self.service.format_default_chunks(query)
self.assertIn(query, chunks[0])
def test_keyword_coverage_caps_by_unique_query_tokens(self):
coverage = self.service._calculate_keyword_coverage(["违约", "违约"], ["违约"])
self.assertEqual(coverage, 1.0)
def test_search_relevant_documents_handles_many_documents(self):
docs = [
TestKnowledgeBaseDocument(
id=f"doc-{index}",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name=f"doc-{index}.txt",
content_excerpt="违约责任和付款节点需要核查。",
)
for index in range(20)
]
self.service.document_repository.list_all_for_tenant = Mock(return_value=docs)
results = self.service.search_relevant_documents("tenant-1", "违约付款", limit=5)
self.assertLessEqual(len(results), 5)
def test_build_summary_points_uses_excerpt_limit(self):
long_excerpt = "付款节点" * 50
points = self.service._build_summary_points(
"付款条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": long_excerpt, "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertTrue(any(item.endswith("...") for item in points if "付款节点" in item))
def test_search_results_include_created_at(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="doc.txt",
content_excerpt="违约责任应明确。",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "违约责任", limit=5)
self.assertIn("created_at", results[0])
def test_build_chat_payload_summary_has_at_most_four_points(self):
payload = self.service.build_chat_payload(
"违约付款保密续约",
[
{"id": f"doc-{index}", "knowledge_base_id": "kb-1", "file_name": f"doc-{index}.txt", "content": f"内容{index}", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
for index in range(6)
],
)
summary_lines = payload["chunks"][1].splitlines()
bullet_lines = [line for line in summary_lines if line.startswith("-")]
self.assertLessEqual(len(bullet_lines), 4)
def test_default_sources_have_none_score(self):
self.assertIsNone(self.service.format_default_sources()[0]["score"])
def test_extract_keywords_keeps_ascii_and_cjk(self):
keywords = self.service._extract_keywords("payment 付款")
self.assertIn("payment", keywords)
self.assertIn("付款", keywords)
def test_domain_bonus_only_applies_for_shared_terms(self):
bonus = self.service._calculate_domain_bonus("付款条款", "这个文本没有对应词")
self.assertEqual(bonus, 0.0)
def test_dedupe_results_uses_file_and_content_pair(self):
results = [
{"file_name": "same.txt", "content": "A", "score": 3},
{"file_name": "same.txt", "content": "B", "score": 2},
]
deduped = self.service._dedupe_results(results, 5)
self.assertEqual(len(deduped), 2)
def test_build_context_trims_to_three_sources(self):
context, sources = self.service.build_context_from_results([
{"id": f"doc-{index}", "knowledge_base_id": "kb-1", "file_name": f"doc-{index}.txt", "content": f"内容{index}", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}
for index in range(4)
])
self.assertEqual(len(sources), 3)
self.assertNotIn("[文档4]", context)
def test_generate_answer_with_context_without_hints(self):
answer = self.service.generate_answer_with_context("随机问题", "[文档1] 一般性内容")
self.assertIn("根据知识库中的相关文档", answer)
self.assertNotIn("建议重点关注:\n-", answer)
def test_format_default_chunks_with_multiple_hints(self):
chunks = self.service.format_default_chunks("违约付款")
self.assertIn("违约责任", chunks[-1])
self.assertIn("付款节点", chunks[-1])
def test_build_summary_points_without_results_still_uses_hints(self):
points = self.service._build_summary_points("续约条款", [])
self.assertTrue(points)
def test_search_relevant_documents_handles_empty_doc_content(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="empty.txt",
content_excerpt="",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "续约条款", limit=5)
self.assertEqual(results, [])
def test_answer_chunks_are_list_of_strings(self):
chunks = self.service.format_answer_chunks("正文", ["摘要"])
self.assertTrue(all(isinstance(item, str) for item in chunks))
def test_default_chunks_are_list_of_strings(self):
chunks = self.service.format_default_chunks("保密条款")
self.assertTrue(all(isinstance(item, str) for item in chunks))
def test_summary_points_use_excerpt_after_hints(self):
points = self.service._build_summary_points(
"付款条款",
[{"id": "doc-1", "knowledge_base_id": "kb-1", "file_name": "doc.txt", "content": "付款节点需明确且发票前置要求应写清。", "score": 1.0, "created_at": "2026-04-16T10:00:00+00:00"}],
)
self.assertGreaterEqual(len(points), 2)
def test_default_sources_content_is_user_friendly(self):
sources = self.service.format_default_sources()
self.assertIn("补充资料", sources[0]["content"])
def test_search_relevant_documents_returns_ranked_chunks(self):
mock_doc = TestKnowledgeBaseDocument(
id="doc-1",
knowledge_base_id="kb-1",
tenant_id="tenant-1",
file_name="ranked.txt",
content_excerpt="第一段是普通内容。第二段写明违约责任、付款节点和保密义务。第三段是其他内容。",
)
self.service.document_repository.list_all_for_tenant = Mock(return_value=[mock_doc])
results = self.service.search_relevant_documents("tenant-1", "违约付款保密", limit=5)
self.assertGreaterEqual(len(results), 1)
self.assertIn("违约", results[0]["content"])
if __name__ == "__main__":
unittest.main()