feat(phase24-b3): NemotronProvider 抽取 + incident-card 重構
Phase 24 B3: - 新增 ai_providers/nemotron.py: NemotronProvider 封裝 K8s Tool Calling 搬移自 openclaw.py _call_nemotron_tools (L1623-1785) capabilities=tool_calling, privacy_level=cloud - ai_router.py: 加入 NemotronProvider 到 Registry - ai_providers/__init__.py: 匯出 NemotronProvider Phase R-UI2 (架構師 Warning): - incident-card.tsx: 抽取 useApprovalAction hook handleApprove/handleReject 60行重複邏輯 → 共用 hook 行為完全不變,維護性提升 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,5 +10,6 @@ AI Provider Registry & Dual-Track Routing Architecture
|
||||
"""
|
||||
|
||||
from src.services.ai_providers.interfaces import AIProvider, AIResult
|
||||
from src.services.ai_providers.nemotron import NemotronProvider
|
||||
|
||||
__all__ = ["AIProvider", "AIResult"]
|
||||
__all__ = ["AIProvider", "AIResult", "NemotronProvider"]
|
||||
|
||||
264
apps/api/src/services/ai_providers/nemotron.py
Normal file
264
apps/api/src/services/ai_providers/nemotron.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Nemotron Tool Calling Provider - Phase 24 ADR-052
|
||||
==================================================
|
||||
封裝 NVIDIA Nemotron Tool Calling 能力,供 AIRouter 路由。
|
||||
|
||||
搬移自: openclaw.py _call_nemotron_tools (L1623-1785)
|
||||
特性: K8s Tool Calling,83.3% 精準度,HITL 高風險保護
|
||||
|
||||
架構鐵律: AIRouter → NemotronProvider → NvidiaProvider → NVIDIA NIM
|
||||
|
||||
2026-04-02 Claude Code: Phase 24 B3 NemotronProvider 抽取
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.core.config import get_settings
|
||||
from src.services.ai_providers.interfaces import AIProvider, AIResult, is_provider_enabled_by_env
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
# 預定義 K8s Tool Definitions (搬移自 openclaw.py _call_nemotron_tools)
|
||||
_K8S_TOOLS: list[dict] = [
|
||||
{
|
||||
"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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class NemotronProvider:
|
||||
"""
|
||||
NVIDIA Nemotron Tool Calling Provider
|
||||
|
||||
privacy_level: cloud (NIM 是雲端 GPU,首席架構師 Q2 裁示)
|
||||
capabilities: tool_calling (K8s 操作決策專用)
|
||||
|
||||
呼叫路徑:
|
||||
AIRouter → NemotronProvider → NvidiaProvider (ADR-036) → NVIDIA NIM API
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# NvidiaProvider 採用懶加載,避免 import-time 副作用
|
||||
self._nvidia: Any | None = None
|
||||
|
||||
def _get_nvidia(self) -> Any:
|
||||
if self._nvidia is None:
|
||||
from src.services.nvidia_provider import get_nvidia_provider
|
||||
self._nvidia = get_nvidia_provider()
|
||||
return self._nvidia
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "nemotron"
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return is_provider_enabled_by_env("nemotron")
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"tool_calling"}
|
||||
|
||||
@property
|
||||
def privacy_level(self) -> str:
|
||||
# NIM 是雲端 GPU,首席架構師 Q2 裁示: cloud 等級
|
||||
return "cloud"
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
prompt: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> AIResult:
|
||||
"""
|
||||
執行 K8s Tool Calling 分析
|
||||
|
||||
context 結構:
|
||||
- incident_id: str (Incident ID)
|
||||
- reasoning: str (OpenClaw 推理結果)
|
||||
- target_resource: str (目標資源名稱)
|
||||
- suggested_action: str (OpenClaw 建議操作)
|
||||
- namespace: str (K8s namespace,預設 awoooi-prod)
|
||||
|
||||
回傳 AIResult.raw_response 為 JSON 字串:
|
||||
{"tools": [...], "validation": str, "latency_ms": float}
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
start = time.perf_counter()
|
||||
context = context or {}
|
||||
|
||||
# 從 context 取出欄位,fallback 到 prompt
|
||||
incident_id = context.get("incident_id", "UNKNOWN")
|
||||
reasoning = context.get("reasoning", prompt)
|
||||
target_resource = context.get("target_resource", "unknown")
|
||||
suggested_action = context.get("suggested_action", prompt)
|
||||
namespace = context.get("namespace", "awoooi-prod")
|
||||
|
||||
tool_prompt = f"""根據以下 AI 分析結果,生成對應的 kubectl 操作指令:
|
||||
|
||||
## Incident 上下文
|
||||
- Incident ID: {incident_id}
|
||||
- 目標資源: {target_resource}
|
||||
- Namespace: {namespace}
|
||||
|
||||
## OpenClaw 分析
|
||||
- 建議操作: {suggested_action}
|
||||
- 推理過程: {reasoning[:500]}
|
||||
|
||||
## 你的任務
|
||||
生成最適合的 kubectl 操作。如果操作有風險,請標註驗證步驟。
|
||||
"""
|
||||
|
||||
try:
|
||||
timeout = getattr(settings, "NEMOTRON_TIMEOUT_SECONDS", 30)
|
||||
nvidia = self._get_nvidia()
|
||||
|
||||
result = await asyncio.wait_for(
|
||||
nvidia.tool_call(
|
||||
messages=[{"role": "user", "content": tool_prompt}],
|
||||
tools=_K8S_TOOLS,
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
latency_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
# 解析 Tool Calling 結果 (搬移自 openclaw.py L1734-1756)
|
||||
tools: list[dict] = []
|
||||
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 "❌ 驗證失敗"
|
||||
|
||||
payload = {
|
||||
"tools": tools,
|
||||
"validation": validation_status,
|
||||
"latency_ms": latency_ms,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"nemotron_provider_success",
|
||||
incident_id=incident_id,
|
||||
tool_count=len(tools),
|
||||
validation=validation_status,
|
||||
latency_ms=round(latency_ms, 1),
|
||||
)
|
||||
|
||||
return AIResult(
|
||||
raw_response=_json.dumps(payload, ensure_ascii=False),
|
||||
success=True,
|
||||
provider=self.name,
|
||||
latency_ms=latency_ms,
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
latency_ms = (time.perf_counter() - start) * 1000
|
||||
timeout_secs = getattr(settings, "NEMOTRON_TIMEOUT_SECONDS", 30)
|
||||
logger.warning(
|
||||
"nemotron_provider_timeout",
|
||||
incident_id=incident_id,
|
||||
timeout_seconds=timeout_secs,
|
||||
latency_ms=round(latency_ms, 1),
|
||||
)
|
||||
return AIResult(
|
||||
raw_response="",
|
||||
success=False,
|
||||
provider=self.name,
|
||||
latency_ms=latency_ms,
|
||||
error=f"Tool calling timeout after {timeout_secs}s",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
latency_ms = (time.perf_counter() - start) * 1000
|
||||
logger.error(
|
||||
"nemotron_provider_error",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
latency_ms=round(latency_ms, 1),
|
||||
)
|
||||
return AIResult(
|
||||
raw_response="",
|
||||
success=False,
|
||||
provider=self.name,
|
||||
latency_ms=latency_ms,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""健康檢查:嘗試初始化 NvidiaProvider"""
|
||||
try:
|
||||
nvidia = self._get_nvidia()
|
||||
# NvidiaProvider 有 health_check 就用,沒有就只驗證能實例化
|
||||
if hasattr(nvidia, "health_check"):
|
||||
return await nvidia.health_check()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user