feat(trust): ADR-088 Trust Score 持久化 — L4 自動放行核心
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m40s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m40s
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>
This commit is contained in:
@@ -32,8 +32,9 @@ import structlog
|
||||
|
||||
from src.models.approval import ApprovalRequest
|
||||
from src.models.incident import IncidentStatus
|
||||
from src.repositories.interfaces import ILearningRepository
|
||||
from src.repositories.interfaces import ILearningRepository, ITrustRepository
|
||||
from src.repositories.learning_repository import get_learning_repository
|
||||
from src.repositories.trust_repository import get_trust_repository
|
||||
from src.services.trust_engine import get_trust_manager
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -156,9 +157,11 @@ class LearningService:
|
||||
def __init__(
|
||||
self,
|
||||
repository: ILearningRepository | None = None,
|
||||
trust_repository: ITrustRepository | None = None,
|
||||
):
|
||||
self._trust_manager = get_trust_manager()
|
||||
self._repository = repository or get_learning_repository()
|
||||
self._trust_repo = trust_repository or get_trust_repository()
|
||||
|
||||
async def process_execution_result(
|
||||
self,
|
||||
@@ -200,6 +203,9 @@ class LearningService:
|
||||
)
|
||||
feedback_type = FeedbackType.EXECUTION_FAILURE
|
||||
|
||||
# ADR-088: 持久化信任分數到 PostgreSQL (Pod 重啟後不歸零)
|
||||
await self._persist_trust(action_pattern)
|
||||
|
||||
# 取得更新後的信任分數
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_after = trust_record.score if trust_record else 0
|
||||
@@ -325,6 +331,9 @@ class LearningService:
|
||||
)
|
||||
playbook_updated = await self._demote_playbook(feedback.incident_id)
|
||||
|
||||
# ADR-088: 持久化信任分數到 PostgreSQL (Pod 重啟後不歸零)
|
||||
await self._persist_trust(action_pattern)
|
||||
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_after = trust_record.score if trust_record else 0
|
||||
|
||||
@@ -955,6 +964,36 @@ class LearningService:
|
||||
"learning_status": learning_status,
|
||||
}
|
||||
|
||||
async def _persist_trust(self, action_pattern: str) -> None:
|
||||
"""
|
||||
将内存中的 TrustRecord 持久化到 PostgreSQL。
|
||||
|
||||
ADR-088: 每次 approve/reject 後同步寫入 DB,
|
||||
確保 Pod 重啟後信任分數不歸零。
|
||||
|
||||
2026-04-17 ogt + Claude Sonnet 4.6(亞太): Phase 4 信任持久化
|
||||
"""
|
||||
record = self._trust_manager.get_trust_record(action_pattern)
|
||||
if not record:
|
||||
return
|
||||
try:
|
||||
await self._trust_repo.upsert(
|
||||
action_pattern=action_pattern,
|
||||
score=record.score,
|
||||
total_approvals=record.total_approvals,
|
||||
total_rejections=record.total_rejections,
|
||||
last_approval_by=record.last_approval_by,
|
||||
last_approval_at=record.last_approval_at,
|
||||
last_rejection_by=record.last_rejection_by,
|
||||
last_rejection_at=record.last_rejection_at,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"trust_persist_failed",
|
||||
action_pattern=action_pattern,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
def _get_action_tier(self, action: str) -> int:
|
||||
"""取得動作的 Tier"""
|
||||
tier_actions = {
|
||||
|
||||
@@ -362,6 +362,46 @@ class TrustScoreManager:
|
||||
record.score = 0
|
||||
logger.warning("[TrustEngine] All trust scores reset!")
|
||||
|
||||
def bulk_load(self, records: list[dict]) -> int:
|
||||
"""
|
||||
從 DB 批量載入 trust records 到記憶體(啟動 warm-up 用)。
|
||||
|
||||
ADR-088: Pod 重啟後從 PostgreSQL 恢復信任分數,
|
||||
確保 AI 不會因重啟而失憶歸零。
|
||||
|
||||
Args:
|
||||
records: list[dict] — 每筆含 action_pattern, score,
|
||||
total_approvals, total_rejections,
|
||||
last_approval_by, last_approval_at,
|
||||
last_rejection_by, last_rejection_at
|
||||
|
||||
Returns:
|
||||
int: 載入筆數
|
||||
|
||||
2026-04-17 ogt + Claude Sonnet 4.6(亞太): ADR-088
|
||||
"""
|
||||
loaded = 0
|
||||
for r in records:
|
||||
pattern = r.get("action_pattern")
|
||||
if not pattern:
|
||||
continue
|
||||
record = TrustRecord(
|
||||
action_pattern=pattern,
|
||||
score=r.get("score", 0),
|
||||
total_approvals=r.get("total_approvals", 0),
|
||||
total_rejections=r.get("total_rejections", 0),
|
||||
last_approval_by=r.get("last_approval_by"),
|
||||
last_approval_at=r.get("last_approval_at"),
|
||||
last_rejection_by=r.get("last_rejection_by"),
|
||||
last_rejection_at=r.get("last_rejection_at"),
|
||||
)
|
||||
self._records[pattern] = record
|
||||
loaded += 1
|
||||
|
||||
if loaded:
|
||||
logger.info(f"[TrustEngine] Warm-up: loaded {loaded} trust records from DB")
|
||||
return loaded
|
||||
|
||||
|
||||
# ==================== Pattern Matching Utilities ====================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user