test_douyin_fifty_plus_scoring.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """M9E:抖音 50+ 进评分——35/35/30、not_attempted 走 35/35、画像缺失→TECHNICAL_RETRY+停游走、非抖音仍 50/50。"""
  2. from copy import deepcopy
  3. from pathlib import Path
  4. from content_agent.business_modules import walk_strategy
  5. from content_agent.business_modules.rule_judgment import evaluator
  6. from content_agent.integrations.policy_json import JsonPolicyBundleStore
  7. POLICY = JsonPolicyBundleStore(Path(".")).load_policy_bundle("V4")
  8. def _bundle(query, platform_score, fifty=None):
  9. bundle = {
  10. "schema_version": "evidence_bundle.v1",
  11. "source_evidence": {"source_kind": "pg_pattern_v2"},
  12. "content": {
  13. "decision_target_type": "content",
  14. "decision_target_id": "c1",
  15. "content_discovery_id": "cd1",
  16. "search_query_id": "q_001",
  17. "platform": "douyin",
  18. "platform_content_id": "c1",
  19. "platform_content_format": "video",
  20. "description": "x",
  21. "author": {"platform_author_id": "a1"},
  22. "platform_author_id": "a1",
  23. },
  24. "pattern_match_result": {
  25. "query_relevance_score": query,
  26. "judge_status": "ok",
  27. "final_status": "ok",
  28. },
  29. "content_engagement_metrics": {
  30. "platform_performance": {
  31. "platform_performance_score": platform_score,
  32. "platform_performance_components": [],
  33. "missing_observable_fields": [],
  34. }
  35. },
  36. "content_risk_check": {"risk_level": "low", "age_50_plus_safety": "safe"},
  37. "run_context": {
  38. "run_id": "run",
  39. "policy_run_id": "policy_run",
  40. "runtime_record_schema_version": "runtime_record.v1",
  41. "decision_input_snapshot_id": "evidence:c1",
  42. },
  43. }
  44. if fifty is not None:
  45. bundle["content_audience_50plus"] = fifty
  46. return bundle
  47. def _decide(bundle):
  48. return evaluator.decide("run", "policy_run", 1, bundle, POLICY)
  49. def test_douyin_ok_uses_35_35_30():
  50. fifty = {"status": "ok", "score": 76, "components": {"band": "50+", "band_tgi": 171.5}}
  51. decision = _decide(_bundle(80, 70, fifty))
  52. scorecard = decision["scorecard"]
  53. assert scorecard["fifty_plus_status"] == "ok"
  54. assert scorecard["fifty_plus_score"] == 76
  55. assert scorecard["score_weights"] == {
  56. "query_relevance": 0.35,
  57. "platform_performance": 0.35,
  58. "fifty_plus": 0.3,
  59. }
  60. assert abs(decision["score"] - (0.35 * 80 + 0.35 * 70 + 0.30 * 76)) < 0.01 # 75.3
  61. assert decision["decision_action"] == "ADD_TO_CONTENT_POOL"
  62. def test_douyin_not_attempted_uses_35_35_two_terms():
  63. decision = _decide(_bundle(80, 70, {"status": "not_attempted"}))
  64. assert decision["scorecard"]["fifty_plus_status"] == "not_attempted"
  65. assert "fifty_plus_score" not in decision["scorecard"]
  66. assert decision["scorecard"]["score_weights"] == {
  67. "query_relevance": 0.35,
  68. "platform_performance": 0.35,
  69. }
  70. assert abs(decision["score"] - (0.35 * 80 + 0.35 * 70)) < 0.01 # 52.5
  71. def test_douyin_portrait_unavailable_is_technical_retry_and_stops_walk():
  72. decision = _decide(_bundle(80, 70, {"status": "unavailable"}))
  73. assert decision["score"] is None
  74. assert decision["decision_action"] == "TECHNICAL_RETRY_REQUIRED"
  75. assert decision["decision_reason_code"] == "portrait_unavailable"
  76. # 停游走:TECHNICAL_RETRY_REQUIRED 映射 path_stop。
  77. assert walk_strategy._action_for_decision("TECHNICAL_RETRY_REQUIRED")["edge_id"] == "path_stop"
  78. def test_douyin_portrait_incomplete_reason():
  79. decision = _decide(_bundle(80, 70, {"status": "incomplete"}))
  80. assert decision["decision_action"] == "TECHNICAL_RETRY_REQUIRED"
  81. assert decision["decision_reason_code"] == "portrait_incomplete"
  82. def test_non_douyin_keeps_50_50_when_no_block():
  83. decision = _decide(_bundle(80, 70)) # 无 content_audience_50plus 块
  84. scorecard = decision["scorecard"]
  85. assert "fifty_plus_status" not in scorecard
  86. assert "fifty_plus_score" not in scorecard
  87. assert scorecard["score_weights"] == {
  88. "query_relevance": 0.5,
  89. "platform_performance": 0.5,
  90. }
  91. assert abs(decision["score"] - (0.5 * 80 + 0.5 * 70)) < 0.01 # 75.0
  92. def test_douyin_ok_uses_policy_score_weight_profile():
  93. policy = deepcopy(POLICY)
  94. profiles = policy["rule_pack"]["scorecard"]["score_weight_profiles"]["profiles"]
  95. ok_profile = next(profile for profile in profiles if profile["profile_id"] == "audience_50plus_available_v1")
  96. ok_profile["weights"] = {"query_relevance": 40, "platform_performance": 30, "fifty_plus": 30}
  97. decision = evaluator.decide(
  98. "run",
  99. "policy_run",
  100. 1,
  101. _bundle(80, 70, {"status": "ok", "score": 76}),
  102. policy,
  103. )
  104. assert abs(decision["score"] - (0.40 * 80 + 0.30 * 70 + 0.30 * 76)) < 0.01
  105. assert decision["scorecard"]["score_weights"] == {
  106. "query_relevance": 0.4,
  107. "platform_performance": 0.3,
  108. "fifty_plus": 0.3,
  109. }