新增 5 個 Agent + Orchestrator + DecisionManager 接線: - protocol.py: DiagnosisReport / ActionPlan / ReviewVerdict / CriticReport / DecisionPackage 型別系統 - DiagnosticianAgent: RCA 根因分析,confidence < 0.4 → ABSTAIN - SolverAgent: 修復方案軍師,blast_radius 評分 + 降級 rule-based mock - ReviewerAgent: 安全審查,HARD_RULES 靜態 pattern + blast_radius 閾值 (>50 revision, >80 reject) - CriticAgent: 刻意唱反調,強制 3 問批判性思維,critical challenge → REJECT - CoordinatorAgent: 純規則聚合,6 級決策閘,REQUEST_REVISION → 強制人工 - AgentOrchestrator: 30s 全局超時,Reviewer ‖ Critic 並行,DB Immutable Event Sourcing + Redis Streams - DecisionManager: AIOPS_P2_ENABLED gate + _package_to_proposal_data 橋接既有 proposal_data 格式 - AgentSession DB table + 4 個複合 index - ADR-082 決策記錄 Gate 2 修復(7 項): - CRITICAL: DELETE FROM regex lookahead 位置錯誤(移至 FROM 後) - CRITICAL: REQUEST_REVISION 可抵達 auto-execute 路徑(改回 requires_human_approval=True) - IMPORTANT: _extract_json flat regex 不支援巢狀 JSON(改 find/rfind 邊界提取) - IMPORTANT: all_degraded 遺漏 verdict.degraded(補全 4 個 Agent) - IMPORTANT: Solver ABSTAIN guard 放行降級假設(改為無論 hypotheses 有無均跳過) - IMPORTANT: dataclasses.asdict() Enum 未序列化導致 DB 寫入靜默失敗(加 json.dumps default handler) - IMPORTANT: P2 gate 直讀屬性繞過父 Phase 守衛(改用 is_phase_enabled(2)) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
5.0 KiB
Python
195 lines
5.0 KiB
Python
"""
|
||
Base Agent - 專家 Agent 基礎類別
|
||
================================
|
||
|
||
定義所有專家 Agent 的共用介面和工具
|
||
|
||
使用 claude-agent-sdk 的 AgentDefinition
|
||
符合 ADR-009 架構規範
|
||
"""
|
||
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass, field
|
||
from datetime import UTC, datetime
|
||
from enum import Enum
|
||
from typing import Any, Generic, TypeVar
|
||
|
||
import structlog
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
# =============================================================================
|
||
# Agent Result Base
|
||
# =============================================================================
|
||
|
||
|
||
class AgentStatus(str, Enum):
|
||
"""Agent 執行狀態"""
|
||
PENDING = "pending"
|
||
RUNNING = "running"
|
||
SUCCESS = "success"
|
||
FAILED = "failed"
|
||
TIMEOUT = "timeout"
|
||
|
||
|
||
@dataclass
|
||
class AgentResult:
|
||
"""
|
||
Agent 執行結果基類
|
||
|
||
所有專家 Agent 的輸出都必須包含:
|
||
- agent_name: 識別哪個 Agent
|
||
- status: 執行狀態
|
||
- confidence: 信心分數 (0-1)
|
||
- analysis: 分析摘要
|
||
- latency_ms: 執行時間
|
||
"""
|
||
agent_name: str
|
||
status: AgentStatus
|
||
confidence: float
|
||
analysis: str
|
||
latency_ms: int
|
||
error: str | None = None
|
||
raw_response: dict[str, Any] = field(default_factory=dict)
|
||
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
"""轉換為 dict (API 回傳用)"""
|
||
return {
|
||
"agent_name": self.agent_name,
|
||
"status": self.status.value,
|
||
"confidence": self.confidence,
|
||
"analysis": self.analysis,
|
||
"latency_ms": self.latency_ms,
|
||
"error": self.error,
|
||
"timestamp": self.timestamp.isoformat(),
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# Base Agent
|
||
# =============================================================================
|
||
|
||
T = TypeVar("T", bound=AgentResult)
|
||
|
||
|
||
class BaseAgent(ABC, Generic[T]):
|
||
"""
|
||
專家 Agent 基礎類別
|
||
|
||
所有專家 Agent 都繼承此類別,並實作:
|
||
- analyze(): 核心分析邏輯
|
||
- _build_prompt(): 建構 Prompt
|
||
- _parse_response(): 解析回應
|
||
|
||
使用方式:
|
||
```python
|
||
agent = SecurityAgent()
|
||
result = await agent.analyze(incident_context)
|
||
```
|
||
"""
|
||
|
||
# Agent 識別資訊 (子類別覆寫)
|
||
AGENT_NAME: str = "base"
|
||
AGENT_DESCRIPTION: str = "Base Agent"
|
||
AGENT_TOOLS: list[str] = ["Read", "Grep"]
|
||
|
||
def __init__(self, timeout_sec: float = 30.0):
|
||
"""
|
||
初始化 Agent
|
||
|
||
Args:
|
||
timeout_sec: 執行超時時間 (秒)
|
||
"""
|
||
self.timeout_sec = timeout_sec
|
||
self.logger = logger.bind(agent=self.AGENT_NAME)
|
||
|
||
@abstractmethod
|
||
async def analyze(self, context: dict[str, Any]) -> T:
|
||
"""
|
||
執行分析 (子類別必須實作)
|
||
|
||
Args:
|
||
context: 分析上下文 (incident 資訊)
|
||
|
||
Returns:
|
||
AgentResult 子類別實例
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def _build_prompt(self, context: dict[str, Any]) -> str:
|
||
"""
|
||
建構 Prompt (子類別必須實作)
|
||
|
||
Args:
|
||
context: 分析上下文
|
||
|
||
Returns:
|
||
給 LLM 的 Prompt
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def _parse_response(self, response: str) -> dict[str, Any]:
|
||
"""
|
||
解析 LLM 回應 (子類別必須實作)
|
||
|
||
Args:
|
||
response: LLM 原始回應
|
||
|
||
Returns:
|
||
解析後的結構化資料
|
||
"""
|
||
pass
|
||
|
||
def _extract_json(self, text: str) -> dict[str, Any]:
|
||
"""
|
||
從 LLM 回應中提取 JSON
|
||
|
||
支援:
|
||
- ```json ... ``` 區塊
|
||
- 純 JSON 文字
|
||
"""
|
||
import json
|
||
import re
|
||
|
||
# 嘗試 ```json ... ``` 格式
|
||
match = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
|
||
if match:
|
||
try:
|
||
return json.loads(match.group(1))
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 嘗試從第一個 { 到最後一個 } 提取(支援巢狀 JSON)
|
||
# Gate 2: 舊 r"\{[^{}]*\}" 會拒絕巢狀物件,造成所有 Agent LLM 回應解析失敗
|
||
start = text.find("{")
|
||
end = text.rfind("}")
|
||
if start != -1 and end > start:
|
||
try:
|
||
return json.loads(text[start:end + 1])
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 嘗試整段解析
|
||
try:
|
||
return json.loads(text)
|
||
except json.JSONDecodeError:
|
||
self.logger.warning("json_parse_failed", text=text[:200])
|
||
return {}
|
||
|
||
def _get_agent_definition(self) -> dict[str, Any]:
|
||
"""
|
||
取得 Claude Agent SDK 的 AgentDefinition
|
||
|
||
Returns:
|
||
符合 SDK 規範的 AgentDefinition dict
|
||
"""
|
||
return {
|
||
"name": self.AGENT_NAME,
|
||
"description": self.AGENT_DESCRIPTION,
|
||
"tools": self.AGENT_TOOLS,
|
||
}
|