4a93bd1c44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1645 lines
71 KiB
Python
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() |