fix(playbook+flywheel): 修復 PlaybookSource enum + repair_steps 相容 + KM stats raw SQL
Some checks failed
CD Pipeline / build-and-deploy (push) Successful in 14m58s
Type Sync Check / check-type-sync (push) Failing after 1m17s

修復三個串聯 bug,讓 Playbook seed 能正常執行:

1. PlaybookSource 新增 YAML_RULE enum(alert_rules.yaml 匯入專用)
2. playbook_seed_service: source=YAML_RULE,dedup 改用 raw SQL by name,
   不再呼叫 list_playbooks(舊格式 repair_steps 會 validation error)
3. playbook_repository._orm_to_pydantic: 舊格式 repair_steps 補齊
   step_number/action_type 必填欄位(向下相容)
4. flywheel_stats_service: embedding IS NULL 改用 raw SQL,
   修復 KnowledgeEntryRecord ORM 無 embedding 屬性的 AttributeError

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-15 23:31:56 +08:00
parent 4bee14ae08
commit 800ab1685f
4 changed files with 34 additions and 8 deletions

View File

@@ -39,6 +39,7 @@ class PlaybookSource(str, Enum):
EXTRACTED = "extracted" # 從 Incident 自動萃取
MANUAL = "manual" # 人工建立
YAML_RULE = "yaml_rule" # 從 alert_rules.yaml 匯入2026-04-15 ogt
class ActionType(str, Enum):

View File

@@ -79,8 +79,24 @@ def _pydantic_to_orm(playbook: Playbook) -> PlaybookRecord:
)
def _normalize_repair_step(step: dict) -> dict:
"""舊格式 repair_steps 補齊必填欄位2026-04-15 ogt: 向下相容)"""
step = dict(step)
if "step_number" not in step:
step["step_number"] = 1
if "action_type" not in step:
cmd = step.get("command", "")
step["action_type"] = "kubectl" if cmd and not cmd.startswith("ssh") else "manual"
return step
def _orm_to_pydantic(record: PlaybookRecord) -> Playbook:
"""PlaybookRecord ORM → Pydantic Playbook"""
raw_steps = record.repair_steps or []
normalized_steps = [
_normalize_repair_step(s) if isinstance(s, dict) else s
for s in raw_steps
]
return Playbook.model_validate({
"playbook_id": record.playbook_id,
"name": record.name,
@@ -88,7 +104,7 @@ def _orm_to_pydantic(record: PlaybookRecord) -> Playbook:
"status": record.status,
"source": record.source,
"symptom_pattern": record.symptom_pattern,
"repair_steps": record.repair_steps,
"repair_steps": normalized_steps,
"estimated_duration_minutes": record.estimated_duration_minutes,
"source_incident_ids": record.source_incident_ids,
"ai_confidence": float(record.ai_confidence),

View File

@@ -225,8 +225,10 @@ class FlywheelStatsService:
async with get_db_context() as db:
# 未向量化數量 (embedding IS NULL = 未向量化)
# 2026-04-15 ogt: KnowledgeEntryRecord ORM 不宣告 embedding 欄位pgvector
# 改用 raw SQL 避免 AttributeError
unvectorized_q = await db.execute(
select(func.count()).where(KnowledgeEntryRecord.embedding.is_(None))
text("SELECT COUNT(*) FROM knowledge_entries WHERE embedding IS NULL")
)
unvectorized = unvectorized_q.scalar_one_or_none() or 0

View File

@@ -42,15 +42,22 @@ async def seed_playbooks_from_rules() -> None:
repo = get_playbook_repository()
# 取得現有 playbook source_ids避免重複建立
existing, _ = await repo.list_playbooks(status=PlaybookStatus.APPROVED, limit=500)
existing_sources = {p.source for p in existing if p.source}
# 取得現有 YAML_RULE playbook依 name 去重避免重複建立
# 2026-04-15 ogt: 不再用 list_playbooks舊格式 repair_steps 會 validation error
# 改用 raw SQL 只撈 name 欄位,更穩健
from src.db.session import get_db_context
from sqlalchemy import text as sa_text
async with get_db_context() as db:
rows = await db.execute(
sa_text("SELECT name FROM playbooks WHERE source = 'yaml_rule'")
)
existing_names = {r[0] for r in rows.fetchall()}
seeded = 0
for rule in rules:
rule_id = rule.get("id", "")
source_key = f"alert_rule:{rule_id}"
if source_key in existing_sources:
rule_name = rule.get("description", rule_id)
if rule_name in existing_names:
continue
resp = rule.get("response", {})
@@ -71,7 +78,7 @@ async def seed_playbooks_from_rules() -> None:
name=rule.get("description", rule_id),
description=resp.get("description", rule.get("description", "")),
status=PlaybookStatus.APPROVED,
source=source_key,
source=PlaybookSource.YAML_RULE,
symptom_pattern=SymptomPattern(
alert_names=alertnames,
affected_services=[],