Files
awoooi/apps/api/src/services/incident_memory.py
OG T a94bb57d8b feat(types): ADR-046 IncidentConverter + IncidentEngineAdapter
實作 ADR-046 Option B: IncidentConverter 轉換層,解決
BrainIncident (lewooogo-brain) 與 LocalIncident (apps/api) 型別邊界問題。

變更:
- 新增 src/utils/incident_converter.py
  - brain_to_local(): BrainIncident → LocalIncident
  - local_to_brain(): LocalIncident → BrainIncident
  - ESCALATED → MITIGATING 映射 (brain 無 ESCALATED)
- incident_engine.py: 新增 IncidentEngineAdapter 包裝層
  - process_signal() / get_incident() 輸出轉換為 LocalIncident
  - get_incident_engine() 返回 IncidentEngineAdapter
- incident_memory.py: 加入 brain_to_local import,更新 _record_to_incident 說明
- ADR-046: 標記三個轉換點全部完成

解鎖: #123 proposal_service.py 清理 (下一步)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:47:54 +08:00

230 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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