Some checks are pending
CD Pipeline / build-and-deploy (push) Has started running
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>
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""
|
||
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 修正: 改用 PlaybookEmbeddingRepository,Service 不直接操作 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))
|