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>
This commit is contained in:
253
apps/api/src/agents/protocol.py
Normal file
253
apps/api/src/agents/protocol.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
AWOOOI AIOps Phase 2 — 多 Agent 協作訊息協定
|
||||
==============================================
|
||||
定義 5 個 Agent 間傳遞的不可變資料型別。
|
||||
|
||||
設計原則:
|
||||
1. 每個 Agent 有明確的 Input / Output 型別(不共用 dict)
|
||||
2. 所有型別都是 dataclass(快速、可序列化、無外部依賴)
|
||||
3. 降級 / 棄權用明確 AgentVote.ABSTAIN,不用 None 代替
|
||||
4. 全程 immutable — Agent 不得修改彼此的輸出(防 prompt 污染)
|
||||
|
||||
ADR-082: 多 Agent 協作架構(Phase 2)
|
||||
2026-04-15 ogt + Claude Sonnet 4.6(亞太): Phase 2 初始建立
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Enums
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class AgentRole(str, Enum):
|
||||
"""Phase 2 五角色標識"""
|
||||
DIAGNOSTICIAN = "diagnostician"
|
||||
SOLVER = "solver"
|
||||
REVIEWER = "reviewer"
|
||||
CRITIC = "critic"
|
||||
COORDINATOR = "coordinator"
|
||||
|
||||
|
||||
class AgentVote(str, Enum):
|
||||
"""Agent 投票結果"""
|
||||
APPROVE = "approve"
|
||||
REJECT = "reject"
|
||||
REQUEST_REVISION = "request_revision"
|
||||
ABSTAIN = "abstain" # 熔斷 / 超時 / 無足夠資訊
|
||||
DEGRADED = "degraded" # 降級路徑(rule-based mock)
|
||||
|
||||
|
||||
class AgentSessionStatus(str, Enum):
|
||||
"""AgentSession 整體狀態"""
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
DEGRADED = "degraded" # 部分 Agent 熔斷但仍完成
|
||||
FAILED = "failed" # Coordinator 無法輸出任何結論
|
||||
TIMEOUT = "timeout" # 全流程 > 30s
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Diagnostician Output
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class Hypothesis:
|
||||
"""單一根因假設"""
|
||||
description: str
|
||||
confidence: float # 0.0 ~ 1.0
|
||||
evidence_chain: list[str] # 支持此假設的 evidence key
|
||||
category: str = "" # alert_category(KubePod / HostDisk 等)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiagnosisReport:
|
||||
"""
|
||||
Diagnostician Agent 輸出
|
||||
|
||||
包含多個根因假設(按信心排序),
|
||||
top-1 confidence < 0.4 觸發 Coordinator 回退 Investigator 重抓。
|
||||
"""
|
||||
hypotheses: list[Hypothesis]
|
||||
evidence_snapshot_id: str
|
||||
latency_ms: int
|
||||
vote: AgentVote = AgentVote.APPROVE # 資訊足夠 = APPROVE;不足 = ABSTAIN
|
||||
degraded: bool = False # 熔斷降級標記
|
||||
|
||||
@property
|
||||
def top_hypothesis(self) -> Hypothesis | None:
|
||||
"""最高信心假設"""
|
||||
return self.hypotheses[0] if self.hypotheses else None
|
||||
|
||||
@property
|
||||
def top_confidence(self) -> float:
|
||||
return self.top_hypothesis.confidence if self.top_hypothesis else 0.0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Solver Output
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class CandidateAction:
|
||||
"""單一候選修復動作"""
|
||||
action: str # 動作描述(e.g. "restart_service:awoooi-api")
|
||||
blast_radius: int # 0-100:影響範圍評分
|
||||
rollback_cost: int # 0-100:回滾難度
|
||||
confidence: float # 0.0 ~ 1.0
|
||||
rationale: str = "" # 為什麼選此方案
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionPlan:
|
||||
"""
|
||||
Solver Agent 輸出
|
||||
|
||||
對每個根因假設提出 ≥1 個候選方案(含 blast_radius / rollback_cost)。
|
||||
blast_radius > 50 → Reviewer 必須標 `request_revision`。
|
||||
"""
|
||||
candidates: list[CandidateAction]
|
||||
diagnosis_report: DiagnosisReport
|
||||
latency_ms: int
|
||||
vote: AgentVote = AgentVote.APPROVE
|
||||
degraded: bool = False
|
||||
|
||||
@property
|
||||
def top_candidate(self) -> CandidateAction | None:
|
||||
"""最高信心候選方案"""
|
||||
return max(self.candidates, key=lambda c: c.confidence) if self.candidates else None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Reviewer Output
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class ReviewVerdict:
|
||||
"""
|
||||
Reviewer Agent 輸出(安全審查)
|
||||
|
||||
硬核拒絕 HARD_RULES 觸碰動作(delete node / DROP TABLE / force push 等)。
|
||||
vote = REJECT 時,Coordinator 不得執行任何候選方案。
|
||||
"""
|
||||
vote: AgentVote
|
||||
reason: str
|
||||
blocked_actions: list[str] # 被拒絕的動作清單
|
||||
safe_actions: list[str] # 通過安全審查的動作
|
||||
latency_ms: int
|
||||
degraded: bool = False
|
||||
|
||||
@property
|
||||
def is_safe(self) -> bool:
|
||||
return self.vote == AgentVote.APPROVE and bool(self.safe_actions)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Critic Output
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class Challenge:
|
||||
"""Critic 的單一質疑"""
|
||||
target: str # "diagnosis" | "action:{action_str}"
|
||||
argument: str # 質疑的具體理由
|
||||
severity: str # "minor" | "major" | "critical"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CriticReport:
|
||||
"""
|
||||
Critic Agent 輸出(刻意唱反調)
|
||||
|
||||
challenge_count > 0 是 Phase 2 退出條件之一。
|
||||
major/critical challenge 觸發 Coordinator 降低對 Solver 方案的信心。
|
||||
"""
|
||||
challenges: list[Challenge]
|
||||
overall_assessment: str
|
||||
latency_ms: int
|
||||
vote: AgentVote = AgentVote.APPROVE # APPROVE=無重大反對;REJECT=有 critical challenge
|
||||
degraded: bool = False
|
||||
|
||||
@property
|
||||
def challenge_count(self) -> int:
|
||||
return len(self.challenges)
|
||||
|
||||
@property
|
||||
def has_critical_challenge(self) -> bool:
|
||||
return any(c.severity == "critical" for c in self.challenges)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Coordinator Output
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class DecisionPackage:
|
||||
"""
|
||||
Coordinator Agent 輸出(最終決策包)
|
||||
|
||||
包含:
|
||||
- recommended_action: 最終推薦動作(None = 棄權 / 升級人工)
|
||||
- confidence: 綜合信心(Solver × Reviewer × Critic 加權)
|
||||
- requires_human_approval: 是否需要人工審核
|
||||
- debate_summary: 辯證歷程摘要(供 Audit Trail + 學習閉環)
|
||||
- session_status: 整體辯證狀態
|
||||
"""
|
||||
recommended_action: str | None
|
||||
confidence: float
|
||||
requires_human_approval: bool
|
||||
debate_summary: str
|
||||
session_status: AgentSessionStatus
|
||||
latency_ms: int
|
||||
|
||||
# 保留各 Agent 原始輸出(供學習閉環查詢)
|
||||
diagnosis: DiagnosisReport | None = None
|
||||
action_plan: ActionPlan | None = None
|
||||
reviewer_verdict: ReviewVerdict | None = None
|
||||
critic_report: CriticReport | None = None
|
||||
|
||||
# 棄選方案(含原因)
|
||||
rejected_actions: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
# 阻擋原因(requires_human_approval=True 時說明)
|
||||
blocked_reason: str | None = None
|
||||
|
||||
# 全部 Agent 都降級(更嚴格的人工審核信號)
|
||||
all_agents_degraded: bool = False
|
||||
|
||||
@property
|
||||
def is_actionable(self) -> bool:
|
||||
"""可以執行(有推薦動作且信心 > 0.4 且 Reviewer 通過)"""
|
||||
if not self.recommended_action:
|
||||
return False
|
||||
if self.confidence < 0.4:
|
||||
return False
|
||||
if self.reviewer_verdict and self.reviewer_verdict.vote == AgentVote.REJECT:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Agent Session Record(DB 寫入用)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class AgentTurn:
|
||||
"""
|
||||
單次 Agent 發言記錄
|
||||
|
||||
寫入 `agent_sessions` 表的一行,
|
||||
session_id + agent_role 唯一確定一次辯證發言。
|
||||
"""
|
||||
session_id: str
|
||||
incident_id: str
|
||||
agent_role: AgentRole
|
||||
input_hash: str # sha256(input_json)[:16]
|
||||
output_json: dict[str, Any] # Agent 原始輸出
|
||||
latency_ms: int
|
||||
vote: AgentVote
|
||||
degraded: bool = False
|
||||
Reference in New Issue
Block a user