Files
awoooi/apps/api/src/services/drift_interpreter.py
Your Name 2aaaa5654f
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m16s
CD Pipeline / build-and-deploy (push) Successful in 3m52s
CD Pipeline / post-deploy-checks (push) Successful in 1m30s
fix(drift): parse ollama json wrapped responses
2026-05-06 19:39:01 +08:00

224 lines
7.4 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
import re
from typing import TYPE_CHECKING
import structlog
from src.models.drift import DriftIntent, DriftInterpretation
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不要任何額外說明。
"""
def _strip_think_blocks(text: str) -> str:
"""移除 qwen/deepseek 類模型常見的 <think> 推理段。"""
return re.sub(r"<think>[\s\S]*?</think>", "", text, flags=re.IGNORECASE).strip()
def _extract_first_json_object(text: str) -> dict | None:
"""
從 LLM 回應中擷取第一個 JSON object。
Ollama qwen3/deepseek 常會在 JSON 前後加 `<think>` 或短句;這些文字不應
讓 drift intent 直接降級成 UNKNOWN。
"""
cleaned = _strip_think_blocks(text)
candidates = [cleaned]
candidates.extend(match.group(1).strip() for match in re.finditer(r"```(?:json)?\s*([\s\S]+?)```", cleaned))
start = cleaned.find("{")
if start >= 0:
in_string = False
escaped = False
depth = 0
for idx, ch in enumerate(cleaned[start:], start=start):
if escaped:
escaped = False
continue
if ch == "\\":
escaped = True
continue
if ch == '"':
in_string = not in_string
continue
if in_string:
continue
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
candidates.append(cleaned[start : idx + 1])
break
for candidate in candidates:
try:
data = json.loads(candidate)
except Exception:
continue
if isinstance(data, dict):
return data
return None
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,
alert_context={
"intent_hint": "config",
"task_type": "diagnose",
"enforce_ollama_first": True,
"allow_gcp_heavy_model": True,
"target_resource": "config-drift",
"alert_type": "ConfigDriftInternalScan",
},
)
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 回應"""
data = _extract_first_json_object(text)
if data is None:
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