feat(phase-6.4g-6.5b): API Synaptic Integration + Dual-State WarRoom UI

Phase 6.4g (API 突觸對接):
- lewooogo-brain dependency binding in apps/api/pyproject.toml
- POST /api/v1/incidents/{id}/propose route (proposals.py)
- Guardrails integration (8/8 tests passed)

Phase 6.5a (視覺皮層建置):
- DualStateIncidentCard.tsx with Nothing.tech visual compliance
- Ping radar animation for alert state
- Tier-based decision layer UI (AI 執行中 / 等待親核)

Phase 6.5b (神經網路串接):
- Main warroom page integration (page.tsx)
- IncidentResponse → DualState mapper function
- Empty state: "系統穩定。0 活躍異常。"

Tests:
- test_guardrails.py (8/8)
- test_incident_engine.py (6/6)
- test_skill_loader.py (6/6)
- Frontend build: 0 errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-23 11:58:28 +08:00
parent 8eaf2acb0d
commit cb5d0ecfe4
17 changed files with 2206 additions and 39 deletions

View File

View File

@@ -0,0 +1,98 @@
"""
Proposals Router - Phase 6.4g 突觸對接
======================================
POST /api/v1/incidents/{incident_id}/propose
整合 lewooogo-brain 積木模組實現決策提案生成。
"""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from typing import List
router = APIRouter(prefix="/api/v1/incidents", tags=["Proposals"])
class ProposalCreateRequest(BaseModel):
require_dry_run: bool = Field(
default=True,
description="強制要求演練模式,此參數將直接餵給 Guardrails 進行驗證"
)
class ProposalResponse(BaseModel):
proposal_id: str = Field(..., description="決策書唯一識別碼")
incident_id: str = Field(..., description="關聯的事件 ID")
actions: List[str] = Field(..., description="生成的具體作戰指令清單")
tier: int = Field(..., description="判定之授權級別 (1: 自主, 2: 授權, 3: 親核)")
guardrails_passed: bool = Field(..., description="是否完全通過防爆圈檢測")
rejection_reason: str | None = Field(default=None, description="若未通過防爆圈,顯示阻擋原因")
def get_proposal_engine():
"""Phase 6.4g 暫時性 Mock DI驗證路由暢通"""
from lewooogo_brain.interfaces.proposal_engine import Proposal, Guardrails
from uuid import uuid4
class MockEngine:
async def generate(self, incident_id: str) -> tuple[Proposal | None, str]:
return Proposal(
proposal_id=f"prop-{str(uuid4())[:8]}",
incident_id=incident_id,
action="kubectl get pods -n awoooi-prod",
description="Mock proposal for testing",
risk_level="low",
guardrails=self.get_default_guardrails().model_dump(),
metadata={"generated_by": "mock"},
), "Proposal generated (mock)"
async def generate_with_skill(self, incident_id: str, skill_id: str):
return await self.generate(incident_id)
def get_default_guardrails(self) -> Guardrails:
return Guardrails(require_dry_run=True)
return MockEngine()
@router.post(
"/{incident_id}/propose",
response_model=ProposalResponse,
status_code=status.HTTP_201_CREATED,
summary="生成決策提案 (Phase 6.4g)",
description="使用 lewooogo-brain 積木生成決策提案",
)
async def generate_decision_proposal(
incident_id: str,
request: ProposalCreateRequest,
engine=Depends(get_proposal_engine)
):
try:
# Guardrails 檢查: require_dry_run 必須為 True
if not request.require_dry_run:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Guardrail triggered: require_dry_run must be True"
)
proposal, message = await engine.generate(incident_id=incident_id)
if proposal is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=message
)
# 計算 tier 基於 risk_level
tier_map = {"low": 1, "medium": 2, "high": 3}
tier = tier_map.get(proposal.risk_level, 2)
return ProposalResponse(
proposal_id=proposal.proposal_id,
incident_id=proposal.incident_id,
actions=[proposal.action],
tier=tier,
guardrails_passed=proposal.guardrails.get("require_dry_run", False),
rejection_reason=None
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal Error: {str(e)}")