business_harness.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. from __future__ import annotations
  2. from typing import Any
  3. from sqlalchemy import select
  4. from supply_infra.db.models.business_harness import (
  5. ContentDemandCoverage,
  6. ContentPerformanceFact,
  7. DailyDemandPackage,
  8. DailyDemandTask,
  9. DemandAttributionSnapshot,
  10. DemandEvaluationSnapshot,
  11. DemandScoreContribution,
  12. EvidencePackage,
  13. PlatformDemand,
  14. PlatformDemandCategoryRel,
  15. PlatformDemandVersion,
  16. RawDemandExpression,
  17. StandardDemandAlias,
  18. StrategyVersion,
  19. )
  20. from supply_infra.db.session import get_session
  21. def get_published_demand_package(
  22. *,
  23. biz_dt: str | None = None,
  24. run_id: str | None = None,
  25. ) -> dict[str, Any] | None:
  26. with get_session() as session:
  27. stmt = select(DailyDemandPackage).where(
  28. DailyDemandPackage.status == "published"
  29. )
  30. if biz_dt:
  31. stmt = stmt.where(DailyDemandPackage.biz_dt == biz_dt)
  32. if run_id:
  33. stmt = stmt.where(DailyDemandPackage.run_id == run_id)
  34. package = session.scalar(
  35. stmt.order_by(
  36. DailyDemandPackage.biz_dt.desc(),
  37. DailyDemandPackage.package_version.desc(),
  38. DailyDemandPackage.created_at.desc(),
  39. ).limit(1)
  40. )
  41. if package is None:
  42. return None
  43. evidence = session.get(EvidencePackage, package.evidence_package_id)
  44. strategy = session.get(StrategyVersion, package.strategy_version_id)
  45. tasks = list(
  46. session.scalars(
  47. select(DailyDemandTask)
  48. .where(
  49. DailyDemandTask.demand_package_id
  50. == package.demand_package_id
  51. )
  52. .order_by(DailyDemandTask.action_tier, DailyDemandTask.task_id)
  53. ).all()
  54. )
  55. items: list[dict[str, Any]] = []
  56. for task in tasks:
  57. evaluation = session.get(
  58. DemandEvaluationSnapshot,
  59. task.evaluation_id,
  60. )
  61. contributions = list(
  62. session.scalars(
  63. select(DemandScoreContribution)
  64. .where(
  65. DemandScoreContribution.evaluation_id
  66. == task.evaluation_id
  67. )
  68. .order_by(
  69. DemandScoreContribution.score_type,
  70. DemandScoreContribution.dimension,
  71. )
  72. ).all()
  73. )
  74. payload = dict(task.task_payload_json)
  75. payload["task_id"] = task.task_id
  76. payload["evaluation_id"] = task.evaluation_id
  77. payload["score_contributions"] = [
  78. {
  79. "score_type": row.score_type,
  80. "dimension": row.dimension,
  81. "raw_value": row.raw_value_json,
  82. "normalized_value": float(row.normalized_value),
  83. "configured_weight": float(row.configured_weight),
  84. "effective_weight": float(row.effective_weight),
  85. "contribution": float(row.contribution),
  86. "data_status": row.data_status,
  87. "reason": row.reason,
  88. }
  89. for row in contributions
  90. ]
  91. if evaluation is not None:
  92. payload["decision_reason"] = evaluation.decision_reason
  93. payload["missing_dimensions"] = (
  94. evaluation.missing_dimensions_json
  95. )
  96. items.append(payload)
  97. return {
  98. "demand_package_id": package.demand_package_id,
  99. "run_id": package.run_id,
  100. "biz_dt": package.biz_dt,
  101. "package_version": package.package_version,
  102. "status": package.status,
  103. "content_hash": package.content_hash,
  104. "item_count": package.item_count,
  105. "published_at": (
  106. package.published_at.isoformat()
  107. if package.published_at is not None
  108. else None
  109. ),
  110. "evidence_package": (
  111. {
  112. "package_id": evidence.package_id,
  113. "status": evidence.status,
  114. "source_snapshot_hash": evidence.source_snapshot_hash,
  115. "source_versions": evidence.source_versions_json,
  116. "evidence_count": evidence.evidence_count,
  117. }
  118. if evidence is not None
  119. else None
  120. ),
  121. "strategy": (
  122. {
  123. "strategy_version_id": strategy.strategy_version_id,
  124. "version_key": strategy.version_key,
  125. "definition": strategy.definition_json,
  126. "change_reason": strategy.change_reason,
  127. }
  128. if strategy is not None
  129. else None
  130. ),
  131. "items": items,
  132. }
  133. def get_platform_demand_history(
  134. platform_demand_id: str,
  135. ) -> dict[str, Any] | None:
  136. with get_session() as session:
  137. demand = session.get(PlatformDemand, platform_demand_id)
  138. if demand is None:
  139. return None
  140. versions = list(
  141. session.scalars(
  142. select(PlatformDemandVersion)
  143. .where(
  144. PlatformDemandVersion.platform_demand_id
  145. == platform_demand_id
  146. )
  147. .order_by(PlatformDemandVersion.version_no.desc())
  148. ).all()
  149. )
  150. version_payloads: list[dict[str, Any]] = []
  151. for row in versions:
  152. category_relations = list(
  153. session.scalars(
  154. select(PlatformDemandCategoryRel)
  155. .where(
  156. PlatformDemandCategoryRel.platform_demand_version_id
  157. == row.platform_demand_version_id
  158. )
  159. .order_by(
  160. PlatformDemandCategoryRel.relation_type,
  161. PlatformDemandCategoryRel.category_id,
  162. )
  163. ).all()
  164. )
  165. evidence_rows = list(
  166. session.scalars(
  167. select(RawDemandExpression)
  168. .join(
  169. StandardDemandAlias,
  170. StandardDemandAlias.expression_id
  171. == RawDemandExpression.expression_id,
  172. )
  173. .where(
  174. StandardDemandAlias.term_id == row.term_id,
  175. RawDemandExpression.package_id == row.package_id,
  176. )
  177. .order_by(
  178. RawDemandExpression.source_type,
  179. RawDemandExpression.source_record_id,
  180. )
  181. ).all()
  182. )
  183. evaluation = session.scalar(
  184. select(DemandEvaluationSnapshot).where(
  185. DemandEvaluationSnapshot.platform_demand_version_id
  186. == row.platform_demand_version_id
  187. )
  188. )
  189. contributions = (
  190. list(
  191. session.scalars(
  192. select(DemandScoreContribution)
  193. .where(
  194. DemandScoreContribution.evaluation_id
  195. == evaluation.evaluation_id
  196. )
  197. .order_by(
  198. DemandScoreContribution.score_type,
  199. DemandScoreContribution.dimension,
  200. )
  201. ).all()
  202. )
  203. if evaluation is not None
  204. else []
  205. )
  206. coverages = list(
  207. session.scalars(
  208. select(ContentDemandCoverage)
  209. .where(
  210. ContentDemandCoverage.platform_demand_version_id
  211. == row.platform_demand_version_id
  212. )
  213. .order_by(
  214. ContentDemandCoverage.content_id,
  215. ContentDemandCoverage.coverage_role,
  216. )
  217. ).all()
  218. )
  219. coverage_payloads: list[dict[str, Any]] = []
  220. for coverage in coverages:
  221. attributions = list(
  222. session.scalars(
  223. select(DemandAttributionSnapshot)
  224. .where(
  225. DemandAttributionSnapshot.coverage_id
  226. == coverage.coverage_id
  227. )
  228. .order_by(DemandAttributionSnapshot.created_at)
  229. ).all()
  230. )
  231. attribution_payloads: list[dict[str, Any]] = []
  232. for attribution in attributions:
  233. fact = session.get(
  234. ContentPerformanceFact,
  235. attribution.performance_fact_id,
  236. )
  237. attribution_payloads.append(
  238. {
  239. "attribution_id": attribution.attribution_id,
  240. "support_state": attribution.support_state,
  241. "attributable_rov": (
  242. float(attribution.attributable_rov)
  243. if attribution.attributable_rov is not None
  244. else None
  245. ),
  246. "attributable_vov": (
  247. float(attribution.attributable_vov)
  248. if attribution.attributable_vov is not None
  249. else None
  250. ),
  251. "attribution_confidence": float(
  252. attribution.attribution_confidence
  253. ),
  254. "validity_influence": float(
  255. attribution.validity_influence
  256. ),
  257. "priority_influence": float(
  258. attribution.priority_influence
  259. ),
  260. "reason": attribution.reason,
  261. "performance_fact": (
  262. {
  263. "performance_fact_id": (
  264. fact.performance_fact_id
  265. ),
  266. "biz_dt": fact.biz_dt,
  267. "source": fact.source,
  268. "exposure_count": fact.exposure_count,
  269. "sample_size": fact.sample_size,
  270. "content_quality": (
  271. float(fact.content_quality)
  272. if fact.content_quality is not None
  273. else None
  274. ),
  275. "rov": (
  276. float(fact.rov)
  277. if fact.rov is not None
  278. else None
  279. ),
  280. "vov": (
  281. float(fact.vov)
  282. if fact.vov is not None
  283. else None
  284. ),
  285. "observed_at": fact.observed_at.isoformat(),
  286. }
  287. if fact is not None
  288. else None
  289. ),
  290. }
  291. )
  292. coverage_payloads.append(
  293. {
  294. "coverage_id": coverage.coverage_id,
  295. "task_id": coverage.task_id,
  296. "content_id": coverage.content_id,
  297. "content_url": coverage.content_url,
  298. "source_type": coverage.source_type,
  299. "coverage_role": coverage.coverage_role,
  300. "coverage_score": float(coverage.coverage_score),
  301. "evidence": coverage.evidence_json,
  302. "status": coverage.status,
  303. "content_published_at": (
  304. coverage.content_published_at.isoformat()
  305. if coverage.content_published_at is not None
  306. else None
  307. ),
  308. "attributions": attribution_payloads,
  309. }
  310. )
  311. version_payloads.append(
  312. {
  313. "platform_demand_version_id": (
  314. row.platform_demand_version_id
  315. ),
  316. "version_no": row.version_no,
  317. "biz_dt": row.biz_dt,
  318. "run_id": row.run_id,
  319. "evidence_package_id": row.package_id,
  320. "name": row.name,
  321. "description": row.description,
  322. "cognition_confidence": float(row.cognition_confidence),
  323. "reason": row.reason,
  324. "change_type": row.change_type,
  325. "evidence_hash": row.evidence_hash,
  326. "raw_evidence": [
  327. {
  328. "expression_id": evidence.expression_id,
  329. "source_type": evidence.source_type,
  330. "source_record_id": evidence.source_record_id,
  331. "raw_text": evidence.raw_text,
  332. "original_payload": (
  333. evidence.original_payload_json
  334. ),
  335. "content_hash": evidence.content_hash,
  336. "data_quality": evidence.data_quality,
  337. }
  338. for evidence in evidence_rows
  339. ],
  340. "category_relations": [
  341. {
  342. "category_id": int(relation.category_id),
  343. "relation_type": relation.relation_type,
  344. "relation_source": relation.relation_source,
  345. "reason": relation.reason,
  346. "confidence": float(relation.confidence),
  347. "is_inferred": bool(relation.is_inferred),
  348. "status": relation.status,
  349. }
  350. for relation in category_relations
  351. ],
  352. "evaluation": (
  353. {
  354. "evaluation_id": evaluation.evaluation_id,
  355. "strategy_version_id": (
  356. evaluation.strategy_version_id
  357. ),
  358. "validity_score": float(
  359. evaluation.validity_score
  360. ),
  361. "local_supply_priority": float(
  362. evaluation.local_supply_priority
  363. ),
  364. "data_confidence": float(
  365. evaluation.data_confidence
  366. ),
  367. "posterior_state": evaluation.posterior_state,
  368. "action_tier": evaluation.action_tier,
  369. "metrics": evaluation.metrics_json,
  370. "missing_dimensions": (
  371. evaluation.missing_dimensions_json
  372. ),
  373. "decision_reason": evaluation.decision_reason,
  374. "score_contributions": [
  375. {
  376. "score_type": contribution.score_type,
  377. "dimension": contribution.dimension,
  378. "raw_value": (
  379. contribution.raw_value_json
  380. ),
  381. "normalized_value": float(
  382. contribution.normalized_value
  383. ),
  384. "configured_weight": float(
  385. contribution.configured_weight
  386. ),
  387. "effective_weight": float(
  388. contribution.effective_weight
  389. ),
  390. "contribution": float(
  391. contribution.contribution
  392. ),
  393. "data_status": contribution.data_status,
  394. "reason": contribution.reason,
  395. }
  396. for contribution in contributions
  397. ],
  398. }
  399. if evaluation is not None
  400. else None
  401. ),
  402. "content_coverage": coverage_payloads,
  403. }
  404. )
  405. return {
  406. "platform_demand_id": demand.platform_demand_id,
  407. "name": demand.name,
  408. "description": demand.description,
  409. "status": demand.status,
  410. "lifecycle_state": demand.lifecycle_state,
  411. "versions": version_payloads,
  412. }