test_p8_strategy_review.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. from content_agent.business_modules import learning_review
  2. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  3. def test_strategy_review_outputs_p8_contract_with_missing_feedback(tmp_path):
  4. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  5. run_id = "run_001"
  6. policy_run_id = "policy_001"
  7. runtime.prepare_run(run_id)
  8. _write_minimal_runtime(runtime, run_id, policy_run_id)
  9. review = learning_review.run(run_id, policy_run_id, runtime)
  10. assert review["data_window"]["scope"] == "single_run"
  11. assert review["metric_summary"]["search_query_count"] == 4
  12. assert review["metric_summary"]["pooled_content_count"] == 1
  13. assert review["query_review"]["effective_queries"][0]["search_query"] == "银发旅行"
  14. assert review["query_review"]["review_queries"][0]["search_query_effect_status"] == "pending"
  15. assert review["rule_review"]["decision_distribution"]["ADD_TO_CONTENT_POOL"] == 1
  16. assert review["walk_review"]["walk_action_count"] == 1
  17. assert review["asset_review"]["content_asset_count"] == 1
  18. assert review["asset_review"]["search_clue_assets"]["promoted_count"] == 1
  19. assert review["performance_feedback"]["performance_feedback_status"] == "missing"
  20. assert review["recommendations"]
  21. assert all(item["requires_human_approval"] is True for item in review["recommendations"])
  22. written = runtime.read_json(run_id, "strategy_review.json")
  23. assert written["raw_payload"]["recommendations"] == review["recommendations"]
  24. def test_strategy_review_filters_legacy_contract_values(tmp_path):
  25. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  26. run_id = "run_legacy"
  27. policy_run_id = "policy_legacy"
  28. runtime.prepare_run(run_id)
  29. _write_minimal_runtime(runtime, run_id, policy_run_id)
  30. runtime.append_jsonl(
  31. run_id,
  32. "search_clues.jsonl",
  33. [
  34. {
  35. "run_id": run_id,
  36. "policy_run_id": policy_run_id,
  37. "clue_id": "clue_old",
  38. "search_query_id": "q_old",
  39. "search_query": "旧口径",
  40. "search_query_effect_status": "weak_" + "effective",
  41. "result_count": 10,
  42. "pooled_content_count": 10,
  43. "review_content_count": 0,
  44. "pending_content_count": 0,
  45. "rejected_content_count": 0,
  46. "raw_payload": {},
  47. }
  48. ],
  49. )
  50. runtime.append_jsonl(
  51. run_id,
  52. "rule_decisions.jsonl",
  53. [
  54. {
  55. "run_id": run_id,
  56. "policy_run_id": policy_run_id,
  57. "decision_id": "decision_old",
  58. "decision_target_type": "content",
  59. "decision_target_id": "content_old",
  60. "decision_action": "HOLD_CONTENT_" + "PENDING",
  61. "decision_reason_code": "old_status",
  62. "search_query_effect_status": "block" + "ed",
  63. "raw_payload": {},
  64. }
  65. ],
  66. )
  67. review = learning_review.run(run_id, policy_run_id, runtime)
  68. assert "weak_" + "effective" not in {
  69. item["search_query_effect_status"]
  70. for bucket in review["query_review"].values()
  71. if isinstance(bucket, list)
  72. for item in bucket
  73. }
  74. assert "HOLD_CONTENT_" + "PENDING" not in review["rule_review"]["decision_distribution"]
  75. def test_strategy_review_builds_v4_single_run_summary(tmp_path):
  76. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  77. run_id = "run_v4"
  78. policy_run_id = "policy_v4"
  79. runtime.prepare_run(run_id)
  80. _write_v4_runtime(runtime, run_id, policy_run_id)
  81. review = learning_review.run(run_id, policy_run_id, runtime)
  82. v4_summary = review["v4_summary"]
  83. assert v4_summary["schema_version"] == "v4_strategy_review_summary.v1"
  84. assert v4_summary["summary_status"] == "available"
  85. assert v4_summary["v4_decision_count"] == 4
  86. assert v4_summary["score_buckets"] == {
  87. "pool": 1,
  88. "review": 1,
  89. "reject": 1,
  90. "technical_retry": 1,
  91. "unknown": 0,
  92. }
  93. assert v4_summary["missing_observable_fields"] == [
  94. {"field": "statistics.play_count", "reason": "natural_platform_missing", "count": 1},
  95. {"field": "statistics.share_count", "reason": "runtime_missing", "count": 1},
  96. ]
  97. assert v4_summary["technical_retry"]["count"] == 1
  98. assert v4_summary["allow_walk_distribution"] == {
  99. "allowed": 1,
  100. "denied": 3,
  101. "missing": 0,
  102. }
  103. assert v4_summary["walk_gate_review"]["v4_gate_distribution"]["allowed"] == 1
  104. assert v4_summary["walk_gate_review"]["v4_gate_distribution"]["denied"] == 1
  105. assert v4_summary["walk_gate_review"]["deny_reasons"] == [
  106. {"reason_code": "v4_allow_walk_denied", "count": 1}
  107. ]
  108. assert review["walk_review"]["deny_reasons"] == [
  109. {"reason_code": "v4_allow_walk_denied", "count": 1}
  110. ]
  111. assert runtime.read_json(run_id, "strategy_review.json")["raw_payload"]["v4_summary"]
  112. def _write_minimal_runtime(runtime, run_id: str, policy_run_id: str) -> None:
  113. runtime.write_json(
  114. run_id,
  115. "final_output.json",
  116. {
  117. "schema_version": "runtime_record.v1",
  118. "run_id": run_id,
  119. "policy_run_id": policy_run_id,
  120. "policy": {
  121. "strategy_version": "V1",
  122. "policy_bundle_id": "douyin_policy_bundle_v1",
  123. "rule_pack_id": "douyin_content_rules_v1",
  124. "rule_pack_version": "V1",
  125. },
  126. "walk_strategy": {"walk_strategy_version": "V1.0"},
  127. "content_assets": [
  128. {
  129. "content_asset_id": "asset_001",
  130. "platform": "douyin",
  131. "platform_content_id": "content_001",
  132. "source_path_record_ids": ["path_001"],
  133. "decision_ids": ["decision_001"],
  134. }
  135. ],
  136. "author_assets": [{"author_asset_id": "author_001"}],
  137. "summary": {
  138. "search_query_count": 4,
  139. "discovered_content_count": 3,
  140. "pooled_content_count": 1,
  141. "review_content_count": 1,
  142. "rejected_content_count": 1,
  143. },
  144. },
  145. )
  146. runtime.append_jsonl(
  147. run_id,
  148. "search_clues.jsonl",
  149. [
  150. _clue(run_id, policy_run_id, "clue_001", "q_001", "银发旅行", "success", 1),
  151. _clue(run_id, policy_run_id, "clue_002", "q_002", "怀旧故事", "pending", 0),
  152. _clue(run_id, policy_run_id, "clue_003", "q_003", "广场舞", "failed", 0),
  153. _clue(run_id, policy_run_id, "clue_004", "q_004", "保健品", "rule_blocked", 0),
  154. ],
  155. )
  156. runtime.append_jsonl(
  157. run_id,
  158. "rule_decisions.jsonl",
  159. [
  160. _decision(
  161. run_id,
  162. policy_run_id,
  163. "decision_001",
  164. "content_001",
  165. "ADD_TO_CONTENT_POOL",
  166. "passed",
  167. "success",
  168. search_query_id="q_001",
  169. ),
  170. _decision(run_id, policy_run_id, "decision_002", "content_002", "KEEP_CONTENT_FOR_REVIEW", "needs_more_evidence", "pending"),
  171. _decision(run_id, policy_run_id, "decision_003", "content_003", "REJECT_CONTENT", "missing_content_portrait", "failed"),
  172. ],
  173. )
  174. runtime.append_jsonl(
  175. run_id,
  176. "discovered_content_items.jsonl",
  177. [
  178. {
  179. "run_id": run_id,
  180. "policy_run_id": policy_run_id,
  181. "content_discovery_id": "discovery_001",
  182. "search_query_id": "q_001",
  183. "platform": "douyin",
  184. "platform_content_id": "content_001",
  185. "raw_payload": {},
  186. }
  187. ],
  188. )
  189. runtime.append_jsonl(
  190. run_id,
  191. "source_path_records.jsonl",
  192. [
  193. {
  194. "run_id": run_id,
  195. "policy_run_id": policy_run_id,
  196. "source_path_record_id": "path_001",
  197. "source_path_type": "decision_to_asset",
  198. "from_node_type": "rule_decision",
  199. "from_node_id": "decision_001",
  200. "to_node_type": "content_asset",
  201. "to_node_id": "asset_001",
  202. "decision_id": "decision_001",
  203. "raw_payload": {},
  204. }
  205. ],
  206. )
  207. runtime.append_jsonl(
  208. run_id,
  209. "walk_actions.jsonl",
  210. [
  211. {
  212. "run_id": run_id,
  213. "policy_run_id": policy_run_id,
  214. "walk_action_id": "wa_001",
  215. "edge_id": "query_next_page",
  216. "walk_action": "fetch_next_page",
  217. "walk_status": "success",
  218. "raw_payload": {},
  219. }
  220. ],
  221. )
  222. def _clue(run_id, policy_run_id, clue_id, query_id, query, status, pooled_count):
  223. return {
  224. "run_id": run_id,
  225. "policy_run_id": policy_run_id,
  226. "clue_id": clue_id,
  227. "search_query_id": query_id,
  228. "search_query": query,
  229. "search_query_effect_status": status,
  230. "result_count": 1,
  231. "pooled_content_count": pooled_count,
  232. "review_content_count": 1 if status == "pending" else 0,
  233. "pending_content_count": 1 if status == "pending" else 0,
  234. "rejected_content_count": 1 if status in {"failed", "rule_blocked"} else 0,
  235. "raw_payload": {},
  236. }
  237. def _decision(
  238. run_id,
  239. policy_run_id,
  240. decision_id,
  241. target_id,
  242. action,
  243. reason,
  244. effect_status,
  245. search_query_id=None,
  246. ):
  247. return {
  248. "run_id": run_id,
  249. "policy_run_id": policy_run_id,
  250. "decision_id": decision_id,
  251. "decision_target_type": "content",
  252. "decision_target_id": target_id,
  253. "decision_action": action,
  254. "decision_reason_code": reason,
  255. "search_query_effect_status": effect_status,
  256. "source_evidence": {"search_query_id": search_query_id} if search_query_id else {},
  257. "raw_payload": {},
  258. }
  259. def _write_v4_runtime(runtime, run_id: str, policy_run_id: str) -> None:
  260. runtime.write_json(
  261. run_id,
  262. "final_output.json",
  263. {
  264. "schema_version": "runtime_record.v1",
  265. "run_id": run_id,
  266. "policy_run_id": policy_run_id,
  267. "policy": {"strategy_version": "V4"},
  268. "walk_strategy": {"walk_strategy_version": "V4"},
  269. "content_assets": [{"content_asset_id": "asset_001"}],
  270. "author_assets": [],
  271. "summary": {
  272. "search_query_count": 4,
  273. "discovered_content_count": 4,
  274. "pooled_content_count": 1,
  275. "review_content_count": 2,
  276. "rejected_content_count": 1,
  277. },
  278. },
  279. )
  280. runtime.append_jsonl(
  281. run_id,
  282. "search_clues.jsonl",
  283. [
  284. _clue(run_id, policy_run_id, "clue_v4", "q_v4", "父爱感悟", "success", 1)
  285. ],
  286. )
  287. runtime.append_jsonl(
  288. run_id,
  289. "rule_decisions.jsonl",
  290. [
  291. _v4_decision(run_id, policy_run_id, "decision_pool", "content_pool", "ADD_TO_CONTENT_POOL", "success", 80, 70, True, []),
  292. _v4_decision(run_id, policy_run_id, "decision_review", "content_review", "KEEP_CONTENT_FOR_REVIEW", "pending", 60, 60, False, [{"field": "statistics.share_count", "reason": "runtime_missing"}]),
  293. _v4_decision(run_id, policy_run_id, "decision_reject", "content_reject", "REJECT_CONTENT", "failed", 40, 70, False, [{"field": "statistics.play_count", "reason": "natural_platform_missing"}]),
  294. _v4_decision(run_id, policy_run_id, "decision_retry", "content_retry", "KEEP_CONTENT_FOR_REVIEW", "pending", None, None, False, [], reason="v4_technical_retry_needed"),
  295. ],
  296. )
  297. runtime.append_jsonl(run_id, "discovered_content_items.jsonl", [])
  298. runtime.append_jsonl(run_id, "source_path_records.jsonl", [])
  299. runtime.append_jsonl(
  300. run_id,
  301. "walk_actions.jsonl",
  302. [
  303. {
  304. "run_id": run_id,
  305. "policy_run_id": policy_run_id,
  306. "walk_action_id": "walk_allowed",
  307. "edge_id": "hashtag_to_query",
  308. "walk_status": "success",
  309. "raw_payload": {
  310. "decision_id": "decision_pool",
  311. "allow_walk": True,
  312. "allow_walk_reason": "query>=70/platform>=65/score>=70",
  313. "walk_gate_status": "allowed",
  314. "walk_gate_snapshot": {"query_relevance_score": 80, "platform_performance_score": 70, "score": 75},
  315. },
  316. },
  317. {
  318. "run_id": run_id,
  319. "policy_run_id": policy_run_id,
  320. "walk_action_id": "walk_denied",
  321. "edge_id": "author_to_works",
  322. "walk_status": "skipped",
  323. "reason_code": "v4_allow_walk_denied",
  324. "raw_payload": {
  325. "decision_id": "decision_review",
  326. "allow_walk": False,
  327. "allow_walk_reason": "score below allow_walk threshold",
  328. "walk_gate_status": "denied",
  329. "walk_gate_reason_code": "v4_allow_walk_denied",
  330. "walk_gate_snapshot": {"query_relevance_score": 60, "platform_performance_score": 60, "score": 60},
  331. },
  332. },
  333. ],
  334. )
  335. def _v4_decision(
  336. run_id,
  337. policy_run_id,
  338. decision_id,
  339. target_id,
  340. action,
  341. effect_status,
  342. query_score,
  343. platform_score,
  344. allow_walk,
  345. missing_fields,
  346. reason=None,
  347. ):
  348. score = (
  349. round(query_score * 0.5 + platform_score * 0.5, 2)
  350. if query_score is not None and platform_score is not None
  351. else None
  352. )
  353. return {
  354. "run_id": run_id,
  355. "policy_run_id": policy_run_id,
  356. "decision_id": decision_id,
  357. "decision_target_type": "content",
  358. "decision_target_id": target_id,
  359. "decision_action": action,
  360. "decision_reason_code": reason or (
  361. "v4_query_and_platform_pass"
  362. if action == "ADD_TO_CONTENT_POOL"
  363. else "v4_score_review_needed"
  364. if action == "KEEP_CONTENT_FOR_REVIEW"
  365. else "v4_query_or_score_below_threshold"
  366. ),
  367. "search_query_effect_status": effect_status,
  368. "score": score,
  369. "scorecard": {
  370. "schema_version": "v4_scorecard.v1",
  371. "query_relevance_score": query_score,
  372. "platform_performance_score": platform_score,
  373. "missing_observable_fields": missing_fields,
  374. "score_missing": score is None,
  375. "failure_type": "llm_timeout" if score is None else None,
  376. },
  377. "decision_replay_data": {
  378. "allow_walk": allow_walk,
  379. "allow_walk_reason": (
  380. "query>=70/platform>=65/score>=70"
  381. if allow_walk
  382. else "score below allow_walk threshold"
  383. ),
  384. "walk_gate_snapshot": {
  385. "query_relevance_score": query_score,
  386. "platform_performance_score": platform_score,
  387. "score": score,
  388. },
  389. },
  390. "raw_payload": {},
  391. }