From a769738499af360703b3322f76b26ab15acdc133 Mon Sep 17 00:00:00 2001 From: OG T Date: Mon, 23 Mar 2026 12:25:39 +0800 Subject: [PATCH] feat(api): Phase 6.4h replace mock DI with real ProposalService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove MockEngine and embedded Proposal/Guardrails classes - Import real ProposalService with OpenClaw LLM integration - Use get_real_proposal_service() for dependency injection - ProposalService integrates: - OpenClaw LLM (Ollama → Gemini → Claude fallback) - Redis Working Memory - PostgreSQL Episodic Memory - TrustEngine risk assessment - Add llm_provider, llm_confidence, kubectl_command to response - Map ApprovalRiskLevel to Tier (LOW=1, MEDIUM=2, CRITICAL=3) Co-Authored-By: Claude Opus 4.5 --- apps/api/src/routers/proposals.py | 164 +++++++++++++++++++----------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/apps/api/src/routers/proposals.py b/apps/api/src/routers/proposals.py index 8ffd65bc..3bead133 100644 --- a/apps/api/src/routers/proposals.py +++ b/apps/api/src/routers/proposals.py @@ -1,24 +1,33 @@ """ -Proposals Router - Phase 6.4g 突觸對接 -====================================== +Proposals Router - Phase 6.4h 真實大腦植入 +========================================== POST /api/v1/incidents/{incident_id}/propose -整合 lewooogo-brain 積木模組實現決策提案生成。 +整合真實 ProposalService + OpenClaw LLM 實現決策提案生成。 """ from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from typing import List +import structlog + +from src.services.proposal_service import get_proposal_service, ProposalService +from src.models.approval import RiskLevel as ApprovalRiskLevel + +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") @@ -26,94 +35,127 @@ class ProposalResponse(BaseModel): 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_proposal_engine(): + +def get_real_proposal_service() -> ProposalService: """ - Phase 6.4g 暫時性 Mock DI,驗證路由暢通 - NOTE: 完全內嵌,不依賴外部 lewooogo-brain 套件 (Docker 相容) + Phase 6.4h 真實依賴注入: 返回 ProposalService 單例 + + ProposalService 整合: + - OpenClaw LLM (Ollama → Gemini → Claude fallback) + - Redis Working Memory + - PostgreSQL Episodic Memory + - TrustEngine 風險評估 """ - from uuid import uuid4 - from pydantic import BaseModel - - # 內嵌 Guardrails 定義 (Docker 相容) - class Guardrails(BaseModel): - require_dry_run: bool = True - allowed_namespace: list[str] = ["awoooi-prod"] - forbidden_commands: list[str] = ["rm -rf", "drop table", "kubectl delete ns"] - max_retries: int = 1 - timeout_sec: int = 60 - - # 內嵌 Proposal 定義 (Docker 相容) - class Proposal(BaseModel): - proposal_id: str - incident_id: str - action: str - description: str - risk_level: str - guardrails: dict - metadata: dict = {} - - 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() + return get_proposal_service() @router.post( "/{incident_id}/propose", response_model=ProposalResponse, status_code=status.HTTP_201_CREATED, - summary="生成決策提案 (Phase 6.4g)", - description="使用 lewooogo-brain 積木生成決策提案", + summary="生成決策提案 (Phase 6.4h)", + description="使用真實 OpenClaw LLM + TrustEngine 生成決策提案", ) async def generate_decision_proposal( incident_id: str, request: ProposalCreateRequest, - engine=Depends(get_proposal_engine) + service: ProposalService = Depends(get_real_proposal_service), ): + """ + 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: - # Guardrails 檢查: require_dry_run 必須為 True + # 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" ) - proposal, message = await engine.generate(incident_id=incident_id) + logger.info( + "proposal_generation_start", + incident_id=incident_id, + ) - if proposal is None: + # 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 ) - # 計算 tier 基於 risk_level - tier_map = {"low": 1, "medium": 2, "high": 3} - tier = tier_map.get(proposal.risk_level, 2) + # 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=proposal.proposal_id, - incident_id=proposal.incident_id, - actions=[proposal.action], + proposal_id=str(approval.id), + incident_id=incident_id, + actions=actions, tier=tier, - guardrails_passed=proposal.guardrails.get("require_dry_run", False), - rejection_reason=None + 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: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal Error: {str(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)}" + )