Files
awoooi/apps/api/src/services/playbook_embedding_service.py
OG T 670cd5df86
Some checks are pending
CD Pipeline / build-and-deploy (push) Has started running
refactor(flywheel): 首席架構師審查修正 C1/C2/I1/I2/I3/I4/M1
C1 — Repository 層修正 (積木化鐵律):
  新增 PlaybookEmbeddingRepository (pgvector UPSERT)
  playbook_embedding_service 改透過 Repository 存取 DB,不再直接 db.execute(text(...))

C2 — Router 層業務邏輯移入 Service 層:
  create_incident_for_approval + extract_affected_services (去掉底線前綴) 移入 incident_service.py
  webhooks.py 改從 incident_service import,自身不再含業務邏輯

I1 — _infra_jobs 提升為 module-level frozenset (_INFRA_JOB_NAMES),避免每次呼叫重建

I2 — _persist_embeddings_to_db 補齊 PlaybookRAGService / list[Playbook] 型別標注

I3 — embedding 格式顯式化: "[" + ",".join(str(float(x)) for x in embedding) + "]"
  防止 pgvector 因格式差異靜默解析失敗

I4 — import asyncio 移至 main.py 頂層,移除 try 區塊內重複 import

M1 — similarity.py: 移除死代碼 `if union > 0 else 0.0`
  union 在兩個集合都非空時不可能為 0

2026-04-10 Asia/Taipei — Claude Sonnet 4.6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:35:10 +08:00

126 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Playbook Embedding Service — Phase 4 飛輪冷啟動修復
====================================================
ADR-067 延伸: Playbook 向量持久化到 PostgreSQL playbook_embeddings 表
職責:
- 啟動時掃描 APPROVED Playbooks重建 Redis 向量快取
- 同步持久化到 playbook_embeddings (pgvector) 供跨重啟使用
呼叫方: main.py lifespan (asyncio.create_task — 非阻塞)
2026-04-10 Claude Sonnet 4.6 Asia/Taipei
修正 (首席架構師審查 2026-04-10):
C1: _persist_embeddings_to_db 改用 PlaybookEmbeddingRepository (積木化修復)
I2: 補齊 _persist_embeddings_to_db 型別標注
I3: embedding 格式顯式格式化,防止 pgvector 解析錯誤
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import structlog
if TYPE_CHECKING:
from src.models.playbook import Playbook
from src.services.playbook_rag import PlaybookRAGService
logger = structlog.get_logger(__name__)
async def ensure_playbook_embeddings_indexed() -> None:
"""
確保所有 APPROVED Playbooks 都有向量索引。
執行步驟:
1. 從 PlaybookService 取得所有 APPROVED Playbooks
2. 呼叫 PlaybookRAGService.reindex_all_playbooks → 更新 Redis 向量快取
3. 將向量持久化到 playbook_embeddings (pgvector) 表
"""
try:
from src.models.playbook import PlaybookStatus
from src.services.playbook_service import get_playbook_service
from src.services.playbook_rag import get_playbook_rag_service
playbook_service = get_playbook_service()
playbooks, _total = await playbook_service.list_playbooks(
status=PlaybookStatus.APPROVED, limit=500
)
if not playbooks:
logger.info("playbook_embedding_indexing_skipped", reason="no approved playbooks")
return
logger.info("playbook_embedding_indexing_start", count=len(playbooks))
# Step 1: 重建 Redis 向量快取
rag_service = await get_playbook_rag_service()
success, failed = await rag_service.reindex_all_playbooks(playbooks)
logger.info("playbook_embedding_redis_done", success=success, failed=failed)
# Step 2: 持久化到 PostgreSQL playbook_embeddings 表
await _persist_embeddings_to_db(rag_service, playbooks)
except Exception as e:
logger.warning("playbook_embedding_indexing_error", error=str(e))
async def _persist_embeddings_to_db(
rag_service: "PlaybookRAGService",
playbooks: "list[Playbook]",
) -> None:
"""
將 Redis 向量快取同步寫入 playbook_embeddings DB 表 (持久化層)。
C1 修正: 改用 PlaybookEmbeddingRepositoryService 不直接操作 SQL。
I3 修正: embedding 格式由 Repository 層統一處理,防止 pgvector 解析錯誤。
"""
try:
from src.db.base import get_db_context
from src.repositories.playbook_embedding_repository import PlaybookEmbeddingRepository
persisted = 0
skipped = 0
async with get_db_context() as db:
repo = PlaybookEmbeddingRepository(db)
for playbook in playbooks:
try:
embedding = await rag_service.get_playbook_embedding(playbook.playbook_id)
if not embedding:
skipped += 1
continue
sp = playbook.symptom_pattern
alert_names = list(sp.alert_names) if sp else []
keywords = list(sp.keywords) if sp else []
ok = await repo.upsert(
playbook_id=playbook.playbook_id,
embedding=embedding,
alert_names=alert_names,
keywords=keywords,
)
if ok:
persisted += 1
else:
skipped += 1
except Exception as e:
logger.warning(
"playbook_embedding_persist_error",
playbook_id=playbook.playbook_id,
error=str(e),
)
skipped += 1
await db.commit()
logger.info("playbook_embedding_db_done", persisted=persisted, skipped=skipped)
except Exception as e:
logger.warning("playbook_embedding_db_error", error=str(e))