阶段 当前状态 说明 Phase 0~3 基本完成 Document IR、证据链、人工确认、不可变 PlanData Snapshot、海报/PPT 投影已实现 Phase 4 代码完成 场景、策略、模板版本,校验/发布门禁,确定性 resolver 和严格槽位合并已实现 Phase 5 代码完成 输入冻结、PPTX/海报对账、失败关闭、幂等、心跳、重试和冻结输入重放已实现 Phase 6 基础完成 质量看板、结构化告警、Golden 审批、留存清理、孤儿检查和灰度开关已实现 正式上线 未完成 缺真实样本、生产模板、业务规则签字和灰度观察 目前验证基线: PPT/海报专项测试:107 passed, 1 skipped Vue TypeScript 检查:通过 前端生产构建:通过 代码变更仍在工作区,尚未提交 仓库全量测试仍有既有失败/挂起项,暂时不能宣称全仓测试完全绿色 仍未完成的代码任务主要有: 影子解析差异流水线 目前有灰度开关,但还没有完整的“新旧解析同时运行、字段差异入库、按保司/profile 聚合”的影子比较任务。 自动视觉回归 目前实现的是 DOM 模块、溢出、尺寸、文本和数值检查;还缺基于真实模板和标准图片的像素差异、字体缺失、遮挡和裁切回归。 告警通道接入 后台已经能产生结构化质量告警,但尚未自动推送到邮件、企微或其他通知通道。 留存任务生产化 dry-run、实删服务和失败审计已经具备,但尚未接入周期性 Celery/定时任务,也没有自动重试失败清理批次。 旧链路最终下线 旧 PPT 解析器和海报紧凑解析仍保留为回滚路径。需要全量灰度稳定后才能删除或彻底关闭写入口。 全仓测试收口 需要处理现有无关失败和挂起测试,建立真正全绿的 CI 基线。 仍需外部输入和生产环境完成的事项: 至少 30 份脱敏 Golden PDF,并完成双人标注和精确率验收。 业务专家确认派生公式、缺失值、可比较性和结论策略。 上传并标注真实生产 PPTX 的语义 shape、页面类型和容量。 安装生产字体并建立视觉基准图片。 实际执行数据库迁移 038~040。 完成留存 dry-run、实删演练以及 10% → 30% → 100% 灰度。 观察期通过后开启 SCENARIO_ENGINE_V2,目前它仍默认关闭;真实清理开关也默认关闭。 完整状态记录在 [PPT与海报Phase4至6补充实施记录](D:/work/code/python/coding/baodanagent/docs/PPT与海报Phase4至6补充实施记录_20260802.md)。
132 lines
5.3 KiB
Python
132 lines
5.3 KiB
Python
"""Confirmed PlanData 的只读海报与 PPT 投影。"""
|
|
from __future__ import annotations
|
|
|
|
|
|
def to_poster_projection(plan_data: dict) -> dict:
|
|
identity = plan_data.get("identity") or {}
|
|
policy = plan_data.get("policy") or {}
|
|
scenarios = plan_data.get("benefitScenarios") or []
|
|
rows = _single_comparable_rows(scenarios)
|
|
projection = {
|
|
"plan_type": _value(identity.get("productType")),
|
|
"product_name": _value(identity.get("productName")),
|
|
"age": _value(identity.get("insuredAge")),
|
|
"gender": _value(identity.get("insuredGender")),
|
|
"currency": _value(policy.get("currency")),
|
|
"annual_premium": _value(policy.get("annualPremium")),
|
|
"premium_term": _value(policy.get("premiumPaymentPeriod")),
|
|
"sum_assured": _value(policy.get("sumAssured")),
|
|
"total_premium": _contract_value(policy.get("contractualTotalPremium")),
|
|
"benefit_table": [_legacy_row(row) for row in rows],
|
|
"meta": {
|
|
"projectionVersion": "poster-projection-v1",
|
|
"reconciliationStatus": "confirmed",
|
|
},
|
|
}
|
|
by_year = {
|
|
_value(row.get("policyYear")): _value(row.get("totalSurrenderValue"))
|
|
for row in rows
|
|
if _value(row.get("policyYear")) is not None
|
|
}
|
|
for year in (10, 20, 30):
|
|
projection[f"surrender_value_{year}"] = by_year.get(year)
|
|
return projection
|
|
|
|
|
|
def to_deck_contract(plan_data: dict, *, document_sha256: str | None = None) -> dict:
|
|
identity = plan_data.get("identity") or {}
|
|
policy = plan_data.get("policy") or {}
|
|
scenarios = plan_data.get("benefitScenarios") or []
|
|
comparable = len(scenarios) <= 1
|
|
return {
|
|
"schemaVersion": "deck-contract-v1",
|
|
"kind": _value(identity.get("productType")),
|
|
"productName": _value(identity.get("productName")),
|
|
"insured": {
|
|
"name": _value(identity.get("insuredName")),
|
|
"age": _value(identity.get("insuredAge")),
|
|
"gender": _value(identity.get("insuredGender")),
|
|
"smoker": _value(identity.get("insuredSmoker")),
|
|
},
|
|
"policy": {
|
|
**{key: _contract_value(value) for key, value in policy.items()},
|
|
"currency": _value(policy.get("currency")),
|
|
"sumInsured": _value(policy.get("sumAssured")),
|
|
"annualPremium": _value(policy.get("annualPremium")),
|
|
"payYears": _value(policy.get("premiumPaymentPeriod")),
|
|
"contractualTotalPremium": _contract_value(policy.get("contractualTotalPremium")),
|
|
},
|
|
"benefitScenarios": scenarios,
|
|
"benefitRows": [_deck_row(row) for row in _single_comparable_rows(scenarios)] if comparable else [],
|
|
"withdrawalRows": [_unwrap_row(row) for row in plan_data.get("withdrawals") or []],
|
|
"coverageItems": list(plan_data.get("riders") or []),
|
|
"indexAccounts": list((plan_data.get("assumptions") or {}).get("indexAccounts") or []),
|
|
"source": {"pdfHash": document_sha256 or "", "parser": "plan-data-v1"},
|
|
"comparable": comparable,
|
|
"nonComparableReason": None if comparable else "存在多个利益场景,需由已发布生成策略明确选择后才能比较",
|
|
"reconciliationStatus": "confirmed",
|
|
}
|
|
|
|
|
|
def _single_comparable_rows(scenarios: list[dict]) -> list[dict]:
|
|
if not scenarios:
|
|
return []
|
|
if len(scenarios) == 1:
|
|
return list(scenarios[0].get("rows") or [])
|
|
base = [item for item in scenarios if item.get("scenarioType") == "base"]
|
|
return list(base[0].get("rows") or []) if len(base) == 1 else []
|
|
|
|
|
|
def _legacy_row(row: dict) -> dict:
|
|
return {
|
|
"policy_year": _value(row.get("policyYear")),
|
|
"age": _value(row.get("age")),
|
|
"total_premium_paid": _value(row.get("totalPremiumPaid")),
|
|
"guaranteed_cash_value": _value(row.get("guaranteedCashValue")),
|
|
"non_guaranteed_cash_value": _value(row.get("nonGuaranteedCashValue")),
|
|
"total_surrender_value": _value(row.get("totalSurrenderValue")),
|
|
"death_benefit": _value(row.get("deathBenefit")),
|
|
}
|
|
|
|
|
|
def _deck_row(row: dict) -> dict:
|
|
return {
|
|
**{key: _value(value) for key, value in row.items() if key != "rowVariant"},
|
|
"policyYear": _value(row.get("policyYear")),
|
|
"age": _value(row.get("age")),
|
|
"totalPremiumPaid": _value(row.get("totalPremiumPaid")),
|
|
"guaranteedCashValue": _value(row.get("guaranteedCashValue")),
|
|
"nonGuaranteedCashValue": _value(row.get("nonGuaranteedCashValue")),
|
|
"totalSurrenderValue": _value(row.get("totalSurrenderValue")),
|
|
"deathBenefit": _value(row.get("deathBenefit")),
|
|
"evidence": _row_evidence(row),
|
|
"sourcePage": _first_evidence_page(row),
|
|
}
|
|
|
|
|
|
def _row_evidence(row: dict) -> list[dict]:
|
|
result = []
|
|
for value in row.values():
|
|
if isinstance(value, dict):
|
|
result.extend(value.get("evidence") or [])
|
|
return result
|
|
|
|
|
|
def _first_evidence_page(row: dict):
|
|
evidence = _row_evidence(row)
|
|
return evidence[0].get("pageNumber") if evidence else None
|
|
|
|
|
|
def _unwrap_row(row: dict) -> dict:
|
|
return {key: _value(value) for key, value in row.items()}
|
|
|
|
|
|
def _value(field):
|
|
return field.get("value") if isinstance(field, dict) else field
|
|
|
|
|
|
def _contract_value(field):
|
|
if not isinstance(field, dict) or field.get("status") == "derived":
|
|
return None
|
|
return field.get("value")
|