Some checks failed
CI / test (push) Failing after 37s
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).
86 lines
2.8 KiB
Python
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]
|