From 1f2e8b1e84205bbe0f249943a0f4f20d94c4b249 Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 24 May 2026 16:21:04 +0800 Subject: [PATCH] V10.430 preserve Nemotron decision callback IDs --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 ++- docs/memory/history_logs.md | 1 + services/nemoton_dispatcher_service.py | 4 ++++ services/telegram_templates.py | 6 ++++-- tests/test_nemotron_decision_envelope.py | 8 ++++++- tests/test_telegram_triaged_alert_format.py | 23 +++++++++++++++++++++ 7 files changed, 42 insertions(+), 5 deletions(-) diff --git a/config.py b/config.py index 131e03a..2050d58 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.429" +SYSTEM_VERSION = "V10.430" 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 29011c9..f34e6f4 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.429 +> **適用版本**: V10.430 --- @@ -44,6 +44,7 @@ - 證據不足時不得輸出空泛效益預測;必須標記 `data_quality=missing|partial|stale`,並把建議行動降級成 `human_review`、`needs_research` 或 `silence_alert`。 - Telegram `triaged_alert()` 已支援渲染 `decision_envelope`,讓告警固定呈現嚴重度、證據、建議行動、預期影響、信心度與追蹤 ID;後續觀測台與 PPT 也應共用同一份欄位語意。 - NemoTron `price_alert` / `human_review` 派發會把同款證據、價差、七日銷量變化、營收流失、HITL 邊界與資料品質寫入同一份 `decision_envelope`,並同步放入 EventRouter event 與 KM metadata;12 Agent 後續只能沿用此信封補充分析,不得繞過 matcher / feeder / review service 直接改價或覆寫比價資料。 +- EventRouter / Telegram 的 HITL callback 必須優先使用 `decision_envelope.decision_id` 作為事件追蹤 ID;若上游未帶 `event.id`,`triaged_alert()` 仍會用 `decision_id` 產生 `momo:eig:*` callback,避免價格決策審核落成 `unknown`。 - 競品比價相關的 Agent 建議只能讀 `competitor_match_attempts` / review queue / `competitor_prices` 的既有證據;不得直接寫 `competitor_prices` 或覆蓋 `_should_upsert_competitor_price()` 的保護規則。 ## 一、四 AI Agent 路由架構 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 3f918b9..6c5050c 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.430 NemoTron 決策 callback 追蹤 ID 修補**: `NemoTronDispatcher._send_telegram()` 會把 `decision_envelope.decision_id` 提升為 EventRouter `event.id`;`triaged_alert()` 也會在上游缺 `event.id` 時改用 `decision_id` 產生 `momo:eig:*` callback,避免價格決策通知的「忽略此事件」audit 落成 `unknown` 而無法追查。 - **V10.429 111 / NemoTron 治理回歸補齊**: 補齊 `.env.example` 中 111 circuit breaker、111 allowlist proxy、部署 smoke、資料庫與 Redis runtime keys,並同步大檔 inventory 行數,讓完整測試可覆蓋最新 `V10.425`–`V10.428` 變更;此版不放寬商品比對門檻、不修改 `competitor_prices` 寫入規則。 - **V10.428 NemoTron 價格決策信封落地**: `NemoTronDispatcher` 的 `price_alert` 與 `human_review` 事件現在會產生 12 Agent 共用 `decision_envelope`,把同款證據、價差、七日銷量變化、營收流失、建議行動、HITL guardrails、資料品質與 trace 同步寫入 EventRouter event 與 KM metadata;這讓 Telegram、AI 觀測台、PPT QA 與後續 Agent 協作能讀同一份可稽核證據,而不是各自解析告警文字。 - **V10.427 111 fallback circuit breaker**: `OllamaService` 在選到 111 final fallback 前先讀 `ai_calls` 近 60 分鐘比例;若 Ollama 呼叫 >=20、111 >=5 且占比 >=5%,會短暫跳過 111 並清除 resolved host cache,避免 111 在已偏高時繼續承接長任務。DB 觀測失敗採 fail-open,避免觀測層故障反向中斷 GCP-A/GCP-B 正常路由。 diff --git a/services/nemoton_dispatcher_service.py b/services/nemoton_dispatcher_service.py index aa29875..526b619 100644 --- a/services/nemoton_dispatcher_service.py +++ b/services/nemoton_dispatcher_service.py @@ -1612,6 +1612,7 @@ class NemotronDispatcher: from services.event_router import dispatch_sync payload = {"raw_message": message} event = { + "id": "", "event_type": "nemoton_dispatch_alert", "severity": "alert", "source": "NemoTron.Dispatcher", @@ -1621,8 +1622,11 @@ class NemotronDispatcher: "payload": payload, } if isinstance(decision_envelope, dict) and decision_envelope: + event["id"] = str(decision_envelope.get("decision_id") or "")[:52] event["decision_envelope"] = decision_envelope payload["decision_envelope"] = decision_envelope + if not event["id"]: + event.pop("id", None) dispatch_sync(event=event) except Exception as e: logger.error(f"[Dispatcher] EventRouter dispatch 失敗: {e}") diff --git a/services/telegram_templates.py b/services/telegram_templates.py index 7783a00..5ed64d6 100644 --- a/services/telegram_templates.py +++ b/services/telegram_templates.py @@ -788,7 +788,7 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str, event_type = base_event.get("event_type", "alert") title = escape(str(base_event.get("title", ""))) summary = escape(str(base_event.get("summary", ""))) - event_id = base_event.get("id", "unknown") + event_id = base_event.get("id") safe_ai_summary = escape(str(ai_summary or "")) safe_ai_cause = escape(str(ai_cause or "")) if ai_cause else None safe_actions = [escape(str(a)) for a in (ai_actions or [])] @@ -817,6 +817,8 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str, decision_envelope = base_event.get("decision_envelope") or base_event.get("decision") if isinstance(decision_envelope, dict): lines += _format_decision_envelope(decision_envelope) + if not event_id: + event_id = decision_envelope.get("decision_id") if safe_actions: lines += ["📋 建議行動:"] + [f" • {a}" for a in safe_actions] + [""] if safe_executed: @@ -828,7 +830,7 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str, message = "\n".join(lines) # ADR-012: eig=event_ignore,event_id 截斷確保 ≤ 60 bytes(留 buffer) - _eid = str(event_id)[:52] + _eid = str(event_id or "unknown")[:52] keyboard = {"inline_keyboard": [ [{"text": "🛑 忽略此事件", "callback_data": f"momo:eig:{_eid}"}], diff --git a/tests/test_nemotron_decision_envelope.py b/tests/test_nemotron_decision_envelope.py index 01ce159..c303d51 100644 --- a/tests/test_nemotron_decision_envelope.py +++ b/tests/test_nemotron_decision_envelope.py @@ -49,12 +49,18 @@ def test_send_telegram_attaches_decision_envelope_to_event_router(monkeypatch): fake_event_router.dispatch_sync = lambda event: captured.append(event) monkeypatch.setitem(sys.modules, "services.event_router", fake_event_router) - envelope = {"decision_type": "price_alert", "severity": "P2", "guardrails": {"can_auto_execute": False}} + envelope = { + "decision_id": "nemotron:price_alert:SKU-1:abcdef12", + "decision_type": "price_alert", + "severity": "P2", + "guardrails": {"can_auto_execute": False}, + } module.NemotronDispatcher()._send_telegram("hello price alert", decision_envelope=envelope) assert len(captured) == 1 event = captured[0] assert event["event_type"] == "nemoton_dispatch_alert" + assert event["id"] == "nemotron:price_alert:SKU-1:abcdef12" assert event["decision_envelope"] == envelope assert event["payload"]["decision_envelope"] == envelope assert event["payload"]["raw_message"] == "hello price alert" diff --git a/tests/test_telegram_triaged_alert_format.py b/tests/test_telegram_triaged_alert_format.py index 9fac9aa..2c7a6bf 100644 --- a/tests/test_telegram_triaged_alert_format.py +++ b/tests/test_telegram_triaged_alert_format.py @@ -194,3 +194,26 @@ def test_triaged_alert_renders_decision_envelope_contract(): assert "revenue_loss_7d=42000" in msg assert "ai_call_id=123" in msg assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:decision_env_001" + + +def test_triaged_alert_uses_decision_id_when_event_id_missing(): + _msg, keyboard = triaged_alert( + base_event={ + "event_type": "price_alert", + "title": "MOMO / PChome 價格威脅", + "summary": "高信心同款且 PChome 低價。", + "decision_envelope": { + "decision_id": "nemotron:price_alert:SKU-1:abcdef12", + "decision_type": "price_alert", + "severity": "P2", + "guardrails": {"can_auto_execute": False, "data_quality": "complete"}, + }, + }, + tier_label="NemoTron · P2", + ai_summary="建議進人工價格審核。", + ) + + assert ( + keyboard["inline_keyboard"][0][0]["callback_data"] + == "momo:eig:nemotron:price_alert:SKU-1:abcdef12" + )