feat(ai): Phase 22 OpenClaw + Nemotron 協作架構 (ADR-044)
All checks were successful
E2E Health Check / e2e-health (push) Successful in 17s
All checks were successful
E2E Health Check / e2e-health (push) Successful in 17s
統帥批准實作「仲裁-執行分工」架構: - OpenClaw = 仲裁者 (Why + Risk Level) - Nemotron = 執行者 (How + kubectl Command) 新增功能: - config.py: ENABLE_NEMOTRON_COLLABORATION Feature Flag - openclaw.py: generate_incident_proposal_with_tools() - openclaw.py: _call_nemotron_tools() Nemotron 呼叫 - telegram_gateway.py: TelegramMessage Nemotron 欄位 - telegram_gateway.py: format_with_nemotron() 雙區塊格式 - decision_manager.py: 整合協作方法 - proposal_service.py: 整合協作方法 觸發條件: - LOW 風險 → 僅 OpenClaw - MEDIUM/HIGH/CRITICAL → OpenClaw + Nemotron 雙軌 首席架構師審查: 83/100 條件通過 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -552,11 +552,12 @@ class DecisionManager:
|
||||
# Expert System 同步執行 (立即可用)
|
||||
expert_result = expert_analyze(incident)
|
||||
|
||||
# LLM 非同步執行
|
||||
# LLM 非同步執行 (Phase 22: OpenClaw + Nemotron 協作)
|
||||
# 2026-03-31 Claude Code: 使用 _with_tools 方法啟用雙軌協作
|
||||
try:
|
||||
signals_dict = [s.model_dump() for s in incident.signals]
|
||||
|
||||
llm_result, provider, success = await self._openclaw.generate_incident_proposal(
|
||||
llm_result, provider, success = await self._openclaw.generate_incident_proposal_with_tools(
|
||||
incident_id=incident.incident_id,
|
||||
severity=incident.severity.value,
|
||||
signals=signals_dict,
|
||||
|
||||
@@ -1383,6 +1383,282 @@ Focus on:
|
||||
)
|
||||
return None, provider, False
|
||||
|
||||
# =========================================================================
|
||||
# Phase 22: OpenClaw + Nemotron 協作 (ADR-044)
|
||||
# 2026-03-31 Claude Code: 統帥批准實作
|
||||
# =========================================================================
|
||||
|
||||
async def generate_incident_proposal_with_tools(
|
||||
self,
|
||||
incident_id: str,
|
||||
severity: str,
|
||||
signals: list[dict],
|
||||
affected_services: list[str],
|
||||
expert_context: dict | None = None,
|
||||
) -> tuple[dict | None, str, bool]:
|
||||
"""
|
||||
Phase 22: OpenClaw + Nemotron 協作生成修復提案
|
||||
|
||||
架構:
|
||||
- OpenClaw = 仲裁者 (Arbitrator) - 決定「為什麼」和「風險等級」
|
||||
- Nemotron = 執行者 (Executor) - 決定「怎麼做」和「具體指令」
|
||||
|
||||
觸發條件:
|
||||
- LOW 風險 → 僅 OpenClaw,跳過 Nemotron
|
||||
- MEDIUM/HIGH/CRITICAL → OpenClaw + Nemotron 雙軌
|
||||
|
||||
Args:
|
||||
incident_id: Incident ID
|
||||
severity: 嚴重度 (P0/P1/P2/P3)
|
||||
signals: 關聯的告警訊號
|
||||
affected_services: 受影響服務
|
||||
expert_context: Expert System 初步診斷 (可選)
|
||||
|
||||
Returns:
|
||||
(proposal_dict, provider, success)
|
||||
proposal_dict 新增:
|
||||
- nemotron_enabled: bool
|
||||
- nemotron_tools: list[dict] (如果啟用)
|
||||
- nemotron_validation: str
|
||||
- nemotron_latency_ms: float
|
||||
"""
|
||||
# Feature Flag 檢查
|
||||
if not settings.ENABLE_NEMOTRON_COLLABORATION:
|
||||
logger.info(
|
||||
"nemotron_collaboration_disabled",
|
||||
incident_id=incident_id,
|
||||
reason="Feature flag disabled",
|
||||
)
|
||||
return await self.generate_incident_proposal(
|
||||
incident_id, severity, signals, affected_services, expert_context
|
||||
)
|
||||
|
||||
# Step 1: OpenClaw 仲裁
|
||||
proposal, provider, success = await self.generate_incident_proposal(
|
||||
incident_id, severity, signals, affected_services, expert_context
|
||||
)
|
||||
|
||||
if not success or proposal is None:
|
||||
return proposal, provider, success
|
||||
|
||||
# Step 2: 判斷是否需要 Nemotron
|
||||
risk_level = proposal.get("risk_level", "low").lower()
|
||||
if risk_level == "low":
|
||||
proposal["nemotron_enabled"] = False
|
||||
logger.info(
|
||||
"nemotron_skipped_low_risk",
|
||||
incident_id=incident_id,
|
||||
risk_level=risk_level,
|
||||
)
|
||||
return proposal, provider, True
|
||||
|
||||
# Step 3: 呼叫 Nemotron Tool Calling
|
||||
logger.info(
|
||||
"nemotron_collaboration_start",
|
||||
incident_id=incident_id,
|
||||
risk_level=risk_level,
|
||||
)
|
||||
|
||||
try:
|
||||
nemotron_result = await self._call_nemotron_tools(
|
||||
incident_id=incident_id,
|
||||
reasoning=proposal.get("reasoning", ""),
|
||||
target_resource=proposal.get("target_resource", ""),
|
||||
suggested_action=proposal.get("action", ""),
|
||||
namespace=proposal.get("namespace", "awoooi-prod"),
|
||||
)
|
||||
|
||||
proposal["nemotron_enabled"] = True
|
||||
proposal["nemotron_tools"] = nemotron_result.get("tools", [])
|
||||
proposal["nemotron_validation"] = nemotron_result.get("validation", "⏳ 驗證中")
|
||||
proposal["nemotron_latency_ms"] = nemotron_result.get("latency_ms", 0.0)
|
||||
|
||||
logger.info(
|
||||
"nemotron_collaboration_complete",
|
||||
incident_id=incident_id,
|
||||
tools_count=len(proposal["nemotron_tools"]),
|
||||
validation=proposal["nemotron_validation"],
|
||||
latency_ms=proposal["nemotron_latency_ms"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Nemotron 失敗不阻塞主流程,降級為純 OpenClaw
|
||||
logger.warning(
|
||||
"nemotron_collaboration_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
proposal["nemotron_enabled"] = False
|
||||
proposal["nemotron_tools"] = None
|
||||
proposal["nemotron_validation"] = "❌ 呼叫失敗"
|
||||
proposal["nemotron_latency_ms"] = 0.0
|
||||
|
||||
return proposal, provider, True
|
||||
|
||||
async def _call_nemotron_tools(
|
||||
self,
|
||||
incident_id: str,
|
||||
reasoning: str,
|
||||
target_resource: str,
|
||||
suggested_action: str,
|
||||
namespace: str = "awoooi-prod",
|
||||
) -> dict:
|
||||
"""
|
||||
呼叫 Nemotron 執行 Tool Calling
|
||||
|
||||
Args:
|
||||
incident_id: Incident ID
|
||||
reasoning: OpenClaw 推理結果
|
||||
target_resource: 目標資源名稱
|
||||
suggested_action: OpenClaw 建議的操作
|
||||
namespace: K8s namespace
|
||||
|
||||
Returns:
|
||||
{
|
||||
"tools": [{"tool": str, "args": dict, "valid": bool}],
|
||||
"validation": str,
|
||||
"latency_ms": float
|
||||
}
|
||||
"""
|
||||
import asyncio
|
||||
from src.services.nvidia_provider import get_nvidia_provider
|
||||
|
||||
nvidia = get_nvidia_provider()
|
||||
start_time = time.time()
|
||||
|
||||
# 建構 Tool Calling prompt
|
||||
tool_prompt = f"""根據以下 AI 分析結果,生成對應的 kubectl 操作指令:
|
||||
|
||||
## Incident 上下文
|
||||
- Incident ID: {incident_id}
|
||||
- 目標資源: {target_resource}
|
||||
- Namespace: {namespace}
|
||||
|
||||
## OpenClaw 分析
|
||||
- 建議操作: {suggested_action}
|
||||
- 推理過程: {reasoning[:500]}
|
||||
|
||||
## 你的任務
|
||||
生成最適合的 kubectl 操作。如果操作有風險,請標註驗證步驟。
|
||||
"""
|
||||
|
||||
# 定義可用 Tools (K8s 操作)
|
||||
k8s_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "restart_deployment",
|
||||
"description": "重啟 Deployment (rollout restart)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deployment_name": {"type": "string"},
|
||||
"namespace": {"type": "string", "default": "awoooi-prod"},
|
||||
},
|
||||
"required": ["deployment_name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "scale_deployment",
|
||||
"description": "調整 Deployment 副本數",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deployment_name": {"type": "string"},
|
||||
"replicas": {"type": "integer"},
|
||||
"namespace": {"type": "string", "default": "awoooi-prod"},
|
||||
},
|
||||
"required": ["deployment_name", "replicas"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_pod",
|
||||
"description": "刪除 Pod (強制重建)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pod_name": {"type": "string"},
|
||||
"namespace": {"type": "string", "default": "awoooi-prod"},
|
||||
},
|
||||
"required": ["pod_name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
# 設置超時
|
||||
timeout = settings.NEMOTRON_TIMEOUT_SECONDS
|
||||
|
||||
result = await asyncio.wait_for(
|
||||
nvidia.tool_call(
|
||||
messages=[{"role": "user", "content": tool_prompt}],
|
||||
tools=k8s_tools,
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
|
||||
# 解析 Tool Calling 結果
|
||||
tools = []
|
||||
validation_passed = True
|
||||
|
||||
if result and hasattr(result, "tool_calls") and result.tool_calls:
|
||||
for tc in result.tool_calls:
|
||||
tool_entry = {
|
||||
"tool": tc.tool_name if hasattr(tc, "tool_name") else str(tc.get("name", "unknown")),
|
||||
"args": tc.arguments if hasattr(tc, "arguments") else tc.get("arguments", {}),
|
||||
"valid": tc.valid if hasattr(tc, "valid") else True,
|
||||
}
|
||||
tools.append(tool_entry)
|
||||
if not tool_entry["valid"]:
|
||||
validation_passed = False
|
||||
elif result and isinstance(result, dict) and result.get("tool_calls"):
|
||||
for tc in result["tool_calls"]:
|
||||
tool_entry = {
|
||||
"tool": tc.get("name", "unknown"),
|
||||
"args": tc.get("arguments", {}),
|
||||
"valid": True,
|
||||
}
|
||||
tools.append(tool_entry)
|
||||
|
||||
validation_status = "✅ 驗證通過" if validation_passed and tools else "❌ 驗證失敗"
|
||||
|
||||
return {
|
||||
"tools": tools,
|
||||
"validation": validation_status,
|
||||
"latency_ms": latency_ms,
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
logger.warning(
|
||||
"nemotron_tool_call_timeout",
|
||||
incident_id=incident_id,
|
||||
timeout_seconds=settings.NEMOTRON_TIMEOUT_SECONDS,
|
||||
)
|
||||
return {
|
||||
"tools": [],
|
||||
"validation": "⏳ 呼叫超時",
|
||||
"latency_ms": latency_ms,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
logger.error(
|
||||
"nemotron_tool_call_error",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
raise
|
||||
|
||||
# =========================================================================
|
||||
# Shadow Mode Auto-Tuning
|
||||
# =========================================================================
|
||||
|
||||
@@ -155,10 +155,12 @@ class ProposalService:
|
||||
)
|
||||
|
||||
# 2. 呼叫 OpenClaw LLM 生成提案 (Phase 6.4 核心)
|
||||
# Phase 22: 升級為 OpenClaw + Nemotron 協作 (ADR-044)
|
||||
# 2026-03-31 Claude Code: 使用 _with_tools 方法啟用雙軌協作
|
||||
target = incident.affected_services[0] if incident.affected_services else "unknown"
|
||||
signals_dict = [s.model_dump() for s in incident.signals]
|
||||
|
||||
llm_proposal, provider, llm_success = await self._openclaw.generate_incident_proposal(
|
||||
llm_proposal, provider, llm_success = await self._openclaw.generate_incident_proposal_with_tools(
|
||||
incident_id=incident_id,
|
||||
severity=incident.severity.value,
|
||||
signals=signals_dict,
|
||||
|
||||
@@ -155,6 +155,15 @@ class TelegramMessage:
|
||||
# 2026-03-29 ogt: AI Provider 來源顯示
|
||||
ai_provider: str = "" # ollama/gemini/claude/expert_system/mock
|
||||
|
||||
# ==========================================================================
|
||||
# Phase 22: Nemotron 協作欄位 (ADR-044)
|
||||
# 2026-03-31 Claude Code: OpenClaw + Nemotron 雙軌顯示
|
||||
# ==========================================================================
|
||||
nemotron_enabled: bool = False # 是否啟用 Nemotron 協作
|
||||
nemotron_tools: list[dict] | None = None # Tool Calling 結果 [{"tool": str, "args": dict, "valid": bool}]
|
||||
nemotron_validation: str = "" # "✅ 驗證通過" / "❌ 驗證失敗" / "⏳ 驗證中"
|
||||
nemotron_latency_ms: float = 0.0 # Nemotron 呼叫延遲 (ms)
|
||||
|
||||
def format(self) -> str:
|
||||
"""
|
||||
格式化為 SOUL.md 規範的訊息 (含 AI 仲裁 + SignOz)
|
||||
@@ -270,6 +279,124 @@ class TelegramMessage:
|
||||
|
||||
return message[:900]
|
||||
|
||||
def format_with_nemotron(self) -> str:
|
||||
"""
|
||||
格式化含 Nemotron 結果的訊息 (Phase 22 ADR-044)
|
||||
|
||||
格式:
|
||||
═══════════════════════════
|
||||
🚨 CRITICAL | harbor-core
|
||||
═══════════════════════════
|
||||
📋 INC-20260331-0001
|
||||
🎯 資源: harbor-core-7d4b8c9f5
|
||||
━━━━━━━━━━━━━━━━━━━
|
||||
🤖 OpenClaw 仲裁
|
||||
├ 📊 信心: 🟢 85%
|
||||
├ 👥 責任: BE (後端)
|
||||
└ 💡 原因: JVM Heap 配置不當
|
||||
━━━━━━━━━━━━━━━━━━━
|
||||
🔧 Nemotron 執行方案
|
||||
✅ restart_deployment: awoooi-api
|
||||
✅ scale_deployment: replicas=3
|
||||
└ 驗證: ✅ 驗證通過
|
||||
━━━━━━━━━━━━━━━━━━━
|
||||
🔧 建議: 刪除 Pod
|
||||
⏱️ 停機: ~30s
|
||||
|
||||
Returns:
|
||||
str: 格式化的 Telegram 訊息 (max 1000 字元)
|
||||
"""
|
||||
# 責任映射
|
||||
resp_map = {
|
||||
"FE": "👨💻 FE (前端)",
|
||||
"BE": "⚙️ BE (後端)",
|
||||
"INFRA": "🏗️ INFRA (基礎設施)",
|
||||
"DB": "🗄️ DB (資料庫)",
|
||||
"COLLAB": "🤝 COLLAB (協同處理)",
|
||||
}
|
||||
resp_display = resp_map.get(self.primary_responsibility, "❓ 未知")
|
||||
|
||||
# 信心度顯示
|
||||
confidence_pct = int(self.confidence * 100)
|
||||
if confidence_pct >= 80:
|
||||
conf_emoji = "🟢"
|
||||
elif confidence_pct >= 70:
|
||||
conf_emoji = "🟡"
|
||||
else:
|
||||
conf_emoji = "🔴"
|
||||
|
||||
# 自動生成事件編號
|
||||
if self.incident_id:
|
||||
incident_id = self.incident_id
|
||||
elif self.approval_id.startswith("INC-"):
|
||||
incident_id = self.approval_id
|
||||
else:
|
||||
incident_id = f"INC-{self.approval_id[:8].upper()}"
|
||||
|
||||
# HTML 轉義
|
||||
safe_resource = html.escape(self.resource_name[:35])
|
||||
safe_root_cause = html.escape(self.root_cause[:50])
|
||||
safe_action = html.escape(self.suggested_action[:35])
|
||||
safe_downtime = html.escape(self.estimated_downtime)
|
||||
|
||||
# AI Provider 顯示
|
||||
if self.confidence > 0 and self.ai_provider:
|
||||
provider_names = {
|
||||
"ollama": "Ollama",
|
||||
"gemini": "Gemini",
|
||||
"claude": "Claude",
|
||||
"nvidia": "Nemotron",
|
||||
}
|
||||
provider_display = provider_names.get(self.ai_provider.lower(), self.ai_provider.upper())
|
||||
source_label = f"🤖 <b>{provider_display} 仲裁</b>"
|
||||
elif self.confidence > 0:
|
||||
source_label = "🤖 <b>OpenClaw 仲裁</b>"
|
||||
else:
|
||||
source_label = "⚙️ <b>規則匹配</b>"
|
||||
|
||||
# Nemotron 區塊
|
||||
nemotron_block = ""
|
||||
if self.nemotron_enabled and self.nemotron_tools:
|
||||
tools_lines = []
|
||||
for t in self.nemotron_tools[:3]: # 最多顯示 3 個
|
||||
valid_emoji = "✅" if t.get("valid", False) else "❌"
|
||||
tool_name = html.escape(str(t.get("tool", "unknown"))[:20])
|
||||
args_str = str(t.get("args", {}))[:25]
|
||||
safe_args = html.escape(args_str)
|
||||
tools_lines.append(f" {valid_emoji} {tool_name}: {safe_args}")
|
||||
|
||||
tools_str = "\n".join(tools_lines)
|
||||
validation_display = html.escape(self.nemotron_validation or "⏳ 驗證中")
|
||||
|
||||
nemotron_block = (
|
||||
f"━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"🔧 <b>Nemotron 執行方案</b>\n"
|
||||
f"{tools_str}\n"
|
||||
f"└ 驗證: {validation_display}\n"
|
||||
)
|
||||
if self.nemotron_latency_ms > 0:
|
||||
nemotron_block += f"└ 延遲: {self.nemotron_latency_ms:.0f}ms\n"
|
||||
|
||||
# 組裝訊息
|
||||
message = (
|
||||
f"═══════════════════════════\n"
|
||||
f"{self.status_emoji} <b>{html.escape(self.risk_level)}</b> | {html.escape(self.resource_name[:25])}\n"
|
||||
f"═══════════════════════════\n"
|
||||
f"📋 <code>{html.escape(incident_id)}</code>\n"
|
||||
f"🎯 資源: <code>{safe_resource}</code>\n"
|
||||
f"━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"{source_label}\n"
|
||||
f"├ 📊 信心: {conf_emoji} {confidence_pct}%\n"
|
||||
f"├ 👥 責任: {resp_display}\n"
|
||||
f"└ 💡 原因: {safe_root_cause}\n"
|
||||
f"{nemotron_block}"
|
||||
f"━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"🔧 建議: {safe_action}\n"
|
||||
f"⏱️ 停機: {safe_downtime}"
|
||||
)
|
||||
|
||||
return message[:1000]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 新訊息模板 (2026-03-29 ogt: ADR-038 Telegram 訊息規範)
|
||||
|
||||
Reference in New Issue
Block a user