161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
"""
|
|
Agent Replay Contract Validator
|
|
===============================
|
|
|
|
Validates that candidate replay outputs line up with candidate-visible replay
|
|
inputs before they are normalized and scored.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from src.services.agent_replay_normalizer import CandidateReplayResult
|
|
|
|
LABEL_LEAK_KEYS = {
|
|
"evaluation_labels",
|
|
"verification_result",
|
|
"execution_success",
|
|
"execution_error",
|
|
"self_healing_score",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AgentReplayContractReport:
|
|
"""Validation result for one candidate replay output batch."""
|
|
|
|
candidate_id: str | None
|
|
inputs: int
|
|
results: int
|
|
valid: bool
|
|
failures: list[str] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": "agent_replay_contract_report_v1",
|
|
"candidate_id": self.candidate_id,
|
|
"inputs": self.inputs,
|
|
"results": self.results,
|
|
"valid": self.valid,
|
|
"failures": list(self.failures),
|
|
}
|
|
|
|
|
|
def validate_candidate_replay_contract(
|
|
*,
|
|
candidate_inputs: list[dict[str, Any]],
|
|
candidate_results: list[dict[str, Any]],
|
|
expected_candidate_id: str | None = None,
|
|
) -> AgentReplayContractReport:
|
|
"""Validate result/input one-to-one alignment and answer-key isolation."""
|
|
failures: list[str] = []
|
|
input_index = _index_inputs(candidate_inputs, failures)
|
|
result_index = _index_results(candidate_results, failures)
|
|
|
|
input_ids = set(input_index)
|
|
result_ids = set(result_index)
|
|
missing = sorted(input_ids - result_ids)
|
|
extra = sorted(result_ids - input_ids)
|
|
if missing:
|
|
failures.append(f"missing_results:{','.join(missing)}")
|
|
if extra:
|
|
failures.append(f"unexpected_results:{','.join(extra)}")
|
|
|
|
candidate_ids = {
|
|
result.candidate_id
|
|
for result in result_index.values()
|
|
if result.candidate_id
|
|
}
|
|
if expected_candidate_id and candidate_ids != {expected_candidate_id}:
|
|
failures.append(
|
|
"candidate_id_mismatch:"
|
|
f"expected={expected_candidate_id};actual={','.join(sorted(candidate_ids))}"
|
|
)
|
|
elif not expected_candidate_id and len(candidate_ids) > 1:
|
|
failures.append(f"multiple_candidate_ids:{','.join(sorted(candidate_ids))}")
|
|
|
|
for incident_id in sorted(input_ids & result_ids):
|
|
expected_run_id = str(input_index[incident_id].get("run_id", ""))
|
|
actual_run_id = result_index[incident_id].run_id
|
|
if expected_run_id != actual_run_id:
|
|
failures.append(
|
|
f"run_id_mismatch:{incident_id}:expected={expected_run_id};actual={actual_run_id}"
|
|
)
|
|
|
|
for line_number, payload in enumerate(candidate_results, start=1):
|
|
leaked = sorted(_find_label_leaks(payload))
|
|
if leaked:
|
|
failures.append(
|
|
f"label_leak:result_line_{line_number}:{','.join(leaked)}"
|
|
)
|
|
|
|
candidate_id = expected_candidate_id
|
|
if candidate_id is None and len(candidate_ids) == 1:
|
|
candidate_id = next(iter(candidate_ids))
|
|
|
|
return AgentReplayContractReport(
|
|
candidate_id=candidate_id,
|
|
inputs=len(candidate_inputs),
|
|
results=len(candidate_results),
|
|
valid=not failures,
|
|
failures=failures,
|
|
)
|
|
|
|
|
|
def _index_inputs(
|
|
candidate_inputs: list[dict[str, Any]],
|
|
failures: list[str],
|
|
) -> dict[str, dict[str, Any]]:
|
|
indexed: dict[str, dict[str, Any]] = {}
|
|
for line_number, payload in enumerate(candidate_inputs, start=1):
|
|
incident_id = str(payload.get("incident_id", "")).strip()
|
|
run_id = str(payload.get("run_id", "")).strip()
|
|
if not incident_id or not run_id:
|
|
failures.append(f"invalid_input:line_{line_number}:missing_incident_or_run_id")
|
|
continue
|
|
if incident_id in indexed:
|
|
failures.append(f"duplicate_input:{incident_id}")
|
|
continue
|
|
indexed[incident_id] = payload
|
|
return indexed
|
|
|
|
|
|
def _index_results(
|
|
candidate_results: list[dict[str, Any]],
|
|
failures: list[str],
|
|
) -> dict[str, CandidateReplayResult]:
|
|
indexed: dict[str, CandidateReplayResult] = {}
|
|
for line_number, payload in enumerate(candidate_results, start=1):
|
|
try:
|
|
result = CandidateReplayResult.from_dict(payload)
|
|
except Exception as exc:
|
|
failures.append(f"invalid_result:line_{line_number}:{exc}")
|
|
continue
|
|
if result.incident_id in indexed:
|
|
failures.append(f"duplicate_result:{result.incident_id}")
|
|
continue
|
|
indexed[result.incident_id] = result
|
|
return indexed
|
|
|
|
|
|
def _find_label_leaks(
|
|
value: Any,
|
|
*,
|
|
prefix: str = "",
|
|
) -> set[str]:
|
|
found: set[str] = set()
|
|
if isinstance(value, dict):
|
|
for key, nested in value.items():
|
|
key_text = str(key)
|
|
path = f"{prefix}.{key_text}" if prefix else key_text
|
|
if key_text in LABEL_LEAK_KEYS:
|
|
found.add(path)
|
|
found.update(_find_label_leaks(nested, prefix=path))
|
|
elif isinstance(value, list):
|
|
for index, nested in enumerate(value):
|
|
path = f"{prefix}[{index}]"
|
|
found.update(_find_label_leaks(nested, prefix=path))
|
|
return found
|