2026-07-24 07:37:22 +00:00
|
|
|
import unittest
|
2026-08-03 05:59:52 +00:00
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from unittest.mock import AsyncMock
|
2026-07-24 07:37:22 +00:00
|
|
|
|
|
|
|
|
from app.api.v1.chat import _canonicalize_message_citations, _compact_cited_refs
|
|
|
|
|
from app.services.rag_service import RAGService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChatCitationTest(unittest.TestCase):
|
|
|
|
|
def test_duplicate_file_citations_are_renumbered_once(self):
|
|
|
|
|
content, refs = _canonicalize_message_citations(
|
|
|
|
|
"泰坦属于土星的卫星[2][4][5]。",
|
|
|
|
|
[
|
|
|
|
|
{"citation_id": 2, "file_path": "内容导航.md", "excerpt": "片段一"},
|
|
|
|
|
{"citation_id": 4, "file_path": "内容导航.md", "excerpt": "片段二"},
|
|
|
|
|
{"citation_id": 5, "file_path": "内容导航.md", "excerpt": "片段三"},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(content, "泰坦属于土星的卫星[1]。")
|
|
|
|
|
self.assertEqual(len(refs), 1)
|
|
|
|
|
self.assertEqual(refs[0]["citation_id"], 1)
|
|
|
|
|
self.assertEqual(refs[0]["excerpt"], "片段一\n\n片段二\n\n片段三")
|
|
|
|
|
|
|
|
|
|
def test_overlapping_blocks_are_not_duplicated(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
RAGService._merge_text_blocks("泰坦属于土星", "泰坦属于土星"),
|
|
|
|
|
"泰坦属于土星",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_single_used_reference_is_compacted_to_one(self):
|
|
|
|
|
content, refs = _compact_cited_refs(
|
|
|
|
|
"泰坦是土星的卫星[3],土卫二也属于土星[3]。",
|
|
|
|
|
[
|
|
|
|
|
{"citation_id": 1, "file_path": "其他一.md"},
|
|
|
|
|
{"citation_id": 2, "file_path": "其他二.md"},
|
|
|
|
|
{"citation_id": 3, "file_path": "内容导航.md"},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(content, "泰坦是土星的卫星[1],土卫二也属于土星[1]。")
|
|
|
|
|
self.assertEqual(refs, [{
|
|
|
|
|
"citation_id": 1,
|
|
|
|
|
"file_path": "内容导航.md",
|
|
|
|
|
"anchor_text": "",
|
|
|
|
|
"excerpt": "",
|
|
|
|
|
}])
|
|
|
|
|
|
|
|
|
|
def test_used_references_follow_first_appearance_order(self):
|
|
|
|
|
content, refs = _compact_cited_refs(
|
|
|
|
|
"先使用第三份[3],再使用第一份[1]。",
|
|
|
|
|
[
|
|
|
|
|
{"citation_id": 1, "file_path": "第一份.md"},
|
|
|
|
|
{"citation_id": 2, "file_path": "第二份.md"},
|
|
|
|
|
{"citation_id": 3, "file_path": "第三份.md"},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(content, "先使用第三份[1],再使用第一份[2]。")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
[(ref["citation_id"], ref["file_path"]) for ref in refs],
|
|
|
|
|
[(1, "第三份.md"), (2, "第一份.md")],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 05:59:52 +00:00
|
|
|
class RAGConversationContextTest(unittest.IsolatedAsyncioTestCase):
|
|
|
|
|
async def test_previous_assistant_answer_is_not_sent_to_model(self):
|
|
|
|
|
db = AsyncMock()
|
|
|
|
|
db.execute.return_value = SimpleNamespace(
|
|
|
|
|
scalar_one_or_none=lambda: SimpleNamespace(config_id=1)
|
|
|
|
|
)
|
|
|
|
|
history = [
|
|
|
|
|
{"role": "user", "content": "包含了哪几次阿波罗计划?"},
|
|
|
|
|
{"role": "assistant", "content": "上一轮完整答案不应再次发送给模型。"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
_, system_prompt, messages = await RAGService._prepare_generation(
|
|
|
|
|
db,
|
|
|
|
|
"项目中的文档包含哪些土星的卫星?",
|
|
|
|
|
1,
|
|
|
|
|
[],
|
|
|
|
|
history,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(messages, [{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": "项目中的文档包含哪些土星的卫星?",
|
|
|
|
|
}])
|
|
|
|
|
self.assertIn("包含了哪几次阿波罗计划?", system_prompt)
|
|
|
|
|
self.assertNotIn("上一轮完整答案不应再次发送给模型。", system_prompt)
|
|
|
|
|
self.assertIn("禁止复述、总结或继续回答先前问题", system_prompt)
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|