Files
ely-eye/bench/tests/test_counterpart.py
T
ZacharyZhang-NY fc0698c242 Add baseline harness: ingest, metrics, ablation arms, external counterpart, audit
- bench/ingest_baseline.py drives the unmodified product IngestionService over
  all 50,789 corpus files in sharded SQLite DBs, then merges them. Thread and
  memory ceilings are set explicitly: ONNX Runtime defaulted to every core
  (95 threads/worker, load 490) and its CPU arena kept freed pages mapped,
  which OOM-killed workers.
- bench/metrics.py: recall@k, MRR, nDCG@10, Wilson lower bound, chance
  baseline. No model, no product import, so the scorer cannot grade itself.
- bench/systems.py + bench/run.py: the five ablation arms over the merged DB,
  latency percentiles, scale curve, and a top-10 equivalence check against the
  live RetrievalService.
- bench/counterpart.py + bench/run_external.py: in-repo Mem0-style flat store
  and Zep-style temporal store as the external reference point.
- bench/audit.py: enforces single-attempt scoring, frozen n, a chance row
  beside every score, and producer != verifier. It already flags the product's
  retry-until-target loop at backend/src/ely_eye/evals.py:2351.
- backend/pyproject.toml: rapidocr-onnxruntime>=1.4.4 was unsatisfiable
  (PyPI maxes at 1.2.3); the successor is the renamed rapidocr package.
2026-10-06 14:36:02 -04:00

126 lines
5.1 KiB
Python

"""Behaviour checks for the memory counterparts: scoping, dedup, and invalidation are the whole point."""
from __future__ import annotations
from pathlib import Path
import pytest
from bench.counterpart import (
Counterpart,
MemoryItem,
SourceMeta,
build_items,
overlap,
split_items,
subject_of,
supersede,
)
from bench.systems import Atom
MODEL_DIR = Path("/nonexistent-model")
def item(text: str, source_id: str, holder: str, observed_at: str, subject: str = "") -> MemoryItem:
return MemoryItem(
item_id=f"{source_id}#0",
text=text,
source_id=source_id,
holder=holder,
scenario="assistant-memory",
observed_at=observed_at,
subject=subject,
valid_from=observed_at,
)
def test_split_items_cuts_at_the_length_limit() -> None:
chunks = split_items("word " * 300)
assert chunks, "a long text must produce items"
assert all(len(chunk) <= 400 for chunk in chunks)
assert split_items(" ") == []
def test_split_items_merges_short_sentences_into_one_item() -> None:
assert len(split_items("The keys are in the drawer. They are blue.")) == 1
def test_subject_of_needs_a_repeated_content_token() -> None:
assert subject_of("the camera camera camera") == "camera"
assert subject_of("a single camera mention") == ""
assert subject_of("") == ""
def test_overlap_separates_a_restatement_from_a_new_statement() -> None:
assert overlap("the keys are in the blue drawer", "the keys are in the blue drawer") == 1.0
assert overlap("the keys are in the blue drawer", "completely unrelated bicycle garage") < 0.5
assert overlap("", "anything") == 0.0
def test_build_items_dedups_within_a_holder_and_keeps_both_across_holders() -> None:
index = {
"src_a": SourceMeta("src_a", "ana", "assistant-memory", "conversation", "2023-01-01T00:00:00+00:00"),
"src_b": SourceMeta("src_b", "bo", "assistant-memory", "conversation", "2023-01-02T00:00:00+00:00"),
}
atoms = [
Atom(atom_id="a1", source_id="src_a", text="The keys hang on the red door."),
Atom(atom_id="a2", source_id="src_a", text="The keys hang on the red door."),
Atom(atom_id="b1", source_id="src_b", text="The keys hang on the red door."),
]
items = build_items(atoms, index)
assert len(items) == 2
assert {i.holder for i in items} == {"ana", "bo"}
assert all(i.item_id.startswith(f"{i.source_id}#") for i in items)
def test_supersede_closes_the_older_statement_about_the_same_subject() -> None:
older = item("my camera collection is in the attic", "src_1", "ana", "2023-01-01T00:00:00+00:00", "camera")
newer = item("the bicycle is parked in the garage now", "src_2", "ana", "2023-06-01T00:00:00+00:00", "camera")
assert supersede([older, newer]) == 1
assert older.valid_to == "2023-06-01T00:00:00+00:00"
assert newer.valid_to is None
def test_supersede_leaves_a_restatement_alone() -> None:
first = item("the keys hang on the red door", "src_1", "ana", "2023-01-01T00:00:00+00:00", "keys")
again = item("the keys hang on the red door too", "src_2", "ana", "2023-02-01T00:00:00+00:00", "keys")
assert supersede([first, again]) == 0
assert first.valid_to is None
def test_supersede_is_scoped_to_one_holder() -> None:
ana = item("my camera collection is in the attic", "src_1", "ana", "2023-01-01T00:00:00+00:00", "camera")
bo = item("the bicycle is parked in the garage now", "src_2", "bo", "2023-06-01T00:00:00+00:00", "camera")
assert supersede([ana, bo]) == 0
assert ana.valid_to is None
def test_a_query_only_sees_its_own_holders_items() -> None:
ana = item("the keys hang on the red door", "src_ana", "ana", "2023-01-01T00:00:00+00:00")
bo = item("the keys hang on the green door", "src_bo", "bo", "2023-01-01T00:00:00+00:00")
store = Counterpart(name="mem0_flat", temporal=False, items=[ana, bo], model_dir=MODEL_DIR)
mask = store.visible("ana", None)
assert [store.items[i].holder for i, seen in enumerate(mask) if seen] == ["ana"]
def test_the_temporal_store_hides_closed_items_and_the_flat_store_does_not() -> None:
closed = item("the keys hang on the red door", "src_old", "ana", "2023-01-01T00:00:00+00:00")
closed.valid_to = "2023-06-01T00:00:00+00:00"
open_item = item("the keys hang on the green door", "src_new", "ana", "2023-06-01T00:00:00+00:00")
items = [closed, open_item]
flat = Counterpart(name="mem0_flat", temporal=False, items=items, model_dir=MODEL_DIR)
temporal = Counterpart(name="zep_temporal", temporal=True, items=items, model_dir=MODEL_DIR)
as_of = "2023-12-01T00:00:00+00:00"
assert sum(flat.visible("ana", as_of)) == 2
assert [store.source_id for i, store in enumerate(temporal.items) if temporal.visible("ana", as_of)[i]] == [
"src_new"
]
before = "2023-02-01T00:00:00+00:00"
assert sum(temporal.visible("ana", before)) == 1
def test_percentile_needs_timings() -> None:
store = Counterpart(name="mem0_flat", temporal=False, items=[], model_dir=MODEL_DIR)
with pytest.raises(SystemExit, match="no timings recorded"):
store.percentile(0.5)