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

@@ -0,0 +1,104 @@
"""
Playbook Embedding Repository — pgvector 持久化層
=================================================
C1 修正 (首席架構師審查 2026-04-10 Claude Sonnet 4.6 Asia/Taipei):
違規修復: playbook_embedding_service.py 直接 db.execute(text(...)) 繞過 Repository 層
解法: 移至此 RepositoryService 層透過此介面存取
職責: playbook_embeddings 表的 CRUD (pgvector)
對應遷移: migrations/flywheel_playbook_embeddings.sql
"""
from __future__ import annotations
import structlog
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
logger = structlog.get_logger(__name__)
class PlaybookEmbeddingRepository:
"""
Playbook Embedding Repository
職責: playbook_embeddings 表 CRUD
使用 pgvector 儲存 nomic-embed-text 768 維向量
Args:
db: SQLAlchemy AsyncSession (DI 注入)
"""
def __init__(self, db: AsyncSession) -> None:
self._db = db
async def upsert(
self,
playbook_id: str,
embedding: list[float],
alert_names: list[str],
keywords: list[str],
) -> bool:
"""
新增或更新 Playbook 向量 (UPSERT)。
pgvector 格式: '[x,y,z,...]' 字串
ON CONFLICT (playbook_id) → 更新向量快照與 updated_at
Args:
playbook_id: Playbook ID
embedding: 768 維浮點向量 (list[float])
alert_names: 索引時的 alert_names 快照
keywords: 索引時的 keywords 快照
Returns:
True 表示成功
"""
try:
# 顯式格式化保證 pgvector 可解析,避免 str(list) 空格差異
vec_str = "[" + ",".join(str(float(x)) for x in embedding) + "]"
await self._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_id,
"embedding": vec_str,
"alert_names": alert_names,
"keywords": keywords,
},
)
return True
except Exception as e:
logger.warning(
"playbook_embedding_upsert_failed",
playbook_id=playbook_id,
error=str(e),
)
return False
async def delete(self, playbook_id: str) -> bool:
"""刪除 Playbook 向量記錄。"""
try:
await self._db.execute(
text("DELETE FROM playbook_embeddings WHERE playbook_id = :pid"),
{"pid": playbook_id},
)
return True
except Exception as e:
logger.warning(
"playbook_embedding_delete_failed",
playbook_id=playbook_id,
error=str(e),
)
return False