| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 |
- from __future__ import annotations
- import hashlib
- import json
- import uuid
- from datetime import datetime
- from decimal import Decimal
- from typing import Any
- from sqlalchemy import func, select
- from supply_infra.db.models.business_harness import (
- DailyDemandPackage,
- DailyDemandTask,
- DemandAttributionSnapshot,
- DemandEvaluationSnapshot,
- DemandScoreContribution,
- EvidencePackage,
- PlatformDemandCategoryRel,
- PlatformDemandVersion,
- RawDemandExpression,
- StandardDemandAlias,
- StrategyVersion,
- )
- from supply_infra.db.models.demand_grade import DemandGrade
- from supply_infra.db.session import get_session
- from supply_infra.pipeline.contracts import StepContext
- from supply_infra.pipeline.dates import china_now
- _NAMESPACE = uuid.UUID("53a14b2c-8ec3-5b88-a684-aa09737b6570")
- _STRATEGY_KEY = "harness-v1"
- _STRATEGY_DEFINITION: dict[str, Any] = {
- "validity_weights": {
- "legacy_grade_score": 0.35,
- "prior_heat": 0.20,
- "source_coverage": 0.15,
- "posterior_rov": 0.15,
- "attributed_feedback": 0.15,
- },
- "local_priority_weights": {
- "validity": 0.30,
- "prior_heat": 0.20,
- "grade_band": 0.15,
- "supply_gap": 0.20,
- "attributed_feedback": 0.15,
- },
- "action_thresholds": {
- "assure_supply": {"validity": 0.75, "priority": 0.55},
- "priority": {"validity": 0.60, "priority": 0.70},
- "targeted_validation": {"validity": 0.45, "confidence_below": 0.75},
- "exploration": {"validity": 0.25},
- },
- "posterior_min_samples": 1,
- "exploration_allocation": 1,
- "schema_version": 1,
- }
- def _uuid(key: str) -> str:
- return str(uuid.uuid5(_NAMESPACE, key))
- def _hash(payload: Any) -> str:
- canonical = json.dumps(
- payload,
- ensure_ascii=False,
- sort_keys=True,
- separators=(",", ":"),
- default=str,
- )
- return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
- def _clamp(value: float) -> float:
- return max(0.0, min(1.0, value))
- def _json_list(raw: str | None) -> list[Any]:
- if not raw:
- return []
- try:
- value = json.loads(raw)
- except (TypeError, json.JSONDecodeError):
- return []
- return value if isinstance(value, list) else []
- def _weighted_score(
- dimensions: list[dict[str, Any]],
- ) -> tuple[Decimal, list[dict[str, Any]]]:
- available = [item for item in dimensions if item["normalized"] is not None]
- weight_sum = sum(float(item["weight"]) for item in available)
- if weight_sum <= 0:
- return Decimal("0.00000"), []
- contributions: list[dict[str, Any]] = []
- total = 0.0
- for item in available:
- effective_weight = float(item["weight"]) / weight_sum
- contribution = _clamp(float(item["normalized"])) * effective_weight
- total += contribution
- contributions.append(
- {
- **item,
- "effective_weight": effective_weight,
- "contribution": contribution,
- }
- )
- return Decimal(str(round(_clamp(total), 5))), contributions
- def _grade_band(grade: str) -> float:
- return {
- "S": 1.0,
- "A": 0.85,
- "B": 0.65,
- "C": 0.40,
- "D": 0.20,
- }.get(str(grade).upper(), 0.30)
- def _action_tier(
- definition: dict[str, Any],
- validity: float,
- priority: float,
- confidence: float,
- *,
- has_posterior: bool,
- ) -> str:
- thresholds = definition["action_thresholds"]
- assure = thresholds["assure_supply"]
- if (
- has_posterior
- and validity >= assure["validity"]
- and priority >= assure["priority"]
- ):
- return "assure_supply"
- priority_rule = thresholds["priority"]
- if validity >= priority_rule["validity"] and priority >= priority_rule["priority"]:
- return "priority"
- validate = thresholds["targeted_validation"]
- if (
- validity >= validate["validity"]
- and confidence < validate["confidence_below"]
- ):
- return "targeted_validation"
- if validity >= thresholds["exploration"]["validity"]:
- return "exploration"
- return "suppress"
- def _ensure_strategy(session) -> StrategyVersion:
- strategy = session.scalar(
- select(StrategyVersion)
- .where(StrategyVersion.status == "active")
- .order_by(StrategyVersion.activated_at.desc())
- .limit(1)
- )
- if strategy is not None:
- return strategy
- legacy = session.scalar(
- select(StrategyVersion).where(StrategyVersion.version_key == _STRATEGY_KEY)
- )
- if legacy is not None:
- legacy.status = "active"
- legacy.activated_at = china_now()
- session.flush()
- return legacy
- strategy = StrategyVersion(
- strategy_version_id=_uuid(f"strategy-version:{_STRATEGY_KEY}"),
- version_key=_STRATEGY_KEY,
- status="active",
- definition_json=_STRATEGY_DEFINITION,
- change_reason="平台需求双评分与五类行动层初始显式策略",
- created_by="system",
- activated_at=china_now(),
- )
- session.add(strategy)
- session.flush()
- return strategy
- def _raw_dimension(
- name: str,
- raw: Any,
- normalized: float | None,
- weight: float,
- reason: str,
- ) -> dict[str, Any]:
- return {
- "dimension": name,
- "raw": {"value": raw},
- "normalized": normalized,
- "weight": weight,
- "data_status": "available" if normalized is not None else "missing",
- "reason": reason,
- }
- def _evaluation_inputs(
- session,
- version: PlatformDemandVersion,
- grade: DemandGrade,
- definition: dict[str, Any],
- ) -> tuple[
- dict[str, Any],
- list[dict[str, Any]],
- list[dict[str, Any]],
- list[str],
- ]:
- strategies = [str(value) for value in _json_list(grade.strategies)]
- videos = [str(value) for value in _json_list(grade.video_list)]
- posterior_available = bool(grade.has_posterior and grade.posterior_rov_count > 0)
- posterior_raw = (
- float(grade.posterior_rov_avg)
- if posterior_available and grade.posterior_rov_avg is not None
- else None
- )
- attributed = session.execute(
- select(
- func.avg(DemandAttributionSnapshot.validity_influence),
- func.avg(DemandAttributionSnapshot.priority_influence),
- func.count(),
- )
- .join(
- PlatformDemandVersion,
- PlatformDemandVersion.platform_demand_version_id
- == DemandAttributionSnapshot.platform_demand_version_id,
- )
- .where(
- PlatformDemandVersion.platform_demand_id
- == version.platform_demand_id,
- DemandAttributionSnapshot.support_state.in_(("support", "oppose")),
- )
- ).one()
- attributed_validity = (
- float(attributed[0]) if attributed[0] is not None else None
- )
- attributed_priority = (
- float(attributed[1]) if attributed[1] is not None else None
- )
- validity_weights = definition["validity_weights"]
- validity_dimensions = [
- _raw_dimension(
- "legacy_grade_score",
- float(grade.score) if grade.score is not None else None,
- _clamp(float(grade.score) / 100.0) if grade.score is not None else None,
- float(validity_weights["legacy_grade_score"]),
- "旧分级来源内排名归一到 0~1,仅作为过渡信号",
- ),
- _raw_dimension(
- "prior_heat",
- (
- float(grade.prior_total_score)
- if grade.prior_total_score is not None
- else None
- ),
- (
- _clamp(float(grade.prior_total_score) / 4.0)
- if grade.prior_total_score is not None
- else None
- ),
- float(validity_weights["prior_heat"]),
- "四类先验总分按理论范围 0~4 归一",
- ),
- _raw_dimension(
- "source_coverage",
- {"strategies": strategies, "count": len(strategies)},
- _clamp(len(strategies) / 4.0),
- float(validity_weights["source_coverage"]),
- "来源覆盖越多,需求成立的交叉支持越充分",
- ),
- _raw_dimension(
- "posterior_rov",
- {
- "rov": posterior_raw,
- "sample_count": int(grade.posterior_rov_count or 0),
- },
- _clamp(0.5 + posterior_raw / 2.0) if posterior_raw is not None else None,
- float(validity_weights["posterior_rov"]),
- "仅在存在有效样本时使用后验;缺失不按零处理",
- ),
- _raw_dimension(
- "attributed_feedback",
- {
- "validity_influence": attributed_validity,
- "sample_count": int(attributed[2] or 0),
- },
- (
- _clamp(0.5 + attributed_validity * 5.0)
- if attributed_validity is not None
- else None
- ),
- float(validity_weights["attributed_feedback"]),
- "只消费已关联内容且完成表现归因的支持/反对反馈",
- ),
- ]
- validity, _ = _weighted_score(validity_dimensions)
- local_weights = definition["local_priority_weights"]
- local_dimensions = [
- _raw_dimension(
- "validity",
- float(validity),
- float(validity),
- float(local_weights["validity"]),
- "局部供给首先受需求成立度约束",
- ),
- _raw_dimension(
- "prior_heat",
- (
- float(grade.prior_total_score)
- if grade.prior_total_score is not None
- else None
- ),
- (
- _clamp(float(grade.prior_total_score) / 4.0)
- if grade.prior_total_score is not None
- else None
- ),
- float(local_weights["prior_heat"]),
- "当前主题内先验热度",
- ),
- _raw_dimension(
- "grade_band",
- grade.grade,
- _grade_band(grade.grade),
- float(local_weights["grade_band"]),
- "旧等级仅作为过渡期的相对位置快照",
- ),
- _raw_dimension(
- "supply_gap",
- {"known_content_count": len(videos)},
- 1.0 - _clamp(len(videos) / 10.0) if grade.video_list is not None else None,
- float(local_weights["supply_gap"]),
- "已有内容越少,局部补供价值越高;未知供给不按零处理",
- ),
- _raw_dimension(
- "attributed_feedback",
- {
- "priority_influence": attributed_priority,
- "sample_count": int(attributed[2] or 0),
- },
- (
- _clamp(0.5 + attributed_priority * 5.0)
- if attributed_priority is not None
- else None
- ),
- float(local_weights["attributed_feedback"]),
- "内容表现归因对次日局部供给优先级的影响",
- ),
- ]
- missing = [
- item["dimension"]
- for item in validity_dimensions + local_dimensions
- if item["normalized"] is None
- ]
- metrics = {
- "grade": grade.grade,
- "strategies": strategies,
- "videos": videos,
- "posterior_available": posterior_available,
- "posterior_rov_count": int(grade.posterior_rov_count or 0),
- "attributed_feedback_count": int(attributed[2] or 0),
- }
- return metrics, validity_dimensions, local_dimensions, list(dict.fromkeys(missing))
- def _store_contributions(
- session,
- evaluation_id: str,
- score_type: str,
- contributions: list[dict[str, Any]],
- ) -> None:
- for item in contributions:
- session.add(
- DemandScoreContribution(
- contribution_id=_uuid(
- f"score-contribution:{evaluation_id}:"
- f"{score_type}:{item['dimension']}"
- ),
- evaluation_id=evaluation_id,
- score_type=score_type,
- dimension=item["dimension"],
- raw_value_json=item["raw"],
- normalized_value=Decimal(str(round(item["normalized"], 7))),
- configured_weight=Decimal(str(round(item["weight"], 7))),
- effective_weight=Decimal(
- str(round(item["effective_weight"], 7))
- ),
- contribution=Decimal(str(round(item["contribution"], 7))),
- data_status=item["data_status"],
- reason=item["reason"],
- )
- )
- def publish_daily_demand_package(context: StepContext) -> dict[str, Any]:
- with get_session() as session:
- existing = session.scalar(
- select(DailyDemandPackage).where(
- DailyDemandPackage.run_id == context.run_id
- )
- )
- if existing is not None:
- return {
- "success": existing.status == "published",
- "idempotent_replay": True,
- "demand_package_id": existing.demand_package_id,
- "biz_dt": existing.biz_dt,
- "item_count": existing.item_count,
- "status": existing.status,
- "content_hash": existing.content_hash,
- }
- evidence_package = session.scalar(
- select(EvidencePackage).where(EvidencePackage.run_id == context.run_id)
- )
- if evidence_package is None or evidence_package.status != "frozen":
- return {
- "success": False,
- "error_code": "evidence_package_not_frozen",
- "error": "平台需求证据包不存在或尚未冻结",
- }
- versions = list(
- session.scalars(
- select(PlatformDemandVersion)
- .where(PlatformDemandVersion.run_id == context.run_id)
- .order_by(PlatformDemandVersion.platform_demand_version_id)
- ).all()
- )
- if not versions:
- return {
- "success": False,
- "error_code": "platform_demand_versions_missing",
- "error": "没有可发布的平台需求版本",
- }
- strategy = _ensure_strategy(session)
- previous_package_version = int(
- session.scalar(
- select(func.max(DailyDemandPackage.package_version)).where(
- DailyDemandPackage.biz_dt == context.biz_dt
- )
- )
- or 0
- )
- package_id = _uuid(f"daily-demand-package:{context.run_id}")
- package = DailyDemandPackage(
- demand_package_id=package_id,
- run_id=context.run_id,
- evidence_package_id=evidence_package.package_id,
- strategy_version_id=strategy.strategy_version_id,
- biz_dt=context.biz_dt,
- package_version=previous_package_version + 1,
- status="building",
- content_hash="0" * 64,
- item_count=0,
- published_at=None,
- )
- session.add(package)
- session.flush()
- task_payloads: list[dict[str, Any]] = []
- tier_counts: dict[str, int] = {}
- for version in versions:
- grade = session.get(DemandGrade, int(version.source_demand_grade_id))
- if grade is None:
- raise ValueError(
- "平台需求版本引用的 DemandGrade 不存在: "
- f"{version.source_demand_grade_id}"
- )
- definition = dict(strategy.definition_json)
- metrics, validity_dims, local_dims, missing = _evaluation_inputs(
- session,
- version,
- grade,
- definition,
- )
- validity, validity_contributions = _weighted_score(validity_dims)
- local_priority, local_contributions = _weighted_score(local_dims)
- configured_validity_weight = sum(float(item["weight"]) for item in validity_dims)
- available_validity_weight = sum(
- float(item["weight"])
- for item in validity_dims
- if item["normalized"] is not None
- )
- data_confidence = Decimal(
- str(
- round(
- _clamp(
- available_validity_weight
- / configured_validity_weight
- ),
- 5,
- )
- )
- )
- has_posterior = bool(metrics["posterior_available"])
- action_tier = _action_tier(
- definition,
- float(validity),
- float(local_priority),
- float(data_confidence),
- has_posterior=has_posterior,
- )
- posterior_state = "available" if has_posterior else "missing"
- evaluation_id = _uuid(
- f"demand-evaluation:{version.platform_demand_version_id}"
- )
- reason = (
- f"成立度={validity},局部供给优先级={local_priority},"
- f"数据置信度={data_confidence},行动层={action_tier};"
- f"缺失维度={missing or ['无']}。"
- )
- evaluation = DemandEvaluationSnapshot(
- evaluation_id=evaluation_id,
- platform_demand_version_id=version.platform_demand_version_id,
- run_id=context.run_id,
- strategy_version_id=strategy.strategy_version_id,
- biz_dt=context.biz_dt,
- validity_score=validity,
- local_supply_priority=local_priority,
- data_confidence=data_confidence,
- posterior_state=posterior_state,
- action_tier=action_tier,
- metrics_json=metrics,
- missing_dimensions_json=missing,
- decision_reason=reason,
- )
- session.add(evaluation)
- _store_contributions(
- session,
- evaluation_id,
- "validity",
- validity_contributions,
- )
- _store_contributions(
- session,
- evaluation_id,
- "local_supply_priority",
- local_contributions,
- )
- relation_rows = list(
- session.scalars(
- select(PlatformDemandCategoryRel).where(
- PlatformDemandCategoryRel.platform_demand_version_id
- == version.platform_demand_version_id
- )
- ).all()
- )
- aliases = [
- str(value)
- for value in session.scalars(
- select(RawDemandExpression.raw_text)
- .join(
- StandardDemandAlias,
- StandardDemandAlias.expression_id
- == RawDemandExpression.expression_id,
- )
- .where(StandardDemandAlias.term_id == version.term_id)
- ).all()
- ]
- search_terms = list(dict.fromkeys([version.name, *aliases]))
- existing_content = [str(value) for value in metrics["videos"]]
- hypotheses = (
- ["需要通过新内容验证该需求的真实 ROV/VOV"]
- if not has_posterior
- else ["验证后验表现是否能在新增内容上持续"]
- )
- task_payload = {
- "platform_demand_id": version.platform_demand_id,
- "platform_demand_version_id": version.platform_demand_version_id,
- "version_no": version.version_no,
- "biz_dt": context.biz_dt,
- "run_id": context.run_id,
- "strategy_version": strategy.version_key,
- "name": version.name,
- "description": version.description,
- "reason": version.reason,
- "category_relations": [
- {
- "category_id": int(row.category_id),
- "relation_type": row.relation_type,
- "relation_source": row.relation_source,
- "reason": row.reason,
- "confidence": float(row.confidence),
- "is_inferred": bool(row.is_inferred),
- }
- for row in relation_rows
- ],
- "validity_score": float(validity),
- "local_supply_priority": float(local_priority),
- "data_confidence": float(data_confidence),
- "posterior_state": posterior_state,
- "action_tier": action_tier,
- "search_terms": search_terms,
- "exclude_terms": [],
- "hit_rules": {
- "semantic_match_required": True,
- "must_reference_platform_demand_version": True,
- },
- "hypotheses": hypotheses,
- "evidence_gaps": missing,
- "existing_content": existing_content,
- "allocation": {
- "mode": action_tier,
- "exploration_quota": (
- definition["exploration_allocation"]
- if action_tier == "exploration"
- else 0
- ),
- },
- "decision_reason": reason,
- }
- session.add(
- DailyDemandTask(
- task_id=_uuid(
- f"daily-demand-task:{package_id}:"
- f"{version.platform_demand_version_id}"
- ),
- demand_package_id=package_id,
- platform_demand_version_id=version.platform_demand_version_id,
- evaluation_id=evaluation_id,
- action_tier=action_tier,
- search_terms_json=search_terms,
- exclude_terms_json=[],
- hit_rules_json=task_payload["hit_rules"],
- hypotheses_json=hypotheses,
- evidence_gaps_json=missing,
- existing_content_json=existing_content,
- allocation_json=task_payload["allocation"],
- task_payload_json=task_payload,
- )
- )
- task_payloads.append(task_payload)
- tier_counts[action_tier] = tier_counts.get(action_tier, 0) + 1
- package.item_count = len(task_payloads)
- package.content_hash = _hash(task_payloads)
- package.status = "published"
- package.published_at = china_now()
- session.flush()
- return {
- "success": True,
- "idempotent_replay": False,
- "demand_package_id": package_id,
- "biz_dt": context.biz_dt,
- "package_version": package.package_version,
- "strategy_version": strategy.version_key,
- "status": package.status,
- "item_count": package.item_count,
- "tier_counts": tier_counts,
- "content_hash": package.content_hash,
- "published_at": (
- package.published_at.isoformat()
- if isinstance(package.published_at, datetime)
- else None
- ),
- }
|