feat(playbook): version generated playbooks
All checks were successful
CD Pipeline / tests (push) Successful in 1m34s
Code Review / ai-code-review (push) Successful in 28s
Type Sync Check / check-type-sync (push) Successful in 1m10s
CD Pipeline / build-and-deploy (push) Successful in 10m19s
CD Pipeline / post-deploy-checks (push) Successful in 3m1s
All checks were successful
CD Pipeline / tests (push) Successful in 1m34s
Code Review / ai-code-review (push) Successful in 28s
Type Sync Check / check-type-sync (push) Successful in 1m10s
CD Pipeline / build-and-deploy (push) Successful in 10m19s
CD Pipeline / post-deploy-checks (push) Successful in 3m1s
This commit is contained in:
@@ -179,10 +179,31 @@ class LLMPlaybookGenerator:
|
||||
playbook.status = PlaybookStatus.DRAFT
|
||||
|
||||
if persist:
|
||||
playbook = await self._service().create(playbook)
|
||||
playbook = await self._persist_with_lineage(playbook)
|
||||
|
||||
return self._record(playbook, "success", provider, "")
|
||||
|
||||
async def _persist_with_lineage(self, playbook: Playbook) -> Playbook:
|
||||
"""Create a new lineage version when a close approved Playbook exists."""
|
||||
try:
|
||||
recommendations = await self._service().get_recommendations(
|
||||
symptoms=playbook.symptom_pattern,
|
||||
top_k=1,
|
||||
use_rag=False,
|
||||
)
|
||||
if recommendations and recommendations[0].similarity_score >= 0.85:
|
||||
base = recommendations[0].playbook
|
||||
created = await self._service().create_new_version(
|
||||
base_playbook_id=base.playbook_id,
|
||||
candidate=playbook,
|
||||
reason="ADR-104 local LLM generated improved Playbook from successful incident",
|
||||
)
|
||||
if created is not None:
|
||||
return created
|
||||
except Exception as exc:
|
||||
logger.warning("playbook_generation_lineage_fallback", error=str(exc))
|
||||
return await self._service().create(playbook)
|
||||
|
||||
async def _call_local_llm(
|
||||
self,
|
||||
prompt: str,
|
||||
|
||||
@@ -28,6 +28,7 @@ from src.models.playbook import (
|
||||
RepairStep,
|
||||
RiskLevel,
|
||||
SymptomPattern,
|
||||
generate_playbook_id,
|
||||
)
|
||||
from src.repositories.interfaces import IPlaybookRepository
|
||||
from src.repositories.playbook_repository import get_playbook_repository
|
||||
@@ -79,10 +80,32 @@ class IPlaybookService(Protocol):
|
||||
self,
|
||||
symptoms: SymptomPattern,
|
||||
top_k: int = 3,
|
||||
use_rag: bool = True,
|
||||
) -> list[PlaybookRecommendation]:
|
||||
"""取得 Playbook 推薦"""
|
||||
...
|
||||
|
||||
async def get_by_id(self, playbook_id: str) -> Playbook | None:
|
||||
"""取得 Playbook"""
|
||||
...
|
||||
|
||||
async def create_new_version(
|
||||
self,
|
||||
base_playbook_id: str,
|
||||
candidate: Playbook,
|
||||
reason: str,
|
||||
) -> Playbook | None:
|
||||
"""從既有 Playbook 建立下一版"""
|
||||
...
|
||||
|
||||
async def update_with_validation(
|
||||
self,
|
||||
playbook_id: str,
|
||||
update_data: dict,
|
||||
) -> Playbook | None:
|
||||
"""驗證後更新 Playbook"""
|
||||
...
|
||||
|
||||
async def approve(
|
||||
self,
|
||||
playbook_id: str,
|
||||
@@ -463,6 +486,51 @@ class PlaybookService:
|
||||
"""更新 Playbook"""
|
||||
return await self._repository.update(playbook)
|
||||
|
||||
async def create_new_version(
|
||||
self,
|
||||
base_playbook_id: str,
|
||||
candidate: Playbook,
|
||||
reason: str,
|
||||
) -> Playbook | None:
|
||||
"""
|
||||
從既有 Playbook 建立下一版。
|
||||
|
||||
ADR-104 T4: LLM 生成的改良方案不覆蓋舊 Playbook,而是建立 lineage:
|
||||
root(parent_playbook_id) -> v2 -> v3。舊版在新版 APPROVED 前仍可用。
|
||||
"""
|
||||
base = await self._repository.get_by_id(base_playbook_id)
|
||||
if not base:
|
||||
logger.warning("playbook_version_base_missing", base_playbook_id=base_playbook_id)
|
||||
return None
|
||||
|
||||
root_id = base.parent_playbook_id or base.playbook_id
|
||||
candidate.playbook_id = generate_playbook_id()
|
||||
candidate.version = base.version + 1
|
||||
candidate.parent_playbook_id = root_id
|
||||
candidate.supersedes_playbook_id = base.playbook_id
|
||||
candidate.version_reason = reason[:500]
|
||||
candidate.success_count = 0
|
||||
candidate.failure_count = 0
|
||||
candidate.last_used_at = None
|
||||
candidate.approved_by = None
|
||||
candidate.approved_at = None
|
||||
if base.playbook_id not in candidate.source_incident_ids:
|
||||
candidate.notes = (
|
||||
(candidate.notes or "")
|
||||
+ f"\n[Version lineage: v{candidate.version} supersedes {base.playbook_id}]"
|
||||
).strip()
|
||||
|
||||
created = await self._repository.create(candidate)
|
||||
logger.info(
|
||||
"playbook_version_created",
|
||||
playbook_id=created.playbook_id,
|
||||
base_playbook_id=base.playbook_id,
|
||||
root_playbook_id=root_id,
|
||||
version=created.version,
|
||||
reason=reason,
|
||||
)
|
||||
return created
|
||||
|
||||
async def update_with_validation(
|
||||
self,
|
||||
playbook_id: str,
|
||||
|
||||
Reference in New Issue
Block a user