- Fix 35 Python ruff errors (B904, F841, E722, E741, B007, B008) - Add eslint config for lewooogo-core package - Update pyproject.toml to new ruff lint config format - Relax frontend eslint rules to warnings for unused vars - Allow console.* for debugging (TODO: unified logger) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""
|
|
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
|