Give every memory a holder, an origin, a time and a scenario
An EvidenceAtom could say what was remembered and nothing else: not whose memory it is, not where it came from, not when it was true, not in what situation it may be used. The baseline measured the cost of that: the same ranker scores recall@10 0.412 over the whole corpus and 0.866 inside one holder's memory, so the missing holder column is worth more than any ranking change in the ablation. - schemas.py: MemoryOrigin, TimePrecision, Visibility; EvidenceAtom gains holder_id, origin, scenario, event_time / event_time_end / event_time_precision / event_time_source, valid_from / valid_to, supersedes, visibility, retention_until. `time` is renamed `ingested_at` — ingestion time gets one name, not two. A validator refuses valid_to before valid_from, event_time_end before event_time, and an atom that supersedes itself. - migrations.py: schema v2 with PRAGMA user_version. A first-version database is upgraded in place (ALTER + RENAME COLUMN), rows intact, and a database newer than this build refuses to open instead of being silently downgraded. - v_atoms_current hides memories that are closed, lapsed or not yet begun; unknown bounds read as visible. Time travel is db.atoms_as_of(moment, holder) because a SQLite view cannot take a parameter. - evaluation_schemas.py: the proof and benchmark report shapes move out of schemas.py to keep it under the file-size limit; importers follow. - backend/tests/test_memory_schema.py: 8 tests, including the v1 -> v2 upgrade and the validity timeline. Verification of record: pytest backend/tests bench/tests -q -> 70 passed; ruff check on every touched file -> all checks passed; mypy (fresh cache) on db.py, migrations.py, schemas.py, evaluation_schemas.py and backend/tests -> no issues. evals.py carries 40 pre-existing mypy errors at HEAD and 40 after this change (measured on both copies); its pyright findings are the same debt, and p0-6 rewrites that file. `peft` and `pyarrow.parquet` "could not be resolved" is the documented pyright artifact: pi-lens runs it with 0 config files, and both import at runtime.
This commit is contained in:
1 parent
00dc9f8eaf
commit
181f6bf57e
12 files changed
+1100
-319
No files matched your search
+26
-1
@@ -12,19 +12,21 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from bench.counterpart import (
|
||||
Counterpart,
|
||||
SourceMeta,
|
||||
build_counterparts,
|
||||
build_items,
|
||||
load_item_atoms,
|
||||
load_source_index,
|
||||
)
|
||||
from bench.ingest_baseline import RUNS, database_path
|
||||
from bench.metrics import aggregate, score_query
|
||||
from bench.metrics import aggregate, chance_recall_at_k, score_query
|
||||
from bench.run import Query, git_commit, holder_index, load_queries
|
||||
from bench.sets_common import ROOT
|
||||
from bench.systems import machine_facts
|
||||
@@ -37,6 +39,22 @@ SCOPES = (HOLDER_SCOPE, WHOLE_CORPUS_SCOPE)
|
||||
PROGRESS_EVERY = 500
|
||||
|
||||
|
||||
def chance_rows(index: dict[str, SourceMeta], queries: list[Query]) -> dict[str, Any]:
|
||||
"""What random guessing scores in each candidate set, so a scoped score is not read as skill."""
|
||||
per_holder: dict[str, int] = {}
|
||||
for meta in index.values():
|
||||
per_holder[meta.holder] = per_holder.get(meta.holder, 0) + 1
|
||||
sizes = [per_holder[query.holder] for query in queries if query.holder in per_holder]
|
||||
if not sizes:
|
||||
raise SystemExit("no query holder appears in the corpus index")
|
||||
return {
|
||||
"recall_at_10": chance_recall_at_k(1, len(index), TOP_K),
|
||||
"recall_at_10_holder_scope": sum(chance_recall_at_k(1, size, TOP_K) for size in sizes) / len(sizes),
|
||||
"median_holder_sources": float(statistics.median(sizes)),
|
||||
"note": "expected recall@10 of a random ranking inside the candidate set each row is allowed to see",
|
||||
}
|
||||
|
||||
|
||||
def score_store(store: Counterpart, queries: list[Query], holder_of: dict[str, str], scope: str) -> dict[str, Any]:
|
||||
"""Rank every query against one counterpart store and aggregate the same metrics the arms use.
|
||||
|
||||
@@ -88,6 +106,12 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
"- extraction is lexical for both stores; Mem0's real add() and Zep's graph builder call a "
|
||||
"generator model, which this round does not have"
|
||||
),
|
||||
(
|
||||
f"- chance: random guessing scores recall@10 {report['chance']['recall_at_10']:.5f} over the whole "
|
||||
f"corpus and {report['chance']['recall_at_10_holder_scope']:.4f} inside one holder's memory "
|
||||
f"(median {report['chance']['median_holder_sources']:.0f} sources per holder); read each row against "
|
||||
"its own floor"
|
||||
),
|
||||
"",
|
||||
(
|
||||
"| store | items | closed | recall@10 | hit@10 | hit@10 Wilson 95% lower | MRR | "
|
||||
@@ -166,6 +190,7 @@ def main() -> int:
|
||||
"extraction": "lexical (see bench/counterpart.py); LLM extraction deferred with the generator",
|
||||
"machine": machine_facts(),
|
||||
},
|
||||
"chance": chance_rows(index, queries),
|
||||
"stores": [score_store(store, queries, holder_of, scope) for store in stores for scope in SCOPES],
|
||||
}
|
||||
REPORTS.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in new issue
Block a user