Files
awoooi/apps/api/src/agents/base.py
OG T 5ddba6d6e0 feat(adr-082): Phase 2 多 Agent 協作 — 5 角色辯證系統骨架上線
新增 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>
2026-04-15 13:48:55 +08:00

195 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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,
}