fix(auto-repair): preserve exact playbook candidates
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user