feat(market-intel): add opportunity plan preview
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
This commit is contained in:
142
services/market_intel/opportunity_plan.py
Normal file
142
services/market_intel/opportunity_plan.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""市場情報機會與威脅判讀 preview。
|
||||
|
||||
只定義未來 opportunity / threat scoring 規則;不建立 queue、不寫 DB、
|
||||
不派送 Telegram、不產生 AI 報告。
|
||||
"""
|
||||
|
||||
|
||||
OPPORTUNITY_RULES = (
|
||||
{
|
||||
"key": "competitor_price_threat",
|
||||
"label": "競品活動價低於 MOMO 參考價",
|
||||
"severity": "high",
|
||||
"requires_confirmed_match": True,
|
||||
"minimum_evidence": [
|
||||
"confirmed_or_reviewed_match",
|
||||
"current_market_price",
|
||||
"momo_reference_price",
|
||||
],
|
||||
"action_hint": "進入價格威脅審核,不自動調價。",
|
||||
},
|
||||
{
|
||||
"key": "promotion_gap",
|
||||
"label": "競品有強促銷但我方未跟活動",
|
||||
"severity": "medium",
|
||||
"requires_confirmed_match": True,
|
||||
"minimum_evidence": [
|
||||
"active_competitor_campaign",
|
||||
"confirmed_match",
|
||||
"momo_campaign_absent_or_weaker",
|
||||
],
|
||||
"action_hint": "標記為活動缺口,交由人工決策是否跟進。",
|
||||
},
|
||||
{
|
||||
"key": "deep_discount_overlap",
|
||||
"label": "同品或近似品出現深折重疊",
|
||||
"severity": "medium",
|
||||
"requires_confirmed_match": False,
|
||||
"minimum_evidence": [
|
||||
"needs_review_match_score_above_threshold",
|
||||
"discount_rate_over_threshold",
|
||||
"campaign_context",
|
||||
],
|
||||
"action_hint": "送入商品比對審核,不直接派送威脅告警。",
|
||||
},
|
||||
{
|
||||
"key": "campaign_ending_watch",
|
||||
"label": "高折扣活動即將結束",
|
||||
"severity": "low",
|
||||
"requires_confirmed_match": False,
|
||||
"minimum_evidence": [
|
||||
"campaign_end_at",
|
||||
"recent_price_snapshot",
|
||||
"discount_or_coupon_signal",
|
||||
],
|
||||
"action_hint": "排入觀察清單,避免過期活動持續推播。",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def build_opportunity_plan_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
match_review_plan,
|
||||
scheduler_plan,
|
||||
legacy_source_bridge,
|
||||
):
|
||||
"""建立市場機會與威脅判讀計畫;不執行任何推播或寫入。"""
|
||||
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")
|
||||
)
|
||||
scheduler_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")
|
||||
)
|
||||
match_review_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")
|
||||
)
|
||||
gate_checks = {
|
||||
"match_review_plan_present": match_review_plan.get("mode")
|
||||
== "match_review_plan_preview",
|
||||
"match_review_candidates_available": bool(
|
||||
match_review_plan.get("ready_for_review_queue")
|
||||
),
|
||||
"auto_confirm_disabled": not bool(
|
||||
(match_review_plan.get("thresholds") or {}).get("auto_confirm_enabled")
|
||||
),
|
||||
"scheduler_plan_preview_safe": scheduler_safe,
|
||||
"legacy_bridge_planned_safe": legacy_bridge_safe,
|
||||
"database_write_still_blocked": not bool(
|
||||
runtime_status.database_write_allowed
|
||||
),
|
||||
"match_review_preview_safe": match_review_safe,
|
||||
"manual_operator_approval": False,
|
||||
}
|
||||
blocked_reasons = [
|
||||
key for key, passed in gate_checks.items()
|
||||
if not passed
|
||||
]
|
||||
|
||||
return {
|
||||
"mode": "opportunity_plan_preview",
|
||||
"ready_for_opportunity_queue": False,
|
||||
"opportunity_queue_created": False,
|
||||
"threat_alert_dispatched": 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,
|
||||
"rule_count": len(OPPORTUNITY_RULES),
|
||||
"rules": list(OPPORTUNITY_RULES),
|
||||
"gate_checks": gate_checks,
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"severity_policy": {
|
||||
"high": "只建立人工審核項,不自動調價、不直接派送高頻告警。",
|
||||
"medium": "進入日報摘要或審核清單,需 confirmed match 才能升級。",
|
||||
"low": "只做觀察,不觸發即時通知。",
|
||||
},
|
||||
"operator_sequence": [
|
||||
"先完成商品比對審核,至少取得 confirmed 或 needs_review 高信心候選",
|
||||
"用 campaign / price history 計算威脅與機會分數",
|
||||
"高風險項先進人工審核清單,不直接派送 Telegram",
|
||||
"人工確認後才允許進入 OpenClaw / Hermes 摘要",
|
||||
"所有 AI 摘要必須附上 DB evidence id 與規則 key",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_dispatch_alerts_from_preview",
|
||||
"do_not_generate_ai_summary_from_preview",
|
||||
"do_not_auto_adjust_price",
|
||||
"do_not_escalate_without_confirmed_match",
|
||||
"do_not_write_opportunity_queue_from_preview",
|
||||
],
|
||||
}
|
||||
@@ -27,6 +27,7 @@ from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_prefligh
|
||||
from services.market_intel.mcp_fetch_gate import build_mcp_fetch_gate_preview
|
||||
from services.market_intel.mcp_readiness import build_mcp_readiness_plan
|
||||
from services.market_intel.migration_blueprint import build_migration_blueprint
|
||||
from services.market_intel.opportunity_plan import build_opportunity_plan_preview
|
||||
from services.market_intel.platform_seed import build_platform_seed_rows
|
||||
from services.market_intel.platform_seed_db_diff import build_platform_seed_db_diff_plan
|
||||
from services.market_intel.scheduler_plan import build_scheduler_attach_plan
|
||||
@@ -70,7 +71,7 @@ class MarketIntelRuntimeStatus:
|
||||
class MarketIntelService:
|
||||
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
|
||||
|
||||
phase = "phase_34_match_review_plan_preview"
|
||||
phase = "phase_35_opportunity_plan_preview"
|
||||
|
||||
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
|
||||
return MarketIntelRuntimeStatus(
|
||||
@@ -393,6 +394,17 @@ class MarketIntelService:
|
||||
plan["phase"] = self.phase
|
||||
return plan
|
||||
|
||||
def build_opportunity_plan(self):
|
||||
"""回報市場機會與威脅判讀計畫;不派送、不產生 AI 報告。"""
|
||||
plan = build_opportunity_plan_preview(
|
||||
runtime_status=self.get_runtime_status(),
|
||||
match_review_plan=self.build_match_review_plan(),
|
||||
scheduler_plan=self.build_scheduler_plan(),
|
||||
legacy_source_bridge=self.build_legacy_source_bridge(),
|
||||
)
|
||||
plan["phase"] = self.phase
|
||||
return plan
|
||||
|
||||
def build_platform_seed_writer_plan(self, platform_code="all"):
|
||||
"""建立 platform seed writer dry-run plan,不建立 DB session。"""
|
||||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||||
@@ -474,6 +486,7 @@ class MarketIntelService:
|
||||
mcp_fetch_gate = self.build_mcp_fetch_gate()
|
||||
scheduler_plan = self.build_scheduler_plan()
|
||||
match_review_plan = self.build_match_review_plan()
|
||||
opportunity_plan = self.build_opportunity_plan()
|
||||
checks = {
|
||||
"schema_smoke_passed": bool(schema_smoke["passed"]),
|
||||
"feature_flags_default_safe": bool(
|
||||
@@ -530,6 +543,12 @@ class MarketIntelService:
|
||||
and not match_review_plan["auto_confirm_executed"]
|
||||
and not match_review_plan["database_write_executed"]
|
||||
),
|
||||
"opportunity_plan_preview_safe": bool(
|
||||
opportunity_plan["mode"] == "opportunity_plan_preview"
|
||||
and not opportunity_plan["opportunity_queue_created"]
|
||||
and not opportunity_plan["threat_alert_dispatched"]
|
||||
and not opportunity_plan["database_write_executed"]
|
||||
),
|
||||
}
|
||||
ready_for_production_deploy = all(checks.values())
|
||||
blocked_reasons = [
|
||||
@@ -660,6 +679,7 @@ class MarketIntelService:
|
||||
"/api/market_intel/mcp_fetch_gate",
|
||||
"/api/market_intel/scheduler_plan",
|
||||
"/api/market_intel/match_review_plan",
|
||||
"/api/market_intel/opportunity_plan",
|
||||
],
|
||||
"status": status.to_dict(),
|
||||
"schema_smoke": schema_smoke,
|
||||
@@ -681,4 +701,5 @@ class MarketIntelService:
|
||||
"mcp_fetch_gate": mcp_fetch_gate,
|
||||
"scheduler_plan": scheduler_plan,
|
||||
"match_review_plan": match_review_plan,
|
||||
"opportunity_plan": opportunity_plan,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user