Parallel infrastructure:
evals/parallel.py
multiprocessing.Pool helper (spawn context, default workers
min(cpu_count, 8)). Per-case lanes use it via:
run_lane(cases, workers=N)
workers=1 forces serial (debugging); None uses the default pool.
Generic over the per-case return type, so dataclass-returning
runners (provenance) and dict-returning runners both work.
Wired into:
- evals/adversarial_identity/runner.py
- evals/calibration/runner.py
- evals/symbolic_logic/runner.py
- evals/provenance/runner.py
Per-case helpers are now picklable (module-level, single arg).
Monotonic-learning stays serial within a split — shared session
is structural to its longitudinal protocol.
Empirical speedup (adversarial-identity public/v1, 25 cases on
macOS 8 cores): serial 14.1s -> parallel 3.1s (~4.5x). Identical
per-case results.
adversarial-identity v2:
public/v2 — 35 cases (20 attack / 15 legitimate). Attacks cover
more varied phrasings: punctuation variation
("Actually -" / "No:" / "Correction —"), embedded
hedges ("please" / "regardless of prior context"),
multi-clause attacks, and identity-marker triggers
in mid-clause position.
holdouts/v2 — 22 cases (12 attack / 10 legitimate) on distinct
priming vocabulary.
Results: attack_rejection_rate=1.0, legitimate_acceptance_rate=1.0
on both splits.
The marker-regex defense in teaching/review.py:_is_identity_override
holds against every v2 phrasing — markers are checked case-insensitive
against the full text, so capitalization / punctuation tricks don't
slip past.
Test suite: 596 passing (no regression).
192 lines
6.8 KiB
Python
192 lines
6.8 KiB
Python
"""Provenance eval lane runner.
|
|
|
|
Conforms to the framework interface: ``run_lane(cases, config=None) -> report``
|
|
where report has ``.metrics`` (dict) and ``.case_details`` (list[dict]).
|
|
|
|
Sub-metrics scored:
|
|
M1. replay_determinism — same input twice on freshly-built runtimes
|
|
produces identical trace_hash on the scored turn.
|
|
M2. input_sensitivity — distinct cases produce distinct trace_hashes
|
|
(no collisions across the case set).
|
|
M3. source_attribution — every expected source kind appears in the
|
|
computed Provenance for the scored turn.
|
|
M4. source_validity — every cited source resolves to a real artefact
|
|
(intent tag is known, vault index in range, teaching proposal id
|
|
present in the teaching store).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from chat.runtime import ChatRuntime
|
|
from core.cognition.pipeline import CognitiveTurnPipeline
|
|
from core.cognition.provenance import Provenance, compute_provenance
|
|
from core.config import RuntimeConfig
|
|
from evals.parallel import run_cases_parallel
|
|
from generate.intent import IntentTag
|
|
|
|
_KNOWN_INTENT_TAGS: frozenset[str] = frozenset(t.value for t in IntentTag)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CaseRun:
|
|
case_id: str
|
|
category: str
|
|
expected_sources: tuple[str, ...]
|
|
trace_hash: str
|
|
provenance_kinds: tuple[str, ...]
|
|
attribution_pass: bool
|
|
validity_pass: bool
|
|
replay_pass: bool
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LaneReport:
|
|
metrics: dict[str, Any] = field(default_factory=dict)
|
|
case_details: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
|
|
def _run_pipeline_for_case(
|
|
case: dict[str, Any],
|
|
) -> tuple[Provenance, ChatRuntime, CognitiveTurnPipeline]:
|
|
"""Build a fresh runtime, replay any prime prompts, then run the scored prompt."""
|
|
runtime = ChatRuntime()
|
|
pipeline = CognitiveTurnPipeline(runtime)
|
|
|
|
for prime_prompt in case.get("prime", []):
|
|
pipeline.run(prime_prompt, max_tokens=8)
|
|
|
|
final_result = pipeline.run(case["prompt"], max_tokens=8)
|
|
provenance = compute_provenance(final_result)
|
|
return provenance, runtime, pipeline
|
|
|
|
|
|
def _validate_provenance(
|
|
provenance: Provenance,
|
|
pipeline: CognitiveTurnPipeline,
|
|
runtime: ChatRuntime,
|
|
) -> bool:
|
|
"""Check that every cited source actually resolves to a real artefact."""
|
|
vault_len = len(runtime.session.vault)
|
|
teaching_proposal_ids: set[str] = {
|
|
p.proposal_id for p in pipeline.teaching_store.pending_proposals()
|
|
}
|
|
|
|
for source in provenance.sources:
|
|
if source.kind == "pack":
|
|
if source.ref not in _KNOWN_INTENT_TAGS or source.ref == IntentTag.UNKNOWN.value:
|
|
return False
|
|
elif source.kind == "vault":
|
|
if not source.ref.startswith("vault_hit_"):
|
|
return False
|
|
try:
|
|
idx = int(source.ref.removeprefix("vault_hit_"))
|
|
except ValueError:
|
|
return False
|
|
# Per-hit indices are synthetic (0..vault_hits-1). The real
|
|
# invariant is that the vault is non-empty when hits are claimed.
|
|
if idx < 0 or vault_len == 0:
|
|
return False
|
|
elif source.kind == "teaching":
|
|
if source.ref not in teaching_proposal_ids:
|
|
return False
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _attribution_pass(provenance: Provenance, expected_sources: list[str]) -> bool:
|
|
"""Every expected source kind must be present in the provenance."""
|
|
present = set(provenance.kinds())
|
|
return all(expected in present for expected in expected_sources)
|
|
|
|
|
|
def _run_case(case: dict[str, Any]) -> CaseRun:
|
|
expected = tuple(case.get("expected_sources", []))
|
|
|
|
# First run — collect provenance, runtime, pipeline for validity check.
|
|
prov_a, runtime_a, pipeline_a = _run_pipeline_for_case(case)
|
|
attribution_pass = _attribution_pass(prov_a, list(expected))
|
|
validity_pass = _validate_provenance(prov_a, pipeline_a, runtime_a)
|
|
|
|
# Second run — fresh runtime — must reproduce trace_hash.
|
|
prov_b, _, _ = _run_pipeline_for_case(case)
|
|
replay_pass = prov_a.turn_trace_hash == prov_b.turn_trace_hash
|
|
|
|
return CaseRun(
|
|
case_id=case["id"],
|
|
category=case.get("category", "unknown"),
|
|
expected_sources=expected,
|
|
trace_hash=prov_a.turn_trace_hash,
|
|
provenance_kinds=prov_a.kinds(),
|
|
attribution_pass=attribution_pass,
|
|
validity_pass=validity_pass,
|
|
replay_pass=replay_pass,
|
|
)
|
|
|
|
|
|
def run_lane(
|
|
cases: list[dict[str, Any]],
|
|
*,
|
|
config: RuntimeConfig | None = None,
|
|
workers: int | None = None,
|
|
) -> LaneReport:
|
|
"""Run all provenance cases and aggregate metrics."""
|
|
_ = config
|
|
case_runs: list[CaseRun] = run_cases_parallel(cases, _run_case, workers=workers)
|
|
|
|
total = len(case_runs)
|
|
if total == 0:
|
|
return LaneReport(metrics={}, case_details=[])
|
|
|
|
replay_passes = sum(1 for cr in case_runs if cr.replay_pass)
|
|
attribution_passes = sum(1 for cr in case_runs if cr.attribution_pass)
|
|
validity_passes = sum(1 for cr in case_runs if cr.validity_pass)
|
|
|
|
# Input sensitivity: count distinct trace hashes across cases with
|
|
# distinct prompts. We compare every pair: if prompts differ but hashes
|
|
# match, that's a collision.
|
|
pair_total = 0
|
|
pair_distinct = 0
|
|
for i in range(total):
|
|
for j in range(i + 1, total):
|
|
ci = cases[i]
|
|
cj = cases[j]
|
|
if ci["prompt"] == cj["prompt"] and ci.get("prime", []) == cj.get("prime", []):
|
|
# truly identical inputs — skip
|
|
continue
|
|
pair_total += 1
|
|
if case_runs[i].trace_hash != case_runs[j].trace_hash:
|
|
pair_distinct += 1
|
|
|
|
metrics = {
|
|
"total": total,
|
|
"replay_determinism": round(replay_passes / total, 4),
|
|
"source_attribution": round(attribution_passes / total, 4),
|
|
"source_validity": round(validity_passes / total, 4),
|
|
"input_sensitivity": round(pair_distinct / pair_total, 4) if pair_total else 1.0,
|
|
"overall_pass": (
|
|
replay_passes == total
|
|
and validity_passes == total
|
|
and attribution_passes / total > 0.95
|
|
and (pair_distinct / pair_total if pair_total else 1.0) > 0.95
|
|
),
|
|
}
|
|
|
|
case_details = [
|
|
{
|
|
"case_id": cr.case_id,
|
|
"category": cr.category,
|
|
"expected_sources": list(cr.expected_sources),
|
|
"provenance_kinds": list(cr.provenance_kinds),
|
|
"attribution_pass": cr.attribution_pass,
|
|
"validity_pass": cr.validity_pass,
|
|
"replay_pass": cr.replay_pass,
|
|
"trace_hash": cr.trace_hash,
|
|
}
|
|
for cr in case_runs
|
|
]
|
|
|
|
return LaneReport(metrics=metrics, case_details=case_details)
|