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

354 lines
11 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.
"""市場情報機會與威脅告警候選 preview。
只定義未來 alert candidate、Telegram 與 AI 摘要的 gate、去重與節流
不建立 queue、不寫 DB、不派送訊息、不呼叫 LLM。
"""
from services.market_intel.ai_controlled_service_compat import compatibility_flag
ALERT_CHANNELS = (
{
"key": "operator_review",
"label": "AI 例外決策清單",
"minimum_level": "watch",
"requires_operator_approval": False,
"dispatch_target": "market_intel_review_queue",
},
{
"key": "daily_digest",
"label": "每日市場情報摘要",
"minimum_level": "medium",
"requires_operator_approval": True,
"dispatch_target": "openclaw_daily_digest",
},
{
"key": "telegram_candidate",
"label": "Telegram 候選告警",
"minimum_level": "high",
"requires_operator_approval": True,
"dispatch_target": "telegram_market_intel_channel",
},
{
"key": "ai_briefing_candidate",
"label": "AI 摘要候選",
"minimum_level": "medium",
"requires_operator_approval": True,
"dispatch_target": "hermes_openclaw_briefing",
},
)
ALERT_GATES = (
{
"key": "evidence_bundle_ready",
"label": "必須有完整 evidence bundle",
"required_for": ["watch", "medium", "high", "critical"],
},
{
"key": "score_threshold_met",
"label": "分數需達 channel 最低門檻",
"required_for": ["medium", "high", "critical"],
},
{
"key": "dedupe_key_clear",
"label": "同日同商品同活動不得重複派送",
"required_for": ["medium", "high", "critical"],
},
{
"key": "freshness_window_valid",
"label": "價格與活動快照需在 freshness window 內",
"required_for": ["medium", "high", "critical"],
},
{
"key": "operator_approval",
"label": "AI 受控批准後才可進 Telegram 或 AI 摘要候選",
"required_for": ["high", "critical"],
},
)
_APPROVAL_REQUIRED_FOR_DISPATCH_COMPAT_KEY = compatibility_flag(
"approval_required_for_dispatch"
)
THROTTLE_POLICY = {
"dedupe_window_hours": 24,
"max_telegram_candidates_per_day": 12,
"max_critical_candidates_per_hour": 3,
"max_ai_briefing_items_per_digest": 20,
}
REVIEW_STATES = (
{
"key": "draft",
"label": "候選草案",
"dispatch_allowed": False,
"description": "由 scoring / evidence 形成候選,但尚未進 AI 例外決策。",
},
{
"key": "needs_review",
"label": "待 AI 例外決策",
"dispatch_allowed": False,
"description": "資料完整但尚未批准,不得推送或產生 AI 摘要。",
},
{
"key": "approved_for_digest",
"label": "批准進每日摘要",
"dispatch_allowed": False,
"description": "只可進每日摘要候選,仍不得直接推 Telegram。",
},
{
"key": "approved_for_telegram",
"label": "批准進 Telegram 候選",
"dispatch_allowed": True,
"description": "仍需經 dispatch gate 與 throttle再進下一階段。",
},
{
"key": "rejected",
"label": "駁回",
"dispatch_allowed": False,
"description": "保留審核理由,不再升級。",
},
{
"key": "deferred",
"label": "延後觀察",
"dispatch_allowed": False,
"description": "等待新價格快照或活動狀態更新。",
},
)
REVIEW_ACTIONS = (
{
"key": "approve_digest",
"from": ["needs_review"],
"to": "approved_for_digest",
"requires_reason": True,
},
{
"key": "approve_telegram",
"from": ["needs_review", "approved_for_digest"],
"to": "approved_for_telegram",
"requires_reason": True,
},
{
"key": "reject",
"from": ["draft", "needs_review", "approved_for_digest"],
"to": "rejected",
"requires_reason": True,
},
{
"key": "defer",
"from": ["needs_review", "approved_for_digest"],
"to": "deferred",
"requires_reason": True,
},
)
REVIEW_QUEUE_CONTRACT = {
"table_name": "market_alert_review_queue",
"primary_key": "id",
"required_fields": [
"alert_candidate_id",
"review_state",
"priority_lane",
"threshold_level",
"total_score",
"evidence_bundle_id",
"dedupe_key",
"source_batch_id",
"created_at",
"updated_at",
],
"audit_fields": [
"reviewer_identity",
"review_action",
"review_reason",
"reviewed_at",
"previous_state",
"next_state",
],
"forbidden_fields": [
"customer_personal_data",
"login_required_price",
"cart_or_order_data",
"payment_or_shipping_data",
],
}
REVIEW_QUEUE_INDEXES = (
{
"key": "idx_market_alert_review_queue_state_priority",
"columns": ["review_state", "priority_lane", "created_at"],
"purpose": "審核台依狀態與優先級快速排序。",
},
{
"key": "ux_market_alert_review_queue_dedupe",
"columns": ["dedupe_key"],
"purpose": "避免同商品同活動同日重複進入審核佇列。",
},
{
"key": "idx_market_alert_review_queue_bundle",
"columns": ["evidence_bundle_id", "source_batch_id"],
"purpose": "回查 evidence bundle 與來源批次。",
},
)
REVIEW_PRIORITY_LANES = (
{
"key": "critical",
"label": "立即審核",
"max_age_hours": 2,
"minimum_threshold": "critical",
},
{
"key": "high",
"label": "今日優先",
"max_age_hours": 8,
"minimum_threshold": "high",
},
{
"key": "medium",
"label": "每日摘要候選",
"max_age_hours": 24,
"minimum_threshold": "medium",
},
{
"key": "watch",
"label": "觀察池",
"max_age_hours": 72,
"minimum_threshold": "watch",
},
)
def build_opportunity_alert_plan_preview(
*,
runtime_status,
opportunity_plan,
scoring_plan,
evidence_plan,
scheduler_plan,
):
"""建立告警候選與派送 gate 計畫;不建立或派送任何告警。"""
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")
and not opportunity_plan.get("database_write_executed")
)
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")
)
evidence_preview_safe = (
evidence_plan.get("mode") == "opportunity_evidence_plan_preview"
and not evidence_plan.get("evidence_bundle_created")
and not evidence_plan.get("evidence_query_executed")
and not evidence_plan.get("database_write_executed")
)
scheduler_preview_safe = (
scheduler_plan.get("mode") == "scheduler_attach_plan_preview"
and not scheduler_plan.get("scheduler_registration_executed")
and not scheduler_plan.get("crawler_job_started")
)
gate_checks = {
"opportunity_plan_preview_safe": opportunity_preview_safe,
"scoring_plan_preview_safe": scoring_preview_safe,
"evidence_plan_preview_safe": evidence_preview_safe,
"scheduler_plan_preview_safe": scheduler_preview_safe,
"evidence_bundle_ready": bool(
evidence_plan.get("ready_for_evidence_bundle")
),
"scoring_job_ready": bool(scoring_plan.get("ready_for_scoring_job")),
"opportunity_queue_ready": bool(
opportunity_plan.get("ready_for_opportunity_queue")
),
"database_write_still_blocked": not bool(
runtime_status.database_write_allowed
),
"operator_approval": False,
}
blocked_reasons = [
key for key, passed in gate_checks.items()
if not passed
]
return {
"mode": "opportunity_alert_plan_preview",
"ready_for_alert_candidates": False,
"alert_candidate_created": False,
"alert_queue_created": False,
"review_queue_created": False,
"review_queue_contract_defined": True,
"review_queue_contract_written": False,
"review_queue_table_created": False,
"review_action_executed": False,
"approval_record_written": False,
"telegram_dispatched": False,
"ai_summary_generated": False,
"llm_call_executed": False,
"notification_sent": 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,
"channel_count": len(ALERT_CHANNELS),
"channels": list(ALERT_CHANNELS),
"alert_gates": list(ALERT_GATES),
"review_state_count": len(REVIEW_STATES),
"review_states": list(REVIEW_STATES),
"review_actions": list(REVIEW_ACTIONS),
"review_queue_contract": REVIEW_QUEUE_CONTRACT,
"review_queue_indexes": list(REVIEW_QUEUE_INDEXES),
"review_priority_lanes": list(REVIEW_PRIORITY_LANES),
"throttle_policy": THROTTLE_POLICY,
"gate_checks": gate_checks,
"blocked_reasons": blocked_reasons,
"approval_policy": {
"ai_controlled_approval_required_for_dispatch": True,
_APPROVAL_REQUIRED_FOR_DISPATCH_COMPAT_KEY: True,
"approval_reason_required": True,
"reviewer_identity_required": True,
"approved_candidate_still_requires_dispatch_gate": True,
"queue_item_requires_evidence_bundle": True,
"queue_item_requires_dedupe_key": True,
},
"payload_contract": {
"required_fields": [
"alert_candidate_id",
"rule_key",
"threshold_level",
"total_score",
"bundle_id",
"source_batch_id",
"dedupe_key",
"created_at",
],
"forbidden_fields": [
"customer_personal_data",
"login_required_price",
"cart_or_order_data",
],
},
"operator_sequence": [
"先由 evidence bundle 產生 alert candidate 草案",
"依 throttle_policy 去重與節流,不直接推播",
"medium 以上進每日摘要候選high 以上才可進 Telegram 候選",
"AI 受控批准後才允許 AI 摘要或 Telegram 候選進入下一階段",
"所有派送內容必須附 bundle id、source batch id 與 dedupe key",
],
"safe_boundaries": [
"do_not_create_alert_queue_from_preview",
"do_not_create_review_queue_table_from_preview",
"do_not_write_review_queue_contract_from_preview",
"do_not_dispatch_telegram_from_alert_preview",
"do_not_call_llm_from_alert_preview",
"do_not_notify_without_evidence_bundle",
"do_not_send_duplicate_alerts",
"do_not_include_personal_or_login_required_data",
],
}