fix(auto-repair): preserve exact playbook candidates
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 5m46s
CD Pipeline / build-and-deploy (push) Successful in 4m6s
CD Pipeline / post-deploy-checks (push) Successful in 1m28s

This commit is contained in:
Your Name
2026-05-13 23:37:59 +08:00
parent 5161a9dfd6
commit a0a0731cd6
3 changed files with 138 additions and 21 deletions

View File

@@ -14,6 +14,7 @@ Phase 3 ADR-030: RAG 向量搜尋整合
- 封裝所有業務邏輯
"""
import re as _re
from typing import Protocol
import structlog
@@ -32,13 +33,11 @@ from src.models.playbook import (
)
from src.repositories.interfaces import IPlaybookRepository
from src.repositories.playbook_repository import get_playbook_repository
from src.services.playbook_rag import get_playbook_rag_service
from src.services.playbook_rag import PlaybookMatch, get_playbook_rag_service
from src.utils.timezone import now_taipei
logger = structlog.get_logger(__name__)
import re as _re
def _parse_ssh_command(ssh_cmd: str) -> tuple[str, str]:
"""
@@ -275,16 +274,16 @@ class PlaybookService:
payload = KMWritePayload(
path_type="playbook_extract",
entry_create_kwargs=dict(
title=f"[Playbook] {playbook.name}",
content=body,
entry_type=EntryType.INCIDENT_CASE,
category="auto_repair",
tags=[*playbook.tags, "playbook", "auto_extracted", playbook.status.value],
source=EntrySource.AI_EXTRACTED,
related_incident_id=incident.incident_id,
created_by="playbook_service",
),
entry_create_kwargs={
"title": f"[Playbook] {playbook.name}",
"content": body,
"entry_type": EntryType.INCIDENT_CASE,
"category": "auto_repair",
"tags": [*playbook.tags, "playbook", "auto_extracted", playbook.status.value],
"source": EntrySource.AI_EXTRACTED,
"related_incident_id": incident.incident_id,
"created_by": "playbook_service",
},
incident_id=incident.incident_id,
)
result = await km_write_with_flag(payload)
@@ -348,6 +347,17 @@ class PlaybookService:
vector_weight=0.6,
jaccard_weight=0.4,
)
hybrid_by_id = {match.playbook_id: match for match in hybrid_matches}
for playbook_id, jaccard_score in jaccard_results:
if playbook_id in hybrid_by_id:
continue
hybrid_matches.append(
PlaybookMatch(
playbook_id=playbook_id,
similarity_score=jaccard_score,
match_type="jaccard",
)
)
# 補充 playbook_map (RAG 可能找到 Jaccard 沒找到的)
for match in hybrid_matches:
@@ -404,9 +414,9 @@ class PlaybookService:
)
)
# Step 4: 按綜合分數排序 (similarity * success_rate)
# Step 4: 先保住 exact signal避免精準 Playbook 被語意近似項擠掉。
recommendations.sort(
key=lambda r: r.similarity_score * (0.5 + 0.5 * r.playbook.success_rate),
key=lambda r: self._recommendation_priority(r, symptoms),
reverse=True,
)
@@ -821,6 +831,25 @@ class PlaybookService:
return matched
@staticmethod
def _normalized_overlap(left: list[str], right: list[str]) -> bool:
left_values = {value.casefold() for value in left if value}
right_values = {value.casefold() for value in right if value}
return bool(left_values & right_values)
def _recommendation_priority(
self,
recommendation: PlaybookRecommendation,
symptoms: SymptomPattern,
) -> tuple[bool, bool, float]:
pattern = recommendation.playbook.symptom_pattern
alert_exact = self._normalized_overlap(symptoms.alert_names, pattern.alert_names)
service_exact = self._normalized_overlap(symptoms.affected_services, pattern.affected_services)
quality_score = recommendation.similarity_score * (
0.5 + 0.5 * recommendation.playbook.success_rate
)
return (alert_exact, service_exact, quality_score)
def _generate_recommendation_reason(
self,
playbook: Playbook,