refactor(flywheel): 首席架構師審查修正 C1/C2/I1/I2/I3/I4/M1
Some checks are pending
CD Pipeline / build-and-deploy (push) Has started running
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:
@@ -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 修正: 改用 PlaybookEmbeddingRepository,Service 不直接操作 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))
|
||||
|
||||
Reference in New Issue
Block a user