fix(decision-logs): dedupe on (eval_id, decision_type, context) (resolves T8)
Some checks failed
CI / test (push) Failing after 37s
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).
This commit is contained in:
parent
6d32653675
commit
4bcab065f1
@ -3,6 +3,7 @@
|
|||||||
Pulled out of ``web/routers/intelligent_evals.py`` so the router only handles
|
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.
|
HTTP validation and error translation. The ORM writes and reads now live here.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
@ -10,6 +11,18 @@ from sqlmodel import Session, select
|
|||||||
from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalDecisionLogDB
|
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(
|
def create_decision_log(
|
||||||
eval_id: str,
|
eval_id: str,
|
||||||
decision_type: str,
|
decision_type: str,
|
||||||
@ -18,11 +31,32 @@ def create_decision_log(
|
|||||||
context: dict[str, Any],
|
context: dict[str, Any],
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Create a decision log entry. Raises ``LookupError`` if eval not found."""
|
"""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)
|
eval_db = session.get(IntelligentEvalDB, eval_id)
|
||||||
if eval_db is None:
|
if eval_db is None:
|
||||||
raise LookupError(f"intelligent eval {eval_id} not found")
|
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(
|
log = IntelligentEvalDecisionLogDB(
|
||||||
eval_id=eval_id,
|
eval_id=eval_id,
|
||||||
decision_type=decision_type,
|
decision_type=decision_type,
|
||||||
@ -33,16 +67,7 @@ def create_decision_log(
|
|||||||
session.add(log)
|
session.add(log)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(log)
|
session.refresh(log)
|
||||||
|
return _log_to_dict(log)
|
||||||
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 list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
||||||
@ -57,15 +82,4 @@ def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
|||||||
.order_by(IntelligentEvalDecisionLogDB.created_at.desc())
|
.order_by(IntelligentEvalDecisionLogDB.created_at.desc())
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return [
|
return [_log_to_dict(log) for log in logs]
|
||||||
{
|
|
||||||
"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,
|
|
||||||
}
|
|
||||||
for log in logs
|
|
||||||
]
|
|
||||||
|
|||||||
@ -107,15 +107,6 @@ def test_decision_log_is_append_only_on_context_change(
|
|||||||
assert rows[1].get_context() == {"slot": "10-12h", "deficit": 1}
|
assert rows[1].get_context() == {"slot": "10-12h", "deficit": 1}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
|
||||||
reason=(
|
|
||||||
"Known gap: decision-logs are appended on every call regardless of "
|
|
||||||
"(eval_id, decision_type, context) identity — i.e. no dedupe. The same "
|
|
||||||
"decision made twice produces two identical rows. Tracked in "
|
|
||||||
".scratch/v111-architecture-scan.md."
|
|
||||||
),
|
|
||||||
strict=False,
|
|
||||||
)
|
|
||||||
def test_decision_log_dedupes_identical_entries(
|
def test_decision_log_dedupes_identical_entries(
|
||||||
client: TestClient, db_session: Session
|
client: TestClient, db_session: Session
|
||||||
):
|
):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user