agentsight-api / src /training /run_test.py
Minato Namikaze
Deploy to Hugging Face Spaces
2aed081
Raw
History Blame Contribute Delete
7.21 kB
"""
Final test-set evaluation.
RULES
─────
1. Run this script EXACTLY ONCE β€” the run that produces the numbers for the paper.
2. Before running, verify the SHA-256 of data/splits/test.json matches the seal below.
3. Load the best threshold from best_agentsight_meta.json β€” do NOT re-tune on test.
SHA-256 seal for data/splits/test.json:
9604aae8eb5aec4ae666cfbe3053910f0570a807a4fa5515223dbca1aa66a7d8
"""
import os
import sys
import json
import hashlib
import torch
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.join(script_dir, "..", "..")
sys.path.insert(0, project_root)
from src.models.agentsight import AgentSightModel
from src.data.preprocessor import StepPreprocessor
from src.training.evaluate import evaluate, step_localization_accuracy
TEST_SHA256 = "9604aae8eb5aec4ae666cfbe3053910f0570a807a4fa5515223dbca1aa66a7d8"
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def generate_test_predictions():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Device: {device}")
# ── 1. Hash verification (integrity seal) ────────────────────────────────
test_file = os.path.join(project_root, "data", "splits", "test.json")
actual_hash = sha256_file(test_file)
if actual_hash != TEST_SHA256:
raise RuntimeError(
f"TEST SET HASH MISMATCH!\n"
f" Expected : {TEST_SHA256}\n"
f" Got : {actual_hash}\n"
"The test file has been modified β€” this run is invalid."
)
print(f"[βœ“] Test set hash verified: {actual_hash[:16]}…")
# ── 2. Load model ─────────────────────────────────────────────────────────
weights_path = os.path.join(project_root, "src", "models", "best_agentsight.pth")
meta_path = weights_path.replace(".pth", "_meta.json")
threshold = 0.5 # fallback
if os.path.exists(meta_path):
with open(meta_path) as f:
meta = json.load(f)
threshold = meta.get("threshold", 0.5)
print(f"Loaded threshold from meta: {threshold:.3f} "
f"(val step-acc={meta.get('val_step_acc',0)*100:.1f}%, "
f"val F1={meta.get('val_f1',0)*100:.1f}%)")
else:
print(f"Warning: no _meta.json found β€” using default threshold {threshold}")
preprocessor = StepPreprocessor()
model = AgentSightModel()
model.load_state_dict(torch.load(weights_path, map_location=device))
model.to(device)
model.eval()
# ── 3. Load test data ─────────────────────────────────────────────────────
with open(test_file) as f:
test_samples = json.load(f)
print(f"Loaded {len(test_samples)} test trajectories.")
# ── 4. Per-trajectory predictions ────────────────────────────────────────
predictions_dump = []
with torch.no_grad():
for sample in test_samples:
is_hal_true = sample.get("is_hallucination", False)
if isinstance(is_hal_true, str):
is_hal_true = is_hal_true.lower() == "true"
true_step = sample.get("hallucination_step")
if true_step is not None and is_hal_true:
true_step = int(true_step)
else:
true_step = None
try:
steps_enc = preprocessor.encode_trajectory(sample)
except Exception:
steps_enc = []
if not steps_enc:
predictions_dump.append({
"trajectory_id": sample.get("model_id", "unknown"),
"domain": sample.get("question_domain", "unknown"),
"true_is_hallucination": is_hal_true,
"true_hallucination_step": true_step,
"pred_is_hallucination": False,
"pred_hallucination_step": None,
"max_probability": 0.0,
"encoding_failed": True,
})
continue
ids = torch.stack([s["encoding"]["input_ids"].squeeze(0) for s in steps_enc]).to(device)
mask = torch.stack([s["encoding"]["attention_mask"].squeeze(0) for s in steps_enc]).to(device)
vocab_size = model.encoder.config.vocab_size
ids = torch.clamp(ids, 0, vocab_size - 1)
logits = model(ids, mask)
probs = torch.sigmoid(logits).cpu().tolist()
if isinstance(probs, float):
probs = [probs]
max_prob = max(probs)
max_idx = probs.index(max_prob)
pred_is_hal = max_prob > threshold
pred_step = steps_enc[max_idx]["step_idx"] if pred_is_hal else None
predictions_dump.append({
"trajectory_id": sample.get("model_id", "unknown"),
"domain": sample.get("question_domain", "unknown"),
"true_is_hallucination": is_hal_true,
"true_hallucination_step": true_step,
"pred_is_hallucination": pred_is_hal,
"pred_hallucination_step": pred_step,
"max_probability": max_prob,
"encoding_failed": False,
})
out_file = os.path.join(project_root, "test_predictions.json")
with open(out_file, "w") as f:
json.dump(predictions_dump, f, indent=4)
print(f"Saved detailed predictions β†’ {out_file}")
# ── 5. Formal evaluation ──────────────────────────────────────────────────
metrics = evaluate(model, test_samples, preprocessor, threshold=threshold)
print("\n" + "=" * 58)
print(" FINAL TEST SET METRICS (AgentHallu benchmark)")
print("=" * 58)
print(f" Step Localization Acc : {metrics['step_acc']*100:.1f}%")
print(f" Judgment Macro-F1 : {metrics['judgment_f1']*100:.1f}%")
print(f" Judgment Precision : {metrics['judgment_precision']*100:.1f}%")
print(f" Judgment Recall : {metrics['judgment_recall']*100:.1f}%")
print(f" Decision Threshold : {threshold:.3f}")
print(f" N test samples : {metrics['n_samples']}")
print("=" * 58)
print("\n Reference baselines (AgentHallu paper):")
print(" Random β€” F1: 48.5%, Step-Acc: 8.7%")
print(" Open-source avg β€” F1: 45.8%, Step-Acc: 10.9%")
print(" Gemini-2.5-Pro β€” F1: 64.6%, Step-Acc: 41.1%")
print(" GPT-5 β€” F1: 70.2%, Step-Acc: n/a")
print("=" * 58)
if __name__ == "__main__":
generate_test_predictions()