policy_json.py 7.7 KB

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