feat(api): Phase 16 R1.2 完善 PostgreSQL 整合 + 封存策略

lewooogo-brain:
- 新增 IIncidentDbAdapter Protocol (DI 模式)
- load_incident 支援 Episodic Memory 回填
- persist_incident 透過 db_adapter 執行

apps/api:
- 新增 IncidentDbAdapter 實現 (SQLAlchemy 操作封裝)
- 絞殺者模式完整整合 lewooogo-brain + PostgreSQL

Skill 06 v1.4:
- 新增「封存而非刪除」策略 (統帥裁示)
- 封存目錄結構 + ARCHIVE_LOG.md 格式
- 90 天保留期 + 48hr 驗證期

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-25 15:31:03 +08:00
parent a202a2693a
commit 20984fd354
3 changed files with 378 additions and 37 deletions

View File

@@ -475,12 +475,136 @@ class DualIncidentMemory:
)
# =============================================================================
# Phase 16: IncidentDbAdapter (DI 注入實現)
# =============================================================================
class IncidentDbAdapter:
"""
Incident DB Adapter - 實現 lewooogo-brain 的 IIncidentDbAdapter
Phase 16: 將 apps/api 的 SQLAlchemy Model 操作封裝為 adapter
注入到 lewooogo-brain 的 DualIncidentMemory
"""
async def load(self, incident_id: str) -> Incident | None:
"""從 PostgreSQL 載入 Incident"""
try:
async with get_db_context() as db:
from sqlalchemy import select
stmt = select(IncidentRecord).where(
IncidentRecord.incident_id == incident_id
)
result = await db.execute(stmt)
record = result.scalar_one_or_none()
if record:
return self._record_to_incident(record)
return None
except Exception as e:
logger.error("db_adapter_load_failed", incident_id=incident_id, error=str(e))
return None
async def save(self, incident: Incident) -> bool:
"""儲存 Incident 到 PostgreSQL (upsert)"""
try:
async with get_db_context() as db:
from sqlalchemy import select
# 檢查是否已存在
stmt = select(IncidentRecord).where(
IncidentRecord.incident_id == incident.incident_id
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 更新現有記錄
existing.status = incident.status.value
existing.severity = incident.severity.value
existing.signals = [
s.model_dump(mode="json") for s in incident.signals
]
existing.affected_services = incident.affected_services
existing.updated_at = incident.updated_at
if incident.resolved_at:
existing.resolved_at = incident.resolved_at
if incident.closed_at:
existing.closed_at = incident.closed_at
else:
# 建立新記錄
record = IncidentRecord(
incident_id=incident.incident_id,
status=incident.status.value,
severity=incident.severity.value,
signals=[
s.model_dump(mode="json") for s in incident.signals
],
affected_services=incident.affected_services,
decision_chain=(
incident.decision_chain.model_dump(mode="json")
if hasattr(incident, 'decision_chain') and incident.decision_chain
else None
),
proposal_ids=[str(pid) for pid in incident.proposal_ids],
outcome=(
incident.outcome.model_dump(mode="json")
if hasattr(incident, 'outcome') and incident.outcome
else None
),
created_at=incident.created_at,
updated_at=incident.updated_at,
resolved_at=incident.resolved_at,
closed_at=incident.closed_at,
ttl_days=getattr(incident, 'ttl_days', 30),
vectorized=getattr(incident, 'vectorized', False),
)
db.add(record)
logger.debug("db_adapter_save_success", incident_id=incident.incident_id)
return True
except Exception as e:
logger.error("db_adapter_save_failed", incident_id=incident.incident_id, error=str(e))
return False
def _record_to_incident(self, record: IncidentRecord) -> Incident:
"""將 DB Record 轉換為 Incident (lewooogo-brain 版本)"""
# 延遲導入 lewooogo-brain 的 Incident
from lewooogo_brain.interfaces.incident_processor import (
Incident as BrainIncident,
IncidentStatus as BrainIncidentStatus,
Severity as BrainSeverity,
Signal as BrainSignal,
)
# 重建 Signals
signals = []
for s in record.signals or []:
signals.append(BrainSignal.model_validate(s))
return BrainIncident(
incident_id=record.incident_id,
status=BrainIncidentStatus(record.status),
severity=BrainSeverity(record.severity),
signals=signals,
affected_services=record.affected_services or [],
proposal_ids=record.proposal_ids or [],
created_at=record.created_at,
updated_at=record.updated_at,
resolved_at=record.resolved_at,
closed_at=record.closed_at,
)
# =============================================================================
# Singleton + 絞殺者模式 (Phase 16 R1.2)
# =============================================================================
_dual_memory: DualIncidentMemory | None = None
_new_engine_memory: Any | None = None # lewooogo-brain 版本
_db_adapter: IncidentDbAdapter | None = None # DB Adapter singleton
def get_incident_memory() -> DualIncidentMemory:
@@ -516,12 +640,12 @@ def _get_new_engine_memory() -> Any:
注意事項:
- 需要 lewooogo-brain 已安裝 (Dockerfile 已配置)
- PostgreSQL 整合尚未完成 (TODO in lewooogo-brain)
- PostgreSQL 透過 IncidentDbAdapter 注入 (Phase 16 DI 模式)
- 初次啟用建議 48 小時監控
回滾: 設定 USE_NEW_ENGINE=false 即可瞬間切回
"""
global _new_engine_memory
global _new_engine_memory, _db_adapter
if _new_engine_memory is None:
try:
@@ -530,20 +654,24 @@ def _get_new_engine_memory() -> Any:
DualIncidentMemory as NewDualIncidentMemory,
)
from src.core.redis_client import get_redis
from src.db.base import get_db_context
redis_client = get_redis()
# 初始化 lewooogo-brain 版本
# 初始化 DB Adapter (Phase 16 DI 模式)
if _db_adapter is None:
_db_adapter = IncidentDbAdapter()
# 初始化 lewooogo-brain 版本 (含 DB Adapter)
_new_engine_memory = NewDualIncidentMemory(
redis_client=redis_client,
pg_session_factory=get_db_context,
db_adapter=_db_adapter,
key_prefix="awoooi:incidents",
)
logger.info(
"incident_memory_initialized",
engine="lewooogo_brain_package",
db_adapter="IncidentDbAdapter",
redis_connected=True,
)