Files
ewoooc/services/market_intel/opportunity_evidence.py
OoO 8f6b3a4b41
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
feat(market-intel): add opportunity evidence plan preview
2026-05-18 19:07:45 +08:00

257 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""市場情報機會與威脅 evidence bundle preview。
只定義未來 scoring / alert / AI 摘要必須攜帶的可追溯證據包;
不查 DB、不產生 sample evidence、不建立 queue、不派送 Telegram。
"""
EVIDENCE_SECTIONS = (
{
"key": "campaign_context",
"label": "活動脈絡",
"source_tables": [
"market_campaigns",
"market_campaign_snapshots",
"market_crawler_runs",
],
"required_fields": [
"campaign_id",
"platform_code",
"campaign_name",
"campaign_url",
"status",
"start_at",
"end_at",
"snapshot_id",
"batch_id",
],
"purpose": "確認活動存在、時間狀態與爬取批次。",
},
{
"key": "market_product_snapshot",
"label": "競品商品快照",
"source_tables": [
"market_campaign_products",
"market_product_price_history",
],
"required_fields": [
"market_product_id",
"platform_product_id",
"product_url",
"name",
"price",
"original_price",
"discount_rate",
"coupon_text",
"rank_position",
"crawled_at",
],
"purpose": "確認競品活動商品與當下價格、折扣、排序訊號。",
},
{
"key": "match_review_state",
"label": "商品比對審核狀態",
"source_tables": [
"market_product_matches",
],
"required_fields": [
"match_id",
"momo_i_code",
"match_score",
"match_status",
"match_reason_json",
"reviewed_at",
"reviewed_by",
],
"purpose": "確認是否可把競品商品升級為同品或高信心候選。",
},
{
"key": "momo_reference",
"label": "MOMO 參考商品",
"source_tables": [
"products",
"price_records",
"daily_sales_snapshot",
],
"required_fields": [
"momo_i_code",
"product_name",
"brand",
"reference_price",
"latest_price_seen_at",
"sales_signal_at",
],
"purpose": "確認我方價格與銷售訊號,避免只靠競品資料判讀。",
},
{
"key": "scoring_trace",
"label": "分數計算軌跡",
"source_tables": [
"market_product_price_history",
"market_product_matches",
],
"required_fields": [
"dimension_scores",
"total_score",
"threshold_level",
"formula_version",
"computed_from_batch_id",
],
"purpose": "讓人工審核、AI 摘要與 Telegram 候選都能追溯分數來源。",
},
)
ALERT_ESCALATION_GATES = (
{
"key": "confirmed_or_reviewed_match",
"label": "必須有 confirmed 或 needs_review 高信心比對證據",
"required_for": ["high", "critical"],
},
{
"key": "fresh_price_snapshot",
"label": "競品價格快照需在有效時間窗內",
"required_for": ["medium", "high", "critical"],
},
{
"key": "momo_reference_price",
"label": "必須有可追溯的 MOMO 參考價",
"required_for": ["high", "critical"],
},
{
"key": "campaign_still_active",
"label": "活動仍為 active 或 upcomingended 不得進即時告警",
"required_for": ["medium", "high", "critical"],
},
{
"key": "operator_approval",
"label": "人工批准後才可進 Telegram 或 AI 摘要候選",
"required_for": ["critical"],
},
)
def _schema_tables(schema_smoke):
expected_tables = schema_smoke.get("expected_tables") or []
if expected_tables:
return set(expected_tables)
smoke = schema_smoke.get("schema_smoke") or schema_smoke
return {
item.get("table")
for item in smoke.get("tables", [])
if item.get("table")
}
def build_opportunity_evidence_plan_preview(
*,
runtime_status,
opportunity_plan,
scoring_plan,
match_review_plan,
legacy_source_bridge,
schema_smoke,
):
"""建立 evidence bundle 計畫;不查資料、不產生範例 evidence。"""
available_tables = _schema_tables(schema_smoke)
required_market_tables = {
table
for section in EVIDENCE_SECTIONS
for table in section["source_tables"]
if table.startswith("market_")
}
required_market_tables_declared = required_market_tables <= available_tables
scoring_preview_safe = (
scoring_plan.get("mode") == "opportunity_scoring_plan_preview"
and not scoring_plan.get("score_calculation_executed")
and not scoring_plan.get("scoring_job_created")
and not scoring_plan.get("database_write_executed")
)
opportunity_preview_safe = (
opportunity_plan.get("mode") == "opportunity_plan_preview"
and not opportunity_plan.get("opportunity_queue_created")
and not opportunity_plan.get("threat_alert_dispatched")
)
match_review_preview_safe = (
match_review_plan.get("mode") == "match_review_plan_preview"
and not match_review_plan.get("auto_confirm_executed")
and not match_review_plan.get("database_write_executed")
)
legacy_bridge_safe = (
legacy_source_bridge.get("mode") == "legacy_source_bridge_planned"
and not legacy_source_bridge.get("read_only_query_executed")
and not legacy_source_bridge.get("database_write_executed")
)
gate_checks = {
"opportunity_plan_preview_safe": opportunity_preview_safe,
"scoring_plan_preview_safe": scoring_preview_safe,
"match_review_preview_safe": match_review_preview_safe,
"legacy_bridge_planned_safe": legacy_bridge_safe,
"schema_smoke_passed": bool(
(schema_smoke.get("schema_smoke") or schema_smoke).get("passed")
),
"required_market_tables_declared": required_market_tables_declared,
"confirmed_match_evidence_available": bool(
match_review_plan.get("ready_for_review_queue")
),
"scoring_trace_available": bool(scoring_plan.get("ready_for_scoring_job")),
"database_write_still_blocked": not bool(
runtime_status.database_write_allowed
),
"manual_operator_approval": False,
}
blocked_reasons = [
key for key, passed in gate_checks.items()
if not passed
]
return {
"mode": "opportunity_evidence_plan_preview",
"ready_for_evidence_bundle": False,
"evidence_bundle_created": False,
"evidence_query_executed": False,
"sample_evidence_generated": False,
"alert_candidate_created": False,
"telegram_dispatched": False,
"ai_summary_generated": False,
"database_session_created": False,
"database_write_executed": False,
"database_commit_executed": False,
"external_network_executed": False,
"scheduler_attached": False,
"writes_executed": False,
"would_write_database": False,
"section_count": len(EVIDENCE_SECTIONS),
"sections": list(EVIDENCE_SECTIONS),
"escalation_gates": list(ALERT_ESCALATION_GATES),
"required_market_tables": sorted(required_market_tables),
"gate_checks": gate_checks,
"blocked_reasons": blocked_reasons,
"bundle_contract": {
"identity": "platform_code + campaign_id + market_product_id + match_id",
"freshness_window_hours": 24,
"dedupe_key": "rule_key + momo_i_code + platform_product_id + campaign_id + crawled_at_date",
"must_include": [
"rule_key",
"threshold_level",
"total_score",
"evidence_sections",
"source_batch_id",
],
},
"operator_sequence": [
"先確認 scoring plan 只基於正式 market_* evidence",
"建立 evidence bundle 時逐一附上來源 table 與 primary key",
"缺任一高風險 evidence 時只能降級為 watch",
"人工審核通過後才可建立 alert candidate",
"AI 摘要與 Telegram 候選必須引用 bundle id 與 source_batch_id",
],
"safe_boundaries": [
"do_not_query_database_from_evidence_preview",
"do_not_generate_placeholder_evidence",
"do_not_create_alert_candidate_from_preview",
"do_not_dispatch_telegram_from_evidence_preview",
"do_not_generate_ai_summary_from_evidence_preview",
"do_not_escalate_without_bundle_contract",
],
}