terminal.py, incident.py, utils/timezone.py 同樣問題。 CI runner Python 3.10 無 UTC 常數,導致所有模型靜默 import 失敗。 # 2026-04-06 ogt: 完整修復,不再有漏網之魚 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
6.0 KiB
Python
259 lines
6.0 KiB
Python
"""
|
||
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
|