"""Decision-log service (P3 deepening, S2). Pulled out of ``web/routers/intelligent_evals.py`` so the router only handles HTTP validation and error translation. The ORM writes and reads now live here. """ import json from typing import Any from sqlmodel import Session, select from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalDecisionLogDB def _log_to_dict(log: IntelligentEvalDecisionLogDB) -> dict: return { "id": log.id, "eval_id": log.eval_id, "decision_type": log.decision_type, "reason": log.reason, "context": log.get_context(), "cron_id": log.cron_id, "created_at": log.created_at.isoformat() if log.created_at else None, } def create_decision_log( eval_id: str, decision_type: str, reason: str, cron_id: str, context: dict[str, Any], session: Session, ) -> dict: """Create a decision log entry, or return the existing one if the (eval_id, decision_type, context) tuple is already recorded. P3 真问题修复 (T8): the previous implementation appended a new row on every call, so the same worker re-emitting an identical decision during a single minute produced duplicate audit rows. Dedupe on the JSON representation of ``context`` keeps the table append-only and audit-clean. Raises ``LookupError`` if eval not found. """ eval_db = session.get(IntelligentEvalDB, eval_id) if eval_db is None: raise LookupError(f"intelligent eval {eval_id} not found") context_json = json.dumps(context, sort_keys=True) # Dedupe: same (eval, decision_type, context) → return existing. for existing in session.exec( select(IntelligentEvalDecisionLogDB).where( IntelligentEvalDecisionLogDB.eval_id == eval_id, IntelligentEvalDecisionLogDB.decision_type == decision_type, ) ).all(): if existing.get_context() == context: return _log_to_dict(existing) log = IntelligentEvalDecisionLogDB( eval_id=eval_id, decision_type=decision_type, reason=reason, cron_id=cron_id, ) log.set_context(context) session.add(log) session.commit() session.refresh(log) return _log_to_dict(log) def list_decision_logs(eval_id: str, session: Session) -> list[dict]: """List decision logs for an eval. Raises ``LookupError`` if eval not found.""" eval_db = session.get(IntelligentEvalDB, eval_id) if eval_db is None: raise LookupError(f"intelligent eval {eval_id} not found") logs = session.exec( select(IntelligentEvalDecisionLogDB) .where(IntelligentEvalDecisionLogDB.eval_id == eval_id) .order_by(IntelligentEvalDecisionLogDB.created_at.desc()) ).all() return [_log_to_dict(log) for log in logs]