diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py index 0c2bca8b..63941918 100644 --- a/apps/api/src/services/decision_manager.py +++ b/apps/api/src/services/decision_manager.py @@ -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 ~12min,AI 分析結果歷史永久丟失 + # 寫入: 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 TTL,AI 分析歷史永久丟失 + + 寫入格式: 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