Files
awoooi/apps/api/src/services/resource_resolver.py
OG T 938df7f291 fix(api): 全面清除假信心分數 - 遵循 feedback_confidence_truthfulness.md
🔴 違規修正: 規則匹配/Expert System 不是 AI 分析,confidence 必須 = 0.0

修正檔案:
- agents/action_planner.py: 0.9 → 0.0
- agents/blast_radius.py: 0.85/0.5/0.9 → 0.0
- agents/security.py: 計算公式 → 0.0
- signoz_webhook.py: 0.7 → 0.0
- auto_approve.py: default 0.5 → 0.0
- ci_auto_repair.py: 整個計算函數 → return 0.0
- error_analyzer_service.py: default 0.5 → 0.0
- intent_classifier.py: 計算公式 → 0.0
- openclaw.py: default 0.5 → 0.0
- resource_resolver.py: 0.8 → 0.0
- k8s_naming.py: 0.9/0.7 → 0.0

只有 LLM 真實分析返回的 confidence 才能 > 0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 16:00:46 +08:00

477 lines
14 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.
"""
Resource Resolver - ADR-016 K8s 資源動態驗證
=============================================
在 AI 產生 kubectl 指令後,動態驗證資源是否存在於 K8s 叢集中。
若不存在,嘗試模糊匹配或回報需人工確認。
流程:
1. 正規化資源名稱 (k8s_naming.py)
2. 調用 MCP Tool 驗證資源存在性
3. 模糊匹配 namespace 內的 Deployments
4. 回傳匹配結果或候選列表
版本: v1.1
建立: 2026-03-26 (台北時區)
建立者: Claude Code (首席架構師)
修改: 2026-03-26 (台北) - 新增 IResourceResolver Protocol (P2 架構改進)
@see docs/adr/ADR-016-k8s-resource-naming.md
"""
from dataclasses import dataclass, field
from difflib import SequenceMatcher
from typing import Protocol, runtime_checkable
import structlog
from src.utils.k8s_naming import (
ResourceType,
extract_resource_hints,
normalize_resource_name,
)
logger = structlog.get_logger(__name__)
# =============================================================================
# Types
# =============================================================================
@dataclass
class ResolveResult:
"""資源解析結果"""
success: bool
resource_name: str | None
namespace: str | None
resource_type: ResourceType
confidence: float # 0.0 - 1.0
is_k8s_resource: bool = True
requires_confirmation: bool = False
candidates: list[str] = field(default_factory=list)
note: str | None = None
original_input: str = ""
@dataclass
class K8sResource:
"""K8s 資源資訊"""
name: str
namespace: str
kind: str # Deployment, StatefulSet, Pod, etc.
replicas: int | None = None
ready: bool = True
# =============================================================================
# Protocol Interface (Phase 17 P2 - 架構審查)
# =============================================================================
@runtime_checkable
class IResourceResolver(Protocol):
"""
ResourceResolver 介面定義
用途:
- 依賴注入 (DI) 時的型別約束
- 測試時 Mock 的型別檢查
- 符合 leWOOOgo 積木化規範
@see feedback_resource_resolver_di.md
"""
async def resolve(
self,
raw_resource: str,
namespace: str = "awoooi-prod",
resource_kind: str = "deployment",
) -> ResolveResult:
"""解析原始資源名稱為有效的 K8s 資源"""
...
# =============================================================================
# Resource Resolver Implementation
# =============================================================================
class ResourceResolver:
"""
K8s 資源名稱解析器 - 確保 kubectl 指令有效
整合:
- 靜態正規化 (k8s_naming.py)
- 動態驗證 (MCP K8s Tool)
- 模糊匹配 (Levenshtein distance)
"""
def __init__(self):
self._cached_resources: dict[str, list[K8sResource]] = {}
self._cache_ttl: int = 60 # 快取 60 秒
async def resolve(
self,
raw_resource: str,
namespace: str = "awoooi-prod",
resource_kind: str = "deployment",
) -> ResolveResult:
"""
解析原始資源名稱為有效的 K8s 資源
Args:
raw_resource: 原始資源名稱 (可能是 URL、域名、或 K8s 名稱)
namespace: 目標命名空間
resource_kind: 資源類型 (deployment, statefulset, pod)
Returns:
ResolveResult: 解析結果
"""
logger.info(
"resource_resolve_start",
raw=raw_resource,
namespace=namespace,
kind=resource_kind,
)
# Step 1: 靜態正規化
normalized = normalize_resource_name(raw_resource, namespace)
# 非 K8s 資源直接返回
if not normalized.is_k8s_resource:
return ResolveResult(
success=True,
resource_name=normalized.normalized,
namespace=None,
resource_type=ResourceType.UNKNOWN,
confidence=normalized.confidence,
is_k8s_resource=False,
note=normalized.note,
original_input=raw_resource,
)
# 正規化失敗
if not normalized.success or not normalized.normalized:
return ResolveResult(
success=False,
resource_name=None,
namespace=namespace,
resource_type=ResourceType.UNKNOWN,
confidence=0.0,
requires_confirmation=True,
note=normalized.note,
original_input=raw_resource,
)
# Step 2: 動態驗證 (調用 K8s API)
resource_exists = await self._check_resource_exists(
normalized.normalized,
normalized.namespace or namespace,
resource_kind,
)
if resource_exists:
logger.info(
"resource_verified",
resource=normalized.normalized,
namespace=normalized.namespace or namespace,
)
return ResolveResult(
success=True,
resource_name=normalized.normalized,
namespace=normalized.namespace or namespace,
resource_type=normalized.resource_type,
confidence=1.0,
note="Verified via K8s API",
original_input=raw_resource,
)
# Step 3: 模糊匹配
candidates = await self._fuzzy_match(
raw_resource,
normalized.namespace or namespace,
resource_kind,
)
if len(candidates) == 1:
best_match = candidates[0]
logger.info(
"resource_fuzzy_matched",
original=raw_resource,
matched=best_match,
)
return ResolveResult(
success=True,
resource_name=best_match,
namespace=normalized.namespace or namespace,
resource_type=normalized.resource_type,
confidence=0.0, # 🔴 模糊匹配,非 AI
note=f"Fuzzy matched from '{raw_resource}'",
original_input=raw_resource,
)
if len(candidates) > 1:
logger.warning(
"resource_multiple_matches",
original=raw_resource,
candidates=candidates,
)
return ResolveResult(
success=False,
resource_name=None,
namespace=normalized.namespace or namespace,
resource_type=normalized.resource_type,
confidence=0.0,
requires_confirmation=True,
candidates=candidates,
note=f"Multiple matches for '{raw_resource}': {candidates}",
original_input=raw_resource,
)
# Step 4: 無匹配
logger.warning(
"resource_not_found",
original=raw_resource,
normalized=normalized.normalized,
namespace=normalized.namespace or namespace,
)
return ResolveResult(
success=False,
resource_name=normalized.normalized,
namespace=normalized.namespace or namespace,
resource_type=normalized.resource_type,
confidence=0.0,
requires_confirmation=True,
note=f"Resource '{normalized.normalized}' not found in namespace '{normalized.namespace or namespace}'",
original_input=raw_resource,
)
async def _check_resource_exists(
self,
name: str,
namespace: str,
kind: str = "deployment",
) -> bool:
"""
透過 MCP K8s Tool 檢查資源是否存在
Args:
name: 資源名稱
namespace: 命名空間
kind: 資源類型
Returns:
bool: 是否存在
"""
try:
# 嘗試導入 MCP Registry
from src.plugins.mcp.registry import get_mcp_registry
registry = get_mcp_registry()
result = await registry.call_tool(
tool_name="kubectl_get",
arguments={
"resource": f"{kind}s", # deployments, statefulsets, pods
"name": name,
"namespace": namespace,
},
)
if result.success and result.data:
# 檢查是否真的找到資源
data = result.data
if isinstance(data, dict):
# 單一資源
return data.get("metadata", {}).get("name") == name
elif isinstance(data, list):
# 資源列表
return any(
r.get("metadata", {}).get("name") == name
for r in data
)
return False
except ImportError:
logger.warning(
"mcp_registry_not_available",
note="Falling back to static validation only",
)
return False
except Exception as e:
logger.warning(
"k8s_check_failed",
resource=name,
namespace=namespace,
error=str(e),
)
return False
async def _fuzzy_match(
self,
raw_resource: str,
namespace: str,
kind: str = "deployment",
) -> list[str]:
"""
在 namespace 內模糊匹配資源
Args:
raw_resource: 原始輸入
namespace: 命名空間
kind: 資源類型
Returns:
list[str]: 匹配的資源名稱列表 (按相似度排序)
"""
try:
# 取得 namespace 內所有資源
resources = await self._list_resources(namespace, kind)
if not resources:
return []
# 提取關鍵字
hints = extract_resource_hints(raw_resource)
# 計算相似度
scored: list[tuple[str, float]] = []
for res in resources:
score = self._calculate_similarity(res.name, hints, raw_resource)
if score > 0.3: # 閾值
scored.append((res.name, score))
# 排序並返回
scored.sort(key=lambda x: x[1], reverse=True)
return [name for name, _ in scored[:5]] # 最多 5 個候選
except Exception as e:
logger.warning(
"fuzzy_match_failed",
error=str(e),
)
return []
async def _list_resources(
self,
namespace: str,
kind: str = "deployment",
) -> list[K8sResource]:
"""
列出 namespace 內所有指定類型的資源
"""
try:
from src.plugins.mcp.registry import get_mcp_registry
registry = get_mcp_registry()
result = await registry.call_tool(
tool_name="kubectl_get",
arguments={
"resource": f"{kind}s",
"namespace": namespace,
},
)
if result.success and result.data:
resources: list[K8sResource] = []
items = result.data if isinstance(result.data, list) else [result.data]
for item in items:
if isinstance(item, dict):
metadata = item.get("metadata", {})
spec = item.get("spec", {})
resources.append(K8sResource(
name=metadata.get("name", ""),
namespace=metadata.get("namespace", namespace),
kind=kind,
replicas=spec.get("replicas"),
))
return resources
return []
except Exception as e:
logger.warning(
"list_resources_failed",
namespace=namespace,
kind=kind,
error=str(e),
)
return []
def _calculate_similarity(
self,
resource_name: str,
hints: list[str],
original: str,
) -> float:
"""
計算資源名稱與輸入的相似度
綜合考慮:
1. 直接子字串匹配
2. 關鍵字匹配
3. Levenshtein 距離
"""
score = 0.0
name_lower = resource_name.lower()
original_lower = original.lower()
# 1. 直接包含關係
if name_lower in original_lower or original_lower in name_lower:
score += 0.5
# 2. 關鍵字匹配
matched_hints = sum(1 for h in hints if h in name_lower)
if hints:
score += (matched_hints / len(hints)) * 0.3
# 3. 序列相似度
ratio = SequenceMatcher(None, name_lower, original_lower).ratio()
score += ratio * 0.2
return min(score, 1.0)
# =============================================================================
# Dependency Injection Support (Phase 17 P2 改進)
# =============================================================================
_resolver: IResourceResolver | None = None
def get_resource_resolver() -> IResourceResolver:
"""
取得 ResourceResolver 實例
支援兩種模式:
1. 預設: 使用內部 singleton (向後相容)
2. DI: 透過 set_resource_resolver() 注入自訂實例
測試時可使用 set_resource_resolver(mock_resolver) 注入 mock
Returns:
IResourceResolver: 實作 IResourceResolver Protocol 的實例
"""
global _resolver
if _resolver is None:
_resolver = ResourceResolver()
return _resolver
def set_resource_resolver(resolver: IResourceResolver | None) -> None:
"""
注入 ResourceResolver 實例 (用於 DI 或測試)
Args:
resolver: 自訂 ResourceResolver 實例None 則重置為預設
Example:
# 測試時注入 mock
mock_resolver = MockResourceResolver()
set_resource_resolver(mock_resolver)
# 測試後重置
set_resource_resolver(None)
"""
global _resolver
_resolver = resolver