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>
This commit is contained in:
OG T
2026-03-31 22:47:54 +08:00
parent 95de7e0e15
commit a94bb57d8b
4 changed files with 281 additions and 21 deletions

View File

@@ -28,6 +28,7 @@ from src.core.redis_client import get_redis
from src.models.incident import Incident
from src.services.graph_rag import BlastRadiusResult, topology_graph
from src.services.incident_memory import get_incident_memory
from src.utils.incident_converter import brain_to_local
logger = structlog.get_logger(__name__)
@@ -69,11 +70,11 @@ class IIncidentEngine(Protocol):
self,
signal_data: dict[str, Any],
) -> Incident | None:
"""處理 Signal: 原子建立或聚合 Incident"""
"""處理 Signal: 原子建立或聚合 Incident (返回 LocalIncidentADR-046)"""
...
async def get_incident(self, incident_id: str) -> Incident | None:
"""取得指定 Incident"""
"""取得指定 Incident (返回 LocalIncidentADR-046)"""
...
async def update_status(
@@ -106,7 +107,13 @@ class IncidentMemoryAdapter:
self._memory = memory
async def load_incident(self, incident_id: str) -> Any:
"""從 Working Memory 載入 Incident"""
"""
從 Working Memory 載入 Incident (返回 BrainIncident供 brain engine 內部使用)
注意: 此 adapter 注入 brain enginebrain 內部呼叫時期望 BrainIncident。
本地服務透過 IncidentEngineAdapter.get_incident() 取得 LocalIncident。
(ADR-046 - 2026-04-01 ogt)
"""
return await self._memory.load_incident(incident_id)
async def save_incident(self, incident: Any, ttl_seconds: int = 604800) -> bool:
@@ -188,18 +195,56 @@ class BlastRadiusAdapter:
return [target] if target != "unknown" else []
# =============================================================================
# IncidentEngineAdapter (ADR-046: brain 輸出轉換為 LocalIncident)
# =============================================================================
class IncidentEngineAdapter:
"""
Incident Engine Adapter - 包裝 lewooogo-brain IncidentEngine
ADR-046 (2026-04-01 ogt): brain 引擎輸出 BrainIncident
透過 brain_to_local() 轉換為 LocalIncident供本地服務消費。
這是本地服務與 brain 引擎之間的邊界層。
讓 IIncidentEngine Protocol 的返回型別宣告 Incident 得以成立。
"""
def __init__(self, brain_engine: Any) -> None:
self._engine = brain_engine
async def process_signal(self, signal_data: dict[str, Any]) -> Incident | None:
"""處理 Signal返回 LocalIncident (brain 輸出轉換)"""
brain_incident = await self._engine.process_signal(signal_data)
if brain_incident is None:
return None
return brain_to_local(brain_incident)
async def get_incident(self, incident_id: str) -> Incident | None:
"""取得 Incident返回 LocalIncident (brain 輸出轉換)"""
brain_incident = await self._engine.get_incident(incident_id)
if brain_incident is None:
return None
return brain_to_local(brain_incident)
async def update_status(self, incident_id: str, status: Any) -> bool:
"""更新狀態 (直接委派brain status 與 local status 值相容)"""
return await self._engine.update_status(incident_id, status)
# =============================================================================
# Singleton (Phase R-R2: 僅保留 lewooogo-brain 版本)
# =============================================================================
_new_incident_engine: Any | None = None
_new_incident_engine: IncidentEngineAdapter | None = None
def get_incident_engine() -> Any:
def get_incident_engine() -> IncidentEngineAdapter:
"""
取得 Incident Engine 實例 (Singleton)
Phase R-R2: 統一使用 lewooogo-brain IncidentEngine。
ADR-046: 返回 IncidentEngineAdapter輸出已轉換為 LocalIncident。
回滾方式: git revert Phase R-R2 commit + redeploy。
Raises:
@@ -214,12 +259,13 @@ def get_incident_engine() -> Any:
memory_adapter = IncidentMemoryAdapter(get_incident_memory())
blast_adapter = BlastRadiusAdapter()
_new_incident_engine = NewIncidentEngine(
brain_engine = NewIncidentEngine(
memory=memory_adapter,
blast_analyzer=blast_adapter,
logger=logger,
)
logger.info("incident_engine_initialized", version="lewooogo-brain")
_new_incident_engine = IncidentEngineAdapter(brain_engine)
logger.info("incident_engine_initialized", version="lewooogo-brain", adapter="IncidentEngineAdapter")
except ImportError as e:
logger.error("lewooogo_brain_engines_not_available", error=str(e))

View File

@@ -25,6 +25,7 @@ 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__)
@@ -121,9 +122,13 @@ class IncidentDbAdapter:
return False
def _record_to_incident(self, record: IncidentRecord) -> Any:
# 2026-04-01 ogt: 返回 BrainIncident (lewooogo-brain),非本地 Incident
# 型別為 Any 避免靜態分析誤報,實際為 lewooogo_brain.interfaces.incident_processor.Incident
"""將 DB Record 轉換為 Incident (lewooogo-brain 版本)"""
"""
將 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,
)