test_search_intent.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import copy
  2. import pytest
  3. from content_agent.business_modules import search_intent
  4. from content_agent.errors import ContentAgentError
  5. from content_agent.integrations.query_prompt_config import DEFAULT_PROFILE
  6. from content_agent.run_service import RunService
  7. from content_agent.schemas import RunStartRequest
  8. from tests.p1_helpers import FakeQueryVariantClient, REAL_SOURCE_FIXTURE
  9. FORBIDDEN_FIXED_BUSINESS_TERMS = [
  10. "\u8d2a\u8150",
  11. "\u57fa\u5c42\u516c\u804c\u4eba\u5458",
  12. "\u6848\u4f8b",
  13. "\u89e3\u8bfb",
  14. "\u8b66\u793a",
  15. ]
  16. class _Runtime:
  17. def __init__(self):
  18. self.rows = {}
  19. def append_jsonl(self, _run_id, filename, rows):
  20. self.rows[filename] = rows
  21. def _seed_pack():
  22. return {
  23. "seed_terms": ["中医养生"],
  24. "itemset_items": ["补气血"],
  25. "category_bindings": [{"category_id": "c1"}],
  26. "element_bindings": [
  27. {
  28. "element_id": "e1",
  29. "category_id": "c1",
  30. "sample_elements": [
  31. {
  32. "id": "point_1",
  33. "name": "食疗补气血",
  34. "point_type": "目的点",
  35. "point_text": "分享气血食疗方法",
  36. "post_id": "p1",
  37. },
  38. {
  39. "id": "point_2",
  40. "name": "八段锦",
  41. "point_type": "灵感点",
  42. "point_text": "办公室八段锦",
  43. "post_id": "p2",
  44. },
  45. {
  46. "id": "point_3",
  47. "name": "日常保健",
  48. "point_type": "关键点",
  49. "point_text": "不应该进入搜索词",
  50. "post_id": "p3",
  51. },
  52. ],
  53. }
  54. ],
  55. "pattern_source_system": "pg_pattern_v2",
  56. "pattern_execution_id": 1987,
  57. "mining_config_id": 58,
  58. "source_post_id": "60219550",
  59. "matched_post_ids": ["60219550"],
  60. "itemset_ids": [1607977],
  61. "support": 0.2,
  62. "absolute_support": 31,
  63. "confidence": 0.8,
  64. }
  65. def test_search_seed_and_queries_do_not_inject_fixed_business_terms(tmp_path):
  66. service = RunService(
  67. runtime_root=tmp_path / "runtime" / "v1",
  68. query_variant_client=FakeQueryVariantClient(
  69. {
  70. "爱国情感": "家国叙事素材",
  71. "人物故事": "榜样人物素材",
  72. }
  73. ),
  74. )
  75. state = service.start_run(
  76. RunStartRequest(platform_mode="mock", source=str(REAL_SOURCE_FIXTURE), strategy_version="V1")
  77. )
  78. run_id = state["run_id"]
  79. pattern_seed_pack = service.read_json(run_id, "pattern_seed_pack.json")
  80. queries = service.read_jsonl(run_id, "search_queries.jsonl")
  81. p2_queries = [
  82. row
  83. for row in queries
  84. if row["search_query_generation_method"] in {"item_single", "llm_variant"}
  85. ]
  86. assert pattern_seed_pack["seed_terms"] == ["爱国情感", "人物故事"]
  87. assert [row["search_query_id"] for row in p2_queries] == ["q_001", "q_002", "q_003", "q_004"]
  88. assert [row["search_query"] for row in p2_queries] == [
  89. "爱国情感",
  90. "家国叙事素材",
  91. "人物故事",
  92. "榜样人物素材",
  93. ]
  94. assert [row["search_query_generation_method"] for row in p2_queries] == [
  95. "item_single",
  96. "llm_variant",
  97. "item_single",
  98. "llm_variant",
  99. ]
  100. assert p2_queries[1]["llm_variant_of"] == "q_001"
  101. assert p2_queries[3]["llm_variant_of"] == "q_003"
  102. for value in [
  103. *pattern_seed_pack["seed_terms"],
  104. *(row["search_query"] for row in p2_queries),
  105. ]:
  106. assert not any(term in value for term in FORBIDDEN_FIXED_BUSINESS_TERMS)
  107. def test_search_queries_preserve_source_terms_for_replay(tmp_path):
  108. service = RunService(
  109. runtime_root=tmp_path / "runtime" / "v1",
  110. query_variant_client=FakeQueryVariantClient(
  111. {
  112. "爱国情感": "家国叙事素材",
  113. "人物故事": "榜样人物素材",
  114. }
  115. ),
  116. )
  117. state = service.start_run(
  118. RunStartRequest(platform_mode="mock", source=str(REAL_SOURCE_FIXTURE), strategy_version="V1")
  119. )
  120. queries = service.read_jsonl(state["run_id"], "search_queries.jsonl")
  121. p2_queries = [
  122. query
  123. for query in queries
  124. if query["search_query_generation_method"] in {"item_single", "llm_variant"}
  125. ]
  126. expected_source_terms = [["爱国情感"], ["爱国情感"], ["人物故事"], ["人物故事"]]
  127. for query, source_terms in zip(p2_queries, expected_source_terms, strict=True):
  128. assert query["query_source_terms"] == source_terms
  129. assert query["query_source_fields"] == ["seed_terms"]
  130. assert query["raw_payload"]["query_source_terms"] == source_terms
  131. assert query["pattern_seed_ref"]["source_field"] == "seed_terms"
  132. assert query["pattern_seed_ref"]["seed_term"] == source_terms[0]
  133. assert query["raw_payload"]["pattern_seed_ref"]["seed_term"] == source_terms[0]
  134. llm_queries = [
  135. query
  136. for query in p2_queries
  137. if query["search_query_generation_method"] == "llm_variant"
  138. ]
  139. assert len(llm_queries) == 2
  140. for query in llm_queries:
  141. assert query["raw_payload"]["llm_prompt_version"] == "fake-query-prompt-v1"
  142. assert query["raw_payload"]["llm_generation_model"] == "fake-query-model"
  143. assert query["raw_payload"]["llm_input_evidence"]["source_field"] == "seed_terms"
  144. assert query["raw_payload"]["llm_input_evidence"]["itemset_items"]
  145. def test_search_intent_custom_evidence_fields_whitelist():
  146. client = FakeQueryVariantClient({"中医养生": "气血食疗"})
  147. client.profile = copy.deepcopy(DEFAULT_PROFILE)
  148. client.profile["evidence_fields"] = ["seed_term", "support"]
  149. runtime = _Runtime()
  150. queries = search_intent.run("run_1", "policy_1", _seed_pack(), runtime, client)
  151. llm_query = [row for row in queries if row["search_query_generation_method"] == "llm_variant"][0]
  152. assert list(llm_query["llm_input_evidence"].keys()) == ["seed_term", "support"]
  153. assert list(llm_query["raw_payload"]["llm_input_evidence"].keys()) == ["seed_term", "support"]
  154. assert llm_query["query_source_fields"] == ["seed_terms"]
  155. def test_v4_search_intent_uses_direct_sources_without_llm_variant():
  156. client = FakeQueryVariantClient({"中医养生": "气血食疗"})
  157. runtime = _Runtime()
  158. queries = search_intent.run(
  159. "run_1",
  160. "policy_1",
  161. _seed_pack(),
  162. runtime,
  163. client,
  164. strategy_version="V4",
  165. )
  166. assert client.calls == []
  167. assert [row["search_query"] for row in queries] == [
  168. "中医养生",
  169. "气血食疗方法",
  170. "办公室八段锦",
  171. "食疗补气血",
  172. "八段锦",
  173. "日常保健",
  174. ]
  175. assert [row["search_query_generation_method"] for row in queries] == [
  176. "seed_term",
  177. "piaoquan_topic_point",
  178. "piaoquan_topic_point",
  179. "category_leaf_element",
  180. "category_leaf_element",
  181. "category_leaf_element",
  182. ]
  183. assert all("llm_variant_of" not in row for row in queries)
  184. assert runtime.rows["search_queries.jsonl"] == queries
  185. first_point = queries[1]
  186. assert first_point["pattern_seed_ref"]["query_source_type"] == "piaoquan_point_text"
  187. assert first_point["pattern_seed_ref"]["query_source_text"] == "气血食疗方法"
  188. assert first_point["pattern_seed_ref"]["query_source_rank"] == 1
  189. assert first_point["query_source_refs"][0]["source_ref"]["point_type"] == "目的点"
  190. assert first_point["raw_payload"]["query_source_refs"][0]["query_source_text"] == "气血食疗方法"
  191. assert queries[3]["pattern_seed_ref"]["query_source_type"] == "category_terminal_element"
  192. assert queries[3]["raw_payload"]["query_source_refs"][0]["generation_method"] == "category_leaf_element"
  193. def test_v4_search_intent_dedupes_by_direct_source_priority():
  194. client = FakeQueryVariantClient({"中医养生": "气血食疗"})
  195. runtime = _Runtime()
  196. seed_pack = _seed_pack()
  197. seed_pack["seed_terms"] = ["中医养生", "气血食疗方法"]
  198. seed_pack["element_bindings"][0]["sample_elements"][0]["point_text"] = "分享气血食疗方法"
  199. seed_pack["element_bindings"][0]["sample_elements"][0]["name"] = "气血食疗方法"
  200. queries = search_intent.run(
  201. "run_1",
  202. "policy_1",
  203. seed_pack,
  204. runtime,
  205. client,
  206. strategy_version="V4",
  207. )
  208. assert client.calls == []
  209. matching = [row for row in queries if row["search_query"] == "气血食疗方法"]
  210. assert len(matching) == 1
  211. assert matching[0]["search_query_generation_method"] == "seed_term"
  212. assert matching[0]["pattern_seed_ref"]["query_source_type"] == "seed_term"
  213. def test_search_intent_custom_generic_filter_blocks_query():
  214. client = FakeQueryVariantClient({"中医养生": "禁用泛词"})
  215. client.profile = copy.deepcopy(DEFAULT_PROFILE)
  216. client.profile["generic_filter"] = {"queries": ["禁用泛词"], "tokens": []}
  217. with pytest.raises(ContentAgentError) as exc:
  218. search_intent.run("run_1", "policy_1", _seed_pack(), _Runtime(), client)
  219. assert exc.value.error_code == "QUERY_GENERATION_FAILED"
  220. assert exc.value.detail["reason"] == "llm_variant_generic"
  221. def test_search_intent_rejects_unsupported_variants_per_seed():
  222. client = FakeQueryVariantClient({"中医养生": "气血食疗"})
  223. client.profile = copy.deepcopy(DEFAULT_PROFILE)
  224. client.profile["variants_per_seed"] = 2
  225. with pytest.raises(ContentAgentError) as exc:
  226. search_intent.run("run_1", "policy_1", _seed_pack(), _Runtime(), client)
  227. assert exc.value.error_code == "QUERY_GENERATION_FAILED"
  228. assert exc.value.detail == {"reason": "variants_per_seed_unsupported", "variants_per_seed": 2}