""" Incident Memory Provider - 事件記憶體提供者 ============================================ Phase 6.4e: DualIncidentMemory 整合 Phase 16 R1.2: 絞殺者模式 (Strangler Fig Pattern) 2026-03-26 Phase R-R2 (2026-04-01 ogt): 移除內嵌 DualIncidentMemory 重複邏輯, 全面切換至 lewooogo-brain。回滾方式: git revert + redeploy。 設計: - IncidentDbAdapter: SQLAlchemy Bridge,注入 lewooogo-brain DualIncidentMemory - 雙層記憶體: Working (Redis) + Episodic (PostgreSQL) - 反向索引: namespace:target -> incident_id 統帥鐵律: - Working Memory (Redis): 7 天 TTL - Episodic Memory (PostgreSQL): 永久 - 反向索引: 30 分鐘 TTL (聚合窗口) """ from typing import Any import structlog from src.core.redis_client import get_redis from src.db.base import get_db_context from src.db.models import IncidentRecord from src.models.incident import Incident from src.utils.incident_converter import brain_to_local logger = structlog.get_logger(__name__) # ============================================================================= # 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) -> Any: """ 將 DB Record 轉換為 BrainIncident (lewooogo-brain 版本) 注意: 返回 BrainIncident 供 lewooogo-brain DualIncidentMemory 內部使用。 本地服務消費時透過 IncidentConverter.brain_to_local() 轉換。 (ADR-046 - 2026-04-01 ogt) """ from lewooogo_brain.interfaces.incident_processor import ( Incident as BrainIncident, ) from lewooogo_brain.interfaces.incident_processor import ( IncidentStatus as BrainIncidentStatus, ) from lewooogo_brain.interfaces.incident_processor import ( Severity as BrainSeverity, ) from lewooogo_brain.interfaces.incident_processor import ( Signal as BrainSignal, ) 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 R-R2: 僅保留 lewooogo-brain 版本) # ============================================================================= _new_engine_memory: Any | None = None _db_adapter: IncidentDbAdapter | None = None def get_incident_memory() -> Any: """ 取得 DualIncidentMemory 實例 (Singleton) Phase R-R2: 統一使用 lewooogo-brain 套件版本。 回滾方式: git revert Phase R-R2 commit + redeploy。 """ return _get_new_engine_memory() def _get_new_engine_memory() -> Any: """ 取得 lewooogo-brain 套件版本 注意事項: - 需要 lewooogo-brain 已安裝 (Dockerfile 已配置) - PostgreSQL 透過 IncidentDbAdapter 注入 (Phase 16 DI 模式) """ global _new_engine_memory, _db_adapter if _new_engine_memory is None: try: from lewooogo_brain.adapters.incident_memory import ( DualIncidentMemory as NewDualIncidentMemory, ) redis_client = get_redis() if _db_adapter is None: _db_adapter = IncidentDbAdapter() _new_engine_memory = NewDualIncidentMemory( redis_client=redis_client, db_adapter=_db_adapter, key_prefix="awoooi:incidents", ) logger.info( "incident_memory_initialized", engine="lewooogo_brain_package", db_adapter="IncidentDbAdapter", redis_connected=True, ) except ImportError as e: logger.error( "lewooogo_brain_not_available", error=str(e), ) raise except Exception as e: logger.error( "new_engine_init_failed", error=str(e), ) raise return _new_engine_memory