policy_json.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from typing import Any
  4. from content_agent.constants import DEFAULT_POLICY_BUNDLE_ID, EVIDENCE_BUNDLE_SCHEMA_VERSION, RUNTIME_SCHEMA_VERSION
  5. from content_agent.errors import ContentAgentError, ErrorCode
  6. from content_agent.integrations import config_store
  7. from content_agent.integrations.walk_strategy_json import WalkStrategyStore
  8. class JsonPolicyBundleStore:
  9. def __init__(self, root_dir: Path | str = Path(".")) -> None:
  10. self.root_dir = Path(root_dir)
  11. def load_policy_bundle(self, strategy_version: str) -> dict[str, Any]:
  12. rule_pack_path = self.root_dir / "product_documents/规则包/douyin_rule_packs.v1.json"
  13. if not rule_pack_path.exists():
  14. raise FileNotFoundError(rule_pack_path)
  15. rule_package, rule_pack_text = config_store.load_json(rule_pack_path)
  16. rule_pack_hash = config_store.sha256_text(rule_pack_text)
  17. actual_strategy_version = _strategy_version(rule_package)
  18. if strategy_version != actual_strategy_version:
  19. raise ValueError(f"unknown strategy_version: {strategy_version}")
  20. dispatch = _select_dispatch(rule_package, actual_strategy_version)
  21. rule_pack = _find_rule_pack_by_dispatch(rule_package, dispatch)
  22. rule_pack_by_entity = _build_rule_pack_by_entity(rule_package, actual_strategy_version)
  23. walk_strategy = WalkStrategyStore(self.root_dir).load_walk_strategy()
  24. # V4 判定/游走门槛:独立可手维护配置(不走 Excel→规则包管线),按平台解耦。
  25. score_thresholds_path = self.root_dir / "tech_documents/数据接口与来源/score_thresholds.json"
  26. score_thresholds: dict[str, Any] = {}
  27. if score_thresholds_path.exists():
  28. score_thresholds, _ = config_store.load_json(score_thresholds_path)
  29. bundle = {
  30. "policy_bundle_id": DEFAULT_POLICY_BUNDLE_ID,
  31. "strategy_version": actual_strategy_version,
  32. "rule_package_id": rule_package.get("package_id"),
  33. "rule_package_name": rule_package.get("package_name"),
  34. "rule_pack": rule_pack,
  35. "rule_pack_id": rule_pack["rule_pack_id"],
  36. "rule_pack_version": rule_pack["version"],
  37. "dispatch": dispatch,
  38. "dispatch_id": dispatch["dispatch_id"],
  39. "rule_pack_by_entity": rule_pack_by_entity,
  40. "runtime_stage": dispatch["runtime_stage"],
  41. "target_entity": dispatch["target_entity"],
  42. "content_format": dispatch["content_format"],
  43. "evidence_bundle_schema_version": EVIDENCE_BUNDLE_SCHEMA_VERSION,
  44. "runtime_record_schema_version": RUNTIME_SCHEMA_VERSION,
  45. "shared_contracts": rule_package.get("shared_contracts", {}),
  46. "source_evidence_policy": rule_package.get("source_evidence_policy", {}),
  47. "effect_status_mapping": rule_package.get("effect_status_mapping", []),
  48. "query_effect_aggregation": rule_package.get("query_effect_aggregation", []),
  49. "runtime_status_contract": rule_package.get("runtime_status_contract", {}),
  50. "decision_reason_codes": rule_package.get("decision_reason_codes", []),
  51. "strategy_id": (rule_package.get("strategy_binding") or {}).get(
  52. "strategy_id", "douyin_content_find_v1"
  53. ),
  54. "walk_strategy_id": walk_strategy.get("strategy_id"),
  55. "walk_strategy_version": walk_strategy.get("walk_strategy_version"),
  56. "walk_strategy_source_ref": walk_strategy.get("walk_strategy_source_ref"),
  57. "strategy_source_ref": {
  58. "file": str(rule_pack_path),
  59. "updated_at": rule_package.get("updated_at"),
  60. "content_sha256": rule_pack_hash,
  61. "generated_from": "p5_rule_pack_only",
  62. },
  63. "rule_pack_source_ref": {
  64. "file": str(rule_pack_path),
  65. "updated_at": rule_package.get("updated_at"),
  66. "content_sha256": rule_pack_hash,
  67. "generated_from": "local_product_json",
  68. },
  69. "policy_bundle_hash": rule_pack_hash,
  70. "score_thresholds": score_thresholds,
  71. }
  72. return bundle
  73. def _strategy_version(rule_package: dict[str, Any]) -> str:
  74. binding_version = (rule_package.get("strategy_binding") or {}).get("strategy_version")
  75. return binding_version or "V1"
  76. def _select_dispatch(
  77. rule_package: dict[str, Any],
  78. strategy_version: str,
  79. *,
  80. target_entity: str = "Content",
  81. content_format: str = "video",
  82. runtime_stage: str = "V1.0",
  83. platform: str = "douyin",
  84. ) -> dict[str, Any]:
  85. matches = _enabled_dispatches(
  86. rule_package,
  87. strategy_version,
  88. target_entity=target_entity,
  89. content_format=content_format,
  90. runtime_stage=runtime_stage,
  91. platform=platform,
  92. )
  93. return _assert_single_enabled_dispatch(matches, target_entity=target_entity, content_format=content_format)
  94. def _enabled_dispatches(
  95. rule_package: dict[str, Any],
  96. strategy_version: str,
  97. *,
  98. target_entity: str,
  99. content_format: str,
  100. runtime_stage: str,
  101. platform: str,
  102. ) -> list[dict[str, Any]]:
  103. return [
  104. dispatch
  105. for dispatch in rule_package.get("rule_pack_dispatch", [])
  106. if dispatch.get("dispatch_enabled")
  107. and dispatch.get("platform") == platform
  108. and dispatch.get("runtime_stage") == runtime_stage
  109. and dispatch.get("strategy_version") == strategy_version
  110. and dispatch.get("target_entity") == target_entity
  111. and dispatch.get("content_format") == content_format
  112. ]
  113. def _assert_single_enabled_dispatch(
  114. matches: list[dict[str, Any]],
  115. *,
  116. target_entity: str,
  117. content_format: str,
  118. ) -> dict[str, Any]:
  119. if len(matches) == 1:
  120. return matches[0]
  121. if not matches:
  122. raise ValueError(f"dispatch not found for {target_entity}/{content_format}")
  123. conflict_rule_pack_ids = [dispatch.get("rule_pack_id") for dispatch in matches]
  124. raise ContentAgentError(
  125. ErrorCode.CONFIG_RULE_PACK_DISPATCH_CONFLICT,
  126. f"CONFIG_RULE_PACK_DISPATCH_CONFLICT: multiple enabled dispatches for "
  127. f"{target_entity}/{content_format}: {conflict_rule_pack_ids}",
  128. {"target_entity": target_entity, "content_format": content_format,
  129. "conflict_rule_pack_ids": conflict_rule_pack_ids},
  130. )
  131. def _build_rule_pack_by_entity(rule_package: dict[str, Any], strategy_version: str) -> dict[str, Any]:
  132. by_entity: dict[str, Any] = {}
  133. for dispatch in rule_package.get("rule_pack_dispatch", []):
  134. if not (
  135. dispatch.get("dispatch_enabled")
  136. and dispatch.get("platform") == "douyin"
  137. and dispatch.get("runtime_stage") == "V1.0"
  138. and dispatch.get("strategy_version") == strategy_version
  139. ):
  140. continue
  141. by_entity[dispatch["target_entity"]] = {
  142. "dispatch": dispatch,
  143. "rule_pack": _find_rule_pack_by_dispatch(rule_package, dispatch),
  144. }
  145. return by_entity
  146. def _find_rule_pack_by_dispatch(rule_package: dict[str, Any], dispatch: dict[str, Any]) -> dict[str, Any]:
  147. matches = [
  148. rule_pack
  149. for rule_pack in rule_package.get("rule_packs", [])
  150. if rule_pack.get("enabled")
  151. and rule_pack.get("rule_pack_id") == dispatch.get("rule_pack_id")
  152. and rule_pack.get("version") == dispatch.get("rule_pack_version")
  153. ]
  154. if len(matches) != 1:
  155. raise ValueError(
  156. f"dispatch {dispatch.get('dispatch_id')} matched {len(matches)} enabled rule packs"
  157. )
  158. return matches[0]