refactor(api): Phase 22 技術債修復 - 業務邏輯分層
Some checks failed
E2E Health Check / e2e-health (push) Has been cancelled
Some checks failed
E2E Health Check / e2e-health (push) Has been cancelled
P2.3: LearningService.get_learning_summary() 業務邏輯移至 Service 層 - Repository 只提供原始統計數據 - Service 計算 best_action 和 learning_status P2.6: Playbook similarity 計算邏輯抽取 - 新增 src/utils/similarity.py - Repository 從 utils 導入,不再定義演算法 2026-03-31 Claude Code (首席架構師技術債修復) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ Phase 7.2: Repository 實作
|
||||
- 實作 IPlaybookRepository Protocol
|
||||
- Redis 為 Working Memory (7天 TTL)
|
||||
- PostgreSQL 為 Episodic Memory
|
||||
|
||||
Phase 22 P2: 相似度計算邏輯移至 utils
|
||||
2026-03-31 Claude Code (首席架構師技術債修復)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -24,6 +27,7 @@ from src.models.playbook import (
|
||||
SymptomPattern,
|
||||
)
|
||||
from src.repositories.interfaces import IPlaybookRepository
|
||||
from src.utils.similarity import calculate_symptom_similarity
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -37,63 +41,6 @@ PLAYBOOK_INDEX_ALERT_PREFIX = "playbook:index:alert:"
|
||||
PLAYBOOK_INDEX_SERVICE_PREFIX = "playbook:index:service:"
|
||||
|
||||
|
||||
def _calculate_jaccard_similarity(set_a: set, set_b: set) -> float:
|
||||
"""計算 Jaccard 相似度"""
|
||||
if not set_a and not set_b:
|
||||
return 1.0
|
||||
intersection = len(set_a & set_b)
|
||||
union = len(set_a | set_b)
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def calculate_symptom_similarity(
|
||||
pattern_a: SymptomPattern,
|
||||
pattern_b: SymptomPattern,
|
||||
) -> float:
|
||||
"""
|
||||
計算症狀相似度
|
||||
|
||||
算法: 加權 Jaccard 相似度
|
||||
|
||||
維度權重:
|
||||
- alert_names: 0.35 (最重要)
|
||||
- affected_services: 0.30
|
||||
- severity: 0.15
|
||||
- keywords: 0.20
|
||||
|
||||
Returns:
|
||||
float: 0.0 ~ 1.0 相似度分數
|
||||
"""
|
||||
weights = {
|
||||
"alert_names": 0.35,
|
||||
"affected_services": 0.30,
|
||||
"severity": 0.15,
|
||||
"keywords": 0.20,
|
||||
}
|
||||
|
||||
scores = {
|
||||
"alert_names": _calculate_jaccard_similarity(
|
||||
set(pattern_a.alert_names),
|
||||
set(pattern_b.alert_names),
|
||||
),
|
||||
"affected_services": _calculate_jaccard_similarity(
|
||||
set(pattern_a.affected_services),
|
||||
set(pattern_b.affected_services),
|
||||
),
|
||||
"severity": (
|
||||
1.0
|
||||
if set(pattern_a.severity_range) & set(pattern_b.severity_range)
|
||||
else 0.0
|
||||
),
|
||||
"keywords": _calculate_jaccard_similarity(
|
||||
set(pattern_a.keywords),
|
||||
set(pattern_b.keywords),
|
||||
),
|
||||
}
|
||||
|
||||
return sum(weights[k] * scores[k] for k in weights)
|
||||
|
||||
|
||||
class PlaybookRepository:
|
||||
"""
|
||||
Playbook Repository 實作
|
||||
|
||||
@@ -655,7 +655,12 @@ class LearningService:
|
||||
"""
|
||||
取得學習摘要
|
||||
|
||||
2026-03-29 P0 修正: 委託 Repository 實作
|
||||
Phase 22 P2: 業務邏輯移至 Service 層
|
||||
2026-03-31 Claude Code (首席架構師技術債修復)
|
||||
|
||||
邏輯:
|
||||
- 從 Repository 取得原始統計數據
|
||||
- 在 Service 層計算 best_action 和 learning_status
|
||||
|
||||
Returns:
|
||||
{
|
||||
@@ -667,7 +672,51 @@ class LearningService:
|
||||
'learning_status': 'sufficient',
|
||||
}
|
||||
"""
|
||||
return await self._repository.get_learning_summary(anomaly_key)
|
||||
# 從 Repository 取得原始統計
|
||||
all_stats = await self._repository.get_all_repair_stats(anomaly_key)
|
||||
|
||||
if not all_stats:
|
||||
return {
|
||||
"anomaly_key": anomaly_key,
|
||||
"total_repair_attempts": 0,
|
||||
"overall_success_rate": 0.0,
|
||||
"actions_tried": [],
|
||||
"best_action": None,
|
||||
"learning_status": "insufficient",
|
||||
}
|
||||
|
||||
# === 以下為業務邏輯,應在 Service 層 ===
|
||||
|
||||
total_attempts = sum(s["total"] for s in all_stats.values())
|
||||
total_success = sum(s["success"] for s in all_stats.values())
|
||||
overall_rate = total_success / total_attempts if total_attempts > 0 else 0.0
|
||||
|
||||
# 找出最佳動作 (需要至少 3 次數據)
|
||||
best_action = None
|
||||
best_rate = 0.0
|
||||
for action, stats in all_stats.items():
|
||||
if stats["total"] >= 3 and stats["success_rate"] > best_rate:
|
||||
best_rate = stats["success_rate"]
|
||||
best_action = {"action": action, "success_rate": best_rate}
|
||||
|
||||
# 判斷學習狀態
|
||||
if total_attempts < 3:
|
||||
learning_status = "insufficient"
|
||||
elif total_attempts < 10:
|
||||
learning_status = "learning"
|
||||
elif overall_rate >= 0.8:
|
||||
learning_status = "excellent"
|
||||
else:
|
||||
learning_status = "sufficient"
|
||||
|
||||
return {
|
||||
"anomaly_key": anomaly_key,
|
||||
"total_repair_attempts": total_attempts,
|
||||
"overall_success_rate": overall_rate,
|
||||
"actions_tried": list(all_stats.keys()),
|
||||
"best_action": best_action,
|
||||
"learning_status": learning_status,
|
||||
}
|
||||
|
||||
def _get_action_tier(self, action: str) -> int:
|
||||
"""取得動作的 Tier"""
|
||||
|
||||
87
apps/api/src/utils/similarity.py
Normal file
87
apps/api/src/utils/similarity.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Similarity Calculation Utils
|
||||
=============================
|
||||
Phase 22 P2: 將相似度計算邏輯從 Repository 移出
|
||||
|
||||
設計原則:
|
||||
- 演算法邏輯應獨立於資料存取層
|
||||
- Repository 只負責 CRUD,不負責演算法
|
||||
- Service 層可以使用這些工具函數
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-31 (台北時區)
|
||||
建立者: Claude Code (首席架構師技術債修復)
|
||||
"""
|
||||
|
||||
from src.models.playbook import SymptomPattern
|
||||
|
||||
|
||||
def calculate_jaccard_similarity(set_a: set, set_b: set) -> float:
|
||||
"""
|
||||
計算 Jaccard 相似度
|
||||
|
||||
Jaccard = |A ∩ B| / |A ∪ B|
|
||||
|
||||
Args:
|
||||
set_a: 集合 A
|
||||
set_b: 集合 B
|
||||
|
||||
Returns:
|
||||
float: 0.0 ~ 1.0
|
||||
"""
|
||||
if not set_a and not set_b:
|
||||
return 1.0 # 兩個空集合視為完全相同
|
||||
if not set_a or not set_b:
|
||||
return 0.0
|
||||
|
||||
intersection = len(set_a & set_b)
|
||||
union = len(set_a | set_b)
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def calculate_symptom_similarity(
|
||||
pattern_a: SymptomPattern,
|
||||
pattern_b: SymptomPattern,
|
||||
) -> float:
|
||||
"""
|
||||
計算症狀相似度
|
||||
|
||||
算法: 加權 Jaccard 相似度
|
||||
|
||||
維度權重:
|
||||
- alert_names: 0.35 (最重要)
|
||||
- affected_services: 0.30
|
||||
- severity: 0.15
|
||||
- keywords: 0.20
|
||||
|
||||
Returns:
|
||||
float: 0.0 ~ 1.0 相似度分數
|
||||
"""
|
||||
weights = {
|
||||
"alert_names": 0.35,
|
||||
"affected_services": 0.30,
|
||||
"severity": 0.15,
|
||||
"keywords": 0.20,
|
||||
}
|
||||
|
||||
scores = {
|
||||
"alert_names": calculate_jaccard_similarity(
|
||||
set(pattern_a.alert_names),
|
||||
set(pattern_b.alert_names),
|
||||
),
|
||||
"affected_services": calculate_jaccard_similarity(
|
||||
set(pattern_a.affected_services),
|
||||
set(pattern_b.affected_services),
|
||||
),
|
||||
"severity": (
|
||||
1.0
|
||||
if set(pattern_a.severity_range) & set(pattern_b.severity_range)
|
||||
else 0.0
|
||||
),
|
||||
"keywords": calculate_jaccard_similarity(
|
||||
set(pattern_a.keywords),
|
||||
set(pattern_b.keywords),
|
||||
),
|
||||
}
|
||||
|
||||
return sum(weights[k] * scores[k] for k in weights)
|
||||
Reference in New Issue
Block a user