Files
ewoooc/services/market_intel/opportunity_evidence.py
ogt 08d9e3fe7d
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
清除市場情報 P3 相容人工語意
2026-07-01 18:24:51 +08:00

260 lines
8.9 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。
"""
from services.market_intel.ai_controlled_service_compat import compatibility_flag
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 例外決策、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": "AI 受控批准後才可進 Telegram 或 AI 摘要候選",
"required_for": ["critical"],
},
)
_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval")
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
),
_OPERATOR_APPROVAL_COMPAT_KEY: 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",
"AI 例外決策通過後才可建立 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",
],
}