mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(memory): add Jev pre-screening and signal classification over a shared TypeSafe client - refactor(typesafe): extract a shared Jev (TypeSafe) client and unify its configuration: one connection-precedence rule, credential fingerprint, wire-size counting and error taxonomy, shared by the guardrail gate and the memory hooks. - feat(memory): wire `memory.prescreen` and `memory.signal_classification` into DeerMem through one injected judge hook (both off by default, `enforce` gated by the shadow evaluation). - config: add the top-level `typesafe:` block and the two judging slots to `config.example.yaml` and the Helm values, bump `config_version` to 50, and sync the Helm README example with it. - test(memory): add the shadow-evaluation script (`eval_memory_prescreen.py`) with per-stratum gates, censoring and the no-network baseline. Review follow-ups folded in: - `mutations_accepted` counts only facts that survive capacity enforcement and the upload-event scrub that precedes persistence, only removals/reinforcements of ids that existed, and persisted near-duplicate merges (matched by id, or by identity for legacy id-less facts); it is published only after persistence succeeds. - The cached manager hot-reloads its judge when a memory slot or the shared `typesafe` block changes (including a credential rotation); publication is serialized and revalidates the config generation, so a build started before an `enforce` -> `off` rollback cannot land after it. - Cache provenance is per answer: the `cached` flag and the reported model follow the answers a side actually consumed, across partial merges and answerless retries. - An oversized noul integer stays a per-question error instead of raising. - Incremental call savings use the shared scored denominator. - The sensitive-stratum gate requires evidence for each of identity, preference and correction. * fix(typesafe): resolve one credential per layer and refuse unusable connection values Review follow-ups on the shared TypeSafe client (#5906): - A consumer's `api_key_env` is no longer overridden by a literal `api_key` in the shared `typesafe:` block: the first layer that sets either credential setting decides it, and inside one layer the literal key wins (the standalone semantics). A consumer whose endpoint does not accept the block's key no longer sends it on every call, and the missing-credential message keeps naming the variable that layer configured. - `base_url` is validated where it is configured: `http(s)` only, with no query, fragment, embedded credentials, whitespace or control characters, and a malformed port fails at construction instead of on every request (httpx raises `UnsupportedProtocol` / `InvalidURL` per call, and the query would have been swallowed into the appended endpoint path). - A credential that cannot travel as a header value — surrounding whitespace from a secret mounted from a file, a non-printable character — is refused when the connection resolves. It used to construct fine and then put the whole `Bearer <key>` value into h11's `LocalProtocolError` message, which the guardrail middleware's `logger.exception` prints. - The client raises transport failures without chaining the original exception, so that message can no longer carry the credential through the cause chain; the exception type stays in the message for diagnosis. - `TypeSafeConfig` validates in strict mode, agreeing with the consumer `config` path: `max_attempts: true` was silently read as `1`. An integer stays a usable float, so `timeout: 5` keeps working. Tests: 20 cases across `tests/test_typesafe_config.py` and `tests/test_typesafe_client.py`, each red on the previous revision and green now. 367 focused typesafe/memory/config tests, `make test-blocking-io` (167) and the eval-script suites (50) pass; `ruff check` and `ruff format --check` are clean. Docs synced: `backend/docs/GUARDRAILS.md`, `typesafe/AGENTS.md`, `config.example.yaml`. * fix(typesafe,memory): bound the reply, record request failures, gate enforce on the evidence Second review round on #5906: the remaining items from the review thread and from the follow-up pass. Typesafe client (the reply is treated as hostile input too): - Send `Accept-Encoding: identity`, refuse a body that carries a non-identity `Content-Encoding`, and read through a bounded `iter_bytes` that stops past `MAX_RESPONSE_BYTES` (64 KiB). A gzip body that expands to 128 MiB used to be decoded in full and, on the gate path, allowed the tool call unexamined; it is now `invalid_response` (a denial with `fail_closed: true`). - Map `RecursionError` from `json.loads` to `invalid_response`: deep nesting escaped the module's taxonomy, so the memory consumers lost the round's record instead of recording a fallback. Memory judge: - A request-level failure is recorded as `request_failed`, never `no_verdict` (`REASON_REQUEST_FAILED` was defined and never emitted, so an unreachable endpoint was indistinguishable from a provider with no usable answer). The adapters' `decide()` now propagates `TypeSafeError` for the coordinator to record and log; `no_verdict` stays for a question-level failure, and an ineligible side keeps its own reason even when the round's request failed. Shadow evaluation (the enablement gate reads the evidence, not one number): - The miss-rate gate requires the exact one-sided Clopper-Pearson upper bound to meet the 1% target; a sample that only meets the point estimate is `INSUFFICIENT` (0/250 was reported as a pass with a 1.19% bound). - New `baseline_incremental_savings` gate: a baseline that saves as many calls as the model — the shipped all-trivial sample — fails instead of enabling. - Negative counts (`mutations_accepted`, message / batch / token counts) are treated as absent rather than scored: `-3` read as "nothing lost", adding a phantom saving to the numerator and the denominator. Docs: the design documents cited but absent from the repository are replaced with in-repo pointers (`MEMORY_IMPROVEMENTS.md`, `agents/memory/AGENTS.md`, `GUARDRAILS.md`), and the gate and limit wording now matches what the code enforces. Tests: 31 new or updated cases, each red on the previous revision (compressed body refused even when it decodes to a valid envelope, body over the cap refused, body at the cap answered, deep nesting mapped, `request_failed` on each request-level failure, the bound and baseline gates, the negative counter). 454 focused tests, `make test-blocking-io` (167) and the eval suites pass; `ruff check` and `ruff format --check` are clean. * fix(memory-eval): pair the baseline comparison on the same records Review follow-up on the incremental-savings gate added in ed930478 (P2): both sides of the comparison are now the records the no-network baseline can score (`trivial_only` plus a readable signals list). Subtracting that subset's heuristic savings from model savings over *all* scorable skips credited the model with batches the baseline never saw: 300 reviewed skips with a single annotated record computed 300-1=299 "incremental", passed the gate and set `enable_recommended`, while the paired comparison was model=1 against heuristic=1 (incremental 0). - `Evidence` carries the paired numbers (`baseline_paired_skips`, `baseline_model_saved_calls`, `baseline_heuristic_saved_calls`) instead of one heuristic count, filled by `build_report` from the baseline summary. - The gate reports `INSUFFICIENT` when no scored skip carries the annotation and when the annotation covers only a subset of the scored skips (the message carries the paired counts and the `paired of scored` coverage), `FAIL` on a non-positive paired gain, and `PASS` only on a positive gain over the full paired set. - Docs state the pairing/coverage requirement in `backend/docs/MEMORY_IMPROVEMENTS.md` and in the script docstring. Tests: the reviewer's counterexample as a report-level regression (it failed with `assert 'PASS' == 'INSUFFICIENT'` on ed930478 while `enable_recommended` was true), a unit case for the paired-subset `INSUFFICIENT`, and the renamed paired fields. 344 focused tests, `make test-blocking-io` (167) and the eval suites pass; `ruff check` / `ruff format --check` clean. * fix(typesafe): bound the sync body read by the deadline `deadline_seconds` only covered the sync path's headers, the completed body and the parse, so an endpoint that drips bytes just under the read timeout held the caller for one read per piece — the whole of `MAX_RESPONSE_BYTES` past the budget. The read loop now checks the budget before every transport read, which bounds the overshoot to one read. The pieces are the transport's own reads, not re-chunked output: `iter_bytes(chunk_size)` pulls many reads inside a single yielded chunk, so a check at that granularity would never see a drip. `tests/test_typesafe_client.py` pins it with a valid envelope delivered as 24 pieces of 50 ms against a 0.2 s deadline: red before (24 pieces pulled), green now (~5), on both the sync and async paths. Docstring and `typesafe/AGENTS.md` track the new granularity. * fix(memory): read the YAML off spelling, and veto a skip over the whole batch Two review findings on the memory judging path. `memory.prescreen.mode` / `memory.signal_classification.mode` rejected the `mode: off` spelling `config.example.yaml` ships and a rollback types: YAML 1.1 parses the unquoted value as a boolean, so both fields saw `False`. The failed reload left the previous judge running, so both fields now normalize `False` to `off` before the `Literal` runs. `True` stays a validation error (no mode it could mean). The examples quote the value, and the docs record why. The pre-screen's L3 veto read `detect_signals`' 6-message window while the batch it answers for is the whole post-watermark feed, so an explicit correction in an earlier turn of an eight-message batch did not stop the skip that then advanced the watermark past it. `detect_signals` gains a `window` parameter (default unchanged) and the updater scans the full feed for the veto; the extraction hint and the reinforcement gate keep the window. Both regressions are red on the previous head: the config test reproduced the `input_value=False` rejection through `AppConfig.from_file`, and the batch test reproduced the prescreen judging (and skipping) a batch whose first turn carries the correction.
1090 lines
54 KiB
Python
1090 lines
54 KiB
Python
"""Shadow evaluation of the memory pre-screen (Jev) from DeerMem extraction records.
|
|
|
|
This is the pre-``enforce`` evidence for ``TypeSafeMemoryPrescreen``: the gates
|
|
documented in ``backend/docs/MEMORY_IMPROVEMENTS.md`` ("Before ``enforce``") and in
|
|
``agents/memory/AGENTS.md``. It is an operator run over **shadow records**, not a CI
|
|
test: in ``mode: shadow`` the extraction call always runs, so every pre-screen
|
|
verdict comes with free ground truth — that same batch's extraction outcome. The
|
|
script makes no network call, reads no credential and prints none; its input is a
|
|
JSONL of DeerMem ``extraction_callback`` payloads, one per line.
|
|
|
|
What one line must contain (the payload the host already emits)
|
|
----------------------------------------------------------------
|
|
|
|
* ``prescreen`` — ``mode``/``verdict``/``probability``/``skip_threshold``/``model``/
|
|
``cached``/``digest``/``signals``/``message_count``/``batch_chars``/
|
|
``duration_ms``/``fallback_reason`` (the judge record, design §5);
|
|
* ``success`` and ``token_usage`` — the extraction call's own record;
|
|
* ``mutations_accepted`` — the pre-screen's "worth remembering" signal, written
|
|
when the apply ran. ``> 0`` on a ``skip`` verdict means the skip would have lost
|
|
a memory.
|
|
|
|
Fields the collector must add (absent from the production payload today)
|
|
-----------------------------------------------------------------------
|
|
|
|
* ``trivial_only`` (bool) — the batch's turns were all trivial (``filter_trivial``).
|
|
The mandatory no-network baseline cannot be computed without it; coverage is
|
|
reported instead of assumed.
|
|
* ``reviewed`` (bool) and optionally ``review_outcome`` (``worth_remembering`` |
|
|
``none_worth_remembering``) — the manual review that §5 gate 3 counts *and*
|
|
whose verdict that gate reads: the review must confirm none of the reviewed
|
|
skips was worth remembering, so a reviewed skip whose outcome (or
|
|
``mutations_accepted`` counter) says otherwise fails the gate. It is also a
|
|
second ground-truth witness for a skip.
|
|
* ``stratum`` (string or list) / ``labels`` (list) — the §5 gate 2 subset, one of
|
|
``identity`` / ``preference`` / ``correction``. A human label, because L3 keeps
|
|
signal-bearing batches out of the network population.
|
|
* ``cost_usd`` (float, optional) — the record's billed cost, when known.
|
|
|
|
Method, and the things it deliberately refuses to do
|
|
----------------------------------------------------
|
|
|
|
* Every record is classified into exactly one population: **network verdicts** (a
|
|
request was really sent), **cache hits** (the same ``digest`` reused), **local
|
|
fallbacks** (L3 / L4 / L5 / L7 / L8 or a failure fallback) and **unjudged** (no
|
|
``prescreen`` record was emitted at all: no side was enabled, or the round was
|
|
forbidden to judge). Only network verdicts are
|
|
classifier samples; mixing populations would let a cache hit or a local refusal
|
|
vote on the model's accuracy.
|
|
* ``saved_call_rate`` (a skip that kept nothing, i.e. the call was really saved)
|
|
and ``miss_rate`` (a skip that lost a memory) share the scored-skip denominator
|
|
and therefore sum to 1; ``saved_call_rate_over_all_verdicts`` keeps the
|
|
network-verdict denominator for the "how many calls did this save" reading.
|
|
* A record whose outcome is unknown (skip without ``mutations_accepted``, an
|
|
extraction that failed before the apply) is **censored**: counted, reasoned
|
|
about and excluded from the rates. It is never dropped to claim a pass.
|
|
* The no-network heuristic baseline is run over the same scored records as the
|
|
model, so both rates have one denominator: the baseline skips a batch when
|
|
``signals`` is empty and ``trivial_only`` is true. If it already captures most
|
|
of the saving, a third-party egress path is not justified. §5 gate 4 needs it
|
|
*and* the recorded tokens and p50/p95 verdict latency: recorded calls alone are
|
|
not the evidence the gate asks for, so missing any of them is ``INSUFFICIENT``
|
|
rather than a pass. A baseline that saves as many calls as the model — which the
|
|
shipped "everything trivial" sample does — is a ``FAIL``, not an enabling run, and
|
|
the comparison must cover the scored skips: both sides are the records the baseline
|
|
can actually score (``trivial_only`` plus a readable signals list), so a run that
|
|
annotates only a subset is ``INSUFFICIENT`` instead of crediting the model with
|
|
batches the heuristic never saw.
|
|
* The miss-rate bound is the exact one-sided **Clopper-Pearson** interval,
|
|
computed here from the regularized incomplete beta function by bisection. No
|
|
normal approximation is used anywhere, and the gate reads the *bound*, not the
|
|
point estimate: a sample whose observed rate is at or below the target while its
|
|
upper bound is above it is ``INSUFFICIENT`` — the evidence does not establish the
|
|
target yet, so the answer is to collect more skips, not to enable.
|
|
* A count field that arrives negative (``mutations_accepted``, ``message_count``,
|
|
``batch_chars``, a token count) is treated as absent: a count cannot be negative,
|
|
and scoring a corrupt one as a saving would tilt every rate toward a pass.
|
|
* The exit code carries the verdict: ``0`` only when every gate is ``PASS``,
|
|
``1`` when any gate is ``FAIL`` or ``INSUFFICIENT`` — so an operator can gate
|
|
enablement on ``python scripts/eval_memory_prescreen.py --records ... && <enable>``.
|
|
|
|
Run from the ``backend/`` directory::
|
|
|
|
PYTHONPATH=. uv run python scripts/eval_memory_prescreen.py --records /tmp/shadow.jsonl --json /tmp/prescreen-report.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import statistics
|
|
from collections import Counter
|
|
from collections.abc import Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
NETWORK = "network"
|
|
CACHE_HIT = "cache_hit"
|
|
LOCAL_FALLBACK = "local_fallback"
|
|
UNJUDGED = "unjudged"
|
|
#: Every input record lands in exactly one of these.
|
|
POPULATIONS: tuple[str, ...] = (NETWORK, CACHE_HIT, LOCAL_FALLBACK, UNJUDGED)
|
|
#: The three populations design §5 defines; ``UNJUDGED`` is the coverage remainder.
|
|
DESIGN_POPULATIONS: tuple[str, ...] = (NETWORK, CACHE_HIT, LOCAL_FALLBACK)
|
|
|
|
VERDICT_SKIP = "skip"
|
|
VERDICT_EXTRACT = "extract"
|
|
|
|
PASS = "PASS"
|
|
FAIL = "FAIL"
|
|
INSUFFICIENT = "INSUFFICIENT"
|
|
|
|
REVIEW_WORTH_REMEMBERING = "worth_remembering"
|
|
REVIEW_NONE_WORTH_REMEMBERING = "none_worth_remembering"
|
|
REVIEW_OUTCOMES: tuple[str, ...] = (REVIEW_WORTH_REMEMBERING, REVIEW_NONE_WORTH_REMEMBERING)
|
|
|
|
#: The §5 gate 2 subset. Labels are collector-supplied human labels.
|
|
SENSITIVE_LABELS = frozenset({"identity", "preference", "correction"})
|
|
|
|
#: The coordinator's fallback reason -> the design lock it discharges.
|
|
FALLBACK_LOCKS: dict[str, str] = {
|
|
"deterministic_signals": "L3",
|
|
"emergency_flush": "L4",
|
|
"over_limit": "L5",
|
|
"shutdown_drain": "L7",
|
|
"staleness_or_consolidation": "L8",
|
|
"request_failed": "failure",
|
|
"no_verdict": "failure",
|
|
"disabled": "off",
|
|
}
|
|
UNRECORDED_REASON = "(unrecorded)"
|
|
|
|
CENSORED_EXTRACTION_FAILED = "extraction_failed"
|
|
CENSORED_OUTCOME_NOT_RECORDED = "outcome_not_recorded"
|
|
|
|
DEFAULT_MISS_TARGET = 0.01
|
|
DEFAULT_CONFIDENCE = 0.95
|
|
DEFAULT_MIN_REVIEWED_SKIPS = 200
|
|
|
|
_BETA_MAX_ITERATIONS = 300
|
|
_BETA_EPSILON = 3e-16
|
|
_BETA_TINY = 1e-300
|
|
_BETA_BISECTIONS = 200
|
|
|
|
|
|
def _as_count(value: Any) -> int | None:
|
|
"""A count, refusing ``bool`` and negatives.
|
|
|
|
``True`` is not a count of mutations, and a negative count is a corrupt line: the
|
|
record must lose the field rather than be scored as a saving (a negative
|
|
``mutations_accepted`` used to read as "no loss", which diluted the miss-rate
|
|
denominator and inflated ``saved_calls``).
|
|
"""
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
return None
|
|
return value
|
|
|
|
|
|
def _as_float(value: Any) -> float | None:
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
return None
|
|
number = float(value)
|
|
return number if math.isfinite(number) else None
|
|
|
|
|
|
def _as_bool(value: Any) -> bool | None:
|
|
return value if isinstance(value, bool) else None
|
|
|
|
|
|
def _first_count(mapping: Mapping[str, Any], keys: Sequence[str]) -> int | None:
|
|
for key in keys:
|
|
value = _as_count(mapping.get(key))
|
|
if value is not None:
|
|
return value
|
|
return None
|
|
|
|
|
|
def _token_counts(usage: Any) -> tuple[int | None, int | None, int | None]:
|
|
"""Read input / output / total tokens from a LangChain-style ``usage_metadata``."""
|
|
if not isinstance(usage, Mapping):
|
|
return (None, None, None)
|
|
input_tokens = _first_count(usage, ("input_tokens", "prompt_tokens"))
|
|
output_tokens = _first_count(usage, ("output_tokens", "completion_tokens"))
|
|
total = _as_count(usage.get("total_tokens"))
|
|
if total is None and (input_tokens is not None or output_tokens is not None):
|
|
total = (input_tokens or 0) + (output_tokens or 0)
|
|
return (input_tokens, output_tokens, total)
|
|
|
|
|
|
def _normalize_review_outcome(value: Any) -> str | None:
|
|
if not isinstance(value, str):
|
|
return None
|
|
text = value.strip().lower()
|
|
return text if text in REVIEW_OUTCOMES else None
|
|
|
|
|
|
def _sensitive_labels(raw: Mapping[str, Any]) -> frozenset[str]:
|
|
"""The record's ``identity`` / ``preference`` / ``correction`` labels, normalised."""
|
|
found: set[str] = set()
|
|
for key in ("stratum", "labels"):
|
|
value = raw.get(key)
|
|
if isinstance(value, str):
|
|
candidates = [value]
|
|
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
candidates = [item for item in value if isinstance(item, str)]
|
|
else:
|
|
continue
|
|
for item in candidates:
|
|
label = item.strip().lower()
|
|
if label in SENSITIVE_LABELS:
|
|
found.add(label)
|
|
return frozenset(found)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Prices:
|
|
"""Operator-supplied list price, in USD per million tokens (``None`` = unpriced)."""
|
|
|
|
input_per_mtok: float | None = None
|
|
output_per_mtok: float | None = None
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return self.input_per_mtok is not None or self.output_per_mtok is not None
|
|
|
|
def estimate(self, input_tokens: int | None, output_tokens: int | None) -> float | None:
|
|
if not self.configured or (input_tokens is None and output_tokens is None):
|
|
return None
|
|
dollars = (input_tokens or 0) * (self.input_per_mtok or 0.0) + (output_tokens or 0) * (self.output_per_mtok or 0.0)
|
|
return dollars / 1_000_000.0
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {"input_per_mtok": self.input_per_mtok, "output_per_mtok": self.output_per_mtok}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Record:
|
|
"""One shadow record, reduced to what the evaluation reads."""
|
|
|
|
index: int
|
|
population: str
|
|
verdict: str | None
|
|
fallback_reason: str | None
|
|
mode: str | None
|
|
model: str | None
|
|
cached: bool | None
|
|
probability: float | None
|
|
skip_threshold: float | None
|
|
digest: str | None
|
|
duration_ms: float | None
|
|
signals: frozenset[str]
|
|
signals_known: bool
|
|
message_count: int | None
|
|
batch_chars: int | None
|
|
success: bool | None
|
|
mutations_accepted: int | None
|
|
input_tokens: int | None
|
|
output_tokens: int | None
|
|
total_tokens: int | None
|
|
cost_usd: float | None
|
|
trivial_only: bool | None
|
|
reviewed: bool
|
|
review_outcome: str | None
|
|
sensitive_labels: frozenset[str]
|
|
censored: bool
|
|
censored_reason: str | None
|
|
|
|
@property
|
|
def skip(self) -> bool:
|
|
return self.verdict == VERDICT_SKIP
|
|
|
|
@property
|
|
def sensitive(self) -> bool:
|
|
return bool(self.sensitive_labels)
|
|
|
|
@property
|
|
def scorable(self) -> bool:
|
|
"""Whether "was this batch worth remembering?" has an answer at all."""
|
|
return self.mutations_accepted is not None or self.review_outcome is not None
|
|
|
|
@property
|
|
def miss(self) -> bool | None:
|
|
"""Whether a skip here would have lost a memory; ``None`` when not scorable.
|
|
|
|
Either witness marks a miss: the ``mutations_accepted`` counter (design
|
|
§5's definition) or a human ``review_outcome``. A review saying "not worth
|
|
remembering" never clears a counter that says otherwise — an enablement
|
|
gate reads the more conservative of the two.
|
|
"""
|
|
if not self.scorable:
|
|
return None
|
|
if self.review_outcome == REVIEW_WORTH_REMEMBERING:
|
|
return True
|
|
return self.mutations_accepted is not None and self.mutations_accepted > 0
|
|
|
|
|
|
def classify_population(record: Mapping[str, Any]) -> str:
|
|
"""Place one raw record in exactly one population (design §5's table).
|
|
|
|
A usable verdict means an answer was consumed: it is a cache hit when the
|
|
adapter says so and a network verdict otherwise. No usable verdict means no
|
|
request produced one — L3 / L4 / L5 / L7 / L8 or a failure fallback. No
|
|
``prescreen`` record at all means no side was enabled for this batch, or the
|
|
round was forbidden to judge: an enabled side always emits its record, even
|
|
when that record is a fallback.
|
|
"""
|
|
prescreen = record.get("prescreen")
|
|
if not isinstance(prescreen, Mapping):
|
|
return UNJUDGED
|
|
verdict = prescreen.get("verdict")
|
|
if verdict not in (VERDICT_SKIP, VERDICT_EXTRACT):
|
|
return LOCAL_FALLBACK
|
|
return CACHE_HIT if prescreen.get("cached") is True else NETWORK
|
|
|
|
|
|
def _censored_reason(population: str, mutations_accepted: int | None, review_outcome: str | None, success: bool | None) -> str | None:
|
|
"""Why this sample has no outcome to score, or ``None`` when it has one."""
|
|
if population not in (NETWORK, CACHE_HIT):
|
|
return None
|
|
if mutations_accepted is not None or review_outcome is not None:
|
|
return None
|
|
if success is False:
|
|
return CENSORED_EXTRACTION_FAILED
|
|
return CENSORED_OUTCOME_NOT_RECORDED
|
|
|
|
|
|
def parse_record(index: int, raw: Mapping[str, Any]) -> Record:
|
|
"""Reduce one payload to the fields the evaluation reads; never raises on shape."""
|
|
block = raw.get("prescreen")
|
|
block = block if isinstance(block, Mapping) else {}
|
|
population = classify_population(raw)
|
|
raw_verdict = block.get("verdict")
|
|
verdict = raw_verdict if raw_verdict in (VERDICT_SKIP, VERDICT_EXTRACT) else None
|
|
review_outcome = _normalize_review_outcome(raw.get("review_outcome"))
|
|
mutations_accepted = _as_count(raw.get("mutations_accepted"))
|
|
success = _as_bool(raw.get("success"))
|
|
input_tokens, output_tokens, total_tokens = _token_counts(raw.get("token_usage"))
|
|
raw_signals = block.get("signals")
|
|
signals_known = isinstance(raw_signals, list)
|
|
signals = frozenset(str(item) for item in raw_signals) if signals_known else frozenset()
|
|
digest = block.get("digest")
|
|
censored_reason = _censored_reason(population, mutations_accepted, review_outcome, success)
|
|
return Record(
|
|
index=index,
|
|
population=population,
|
|
verdict=verdict,
|
|
fallback_reason=block.get("fallback_reason") if isinstance(block.get("fallback_reason"), str) else None,
|
|
mode=block.get("mode") if isinstance(block.get("mode"), str) else None,
|
|
model=block.get("model") if isinstance(block.get("model"), str) else None,
|
|
cached=_as_bool(block.get("cached")),
|
|
probability=_as_float(block.get("probability")),
|
|
skip_threshold=_as_float(block.get("skip_threshold")),
|
|
digest=digest if isinstance(digest, str) and digest else None,
|
|
duration_ms=_as_float(block.get("duration_ms")),
|
|
signals=signals,
|
|
signals_known=signals_known,
|
|
message_count=_as_count(block.get("message_count")),
|
|
batch_chars=_as_count(block.get("batch_chars")),
|
|
success=success,
|
|
mutations_accepted=mutations_accepted,
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
total_tokens=total_tokens,
|
|
cost_usd=_as_float(raw.get("cost_usd")),
|
|
trivial_only=_as_bool(raw.get("trivial_only")),
|
|
reviewed=_as_bool(raw.get("reviewed")) is True or review_outcome is not None,
|
|
review_outcome=review_outcome,
|
|
sensitive_labels=_sensitive_labels(raw),
|
|
censored=censored_reason is not None,
|
|
censored_reason=censored_reason,
|
|
)
|
|
|
|
|
|
def _dig(record: Mapping[str, Any], path: str) -> Any:
|
|
"""Resolve a dotted ``--label`` path (``prescreen.mode``) against a raw record."""
|
|
current: Any = record
|
|
for part in path.split("."):
|
|
if not isinstance(current, Mapping) or part not in current:
|
|
return None
|
|
current = current[part]
|
|
return current
|
|
|
|
|
|
def record_matches(record: Mapping[str, Any], filters: Sequence[tuple[str, str]]) -> bool:
|
|
"""Whether every ``--label KEY=VALUE`` filter matches; strings compare as text."""
|
|
for path, expected in filters:
|
|
value = _dig(record, path)
|
|
if isinstance(value, str):
|
|
if value != expected:
|
|
return False
|
|
continue
|
|
try:
|
|
parsed = json.loads(expected)
|
|
except json.JSONDecodeError:
|
|
return False
|
|
if value != parsed:
|
|
return False
|
|
return True
|
|
|
|
|
|
def parse_label_filter(text: str) -> tuple[str, str]:
|
|
"""``KEY=VALUE`` (dotted keys allowed) for ``--label``; argparse error otherwise."""
|
|
key, separator, value = text.partition("=")
|
|
if not separator or not key.strip():
|
|
raise argparse.ArgumentTypeError(f"--label expects KEY=VALUE (e.g. prescreen.mode=shadow), got {text!r}")
|
|
return key.strip(), value
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LoadResult:
|
|
records: tuple[Record, ...]
|
|
read: int
|
|
filtered_out: int
|
|
malformed_lines: int
|
|
blank_lines: int
|
|
|
|
|
|
def load_records(path: Path, filters: Sequence[tuple[str, str]] = ()) -> LoadResult:
|
|
"""Read the JSONL, apply ``--label`` filtering and parse each line.
|
|
|
|
A line that is not a JSON object is *counted*, not dropped: gate D fails the
|
|
run rather than silently shrinking the denominator.
|
|
"""
|
|
records: list[Record] = []
|
|
malformed = 0
|
|
blank = 0
|
|
read = 0
|
|
filtered = 0
|
|
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
if not line.strip():
|
|
blank += 1
|
|
continue
|
|
try:
|
|
raw = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
malformed += 1
|
|
continue
|
|
if not isinstance(raw, Mapping):
|
|
malformed += 1
|
|
continue
|
|
read += 1
|
|
if filters and not record_matches(raw, filters):
|
|
filtered += 1
|
|
continue
|
|
records.append(parse_record(number, raw))
|
|
return LoadResult(records=tuple(records), read=read, filtered_out=filtered, malformed_lines=malformed, blank_lines=blank)
|
|
|
|
|
|
def _rate(numerator: int, denominator: int) -> float | None:
|
|
return None if denominator <= 0 else numerator / denominator
|
|
|
|
|
|
def _percentile(values: Sequence[float], fraction: float) -> float | None:
|
|
"""Nearest-rank quantile: the smallest sample at or above ``fraction`` of the mass."""
|
|
if not values:
|
|
return None
|
|
ordered = sorted(values)
|
|
return ordered[max(0, math.ceil(fraction * len(ordered)) - 1)]
|
|
|
|
|
|
def _latency(values: Sequence[float]) -> dict[str, Any]:
|
|
if not values:
|
|
return {"samples": 0, "p50": None, "p95": None}
|
|
return {"samples": len(values), "p50": statistics.median(values), "p95": _percentile(values, 0.95)}
|
|
|
|
|
|
def _token_totals(rows: Sequence[Record]) -> dict[str, Any]:
|
|
measured = [record for record in rows if record.input_tokens is not None or record.output_tokens is not None or record.total_tokens is not None]
|
|
return {
|
|
"records": len(measured),
|
|
"input": sum(record.input_tokens or 0 for record in measured),
|
|
"output": sum(record.output_tokens or 0 for record in measured),
|
|
"total": sum(record.total_tokens if record.total_tokens is not None else (record.input_tokens or 0) + (record.output_tokens or 0) for record in measured),
|
|
}
|
|
|
|
|
|
def _record_cost(record: Record, prices: Prices) -> float | None:
|
|
if record.cost_usd is not None:
|
|
return record.cost_usd
|
|
return prices.estimate(record.input_tokens, record.output_tokens)
|
|
|
|
|
|
def _cost_totals(rows: Sequence[Record], prices: Prices) -> dict[str, Any]:
|
|
costs = [_record_cost(record, prices) for record in rows]
|
|
priced = [cost for cost in costs if cost is not None]
|
|
if prices.configured:
|
|
pricing = "cli price per million tokens"
|
|
elif priced:
|
|
pricing = "record cost_usd"
|
|
else:
|
|
pricing = "not configured"
|
|
return {"total_usd": sum(priced) if priced else None, "priced_records": len(priced), "unpriced_records": len(costs) - len(priced), "pricing": pricing}
|
|
|
|
|
|
def summarize_population(records: Sequence[Record], population: str, prices: Prices = Prices()) -> dict[str, Any]:
|
|
"""One population's table row: counts, decision rates, latency, tokens and cost."""
|
|
rows = [record for record in records if record.population == population]
|
|
skips = [record for record in rows if record.skip]
|
|
extracts = [record for record in rows if record.verdict == VERDICT_EXTRACT]
|
|
scored_skips = [record for record in skips if record.scorable]
|
|
saved = [record for record in scored_skips if record.miss is False]
|
|
misses = [record for record in scored_skips if record.miss is True]
|
|
reason_rows = [record.fallback_reason or UNRECORDED_REASON for record in rows if record.population == LOCAL_FALLBACK or record.fallback_reason is not None]
|
|
summary: dict[str, Any] = {
|
|
"records": len(rows),
|
|
"censored": sum(1 for record in rows if record.censored),
|
|
"censored_reasons": dict(Counter(record.censored_reason for record in rows if record.censored_reason)),
|
|
"verdicts": {"skip": len(skips), "extract": len(extracts), "none": len(rows) - len(skips) - len(extracts)},
|
|
"cached": {
|
|
"true": sum(1 for record in rows if record.cached is True),
|
|
"false": sum(1 for record in rows if record.cached is False),
|
|
"unknown": sum(1 for record in rows if record.cached is None),
|
|
},
|
|
"skip_rate": _rate(len(skips), len(rows)),
|
|
"scored_skips": len(scored_skips),
|
|
"saved_calls": len(saved),
|
|
"misses": len(misses),
|
|
"saved_call_rate": _rate(len(saved), len(scored_skips)),
|
|
"miss_rate": _rate(len(misses), len(scored_skips)),
|
|
"saved_call_rate_over_all_verdicts": _rate(len(saved), len(rows)),
|
|
"missed_digests": [record.digest for record in misses if record.digest],
|
|
"latency_ms": _latency([record.duration_ms for record in rows if record.duration_ms is not None]),
|
|
"tokens": _token_totals(rows),
|
|
"cost": _cost_totals(rows, prices),
|
|
}
|
|
if reason_rows:
|
|
summary["fallback_reasons"] = dict(Counter(reason_rows))
|
|
summary["fallback_locks"] = dict(Counter(FALLBACK_LOCKS.get(reason, "unknown") for reason in reason_rows))
|
|
return summary
|
|
|
|
|
|
def _beta_continued_fraction(a: float, b: float, x: float) -> float:
|
|
"""Lentz's continued fraction for the incomplete beta function (Numerical Recipes)."""
|
|
qab, qap, qam = a + b, a + 1.0, a - 1.0
|
|
c = 1.0
|
|
d = 1.0 - qab * x / qap
|
|
if abs(d) < _BETA_TINY:
|
|
d = _BETA_TINY
|
|
d = 1.0 / d
|
|
h = d
|
|
for m in range(1, _BETA_MAX_ITERATIONS + 1):
|
|
m2 = 2 * m
|
|
numerator = m * (b - m) * x / ((qam + m2) * (a + m2))
|
|
d = 1.0 + numerator * d
|
|
if abs(d) < _BETA_TINY:
|
|
d = _BETA_TINY
|
|
c = 1.0 + numerator / c
|
|
if abs(c) < _BETA_TINY:
|
|
c = _BETA_TINY
|
|
d = 1.0 / d
|
|
h *= d * c
|
|
numerator = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
|
d = 1.0 + numerator * d
|
|
if abs(d) < _BETA_TINY:
|
|
d = _BETA_TINY
|
|
c = 1.0 + numerator / c
|
|
if abs(c) < _BETA_TINY:
|
|
c = _BETA_TINY
|
|
d = 1.0 / d
|
|
delta = d * c
|
|
h *= delta
|
|
if abs(delta - 1.0) < _BETA_EPSILON:
|
|
break
|
|
return h
|
|
|
|
|
|
def _regularized_incomplete_beta(a: float, b: float, x: float) -> float:
|
|
"""``I_x(a, b)``, the regularized incomplete beta function."""
|
|
if x <= 0.0:
|
|
return 0.0
|
|
if x >= 1.0:
|
|
return 1.0
|
|
front = math.exp(math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b) + a * math.log(x) + b * math.log1p(-x))
|
|
if x < (a + 1.0) / (a + b + 2.0):
|
|
return front * _beta_continued_fraction(a, b, x) / a
|
|
return 1.0 - front * _beta_continued_fraction(b, a, 1.0 - x) / b
|
|
|
|
|
|
def _beta_quantile(probability: float, a: float, b: float) -> float:
|
|
"""Inverse of :func:`_regularized_incomplete_beta` in ``x`` (bisection; monotone)."""
|
|
if probability <= 0.0:
|
|
return 0.0
|
|
if probability >= 1.0:
|
|
return 1.0
|
|
low, high = 0.0, 1.0
|
|
for _ in range(_BETA_BISECTIONS):
|
|
middle = (low + high) / 2.0
|
|
if _regularized_incomplete_beta(a, b, middle) < probability:
|
|
low = middle
|
|
else:
|
|
high = middle
|
|
return (low + high) / 2.0
|
|
|
|
|
|
def clopper_pearson_upper(misses: int, skips: int, confidence: float = DEFAULT_CONFIDENCE) -> float | None:
|
|
"""Exact one-sided Clopper-Pearson upper bound for the miss rate.
|
|
|
|
Solves ``P(X <= misses) = 1 - confidence`` for the proportion ``p``, i.e.
|
|
``I_{1-p}(skips - misses, misses + 1) = 1 - confidence``, by bisection on the
|
|
regularized incomplete beta. This is the textbook exact bound: no normal
|
|
approximation is used anywhere in this script. ``None`` when there is no
|
|
sample, ``1.0`` when every skip was a miss.
|
|
"""
|
|
if skips <= 0:
|
|
return None
|
|
if not 0.0 < confidence < 1.0:
|
|
raise ValueError(f"confidence must be in (0, 1), got {confidence!r}")
|
|
misses = max(0, min(misses, skips))
|
|
if misses >= skips:
|
|
return 1.0
|
|
bound = 1.0 - _beta_quantile(1.0 - confidence, float(skips - misses), float(misses + 1))
|
|
return min(1.0, max(0.0, bound))
|
|
|
|
|
|
def baseline_summary(records: Sequence[Record], prices: Prices = Prices()) -> dict[str, Any]:
|
|
"""The mandatory no-network baseline, over the same scored records as the model.
|
|
|
|
A batch is a baseline skip when it carries no deterministic signal and the
|
|
collector marked it ``trivial_only``. Records without ``trivial_only`` (or
|
|
without a readable ``signals`` list) cannot be scored by the heuristic: the
|
|
coverage is reported instead of silently shrinking the denominator.
|
|
"""
|
|
evaluated = [record for record in records if record.population == NETWORK]
|
|
computable = [record for record in evaluated if record.trivial_only is not None and record.signals_known]
|
|
scored = [record for record in computable if record.scorable]
|
|
baseline_skips = [record for record in scored if record.trivial_only and not record.signals]
|
|
baseline_saved = [record for record in baseline_skips if record.miss is False]
|
|
baseline_misses = [record for record in baseline_skips if record.miss is True]
|
|
model_skips = [record for record in scored if record.skip]
|
|
model_saved = [record for record in model_skips if record.miss is False]
|
|
model_misses = [record for record in model_skips if record.miss is True]
|
|
baseline_saved_rate = _rate(len(baseline_saved), len(baseline_skips))
|
|
model_saved_rate = _rate(len(model_saved), len(model_skips))
|
|
return {
|
|
"rule": "skip when signals is empty and trivial_only is true",
|
|
"evaluated_verdicts": len(evaluated),
|
|
"computable": len(computable),
|
|
"coverage": _rate(len(computable), len(evaluated)),
|
|
"scored": len(scored),
|
|
"skips": len(baseline_skips),
|
|
"saved_calls": len(baseline_saved),
|
|
"misses": len(baseline_misses),
|
|
"saved_call_rate": baseline_saved_rate,
|
|
"miss_rate": _rate(len(baseline_misses), len(baseline_skips)),
|
|
"model": {
|
|
"skips": len(model_skips),
|
|
"saved_calls": len(model_saved),
|
|
"misses": len(model_misses),
|
|
"saved_call_rate": model_saved_rate,
|
|
"miss_rate": _rate(len(model_misses), len(model_skips)),
|
|
},
|
|
"incremental_saved_calls": len(model_saved) - len(baseline_saved),
|
|
# Difference of saved calls over the shared denominator. Subtracting the
|
|
# conditional rates (saved/skips) would report zero whenever both policies
|
|
# saved every batch they skipped, even if the model skipped far more.
|
|
"incremental_saved_call_rate": _rate(len(model_saved) - len(baseline_saved), len(scored)),
|
|
"trivial_only_missing": sum(1 for record in evaluated if record.trivial_only is None),
|
|
"signals_missing": sum(1 for record in evaluated if not record.signals_known),
|
|
"note": "Both sides are scored over the same scored records; a baseline that nearly matches the model means the third-party egress buys little.",
|
|
"cost": _cost_totals(scored, prices),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Gate:
|
|
gate_id: str
|
|
status: str
|
|
detail: str
|
|
|
|
def as_dict(self) -> dict[str, str]:
|
|
return {"id": self.gate_id, "status": self.status, "detail": self.detail}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Evidence:
|
|
"""The numbers the design §5 gates read (network population only)."""
|
|
|
|
scored_skips: int
|
|
misses: int
|
|
saved_calls: int
|
|
sensitive_scored_skips: Mapping[str, int]
|
|
sensitive_misses: Mapping[str, int]
|
|
reviewed_skips: int
|
|
reviewed_scored_skips: int
|
|
reviewed_missed: int
|
|
saved_skips_with_tokens: int
|
|
latency_samples: int
|
|
#: The no-network comparison, all three over the same records: the ones the
|
|
#: baseline can score (``trivial_only`` and a readable signals list). Counting
|
|
#: the model's savings over *all* scorable skips instead would credit it with
|
|
#: batches the baseline never saw.
|
|
baseline_computable: int
|
|
baseline_paired_skips: int
|
|
baseline_model_saved_calls: int
|
|
baseline_heuristic_saved_calls: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Coverage:
|
|
"""Accounting for the records that could not be scored."""
|
|
|
|
records: int
|
|
censored: int
|
|
malformed_lines: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Thresholds:
|
|
miss_target: float = DEFAULT_MISS_TARGET
|
|
confidence: float = DEFAULT_CONFIDENCE
|
|
min_reviewed_skips: int = DEFAULT_MIN_REVIEWED_SKIPS
|
|
|
|
|
|
def evaluate_gates(evidence: Evidence, coverage: Coverage, thresholds: Thresholds = Thresholds()) -> tuple[Gate, ...]:
|
|
"""Every gate prints ``PASS`` / ``FAIL`` / ``INSUFFICIENT`` — never silence."""
|
|
gates: list[Gate] = []
|
|
miss_rate = _rate(evidence.misses, evidence.scored_skips)
|
|
upper = clopper_pearson_upper(evidence.misses, evidence.scored_skips, thresholds.confidence)
|
|
observed = f"miss_rate={evidence.misses}/{evidence.scored_skips}={miss_rate:.4f}" if miss_rate is not None else ""
|
|
bound = "no bound (no sample)" if upper is None else f"one-sided {thresholds.confidence:.0%} Clopper-Pearson upper bound {upper:.4f}"
|
|
sample = f"sample n={evidence.scored_skips}"
|
|
if miss_rate is None:
|
|
gates.append(Gate("network_miss_rate", INSUFFICIENT, "no scored skip verdict in the network population; there is no sample for the miss rate or its bound"))
|
|
elif miss_rate > thresholds.miss_target:
|
|
# The observation itself is over target: more samples cannot rescue it.
|
|
gates.append(Gate("network_miss_rate", FAIL, f"{observed} is above the target {thresholds.miss_target:.4f}; {bound} ({sample})"))
|
|
elif upper is None or upper > thresholds.miss_target:
|
|
# The point estimate meets the target and the exact bound does not establish
|
|
# it: the sample is underpowered, so the honest answer is more skips.
|
|
gates.append(Gate("network_miss_rate", INSUFFICIENT, f"{observed} meets the target {thresholds.miss_target:.4f}, but {bound} does not ({sample}): too few skips to establish the target"))
|
|
else:
|
|
gates.append(Gate("network_miss_rate", PASS, f"{observed} target<={thresholds.miss_target:.4f}; {bound} ({sample})"))
|
|
|
|
missed_by_label = {label: evidence.sensitive_misses.get(label, 0) for label in sorted(SENSITIVE_LABELS)}
|
|
uncovered = [label for label in sorted(SENSITIVE_LABELS) if evidence.sensitive_scored_skips.get(label, 0) == 0]
|
|
per_label = "; ".join(f"{label}: {missed_by_label[label]} miss(es) over {evidence.sensitive_scored_skips.get(label, 0)} scored skip(s)" for label in sorted(SENSITIVE_LABELS))
|
|
if any(count > 0 for count in missed_by_label.values()):
|
|
gates.append(Gate("sensitive_stratum_zero_misses", FAIL, f"a scored skip in a required stratum was worth remembering ({per_label})"))
|
|
elif uncovered:
|
|
gates.append(Gate("sensitive_stratum_zero_misses", INSUFFICIENT, f"no scored skip carries the {uncovered} stratum label(s); the zero-miss check needs evidence for every required stratum {sorted(SENSITIVE_LABELS)} ({per_label})"))
|
|
else:
|
|
gates.append(Gate("sensitive_stratum_zero_misses", PASS, f"zero misses in every required stratum ({per_label})"))
|
|
|
|
if evidence.reviewed_skips == 0:
|
|
gates.append(Gate("reviewed_skips_confirm_no_loss", INSUFFICIENT, f"no skip verdict is labelled reviewed; the collector must set reviewed=true / review_outcome (target >={thresholds.min_reviewed_skips})"))
|
|
elif evidence.reviewed_missed:
|
|
gates.append(Gate("reviewed_skips_confirm_no_loss", FAIL, f"{evidence.reviewed_missed} reviewed skip verdict(s) were worth remembering; design §5 gate 3 is a review *confirming* none was, so the review sample contradicts the skip"))
|
|
elif evidence.reviewed_skips < thresholds.min_reviewed_skips:
|
|
gates.append(Gate("reviewed_skips_confirm_no_loss", FAIL, f"{evidence.reviewed_skips} reviewed skip verdict(s) of {thresholds.min_reviewed_skips} required ({evidence.reviewed_scored_skips} of them scored)"))
|
|
elif evidence.reviewed_scored_skips < evidence.reviewed_skips:
|
|
gates.append(
|
|
Gate(
|
|
"reviewed_skips_confirm_no_loss",
|
|
INSUFFICIENT,
|
|
f"{evidence.reviewed_skips - evidence.reviewed_scored_skips} of {evidence.reviewed_skips} reviewed skip(s) carry neither a review_outcome nor mutations_accepted, so the review confirms nothing for them",
|
|
)
|
|
)
|
|
else:
|
|
gates.append(Gate("reviewed_skips_confirm_no_loss", PASS, f"{evidence.reviewed_skips} reviewed skip verdict(s), all scored, none worth remembering ({thresholds.min_reviewed_skips} required)"))
|
|
|
|
if coverage.records == 0:
|
|
gates.append(Gate("censoring_disclosed", INSUFFICIENT, "no record was read; there is nothing to account for"))
|
|
elif coverage.malformed_lines > 0:
|
|
gates.append(Gate("censoring_disclosed", FAIL, f"{coverage.malformed_lines} input line(s) were not parseable JSON objects; they were counted but must not be dropped from the denominator"))
|
|
else:
|
|
gates.append(Gate("censoring_disclosed", PASS, f"{coverage.censored} censored sample(s) of {coverage.records} reported with their reasons; none dropped"))
|
|
|
|
if evidence.scored_skips == 0:
|
|
gates.append(Gate("savings_recorded", INSUFFICIENT, "no scored skip verdict, so no saved extraction call is recorded"))
|
|
elif evidence.saved_calls == 0:
|
|
gates.append(Gate("savings_recorded", FAIL, f"no scored skip saved an extraction call over {evidence.scored_skips} scored skip(s); the pre-screen as recorded saves nothing"))
|
|
else:
|
|
absent = [
|
|
name
|
|
for name, count in (
|
|
("tokens on the saved skips", evidence.saved_skips_with_tokens),
|
|
("verdict latency", evidence.latency_samples),
|
|
("the no-network baseline (collector field trivial_only)", evidence.baseline_computable),
|
|
)
|
|
if count == 0
|
|
]
|
|
if absent:
|
|
gates.append(
|
|
Gate(
|
|
"savings_recorded",
|
|
INSUFFICIENT,
|
|
f"{evidence.saved_calls} extraction call(s) recorded saved, but the evidence is incomplete: nothing recorded for "
|
|
+ "; ".join(absent)
|
|
+ " - design §5 gate 4 asks for recorded calls/tokens saved, their p50/p95 latency cost, and the mandatory baseline",
|
|
)
|
|
)
|
|
else:
|
|
recorded = f"{evidence.saved_calls} extraction call(s) recorded saved over {evidence.scored_skips} scored skip(s)"
|
|
observed = f"tokens on {evidence.saved_skips_with_tokens} of them, {evidence.latency_samples} verdict latency sample(s), baseline on {evidence.baseline_computable} record(s)"
|
|
gates.append(Gate("savings_recorded", PASS, f"{recorded}; {observed}"))
|
|
|
|
# Both sides of this comparison are the baseline-computable records, and the gate
|
|
# refuses to conclude from a subset of the scored skips: subtracting a subset's
|
|
# heuristic savings from model savings over all scorable skips would count every
|
|
# unannotated batch as imaginary incremental benefit.
|
|
paired = evidence.baseline_paired_skips
|
|
incremental = evidence.baseline_model_saved_calls - evidence.baseline_heuristic_saved_calls
|
|
if paired == 0:
|
|
gates.append(
|
|
Gate(
|
|
"baseline_incremental_savings",
|
|
INSUFFICIENT,
|
|
"no scored skip carries the baseline annotation (the collector must set trivial_only and signals), so there is nothing to compare the model against",
|
|
)
|
|
)
|
|
elif paired < evidence.scored_skips:
|
|
gates.append(
|
|
Gate(
|
|
"baseline_incremental_savings",
|
|
INSUFFICIENT,
|
|
f"the baseline can score {paired} of {evidence.scored_skips} scored skip(s); the comparison must cover the scored skips, not the subset the annotation happens to reach",
|
|
)
|
|
)
|
|
elif incremental <= 0:
|
|
saved_summary = f"the model saved {evidence.baseline_model_saved_calls} call(s) against the heuristic's {evidence.baseline_heuristic_saved_calls} on the same {paired} scored skip(s): {incremental:+d} incremental"
|
|
gates.append(Gate("baseline_incremental_savings", FAIL, f"{saved_summary}, so the egress buys nothing over the heuristic already in the stack"))
|
|
else:
|
|
gates.append(
|
|
Gate(
|
|
"baseline_incremental_savings",
|
|
PASS,
|
|
f"the model saved {incremental} call(s) more than the no-network heuristic on the same {paired} scored skip(s) ({evidence.baseline_model_saved_calls} vs {evidence.baseline_heuristic_saved_calls})",
|
|
)
|
|
)
|
|
|
|
return tuple(gates)
|
|
|
|
|
|
def build_report(
|
|
records: Sequence[Record],
|
|
*,
|
|
source: str,
|
|
malformed_lines: int = 0,
|
|
filtered_out: int = 0,
|
|
read_lines: int = 0,
|
|
blank_lines: int = 0,
|
|
thresholds: Thresholds = Thresholds(),
|
|
prices: Prices = Prices(),
|
|
label_filters: Sequence[tuple[str, str]] = (),
|
|
) -> dict[str, Any]:
|
|
"""Assemble the full report: populations, model, baseline, stratification, gates."""
|
|
populations = {name: summarize_population(records, name, prices) for name in POPULATIONS}
|
|
network = [record for record in records if record.population == NETWORK]
|
|
scored_skips = [record for record in network if record.skip and record.scorable]
|
|
sensitive_scored_skips = {label: sum(1 for record in scored_skips if label in record.sensitive_labels) for label in sorted(SENSITIVE_LABELS)}
|
|
sensitive_misses = {label: sum(1 for record in scored_skips if label in record.sensitive_labels and record.miss is True) for label in sorted(SENSITIVE_LABELS)}
|
|
reviewed_skips = [record for record in network if record.skip and record.reviewed]
|
|
saved = [record for record in scored_skips if record.miss is False]
|
|
baseline = baseline_summary(records, prices)
|
|
evidence = Evidence(
|
|
scored_skips=len(scored_skips),
|
|
misses=sum(1 for record in scored_skips if record.miss is True),
|
|
saved_calls=len(saved),
|
|
sensitive_scored_skips=sensitive_scored_skips,
|
|
sensitive_misses=sensitive_misses,
|
|
reviewed_skips=len(reviewed_skips),
|
|
reviewed_scored_skips=sum(1 for record in reviewed_skips if record.scorable),
|
|
reviewed_missed=sum(1 for record in reviewed_skips if record.miss is True),
|
|
saved_skips_with_tokens=sum(1 for record in saved if record.total_tokens is not None),
|
|
latency_samples=sum(1 for record in network if record.duration_ms is not None),
|
|
baseline_computable=baseline["computable"],
|
|
baseline_paired_skips=baseline["model"]["skips"],
|
|
baseline_model_saved_calls=baseline["model"]["saved_calls"],
|
|
baseline_heuristic_saved_calls=baseline["saved_calls"],
|
|
)
|
|
coverage = Coverage(records=len(records), censored=sum(1 for record in records if record.censored), malformed_lines=malformed_lines)
|
|
gates = evaluate_gates(evidence, coverage, thresholds)
|
|
verdicts = populations[NETWORK]["verdicts"]
|
|
upper = clopper_pearson_upper(evidence.misses, evidence.scored_skips, thresholds.confidence)
|
|
modes = Counter(record.mode for record in records if record.mode)
|
|
notes = [
|
|
"One record is exactly one population: network verdicts are the only classifier samples; cache hits reuse another verdict and local fallbacks never asked.",
|
|
"saved_call_rate and miss_rate share the scored-skip denominator (so they sum to 1); saved_call_rate_over_all_verdicts keeps the network-verdict denominator.",
|
|
"The no-network baseline needs the collector's trivial_only field; its coverage is reported rather than assumed.",
|
|
"This report embeds no raw record, no conversation text and no credential.",
|
|
]
|
|
if modes.get("enforce"):
|
|
notes.append("Records with mode=enforce carry no extraction outcome for a skip (no call ran), so such a skip is censored unless the collector records a review_outcome; a miss rate is only meaningful over shadow records.")
|
|
report: dict[str, Any] = {
|
|
"script": "eval_memory_prescreen",
|
|
"source": source,
|
|
"input": {
|
|
"records": len(records),
|
|
"read_lines": read_lines,
|
|
"filtered_out": filtered_out,
|
|
"malformed_lines": malformed_lines,
|
|
"blank_lines": blank_lines,
|
|
"label_filters": [f"{key}={value}" for key, value in label_filters],
|
|
},
|
|
"thresholds": {"miss_target": thresholds.miss_target, "confidence": thresholds.confidence, "min_reviewed_skips": thresholds.min_reviewed_skips},
|
|
"pricing": prices.as_dict(),
|
|
"populations": populations,
|
|
"model": {
|
|
"population": NETWORK,
|
|
"scope": "network verdicts only: the classifier samples",
|
|
"modes": dict(modes),
|
|
"verdicts": verdicts,
|
|
"skip_rate": populations[NETWORK]["skip_rate"],
|
|
"scored_skips": evidence.scored_skips,
|
|
"saved_calls": evidence.saved_calls,
|
|
"saved_call_rate": populations[NETWORK]["saved_call_rate"],
|
|
"saved_call_rate_over_all_verdicts": populations[NETWORK]["saved_call_rate_over_all_verdicts"],
|
|
"misses": evidence.misses,
|
|
"miss_rate": populations[NETWORK]["miss_rate"],
|
|
"miss_rate_upper_bound": {"method": "clopper-pearson one-sided exact", "confidence": thresholds.confidence, "value": upper, "misses": evidence.misses, "scored_skips": evidence.scored_skips},
|
|
"missed_digests": populations[NETWORK]["missed_digests"],
|
|
"latency_ms": populations[NETWORK]["latency_ms"],
|
|
"tokens": populations[NETWORK]["tokens"],
|
|
"cost": populations[NETWORK]["cost"],
|
|
},
|
|
"baseline_heuristic": baseline,
|
|
"sensitive_stratum": {
|
|
"labels": sorted(SENSITIVE_LABELS),
|
|
"by_label": {label: {"scored_skips": sensitive_scored_skips[label], "misses": sensitive_misses[label]} for label in sorted(SENSITIVE_LABELS)},
|
|
"note": (
|
|
"Labels come from the collector's stratum/labels field; L3 keeps signal-bearing batches out of the "
|
|
"network population, so this is a human label. The gate checks every required stratum separately, "
|
|
"because a pooled sample can hide a stratum with no evidence."
|
|
),
|
|
},
|
|
"review": {
|
|
"reviewed_skips": evidence.reviewed_skips,
|
|
"reviewed_scored_skips": evidence.reviewed_scored_skips,
|
|
"reviewed_missed": evidence.reviewed_missed,
|
|
"review_outcomes": dict(Counter(record.review_outcome for record in network if record.review_outcome)),
|
|
"note": "reviewed comes from the collector; review_outcome is a second ground-truth witness for a skip. Gate 3 requires the review to confirm none was worth remembering, so a reviewed skip found worth remembering fails it.",
|
|
},
|
|
"censoring": {
|
|
"records": len(records),
|
|
"censored": coverage.censored,
|
|
"reasons": dict(Counter(record.censored_reason for record in records if record.censored_reason)),
|
|
"malformed_lines": malformed_lines,
|
|
"note": "A censored sample has no outcome to score; it is counted and reported, never dropped.",
|
|
},
|
|
"collector_contract": {
|
|
"from the DeerMem extraction_callback payload": "prescreen.{mode,verdict,probability,skip_threshold,model,cached,digest,signals,message_count,batch_chars,duration_ms,fallback_reason}, success, token_usage, mutations_accepted",
|
|
"must be added by the collector": {
|
|
"trivial_only": "bool - every turn in the batch was trivial (filter_trivial); the no-network baseline cannot be computed without it",
|
|
"reviewed": "bool - a human reviewed this skip verdict (design §5 gate 3)",
|
|
"review_outcome": "worth_remembering | none_worth_remembering (optional; a second ground-truth witness)",
|
|
"stratum": "identity | preference | correction (or a labels list); design §5 gate 2 subset",
|
|
"cost_usd": "float (optional) - the record's billed cost, when the collector knows it",
|
|
},
|
|
},
|
|
"notes": notes,
|
|
"gates": [gate.as_dict() for gate in gates],
|
|
"gate_summary": {
|
|
"passed": [gate.gate_id for gate in gates if gate.status == PASS],
|
|
"failed": [gate.gate_id for gate in gates if gate.status == FAIL],
|
|
"insufficient": [gate.gate_id for gate in gates if gate.status == INSUFFICIENT],
|
|
"enable_recommended": all(gate.status == PASS for gate in gates),
|
|
},
|
|
}
|
|
return report
|
|
|
|
|
|
def _format_rate(value: float | None) -> str:
|
|
return "n/a" if value is None else f"{value * 100:.1f}%"
|
|
|
|
|
|
def _format_ms(value: float | None) -> str:
|
|
return "n/a" if value is None else f"{value:.1f}ms"
|
|
|
|
|
|
def _format_ratio(value: float | None) -> str:
|
|
return "n/a" if value is None else f"{value:.4f}"
|
|
|
|
|
|
def _format_cost(cost: Mapping[str, Any]) -> str:
|
|
if cost["total_usd"] is None:
|
|
return f"not configured ({cost['unpriced_records']} record(s) with no --price-in/--price-out and no record cost_usd)"
|
|
return f"${cost['total_usd']:.4f} ({cost['pricing']}; {cost['priced_records']} priced / {cost['unpriced_records']} unpriced)"
|
|
|
|
|
|
def print_report(report: Mapping[str, Any]) -> None:
|
|
"""Print the populations table, the baseline comparison and every gate verdict."""
|
|
input_summary = report["input"]
|
|
filters = ", ".join(input_summary["label_filters"]) or "none"
|
|
print(f"\nMemory pre-screen shadow evaluation: {report['source']}")
|
|
print(f"records: {input_summary['records']} scored; {input_summary['filtered_out']} filtered out; {input_summary['malformed_lines']} malformed line(s); {input_summary['blank_lines']} blank line(s); label filters: {filters}")
|
|
print("\nPopulations (mutually exclusive, design section 5; only network verdicts are classifier samples):")
|
|
print(f" {'population':<15}{'n':>5}{'skip':>6}{'extract':>8}{'cached':>7}{'censored':>9}{'p50':>10}{'p95':>10}")
|
|
for name in POPULATIONS:
|
|
summary = report["populations"][name]
|
|
latency = summary["latency_ms"]
|
|
print(f" {name:<15}{summary['records']:>5}{summary['verdicts']['skip']:>6}{summary['verdicts']['extract']:>8}{summary['cached']['true']:>7}{summary['censored']:>9}{_format_ms(latency['p50']):>10}{_format_ms(latency['p95']):>10}")
|
|
for name in DESIGN_POPULATIONS:
|
|
reasons = report["populations"][name].get("fallback_reasons")
|
|
if reasons:
|
|
rendered = ", ".join(f"{reason}={count} [{FALLBACK_LOCKS.get(reason, 'unknown')}]" for reason, count in sorted(reasons.items()))
|
|
print(f" {name} reasons: {rendered}")
|
|
|
|
model = report["model"]
|
|
verdicts = model["verdicts"]
|
|
print(f"\nModel verdicts ({model['scope']}; modes {model['modes'] or 'none'}):")
|
|
print(f" skip_rate {_format_rate(model['skip_rate'])} ({verdicts['skip']} skip / {verdicts['extract']} extract / {verdicts['none']} no verdict)")
|
|
print(f" scored skips {model['scored_skips']}; saved calls {model['saved_calls']} = {_format_rate(model['saved_call_rate'])} of scored skips ({_format_rate(model['saved_call_rate_over_all_verdicts'])} of all network verdicts)")
|
|
print(f" missed memories {model['misses']} = miss_rate {_format_rate(model['miss_rate'])} (a skip with mutations_accepted > 0)")
|
|
bound = model["miss_rate_upper_bound"]
|
|
print(f" miss-rate upper bound: {_format_ratio(bound['value'])} one-sided at {bound['confidence']:.0%} ({bound['method']}, misses={bound['misses']}, n={bound['scored_skips']})")
|
|
latency = model["latency_ms"]
|
|
tokens = model["tokens"]
|
|
print(f" verdict latency p50 {_format_ms(latency['p50'])} p95 {_format_ms(latency['p95'])} (n={latency['samples']})")
|
|
print(f" tokens in={tokens['input']} out={tokens['output']} total={tokens['total']} (n={tokens['records']}); cost {_format_cost(model['cost'])}")
|
|
if model["missed_digests"]:
|
|
print(f" missed digests: {', '.join(model['missed_digests'])}")
|
|
|
|
baseline = report["baseline_heuristic"]
|
|
print(f"\nNo-network heuristic baseline ({baseline['rule']}):")
|
|
coverage = f"{baseline['computable']}/{baseline['evaluated_verdicts']}"
|
|
print(f" computable on {coverage} network verdicts (coverage {_format_rate(baseline['coverage'])})")
|
|
print(f" collector field trivial_only missing on {baseline['trivial_only_missing']} record(s); signals missing on {baseline['signals_missing']}")
|
|
baseline_rates = f"saved {_format_rate(baseline['saved_call_rate'])}, miss {_format_rate(baseline['miss_rate'])}"
|
|
print(f" baseline: skips {baseline['skips']}, saved calls {baseline['saved_calls']}, misses {baseline['misses']} ({baseline_rates})")
|
|
model_on_set = baseline["model"]
|
|
model_rates = f"saved {_format_rate(model_on_set['saved_call_rate'])}, miss {_format_rate(model_on_set['miss_rate'])}"
|
|
print(f" model on the same {baseline['scored']} scored record(s): skips {model_on_set['skips']}, saved calls {model_on_set['saved_calls']}, misses {model_on_set['misses']} ({model_rates})")
|
|
print(f" incremental saved calls {baseline['incremental_saved_calls']} ({_format_rate(baseline['incremental_saved_call_rate'])} of the scored records)")
|
|
|
|
sensitive = report["sensitive_stratum"]
|
|
review = report["review"]
|
|
censoring = report["censoring"]
|
|
per_label = "; ".join(f"{label}: {data['misses']} miss(es) over {data['scored_skips']} scored skip(s)" for label, data in sensitive["by_label"].items())
|
|
print(f"\nSensitive strata ({', '.join(sensitive['labels'])}): {per_label}.")
|
|
print(f"Review sample: {review['reviewed_skips']} reviewed skip verdict(s) ({review['reviewed_scored_skips']} scored, {review['reviewed_missed']} found worth remembering); review outcomes: {review['review_outcomes'] or 'none'}.")
|
|
print(f"Censoring: {censoring['censored']} censored of {censoring['records']} record(s) {censoring['reasons'] or ''}; malformed lines {censoring['malformed_lines']}.")
|
|
|
|
print("\nGates before enforce (design section 5):")
|
|
for gate in report["gates"]:
|
|
print(f" [{gate['status']:<12}] {gate['id']}: {gate['detail']}")
|
|
summary = report["gate_summary"]
|
|
if summary["enable_recommended"]:
|
|
print("\nEvaluation gates PASSED: every gate is PASS; enforce may be enabled for a controlled window.")
|
|
else:
|
|
print(f"\nDO NOT ENABLE: {len(summary['failed'])} gate(s) failed, {len(summary['insufficient'])} gate(s) lack evidence.")
|
|
if summary["failed"]:
|
|
print(f" failed: {', '.join(summary['failed'])}")
|
|
if summary["insufficient"]:
|
|
print(f" insufficient: {', '.join(summary['insufficient'])}")
|
|
print("\nNotes:")
|
|
for note in report["notes"]:
|
|
print(f" - {note}")
|
|
|
|
|
|
def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--records", type=Path, required=True, help="JSONL of shadow records: one DeerMem extraction_callback payload per line")
|
|
parser.add_argument("--label", action="append", type=parse_label_filter, default=[], metavar="KEY=VALUE", help="keep only records whose (dotted) field matches; repeatable, all must match")
|
|
parser.add_argument("--json", type=Path, default=None, help="write the full report as JSON to this path")
|
|
parser.add_argument("--miss-target", type=float, default=DEFAULT_MISS_TARGET, help="miss-rate point estimate the gate accepts (default: %(default)s)")
|
|
parser.add_argument("--confidence", type=float, default=DEFAULT_CONFIDENCE, help="confidence for the Clopper-Pearson upper bound (default: %(default)s)")
|
|
parser.add_argument("--min-reviewed-skips", type=int, default=DEFAULT_MIN_REVIEWED_SKIPS, help="reviewed skip verdicts required before enforce (default: %(default)s)")
|
|
parser.add_argument("--price-in", type=float, default=None, help="Jev input price in USD per million tokens (optional; enables the cost estimate)")
|
|
parser.add_argument("--price-out", type=float, default=None, help="Jev output price in USD per million tokens (optional; enables the cost estimate)")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
"""Run the evaluation; the exit code is the gate verdict (0 only when all PASS)."""
|
|
args = _parse_args(argv)
|
|
loaded = load_records(args.records, args.label)
|
|
thresholds = Thresholds(miss_target=args.miss_target, confidence=args.confidence, min_reviewed_skips=args.min_reviewed_skips)
|
|
prices = Prices(input_per_mtok=args.price_in, output_per_mtok=args.price_out)
|
|
report = build_report(
|
|
loaded.records,
|
|
source=str(args.records),
|
|
malformed_lines=loaded.malformed_lines,
|
|
filtered_out=loaded.filtered_out,
|
|
read_lines=loaded.read,
|
|
blank_lines=loaded.blank_lines,
|
|
thresholds=thresholds,
|
|
prices=prices,
|
|
label_filters=args.label,
|
|
)
|
|
print_report(report)
|
|
if args.json is not None:
|
|
args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print(f"\nJSON report: {args.json}")
|
|
return 0 if report["gate_summary"]["enable_recommended"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|