feat(api): Phase 7.5-7.6 Playbook 整合決策與自動萃取

Phase 7.5: DecisionManager 三軌決策
- 新增 Playbook 優先匹配 (similarity >= 85%)
- 三軌決策順序: Playbook > LLM > Expert System
- 整合 PlaybookService 推薦引擎

Phase 7.6: 自動萃取機制
- approval_execution.py 成功執行後觸發萃取
- 條件: RESOLVED/CLOSED + effectiveness >= 4
- 滿分 (5) 自動核准 Playbook

測試:
- 13 個 Playbook 單元測試全部通過
- 修復 Incident 模型欄位對應 (reasoning_steps)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 11:09:25 +08:00
parent 6f99113888
commit 2e75a20150
4 changed files with 658 additions and 29 deletions

View File

@@ -8,9 +8,11 @@ Approval Execution Service - Phase 16 R4.2 瘦身 Router 抽取
- ApprovalDBService: 更新狀態
- TimelineService: 記錄事件
- NotificationManager: 發送通知
- Phase 7.6: Playbook 自動萃取
版本: v1.0
版本: v1.1
建立: 2026-03-25 (台北時區)
更新: 2026-03-26 (Phase 7.6 自動萃取)
建立者: Claude Code (Phase 16 R4.2)
"""
@@ -144,6 +146,11 @@ class ApprovalExecutionService:
duration_ms=result.duration_ms,
)
)
# Phase 7.6: 觸發 Playbook 自動萃取 (fire-and-forget)
asyncio.create_task(
self._trigger_playbook_extraction(approval)
)
else:
logger.error(
"background_execution_failed",
@@ -250,6 +257,134 @@ class ApprovalExecutionService:
error=str(e),
)
async def _trigger_playbook_extraction(
self,
approval: ApprovalRequest,
) -> None:
"""
Phase 7.6: 觸發 Playbook 自動萃取
條件:
- 執行成功
- 關聯的 Incident 狀態為 RESOLVED 或 CLOSED
- effectiveness_score >= 4
此函數為 fire-and-forget失敗不影響主流程
"""
try:
# 1. 從 approval 取得關聯的 incident_id
# approval.requested_by 可能包含 incident 資訊,或從 metadata 取得
# 暫時從 description 或 action 解析
incident_id = self._extract_incident_id_from_approval(approval)
if not incident_id:
logger.debug(
"playbook_extraction_skip",
approval_id=str(approval.id),
reason="No incident_id found",
)
return
# 2. 取得 Incident
from src.services.incident_service import get_incident_service
incident_service = get_incident_service()
incident = await incident_service.get_incident(incident_id)
if not incident:
logger.debug(
"playbook_extraction_skip",
approval_id=str(approval.id),
incident_id=incident_id,
reason="Incident not found",
)
return
# 3. 檢查 Incident 狀態
from src.models.incident import IncidentStatus
if incident.status not in [IncidentStatus.RESOLVED, IncidentStatus.CLOSED]:
logger.debug(
"playbook_extraction_skip",
approval_id=str(approval.id),
incident_id=incident_id,
incident_status=incident.status.value,
reason="Incident not resolved",
)
return
# 4. 檢查 effectiveness_score
effectiveness = incident.outcome.effectiveness_score if incident.outcome else 0
if effectiveness < 4:
logger.debug(
"playbook_extraction_skip",
approval_id=str(approval.id),
incident_id=incident_id,
effectiveness=effectiveness,
reason="Low effectiveness score",
)
return
# 5. 觸發萃取
from src.services.playbook_service import get_playbook_service
playbook_service = get_playbook_service()
playbook = await playbook_service.extract_from_incident(
incident=incident,
auto_approve=effectiveness >= 5, # 滿分自動核准
)
if playbook:
logger.info(
"playbook_auto_extracted",
approval_id=str(approval.id),
incident_id=incident_id,
playbook_id=playbook.playbook_id,
playbook_name=playbook.name,
auto_approved=playbook.status.value == "approved",
)
else:
logger.debug(
"playbook_extraction_no_result",
approval_id=str(approval.id),
incident_id=incident_id,
)
except Exception as e:
# 萃取失敗不影響主流程
logger.warning(
"playbook_extraction_error",
approval_id=str(approval.id),
error=str(e),
)
def _extract_incident_id_from_approval(
self,
approval: ApprovalRequest,
) -> str | None:
"""
從 approval 提取關聯的 incident_id
嘗試以下來源:
1. approval.metadata (如果有)
2. approval.description 中的 INC- 模式
3. approval.requested_by 中的 incident 資訊
"""
import re
# 從 description 或 action 中尋找 INC-XXXXXX 模式
text = f"{approval.description or ''} {approval.action or ''}"
match = re.search(r"INC-([A-Z0-9-]+)", text)
if match:
return match.group(0) # 返回完整的 INC-XXXXX
# 從 requested_by 尋找
if approval.requested_by and "INC-" in approval.requested_by:
match = re.search(r"INC-([A-Z0-9-]+)", approval.requested_by)
if match:
return match.group(0)
return None
# =============================================================================
# Singleton Instance