test_knowledge_tool.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import json
  2. from datetime import UTC, datetime
  3. import pytest
  4. from zbt.core.errors import AppError
  5. from zbt.domains.agent.tools import build_agent_tool_registry
  6. from zbt.domains.catalog.repository import InMemoryCatalogRepository
  7. from zbt.domains.catalog.service import ProductCatalogService
  8. from zbt.domains.enrollment.repository import InMemoryEnrollmentRepository
  9. from zbt.domains.enrollment.service import EnrollmentService
  10. from zbt.domains.identity.models import H5User
  11. from zbt.domains.knowledge.index import InMemoryKnowledgeIndex
  12. from zbt.domains.knowledge.repository import InMemoryKnowledgeRepository
  13. from zbt.domains.knowledge.service import KnowledgeService
  14. from zbt.harness.policy import HarnessPolicyEngine
  15. from zbt.harness.tooling import ToolExecutionContext
  16. NOW = datetime(2026, 7, 27, 4, 0, tzinfo=UTC)
  17. def test_customer_knowledge_tool_returns_published_evidence_with_source() -> None:
  18. catalog = ProductCatalogService(InMemoryCatalogRepository(), lambda: NOW)
  19. enrollment = EnrollmentService(
  20. InMemoryEnrollmentRepository(),
  21. catalog,
  22. lambda: NOW,
  23. "x" * 32,
  24. )
  25. knowledge = KnowledgeService(
  26. InMemoryKnowledgeRepository(),
  27. lambda: NOW,
  28. InMemoryKnowledgeIndex(),
  29. )
  30. document = knowledge.create_document(
  31. title="安心医疗险保险条款",
  32. document_type="INSURANCE_TERMS",
  33. source_name="安心医疗险条款.pdf",
  34. product_code="MED-BASIC",
  35. content="疾病医疗等待期为30天,意外伤害不受等待期限制。",
  36. created_by="admin-01",
  37. )
  38. knowledge.index_document(document["document_id"])
  39. knowledge.test_search(document["document_id"], "医疗险等待期是多少天?")
  40. knowledge.publish_document(document["document_id"])
  41. registry = build_agent_tool_registry(catalog, enrollment, knowledge=knowledge)
  42. context = ToolExecutionContext(
  43. persona="customer",
  44. h5_user=H5User(
  45. id="01H5USER000000000000000001",
  46. mobile="18800000001",
  47. mobile_masked="188****0001",
  48. display_name="体验用户",
  49. status="ACTIVE",
  50. created_at=NOW,
  51. ),
  52. )
  53. tool = registry.build(
  54. names=("search_insurance_knowledge",),
  55. context=context,
  56. policy_engine=HarnessPolicyEngine(),
  57. )[0]
  58. result = json.loads(tool.invoke({"query": "医疗险等待期是多少天?", "limit": 3}))
  59. assert result["data"]["total"] == 1
  60. assert result["data"]["items"][0]["title"] == "安心医疗险保险条款"
  61. assert result["data"]["items"][0]["source_name"] == "安心医疗险条款.pdf"
  62. assert result["data"]["items"][0]["document_version"] == 1
  63. assert result["data"]["answer_constraints"]["direct_evidence_only"] is True
  64. assert result["data"]["answer_constraints"]["no_scenario_expansion"] is True
  65. assert result["data"]["answer_constraints"]["no_numeric_inference"] is True
  66. assert "30天" in result["summary"]
  67. assert result["blocks"][0]["type"] == "knowledge_sources"
  68. assert result["blocks"][0]["items"][0]["source_name"] == "安心医疗险条款.pdf"
  69. assert [execution.name for execution in context.executions] == [
  70. "search_insurance_knowledge"
  71. ]
  72. def test_agentic_knowledge_tool_executes_rewrites_with_metadata_filters() -> None:
  73. catalog = ProductCatalogService(InMemoryCatalogRepository(), lambda: NOW)
  74. enrollment = EnrollmentService(
  75. InMemoryEnrollmentRepository(),
  76. catalog,
  77. lambda: NOW,
  78. "x" * 32,
  79. )
  80. knowledge = KnowledgeService(
  81. InMemoryKnowledgeRepository(),
  82. lambda: NOW,
  83. InMemoryKnowledgeIndex(),
  84. )
  85. for title, product_code, content in (
  86. (
  87. "安心医疗险保险条款",
  88. "MED-BASIC",
  89. "疾病医疗等待期为30天,意外伤害不受等待期限制。",
  90. ),
  91. (
  92. "家庭意外险常见问题",
  93. "ACC-FAMILY",
  94. "意外医疗不设置疾病等待期,具体责任以意外险条款为准。",
  95. ),
  96. ):
  97. document = knowledge.create_document(
  98. title=title,
  99. document_type="INSURANCE_TERMS",
  100. source_name=f"{title}.md",
  101. product_code=product_code,
  102. content=content,
  103. created_by="admin-01",
  104. )
  105. knowledge.index_document(document["document_id"])
  106. knowledge.test_search(document["document_id"], content[:20])
  107. knowledge.publish_document(document["document_id"])
  108. registry = build_agent_tool_registry(catalog, enrollment, knowledge=knowledge)
  109. context = ToolExecutionContext(
  110. persona="customer",
  111. h5_user=H5User(
  112. id="01H5USER000000000000000001",
  113. mobile="18800000001",
  114. mobile_masked="188****0001",
  115. display_name="体验用户",
  116. status="ACTIVE",
  117. created_at=NOW,
  118. ),
  119. )
  120. tool = registry.build(
  121. names=("search_insurance_knowledge",),
  122. context=context,
  123. policy_engine=HarnessPolicyEngine(),
  124. )[0]
  125. result = json.loads(
  126. tool.invoke(
  127. {
  128. "query": "安心医疗险多久后可以报销疾病医疗?",
  129. "alternate_queries": [
  130. "安心医疗险疾病等待期",
  131. "MED-BASIC等待期天数",
  132. ],
  133. "product_code": "MED-BASIC",
  134. "document_types": ["INSURANCE_TERMS"],
  135. "limit": 3,
  136. }
  137. )
  138. )
  139. plan = result["data"]["agentic_retrieval"]
  140. assert plan["executed_queries"] == [
  141. "安心医疗险多久后可以报销疾病医疗?",
  142. "安心医疗险疾病等待期",
  143. "MED-BASIC等待期天数",
  144. ]
  145. assert plan["filters"] == {
  146. "product_code": "MED-BASIC",
  147. "document_types": ["INSURANCE_TERMS"],
  148. }
  149. assert plan["attempt"] == 1
  150. assert result["data"]["items"]
  151. assert {
  152. item["product_code"] for item in result["data"]["items"]
  153. } == {"MED-BASIC"}
  154. def test_agentic_knowledge_tool_limits_iterative_retrieval_to_three_rounds() -> None:
  155. catalog = ProductCatalogService(InMemoryCatalogRepository(), lambda: NOW)
  156. enrollment = EnrollmentService(
  157. InMemoryEnrollmentRepository(),
  158. catalog,
  159. lambda: NOW,
  160. "x" * 32,
  161. )
  162. knowledge = KnowledgeService(
  163. InMemoryKnowledgeRepository(),
  164. lambda: NOW,
  165. InMemoryKnowledgeIndex(),
  166. )
  167. context = ToolExecutionContext(
  168. persona="customer",
  169. h5_user=H5User(
  170. id="01H5USER000000000000000001",
  171. mobile="18800000001",
  172. mobile_masked="188****0001",
  173. display_name="体验用户",
  174. status="ACTIVE",
  175. created_at=NOW,
  176. ),
  177. )
  178. tool = build_agent_tool_registry(
  179. catalog,
  180. enrollment,
  181. knowledge=knowledge,
  182. ).build(
  183. names=("search_insurance_knowledge",),
  184. context=context,
  185. policy_engine=HarnessPolicyEngine(),
  186. )[0]
  187. for index in range(3):
  188. result = json.loads(tool.invoke({"query": f"第{index + 1}轮检索问题"}))
  189. assert result["data"]["agentic_retrieval"]["attempt"] == index + 1
  190. with pytest.raises(AppError) as error:
  191. tool.invoke({"query": "第四轮检索问题"})
  192. assert error.value.code == "AGENTIC_RETRIEVAL_LIMIT_REACHED"