diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 91a3a56..aa786fa 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.535 修 ElephantAlpha 價格 trigger statement timeout:`price_drop_alert` / `market_opportunity` / DB evidence prefetch 改為先篩最近有效 PChome identity_v2,再用 `JOIN LATERAL` 查單一 SKU 最新 MOMO 價格;保留 match_score/tags/diagnostic evidence,避免 scheduler 週期性重查整張 `price_records`。 - V10.534 收緊 PChome rescore accepted gate:`no_match / price_basis=none / alert_tier=suppress` 不得再進 `rescore_accepted_current`,並新增 `--retract-unsafe-accepted` 退回舊的 unsafe accepted rows;Dashboard / daily / growth / OpenClaw 文案改為「重算待人工覆核」,避免操作員把人工覆核隊列誤解為可直接採用或可自動寫價。 - V10.533 補 ElephantAlpha legacy OpenClaw advisory 相容:`generate_dynamic_pricing_strategy` 與既有 `generate_market_strategy` / `generate_resource_optimization_strategy` 一樣只記錄為 skipped,不再觸發 `Unrecognized step` 與 circuit breaker;避免舊協調器輸出的建議型動態定價步驟被誤解為真正可執行任務。 - V10.532 修正 PChome coverage / review queue 口徑落差:`fetch_competitor_coverage()` 的 `attempt_status` / `rescore_accepted_count` / `actionable_review_count` 改跟 review queue 一樣統計「沒有新鮮有效 identity」的商品,而不是只看「完全沒有 identity」;這讓已過期 identity 的 `rescore_accepted_current` 待審能正確顯示在 Dashboard / 狀態 API。 diff --git a/config.py b/config.py index aef61c9..a7ba352 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.534" +SYSTEM_VERSION = "V10.535" 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 38164d4..8fb0513 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -160,6 +160,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 `price_drop_alert` / `market_opportunity` trigger 不得對整張 `price_records` 做全表最新價聚合;必須先篩最近有效 `identity_v2` PChome 候選,再用 per-SKU `JOIN LATERAL` 讀最新 MOMO 價格,並把 `match_score`、`tags`、`match_diagnostic_json` 帶入 evidence。 - ElephantAlpha 協調器收到非純 JSON、fenced JSON 或混文字 JSON 時,必須先做容錯抽取;仍無法解析時,只能使用 DB/Hermes 實證生成保守 HITL fallback。fallback 不得放入 OpenClaw `generate_*` 類舊策略步驟,也不得暗示已自動調價。 - ElephantAlpha 執行器若遇到舊版 OpenClaw strategy 類步驟(含 `generate_market_strategy` / `generate_dynamic_pricing_strategy` / `generate_resource_optimization_strategy`),只能記錄為 advisory skipped,不得觸發 circuit breaker,也不得轉成實際排程、外部呼叫或價格行動。 - `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 已完成未經驗證的外部動作。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index bd594b0..a775cfc 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.535 ElephantAlpha price trigger 查詢瘦身**: 正式 scheduler 日誌顯示 `price_drop_alert` trigger 對整張 `price_records` 做 `DISTINCT ON` 最新價造成 statement timeout。`price_drop_alert`、`market_opportunity` 與 EA DB evidence prefetch 改為先篩最近有效 PChome identity_v2 競品,再用 `JOIN LATERAL` 只查該 SKU 最新 MOMO 價格,保留 match_score/tags/diagnostic evidence 給 Telegram HITL,不再用全表最新價子查詢。 - **V10.534 rescore accepted gate 收緊與語意修正**: 正式 96 筆 `rescore_accepted_current` 盤點顯示多數仍是 `manual_review / identity_review`,且有 2 筆 `no_match / none / suppress` 混入。收緊 `classify_match_attempt_row()`:`no_match`、`price_basis=none`、`alert_tier=suppress` 不得 gate pass;新增 `--retract-unsafe-accepted` 可把既有 unsafe accepted 退回 `true_low_confidence`。Dashboard / daily / growth / OpenClaw 文案改成「重算待人工覆核」,明確表示仍需人工確認身份後才可寫正式價差。 - **V10.533 ElephantAlpha legacy OpenClaw advisory 相容**: 正式 scheduler 日誌出現 `Unrecognized step: agent=openclaw action=generate_dynamic_pricing_strategy`,屬於舊協調器把建議型策略文字放進 execution plan。執行器現在將 `generate_dynamic_pricing_strategy` 納入既有 OpenClaw advisory no-op 清單,只記錄 skipped,不觸發 circuit breaker,也不轉成自動調價或外部呼叫。 - **V10.532 coverage / review queue 口徑對齊**: V10.531 materialize 96 筆 `rescore_accepted_current` 後,DB 最新狀態正確,但 `/api/ai/pchome-match/backfill/status` 的 `rescore_accepted_count` 仍為 0。原因是 coverage 的 `attempt_status` 統計只看「完全沒有 identity」商品,而 review queue 看的是「沒有新鮮有效 identity」商品。改為以 `fresh_competitor` 排除條件統計,讓 stale identity 的重算可採用待審能正確上屏;正式價差表仍未被 rescore materialize 寫入。 diff --git a/services/elephant_alpha_autonomous_engine.py b/services/elephant_alpha_autonomous_engine.py index 5506d9c..e2bbf74 100644 --- a/services/elephant_alpha_autonomous_engine.py +++ b/services/elephant_alpha_autonomous_engine.py @@ -393,23 +393,46 @@ class ElephantAlphaAutonomousEngine: rows = session.execute( text(""" SELECT p.i_code AS sku, p.name, p.category, - cp.price AS competitor_price, pr.price AS momo_price, - ((pr.price - cp.price) / NULLIF(pr.price, 0) * 100) AS price_gap_pct, + cp.competitor_price, pr.momo_price, + ((pr.momo_price - cp.competitor_price) / NULLIF(pr.momo_price, 0) * 100) AS price_gap_pct, cp.competitor_product_id, cp.competitor_product_name, + cp.match_score, + cp.tags, + cp.match_diagnostic_json, cp.crawled_at - FROM products p - JOIN ( - SELECT DISTINCT ON (product_id) product_id, price - FROM price_records - ORDER BY product_id, timestamp DESC - ) pr ON pr.product_id = p.id - JOIN competitor_prices cp ON cp.sku = p.i_code - WHERE cp.expires_at > NOW() - AND COALESCE(cp.match_score, 0) >= 0.76 - AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' - AND cp.price < pr.price * 0.85 - AND cp.crawled_at >= NOW() - INTERVAL '2 hours' + FROM ( + SELECT cp.sku, + 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' + AND (cp.expires_at IS NULL OR cp.expires_at > NOW()) + AND cp.price IS NOT NULL + AND cp.price > 0 + AND cp.crawled_at >= NOW() - INTERVAL '2 hours' + AND COALESCE(cp.match_score, 0) >= 0.76 + AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' + ) cp + JOIN products p ON p.i_code = cp.sku + JOIN LATERAL ( + SELECT pr.price AS momo_price + FROM price_records pr + WHERE pr.product_id = p.id + AND pr.price IS NOT NULL + AND pr.price > 0 + ORDER BY pr.timestamp DESC, pr.id DESC + LIMIT 1 + ) pr ON TRUE + WHERE p.status = 'ACTIVE' + AND cp.competitor_price < pr.momo_price * 0.85 + ORDER BY ((pr.momo_price - cp.competitor_price) / NULLIF(pr.momo_price, 0)) DESC NULLS LAST, + cp.crawled_at DESC NULLS LAST LIMIT 10 """) ).mappings().fetchall() @@ -430,23 +453,46 @@ class ElephantAlphaAutonomousEngine: rows = session.execute( text(""" SELECT p.i_code AS sku, p.name, p.category, - cp.price AS competitor_price, pr.price AS momo_price, - ((pr.price - cp.price) / NULLIF(pr.price, 0) * 100) AS price_gap_pct, + cp.competitor_price, pr.momo_price, + ((pr.momo_price - cp.competitor_price) / NULLIF(pr.momo_price, 0) * 100) AS price_gap_pct, cp.competitor_product_id, cp.competitor_product_name, + cp.match_score, + cp.tags, + cp.match_diagnostic_json, cp.crawled_at - FROM products p - JOIN ( - SELECT DISTINCT ON (product_id) product_id, price - FROM price_records - ORDER BY product_id, timestamp DESC - ) pr ON pr.product_id = p.id - JOIN competitor_prices cp ON cp.sku = p.i_code - WHERE cp.expires_at > NOW() - AND COALESCE(cp.match_score, 0) >= 0.76 - AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' - AND cp.price > pr.price * 1.05 - AND cp.crawled_at >= NOW() - INTERVAL '1 hour' + FROM ( + SELECT cp.sku, + 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' + AND (cp.expires_at IS NULL OR cp.expires_at > NOW()) + AND cp.price IS NOT NULL + AND cp.price > 0 + AND cp.crawled_at >= NOW() - INTERVAL '1 hour' + AND COALESCE(cp.match_score, 0) >= 0.76 + AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' + ) cp + JOIN products p ON p.i_code = cp.sku + JOIN LATERAL ( + SELECT pr.price AS momo_price + FROM price_records pr + WHERE pr.product_id = p.id + AND pr.price IS NOT NULL + AND pr.price > 0 + ORDER BY pr.timestamp DESC, pr.id DESC + LIMIT 1 + ) pr ON TRUE + WHERE p.status = 'ACTIVE' + AND cp.competitor_price > pr.momo_price * 1.05 + ORDER BY ((cp.competitor_price - pr.momo_price) / NULLIF(pr.momo_price, 0)) DESC NULLS LAST, + cp.crawled_at DESC NULLS LAST LIMIT 5 """) ).mappings().fetchall() @@ -1468,21 +1514,7 @@ class ElephantAlphaAutonomousEngine: try: rows = session.execute( text(""" - WITH latest_momo AS ( - SELECT DISTINCT ON (p.i_code) - p.i_code AS sku, - p.name, - p.category, - pr.price AS momo_price, - pr.timestamp - FROM products p - JOIN price_records pr ON pr.product_id = p.id - WHERE p.status = 'ACTIVE' - AND pr.price IS NOT NULL - AND pr.price > 0 - ORDER BY p.i_code, pr.timestamp DESC, pr.id DESC - ), - latest_competitor AS ( + WITH latest_competitor AS ( SELECT DISTINCT ON (cp.sku) cp.sku, cp.price AS competitor_price, @@ -1502,21 +1534,33 @@ class ElephantAlphaAutonomousEngine: AND cp.crawled_at >= NOW() - INTERVAL '2 hours' ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST ) - SELECT lm.sku, lm.name, lm.category, - lm.momo_price, + SELECT p.i_code AS sku, p.name, p.category, + pr.momo_price, lc.competitor_price, - ((lm.momo_price - lc.competitor_price) / NULLIF(lm.momo_price, 0) * 100) AS price_gap_pct, + ((pr.momo_price - lc.competitor_price) / NULLIF(pr.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 - WHERE lc.competitor_price < lm.momo_price * 0.85 - OR lc.competitor_price > lm.momo_price * 1.05 - ORDER BY ABS((lm.momo_price - lc.competitor_price) / NULLIF(lm.momo_price, 0)) DESC NULLS LAST, + FROM latest_competitor lc + JOIN products p ON p.i_code = lc.sku + JOIN LATERAL ( + SELECT pr.price AS momo_price + FROM price_records pr + WHERE pr.product_id = p.id + AND pr.price IS NOT NULL + AND pr.price > 0 + ORDER BY pr.timestamp DESC, pr.id DESC + LIMIT 1 + ) pr ON TRUE + WHERE p.status = 'ACTIVE' + AND ( + lc.competitor_price < pr.momo_price * 0.85 + OR lc.competitor_price > pr.momo_price * 1.05 + ) + ORDER BY ABS((pr.momo_price - lc.competitor_price) / NULLIF(pr.momo_price, 0)) DESC NULLS LAST, lc.crawled_at DESC NULLS LAST LIMIT :limit """), diff --git a/tests/test_elephant_alpha_engine.py b/tests/test_elephant_alpha_engine.py index c72114b..be8965f 100644 --- a/tests/test_elephant_alpha_engine.py +++ b/tests/test_elephant_alpha_engine.py @@ -198,6 +198,43 @@ def test_execute_autonomous_decision_uses_db_evidence_without_hermes_prefetch(mo assert notified == ["price_drop_alert"] +def test_price_trigger_queries_use_lateral_latest_price_lookup(monkeypatch): + import services.elephant_alpha_autonomous_engine as engine_module + from services.elephant_alpha_autonomous_engine import ( + AutonomousTrigger, + ElephantAlphaAutonomousEngine, + ) + + captured_sql = [] + + class _FakeResult: + def mappings(self): + return self + + def fetchall(self): + return [] + + class _FakeSession: + def execute(self, statement, params=None): + captured_sql.append(str(statement)) + return _FakeResult() + + def close(self): + return None + + monkeypatch.setattr(engine_module, "get_session", lambda: _FakeSession()) + + engine = ElephantAlphaAutonomousEngine() + asyncio.run(engine._check_price_drop_trigger(AutonomousTrigger("price_drop_alert", {}, 0.8, True))) + asyncio.run(engine._check_market_opportunity_trigger(AutonomousTrigger("market_opportunity", {}, 0.8, True))) + assert engine._fetch_recent_competitor_evidence_actions(top_n=2) is None + + assert len(captured_sql) == 3 + assert all("JOIN LATERAL" in sql for sql in captured_sql) + assert all("SELECT DISTINCT ON (product_id)" not in sql for sql in captured_sql) + assert all("latest_momo" not in sql for sql in captured_sql) + + def test_escalate_resource_optimization_without_evidence_is_suppressed(monkeypatch): import services.elephant_alpha_autonomous_engine as engine_module from services.elephant_alpha_autonomous_engine import (