refactor(flywheel): 首席架構師審查修正 C1/C2/I1/I2/I3/I4/M1
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>
This commit is contained in:
OG T
2026-04-10 11:35:10 +08:00
parent 0cac128a64
commit 670cd5df86
6 changed files with 292 additions and 186 deletions

View File

@@ -6,17 +6,26 @@ ADR-067 延伸: Playbook 向量持久化到 PostgreSQL playbook_embeddings 表
職責:
- 啟動時掃描 APPROVED Playbooks重建 Redis 向量快取
- 同步持久化到 playbook_embeddings (pgvector) 供跨重啟使用
- 已索引且未變更的 Playbook 跳過 (updated_at 比對)
呼叫方: 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__)
@@ -35,7 +44,7 @@ async def ensure_playbook_embeddings_indexed() -> None:
from src.services.playbook_rag import get_playbook_rag_service
playbook_service = get_playbook_service()
playbooks, total = await playbook_service.list_playbooks(
playbooks, _total = await playbook_service.list_playbooks(
status=PlaybookStatus.APPROVED, limit=500
)
@@ -45,15 +54,11 @@ async def ensure_playbook_embeddings_indexed() -> None:
logger.info("playbook_embedding_indexing_start", count=len(playbooks))
# Step 1: 重建 Redis 向量快取 (現有邏輯)
# 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,
)
logger.info("playbook_embedding_redis_done", success=success, failed=failed)
# Step 2: 持久化到 PostgreSQL playbook_embeddings 表
await _persist_embeddings_to_db(rag_service, playbooks)
@@ -62,16 +67,26 @@ async def ensure_playbook_embeddings_indexed() -> None:
logger.warning("playbook_embedding_indexing_error", error=str(e))
async def _persist_embeddings_to_db(rag_service, playbooks) -> None:
"""將 Redis 向量快取同步寫入 playbook_embeddings DB 表 (持久化層)。"""
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 sqlalchemy import text
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)
@@ -83,28 +98,16 @@ async def _persist_embeddings_to_db(rag_service, playbooks) -> None:
alert_names = list(sp.alert_names) if sp else []
keywords = list(sp.keywords) if sp else []
# UPSERT: 已存在則更新向量快照
await db.execute(
text("""
INSERT INTO playbook_embeddings
(playbook_id, embedding, alert_names, keywords, indexed_at, updated_at)
VALUES
(:playbook_id, :embedding, :alert_names, :keywords,
NOW(), NOW())
ON CONFLICT (playbook_id) DO UPDATE SET
embedding = EXCLUDED.embedding,
alert_names = EXCLUDED.alert_names,
keywords = EXCLUDED.keywords,
updated_at = NOW()
"""),
{
"playbook_id": playbook.playbook_id,
"embedding": str(embedding), # pgvector accepts '[x,y,...]' string
"alert_names": alert_names,
"keywords": keywords,
},
ok = await repo.upsert(
playbook_id=playbook.playbook_id,
embedding=embedding,
alert_names=alert_names,
keywords=keywords,
)
persisted += 1
if ok:
persisted += 1
else:
skipped += 1
except Exception as e:
logger.warning(
@@ -116,11 +119,7 @@ async def _persist_embeddings_to_db(rag_service, playbooks) -> None:
await db.commit()
logger.info(
"playbook_embedding_db_done",
persisted=persisted,
skipped=skipped,
)
logger.info("playbook_embedding_db_done", persisted=persisted, skipped=skipped)
except Exception as e:
logger.warning("playbook_embedding_db_error", error=str(e))