強化 EA JSON fallback 與 EDM cache 自癒
Some checks failed
CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
OoO
2026-05-21 18:58:52 +08:00
committed by AiderHeal Bot
parent 31f88898c2
commit 5a5f268358
9 changed files with 288 additions and 22 deletions

View File

@@ -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 # 用於模板顯示

View File

@@ -117,6 +117,7 @@ SQL漏斗(~300筆)
- ElephantAlpha 使用 NVIDIA NIM hosted APIproduction 預設模型為 `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()` |

View File

@@ -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 飆高。

View File

@@ -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)))

View File

@@ -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

View File

@@ -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", "雞仔牌"),
}

View File

@@ -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

View 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

View File

@@ -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