801 lines
25 KiB
Python
801 lines
25 KiB
Python
"""
|
||
Intent Classifier - Phase 13.3 #85
|
||
===================================
|
||
K8s 操作意圖分類器,用於智能路由模型選擇
|
||
|
||
目標: < 100ms 延遲 (規則引擎 < 10ms)
|
||
策略: 方案 A (規則引擎) → 方案 B (LLM 備援)
|
||
|
||
版本: v2.0
|
||
建立: 2026-03-26 (台北時區)
|
||
建立者: Claude Code
|
||
最後修改: 2026-03-26 (台北時區)
|
||
修改者: Claude Code
|
||
|
||
變更紀錄:
|
||
| 版本 | 日期 | 執行者 | 變更內容 |
|
||
|------|------|--------|----------|
|
||
| v1.0 | 2026-03-26 | Claude Code | 初始實作 (舊版 IntentType) |
|
||
| v2.0 | 2026-03-26 | Claude Code | Phase 13.3 #85 升級 (四大核心+輔助意圖) |
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import time
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
from typing import Protocol, runtime_checkable
|
||
|
||
import httpx
|
||
import structlog
|
||
|
||
from src.services.model_registry import get_model_registry
|
||
from src.services.ollama_endpoint_resolver import resolve_ollama_order
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
# LLM 分類 Prompt 模板 (Phase 13.4)
|
||
_LLM_CLASSIFY_PROMPT = """你是 K8s 操作意圖分類專家。根據以下輸入,判斷用戶的操作意圖。
|
||
|
||
可選意圖類型:
|
||
- restart: 重啟 Pod/Deployment/StatefulSet
|
||
- scale: 擴縮容、HPA 調整
|
||
- config: ConfigMap/Secret/ENV 變更
|
||
- diagnose: 日誌查詢、健康檢查、RCA
|
||
- delete: 刪除資源(高風險)
|
||
- rollback: 回滾版本
|
||
- unknown: 無法判斷
|
||
|
||
輸入: {text}
|
||
|
||
請以 JSON 格式回答,只輸出 JSON:
|
||
{{"intent": "<類型>", "confidence": <0.0-1.0>, "reasoning": "<判斷依據>"}}"""
|
||
|
||
|
||
# =============================================================================
|
||
# 意圖類型定義 (Phase 13.3 #85)
|
||
# =============================================================================
|
||
|
||
|
||
class IntentType(Enum):
|
||
"""
|
||
K8s 操作意圖類型
|
||
|
||
四大核心意圖:
|
||
- RESTART: 重啟 Pod/Deployment/StatefulSet
|
||
- SCALE: 擴縮容、HPA 調整
|
||
- CONFIG: ConfigMap/Secret/ENV 變更
|
||
- DIAGNOSE: 日誌查詢、健康檢查、RCA
|
||
|
||
輔助意圖:
|
||
- DELETE: 刪除資源(高風險)
|
||
- ROLLBACK: 回滾版本
|
||
- UNKNOWN: 無法判斷
|
||
|
||
舊版兼容 (已棄用,映射到新意圖):
|
||
- ALERT_TRIAGE → DIAGNOSE
|
||
- DEPLOYMENT → CONFIG
|
||
- QUERY → DIAGNOSE
|
||
- MAINTENANCE → RESTART
|
||
- CODE_REVIEW → DIAGNOSE
|
||
"""
|
||
|
||
# 四大核心意圖
|
||
RESTART = "restart" # 重啟 Pod/Deployment/StatefulSet
|
||
SCALE = "scale" # 擴縮容、HPA 調整
|
||
CONFIG = "config" # ConfigMap/Secret/ENV 變更
|
||
DIAGNOSE = "diagnose" # 日誌查詢、健康檢查、RCA
|
||
|
||
# 輔助意圖
|
||
DELETE = "delete" # 刪除資源(高風險)
|
||
ROLLBACK = "rollback" # 回滾版本
|
||
UNKNOWN = "unknown" # 無法判斷
|
||
|
||
# 舊版兼容 (棄用,保留向後兼容)
|
||
ALERT_TRIAGE = "alert_triage" # → DIAGNOSE
|
||
DEPLOYMENT = "deployment" # → CONFIG
|
||
QUERY = "query" # → DIAGNOSE
|
||
MAINTENANCE = "maintenance" # → RESTART
|
||
CODE_REVIEW = "code_review" # → DIAGNOSE
|
||
|
||
|
||
# 舊版意圖到新版的映射
|
||
LEGACY_INTENT_MAP: dict[IntentType, IntentType] = {
|
||
IntentType.ALERT_TRIAGE: IntentType.DIAGNOSE,
|
||
IntentType.DEPLOYMENT: IntentType.CONFIG,
|
||
IntentType.QUERY: IntentType.DIAGNOSE,
|
||
IntentType.MAINTENANCE: IntentType.RESTART,
|
||
IntentType.CODE_REVIEW: IntentType.DIAGNOSE,
|
||
}
|
||
|
||
|
||
def normalize_intent(intent: IntentType) -> IntentType:
|
||
"""
|
||
正規化意圖 (將舊版意圖映射到新版)
|
||
|
||
Args:
|
||
intent: 原始意圖
|
||
|
||
Returns:
|
||
正規化後的意圖
|
||
"""
|
||
return LEGACY_INTENT_MAP.get(intent, intent)
|
||
|
||
|
||
# =============================================================================
|
||
# 風險等級定義
|
||
# =============================================================================
|
||
|
||
|
||
class RiskLevel(Enum):
|
||
"""意圖風險等級"""
|
||
|
||
LOW = "low" # 只讀操作 (DIAGNOSE)
|
||
MEDIUM = "medium" # 可逆操作 (RESTART, SCALE, ROLLBACK)
|
||
HIGH = "high" # 配置變更 (CONFIG)
|
||
CRITICAL = "critical" # 不可逆操作 (DELETE)
|
||
|
||
|
||
# 意圖對應風險等級
|
||
INTENT_RISK_MAP: dict[IntentType, RiskLevel] = {
|
||
IntentType.DIAGNOSE: RiskLevel.LOW,
|
||
IntentType.RESTART: RiskLevel.MEDIUM,
|
||
IntentType.SCALE: RiskLevel.MEDIUM,
|
||
IntentType.ROLLBACK: RiskLevel.MEDIUM,
|
||
IntentType.CONFIG: RiskLevel.HIGH,
|
||
IntentType.DELETE: RiskLevel.CRITICAL,
|
||
IntentType.UNKNOWN: RiskLevel.MEDIUM,
|
||
# 舊版兼容
|
||
IntentType.ALERT_TRIAGE: RiskLevel.LOW,
|
||
IntentType.DEPLOYMENT: RiskLevel.HIGH,
|
||
IntentType.QUERY: RiskLevel.LOW,
|
||
IntentType.MAINTENANCE: RiskLevel.MEDIUM,
|
||
IntentType.CODE_REVIEW: RiskLevel.LOW,
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 關鍵字規則引擎 (方案 A, < 10ms)
|
||
# =============================================================================
|
||
|
||
|
||
# 核心意圖關鍵字映射
|
||
INTENT_KEYWORDS: dict[IntentType, list[str]] = {
|
||
# 四大核心意圖
|
||
IntentType.RESTART: [
|
||
# 英文
|
||
"restart",
|
||
"reboot",
|
||
"recreate",
|
||
"kill",
|
||
"delete pod",
|
||
"rollout restart",
|
||
# 中文
|
||
"重啟",
|
||
"重新啟動",
|
||
"重建",
|
||
"刪除 pod",
|
||
"殺掉",
|
||
],
|
||
IntentType.SCALE: [
|
||
# 英文
|
||
"scale",
|
||
"replica",
|
||
"hpa",
|
||
"autoscale",
|
||
"scale up",
|
||
"scale down",
|
||
"horizontal pod autoscaler",
|
||
# 中文
|
||
"擴容",
|
||
"縮容",
|
||
"擴縮",
|
||
"副本",
|
||
"水平擴展",
|
||
],
|
||
IntentType.CONFIG: [
|
||
# 英文
|
||
"configmap",
|
||
"secret",
|
||
"env",
|
||
"environment",
|
||
"config",
|
||
"setting",
|
||
"configuration",
|
||
"kubectl apply",
|
||
"helm upgrade",
|
||
# 中文
|
||
"配置",
|
||
"設定",
|
||
"環境變數",
|
||
"部署",
|
||
"更新配置",
|
||
],
|
||
IntentType.DIAGNOSE: [
|
||
# 英文
|
||
"log",
|
||
"logs",
|
||
"describe",
|
||
"get",
|
||
"status",
|
||
"health",
|
||
"check",
|
||
"debug",
|
||
"trace",
|
||
"diagnose",
|
||
"rca",
|
||
"root cause",
|
||
"investigate",
|
||
"why",
|
||
"what happened",
|
||
# 中文
|
||
"日誌",
|
||
"查看",
|
||
"檢查",
|
||
"狀態",
|
||
"健康",
|
||
"診斷",
|
||
"原因",
|
||
"為什麼",
|
||
"什麼問題",
|
||
"分析",
|
||
],
|
||
# 輔助意圖
|
||
IntentType.DELETE: [
|
||
# 英文
|
||
"delete",
|
||
"remove",
|
||
"destroy",
|
||
"kubectl delete",
|
||
"helm uninstall",
|
||
"drop",
|
||
# 中文
|
||
"刪除",
|
||
"移除",
|
||
"銷毀",
|
||
"清除",
|
||
],
|
||
IntentType.ROLLBACK: [
|
||
# 英文
|
||
"rollback",
|
||
"rollout undo",
|
||
"revert",
|
||
"previous version",
|
||
"last version",
|
||
# 中文
|
||
"回滾",
|
||
"回復",
|
||
"還原",
|
||
"上一版",
|
||
"前一版",
|
||
],
|
||
}
|
||
|
||
# 告警關鍵字 (強化 DIAGNOSE 分類)
|
||
ALERT_KEYWORDS: list[str] = [
|
||
"alert",
|
||
"alerting",
|
||
"firing",
|
||
"告警",
|
||
"警報",
|
||
"異常",
|
||
"error",
|
||
"critical",
|
||
"warning",
|
||
"high cpu",
|
||
"high memory",
|
||
"oom",
|
||
"crash",
|
||
"down",
|
||
"timeout",
|
||
"failed",
|
||
"unhealthy",
|
||
]
|
||
|
||
# 資源類型關鍵字 (用於上下文判斷)
|
||
RESOURCE_KEYWORDS: dict[str, list[str]] = {
|
||
"pod": ["pod", "pods", "po"],
|
||
"deployment": ["deployment", "deployments", "deploy"],
|
||
"statefulset": ["statefulset", "statefulsets", "sts"],
|
||
"daemonset": ["daemonset", "daemonsets", "ds"],
|
||
"service": ["service", "services", "svc"],
|
||
"configmap": ["configmap", "configmaps", "cm"],
|
||
"secret": ["secret", "secrets"],
|
||
"ingress": ["ingress", "ingresses", "ing"],
|
||
"namespace": ["namespace", "namespaces", "ns"],
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 分類結果
|
||
# =============================================================================
|
||
|
||
|
||
@dataclass
|
||
class IntentResult:
|
||
"""意圖分類結果"""
|
||
|
||
intent: IntentType # 分類意圖
|
||
confidence: float # 信心度 (0.0-1.0)
|
||
method: str # 分類方法 (keyword/llm)
|
||
risk_level: RiskLevel = field(default=RiskLevel.MEDIUM)
|
||
matched_keywords: list[str] = field(default_factory=list)
|
||
detected_resources: list[str] = field(default_factory=list)
|
||
reasoning: str = ""
|
||
|
||
def __post_init__(self):
|
||
"""初始化後設定風險等級"""
|
||
self.risk_level = INTENT_RISK_MAP.get(self.intent, RiskLevel.MEDIUM)
|
||
|
||
|
||
# =============================================================================
|
||
# Protocol 介面 (支援 DI)
|
||
# =============================================================================
|
||
|
||
|
||
@runtime_checkable
|
||
class IIntentClassifier(Protocol):
|
||
"""Intent Classifier Interface for DI"""
|
||
|
||
async def classify(self, text: str) -> IntentResult:
|
||
"""分類意圖 (非同步)"""
|
||
...
|
||
|
||
def classify_sync(self, text: str) -> IntentResult:
|
||
"""分類意圖 (同步)"""
|
||
...
|
||
|
||
|
||
# =============================================================================
|
||
# 實作
|
||
# =============================================================================
|
||
|
||
|
||
class IntentClassifier:
|
||
"""
|
||
K8s 操作意圖分類器
|
||
|
||
使用兩階段分類策略:
|
||
1. 方案 A: 規則引擎 (關鍵字匹配, < 10ms)
|
||
2. 方案 B: 輕量 LLM (qwen2.5:1b, < 100ms) - 備援
|
||
|
||
Usage:
|
||
classifier = get_intent_classifier()
|
||
result = await classifier.classify("重啟 api-server pod")
|
||
# IntentResult(intent=RESTART, confidence=0.95, method='keyword')
|
||
"""
|
||
|
||
# LLM 備援模型 (從 ModelRegistry 取得)
|
||
_llm_model: str | None = None
|
||
|
||
def __init__(self):
|
||
self._keyword_cache: dict[str, IntentResult] = {}
|
||
self._cache_max_size = 1000 # 最大快取條目
|
||
|
||
@property
|
||
def llm_model(self) -> str:
|
||
"""取得 LLM 備援模型 (延遲載入)"""
|
||
if self._llm_model is None:
|
||
try:
|
||
registry = get_model_registry()
|
||
self._llm_model = registry.get_model("ollama", "intent")
|
||
except Exception:
|
||
self._llm_model = "qwen2.5:1b" # fallback
|
||
return self._llm_model
|
||
|
||
async def classify(self, text: str) -> IntentResult:
|
||
"""
|
||
分類意圖 (非同步)
|
||
|
||
Args:
|
||
text: 用戶輸入或告警內容
|
||
|
||
Returns:
|
||
IntentResult: 分類結果
|
||
"""
|
||
text_lower = text.lower().strip()
|
||
|
||
# 階段 1: 規則引擎快速匹配 (< 10ms)
|
||
result = self._keyword_classify(text_lower)
|
||
if result.confidence >= 0.7: # 信心度閾值
|
||
logger.debug(
|
||
"intent_classified_by_keyword",
|
||
intent=result.intent.value,
|
||
confidence=result.confidence,
|
||
matched_keywords=result.matched_keywords,
|
||
text_preview=text[:50],
|
||
)
|
||
return result
|
||
|
||
# 階段 2: LLM 分類 (< 100ms)
|
||
llm_result = await self._llm_classify(text_lower)
|
||
if llm_result.confidence > result.confidence:
|
||
logger.debug(
|
||
"intent_classified_by_llm",
|
||
intent=llm_result.intent.value,
|
||
confidence=llm_result.confidence,
|
||
text_preview=text[:50],
|
||
)
|
||
return llm_result
|
||
|
||
# 使用規則引擎結果
|
||
logger.debug(
|
||
"intent_classified_fallback",
|
||
intent=result.intent.value,
|
||
confidence=result.confidence,
|
||
text_preview=text[:50],
|
||
)
|
||
return result
|
||
|
||
def classify_sync(self, text: str) -> IntentResult:
|
||
"""
|
||
同步版本 (僅關鍵字匹配)
|
||
|
||
Args:
|
||
text: 用戶輸入或告警內容
|
||
|
||
Returns:
|
||
IntentResult: 分類結果
|
||
"""
|
||
return self._keyword_classify(text.lower().strip())
|
||
|
||
def _keyword_classify(self, text: str) -> IntentResult:
|
||
"""
|
||
規則引擎分類 (方案 A)
|
||
|
||
目標延遲: < 10ms
|
||
|
||
Args:
|
||
text: 已轉小寫的輸入文字
|
||
|
||
Returns:
|
||
IntentResult: 分類結果
|
||
"""
|
||
# 檢查快取
|
||
cache_key = text[:100]
|
||
if cache_key in self._keyword_cache:
|
||
return self._keyword_cache[cache_key]
|
||
|
||
# 計算每個意圖的匹配分數
|
||
scores: dict[IntentType, tuple[int, list[str]]] = {}
|
||
|
||
for intent, keywords in INTENT_KEYWORDS.items():
|
||
score = 0
|
||
matched: list[str] = []
|
||
for keyword in keywords:
|
||
if keyword in text:
|
||
score += 1
|
||
matched.append(keyword)
|
||
# 完整詞匹配加分
|
||
if re.search(rf"\b{re.escape(keyword)}\b", text):
|
||
score += 1
|
||
if score > 0:
|
||
scores[intent] = (score, matched)
|
||
|
||
# 檢測告警內容 (強化 DIAGNOSE)
|
||
is_alert = any(kw in text for kw in ALERT_KEYWORDS)
|
||
if is_alert and IntentType.DIAGNOSE not in scores:
|
||
scores[IntentType.DIAGNOSE] = (1, ["(alert_detected)"])
|
||
|
||
# 檢測資源類型
|
||
detected_resources: list[str] = []
|
||
for resource_type, keywords in RESOURCE_KEYWORDS.items():
|
||
if any(kw in text for kw in keywords):
|
||
detected_resources.append(resource_type)
|
||
|
||
# 選擇最高分意圖
|
||
if not scores:
|
||
result = IntentResult(
|
||
intent=IntentType.UNKNOWN,
|
||
confidence=0.0,
|
||
method="keyword",
|
||
matched_keywords=[],
|
||
detected_resources=detected_resources,
|
||
reasoning="無匹配關鍵字",
|
||
)
|
||
else:
|
||
best_intent = max(scores, key=lambda k: scores[k][0])
|
||
best_score, matched_keywords = scores[best_intent]
|
||
|
||
# 🔴 2026-03-29 修正: 關鍵字匹配不是 AI 分析,信心度設 0
|
||
# 根據 feedback_confidence_truthfulness.md 鐵律
|
||
confidence = 0.0
|
||
|
||
result = IntentResult(
|
||
intent=best_intent,
|
||
confidence=confidence,
|
||
method="keyword",
|
||
matched_keywords=matched_keywords,
|
||
detected_resources=detected_resources,
|
||
reasoning=f"匹配關鍵字: {', '.join(matched_keywords)}",
|
||
)
|
||
|
||
# 快取結果 (LRU 簡易實作)
|
||
if len(self._keyword_cache) >= self._cache_max_size:
|
||
# 移除最舊的一半
|
||
keys = list(self._keyword_cache.keys())
|
||
for k in keys[: len(keys) // 2]:
|
||
del self._keyword_cache[k]
|
||
|
||
self._keyword_cache[cache_key] = result
|
||
return result
|
||
|
||
async def _llm_classify(self, text: str) -> IntentResult:
|
||
"""
|
||
LLM 分類 (方案 B) - Phase 13.4
|
||
|
||
目標延遲: < 100ms (使用 qwen2.5:1b 或配置的 intent 模型)
|
||
|
||
Args:
|
||
text: 已轉小寫的輸入文字
|
||
|
||
Returns:
|
||
IntentResult: 分類結果
|
||
|
||
2026-03-30 Claude Code: 實作 Ollama 整合
|
||
"""
|
||
start_time = time.time()
|
||
|
||
try:
|
||
# 建構 Prompt
|
||
prompt = _LLM_CLASSIFY_PROMPT.format(text=text)
|
||
|
||
# 取得模型配置
|
||
model_name = self.llm_model # qwen2.5:1b 或配置值
|
||
|
||
# 呼叫 Ollama(GCP-A → GCP-B → 111)
|
||
result_text = ""
|
||
last_error: Exception | None = None
|
||
async with httpx.AsyncClient() as client:
|
||
for endpoint in resolve_ollama_order("hermes"):
|
||
if not endpoint.url:
|
||
continue
|
||
try:
|
||
response = await client.post(
|
||
f"{endpoint.url}/api/generate",
|
||
json={
|
||
"model": model_name,
|
||
"prompt": prompt,
|
||
"stream": False,
|
||
"format": "json",
|
||
"options": {
|
||
"num_predict": 128, # 意圖分類只需短回應
|
||
"temperature": 0.0, # 確定性輸出
|
||
"top_p": 0.9,
|
||
},
|
||
},
|
||
timeout=httpx.Timeout(5.0, connect=2.0), # 嚴格超時
|
||
)
|
||
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
result_text = data.get("response", "")
|
||
break
|
||
except Exception as e:
|
||
last_error = e
|
||
logger.warning(
|
||
"intent_llm_endpoint_failed",
|
||
provider=endpoint.provider_name,
|
||
error=str(e),
|
||
error_type=type(e).__name__,
|
||
)
|
||
|
||
if not result_text:
|
||
_kw_result = self._keyword_classify(text)
|
||
return IntentResult(
|
||
intent=_kw_result.intent,
|
||
confidence=_kw_result.confidence,
|
||
method="llm_unavailable_keyword",
|
||
matched_keywords=_kw_result.matched_keywords,
|
||
detected_resources=_kw_result.detected_resources,
|
||
reasoning=(
|
||
f"LLM 全端點不可用({type(last_error).__name__ if last_error else 'no_endpoint'})"
|
||
f" → {_kw_result.reasoning}"
|
||
),
|
||
)
|
||
|
||
# 解析 JSON 回應
|
||
elapsed_ms = (time.time() - start_time) * 1000
|
||
|
||
try:
|
||
parsed = json.loads(result_text)
|
||
intent_str = parsed.get("intent", "unknown").lower()
|
||
confidence = float(parsed.get("confidence", 0.5))
|
||
reasoning = parsed.get("reasoning", "")
|
||
|
||
# 映射到 IntentType
|
||
intent = self._parse_intent_type(intent_str)
|
||
|
||
logger.info(
|
||
"intent_llm_classified",
|
||
intent=intent.value,
|
||
confidence=confidence,
|
||
elapsed_ms=round(elapsed_ms, 1),
|
||
model=model_name,
|
||
)
|
||
|
||
return IntentResult(
|
||
intent=intent,
|
||
confidence=confidence,
|
||
method="llm",
|
||
matched_keywords=[],
|
||
detected_resources=[],
|
||
reasoning=reasoning,
|
||
)
|
||
|
||
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
||
logger.warning(
|
||
"intent_llm_parse_failed",
|
||
error=str(e),
|
||
response_preview=result_text[:100],
|
||
)
|
||
# 2026-04-24 ogt + Claude Sonnet 4.6: JSON 解析失敗也降級至關鍵字結果
|
||
_kw_result = self._keyword_classify(text)
|
||
return IntentResult(
|
||
intent=_kw_result.intent,
|
||
confidence=_kw_result.confidence,
|
||
method="llm_parse_failed_keyword",
|
||
matched_keywords=_kw_result.matched_keywords,
|
||
detected_resources=_kw_result.detected_resources,
|
||
reasoning=f"LLM JSON 解析失敗降級 → {_kw_result.reasoning}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
elapsed_ms = (time.time() - start_time) * 1000
|
||
logger.warning(
|
||
"intent_llm_timeout",
|
||
elapsed_ms=round(elapsed_ms, 1),
|
||
)
|
||
# 2026-04-24 ogt + Claude Sonnet 4.6: LLM 超時直接降級至關鍵字結果
|
||
# 問題:_llm_fallback_result 返回 confidence=0.0/UNKNOWN,和 keyword 結果 confidence 相同
|
||
# classify() 比較 0.0 > 0.0 = False → 走 keyword(正確),但已浪費 5s 超時時間
|
||
# 若 Ollama 後端不通,每次都等 5s 才降級 → ai_router/ci_auto_repair 延遲累積
|
||
# 修法:超時直接回 keyword 結果,method 標記 "llm_timeout_keyword" 供可觀測性追蹤
|
||
_kw_result = self._keyword_classify(text)
|
||
return IntentResult(
|
||
intent=_kw_result.intent,
|
||
confidence=_kw_result.confidence,
|
||
method="llm_timeout_keyword",
|
||
matched_keywords=_kw_result.matched_keywords,
|
||
detected_resources=_kw_result.detected_resources,
|
||
reasoning=f"LLM 超時降級({round(elapsed_ms, 0):.0f}ms)→ {_kw_result.reasoning}",
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.warning(
|
||
"intent_llm_error",
|
||
error=str(e),
|
||
error_type=type(e).__name__,
|
||
)
|
||
# 2026-04-24 ogt + Claude Sonnet 4.6: LLM 錯誤同樣降級至關鍵字結果
|
||
_kw_result = self._keyword_classify(text)
|
||
return IntentResult(
|
||
intent=_kw_result.intent,
|
||
confidence=_kw_result.confidence,
|
||
method="llm_error_keyword",
|
||
matched_keywords=_kw_result.matched_keywords,
|
||
detected_resources=_kw_result.detected_resources,
|
||
reasoning=f"LLM 錯誤降級({type(e).__name__})→ {_kw_result.reasoning}",
|
||
)
|
||
|
||
def _parse_intent_type(self, intent_str: str) -> IntentType:
|
||
"""解析意圖字串為 IntentType"""
|
||
intent_map = {
|
||
"restart": IntentType.RESTART,
|
||
"scale": IntentType.SCALE,
|
||
"config": IntentType.CONFIG,
|
||
"diagnose": IntentType.DIAGNOSE,
|
||
"delete": IntentType.DELETE,
|
||
"rollback": IntentType.ROLLBACK,
|
||
"unknown": IntentType.UNKNOWN,
|
||
}
|
||
return intent_map.get(intent_str.lower(), IntentType.UNKNOWN)
|
||
|
||
def _llm_fallback_result(self, reason: str) -> IntentResult:
|
||
"""LLM 失敗時的 fallback 結果"""
|
||
return IntentResult(
|
||
intent=IntentType.UNKNOWN,
|
||
confidence=0.0,
|
||
method="llm",
|
||
matched_keywords=[],
|
||
detected_resources=[],
|
||
reasoning=reason,
|
||
)
|
||
|
||
def get_supported_intents(self) -> list[dict]:
|
||
"""
|
||
取得支援的意圖清單
|
||
|
||
Returns:
|
||
意圖清單 (含描述和風險等級)
|
||
"""
|
||
intents = [
|
||
{
|
||
"intent": IntentType.RESTART.value,
|
||
"description": "重啟 Pod/Deployment/StatefulSet",
|
||
"risk_level": RiskLevel.MEDIUM.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.RESTART][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.SCALE.value,
|
||
"description": "擴縮容、HPA 調整",
|
||
"risk_level": RiskLevel.MEDIUM.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.SCALE][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.CONFIG.value,
|
||
"description": "ConfigMap/Secret/ENV 變更",
|
||
"risk_level": RiskLevel.HIGH.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.CONFIG][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.DIAGNOSE.value,
|
||
"description": "日誌查詢、健康檢查、RCA",
|
||
"risk_level": RiskLevel.LOW.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.DIAGNOSE][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.DELETE.value,
|
||
"description": "刪除資源(高風險)",
|
||
"risk_level": RiskLevel.CRITICAL.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.DELETE][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.ROLLBACK.value,
|
||
"description": "回滾版本",
|
||
"risk_level": RiskLevel.MEDIUM.value,
|
||
"keywords_sample": INTENT_KEYWORDS[IntentType.ROLLBACK][:5],
|
||
},
|
||
{
|
||
"intent": IntentType.UNKNOWN.value,
|
||
"description": "無法判斷意圖",
|
||
"risk_level": RiskLevel.MEDIUM.value,
|
||
"keywords_sample": [],
|
||
},
|
||
]
|
||
return intents
|
||
|
||
|
||
# =============================================================================
|
||
# Singleton
|
||
# =============================================================================
|
||
|
||
_classifier: IntentClassifier | None = None
|
||
|
||
|
||
def get_intent_classifier() -> IntentClassifier:
|
||
"""取得 IntentClassifier 單例"""
|
||
global _classifier
|
||
if _classifier is None:
|
||
_classifier = IntentClassifier()
|
||
return _classifier
|
||
|
||
|
||
def reset_intent_classifier() -> None:
|
||
"""重置單例 (用於測試)"""
|
||
global _classifier
|
||
_classifier = None
|
||
|
||
|
||
# =============================================================================
|
||
# Convenience Functions
|
||
# =============================================================================
|
||
|
||
|
||
async def classify_intent(text: str) -> IntentResult:
|
||
"""便捷函數: 分類意圖 (非同步)"""
|
||
return await get_intent_classifier().classify(text)
|
||
|
||
|
||
def classify_intent_sync(text: str) -> IntentResult:
|
||
"""便捷函數: 分類意圖 (同步)"""
|
||
return get_intent_classifier().classify_sync(text)
|
||
|
||
|
||
def get_intent_risk(intent: IntentType) -> RiskLevel:
|
||
"""便捷函數: 取得意圖風險等級"""
|
||
return INTENT_RISK_MAP.get(intent, RiskLevel.MEDIUM)
|