V10.535 優化 ElephantAlpha 價格 trigger 查詢
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 已完成未經驗證的外部動作。
|
||||
|
||||
@@ -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 寫入。
|
||||
|
||||
@@ -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
|
||||
"""),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user