p6_walk_helpers.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from typing import Any
  4. from content_agent.business_modules import content_discovery, rule_judgment, source_seed
  5. from content_agent.business_modules.content_discovery import pattern_recall
  6. from content_agent.constants import RUNTIME_RECORD_SCHEMA_VERSION
  7. from content_agent.integrations.policy_json import JsonPolicyBundleStore
  8. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  9. from content_agent.record_payload import with_raw_payload
  10. from tests.gemini_helpers import FakeGeminiVideoClient
  11. from tests.p1_helpers import real_source_payload
  12. class FakeWalkPlatformClient:
  13. def __init__(self, fail_tag: bool = False, tags: list[str] | None = None) -> None:
  14. self.fail_tag = fail_tag
  15. self.tags = tags or ["#人物故事"]
  16. self.search_calls: list[dict[str, Any]] = []
  17. self.author_calls: list[dict[str, Any]] = []
  18. self.portrait_calls: list[str] = []
  19. def search(self, query: dict[str, Any]) -> list[dict[str, Any]]:
  20. self.search_calls.append(dict(query))
  21. method = query.get("search_query_generation_method")
  22. if method == "tag_query" and self.fail_tag:
  23. raise RuntimeError("tag search unavailable")
  24. if method == "tag_query":
  25. return [_platform_result(query, "7390000000000000201", "标签内容", [])]
  26. return []
  27. def fetch_author_works(self, query: dict[str, Any]) -> list[dict[str, Any]]:
  28. self.author_calls.append(dict(query))
  29. return [_platform_result(query, "7390000000000000301", "作者作品", [])]
  30. def fetch_account_fans_portrait(self, account_id: str) -> dict[str, Any]:
  31. # 强 50+ 画像(纯 50- 桶,占比60%/TGI200 → 50+子分高)→ 不改变 walk 既有 allow_walk/入池 行为。
  32. self.portrait_calls.append(account_id)
  33. return {
  34. "fans": {
  35. "age": {
  36. "data": {
  37. "50-": {"percentage": "60%", "preference": "200"},
  38. }
  39. }
  40. }
  41. }
  42. def build_initial_walk_context(tmp_path: Path, *, tags: list[str] | None = None) -> dict[str, Any]:
  43. run_id = "run_001"
  44. policy_run_id = "policy_run_001"
  45. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  46. runtime.prepare_run(run_id)
  47. seed = source_seed.run(run_id, policy_run_id, real_source_payload(), runtime)
  48. search_query = with_raw_payload(
  49. {
  50. "record_schema_version": RUNTIME_RECORD_SCHEMA_VERSION,
  51. "run_id": run_id,
  52. "policy_run_id": policy_run_id,
  53. "search_query_id": "q_001",
  54. "search_query": "人物故事",
  55. "search_query_generation_method": "item_single",
  56. "discovery_start_source": "pattern_itemset",
  57. "previous_discovery_step": "search_query_generated",
  58. "pattern_seed_ref": {"seed_term": "人物故事"},
  59. }
  60. )
  61. runtime.append_jsonl(run_id, "search_queries.jsonl", [search_query])
  62. initial_result = _platform_result(search_query, "7390000000000000001", "首轮内容", tags or ["#人物故事"])
  63. initial_result["has_more"] = True
  64. initial_result["next_cursor"] = "10"
  65. discovered = content_discovery.run(
  66. run_id,
  67. policy_run_id,
  68. [initial_result],
  69. seed["source_context"],
  70. runtime,
  71. )
  72. recalled = pattern_recall.run(
  73. run_id,
  74. policy_run_id,
  75. discovered["discovered_content_items"],
  76. discovered["content_media_records"],
  77. discovered["evidence_bundles"],
  78. seed["source_context"],
  79. runtime,
  80. FakeGeminiVideoClient(),
  81. )
  82. policy_bundle = JsonPolicyBundleStore(Path(".")).load_policy_bundle("V4")
  83. decisions = rule_judgment.run(
  84. run_id,
  85. policy_run_id,
  86. recalled["evidence_bundles"],
  87. policy_bundle,
  88. runtime,
  89. )
  90. return {
  91. "run_id": run_id,
  92. "policy_run_id": policy_run_id,
  93. "runtime": runtime,
  94. "source_context": seed["source_context"],
  95. "pattern_seed_pack": seed["pattern_seed_pack"],
  96. "search_queries": [search_query],
  97. "discovered_content_items": recalled["discovered_content_items"],
  98. "content_media_records": discovered["content_media_records"],
  99. "evidence_bundles": recalled["evidence_bundles"],
  100. "rule_decisions": decisions,
  101. "policy_bundle": policy_bundle,
  102. "gemini_video_client": FakeGeminiVideoClient(),
  103. }
  104. def set_v4_allow_walk(decision: dict[str, Any], allow_walk: bool) -> None:
  105. query_score = 80
  106. # M11 re-baseline:walk fixture(digg=100k,高共鸣占比)新平台分由 rule_judgment 实算为 88.41。
  107. # True 分支必须与实算一致,否则 final_output v4_explanation 校验报 platform_performance_score 不一致。
  108. # False 分支仅用于"显式拒绝游走"测试(不跑 validate_run),沿用低分占位即可。
  109. platform_score = 88.41 if allow_walk else 60
  110. total_score = round(query_score * 0.5 + platform_score * 0.5, 2)
  111. decision["decision_action"] = "ADD_TO_CONTENT_POOL"
  112. decision["decision_reason_code"] = "v4_query_and_platform_pass"
  113. decision["search_query_effect_status"] = "success"
  114. decision["score"] = total_score
  115. decision.setdefault("scorecard", {})["schema_version"] = "v4_scorecard.v1"
  116. decision["scorecard"]["query_relevance_score"] = query_score
  117. decision["scorecard"]["platform_performance_score"] = platform_score
  118. decision["scorecard"]["total_score"] = total_score
  119. replay = decision.setdefault("decision_replay_data", {})
  120. replay["allow_walk"] = allow_walk
  121. replay["allow_walk_reason"] = (
  122. "query>=65/platform>=65/score>=65"
  123. if allow_walk
  124. else "v4_query_and_platform_pass"
  125. )
  126. replay["walk_gate_snapshot"] = {
  127. "query_relevance_score": query_score,
  128. "platform_performance_score": platform_score,
  129. "score": total_score,
  130. }
  131. def _platform_result(
  132. query: dict[str, Any],
  133. platform_content_id: str,
  134. description: str,
  135. tags: list[str],
  136. ) -> dict[str, Any]:
  137. return {
  138. "content_discovery_id": f"{query['search_query_id']}_content_{platform_content_id[-3:]}",
  139. "search_query_id": query["search_query_id"],
  140. "platform": "douyin",
  141. "platform_content_id": platform_content_id,
  142. "platform_content_format": "video",
  143. "description": description,
  144. "platform_author_id": "MS4wLjABAAAA001",
  145. "author_display_name": "作者",
  146. "statistics": {
  147. # M11:平台分改 ratio 后,光大点赞不够;给"高共鸣"占比(转/赞30%、藏/赞50%、评/赞8%)
  148. # 让该 fixture 在新打分下平台分 ~88 ≥ 65 门槛(walk 测试本意:成功内容能继续游走)。
  149. "digg_count": 100_000,
  150. "comment_count": 8_000,
  151. "share_count": 30_000,
  152. "collect_count": 50_000,
  153. },
  154. "tags": tags,
  155. "score": 72,
  156. "risk_level": "low",
  157. "availability": "available",
  158. "discovery_relation": "fake_walk",
  159. "discovery_start_source": query.get("discovery_start_source", "pattern_itemset"),
  160. "previous_discovery_step": query.get("previous_discovery_step", "search_query_direct"),
  161. "content_metadata_source": "fake_platform_search",
  162. "platform_raw_payload": {"content_id": platform_content_id},
  163. }