|
@@ -36,9 +36,7 @@ RULE_DECISION_RAW_PAYLOAD_EXCLUDE_KEYS = {
|
|
|
"source_evidence",
|
|
"source_evidence",
|
|
|
"decision_replay_data",
|
|
"decision_replay_data",
|
|
|
}
|
|
}
|
|
|
-# M9B:抖音 50+ 子分。仅当 bundle 带 content_audience_50plus 块(抖音)时计入;
|
|
|
|
|
-# 非抖音 bundle 无块 → 维持旧 0.5/0.5(零改动)。权重在 evaluator 计算,不进 rule pack
|
|
|
|
|
-# 维度(config 合同要求 active 维度恰为 query_relevance/platform_performance)。
|
|
|
|
|
|
|
+# V5-M2:V4 权重从规则包 score_weight_profiles 读取;缺配置时 fallback 保持历史行为。
|
|
|
V4_PORTRAIT_UNAVAILABLE_REASON = "portrait_unavailable"
|
|
V4_PORTRAIT_UNAVAILABLE_REASON = "portrait_unavailable"
|
|
|
V4_PORTRAIT_INCOMPLETE_REASON = "portrait_incomplete"
|
|
V4_PORTRAIT_INCOMPLETE_REASON = "portrait_incomplete"
|
|
|
|
|
|
|
@@ -69,7 +67,7 @@ def decide(
|
|
|
if _is_v4_policy(policy_bundle):
|
|
if _is_v4_policy(policy_bundle):
|
|
|
replay_marker.update(_v4_walk_gate(False, primary_gate["decision_reason_code"]))
|
|
replay_marker.update(_v4_walk_gate(False, primary_gate["decision_reason_code"]))
|
|
|
if primary_gate["decision_reason_code"] == V4_TECHNICAL_RETRY_REASON:
|
|
if primary_gate["decision_reason_code"] == V4_TECHNICAL_RETRY_REASON:
|
|
|
- scorecard = _v4_scorecard_total(bundle)
|
|
|
|
|
|
|
+ scorecard = _v4_scorecard_total(bundle, policy_bundle)
|
|
|
effect = _effect_status_for_decision(
|
|
effect = _effect_status_for_decision(
|
|
|
policy_bundle,
|
|
policy_bundle,
|
|
|
primary_gate["decision_action"],
|
|
primary_gate["decision_action"],
|
|
@@ -165,7 +163,7 @@ def _decide_v4(
|
|
|
matched_gates: list[dict[str, Any]],
|
|
matched_gates: list[dict[str, Any]],
|
|
|
triggered_blocking_rules: list[str],
|
|
triggered_blocking_rules: list[str],
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- scorecard = _v4_scorecard_total(bundle)
|
|
|
|
|
|
|
+ scorecard = _v4_scorecard_total(bundle, policy_bundle)
|
|
|
score = scorecard.get("total_score")
|
|
score = scorecard.get("total_score")
|
|
|
thresholds = _v4_thresholds(policy_bundle, _get_path(bundle, "content.platform"))
|
|
thresholds = _v4_thresholds(policy_bundle, _get_path(bundle, "content.platform"))
|
|
|
scorecard["score_thresholds"] = thresholds # 随决策落库:供前端展示实际生效门槛(单一真源)
|
|
scorecard["score_thresholds"] = thresholds # 随决策落库:供前端展示实际生效门槛(单一真源)
|
|
@@ -206,7 +204,7 @@ def _decide_v4(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _v4_scorecard_total(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
|
+def _v4_scorecard_total(bundle: dict[str, Any], policy_bundle: dict[str, Any]) -> dict[str, Any]:
|
|
|
query_score = _query_relevance_from_bundle(bundle)
|
|
query_score = _query_relevance_from_bundle(bundle)
|
|
|
platform = _platform_performance_from_bundle(bundle)
|
|
platform = _platform_performance_from_bundle(bundle)
|
|
|
platform_score = platform.get("platform_performance_score") if isinstance(platform, dict) else None
|
|
platform_score = platform.get("platform_performance_score") if isinstance(platform, dict) else None
|
|
@@ -220,26 +218,40 @@ def _v4_scorecard_total(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
|
"missing_observable_fields": missing if isinstance(missing, list) else [],
|
|
"missing_observable_fields": missing if isinstance(missing, list) else [],
|
|
|
}
|
|
}
|
|
|
fifty = _fifty_plus_from_bundle(bundle)
|
|
fifty = _fifty_plus_from_bundle(bundle)
|
|
|
|
|
+ selected_profile, weights = _v4_score_weight_profile(policy_bundle, bundle)
|
|
|
|
|
+ scorecard["score_weights"] = weights
|
|
|
|
|
+ missing_failure = _v4_missing_critical_dimension_failure(selected_profile, bundle)
|
|
|
|
|
+ if missing_failure:
|
|
|
|
|
+ scorecard.update(missing_failure)
|
|
|
|
|
+ scorecard["total_score"] = None
|
|
|
|
|
+ scorecard["score_missing"] = True
|
|
|
|
|
+ scorecard.update(_technical_failure_fields(bundle))
|
|
|
|
|
+ return scorecard
|
|
|
if fifty is None:
|
|
if fifty is None:
|
|
|
- # 非抖音:旧 0.5/0.5,零改动。
|
|
|
|
|
- total = (
|
|
|
|
|
- round(float(query_score) * 0.5 + float(platform_score) * 0.5, 2)
|
|
|
|
|
- if _is_number(query_score) and _is_number(platform_score)
|
|
|
|
|
- else None
|
|
|
|
|
|
|
+ total = _v4_weighted_total(
|
|
|
|
|
+ {"query_relevance": query_score, "platform_performance": platform_score},
|
|
|
|
|
+ weights,
|
|
|
)
|
|
)
|
|
|
else:
|
|
else:
|
|
|
- # 抖音:Query 35% + 平台 35% + 50+ 30%(块存在才计)。
|
|
|
|
|
status = fifty.get("status")
|
|
status = fifty.get("status")
|
|
|
scorecard["fifty_plus_status"] = status
|
|
scorecard["fifty_plus_status"] = status
|
|
|
fifty_score = fifty.get("score")
|
|
fifty_score = fifty.get("score")
|
|
|
if status == "ok" and _is_number(query_score) and _is_number(platform_score) and _is_number(fifty_score):
|
|
if status == "ok" and _is_number(query_score) and _is_number(platform_score) and _is_number(fifty_score):
|
|
|
scorecard["fifty_plus_score"] = fifty_score
|
|
scorecard["fifty_plus_score"] = fifty_score
|
|
|
scorecard["fifty_plus_components"] = fifty.get("components", {})
|
|
scorecard["fifty_plus_components"] = fifty.get("components", {})
|
|
|
- total = round(
|
|
|
|
|
- float(query_score) * 0.35 + float(platform_score) * 0.35 + float(fifty_score) * 0.30, 2
|
|
|
|
|
|
|
+ total = _v4_weighted_total(
|
|
|
|
|
+ {
|
|
|
|
|
+ "query_relevance": query_score,
|
|
|
|
|
+ "platform_performance": platform_score,
|
|
|
|
|
+ "fifty_plus": fifty_score,
|
|
|
|
|
+ },
|
|
|
|
|
+ weights,
|
|
|
)
|
|
)
|
|
|
elif status == "not_attempted" and _is_number(query_score) and _is_number(platform_score):
|
|
elif status == "not_attempted" and _is_number(query_score) and _is_number(platform_score):
|
|
|
- total = round(float(query_score) * 0.35 + float(platform_score) * 0.35, 2)
|
|
|
|
|
|
|
+ total = _v4_weighted_total(
|
|
|
|
|
+ {"query_relevance": query_score, "platform_performance": platform_score},
|
|
|
|
|
+ weights,
|
|
|
|
|
+ )
|
|
|
else:
|
|
else:
|
|
|
# unavailable / incomplete / 缺分 → 技术失败(停游走)。把失败类型+原因写进 scorecard,供技术详情展示。
|
|
# unavailable / incomplete / 缺分 → 技术失败(停游走)。把失败类型+原因写进 scorecard,供技术详情展示。
|
|
|
total = None
|
|
total = None
|
|
@@ -259,6 +271,131 @@ def _v4_scorecard_total(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
|
return scorecard
|
|
return scorecard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+_V4_SCORE_WEIGHT_FALLBACK_PROFILES = {
|
|
|
|
|
+ "default_two_dimension": {"query_relevance": 50, "platform_performance": 50},
|
|
|
|
|
+ "douyin_fifty_plus_ok": {"query_relevance": 35, "platform_performance": 35, "fifty_plus": 30},
|
|
|
|
|
+ "douyin_fifty_plus_not_attempted": {"query_relevance": 35, "platform_performance": 35},
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _v4_score_weights(policy_bundle: dict[str, Any], bundle: dict[str, Any]) -> dict[str, float]:
|
|
|
|
|
+ _, weights = _v4_score_weight_profile(policy_bundle, bundle)
|
|
|
|
|
+ return weights
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _v4_score_weight_profile(policy_bundle: dict[str, Any], bundle: dict[str, Any]) -> tuple[dict[str, Any], dict[str, float]]:
|
|
|
|
|
+ profiles = (
|
|
|
|
|
+ policy_bundle.get("rule_pack", {})
|
|
|
|
|
+ .get("scorecard", {})
|
|
|
|
|
+ .get("score_weight_profiles")
|
|
|
|
|
+ )
|
|
|
|
|
+ generic_profile = _select_generic_score_weight_profile(profiles, bundle)
|
|
|
|
|
+ if generic_profile:
|
|
|
|
|
+ return generic_profile, _decimal_weights(generic_profile.get("weights"))
|
|
|
|
|
+ if not isinstance(profiles, dict) or "profiles" in profiles:
|
|
|
|
|
+ profiles = _V4_SCORE_WEIGHT_FALLBACK_PROFILES
|
|
|
|
|
+ fifty = _fifty_plus_from_bundle(bundle)
|
|
|
|
|
+ if fifty is None:
|
|
|
|
|
+ profile_name = "default_two_dimension"
|
|
|
|
|
+ elif fifty.get("status") == "not_attempted":
|
|
|
|
|
+ profile_name = "douyin_fifty_plus_not_attempted"
|
|
|
|
|
+ else:
|
|
|
|
|
+ profile_name = "douyin_fifty_plus_ok"
|
|
|
|
|
+ raw_weights = profiles.get(profile_name)
|
|
|
|
|
+ if not isinstance(raw_weights, dict) or not raw_weights:
|
|
|
|
|
+ raw_weights = _V4_SCORE_WEIGHT_FALLBACK_PROFILES[profile_name]
|
|
|
|
|
+ return {"profile_id": profile_name, "weights": raw_weights}, _decimal_weights(raw_weights)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _decimal_weights(raw_weights: Any) -> dict[str, float]:
|
|
|
|
|
+ if not isinstance(raw_weights, dict):
|
|
|
|
|
+ return {}
|
|
|
|
|
+ return {
|
|
|
|
|
+ str(key): float(value) / 100
|
|
|
|
|
+ for key, value in raw_weights.items()
|
|
|
|
|
+ if _is_number(value)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _select_generic_score_weight_profile(profiles: Any, bundle: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
|
|
+ if not isinstance(profiles, dict) or not isinstance(profiles.get("profiles"), list):
|
|
|
|
|
+ return None
|
|
|
|
|
+ platform = str(_get_path(bundle, "content.platform") or "")
|
|
|
|
|
+ content_format = str(_get_path(bundle, "content.platform_content_format") or "video")
|
|
|
|
|
+ matches = [
|
|
|
|
|
+ profile
|
|
|
|
|
+ for profile in profiles["profiles"]
|
|
|
|
|
+ if isinstance(profile, dict)
|
|
|
|
|
+ and profile.get("approval_status") == "approved"
|
|
|
|
|
+ and profile.get("runtime_enabled") is True
|
|
|
|
|
+ and _scope_matches(profile.get("platform_scope"), platform)
|
|
|
|
|
+ and _content_format_matches(profile.get("content_format"), content_format)
|
|
|
|
|
+ and _applies_when_matches(str(profile.get("applies_when") or "always"), bundle)
|
|
|
|
|
+ ]
|
|
|
|
|
+ if not matches:
|
|
|
|
|
+ return None
|
|
|
|
|
+ matches.sort(key=lambda row: int(row.get("priority") or 0), reverse=True)
|
|
|
|
|
+ if len(matches) > 1 and int(matches[0].get("priority") or 0) == int(matches[1].get("priority") or 0):
|
|
|
|
|
+ raise ValueError(f"score_weight_profile_priority_conflict: {matches[0].get('profile_id')}, {matches[1].get('profile_id')}")
|
|
|
|
|
+ return matches[0]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _scope_matches(scope: Any, platform: str) -> bool:
|
|
|
|
|
+ values = scope if isinstance(scope, list) else [scope]
|
|
|
|
|
+ return "*" in values or platform in values
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _content_format_matches(scope: Any, content_format: str) -> bool:
|
|
|
|
|
+ if scope in {None, "", "*"}:
|
|
|
|
|
+ return True
|
|
|
|
|
+ values = scope if isinstance(scope, list) else [scope]
|
|
|
|
|
+ return "*" in values or content_format in values
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _applies_when_matches(expr: str, bundle: dict[str, Any]) -> bool:
|
|
|
|
|
+ if expr in {"", "always"}:
|
|
|
|
|
+ return True
|
|
|
|
|
+ if expr == "no_extra_dimension_available":
|
|
|
|
|
+ return _fifty_plus_from_bundle(bundle) is None
|
|
|
|
|
+ if expr.startswith("field_exists:"):
|
|
|
|
|
+ return _get_path(bundle, expr.removeprefix("field_exists:")) is not None
|
|
|
|
|
+ if expr.startswith("field_missing:"):
|
|
|
|
|
+ return _get_path(bundle, expr.removeprefix("field_missing:")) is None
|
|
|
|
|
+ if expr.startswith("field_equals:"):
|
|
|
|
|
+ _, path, expected = expr.split(":", 2)
|
|
|
|
|
+ return str(_get_path(bundle, path)) == expected
|
|
|
|
|
+ raise ValueError(f"unsupported score_weight_profile applies_when: {expr}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _v4_missing_critical_dimension_failure(profile: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
|
|
+ if profile.get("missing_dimension_policy") != "technical_retry":
|
|
|
|
|
+ return None
|
|
|
|
|
+ paths = profile.get("score_source_paths") if isinstance(profile.get("score_source_paths"), dict) else {}
|
|
|
|
|
+ for dimension in profile.get("critical_dimensions") or []:
|
|
|
|
|
+ source_path = paths.get(dimension)
|
|
|
|
|
+ if source_path and _get_path(bundle, source_path) is None:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "failure_type": "critical_dimension_missing",
|
|
|
|
|
+ "final_status": "failed",
|
|
|
|
|
+ "missing_dimension_key": dimension,
|
|
|
|
|
+ "missing_dimension_score_source_path": source_path,
|
|
|
|
|
+ "missing_dimension_failure_reason": f"missing critical dimension {dimension} at {source_path}",
|
|
|
|
|
+ }
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _v4_weighted_total(scores: dict[str, Any], weights: dict[str, float]) -> float | None:
|
|
|
|
|
+ if not weights:
|
|
|
|
|
+ return None
|
|
|
|
|
+ total = 0.0
|
|
|
|
|
+ for key, weight in weights.items():
|
|
|
|
|
+ score = scores.get(key)
|
|
|
|
|
+ if not _is_number(score):
|
|
|
|
|
+ return None
|
|
|
|
|
+ total += float(score) * weight
|
|
|
|
|
+ return round(total, 2)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _fifty_plus_from_bundle(bundle: dict[str, Any]) -> dict[str, Any] | None:
|
|
def _fifty_plus_from_bundle(bundle: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
block = bundle.get("content_audience_50plus")
|
|
block = bundle.get("content_audience_50plus")
|
|
|
return block if isinstance(block, dict) else None
|
|
return block if isinstance(block, dict) else None
|