""" Proposals Router - Phase 6.4h 真實大腦植入 ========================================== POST /api/v1/incidents/{incident_id}/propose 整合真實 ProposalService + OpenClaw LLM 實現決策提案生成。 """ import structlog from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from src.models.approval import RiskLevel as ApprovalRiskLevel from src.services.proposal_service import ProposalService, get_proposal_service logger = structlog.get_logger(__name__) 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="若未通過防爆圈,顯示阻擋原因") # Phase 6.4h: 額外回傳 LLM 決策資訊 llm_provider: str | None = Field(default=None, description="LLM 提供者 (ollama/gemini/claude)") llm_confidence: float | None = Field(default=None, description="LLM 信心度 (0.0-1.0)") kubectl_command: str | None = Field(default=None, description="生成的 kubectl 指令") def get_real_proposal_service() -> ProposalService: """ Phase 6.4h 真實依賴注入: 返回 ProposalService 單例 ProposalService 整合: - OpenClaw LLM (Ollama → Gemini → Claude fallback) - Redis Working Memory - PostgreSQL Episodic Memory - TrustEngine 風險評估 """ return get_proposal_service() @router.post( "/{incident_id}/propose", response_model=ProposalResponse, status_code=status.HTTP_201_CREATED, summary="生成決策提案 (Phase 6.4h)", description="使用真實 OpenClaw LLM + TrustEngine 生成決策提案", ) async def generate_decision_proposal( incident_id: str, request: ProposalCreateRequest, service: ProposalService = Depends(get_real_proposal_service), # noqa: B008 ): """ Phase 6.4h: 真實 LLM 決策提案生成 流程: 1. Guardrails 前置檢查 (require_dry_run 必須為 True) 2. 從 Redis/PostgreSQL 載入 Incident 3. 呼叫 OpenClaw LLM 生成提案 (Ollama → Gemini → Claude fallback) 4. TrustEngine 風險評估與 Tier 判定 5. 建立 ApprovalRequest (向下相容前端) 6. 返回結構化 ProposalResponse """ try: # 1. Guardrails 檢查: require_dry_run 必須為 True if not request.require_dry_run: logger.warning( "guardrails_rejected", incident_id=incident_id, reason="require_dry_run must be True", ) raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Guardrail triggered: require_dry_run must be True" ) logger.info( "proposal_generation_start", incident_id=incident_id, ) # 2. 呼叫真實 ProposalService 生成提案 approval, message = await service.generate_proposal(incident_id) if approval is None: logger.warning( "proposal_generation_failed", incident_id=incident_id, message=message, ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=message ) # 3. 計算 tier 基於 risk_level tier_map = { ApprovalRiskLevel.LOW: 1, # 自主 (AI 可直接執行) ApprovalRiskLevel.MEDIUM: 2, # 授權 (需 1 人簽核) ApprovalRiskLevel.CRITICAL: 3, # 親核 (需 2 人簽核) } tier = tier_map.get(approval.risk_level, 2) # 4. 提取 LLM 資訊 (Phase 6.4h 新增) metadata = approval.metadata or {} kubectl_command = metadata.get("kubectl_command", "") llm_provider = metadata.get("llm_provider") llm_confidence = metadata.get("llm_confidence") # 5. 組裝 actions 清單 actions = [approval.action] if kubectl_command and kubectl_command != approval.action: actions.append(kubectl_command) logger.info( "proposal_generation_complete", incident_id=incident_id, proposal_id=str(approval.id), tier=tier, llm_provider=llm_provider, ) return ProposalResponse( proposal_id=str(approval.id), incident_id=incident_id, actions=actions, tier=tier, guardrails_passed=True, # 通過 TrustEngine 評估 rejection_reason=None, llm_provider=llm_provider, llm_confidence=llm_confidence, kubectl_command=kubectl_command if kubectl_command else None, ) except HTTPException: raise except Exception as e: logger.exception( "proposal_generation_error", incident_id=incident_id, error=str(e), ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal Error: {str(e)}" ) from e