- 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.
126 lines
5.1 KiB
Python
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)
|