Files
awoooi/apps/api/src/repositories/trust_repository.py
OG T 9d6aa7ea45
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m40s
feat(trust): ADR-088 Trust Score 持久化 — L4 自動放行核心
TrustScoreManager 從記憶體升級為 PostgreSQL 持久化,
Pod 重啟後信任分數不再歸零,AI 能真正累積到 L4 自動放行門檻。

變更:
- migrations/adr088_trust_score_persistence.sql: trust_records 表
- db/models.py: TrustRecordDB ORM model
- repositories/interfaces.py: ITrustRepository Protocol
- repositories/trust_repository.py: PG upsert ON CONFLICT DO UPDATE
- services/trust_engine.py: bulk_load() 啟動 warm-up
- services/learning_service.py: _persist_trust() + 2 call sites
- main.py: 啟動時 load_all() → bulk_load()

流程: 批准 5 次 → score=5 寫入 DB → Pod 重啟 → warm-up 讀回
      → evaluate_adjusted_risk MEDIUM→LOW → 自動執行

2026-04-17 ogt + Claude Sonnet 4.6(亞太): ADR-088

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:14:44 +08:00

146 lines
4.8 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.
"""
Trust Repository - PostgreSQL 持久化層
======================================
ADR-088: TrustScoreManager 持久化升級
職責: trust_records 表的 upsert / load_all
設計: 實作 ITrustRepository Protocol
Pod 重啟後信任分數不歸零AI 能真正累積到 L4 自動放行門檻。
2026-04-17 ogt + Claude Sonnet 4.6(亞太): Phase 4 信任持久化
"""
from datetime import datetime
import structlog
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from src.db.base import get_db_context
from src.db.models import TrustRecordDB
from src.repositories.interfaces import ITrustRepository
logger = structlog.get_logger(__name__)
class TrustRepository:
"""
Trust Repository 實作PostgreSQL
使用 PostgreSQL ON CONFLICT DO UPDATE (upsert)
保證並行安全且無競爭條件。
"""
async def upsert(
self,
action_pattern: str,
score: int,
total_approvals: int,
total_rejections: int,
last_approval_by: str | None = None,
last_approval_at: datetime | None = None,
last_rejection_by: str | None = None,
last_rejection_at: datetime | None = None,
) -> bool:
"""
INSERT or UPDATE trust record。
使用 PostgreSQL upsert 保證並行安全。
action_pattern 是主鍵,衝突時更新除 created_at 外的所有欄位。
"""
from src.utils.timezone import now_taipei
try:
async with get_db_context() as db:
stmt = pg_insert(TrustRecordDB).values(
action_pattern=action_pattern,
score=score,
total_approvals=total_approvals,
total_rejections=total_rejections,
last_approval_by=last_approval_by,
last_approval_at=last_approval_at,
last_rejection_by=last_rejection_by,
last_rejection_at=last_rejection_at,
created_at=now_taipei(),
updated_at=now_taipei(),
).on_conflict_do_update(
index_elements=["action_pattern"],
set_={
"score": score,
"total_approvals": total_approvals,
"total_rejections": total_rejections,
"last_approval_by": last_approval_by,
"last_approval_at": last_approval_at,
"last_rejection_by": last_rejection_by,
"last_rejection_at": last_rejection_at,
"updated_at": now_taipei(),
},
)
await db.execute(stmt)
logger.debug(
"trust_record_upserted",
action_pattern=action_pattern,
score=score,
)
return True
except Exception as e:
logger.error(
"trust_record_upsert_failed",
action_pattern=action_pattern,
error=str(e),
)
return False
async def load_all(self) -> list[dict]:
"""
載入所有 trust records供啟動 warm-up。
Returns:
list[dict] — 每筆含 action_pattern, score, total_approvals,
total_rejections, last_approval_by, last_approval_at,
last_rejection_by, last_rejection_at
"""
try:
async with get_db_context() as db:
result = await db.execute(select(TrustRecordDB))
rows = result.scalars().all()
records = [
{
"action_pattern": r.action_pattern,
"score": r.score,
"total_approvals": r.total_approvals,
"total_rejections": r.total_rejections,
"last_approval_by": r.last_approval_by,
"last_approval_at": r.last_approval_at,
"last_rejection_by": r.last_rejection_by,
"last_rejection_at": r.last_rejection_at,
}
for r in rows
]
logger.info("trust_records_loaded", count=len(records))
return records
except Exception as e:
logger.error("trust_records_load_failed", error=str(e))
return []
# =============================================================================
# Singleton
# =============================================================================
_repository: TrustRepository | None = None
def get_trust_repository() -> ITrustRepository:
"""取得 TrustRepository 單例"""
global _repository
if _repository is None:
_repository = TrustRepository()
return _repository