強化 EA JSON fallback 與 EDM cache 自癒
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.382"
|
||||
SYSTEM_VERSION = "V10.383"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ SQL漏斗(~300筆)
|
||||
- ElephantAlpha 使用 NVIDIA NIM hosted API;production 預設模型為 `nvidia/llama-3.3-nemotron-super-49b-v1.5`,`ELEPHANT_ALPHA_FALLBACK_MODELS` 需保留至少一個可呼叫備援;403/404、408/409/425/429、5xx、timeout 與 connection error 必須嘗試下一個模型。
|
||||
- ElephantAlpha L3 HITL 只允許發送有實證、可審核、可行動的升級告警;價格類 trigger 無 Hermes 具體威脅時,只記錄 suppressed escalation telemetry 與 cooldown,不寫 pending `human_review`,不發 Telegram 空告警。
|
||||
- ElephantAlpha 價格類 trigger 的 HITL / 決策 prefetch 必須先使用觸發 SQL 與 `competitor_prices` / `price_records` 的 DB 實證生成 SKU、MOMO / PChome 價差與建議 action lines;完整 Hermes LLM prefetch 預設關閉(`ELEPHANT_ALPHA_HERMES_LLM_PREFETCH_ENABLED=false`),避免 5s timeout 後落入無實證摘要或雲端備援。若無 DB 實證,只記錄 suppressed telemetry / cooldown,不發 Telegram 空告警。
|
||||
- ElephantAlpha 協調器收到非純 JSON、fenced JSON 或混文字 JSON 時,必須先做容錯抽取;仍無法解析時,只能使用 DB/Hermes 實證生成保守 HITL fallback。fallback 不得放入 OpenClaw `generate_*` 類舊策略步驟,也不得暗示已自動調價。
|
||||
- `resource_optimization` 不再交給 LLM 生成「預期效益 / 已執行」敘事,顯示名稱統一為「資源壓力治理」。此 trigger 必須先由程式量測 `action_plans` backlog、P1/P2 數、pending_review、逾時項目與 CPU load;只有 CPU 達門檻、P1/P2 積壓或逾時積壓才發 Telegram「資源壓力告警」。單純 queue 大但 CPU 正常只記錄 telemetry,不派發 Hermes/NemoTron、不宣稱 48 小時效益;Telegram 段落使用「系統處置紀錄」而非泛稱「已執行」,避免暗示 AI 已完成未經驗證的外部動作。
|
||||
- `resource_optimization` 會先執行 `ActionPlanHygieneService` 清理過期噪音:只關閉超過 72 小時的 `code_review_fix` / `openclaw_recommendation` 類 advisory action_plans,以及 NemoTron `direct_response/reply_simple` 舊聊天回覆計畫;將狀態改為 `auto_disabled` 或 `rejected` 並寫入 `metadata_json.hygiene_history`。不刪資料,也不碰 NemoTron human_review / pricing / tool action 類業務行動。
|
||||
- `momo-scheduler` 每 6 小時固定執行 `run_action_plan_hygiene_task()`,讓過期 advisory action_plans 的關閉不再依賴 `resource_optimization` 告警觸發;排程失敗會經 EventRouter 發送 `action_plan_hygiene_failure`。
|
||||
@@ -617,5 +618,6 @@ POSTGRES_HOST=momo-db
|
||||
| 2026-05-20 | 指定日期競品簡報可能混用目前 `competitor_prices` 快取價 | V10.315 起 `fetch_competitor_comparison_results()` 有 start/end date 時改用 `competitor_price_history` 期間快照,MOMO 價格取報表結束日前最新價;即時報表才使用目前有效 `competitor_prices` |
|
||||
| 2026-05-20 | PChome 覆蓋率分子可能被非活躍或無 MOMO 現價 SKU 膨脹 | V10.317 起 `fetch_competitor_coverage()` 的 `valid_matches` 改為 active MOMO latest price 與有效 PChome `identity_v2` 價格交集,確保 daily/growth/PPT/AI 看到的比價資料品質不被舊快取列高估 |
|
||||
| 2026-05-20 | EA HITL 告警可能把非 SKU 診斷誤排成待審 SKU,或在缺少 DB/Hermes 實證時打擾人工 | V10.318 起 `ea_escalation` 僅對含 SKU/價格比較的 actions 使用競價卡片;非 SKU 診斷改為「待確認事項」。價格類低信心事件若無 DB/Hermes 實證,測試鎖定只 suppress、不寫 human_review、不發 Telegram |
|
||||
| 2026-05-21 | ElephantAlpha NIM/LLM 回應偶爾不是純 JSON,會觸發 `json.loads()` 失敗並落入舊式空泛策略 fallback | V10.383 起協調器容忍 fenced/混文字 JSON;無法解析時改用 DB/Hermes 實證 fallback,且 fallback 不再包含 OpenClaw `generate_*` 舊步驟或自動調價暗示 |
|
||||
| 2026-05-20 | Telegram HTML parse mode 不支援 `<br>`,可能導致告警或報告送出 400 | V10.321 起 Telegram template 發送前會把 `<br>` / `<br/>` / `<BR />` 轉為換行;保留其他 HTML 標籤,非 HTML parse mode 不改寫 |
|
||||
| 2026-05-20 | 部分舊 Telegram 入口繞過中央 sanitizer,且 RAG awaiting review 使用錯誤 `chat_id=` 參數會讓人工審核推播失敗 | V10.322 起 Bot API price decision 走 `send_telegram_with_result()`;`price_decision()` 補 `report_url` 相容並 escape 動態欄位;RAG awaiting review 改用 `chat_ids=[...]` 呼叫 `_send_telegram_raw()` |
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化
|
||||
- **V10.383 EA JSON fallback / EDM cache 自癒 / 比對別名補強**: Elephant Alpha 協調器現在可容忍 fenced JSON 與混文字 JSON;若仍無法解析,會改用 DB/Hermes 實證產生保守人工覆核決策,不再輸出舊式 OpenClaw 策略 plan 或自動調價暗示。EDM promo dashboard shared cache 遇到損毀 pickle 會自動刪除並重建,避免每個 worker 重複噴 `UnicodeDecodeError`。Marketplace matcher 補上 Curel/珂潤、Karadium 與兩個強 identity anchor 測試,降低真同款漏報。
|
||||
- **V10.382 唇膏 exact identity 寬價差豁免**: marketplace matcher 對「同品牌 + 共享唇膏 identity anchor + 規格完全一致 + 無色號/變體衝突」的唇膏類商品,允許 sequence score 略低時仍套用 `price_penalty_suppressed_wide_exact_identity`;這只處理 PChome/MOMO 標題順序與行銷字差異造成的真同款漏報,不放寬顯性色號不同的 hard veto。
|
||||
- **V10.381 browse.sh 比價診斷計畫**: PChome feeder 在 `no_result`、`no_match`、低信心、單位價覆核、既有配對保護與爬蟲錯誤時,會把 read-only `browse_diagnostic_json` 寫入 `competitor_match_attempts`,內含 PChome search URL 與建議 `browse get/open` 命令;正式排程仍 API-first,`PCHOME_FEEDER_BROWSE_SH_EXECUTE_ENABLED=false` 預設不自動開瀏覽器,避免瀏覽器彈窗、登入或密碼提示干擾。
|
||||
- **V10.380 111 Ollama final fallback 收斂**: 111 Mac fallback 從救急路徑改成更短的保護路徑,`OLLAMA_111_MAX_TIMEOUT` 預設由 45s 收緊到 20s,並新增 `OLLAMA_111_NUM_PREDICT=512` 輸出上限;落到 111 時仍會降級重模型到 `llama3.2:latest`、縮 `num_ctx=4096`、`keep_alive=5m`,避免 GCP-A/GCP-B 短暫 timeout 後把長篇 Hermes/OpenClaw 工作轉嫁到 111 造成 swap 與 load 飆高。
|
||||
|
||||
@@ -120,11 +120,22 @@ def _load_shared_promo_dashboard_cache(cache_key):
|
||||
if not isinstance(entries, dict):
|
||||
return None
|
||||
return entries.get(cache_key)
|
||||
except Exception:
|
||||
sys_log.debug("promo dashboard shared cache load failed", exc_info=True)
|
||||
except Exception as exc:
|
||||
_discard_shared_promo_dashboard_cache("load_corrupt", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _discard_shared_promo_dashboard_cache(reason, exc=None):
|
||||
"""刪除損毀的跨 worker 快取,避免每個 worker 重複噴 traceback。"""
|
||||
try:
|
||||
os.remove(_PROMO_SHARED_CACHE_FILE)
|
||||
sys_log.info("promo dashboard shared cache discarded | reason=%s | error=%s", reason, exc)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError:
|
||||
sys_log.debug("promo dashboard shared cache discard failed", exc_info=True)
|
||||
|
||||
|
||||
def _write_shared_promo_dashboard_cache(cache_key, data):
|
||||
"""原子寫入促銷 dashboard 共享快取;失敗不阻斷頁面回應。"""
|
||||
cache_file = str(_PROMO_SHARED_CACHE_FILE)
|
||||
@@ -132,10 +143,14 @@ def _write_shared_promo_dashboard_cache(cache_key, data):
|
||||
entries = {}
|
||||
try:
|
||||
if os.path.exists(_PROMO_SHARED_CACHE_FILE):
|
||||
with open(_PROMO_SHARED_CACHE_FILE, 'rb') as f:
|
||||
payload = pickle.load(f)
|
||||
if payload.get('version') == SYSTEM_VERSION and isinstance(payload.get('entries'), dict):
|
||||
entries = payload['entries']
|
||||
try:
|
||||
with open(_PROMO_SHARED_CACHE_FILE, 'rb') as f:
|
||||
payload = pickle.load(f)
|
||||
if payload.get('version') == SYSTEM_VERSION and isinstance(payload.get('entries'), dict):
|
||||
entries = payload['entries']
|
||||
except Exception as exc:
|
||||
_discard_shared_promo_dashboard_cache("write_corrupt", exc)
|
||||
entries = {}
|
||||
entries[cache_key] = data
|
||||
while len(entries) > _PROMO_DASHBOARD_CACHE_MAX:
|
||||
entries.pop(next(iter(entries)))
|
||||
|
||||
@@ -15,6 +15,7 @@ Position: Super Orchestrator above Hermes/NemoTron/OpenClaw
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from json import JSONDecodeError
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
@@ -241,8 +242,22 @@ BUSINESS OBJECTIVE: Optimize e-commerce performance through intelligent automati
|
||||
if not response.success:
|
||||
raise RuntimeError(response.error)
|
||||
|
||||
# Parse and validate response
|
||||
decision_data = json.loads(response.content)
|
||||
# Parse and validate response. Some NIM-compatible models still wrap
|
||||
# JSON in fenced blocks or prepend reasoning text even with json_mode.
|
||||
try:
|
||||
decision_data = self._extract_json_object(response.content)
|
||||
except ValueError as parse_error:
|
||||
logger.warning(
|
||||
"[ElephantAlpha] Coordination JSON parse failed; using evidence fallback. "
|
||||
"model=%s error=%s preview=%r",
|
||||
response.model,
|
||||
parse_error,
|
||||
(response.content or "")[:240],
|
||||
)
|
||||
return self._fallback_decision(
|
||||
business_context,
|
||||
reason="Elephant Alpha 回應不是可解析 JSON,已改用實證 fallback。",
|
||||
)
|
||||
decision = self._parse_strategic_decision(decision_data)
|
||||
|
||||
# Log decision for learning
|
||||
@@ -253,7 +268,46 @@ BUSINESS OBJECTIVE: Optimize e-commerce performance through intelligent automati
|
||||
except Exception as e:
|
||||
logger.error(f"[ElephantAlpha] Coordination failed: {e}")
|
||||
# Fallback to conservative decision
|
||||
return self._fallback_decision(business_context)
|
||||
return self._fallback_decision(
|
||||
business_context,
|
||||
reason=f"Elephant Alpha 協調失敗:{type(e).__name__}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_json_object(raw: str) -> Dict[str, Any]:
|
||||
"""Extract one JSON object from tolerant LLM output."""
|
||||
text_value = (raw or "").strip()
|
||||
if not text_value:
|
||||
raise ValueError("empty response")
|
||||
|
||||
if text_value.startswith("```"):
|
||||
lines = text_value.splitlines()
|
||||
if lines and lines[0].strip().startswith("```"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[-1].strip().startswith("```"):
|
||||
lines = lines[:-1]
|
||||
text_value = "\n".join(lines).strip()
|
||||
|
||||
try:
|
||||
parsed = json.loads(text_value)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
raise ValueError("JSON root is not an object")
|
||||
except JSONDecodeError:
|
||||
pass
|
||||
|
||||
decoder = json.JSONDecoder()
|
||||
for idx, char in enumerate(text_value):
|
||||
if char != "{":
|
||||
continue
|
||||
try:
|
||||
parsed, _end = decoder.raw_decode(text_value[idx:])
|
||||
except JSONDecodeError:
|
||||
continue
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
|
||||
raise ValueError("no JSON object found")
|
||||
|
||||
def _build_coordination_prompt(self, context: Dict[str, Any]) -> str:
|
||||
"""Build detailed coordination prompt for Elephant Alpha"""
|
||||
@@ -431,22 +485,56 @@ Provide your strategic decision in the specified JSON format.
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def _fallback_decision(self, context: Dict[str, Any]) -> StrategicDecision:
|
||||
@staticmethod
|
||||
def _context_concrete_actions(context: Dict[str, Any]) -> List[str]:
|
||||
conditions = context.get("conditions") if isinstance(context, dict) else {}
|
||||
if not isinstance(conditions, dict):
|
||||
return []
|
||||
for key in ("_prefetched_hermes_threats", "_db_evidence_actions"):
|
||||
actions = conditions.get(key)
|
||||
if isinstance(actions, list):
|
||||
cleaned = [str(action).strip() for action in actions if str(action).strip()]
|
||||
if cleaned:
|
||||
return cleaned[:5]
|
||||
return []
|
||||
|
||||
def _fallback_decision(self, context: Dict[str, Any], *, reason: str = "Elephant Alpha unavailable") -> StrategicDecision:
|
||||
"""Fallback decision if Elephant Alpha fails"""
|
||||
concrete_actions = self._context_concrete_actions(context)
|
||||
trigger_type = str((context or {}).get("trigger_type") or "unknown")
|
||||
if concrete_actions:
|
||||
return StrategicDecision(
|
||||
priority="high",
|
||||
agents_required=["hermes", "elephant_alpha"],
|
||||
reasoning=(
|
||||
f"{reason} 已保留 {len(concrete_actions)} 筆 DB/Hermes 價格比對實證;"
|
||||
"僅送人工覆核,不執行自動調價。"
|
||||
),
|
||||
expected_outcome="產生可稽核的人工覆核告警,避免使用無法解析的 LLM 推論文字。",
|
||||
confidence=0.74,
|
||||
execution_plan=[],
|
||||
resource_requirements={
|
||||
"compute_cost": "$0.00",
|
||||
"time_estimate": "人工覆核",
|
||||
"human_oversight": "required",
|
||||
},
|
||||
)
|
||||
|
||||
return StrategicDecision(
|
||||
priority="medium",
|
||||
agents_required=["openclaw"],
|
||||
reasoning="Elephant Alpha unavailable, using conservative OpenClaw strategy",
|
||||
expected_outcome="Basic strategic analysis",
|
||||
agents_required=["elephant_alpha"],
|
||||
reasoning=(
|
||||
f"{reason} trigger={trigger_type},且沒有可稽核的 DB/Hermes 實證;"
|
||||
"不產生策略型行動計畫,避免把推測當成事實。"
|
||||
),
|
||||
expected_outcome="不執行自動動作;需要先確認資料來源或等待下一輪具體實證。",
|
||||
confidence=0.6,
|
||||
execution_plan=[{
|
||||
"step": 1,
|
||||
"agent": "openclaw",
|
||||
"action": "generate_market_analysis",
|
||||
"parameters": context,
|
||||
"expected_duration": "2-3 minutes"
|
||||
}],
|
||||
resource_requirements={"compute_cost": "$0.00", "time_estimate": "5 minutes"}
|
||||
execution_plan=[],
|
||||
resource_requirements={
|
||||
"compute_cost": "$0.00",
|
||||
"time_estimate": "等待實證",
|
||||
"human_oversight": "required",
|
||||
},
|
||||
)
|
||||
|
||||
# Singleton instance
|
||||
|
||||
@@ -304,6 +304,8 @@ SEARCH_NOISE_TOKENS = {
|
||||
}
|
||||
|
||||
SEARCH_IDENTITY_ANCHORS = (
|
||||
"潤浸保濕清爽身體乳液",
|
||||
"閃亮珍珠眼影棒",
|
||||
"智能光感應無線自動除臭芳香噴霧機",
|
||||
"usb精油薰香機",
|
||||
"超音波水氧機",
|
||||
@@ -520,6 +522,8 @@ BRAND_ALIAS_OVERRIDES = {
|
||||
"xiaomi": ("小米有品", "小米", "xiaomi"),
|
||||
"mac": ("m.a.c", "mac", "m a c"),
|
||||
"opi": ("o.p.i", "opi", "o p i"),
|
||||
"curel": ("curel", "珂潤"),
|
||||
"karadium": ("karadium",),
|
||||
"st雞仔牌": ("日本雞仔牌st", "日本st雞仔牌", "st雞仔牌", "雞仔牌st", "雞仔牌"),
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,31 @@ def test_promo_dashboard_shared_cache_ignores_other_versions(tmp_path, monkeypat
|
||||
assert edm_routes._load_shared_promo_dashboard_cache(cache_key) is None
|
||||
|
||||
|
||||
def test_promo_dashboard_shared_cache_discards_corrupt_payload(tmp_path, monkeypatch):
|
||||
from routes import edm_routes
|
||||
|
||||
shared_cache = tmp_path / "promo_dashboard_cache.pkl"
|
||||
shared_cache.write_bytes(b"\x96not-a-pickle")
|
||||
monkeypatch.setattr(edm_routes, "_PROMO_SHARED_CACHE_FILE", shared_cache)
|
||||
|
||||
assert edm_routes._load_shared_promo_dashboard_cache(("edm",)) is None
|
||||
assert not shared_cache.exists()
|
||||
|
||||
|
||||
def test_promo_dashboard_shared_cache_write_recovers_from_corrupt_payload(tmp_path, monkeypatch):
|
||||
from routes import edm_routes
|
||||
|
||||
shared_cache = tmp_path / "promo_dashboard_cache.pkl"
|
||||
shared_cache.write_bytes(b"\x96not-a-pickle")
|
||||
cache_key = ("edm", "default", "desc", "", "bucket", (1, "ts", 1))
|
||||
data = {"items_in_batch": [{"sku": "SKU-1"}]}
|
||||
monkeypatch.setattr(edm_routes, "_PROMO_SHARED_CACHE_FILE", shared_cache)
|
||||
|
||||
edm_routes._write_shared_promo_dashboard_cache(cache_key, data)
|
||||
|
||||
assert edm_routes._load_shared_promo_dashboard_cache(cache_key) == data
|
||||
|
||||
|
||||
def test_sales_analysis_preview_context_cache_avoids_reloading_options(tmp_path, monkeypatch):
|
||||
from routes import sales_routes
|
||||
|
||||
|
||||
99
tests/test_elephant_alpha_orchestrator.py
Normal file
99
tests/test_elephant_alpha_orchestrator.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
def test_elephant_orchestrator_extracts_fenced_json_object():
|
||||
from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator
|
||||
|
||||
payload = ElephantAlphaOrchestrator._extract_json_object(
|
||||
"""```json
|
||||
{"priority":"high","confidence":0.91}
|
||||
```"""
|
||||
)
|
||||
|
||||
assert payload == {"priority": "high", "confidence": 0.91}
|
||||
|
||||
|
||||
def test_elephant_orchestrator_extracts_json_object_from_prefaced_text():
|
||||
from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator
|
||||
|
||||
payload = ElephantAlphaOrchestrator._extract_json_object(
|
||||
'先做分析:\n{"priority":"medium","agents_required":["hermes"]}\n以上。'
|
||||
)
|
||||
|
||||
assert payload["priority"] == "medium"
|
||||
assert payload["agents_required"] == ["hermes"]
|
||||
|
||||
|
||||
def test_elephant_orchestrator_fallback_uses_concrete_evidence_without_plan():
|
||||
from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator
|
||||
|
||||
orchestrator = ElephantAlphaOrchestrator()
|
||||
decision = orchestrator._fallback_decision(
|
||||
{
|
||||
"trigger_type": "market_opportunity",
|
||||
"conditions": {
|
||||
"_prefetched_hermes_threats": [
|
||||
"[SKU-1] 測試商品|MOMO $1,200 vs PChome $990|每件價差 NT$ 210"
|
||||
]
|
||||
},
|
||||
},
|
||||
reason="測試解析失敗",
|
||||
)
|
||||
|
||||
assert decision.priority == "high"
|
||||
assert decision.confidence == 0.74
|
||||
assert decision.execution_plan == []
|
||||
assert "1 筆 DB/Hermes 價格比對實證" in decision.reasoning
|
||||
assert "自動調價" in decision.reasoning
|
||||
|
||||
|
||||
def test_elephant_orchestrator_fallback_without_evidence_has_no_openclaw_action():
|
||||
from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator
|
||||
|
||||
orchestrator = ElephantAlphaOrchestrator()
|
||||
decision = orchestrator._fallback_decision(
|
||||
{"trigger_type": "market_opportunity", "conditions": {}},
|
||||
reason="測試解析失敗",
|
||||
)
|
||||
|
||||
assert decision.confidence == 0.6
|
||||
assert decision.execution_plan == []
|
||||
assert decision.agents_required == ["elephant_alpha"]
|
||||
assert "沒有可稽核的 DB/Hermes 實證" in decision.reasoning
|
||||
|
||||
|
||||
def test_elephant_orchestrator_uses_evidence_fallback_on_non_json_response(monkeypatch):
|
||||
from services.elephant_service import ElephantResponse
|
||||
from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator
|
||||
|
||||
class FakeElephant:
|
||||
def generate(self, **_kwargs):
|
||||
return ElephantResponse(
|
||||
success=True,
|
||||
content="\n我無法輸出 JSON,但建議先處理。",
|
||||
model="fake-model",
|
||||
)
|
||||
|
||||
async def _noop_log(*_args, **_kwargs):
|
||||
raise AssertionError("parse fallback should not log an LLM decision")
|
||||
|
||||
orchestrator = ElephantAlphaOrchestrator()
|
||||
orchestrator.elephant = FakeElephant()
|
||||
monkeypatch.setattr(orchestrator, "_get_recent_performance_metrics", lambda: {})
|
||||
monkeypatch.setattr(orchestrator, "_get_agent_status", lambda: {})
|
||||
monkeypatch.setattr(orchestrator, "_get_system_load", lambda: "normal")
|
||||
monkeypatch.setattr(orchestrator, "_get_pending_actions_count", lambda: 0)
|
||||
monkeypatch.setattr(orchestrator, "_log_decision", _noop_log)
|
||||
|
||||
decision = asyncio.run(orchestrator.analyze_and_coordinate({
|
||||
"trigger_type": "price_drop_alert",
|
||||
"conditions": {
|
||||
"_prefetched_hermes_threats": [
|
||||
"[SKU-9] 實證商品|MOMO $1,000 vs PChome $800|每件價差 NT$ 200"
|
||||
]
|
||||
},
|
||||
}))
|
||||
|
||||
assert decision.priority == "high"
|
||||
assert decision.execution_plan == []
|
||||
assert "1 筆 DB/Hermes 價格比對實證" in decision.reasoning
|
||||
@@ -531,6 +531,38 @@ def test_marketplace_matcher_promotes_nivea_dry_lotion_with_long_shared_anchor()
|
||||
assert "shared_identity_anchor_nivea_dry_lotion" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_curel_body_lotion_with_brand_alias():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【Curel 珂潤】潤浸保濕清爽身體乳液 220ml",
|
||||
"珂潤 Curel 潤浸保濕清爽身體乳液220ml",
|
||||
momo_price=399,
|
||||
competitor_price=399,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
assert "brand_match" in diagnostics.tags
|
||||
assert "shared_identity_anchor" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_karadium_pearl_shadow_stick_anchor():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【KARADIUM】閃亮珍珠眼影棒 1.4g",
|
||||
"karadium 閃亮珍珠眼影棒 1.4g",
|
||||
momo_price=259,
|
||||
competitor_price=259,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
assert "brand_match" in diagnostics.tags
|
||||
assert "shared_identity_anchor" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_refill_core_vs_case_only_pack():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
|
||||
Reference in New Issue
Block a user