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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user