From b6de73a4a15d106779a309390d81d996043c1ef8 Mon Sep 17 00:00:00 2001 From: OoO Date: Thu, 21 May 2026 20:25:28 +0800 Subject: [PATCH] V10.387 improve EA price alert evidence --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 +- docs/memory/history_logs.md | 1 + services/elephant_alpha_autonomous_engine.py | 111 ++++++++++++++++++- services/marketplace_product_matcher.py | 23 ++++ services/telegram_templates.py | 6 + tests/test_elephant_alpha_engine.py | 9 +- tests/test_marketplace_product_matcher.py | 19 ++++ tests/test_telegram_triaged_alert_format.py | 3 + 9 files changed, 173 insertions(+), 4 deletions(-) diff --git a/config.py b/config.py index 26c7919..0faaf27 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.386" +SYSTEM_VERSION = "V10.387" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 512a649..9fbf000 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-21 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.386 +> **適用版本**: V10.387 --- @@ -29,6 +29,7 @@ - Gemini 不可被任何狀態面板或 router 推薦為主提供者:`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`,只能顯示為 fallback 狀態;`llm_model_router` 的 `ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`,需要深推理時才升本地 `deepseek-r1:14b`。 - ElephantAlpha prompt / agent registry 不得再把 OpenClaw 描述為 Gemini 主模型;OpenClaw 是 `qwen2.5-coder:7b` / `qwen3:14b` Ollama-first 策略師,Gemini 僅能在 guard 顯式解鎖後作 emergency fallback。 - 111 `192.168.0.111` 只是最後一道 Mac fallback,不承接 7B+、vision、long-context 模型長駐;`OllamaService.generate()` 落到 111 時會將 `qwen3`、`deepseek-r1`、`hermes3`、`qwen2.5*`、`gemma3`、`llava`、`minicpm-v` 與 7B+ 模型依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK=llama3.2:latest`,並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=20`、`OLLAMA_111_NUM_CTX=4096`、`OLLAMA_111_NUM_PREDICT=512` 封頂,避免 16GB RAM 主機被大 context runner、長輸出與 24h keep-alive 壓到 swap。 +- ElephantAlpha 的 `price_drop_alert` / `market_opportunity` Telegram HITL 告警必須把同款證據獨立呈現,至少包含 `match_type`、`price_basis`、`alert_tier` 與 `match_score`;沒有高信心同款與總價可比證據時,不得把 PChome/MOMO 價差寫成可直接跟價建議。 ## 一、四 AI Agent 路由架構 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 31faf1c..6002436 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化 +- **V10.387 EA 比價 HITL 告警證據排版**: Elephant Alpha 的 DB evidence 與 Hermes pre-fetch action 現在會把 PChome/MOMO 同款證據帶進 Telegram:`match_type`、`price_basis`、`alert_tier` 與 `match_score` 會獨立成「證據」行,讓人工審核能分辨高信心同款、總價可比、單位價覆核與身份覆核,不再只看到乾巴巴的 `MOMO vs PChome` 長句。同版 marketplace matcher 補 Relove「私密潔淨凝露」identity anchor 與聯名款搜尋噪音,避免 PLAY BOY / 小虎等活動詞壓過真同款名稱。 - **V10.386 Gemini compose hard default / KATE 唇膏比對**: `docker-compose.yml` 針對 `momo-app`、`scheduler`、`telegram-bot` 明確釘住 `GEMINI_API_HARD_DISABLED=true` 與 `GEMINI_FALLBACK_ENABLED=false` 的預設,讓 `.env` 保留 API key 時也不會自動產生 Gemini 付費出站;AI SOT 與 compose 測試同步鎖定此契約。同版 marketplace matcher 補 KATE/凱婷「柔霧裸唇膏」identity anchor,避免 MOMO 長標含東京夜喫茶系列與任選文案時漏掉 PChome 同款短標。 - **V10.385 Lactacyd / MAQuillAGE 櫃別同款比對**: Marketplace matcher 補強 Lactacyd 私密潔浴露多款任選長標與 PChome 短標同款放行,並橋接「資生堂東京櫃」與 `MAQuillAGE 心機彩妝` 在「心機星魅蜜光圈潤唇膏」上的櫃別/品牌 alias,避免真同款被 `brand_conflict` 擋掉。 - **V10.384 Karadium 無規格眼影棒同款放行**: Marketplace matcher 對 Karadium「閃亮珍珠眼影棒」新增品牌 + 強 identity anchor 加分,當 PChome 標題省略 1.4g 規格但品名/品牌高度一致、無變體衝突與 hard veto 時仍可進入 exact identity 告警候選,避免同款因規格缺字漏報。 diff --git a/services/elephant_alpha_autonomous_engine.py b/services/elephant_alpha_autonomous_engine.py index e6e0bfc..5336626 100644 --- a/services/elephant_alpha_autonomous_engine.py +++ b/services/elephant_alpha_autonomous_engine.py @@ -35,6 +35,26 @@ from database.manager import get_db_manager, get_session logger = SystemLogger("ElephantAlphaEngine").get_logger() +COMPETITOR_MATCH_TYPE_LABELS = { + "exact": "高信心同款", + "same_product_different_pack": "同商品不同包裝", + "same_line_variant": "同系列不同款", + "comparable": "可比但需覆核", + "no_match": "非同款", +} +COMPETITOR_PRICE_BASIS_LABELS = { + "total_price": "總價可比", + "unit_price": "單位價可比", + "manual_review": "人工覆核後可比", + "none": "不可比", +} +COMPETITOR_ALERT_TIER_LABELS = { + "price_alert_exact": "可直接價格告警", + "unit_price_review": "單位價覆核", + "identity_review": "身份覆核", + "suppress": "不告警", +} + # ---- Configuration ---- SSH_JUMP_HOST = os.getenv("ELEPHANT_ALPHA_JUMP_HOST", "192.168.0.110") SSH_JUMP_USER = os.getenv("ELEPHANT_ALPHA_JUMP_USER", "wooo") @@ -1204,6 +1224,74 @@ class ElephantAlphaAutonomousEngine: self._log.error("Resource pressure Telegram failed (non-blocking): %s", e) # ---- Sub-services ---- + @classmethod + def _jsonish_dict(cls, value: Any) -> Dict[str, Any]: + if isinstance(value, dict): + return value + if isinstance(value, str) and value.strip(): + try: + parsed = json.loads(value) + return parsed if isinstance(parsed, dict) else {} + except Exception: + return {} + return {} + + @classmethod + def _jsonish_list(cls, value: Any) -> List[str]: + if isinstance(value, list): + return [str(item) for item in value] + if isinstance(value, tuple): + return [str(item) for item in value] + if isinstance(value, str) and value.strip(): + try: + parsed = json.loads(value) + if isinstance(parsed, list): + return [str(item) for item in parsed] + except Exception: + return [] + return [] + + @classmethod + def _tag_suffix(cls, tags: List[str], prefix: str) -> str: + marker = f"{prefix}_" + for tag in tags or []: + text = str(tag) + if text.startswith(marker): + return text.removeprefix(marker) + return "" + + @classmethod + def _format_competitor_match_evidence(cls, row: Any) -> str: + diagnostic = cls._jsonish_dict(cls._row_get(row, "match_diagnostic_json")) + tags = cls._jsonish_list(cls._row_get(row, "tags")) + match_type = ( + diagnostic.get("match_type") + or cls._tag_suffix(tags, "match_type") + or "exact" + ) + price_basis = ( + diagnostic.get("price_basis") + or cls._tag_suffix(tags, "price_basis") + or "total_price" + ) + alert_tier = ( + diagnostic.get("alert_tier") + or cls._tag_suffix(tags, "alert_tier") + or "price_alert_exact" + ) + match_score = cls._to_float(cls._row_get(row, "match_score")) + if match_score is None: + match_score = cls._to_float(diagnostic.get("score")) + + parts = [ + COMPETITOR_MATCH_TYPE_LABELS.get(str(match_type), str(match_type)), + COMPETITOR_PRICE_BASIS_LABELS.get(str(price_basis), str(price_basis)), + COMPETITOR_ALERT_TIER_LABELS.get(str(alert_tier), str(alert_tier)), + ] + if match_score is not None and match_score > 0: + parts.append(f"score {match_score:.2f}") + return "證據:" + " / ".join(part for part in parts if part) + @classmethod def _format_competitor_evidence_actions( cls, @@ -1236,7 +1324,13 @@ class ElephantAlphaAutonomousEngine: action = "建議人工確認 PChome identity_v2 後評估跟價或促銷" impact = f"每件價差 NT$ {gap_amount:,.0f}" - parts = [f"[{sku}] {name}", comparison, impact, action] + parts = [ + f"[{sku}] {name}", + comparison, + impact, + cls._format_competitor_match_evidence(row), + action, + ] if competitor_id: parts.append(f"PChome {competitor_id}") actions.append("|".join(parts)) @@ -1277,6 +1371,9 @@ class ElephantAlphaAutonomousEngine: cp.price AS competitor_price, cp.competitor_product_id, cp.competitor_product_name, + cp.match_score, + cp.tags, + cp.match_diagnostic_json, cp.crawled_at FROM competitor_prices cp WHERE cp.source = 'pchome' @@ -1294,6 +1391,9 @@ class ElephantAlphaAutonomousEngine: ((lm.momo_price - lc.competitor_price) / NULLIF(lm.momo_price, 0) * 100) AS price_gap_pct, lc.competitor_product_id, lc.competitor_product_name, + lc.match_score, + lc.tags, + lc.match_diagnostic_json, lc.crawled_at FROM latest_momo lm JOIN latest_competitor lc ON lc.sku = lm.sku @@ -1388,6 +1488,15 @@ class ElephantAlphaAutonomousEngine: parts = [ f"[{sku}] {name}", f"MOMO ${momo:,.0f} vs PChome ${pchome:,.0f} ({gap_pct:+.1f}%)", + self._format_competitor_match_evidence({ + "match_score": getattr(t, "match_score", 0), + "tags": list(getattr(t, "competitor_tags", ()) or ()), + "match_diagnostic_json": { + "match_type": getattr(t, "match_type", "exact"), + "price_basis": getattr(t, "price_basis", "total_price"), + "alert_tier": getattr(t, "alert_tier", "price_alert_exact"), + }, + }), ] if loss > 0: parts.append(f"近 7 日流失 NT$ {loss:,.0f}") diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 3e94d0a..1e8d396 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -290,6 +290,14 @@ SEARCH_NOISE_TOKENS = { "第三代", "經典版", "版白", + "限量聯名款", + "play", + "boy", + "小虎", + "啾啾妹", + "煎妮花", + "涼感潔淨", + "私密處清潔", "溫和不乾澀", "寶寶共和國", "三款", @@ -305,6 +313,7 @@ SEARCH_NOISE_TOKENS = { } SEARCH_IDENTITY_ANCHORS = ( + "私密潔淨凝露", "柔霧裸唇膏", "潤浸保濕清爽身體乳液", "閃亮珍珠眼影棒", @@ -1841,6 +1850,20 @@ def score_marketplace_match( ): score += 0.10 reasons.append("shared_identity_anchor_lactacyd_wash") + if ( + "私密潔淨凝露" in shared_anchor + and {"relove"} <= (left.brand_tokens | right.brand_tokens) + and brand_score >= 0.95 + and not hard_veto + and price_penalty == 0 + and type_score >= 0.95 + and spec_score >= 0.85 + and token_score >= 0.30 + and sequence_score >= 0.40 + and not variant_descriptor_conflict + ): + score += 0.11 + reasons.append("shared_identity_anchor_relove_cleanser") if ( "柔霧裸唇膏" in shared_anchor and {"kate", "凱婷"} & (left.brand_tokens | right.brand_tokens) diff --git a/services/telegram_templates.py b/services/telegram_templates.py index 2aaa4c2..b76cd91 100644 --- a/services/telegram_templates.py +++ b/services/telegram_templates.py @@ -527,6 +527,8 @@ def _parse_ea_action(action: Any) -> Dict[str, Any]: item["pchome_id"] = part.replace("PChome", "", 1).strip() elif part.startswith("建議"): item["action"] = part + elif part.startswith("證據"): + item["evidence"] = part.replace("證據", "", 1).strip(" ::") elif "NT$" in part or "流失" in part or "價差" in part or "價格優勢" in part: item["impact"] = part amount = re.search(r"NT\$\s*([0-9,]+)", part) @@ -590,6 +592,10 @@ def _format_ea_action_card(item: Dict[str, Any], index: int) -> List[str]: else: lines.append(f" 影響:{impact_text}") + evidence = item.get("evidence") + if evidence: + lines.append(f" 證據:{escape(_short_text(evidence, 96))}") + action = str(item.get("action") or "").replace("建議", "", 1).strip(" ::") if action: lines.append(f" 動作:{escape(_short_text(action, 86))}") diff --git a/tests/test_elephant_alpha_engine.py b/tests/test_elephant_alpha_engine.py index d081f17..25d97b1 100644 --- a/tests/test_elephant_alpha_engine.py +++ b/tests/test_elephant_alpha_engine.py @@ -128,13 +128,20 @@ def test_competitor_db_evidence_actions_are_concrete(): "competitor_price": 990, "price_gap_pct": 17.5, "competitor_product_id": "D123456", + "match_score": 0.84, + "tags": [ + "identity_v2", + "match_type_exact", + "price_basis_total_price", + "alert_tier_price_alert_exact", + ], } ], trigger_type="price_drop_alert", ) assert actions == [ - "[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|建議人工確認 PChome identity_v2 後評估跟價或促銷|PChome D123456" + "[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.84|建議人工確認 PChome identity_v2 後評估跟價或促銷|PChome D123456" ] diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index f7a380b..2f93c80 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -593,6 +593,25 @@ def test_marketplace_matcher_promotes_lactacyd_private_wash_multi_option_title() assert "shared_identity_anchor_lactacyd_wash" in diagnostics.reasons +def test_marketplace_matcher_promotes_relove_private_cleanser_campaign_title(): + from services.marketplace_product_matcher import score_marketplace_match, build_search_terms + + source = "【Relove】胺基酸私密潔淨凝露120ml-限量聯名款(小虎/啾啾妹/煎妮花/PLAY BOY私密處清潔 涼感潔淨)" + diagnostics = score_marketplace_match( + source, + "RELOVE金盞花萃取低敏私密潔淨凝露 120ml", + momo_price=629, + competitor_price=629, + ) + terms = build_search_terms(source, max_terms=4) + + assert diagnostics.score >= 0.76 + assert diagnostics.hard_veto is False + assert "shared_identity_anchor_relove_cleanser" in diagnostics.reasons + assert "play" not in terms[0].lower() + assert "boy" not in terms[0].lower() + + def test_marketplace_matcher_bridges_maquillage_shiseido_counter_alias(): from services.marketplace_product_matcher import score_marketplace_match diff --git a/tests/test_telegram_triaged_alert_format.py b/tests/test_telegram_triaged_alert_format.py index 3fc56a9..dc55eca 100644 --- a/tests/test_telegram_triaged_alert_format.py +++ b/tests/test_telegram_triaged_alert_format.py @@ -76,11 +76,13 @@ def test_ea_escalation_uses_structured_incident_brief(): "[5900068] [derma Angel 護妍天使] 集中抗痘精華|" "MOMO $300 vs PChome $250 (+16.7%)|" "每件價差 NT$ 50|" + "證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.86|" "建議人工確認 PChome identity_v2 後評估跟價或促銷|" "PChome DABC53-A9009OEF", "[3518670] L'Occitane 歐舒丹 官方直營 乳油木|" "MOMO $1,220 vs PChome $850 (+30.3%)|" "每件價差 NT$ 370|" + "證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.91|" "建議人工確認 PChome identity_v2 後評估跟價或促銷|" "PChome DDADKS-A900HIG5Y", ], @@ -95,6 +97,7 @@ def test_ea_escalation_uses_structured_incident_brief(): assert "• 最大單件價差:NT$ 370" in msg assert "1. [5900068]" in msg assert "MOMO:$300 PChome:$250" in msg + assert "證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.86" in msg assert "PChome:DABC53-A9009OEF" in msg assert " • [5900068]" not in msg assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:ea_review_test"