fix(decision-manager): AI 分析結果寫入 incidents.decision_chain (DB 長期保存)
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

修復 Gap: decision token 只有 Redis TTL 12min,AI 診斷歷史永久丟失
  - 新增 _persist_decision_to_db() method
  - get_or_create_decision() 完成後 fire-and-forget 寫入 PG
  - 寫入: ts / confidence / risk_level / provider / source / diagnosis[:200]
  - try/except 吞錯不影響主流程,warning log 追蹤

DB/Cache 分層:
  PG (長期): incidents.decision_chain (歷史) + outcomes + KM entries
  Redis (短期): decision token dedup + working memory + playbook cache

2026-04-16 Claude Sonnet 4.6 Asia/Taipei

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-16 01:06:19 +08:00
parent ce1a4d286e
commit 457018c0f9

View File

@@ -1271,6 +1271,14 @@ class DecisionManager:
# 4. 儲存最終結果
await self._save_token(token)
# 4b. 2026-04-16 Claude Sonnet 4.6: 將 AI 分析結果寫入 incidents.decision_chain (DB 長期保存)
# 修復 Gap: decision token 只有 Redis TTL ~12minAI 分析結果歷史永久丟失
# 寫入: diagnosis / confidence / risk_level / provider / source
if token.proposal_data:
_fire_and_forget(
self._persist_decision_to_db(incident.incident_id, token.proposal_data)
)
# 5. ADR-030 Phase 4: 自動執行判斷
if token.state == DecisionState.READY and token.proposal_data:
# 評估是否可以自動執行
@@ -2290,6 +2298,68 @@ class DecisionManager:
return None
async def _persist_decision_to_db(
self, incident_id: str, proposal_data: dict
) -> None:
"""
2026-04-16 Claude Sonnet 4.6: 將 AI 分析結果寫入 incidents.decision_chain (PostgreSQL)
修復 Gap: decision token 只有 Redis TTLAI 分析歷史永久丟失
寫入格式: JSON array每次分析追加一條記錄
欄位: ts / confidence / risk_level / provider / source / diagnosis (前 200 字)
"""
try:
from src.db.base import get_db_context
from sqlalchemy import text as _sa_text
import json as _json
from src.utils.timezone import now_taipei
entry = {
"ts": now_taipei().isoformat(),
"confidence": proposal_data.get("confidence", 0.0),
"risk_level": proposal_data.get("risk_level", "unknown"),
"provider": proposal_data.get("provider", proposal_data.get("source", "")),
"source": proposal_data.get("source", ""),
"diagnosis": str(proposal_data.get("diagnosis", proposal_data.get("description", "")))[:200],
}
async with get_db_context() as db:
# 讀取現有 decision_chain (可能為 null 或 json array)
r = await db.execute(
_sa_text(
"SELECT decision_chain FROM incidents WHERE incident_id = :iid"
),
{"iid": incident_id},
)
row = r.fetchone()
if row is None:
return
existing = row[0] or []
if isinstance(existing, str):
try:
existing = _json.loads(existing)
except Exception:
existing = []
if not isinstance(existing, list):
existing = []
existing.append(entry)
await db.execute(
_sa_text(
"UPDATE incidents SET decision_chain = :dc::json WHERE incident_id = :iid"
),
{"dc": _json.dumps(existing), "iid": incident_id},
)
await db.commit()
logger.debug(
"decision_chain_persisted",
incident_id=incident_id,
confidence=entry["confidence"],
provider=entry["provider"],
)
except Exception as e:
logger.warning("decision_chain_persist_failed", incident_id=incident_id, error=str(e))
async def _save_token(self, token: DecisionToken) -> None:
"""儲存決策令牌到 Redis"""
import json