| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- from __future__ import annotations
- import hashlib
- import json
- import unicodedata
- import uuid
- from decimal import Decimal
- from typing import Any
- from sqlalchemy import func, select
- from supply_infra.db.models.business_harness import (
- EvidencePackage,
- PlatformDemand,
- PlatformDemandCategoryRel,
- PlatformDemandVersion,
- RawDemandExpression,
- StandardDemandAlias,
- StandardDemandTerm,
- )
- from supply_infra.db.models.demand_grade import DemandGrade
- from supply_infra.db.models.demand_grade_category_rel import DemandGradeCategoryRel
- from supply_infra.db.models.demand_feedback import DemandFeedback
- from supply_infra.db.repositories.multi_demand_pool_di_repo import (
- MultiDemandPoolDiRepository,
- )
- from supply_infra.db.session import get_session
- from supply_infra.pipeline.contracts import StepContext
- _NAMESPACE = uuid.UUID("53a14b2c-8ec3-5b88-a684-aa09737b6570")
- def _uuid(key: str) -> str:
- return str(uuid.uuid5(_NAMESPACE, key))
- def _normalized_text(value: str) -> str:
- return " ".join(unicodedata.normalize("NFKC", str(value)).strip().casefold().split())
- def _hash_payload(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 _parse_int_list(raw: str | None) -> list[int]:
- if not raw:
- return []
- try:
- parsed = json.loads(raw)
- except (TypeError, json.JSONDecodeError):
- return []
- if not isinstance(parsed, list):
- return []
- result: list[int] = []
- for value in parsed:
- try:
- result.append(int(value))
- except (TypeError, ValueError):
- continue
- return list(dict.fromkeys(result))
- def _score_confidence(score: Decimal | float | None) -> Decimal:
- if score is None:
- return Decimal("0.50000")
- normalized = max(0.0, min(1.0, float(score) / 100.0))
- return Decimal(str(round(normalized, 5)))
- def _pool_payload(row: Any) -> dict[str, Any]:
- return {
- "id": int(row.id),
- "strategy": row.strategy,
- "demand_id": row.demand_id,
- "demand_name": row.demand_name,
- "weight": float(row.weight) if row.weight is not None else None,
- "type": row.type,
- "video_count": int(row.video_count or 0),
- "video_list": row.video_list,
- "extend": row.extend,
- "real_rov_7d": (
- float(row.real_rov_7d) if row.real_rov_7d is not None else None
- ),
- "real_vov_7d": (
- float(row.real_vov_7d) if row.real_vov_7d is not None else None
- ),
- "biz_dt": row.biz_dt,
- }
- def _existing_result(session, package: EvidencePackage) -> dict[str, Any]:
- version_count = int(
- session.scalar(
- select(func.count())
- .select_from(PlatformDemandVersion)
- .where(PlatformDemandVersion.run_id == package.run_id)
- )
- or 0
- )
- return {
- "success": True,
- "idempotent_replay": True,
- "package_id": package.package_id,
- "biz_dt": package.biz_dt,
- "evidence_count": package.evidence_count,
- "platform_demand_versions": version_count,
- "source_snapshot_hash": package.source_snapshot_hash,
- }
- def materialize_platform_demands(context: StepContext) -> dict[str, Any]:
- """
- Freeze raw expressions and map today's DemandGrade rows into stable,
- append-only platform-demand versions.
- This is an intentionally conservative MVP: one current representative
- demand name maps to one stable canonical object. Future merge/split review
- can re-point terms without changing raw evidence.
- """
- package_id = _uuid(f"evidence-package:{context.run_id}")
- with get_session() as session:
- existing_package = session.scalar(
- select(EvidencePackage).where(EvidencePackage.run_id == context.run_id)
- )
- if existing_package is not None:
- return _existing_result(session, existing_package)
- grades = list(
- session.scalars(
- select(DemandGrade)
- .where(DemandGrade.biz_dt == context.biz_dt)
- .order_by(DemandGrade.id)
- ).all()
- )
- if not grades:
- return {
- "success": False,
- "error_code": "platform_demand_no_grades",
- "error": f"biz_dt={context.biz_dt} 没有可物化的 DemandGrade",
- }
- grade_ids = [int(grade.id) for grade in grades]
- category_map: dict[int, list[int]] = {}
- category_stmt = select(
- DemandGradeCategoryRel.demand_grade_id,
- DemandGradeCategoryRel.category_id,
- ).where(DemandGradeCategoryRel.demand_grade_id.in_(grade_ids))
- for grade_id, category_id in session.execute(category_stmt).all():
- category_map.setdefault(int(grade_id), []).append(int(category_id))
- package = EvidencePackage(
- package_id=package_id,
- run_id=context.run_id,
- biz_dt=context.biz_dt,
- package_version=1,
- status="building",
- source_snapshot_hash="0" * 64,
- source_versions_json={
- "run_id": context.run_id,
- "biz_dt": context.biz_dt,
- "date_snapshot": context.date_snapshot,
- "config_snapshot": context.config_snapshot,
- "input_snapshot": context.input_snapshot,
- },
- evidence_count=0,
- )
- session.add(package)
- session.flush()
- all_content_hashes: set[str] = set()
- created_expressions: set[str] = set()
- version_count = 0
- relation_count = 0
- for grade in grades:
- normalized = _normalized_text(grade.demand_name)
- canonical_key = _hash_payload({"normalized_text": normalized})
- term_id = _uuid(f"standard-term:{canonical_key}")
- platform_demand_id = _uuid(f"platform-demand:{canonical_key}")
- term = session.scalar(
- select(StandardDemandTerm).where(
- StandardDemandTerm.canonical_key == canonical_key
- )
- )
- if term is None:
- term = StandardDemandTerm(
- term_id=term_id,
- canonical_key=canonical_key,
- canonical_text=grade.demand_name,
- normalized_text=normalized,
- status="active",
- )
- session.add(term)
- platform = session.scalar(
- select(PlatformDemand).where(
- PlatformDemand.canonical_key == canonical_key
- )
- )
- if platform is None:
- platform = PlatformDemand(
- platform_demand_id=platform_demand_id,
- canonical_key=canonical_key,
- name=grade.demand_name,
- description=f"可搜索、可供给的平台需求:{grade.demand_name}",
- status="active",
- lifecycle_state="new",
- )
- session.add(platform)
- session.flush()
- pool_ids = _parse_int_list(grade.related_pool_ids)
- pool_rows = MultiDemandPoolDiRepository(session).get_by_ids(pool_ids)
- source_items: list[tuple[str, str, dict[str, Any]]] = []
- for pool_row in pool_rows:
- source_items.append(
- (
- "multi_demand_pool_di",
- str(pool_row.id),
- _pool_payload(pool_row),
- )
- )
- feedback_rows = list(
- session.scalars(
- select(DemandFeedback).where(
- DemandFeedback.platform_demand_id == platform_demand_id,
- DemandFeedback.processing_status == "accepted",
- DemandFeedback.consumed_run_id.is_(None),
- )
- ).all()
- )
- for feedback in feedback_rows:
- source_items.append(
- (
- "demand_feedback",
- str(feedback.id),
- {
- "id": int(feedback.id),
- "demand_name": (
- feedback.content
- or feedback.reason_code
- or feedback.feedback_action
- ),
- "feedback_action": feedback.feedback_action,
- "reason_code": feedback.reason_code,
- "content": feedback.content,
- "resolution_reason": feedback.resolution_reason,
- "impact_json": feedback.impact_json,
- "feedback_user_id": feedback.feedback_user_id,
- "created_at": (
- feedback.created_at.isoformat()
- if feedback.created_at is not None
- else None
- ),
- },
- )
- )
- feedback.consumed_run_id = context.run_id
- if not source_items:
- source_items.append(
- (
- "demand_grade",
- str(grade.id),
- {
- "id": int(grade.id),
- "demand_name": grade.demand_name,
- "grade": grade.grade,
- "score": (
- float(grade.score) if grade.score is not None else None
- ),
- "reason": grade.reason,
- "biz_dt": grade.biz_dt,
- },
- )
- )
- expression_ids: list[str] = []
- expression_hashes: list[str] = []
- for source_type, source_record_id, source_payload in source_items:
- expression_id = _uuid(
- f"raw-expression:{package_id}:{source_type}:{source_record_id}"
- )
- content_hash = _hash_payload(source_payload)
- all_content_hashes.add(content_hash)
- expression_ids.append(expression_id)
- expression_hashes.append(content_hash)
- expression = session.get(RawDemandExpression, expression_id)
- if expression is None:
- expression = RawDemandExpression(
- expression_id=expression_id,
- package_id=package_id,
- source_type=source_type,
- source_record_id=source_record_id,
- raw_text=str(source_payload["demand_name"]),
- original_payload_json=source_payload,
- content_hash=content_hash,
- observed_biz_dt=context.biz_dt,
- data_quality="available",
- )
- session.add(expression)
- created_expressions.add(expression_id)
- alias_id = _uuid(f"standard-alias:{term_id}:{expression_id}")
- if session.get(StandardDemandAlias, alias_id) is None:
- session.add(
- StandardDemandAlias(
- alias_id=alias_id,
- term_id=term_id,
- expression_id=expression_id,
- alias_text=str(source_payload["demand_name"]),
- relation_type="source_expression",
- confidence=Decimal("1.00000"),
- reason="由当日原始需求表达直接归一到代表需求词",
- )
- )
- category_ids = sorted(set(category_map.get(int(grade.id), [])))
- if not category_ids:
- category_ids = sorted(set(_parse_int_list(grade.category_ids)))
- evidence_hash = _hash_payload(
- {
- "grade_id": int(grade.id),
- "grade": grade.grade,
- "score": grade.score,
- "reason": grade.reason,
- "expression_hashes": sorted(expression_hashes),
- "category_ids": category_ids,
- }
- )
- existing_version = session.scalar(
- select(PlatformDemandVersion).where(
- PlatformDemandVersion.platform_demand_id == platform_demand_id,
- PlatformDemandVersion.run_id == context.run_id,
- )
- )
- if existing_version is not None:
- continue
- previous = session.scalar(
- select(PlatformDemandVersion)
- .where(
- PlatformDemandVersion.platform_demand_id == platform_demand_id
- )
- .order_by(PlatformDemandVersion.version_no.desc())
- .limit(1)
- )
- version_no = int(previous.version_no) + 1 if previous is not None else 1
- change_type = (
- "created"
- if previous is None
- else (
- "unchanged"
- if previous.evidence_hash == evidence_hash
- else "updated"
- )
- )
- version_id = _uuid(
- f"platform-demand-version:{platform_demand_id}:{context.run_id}"
- )
- confidence = _score_confidence(grade.score)
- session.add(
- PlatformDemandVersion(
- platform_demand_version_id=version_id,
- platform_demand_id=platform_demand_id,
- term_id=term_id,
- package_id=package_id,
- run_id=context.run_id,
- source_demand_grade_id=int(grade.id),
- version_no=version_no,
- biz_dt=context.biz_dt,
- name=grade.demand_name,
- description=f"可搜索、可验证的平台需求:{grade.demand_name}",
- cognition_confidence=confidence,
- reason=grade.reason,
- change_type=change_type,
- evidence_hash=evidence_hash,
- )
- )
- for index, category_id in enumerate(category_ids):
- relation_type = (
- "primary_attachment" if index == 0 else "auxiliary_attachment"
- )
- session.add(
- PlatformDemandCategoryRel(
- rel_id=_uuid(
- f"platform-category-rel:{version_id}:"
- f"{category_id}:{relation_type}"
- ),
- platform_demand_version_id=version_id,
- category_id=category_id,
- relation_type=relation_type,
- relation_source="demand_grade_snapshot",
- reason=grade.reason,
- confidence=confidence,
- is_inferred=True,
- status="active",
- valid_from_biz_dt=context.biz_dt,
- )
- )
- relation_count += 1
- version_count += 1
- package.source_snapshot_hash = _hash_payload(sorted(all_content_hashes))
- package.evidence_count = len(created_expressions)
- package.status = "frozen"
- session.flush()
- return {
- "success": True,
- "idempotent_replay": False,
- "package_id": package.package_id,
- "biz_dt": context.biz_dt,
- "evidence_count": package.evidence_count,
- "platform_demand_versions": version_count,
- "category_relations": relation_count,
- "source_snapshot_hash": package.source_snapshot_hash,
- }
|