diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index d59a7cb..cc0098c 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -96,7 +96,7 @@ - 2026-07-02 起 PChome controlled apply rollback evidence 必須提供聚合 endpoint;`/api/ai/pchome-growth/mapping-backlog/direct-mapping-retry-candidate-exception-controlled-apply-rollback-evidence-package` 會聚合 receipt replay、drift verifier、drift recovery、compact readback、artifact retention 五類 evidence,輸出 rollback required / ready actions / protected chain / next machine action。此 endpoint 不執行 rollback、不執行 re-apply、不執行 SQL、不寫 DB;0 drift 時必須輸出 no-op evidence,drift detected 時才輸出 check-mode reapply action。 - 2026-07-02 起 `/metrics` 必須匯出 AI automation scheduled health summary gauges:`momo_ai_automation_scheduled_health_summary_total`、`momo_ai_automation_scheduled_health_family_status`、`momo_ai_automation_scheduled_health_primary_human_gate_count`、`momo_ai_automation_scheduled_health_writes_database_count`。Prometheus scrape 不得寄 Telegram、不寫 DB、不執行 current smoke,只讀 scheduled health summary history。 - 2026-07-02 起 P4 source / deployment governance 必須提供 machine-readable report:`scripts/ops/report_source_deploy_runtime_truth.py` 會分層輸出 Gitea / origin / local HEAD source truth、部署檔案 SHA256 readback、正式 `/health` runtime truth、optional container readback 與 GitHub freeze / `momo-db` protected / no DB write / no secret read 安全紅線。此 report 是推 Gitea 與正式部署後的 P4 收斂證據,不得把 source-control success 直接等同 deployment success 或 production runtime success。 -- 2026-07-02 起 `/ai_intelligence` 與 `/observability/overview` 必須同樣套用 AI automation benchmark golden signals:首屏需有 `AI 自動化狀態` 區塊,直接顯示「已自動落地、已驗證、異動狀態、下一步」。`/ai_intelligence` 需以 `/api/ai/pchome-growth/ai-automation-readiness` 只讀更新狀態;`/observability/overview` 需用正式觀測資料分層呈現風險與下一步。raw endpoint、artifact、DB table、manual/human gate 文字不得進首屏訊號。 +- 2026-07-02 起 `/ai_intelligence` 與 `/observability/overview` 必須同樣套用 AI automation benchmark golden signals:首屏需有 `AI 自動化狀態` 區塊,直接顯示「已自動落地、已驗證、異動狀態、下一步」。`/ai_intelligence` 需以 `/api/ai/pchome-growth/ai-automation-surface-summary` 只讀更新首屏,該 endpoint 由 `/api/ai/pchome-growth/ai-automation-readiness` 聚合 safe lanes 後輸出 `golden_signals`、`safe_automation_lanes`、`surface_contract`、`primary_human_gate_count=0`、`writes_database_count=0` 與 `next_machine_action`;`/observability/overview` 需用正式觀測資料分層呈現風險與下一步。raw endpoint、artifact、DB table、manual/human gate 文字不得進首屏訊號。 - V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價;每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據,並保留下一步按鈕。單位價候選需顯示單位價與單位,候選待確認或缺資料則以「待補 / 候選待確認」呈現,不得捏造價格。 - V10.645 起 `/ai_intelligence` 的商品明細分流切換後,必須顯示「這類商品怎麼處理」的行動摘要,包含件數、近 7 天業績、平均可信度、最大價差、代表商品與主按鈕;使用者不得只能看到商品列表而不知道下一步。 - V10.646 起 `/ai_intelligence` 的商品明細必須提供搜尋與排序;搜尋至少涵蓋商品、分類、商品編號與 MOMO 候選資訊,排序至少支援優先級、近 7 天業績、價差、下滑幅度與可信度。搜尋/排序後的行動摘要與明細列表必須使用同一批結果。 diff --git a/docs/guides/external_professional_benchmark.md b/docs/guides/external_professional_benchmark.md index 1999ec1..c0ec74b 100644 --- a/docs/guides/external_professional_benchmark.md +++ b/docs/guides/external_professional_benchmark.md @@ -80,11 +80,14 @@ Baymard 的商品頁與比較 UX 研究強調:使用者需要清楚的 product 3. 將 PPT / AI payload 的比價項目拆成 identity evidence 與 offer evidence。 4. 每週 benchmark 結果若命中上述 TODO,回寫 `TODO_NEXT_STEPS.txt` 或新增 ADR / memory。 5. 將 PChome AI automation benchmark guardrails 套到後續 AI Agent surfaces 與每條 safe automation lane 的 first-viewport summary。 + - 已完成: `/api/ai/pchome-growth/ai-automation-surface-summary` 以 `golden_signals` 固化「已自動落地、已驗證、異動狀態、下一步」。 + - 已完成: `/ai_intelligence` 首屏直接消費 surface summary,raw receipt / hash / DB table / endpoint 細節留在 evidence-on-demand 層。 ## 參考來源 - Google Merchant Center Product data specification: https://support.google.com/merchants/answer/7052112 - Google Search Central Product structured data: https://developers.google.com/search/docs/appearance/structured-data/product +- Google SRE The Four Golden Signals: https://sre.google/sre-book/monitoring-distributed-systems/ - Schema.org Product / Offer / AggregateOffer: https://schema.org/Product, https://schema.org/Offer, https://schema.org/AggregateOffer - Baymard Product Page UX Best Practices: https://baymard.com/blog/current-state-ecommerce-product-page-ux - Baymard Product Comparison UX: https://baymard.com/blog/provide-comparison-features diff --git a/docs/guides/pchome_ai_automation_priority_backlog.md b/docs/guides/pchome_ai_automation_priority_backlog.md index 10860bb..c70780c 100644 --- a/docs/guides/pchome_ai_automation_priority_backlog.md +++ b/docs/guides/pchome_ai_automation_priority_backlog.md @@ -163,6 +163,8 @@ - 已完成: 兩頁都明確呈現「已自動落地、已驗證、異動狀態、下一步」。 - 已完成: `tests/test_ai_surface_benchmark_guardrails.py` 鎖住 DOM、下一步優先與 evidence-on-demand wording。 2. 為後續 safe automation lanes 建立同樣的 first-viewport summary。 + - 已完成: `/api/ai/pchome-growth/ai-automation-surface-summary` 會把 readiness safe lanes 收斂成 `golden_signals`、`safe_automation_lanes`、`surface_contract`、`next_machine_action` 與 no-write 安全欄位。 + - 已完成: `/ai_intelligence` 首屏改吃 surface summary,不再自行拼接 receipt replay / drift verifier 欄位。 完成標準: diff --git a/routes/README.md b/routes/README.md index 5e15a54..67cb762 100644 --- a/routes/README.md +++ b/routes/README.md @@ -25,7 +25,7 @@ | `market_intel_review_post_ai_routes.py` | 市場情報 AI summary persistence / Telegram dispatch 後續只讀延伸 API(掛在 `market_intel_review_bp`) | AI summary persistence run / Telegram dispatch compatibility family | | `market_intel_review_report_routes.py` | 市場情報 report input / report run package / report run readiness / report run receipt / report closeout / report archive / report catalog handoff 後續只讀延伸 API(掛在 `market_intel_review_bp`) | AI summary persistence Telegram report compatibility family | | `api_routes.py` | 通用任務與查詢 API | `/api/run_task`, `/api/history/*` | -| `ai_routes.py` | AI 推薦、競情儀表板與 PChome 成長作戰 API | `/ai_recommend`, `/ai_intelligence`, `/api/ai/status`, `/api/ai/icaim/dashboard`, `/api/ai/pchome-growth/opportunities`, `/api/ai/pchome-growth/mapping-backlog`, `/api/ai/pchome-growth/mapping-backlog/operator-preview`, `/api/ai/pchome-growth/mapping-backlog/direct-mapping-auto-search-package`, `/api/ai/pchome-growth/mapping-backlog/direct-mapping-candidate-decision-package`, `/api/ai/pchome-growth/ai-automation-readiness`, `/api/ai/pchome-growth/mapping-backlog/evidence-enrichment-preview`, `/api/ai/pchome-growth/mapping-backlog/evidence-source-preview`, `/api/ai/pchome-growth/mapping-backlog/evidence-fetch-gate`, `/api/ai/pchome-growth/mapping-backlog/evidence-merge-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-receipt-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-persistence-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-schema-migration-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-apply-readiness-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-generation-request`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-apply-gate-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-request-gate-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-verifier-artifact-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-final-handoff-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-intake`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-execution-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-evidence-intake`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-detached-verification-evidence-validation`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-verifier-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-evidence-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-evidence-execution-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-apply-final-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-runner-readiness`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-execution-plan-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-command-artifact-closeout`, `/api/ai/pchome-growth/backfill-momo-candidates`, `/api/ai/pchome-growth/source-contract`, `/api/ai/pchome-growth/external-offers/csv-dry-run` | +| `ai_routes.py` | AI 推薦、競情儀表板與 PChome 成長作戰 API | `/ai_recommend`, `/ai_intelligence`, `/api/ai/status`, `/api/ai/icaim/dashboard`, `/api/ai/pchome-growth/opportunities`, `/api/ai/pchome-growth/mapping-backlog`, `/api/ai/pchome-growth/mapping-backlog/operator-preview`, `/api/ai/pchome-growth/mapping-backlog/direct-mapping-auto-search-package`, `/api/ai/pchome-growth/mapping-backlog/direct-mapping-candidate-decision-package`, `/api/ai/pchome-growth/ai-automation-readiness`, `/api/ai/pchome-growth/ai-automation-surface-summary`, `/api/ai/pchome-growth/mapping-backlog/evidence-enrichment-preview`, `/api/ai/pchome-growth/mapping-backlog/evidence-source-preview`, `/api/ai/pchome-growth/mapping-backlog/evidence-fetch-gate`, `/api/ai/pchome-growth/mapping-backlog/evidence-merge-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-receipt-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-persistence-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-schema-migration-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-apply-readiness-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-generation-request`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-apply-gate-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-request-gate-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-verifier-artifact-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-final-handoff-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-preview`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-intake`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-execution-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signed-receipt-evidence-intake`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-detached-verification-evidence-validation`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-verifier-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-evidence-execution-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-evidence-execution-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-apply-final-preflight`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-package`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-runner-readiness`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-execution-plan-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-command-artifact-closeout`, `/api/ai/pchome-growth/backfill-momo-candidates`, `/api/ai/pchome-growth/source-contract`, `/api/ai/pchome-growth/external-offers/csv-dry-run` | | `ai_routes.py` | PChome DB apply controlled dry-run 補充索引 | `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-runner-execution-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-post-receipt-parser-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-no-apply-enforcement-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-final-executor-guard-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-pre-apply-replay-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-apply-executor-readiness-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-invocation-receipt-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-no-write-invocation-package-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-execution-preflight-guard-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-runner-invocation-boundary-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-no-execution-receipt-handoff-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-final-no-runner-execution-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-controlled-executor-quarantine-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-execution-envelope-freeze-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-frozen-envelope-verifier-handoff-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-verifier-invocation-lock-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-verifier-no-execution-receipt-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-verifier-receipt-persistence-guard-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-receipt-persistence-storage-boundary-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-storage-boundary-no-write-ledger-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-no-write-ledger-retention-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-retention-boundary-no-write-archive-proof-closeout`, `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-archive-retention-sealed-handoff-proof-closeout` | | `export_routes.py` | 匯出功能 | `/api/export/*` | | `import_routes.py` | 匯入功能 | `/api/import_excel`, `/api/import/monthly_summary` | diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 5178ba0..dcc4c81 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -2813,6 +2813,78 @@ def api_pchome_growth_ai_automation_readiness(): }), 500 +@ai_bp.route('/api/ai/pchome-growth/ai-automation-surface-summary') +@login_required +def api_pchome_growth_ai_automation_surface_summary(): + """First-viewport AI automation summary for product/admin surfaces.""" + try: + from config import DATABASE_PATH + from services.pchome_revenue_growth_service import build_pchome_growth_opportunities + from services.pchome_mapping_backlog_service import ( + build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_drift_verifier_package, + build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package, + build_pchome_growth_ai_automation_readiness, + build_pchome_growth_ai_automation_surface_summary, + ) + + force_refresh = str(request.args.get('refresh') or '').strip().lower() in {'1', 'true', 'yes'} + execute_search = str(request.args.get('execute_search') or '').strip().lower() in {'1', 'true', 'yes'} + execute_fetch = str(request.args.get('execute_fetch') or '').strip().lower() in {'1', 'true', 'yes'} + include_receipt_replay = str(request.args.get('include_receipt_replay', 'true') or '').strip().lower() in {'1', 'true', 'yes'} + include_drift_verifier = str(request.args.get('include_drift_verifier', 'true') or '').strip().lower() in {'1', 'true', 'yes'} + limit = request.args.get('limit', 20, type=int) + batch_size = request.args.get('batch_size', 8, type=int) + limit = max(5, min(limit, 50)) + + payload = None + if not force_refresh: + payload = _get_cached_pchome_growth_payload() + + if payload is None: + engine = _create_icaim_dashboard_engine(DATABASE_PATH) + try: + payload = build_pchome_growth_opportunities(engine, limit=limit) + finally: + engine.dispose() + payload["cache_state"] = "fresh" + _set_pchome_growth_cache(payload) + + receipt_replay = None + drift_verifier = None + if include_receipt_replay or include_drift_verifier: + replay_engine = _create_icaim_dashboard_engine(DATABASE_PATH) + try: + receipt_replay = build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package( + materialize_artifacts=False, + engine=replay_engine, + ) + if include_drift_verifier: + drift_verifier = build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_drift_verifier_package( + engine=replay_engine, + source_receipt_replay=receipt_replay, + ) + finally: + replay_engine.dispose() + + readiness = build_pchome_growth_ai_automation_readiness( + payload, + batch_size=batch_size, + execute_search=execute_search, + execute_fetch=execute_fetch, + controlled_apply_receipt_replay=receipt_replay, + controlled_apply_drift_verifier=drift_verifier, + ) + surface_summary = build_pchome_growth_ai_automation_surface_summary(readiness) + surface_summary["source_endpoint"] = "/api/ai/pchome-growth/ai-automation-readiness" + return jsonify(surface_summary) + except Exception as exc: + logger.error("[PChomeGrowth] AI automation surface summary 讀取失敗: %s", exc, exc_info=True) + return jsonify({ + "success": False, + "error": "PChome AI 自動化首屏摘要暫時無法讀取,請稍後再試。", + }), 500 + + @ai_bp.route('/api/ai/pchome-growth/mapping-backlog/evidence-enrichment-preview') @login_required def api_pchome_growth_evidence_enrichment_preview(): diff --git a/services/pchome_mapping_backlog_service.py b/services/pchome_mapping_backlog_service.py index c72609e..1f8382d 100644 --- a/services/pchome_mapping_backlog_service.py +++ b/services/pchome_mapping_backlog_service.py @@ -93,6 +93,9 @@ DIRECT_MAPPING_RETRY_CANDIDATE_EXCEPTION_CONTROLLED_APPLY_ROLLBACK_EVIDENCE_POLI "read_only_pchome_growth_direct_mapping_retry_candidate_exception_controlled_apply_rollback_evidence" ) AI_AUTOMATION_READINESS_POLICY = "read_only_pchome_growth_ai_automation_readiness" +AI_AUTOMATION_SURFACE_SUMMARY_POLICY = ( + "read_only_pchome_growth_ai_automation_surface_summary" +) EVIDENCE_ENRICHMENT_PREVIEW_POLICY = "read_only_pchome_growth_evidence_enrichment_preview" EVIDENCE_SOURCE_PREVIEW_POLICY = "read_only_pchome_growth_evidence_source_preview" PRODUCT_PAGE_EVIDENCE_PARSER_POLICY = "read_only_pchome_product_page_evidence_parser" @@ -7067,6 +7070,255 @@ def build_pchome_growth_ai_automation_readiness( } +def _surface_signal( + key: str, + label: str, + value: str, + detail: str, + status: str, + next_machine_action: str, +) -> dict[str, Any]: + return { + "key": key, + "label": label, + "value": value, + "detail": detail, + "status": status, + "next_machine_action": next_machine_action, + } + + +def _count_surface_lanes(lanes: list[dict[str, Any]], statuses: set[str]) -> int: + return sum(1 for lane in lanes if str(lane.get("status") or "") in statuses) + + +def _safe_surface_lanes(lanes: list[dict[str, Any]]) -> list[dict[str, Any]]: + safe_lanes: list[dict[str, Any]] = [] + for lane in lanes: + safe_lanes.append( + { + "key": lane.get("key") or "", + "label": lane.get("label") or "", + "status": lane.get("status") or "unknown", + "value": lane.get("value") or 0, + "detail": lane.get("detail") or "", + "next_machine_action": lane.get("next_action") or "", + "writes_database": bool(lane.get("writes_database")), + "ai_exception_mode": lane.get("ai_exception_mode") + or AI_EXCEPTION_MODE_MACHINE_VERIFIABLE, + } + ) + return safe_lanes + + +def _pick_next_surface_machine_action( + summary: dict[str, Any], + lanes: list[dict[str, Any]], +) -> str: + if int(summary.get("controlled_apply_drift_count") or 0): + return "執行漂移恢復、重新回讀與 rollback path 比對" + if int(summary.get("exception_resolution_closeout_receipt_count") or 0): + return "執行 retry search 與 verifier receipts 收斂" + if int(summary.get("exception_auto_resolution_artifact_count") or 0): + return "收斂候選例外的命名證據、組合判別與單位基準" + if int(summary.get("candidate_decision_count") or 0): + return "把候選決策送入 no-write receipt 與證據 verifier" + if int(summary.get("selected_search_target_count") or 0): + return "執行 controlled read-only 同款搜尋候選" + for wanted_status in ("blocked", "blocked_until_verifier", "waiting", "planned"): + for lane in lanes: + if str(lane.get("status") or "") == wanted_status: + next_action = str(lane.get("next_action") or "").strip() + if next_action: + return next_action + return "持續回讀、漂移監控與可回滾驗證" + + +def build_pchome_growth_ai_automation_surface_summary( + readiness: dict[str, Any], +) -> dict[str, Any]: + """Convert readiness internals into a first-viewport product summary.""" + summary = readiness.get("summary") or {} + lanes = [lane for lane in readiness.get("automation_lanes") or [] if isinstance(lane, dict)] + safe_lanes = _safe_surface_lanes(lanes) + safe_lane_count = len(safe_lanes) + ready_lane_count = _count_surface_lanes(lanes, {"active", "ready", "completed"}) + completed_lane_count = _count_surface_lanes(lanes, {"completed"}) + blocked_lane_count = _count_surface_lanes(lanes, {"blocked", "blocked_until_verifier"}) + primary_human_gate_count = int(summary.get(PRIMARY_HUMAN_GATE_COUNT_KEY) or 0) + writes_database_count = int(summary.get("writes_database_count") or 0) + direct_mapping_count = int(summary.get("direct_mapping_count") or 0) + selected_search_target_count = int(summary.get("selected_search_target_count") or 0) + candidate_decision_count = int(summary.get("candidate_decision_count") or 0) + ready_receipt_count = int(summary.get("ready_receipt_count") or 0) + receipt_count = int(summary.get("receipt_count") or 0) + replay_selector_count = int( + summary.get("controlled_apply_replay_selector_count") or 0 + ) + replay_readback_pass_count = int( + summary.get("controlled_apply_replay_readback_pass_count") or 0 + ) + receipt_materialized_count = int( + summary.get("controlled_apply_receipt_materialized_count") or 0 + ) + closeout_verified_count = int( + summary.get("controlled_apply_closeout_verified_count") or 0 + ) + drift_count = int(summary.get("controlled_apply_drift_count") or 0) + drift_verified_count = int(summary.get("controlled_apply_drift_verified_count") or 0) + exception_count = int(summary.get("ai_exception_count") or 0) + next_machine_action = _pick_next_surface_machine_action(summary, lanes) + + if closeout_verified_count: + landed_value = "已完成回讀" + landed_detail = ( + f"{replay_readback_pass_count}/{replay_selector_count} 筆受控落地已收斂," + "持續漂移監控。" + ) + landed_status = "good" + elif receipt_materialized_count: + landed_value = f"{receipt_materialized_count} 筆收據" + landed_detail = "落地收據已產生,等待同輪回讀完成。" + landed_status = "warn" + elif selected_search_target_count or candidate_decision_count or receipt_count: + landed_value = "AI 主流程接管" + landed_detail = ( + f"{selected_search_target_count} 筆搜尋目標、" + f"{candidate_decision_count} 筆候選決策、{ready_receipt_count} 筆收據就緒。" + ) + landed_status = "good" + else: + landed_value = "等待資料" + landed_detail = f"目前還有 {direct_mapping_count} 筆商品對應缺口等待自動化輸入。" + landed_status = "waiting" + + if replay_readback_pass_count: + verified_value = f"{replay_readback_pass_count} 筆已回讀" + verified_detail = "受控落地、收據與正式資料回讀可追蹤。" + verified_status = "good" if not drift_count else "warn" + elif ready_receipt_count: + verified_value = f"{ready_receipt_count} 筆收據就緒" + verified_detail = "證據收據已可進 verifier / dry-run persistence。" + verified_status = "good" + elif ready_lane_count: + verified_value = f"{ready_lane_count}/{safe_lane_count} 條通過" + verified_detail = "安全 lane 已集中成同一個第一視窗摘要。" + verified_status = "warn" if blocked_lane_count else "good" + else: + verified_value = "等待回讀" + verified_detail = "等待下一批可驗證候選或證據收據。" + verified_status = "waiting" + + if drift_count: + change_value = f"{drift_count} 筆漂移" + change_detail = "已進入自動恢復與 rollback path 比對。" + change_status = "bad" + elif exception_count: + change_value = f"{exception_count} 筆 AI 例外" + change_detail = "例外已轉成 machine-verifiable auto-resolution。" + change_status = "warn" + elif blocked_lane_count: + change_value = f"{blocked_lane_count} 條待驗證" + change_detail = "等待 verifier、rollback 與回讀證明補齊。" + change_status = "warn" + else: + change_value = "無漂移" + change_detail = ( + f"漂移驗證 {drift_verified_count} 筆;異動只呈現營運狀態,證據留在證據層。" + ) + change_status = "good" + + signals = [ + _surface_signal( + "automated-landing", + "已自動落地", + landed_value, + landed_detail, + landed_status, + next_machine_action, + ), + _surface_signal( + "verified", + "已驗證", + verified_value, + verified_detail, + verified_status, + next_machine_action, + ), + _surface_signal( + "change-state", + "異動狀態", + change_value, + change_detail, + change_status, + next_machine_action, + ), + _surface_signal( + "next-machine-action", + "下一步", + next_machine_action, + "下一步只顯示可自動執行、可回讀、可回滾的機器動作。", + "good" if not drift_count else "warn", + next_machine_action, + ), + ] + + return { + "policy": AI_AUTOMATION_SURFACE_SUMMARY_POLICY, + "success": bool(readiness.get("success")), + "result": readiness.get("result") or "AI_AUTOMATION_WAITING_FOR_GROWTH_INPUT", + "generated_at": readiness.get("generated_at"), + "summary": { + "safe_lane_count": safe_lane_count, + "ready_lane_count": ready_lane_count, + "completed_lane_count": completed_lane_count, + "blocked_lane_count": blocked_lane_count, + "direct_mapping_count": direct_mapping_count, + "selected_search_target_count": selected_search_target_count, + "candidate_decision_count": candidate_decision_count, + "ready_receipt_count": ready_receipt_count, + "controlled_apply_replay_selector_count": replay_selector_count, + "controlled_apply_replay_readback_pass_count": replay_readback_pass_count, + "controlled_apply_receipt_materialized_count": receipt_materialized_count, + "controlled_apply_closeout_verified_count": closeout_verified_count, + "controlled_apply_drift_count": drift_count, + "controlled_apply_drift_verified_count": drift_verified_count, + "ai_exception_count": exception_count, + PRIMARY_HUMAN_GATE_COUNT_KEY: primary_human_gate_count, + LEGACY_PRIMARY_FLOW_COUNT_KEY: 0, + "writes_database_count": writes_database_count, + "external_network_execute_count": int( + summary.get("external_network_execute_count") or 0 + ), + "next_machine_action": next_machine_action, + }, + "golden_signals": signals, + "safe_automation_lanes": safe_lanes, + "surface_contract": { + "first_viewport_required": True, + "signal_keys": [ + "automated-landing", + "verified", + "change-state", + "next-machine-action", + ], + "raw_evidence_hidden_from_first_viewport": True, + "primary_flow": "ai_controlled", + "exception_resolution": "ai_machine_verifiable", + }, + "safety": { + "read_only_preview": True, + "writes_database": False, + "writes_database_count": writes_database_count, + "persists_receipt": False, + "dispatches_telegram": False, + "llm_calls_in_preview": False, + "gemini_allowed": False, + PRIMARY_HUMAN_GATE_COUNT_KEY: primary_human_gate_count, + }, + } + + def _receipt_payload_hash(receipt: dict[str, Any]) -> str: payload = { "receipt_id": receipt.get("receipt_id") or "", diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index 01b9b29..68f6af7 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -3702,25 +3702,26 @@ async function loadDashboard() { async function loadAutomationBenchmarkStrip() { try { - const response = await fetch('/api/ai/pchome-growth/ai-automation-readiness?include_receipt_replay=true&include_drift_verifier=true&limit=20'); + const response = await fetch('/api/ai/pchome-growth/ai-automation-surface-summary?include_receipt_replay=true&include_drift_verifier=true&limit=20'); const payload = await readJsonResponse(response); - const summary = payload.summary || {}; - const receiptSummary = (payload.receipt_replay || {}).summary || {}; - const driftSummary = (payload.drift_verifier || {}).summary || {}; - const primaryGateCount = Number(summary.primary_human_gate_count || 0); - const selectorCount = Number(receiptSummary.selector_count || receiptSummary.target_selector_count || 0); - const replayPassCount = Number(receiptSummary.receipt_replay_pass_count || receiptSummary.post_apply_readback_pass_count || 0); - const driftCount = Number(driftSummary.drift_count || 0); - const nextAction = payload.next_machine_action || summary.next_machine_action || '持續回讀與漂移監控'; + const signals = Array.isArray(payload.golden_signals) ? payload.golden_signals : []; + const signalByKey = signals.reduce((acc, signal) => { + acc[signal.key] = signal; + return acc; + }, {}); + const landed = signalByKey['automated-landing'] || {}; + const verified = signalByKey.verified || {}; + const change = signalByKey['change-state'] || {}; + const nextAction = signalByKey['next-machine-action'] || {}; - setText('aiBenchmarkLanded', selectorCount ? `${formatCount(selectorCount)} 筆已落地` : '閉環已接入'); - setText('aiBenchmarkLandedDetail', primaryGateCount === 0 ? '主流程無人工阻斷,持續由 AI 受控回讀。' : `仍有 ${formatCount(primaryGateCount)} 個例外需 AI 收斂。`); - setText('aiBenchmarkVerified', replayPassCount ? `${formatCount(replayPassCount)} 筆已回讀` : '收據回讀就緒'); - setText('aiBenchmarkVerifiedDetail', driftCount === 0 ? '目前沒有漂移,證據可追蹤。' : `${formatCount(driftCount)} 筆漂移,進入自動恢復建議。`); - setText('aiBenchmarkChange', driftCount === 0 ? '無異動待修復' : `${formatCount(driftCount)} 筆待修復`); - setText('aiBenchmarkChangeDetail', '異動只呈現營運狀態,詳細收據留在證據層。'); - setText('aiBenchmarkNextAction', humanizeMachineAction(nextAction)); - setText('aiBenchmarkNextActionDetail', '下一步優先顯示可自動執行、可回讀、可回滾的動作。'); + setText('aiBenchmarkLanded', landed.value || 'AI 主流程接管'); + setText('aiBenchmarkLandedDetail', landed.detail || 'AI 自動化狀態已接入首屏摘要。'); + setText('aiBenchmarkVerified', verified.value || '等待回讀'); + setText('aiBenchmarkVerifiedDetail', verified.detail || '等待收據與漂移 verifier 回傳。'); + setText('aiBenchmarkChange', change.value || '整理中'); + setText('aiBenchmarkChangeDetail', change.detail || '異動只呈現營運狀態,詳細證據留在證據層。'); + setText('aiBenchmarkNextAction', humanizeMachineAction(nextAction.value || nextAction.next_machine_action)); + setText('aiBenchmarkNextActionDetail', nextAction.detail || '下一步優先顯示可自動執行、可回讀、可回滾的動作。'); } catch (error) { setText('aiBenchmarkLanded', '等待回讀'); setText('aiBenchmarkVerified', '暫無狀態'); diff --git a/tests/test_ai_surface_benchmark_guardrails.py b/tests/test_ai_surface_benchmark_guardrails.py index 32f0abb..0d55476 100644 --- a/tests/test_ai_surface_benchmark_guardrails.py +++ b/tests/test_ai_surface_benchmark_guardrails.py @@ -25,7 +25,8 @@ def test_ai_intelligence_first_viewport_has_benchmark_golden_signals(): assert "異動狀態" in strip assert "下一步" in strip assert "loadAutomationBenchmarkStrip()" in template - assert "/api/ai/pchome-growth/ai-automation-readiness" in template + assert "/api/ai/pchome-growth/ai-automation-surface-summary" in template + assert "golden_signals" in template def test_observability_overview_first_viewport_has_benchmark_golden_signals(): diff --git a/tests/test_pchome_candidate_decision_lane_closeout.py b/tests/test_pchome_candidate_decision_lane_closeout.py index 1336178..df8ff81 100644 --- a/tests/test_pchome_candidate_decision_lane_closeout.py +++ b/tests/test_pchome_candidate_decision_lane_closeout.py @@ -165,3 +165,39 @@ def test_candidate_decision_lane_closeout_route_defaults_to_cached_no_db_no_sear assert payload["product_readiness"]["primary_human_gate_count"] == 0 assert payload["safety"]["executes_search"] is False assert payload["safety"]["writes_database"] is False + + +def test_ai_automation_surface_summary_route_defaults_to_cached_no_db(monkeypatch): + from routes import ai_routes as routes + + monkeypatch.setattr(routes, "_get_cached_pchome_growth_payload", lambda: _payload()) + + def fail_engine(database_path): + raise AssertionError("cached AI automation surface summary should not open a DB engine") + + monkeypatch.setattr(routes, "_create_icaim_dashboard_engine", fail_engine) + + app = Flask(__name__) + with app.test_request_context( + "/api/ai/pchome-growth/ai-automation-surface-summary" + "?batch_size=1&include_receipt_replay=false&include_drift_verifier=false" + ): + response = routes.api_pchome_growth_ai_automation_surface_summary.__wrapped__() + + payload = response.get_json() + signals = {signal["key"]: signal for signal in payload["golden_signals"]} + assert payload["success"] is True + assert payload["policy"] == "read_only_pchome_growth_ai_automation_surface_summary" + assert payload["source_endpoint"] == "/api/ai/pchome-growth/ai-automation-readiness" + assert payload["summary"]["safe_lane_count"] >= 4 + assert payload["summary"]["primary_human_gate_count"] == 0 + assert payload["summary"]["writes_database_count"] == 0 + assert set(signals) == { + "automated-landing", + "verified", + "change-state", + "next-machine-action", + } + assert signals["next-machine-action"]["next_machine_action"] + assert payload["safety"]["writes_database"] is False + assert payload["safety"]["llm_calls_in_preview"] is False diff --git a/tests/test_pchome_mapping_backlog_report.py b/tests/test_pchome_mapping_backlog_report.py index 936a152..9de869c 100644 --- a/tests/test_pchome_mapping_backlog_report.py +++ b/tests/test_pchome_mapping_backlog_report.py @@ -89,6 +89,7 @@ from services.pchome_mapping_backlog_service import ( build_pchome_direct_mapping_retry_candidate_exception_closeout_verifier_input_package, build_pchome_direct_mapping_retry_candidate_exception_resolution_closeout_package, build_pchome_growth_ai_automation_readiness, + build_pchome_growth_ai_automation_surface_summary, build_pchome_mapping_operator_preview, parse_pchome_product_page_evidence_html, parse_unit_package_basis, @@ -1684,6 +1685,97 @@ def test_ai_automation_readiness_surfaces_controlled_apply_drift_detected(): assert readiness["safety"]["writes_database"] is False +def test_ai_automation_surface_summary_turns_readiness_into_golden_signals(): + readiness = build_pchome_growth_ai_automation_readiness( + _payload(), + batch_size=1, + controlled_apply_receipt_replay={ + "summary": { + "target_selector_count": 4, + "post_apply_readback_pass_count": 4, + "executor_receipt_ready_count": 1, + "executor_receipt_materialized_count": 1, + "executor_receipt_hash_match_count": 1, + }, + }, + controlled_apply_drift_verifier={ + "summary": { + "target_selector_count": 4, + "post_apply_readback_pass_count": 4, + "drift_count": 0, + "drift_verified_count": 1, + "drift_verifier_artifact_materialized_count": 1, + "drift_verifier_artifact_hash_match_count": 1, + }, + }, + ) + + surface = build_pchome_growth_ai_automation_surface_summary(readiness) + signals = {signal["key"]: signal for signal in surface["golden_signals"]} + + assert surface["policy"] == "read_only_pchome_growth_ai_automation_surface_summary" + assert surface["summary"]["safe_lane_count"] >= 4 + assert surface["summary"]["ready_lane_count"] >= 1 + assert surface["summary"]["completed_lane_count"] >= 1 + assert surface["summary"]["controlled_apply_replay_readback_pass_count"] == 4 + assert surface["summary"]["controlled_apply_drift_count"] == 0 + assert surface["summary"]["primary_human_gate_count"] == 0 + assert surface["summary"]["manual_required_as_primary_flow_count"] == 0 + assert surface["summary"]["writes_database_count"] == 0 + assert set(signals) == { + "automated-landing", + "verified", + "change-state", + "next-machine-action", + } + assert signals["automated-landing"]["label"] == "已自動落地" + assert signals["verified"]["label"] == "已驗證" + assert signals["change-state"]["label"] == "異動狀態" + assert signals["next-machine-action"]["label"] == "下一步" + assert signals["automated-landing"]["value"] == "已完成回讀" + assert "4/4" in signals["automated-landing"]["detail"] + assert signals["verified"]["value"] == "4 筆已回讀" + assert signals["change-state"]["value"] == "無漂移" + assert surface["surface_contract"]["first_viewport_required"] is True + assert surface["surface_contract"]["raw_evidence_hidden_from_first_viewport"] is True + assert surface["safety"]["writes_database"] is False + assert surface["safety"]["llm_calls_in_preview"] is False + + +def test_ai_automation_surface_summary_prioritizes_drift_recovery_action(): + readiness = build_pchome_growth_ai_automation_readiness( + _payload(), + batch_size=1, + controlled_apply_receipt_replay={ + "summary": { + "target_selector_count": 4, + "post_apply_readback_pass_count": 3, + "executor_receipt_ready_count": 0, + "executor_receipt_materialized_count": 1, + "executor_receipt_hash_match_count": 0, + }, + }, + controlled_apply_drift_verifier={ + "summary": { + "target_selector_count": 4, + "post_apply_readback_pass_count": 3, + "drift_count": 1, + "drift_verified_count": 0, + }, + }, + ) + + surface = build_pchome_growth_ai_automation_surface_summary(readiness) + signals = {signal["key"]: signal for signal in surface["golden_signals"]} + + assert surface["summary"]["controlled_apply_drift_count"] == 1 + assert surface["summary"]["next_machine_action"] == "執行漂移恢復、重新回讀與 rollback path 比對" + assert signals["change-state"]["value"] == "1 筆漂移" + assert signals["change-state"]["status"] == "bad" + assert signals["next-machine-action"]["value"] == surface["summary"]["next_machine_action"] + assert surface["safety"]["writes_database"] is False + + def test_ai_automation_readiness_reports_candidate_decisions_after_controlled_search(): call_count = {"search": 0}