""" Terminal Models =============== Phase 19.1 - Omni-Terminal API Models Pydantic models for Terminal SSE communication. @see ADR-031 Omni-Terminal SSE Architecture @author Claude Code (首席架構師) @version 1.0.0 @date 2026-03-28 (台北時間) """ from datetime import datetime, timezone from enum import Enum from typing import Any from pydantic import BaseModel, Field # ============================================================================= # Enums # ============================================================================= class TerminalSessionStatus(str, Enum): """Terminal Session 狀態""" PENDING = "pending" PROCESSING = "processing" COMPLETED = "completed" ABORTED = "aborted" ERROR = "error" # ============================================================================= # Request Models # ============================================================================= class SpatialContext(BaseModel): """ 空間感知上下文 (Ghost Payload) 前端隱形夾帶,讓 AI 知道使用者正在看什麼 """ current_page: str = Field( ..., description="統帥當前所在的路由", examples=["/zh-TW/authorizations", "/zh-TW"], ) focused_entity_id: str | None = Field( default=None, description="正在檢視的實體 ID (incident/approval)", examples=["INC-2026-0001", "APR-2026-0001"], ) class TerminalIntentRequest(BaseModel): """ Terminal Intent 請求 統帥輸入的指令或問題 """ intent: str = Field( ..., description="統帥輸入的原始指令或意圖", min_length=1, max_length=2000, examples=["列出所有待審核的 approval", "為什麼 awoooi-api pod 一直重啟?"], ) context: SpatialContext = Field( ..., description="前端空間感知上下文", ) session_id: str | None = Field( default=None, description="現有 Session ID (用於續傳)", ) class TerminalAbortRequest(BaseModel): """中斷執行請求""" reason: str | None = Field( default=None, description="中斷原因", ) # ============================================================================= # Response Models # ============================================================================= class TerminalIntentResponse(BaseModel): """ Intent 請求回應 返回 session_id 供前端訂閱 SSE """ session_id: str = Field( ..., description="Session UUID", ) stream_url: str = Field( ..., description="SSE 串流訂閱 URL", ) created_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), ) class TerminalStatusResponse(BaseModel): """Session 狀態查詢回應""" session_id: str status: TerminalSessionStatus created_at: datetime last_event_id: int message_count: int class TerminalAbortResponse(BaseModel): """中斷執行回應""" session_id: str aborted: bool message: str # ============================================================================= # SSE Event Models # ============================================================================= class TerminalThoughtEvent(BaseModel): """AI 思考軌跡事件""" agent: str = Field( ..., description="代理人名稱", examples=["System", "Investigator", "Strategist"], ) msg: str = Field( ..., description="思考內容", ) class TerminalToolCallEvent(BaseModel): """工具呼叫事件""" tool: str = Field( ..., description="工具名稱", examples=["K8s-Log-Scanner", "Sentry-Query", "Playbook-Lookup"], ) status: str = Field( ..., description="執行狀態", examples=["executing", "completed", "failed"], ) result: Any | None = Field( default=None, description="執行結果", ) class TerminalRenderUIEvent(BaseModel): """GenUI 渲染事件""" component: str = Field( ..., description="組件名稱 (必須在前端 Registry 中)", examples=["ApprovalCard", "MetricsSummaryCard", "SentryErrorCard"], ) props: dict[str, Any] = Field( default_factory=dict, description="組件 Props", ) position: str = Field( default="inline", description="渲染位置", examples=["inline", "modal", "panel"], ) id: str | None = Field( default=None, description="組件 ID (用於更新/移除)", ) class TerminalActionRequestEvent(BaseModel): """核鑰授權請求事件""" approval_id: str = Field( ..., description="Approval ID", ) risk_level: str = Field( ..., description="風險等級", examples=["LOW", "MEDIUM", "CRITICAL"], ) kubectl: str = Field( ..., description="要執行的 kubectl 命令", ) description: str | None = Field( default=None, description="操作說明", ) class TerminalCompleteEvent(BaseModel): """串流完成事件""" session_id: str message: str = "Stream completed" class TerminalErrorEvent(BaseModel): """錯誤事件""" session_id: str error_code: str message: str recoverable: bool = True # ============================================================================= # Session Model (Redis Storage) # ============================================================================= class TerminalSession(BaseModel): """ Terminal Session 儲存在 Redis,TTL 5 分鐘 """ session_id: str user_id: str | None = None intent: str context: SpatialContext status: TerminalSessionStatus = TerminalSessionStatus.PENDING created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) last_event_id: int = 0 message_count: int = 0 def next_event_id(self) -> int: """取得下一個事件 ID""" self.last_event_id += 1 return self.last_event_id