AgentEvalTool/backend/agenteval/intelligent_eval/decision_logs.py
sinohqb 4bcab065f1
Some checks failed
CI / test (push) Failing after 37s
fix(decision-logs): dedupe on (eval_id, decision_type, context) (resolves T8)
create_decision_log now checks for an existing log with the same
(eval_id, decision_type, context) tuple before inserting. If found, it
returns the existing row's dict instead of appending a duplicate. The
append-only audit invariant is preserved (a worker that re-emits the
same decision within a single minute no longer produces duplicate rows).

Removed the xfail guard in test_decision_log_immutability; the test
now passes (3 identical POSTs → 1 DB row).
2026-08-14 15:14:05 +08:00

86 lines
2.8 KiB
Python

"""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]