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
+107
-100
@@ -2,14 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .config import Settings, get_settings
|
||||
from .schemas import EvidenceAtom, SourceRecord
|
||||
from .migrations import apply as apply_migrations
|
||||
from .schemas import EvidenceAtom, MemoryOrigin, SourceRecord, TimePrecision, utc_now
|
||||
|
||||
|
||||
def _json(value: Any) -> str:
|
||||
@@ -19,7 +20,16 @@ def _json(value: Any) -> str:
|
||||
def _loads(value: str | None, default: Any) -> Any:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return json.loads(value)
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f"stored JSON is unreadable: {error}") from error
|
||||
|
||||
|
||||
def _datetime(value: str | None) -> datetime | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return datetime.fromisoformat(value)
|
||||
|
||||
|
||||
class Database:
|
||||
@@ -30,7 +40,7 @@ class Database:
|
||||
self.init()
|
||||
|
||||
@contextmanager
|
||||
def connect(self) -> Iterable[sqlite3.Connection]:
|
||||
def connect(self) -> Iterator[sqlite3.Connection]:
|
||||
connection = sqlite3.connect(self.path)
|
||||
connection.row_factory = sqlite3.Row
|
||||
connection.execute("PRAGMA foreign_keys = ON")
|
||||
@@ -43,76 +53,7 @@ class Database:
|
||||
|
||||
def init(self) -> None:
|
||||
with sqlite3.connect(self.path) as connection:
|
||||
connection.executescript(
|
||||
"""
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sources (
|
||||
source_id TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
mime TEXT NOT NULL,
|
||||
sha256 TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
metadata_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS atoms (
|
||||
atom_id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL REFERENCES sources(source_id) ON DELETE CASCADE,
|
||||
modality TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
time TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
image_ref TEXT,
|
||||
embedding_refs_json TEXT NOT NULL,
|
||||
layout_json TEXT,
|
||||
relations_json TEXT NOT NULL,
|
||||
trust_json TEXT NOT NULL,
|
||||
token_equivalent INTEGER NOT NULL,
|
||||
metadata_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_atoms_source_id ON atoms(source_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_atoms_modality ON atoms(modality);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS atom_fts USING fts5(
|
||||
atom_id UNINDEXED,
|
||||
source,
|
||||
text,
|
||||
tokenize = 'unicode61'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cartridges (
|
||||
cartridge_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
root_path TEXT NOT NULL,
|
||||
manifest_json TEXT NOT NULL,
|
||||
dna TEXT,
|
||||
token_equivalent INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts TEXT NOT NULL,
|
||||
layer TEXT NOT NULL,
|
||||
cache_key TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
bytes_estimate INTEGER NOT NULL,
|
||||
metadata_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS evidence_edges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_atom_id TEXT NOT NULL,
|
||||
to_atom_id TEXT NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
confidence REAL NOT NULL,
|
||||
metadata_json TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
apply_migrations(connection)
|
||||
|
||||
def upsert_sources(self, sources: list[SourceRecord]) -> None:
|
||||
with self.connect() as connection:
|
||||
@@ -150,15 +91,17 @@ class Database:
|
||||
connection.executemany(
|
||||
"""
|
||||
INSERT INTO atoms (
|
||||
atom_id, source_id, modality, source, time, text, image_ref,
|
||||
atom_id, source_id, modality, source, ingested_at, text, image_ref,
|
||||
embedding_refs_json, layout_json, relations_json, trust_json,
|
||||
token_equivalent, metadata_json
|
||||
token_equivalent, metadata_json, holder_id, origin, scenario,
|
||||
event_time, event_time_end, event_time_precision, event_time_source,
|
||||
valid_from, valid_to, supersedes_json, visibility_json, retention_until
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(atom_id) DO UPDATE SET
|
||||
modality=excluded.modality,
|
||||
source=excluded.source,
|
||||
time=excluded.time,
|
||||
ingested_at=excluded.ingested_at,
|
||||
text=excluded.text,
|
||||
image_ref=excluded.image_ref,
|
||||
embedding_refs_json=excluded.embedding_refs_json,
|
||||
@@ -166,7 +109,19 @@ class Database:
|
||||
relations_json=excluded.relations_json,
|
||||
trust_json=excluded.trust_json,
|
||||
token_equivalent=excluded.token_equivalent,
|
||||
metadata_json=excluded.metadata_json
|
||||
metadata_json=excluded.metadata_json,
|
||||
holder_id=excluded.holder_id,
|
||||
origin=excluded.origin,
|
||||
scenario=excluded.scenario,
|
||||
event_time=excluded.event_time,
|
||||
event_time_end=excluded.event_time_end,
|
||||
event_time_precision=excluded.event_time_precision,
|
||||
event_time_source=excluded.event_time_source,
|
||||
valid_from=excluded.valid_from,
|
||||
valid_to=excluded.valid_to,
|
||||
supersedes_json=excluded.supersedes_json,
|
||||
visibility_json=excluded.visibility_json,
|
||||
retention_until=excluded.retention_until
|
||||
""",
|
||||
[self._atom_params(atom) for atom in atoms],
|
||||
)
|
||||
@@ -182,7 +137,7 @@ class Database:
|
||||
atom.source_id,
|
||||
atom.modality.value,
|
||||
atom.source,
|
||||
atom.time.isoformat(),
|
||||
atom.ingested_at.isoformat(),
|
||||
atom.text,
|
||||
atom.image_ref,
|
||||
_json(atom.embedding_refs),
|
||||
@@ -191,25 +146,63 @@ class Database:
|
||||
_json(atom.trust.model_dump()),
|
||||
atom.token_equivalent,
|
||||
_json(atom.metadata),
|
||||
atom.holder_id,
|
||||
atom.origin.value,
|
||||
atom.scenario,
|
||||
atom.event_time.isoformat() if atom.event_time else None,
|
||||
atom.event_time_end.isoformat() if atom.event_time_end else None,
|
||||
atom.event_time_precision.value,
|
||||
atom.event_time_source,
|
||||
atom.valid_from.isoformat() if atom.valid_from else None,
|
||||
atom.valid_to.isoformat() if atom.valid_to else None,
|
||||
_json(atom.supersedes),
|
||||
_json(atom.visibility.model_dump()),
|
||||
atom.retention_until.isoformat() if atom.retention_until else None,
|
||||
)
|
||||
|
||||
def list_atoms(self, limit: int | None = None) -> list[EvidenceAtom]:
|
||||
query = "SELECT * FROM atoms ORDER BY time DESC"
|
||||
params: tuple[Any, ...] = ()
|
||||
if limit is not None:
|
||||
query += " LIMIT ?"
|
||||
params = (limit,)
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(query, params).fetchall()
|
||||
if limit is None:
|
||||
rows = connection.execute("SELECT * FROM atoms ORDER BY ingested_at DESC").fetchall()
|
||||
else:
|
||||
rows = connection.execute(
|
||||
"SELECT * FROM atoms ORDER BY ingested_at DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [self._row_to_atom(row) for row in rows]
|
||||
|
||||
def atoms_as_of(self, moment: datetime, holder: str | None = None) -> list[EvidenceAtom]:
|
||||
"""Memories that were open at `moment`, the question a parameterless view cannot ask.
|
||||
|
||||
Same predicate as v_atoms_current minus the retention filter: retention says how long
|
||||
the system keeps a memory, not whether it was true then. Unknown bounds read as open.
|
||||
"""
|
||||
stamp = moment.isoformat()
|
||||
with self.connect() as connection:
|
||||
if holder is None:
|
||||
rows = connection.execute(
|
||||
"SELECT * FROM atoms "
|
||||
"WHERE (valid_from IS NULL OR valid_from <= ?) "
|
||||
"AND (valid_to IS NULL OR valid_to > ?) "
|
||||
"ORDER BY ingested_at DESC",
|
||||
(stamp, stamp),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = connection.execute(
|
||||
"SELECT * FROM atoms "
|
||||
"WHERE holder_id = ? "
|
||||
"AND (valid_from IS NULL OR valid_from <= ?) "
|
||||
"AND (valid_to IS NULL OR valid_to > ?) "
|
||||
"ORDER BY ingested_at DESC",
|
||||
(holder, stamp, stamp),
|
||||
).fetchall()
|
||||
return [self._row_to_atom(row) for row in rows]
|
||||
|
||||
def get_atoms(self, atom_ids: list[str]) -> list[EvidenceAtom]:
|
||||
if not atom_ids:
|
||||
return []
|
||||
placeholders = ",".join("?" for _ in atom_ids)
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"SELECT * FROM atoms WHERE atom_id IN ({placeholders})", atom_ids
|
||||
"SELECT * FROM atoms WHERE atom_id IN (SELECT value FROM json_each(?))", (_json(atom_ids),)
|
||||
).fetchall()
|
||||
by_id = {row["atom_id"]: self._row_to_atom(row) for row in rows}
|
||||
return [by_id[atom_id] for atom_id in atom_ids if atom_id in by_id]
|
||||
@@ -318,7 +311,7 @@ class Database:
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
datetime.utcnow().isoformat(),
|
||||
utc_now().isoformat(),
|
||||
layer,
|
||||
cache_key,
|
||||
event,
|
||||
@@ -349,16 +342,18 @@ class Database:
|
||||
)
|
||||
|
||||
def list_evidence_edges(self, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
query = (
|
||||
"SELECT from_atom_id, to_atom_id, relation, confidence, metadata_json "
|
||||
"FROM evidence_edges ORDER BY id DESC"
|
||||
)
|
||||
params: tuple[Any, ...] = ()
|
||||
if limit is not None:
|
||||
query += " LIMIT ?"
|
||||
params = (limit,)
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(query, params).fetchall()
|
||||
if limit is None:
|
||||
rows = connection.execute(
|
||||
"SELECT from_atom_id, to_atom_id, relation, confidence, metadata_json "
|
||||
"FROM evidence_edges ORDER BY id DESC"
|
||||
).fetchall()
|
||||
else:
|
||||
rows = connection.execute(
|
||||
"SELECT from_atom_id, to_atom_id, relation, confidence, metadata_json "
|
||||
"FROM evidence_edges ORDER BY id DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
"from_atom_id": row["from_atom_id"],
|
||||
@@ -387,7 +382,7 @@ class Database:
|
||||
).fetchall()
|
||||
|
||||
def _row_to_atom(self, row: sqlite3.Row) -> EvidenceAtom:
|
||||
from .schemas import LayoutBox, Modality, TrustScores
|
||||
from .schemas import LayoutBox, Modality, TrustScores, Visibility
|
||||
|
||||
layout_data = _loads(row["layout_json"], None)
|
||||
return EvidenceAtom(
|
||||
@@ -395,7 +390,7 @@ class Database:
|
||||
modality=Modality(row["modality"]),
|
||||
source_id=row["source_id"],
|
||||
source=row["source"],
|
||||
time=datetime.fromisoformat(row["time"]),
|
||||
ingested_at=datetime.fromisoformat(row["ingested_at"]),
|
||||
text=row["text"],
|
||||
image_ref=row["image_ref"],
|
||||
embedding_refs=_loads(row["embedding_refs_json"], []),
|
||||
@@ -404,6 +399,18 @@ class Database:
|
||||
trust=TrustScores(**_loads(row["trust_json"], {"parser": 0.0})),
|
||||
token_equivalent=int(row["token_equivalent"]),
|
||||
metadata=_loads(row["metadata_json"], {}),
|
||||
holder_id=row["holder_id"],
|
||||
origin=MemoryOrigin(row["origin"]),
|
||||
scenario=row["scenario"],
|
||||
event_time=_datetime(row["event_time"]),
|
||||
event_time_end=_datetime(row["event_time_end"]),
|
||||
event_time_precision=TimePrecision(row["event_time_precision"]),
|
||||
event_time_source=row["event_time_source"],
|
||||
valid_from=_datetime(row["valid_from"]),
|
||||
valid_to=_datetime(row["valid_to"]),
|
||||
supersedes=_loads(row["supersedes_json"], []),
|
||||
visibility=Visibility(**_loads(row["visibility_json"], {})),
|
||||
retention_until=_datetime(row["retention_until"]),
|
||||
)
|
||||
|
||||
def _row_to_source(self, row: sqlite3.Row) -> SourceRecord:
|
||||
|
||||
Reference in new issue
Block a user