feat(api): Phase 6.4h replace mock DI with real ProposalService

- 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 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-23 12:25:39 +08:00
parent be8ed1f7ba
commit a769738499

View File

@@ -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)}"
)