""" AI Decision Models - Phase 2 Structured Output =============================================== CAI-101: OpenClaw AI 結構化輸出模型 防禦性工程鐵律: - 絕對禁止 LLM 輸出無法解析的自由文本 - 必須強制 JSON 格式 + Pydantic 驗證 - blast_radius 為 REQUIRED 欄位,不可遺漏 """ from enum import Enum from pydantic import BaseModel, Field, field_validator class SuggestedAction(str, Enum): """ AI 建議操作類型 必須與 executor.OperationType 對應 2026-04-17 ogt + Claude Sonnet 4.6: 新增 INVESTIGATE/APPLY_HPA/TUNE_RESOURCES 根本原因: prompts.py 列了 6 個值,但 enum 只有 4 個 → NemoTron 輸出 "investigate" → Pydantic 爆炸 → analysis_result = None → 全部 fallback """ RESTART_DEPLOYMENT = "RESTART_DEPLOYMENT" DELETE_POD = "DELETE_POD" SCALE_DEPLOYMENT = "SCALE_DEPLOYMENT" APPLY_HPA = "APPLY_HPA" TUNE_RESOURCES = "TUNE_RESOURCES" INVESTIGATE = "INVESTIGATE" # 調查診斷,不下執行指令 OBSERVE = "OBSERVE" # 觀察等待 NO_ACTION = "NO_ACTION" # 無需處理 class AIRiskLevel(str, Enum): """AI 風險評估等級""" LOW = "low" MEDIUM = "medium" CRITICAL = "critical" class AIDataImpact(str, Enum): """AI 資料影響評估""" NONE = "NONE" READ_ONLY = "READ_ONLY" WRITE = "WRITE" DESTRUCTIVE = "DESTRUCTIVE" class AIBlastRadius(BaseModel): """ 爆炸半徑分析 (REQUIRED - 符合 API 契約) 此物件為必填,LLM 輸出必須包含完整結構 """ affected_pods: int = Field( ..., ge=0, description="受影響的 Pod 數量", ) estimated_downtime: str = Field( ..., description="預估停機時間 (例如: '~30s', '~2 min', '0')", ) related_services: list[str] = Field( default_factory=list, description="相關受影響服務", ) data_impact: AIDataImpact = Field( default=AIDataImpact.NONE, description="資料影響程度", ) @field_validator("data_impact", mode="before") @classmethod def normalize_data_impact(cls, v): """正規化 data_impact (LLM 可能輸出小寫)""" if isinstance(v, str): return v.upper() return v class OpenClawDecision(BaseModel): """ OpenClaw AI 決策輸出 (強制結構化) LLM 必須輸出此格式的 JSON,否則視為解析失敗。 blast_radius 為 REQUIRED 欄位! """ # === 基本操作欄位 === suggested_action: SuggestedAction = Field( ..., description="建議執行的操作類型", ) target_resource: str = Field( ..., description="目標資源名稱 (e.g., 'harbor', 'grafana')", ) namespace: str = Field( default="default", description="Kubernetes namespace", ) # 2026-03-29 ogt: 允許 None,LLM 可能返回 null kubectl_command: str | None = Field( default="", description="具體的 kubectl 指令", ) @field_validator("kubectl_command", mode="before") @classmethod def normalize_kubectl_command(cls, v): """將 null 轉換為空字串""" return v if v is not None else "" # === 風險評估欄位 === risk_level: AIRiskLevel = Field( ..., description="風險等級評估", ) # === REQUIRED: 爆炸半徑 (符合 API 契約) === blast_radius: AIBlastRadius = Field( ..., description="爆炸半徑分析 - REQUIRED", ) # === 分析說明欄位 === action_title: str = Field( default="", description="操作標題 (繁體中文)", ) description: str = Field( default="", description="根本原因分析說明 (繁體中文)", ) reasoning: str = Field( default="", description="給人類主管看的決策理由 (繁體中文)", ) deviation_analysis: str = Field( default="", description="基準線偏差分析 (例如:CPU 85% 超出基準線 45% 達 +4σ)", ) # === 信心度與影響範圍 === # 2026-03-29 ogt: 移除預設值,強制 LLM 必須輸出真實信心分數 # 如果 LLM 沒有輸出 confidence,解析時會補 0.5 並標記為 COLLAB confidence: float = Field( ..., # REQUIRED - 不允許預設值 ge=0.0, le=1.0, description="決策信心度 (0-1) - LLM 必須計算", ) affected_services: list[str] = Field( default_factory=list, description="可能受影響的相關服務", ) # === v6.0 AI 仲裁欄位 === primary_responsibility: str = Field( default="COLLAB", description="主要責任團隊 (FE/BE/INFRA/DB/COLLAB)", ) responsibility_reasoning: str = Field( default="", description="責任判定理由", ) secondary_teams: list[str] = Field( default_factory=list, description="需協助的其他團隊", ) # === v7.0 調優建議與 SignOz 整合 === optimization_suggestions: list[dict] = Field( default_factory=list, description="預防性調優建議 (含 kubectl 指令)", ) signoz_correlation: str = Field( default="", description="SignOz 指標與告警的關聯分析", ) @field_validator("risk_level", mode="before") @classmethod def normalize_risk_level(cls, v): """正規化 risk_level (處理 LLM 可能輸出的非標準值)""" if isinstance(v, str): mapping = { "high": "critical", "severe": "critical", "warning": "medium", "normal": "low", "safe": "low", } return mapping.get(v.lower(), v.lower()) return v @field_validator("suggested_action", mode="before") @classmethod def normalize_suggested_action(cls, v): """ 正規化 suggested_action:大小寫 + 別名映射 + 未知值 fallback 2026-04-17 ogt + Claude Sonnet 4.6(亞太): 根本原因: NemoTron 輸出 "investigate"(小寫) → Pydantic 拒絕 → analysis_result = None 舊版只做 uppercase,未知值仍爆 → 修復為: 先 uppercase,再別名映射,最後 fallback NO_ACTION """ if isinstance(v, str): normalized = v.upper().replace("-", "_").replace(" ", "_") # 別名映射 (LLM 可能輸出非正式名稱) alias_map = { "DIAGNOSE": "INVESTIGATE", "DEBUG": "INVESTIGATE", "MONITOR": "OBSERVE", "WATCH": "OBSERVE", "TUNE": "TUNE_RESOURCES", "HPA": "APPLY_HPA", } normalized = alias_map.get(normalized, normalized) # 未知值 fallback NO_ACTION,絕不讓 Pydantic 爆炸導致 analysis_result = None try: SuggestedAction(normalized) return normalized except ValueError: return "NO_ACTION" return v class OpenClawAnalysisRequest(BaseModel): """分析請求""" force_refresh: bool = Field( default=False, description="強制重新抓取監控數據", ) class OpenClawAnalysisResponse(BaseModel): """分析回應""" success: bool message: str decision: OpenClawDecision | None = None approval_created: bool = Field( default=False, description="是否已建立待簽核卡片", ) approval_id: str | None = Field( default=None, description="建立的 ApprovalRecord ID", ) ai_provider: str = Field( default="unknown", description="使用的 AI 提供者 (ollama/gemini/claude)", ) raw_llm_response: str | None = Field( default=None, description="LLM 原始回應 (debug 用)", )