Files
awoooi/apps/api/src/services/drift_interpreter.py
OG T d952435b60
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 32m34s
fix(drift): 改用 OpenClaw AI Router 取代 Ollama httpx 直連
根因:_call_nemotron() 直接呼叫 Ollama httpx(settings.OLLAMA_URL)
      繞過 AI Router,無 fallback → "All connection attempts failed"
      → Telegram 卡顯示「意圖分析失敗:All connection attempts failed」

修復:改走 get_openclaw().call(prompt)
      自動享有 Provider 降級與 fallback 機制(與其他 Agent 一致)

廢棄:BUG-001 httpx 直連繞過法(nvidia_provider 介面已穩定)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:27:39 +08:00

171 lines
5.8 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.
"""
Drift Interpreter - Phase 25 P2 Config Drift Detection
=======================================================
職責Nemotron 意圖分析(不生成修復指令)
只回答「這是人為操作Hotfix系統自動變更
設計邊界(核心原則):
- 只輸出意圖分析,不生成 kubectl 或 git 指令
- 確定性修復由 DriftRemediator 負責
- Nemotron 超時 → UNKNOWN不阻塞主流程
版本: v1.0
建立: 2026-04-04 (台北時區)
建立者: ogt (首席架構師設計) + Claude Code (實作)
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import structlog
from src.models.drift import DriftIntent, DriftInterpretation, DriftItem
if TYPE_CHECKING:
from src.models.drift import DriftReport
logger = structlog.get_logger(__name__)
_INTENT_PROMPT_TEMPLATE = """你是 AWOOOI GitOps 守門員,請分析以下 K8s 配置漂移的意圖。
## 漂移詳情
{diff_summary}
## 任務
判斷這次漂移最可能的原因:
- emergency_hotfix: 繞過 CI 的緊急修補image tag 改變但無對應 Git commit
- human_error: 誤操作(非預期的隨機欄位改變)
- automated_change: 系統自動變更HPA replicas, 系統注入的 annotation 等)
- unknown: 無法判斷
請以 JSON 回應:
{{
"intent": "emergency_hotfix|human_error|automated_change|unknown",
"explanation": "用繁體中文解釋你的判斷理由(一句話)",
"risk": "HIGH|MEDIUM|LOW",
"confidence": 0.0到1.0之間的數字
}}
只輸出 JSON不要任何額外說明。
"""
class NemotronDriftInterpreter:
"""
使用 Nemotron 分析漂移意圖
職責邊界:
✅ 輸出意圖分析
❌ 不生成修復指令
❌ 不直接呼叫 kubectl 或 git
"""
async def analyze(self, report: "DriftReport") -> DriftInterpretation:
"""
分析漂移意圖
Args:
report: 已分類的 DriftReport
Returns:
DriftInterpretation超時或失敗時回傳 UNKNOWN
"""
if not report.items or (report.high_count == 0 and report.medium_count == 0):
return DriftInterpretation(
intent=DriftIntent.UNKNOWN,
explanation="無顯著漂移,不需要意圖分析",
confidence=1.0,
)
diff_text = self._format_diff_for_prompt(report)
prompt = _INTENT_PROMPT_TEMPLATE.format(diff_summary=diff_text)
result = await self._call_nemotron(prompt)
return result
def _format_diff_for_prompt(self, report: "DriftReport") -> str:
"""格式化 diff 給 Nemotron 分析用"""
lines = []
for item in report.items[:10]: # 最多 10 項避免 token 過多
if item.is_allowlisted:
continue
lines.append(
f"- {item.resource_kind}/{item.resource_name}: "
f"{item.field_path} "
f"Git={str(item.git_value)[:40]}"
f"K8s={str(item.actual_value)[:40]}"
)
return "\n".join(lines) if lines else "(均為白名單欄位)"
async def _call_nemotron(self, prompt: str) -> DriftInterpretation:
"""
呼叫 OpenClaw AI Router 進行意圖分析。
2026-04-17 ogt + Claude Sonnet 4.6: 改用 OpenClaw 取代直接 Ollama httpx 呼叫
根因:直接呼叫 Ollama 繞過 AI Router無 fallback → "All connection attempts failed"
修復:統一走 openclaw.call(),自動享有 Provider 降級與 fallback 機制
廢棄BUG-001 的 httpx 直連繞過法nvidia_provider 介面已於 7eb8375 穩定)
"""
try:
from src.services.openclaw import get_openclaw
openclaw = get_openclaw()
response_text, _provider, success = await openclaw.call(prompt)
if not success or not response_text:
logger.warning("drift_interpreter_openclaw_failed", provider=_provider)
return self._unknown_result("AI Router 回傳失敗或空值")
return self._parse_response(response_text)
except Exception as e:
logger.warning("drift_interpreter_error", error=str(e))
return self._unknown_result(str(e))
def _parse_response(self, text: str) -> DriftInterpretation:
"""解析 Nemotron JSON 回應"""
try:
# 嘗試直接解析
data = json.loads(text)
except Exception:
try:
import re
match = re.search(r"```(?:json)?\s*([\s\S]+?)```", text)
if match:
data = json.loads(match.group(1))
else:
return self._unknown_result("無法解析 JSON")
except Exception:
return self._unknown_result("JSON 解析失敗")
try:
intent_str = data.get("intent", "unknown")
intent = DriftIntent(intent_str) if intent_str in DriftIntent._value2member_map_ else DriftIntent.UNKNOWN
return DriftInterpretation(
intent=intent,
explanation=data.get("explanation", ""),
risk=data.get("risk", "MEDIUM"),
confidence=float(data.get("confidence", 0.0)),
)
except Exception as e:
return self._unknown_result(f"模型解析失敗: {e}")
def _unknown_result(self, reason: str) -> DriftInterpretation:
return DriftInterpretation(
intent=DriftIntent.UNKNOWN,
explanation=f"意圖分析失敗:{reason}",
risk="MEDIUM",
confidence=0.0,
)
_interpreter: NemotronDriftInterpreter | None = None
def get_drift_interpreter() -> NemotronDriftInterpreter:
global _interpreter
if _interpreter is None:
_interpreter = NemotronDriftInterpreter()
return _interpreter