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
@@ -24,6 +24,15 @@ from .config import Settings, get_settings
|
||||
from .db import Database
|
||||
from .embeddings import EmbeddingService
|
||||
from .evals import HashHopEvaluator, PRDProofSuite
|
||||
from .evaluation_schemas import (
|
||||
BenchmarkPredictionReport,
|
||||
BenchmarkRegistryReport,
|
||||
BenchmarkSampleReport,
|
||||
BenchmarkSourceReport,
|
||||
HashHopProof,
|
||||
ProofCheck,
|
||||
ProofSuiteReport,
|
||||
)
|
||||
from .evidence_graph import EvidenceGraphService
|
||||
from .ingestion import IngestionService
|
||||
from .profiles import runtime_profile_report
|
||||
@@ -33,16 +42,9 @@ from .schemas import (
|
||||
AdapterManifest,
|
||||
AdapterStatus,
|
||||
AnswerResponse,
|
||||
BenchmarkRegistryReport,
|
||||
BenchmarkPredictionReport,
|
||||
BenchmarkSampleReport,
|
||||
BenchmarkSourceReport,
|
||||
CartridgeAssetReport,
|
||||
HashHopProof,
|
||||
MemoryStatus,
|
||||
PathIngestRequest,
|
||||
ProofCheck,
|
||||
ProofSuiteReport,
|
||||
RuntimeProfile,
|
||||
RuntimeProfileReport,
|
||||
SystemStatus,
|
||||
|
||||
@@ -8,7 +8,7 @@ from ast import literal_eval
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .schemas import (
|
||||
from .evaluation_schemas import (
|
||||
BenchmarkPredictionReport,
|
||||
BenchmarkPredictionStatus,
|
||||
BenchmarkRegistryReport,
|
||||
@@ -19,7 +19,6 @@ from .schemas import (
|
||||
BenchmarkSpec,
|
||||
)
|
||||
|
||||
|
||||
STANDARD_BENCHMARKS = (
|
||||
BenchmarkSpec(
|
||||
name="MMMU-Pro",
|
||||
@@ -125,7 +124,7 @@ def benchmark_source_report(timeout: int = 20) -> BenchmarkSourceReport:
|
||||
sibling_count=len(siblings),
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
sources.append(
|
||||
BenchmarkSourceStatus(
|
||||
name=benchmark.name,
|
||||
@@ -217,7 +216,7 @@ def load_benchmark_sample(benchmark: BenchmarkSpec, output_root: Path) -> Benchm
|
||||
sample_sha256=sha256_json(normalized),
|
||||
media_artifacts=media_artifacts,
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
message = str(exc).splitlines()[0]
|
||||
status = "gated" if "gated dataset" in message.lower() or "authenticated" in message.lower() else "failed"
|
||||
return BenchmarkSampleStatus(
|
||||
@@ -274,7 +273,7 @@ def predict_benchmark_sample(benchmark: BenchmarkSpec, output_root: Path, runtim
|
||||
output_tokens=generation.output_tokens,
|
||||
media_artifacts=prediction_media,
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
message = str(exc).splitlines()[0]
|
||||
status = "gated" if "gated dataset" in message.lower() or "authenticated" in message.lower() else "failed"
|
||||
config, split = BENCHMARK_SAMPLE_LOADS.get(benchmark.name, (None, None))
|
||||
@@ -351,7 +350,10 @@ def load_records(path: Path) -> list[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return [json.loads(line) for line in handle if line.strip()]
|
||||
if suffix == ".json":
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"{path}: cannot read the benchmark file ({error})") from error
|
||||
if isinstance(data, list):
|
||||
return [dict(item) for item in data if isinstance(item, dict)]
|
||||
if isinstance(data, dict):
|
||||
|
||||
+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:
|
||||
|
||||
@@ -25,6 +25,11 @@ from .cartridge_assets import CartridgeAssetService, merkle_root
|
||||
from .compiler import ContextCompiler
|
||||
from .config import Settings, get_settings
|
||||
from .db import Database
|
||||
from .evaluation_schemas import (
|
||||
HashHopProof,
|
||||
ProofCheck,
|
||||
ProofSuiteReport,
|
||||
)
|
||||
from .evidence_graph import EvidenceGraphService
|
||||
from .ingestion import SUPPORTED_CODE_EXTENSIONS, build_repository_context, find_git_root
|
||||
from .profiles import runtime_profile_report
|
||||
@@ -36,10 +41,7 @@ from .schemas import (
|
||||
CompiledContext,
|
||||
ContextPlan,
|
||||
EvidenceAtom,
|
||||
HashHopProof,
|
||||
Modality,
|
||||
ProofCheck,
|
||||
ProofSuiteReport,
|
||||
RetrievalHit,
|
||||
RuntimeProfile,
|
||||
TrainingAdapterKind,
|
||||
@@ -56,7 +58,7 @@ from .training import (
|
||||
sha256_file,
|
||||
sha256_tree,
|
||||
)
|
||||
from .verifier import VisualContradictionLens, Verifier
|
||||
from .verifier import Verifier, VisualContradictionLens
|
||||
|
||||
|
||||
class HashHopEvaluator:
|
||||
@@ -101,11 +103,14 @@ class HashHopEvaluator:
|
||||
proof = self.generate_text_proof(hops, token_equivalent)
|
||||
root = Path(proof.artifacts[0]).parent
|
||||
chain_path = root / "hashhop_text.jsonl"
|
||||
records = [
|
||||
json.loads(line)
|
||||
for line in chain_path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
try:
|
||||
chain_text = chain_path.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise ValueError(f"{chain_path}: the HashHop chain is unreadable ({error})") from error
|
||||
try:
|
||||
records = [json.loads(line) for line in chain_text.splitlines() if line.strip()]
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f"{chain_path}: the HashHop chain holds a malformed line ({error})") from error
|
||||
decoys = [
|
||||
{"from": secrets.token_hex(16), "to": secrets.token_hex(16)}
|
||||
for _ in range(max(4, hops * 2))
|
||||
@@ -775,7 +780,7 @@ class PRDProofSuite:
|
||||
f"{citation_report.citation_accuracy:.2f}"
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
payload["error"] = str(exc)
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
@@ -876,7 +881,7 @@ class PRDProofSuite:
|
||||
"last_segment": segments[-1] if segments else None,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
payload["error"] = str(exc)
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
@@ -903,7 +908,10 @@ class PRDProofSuite:
|
||||
graph_path = cartridge_root / manifest.artifacts["temporal_graph"]
|
||||
computed_dna = self.cartridges.compute_dna(cartridge_root)
|
||||
atom_rows = parquet_row_count(atoms_path)
|
||||
sources = json.loads(sources_path.read_text(encoding="utf-8")) if sources_path.exists() else []
|
||||
try:
|
||||
sources = json.loads(sources_path.read_text(encoding="utf-8")) if sources_path.exists() else []
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"{sources_path}: the cartridge source list is unreadable ({error})") from error
|
||||
question = "Which Ely-Eye PRD requirements prove long context memory, HashHop, and adapters?"
|
||||
compiler = ContextCompiler(self.settings, self.db)
|
||||
replay_a = compiler.compile(question, RuntimeProfile.library_100m)
|
||||
@@ -1531,7 +1539,7 @@ def peft_config_loads(path: Path) -> bool:
|
||||
|
||||
PeftConfig.from_pretrained(path)
|
||||
return True
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
return False
|
||||
|
||||
|
||||
@@ -1585,7 +1593,7 @@ def training_payload_contract_ok(payload: dict[str, Any], manifest: AdapterManif
|
||||
and payload.get("precision") == TRAINING_PRECISION
|
||||
and payload.get("framework") == TRAINING_FRAMEWORK
|
||||
and payload.get("optimizer_family") == manifest.optimizer_family
|
||||
and payload.get("gradient_checkpointing") is True
|
||||
and bool(payload.get("gradient_checkpointing"))
|
||||
and payload.get("gradient_checkpointing_mode") == GRADIENT_CHECKPOINTING_MODE
|
||||
and payload.get("autocast_dtype") == AUTOCAST_DTYPE
|
||||
and payload.get("unsloth_version") == manifest.unsloth_version
|
||||
@@ -1876,10 +1884,13 @@ def read_memory_capsule(path: Path) -> dict[str, Any]:
|
||||
prefix = b"ELYMEMCAP1\n"
|
||||
if not data.startswith(prefix):
|
||||
raise ValueError(f"Invalid memory capsule header: {path}")
|
||||
payload = zstd.ZstdDecompressor().decompress(data[len(prefix) :])
|
||||
parsed = json.loads(payload.decode("utf-8"))
|
||||
try:
|
||||
payload = zstd.ZstdDecompressor().decompress(data[len(prefix) :])
|
||||
parsed = json.loads(payload.decode("utf-8"))
|
||||
except (zstd.ZstdError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"Cannot decode the memory capsule at {path}: {error}") from error
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError(f"Invalid memory capsule payload: {path}")
|
||||
raise ValueError(f"Invalid memory capsule payload: {path}") # noqa: TRY004 # the capsule's own bytes are wrong, not the caller's argument
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -1896,11 +1907,14 @@ def hashhop_chain_valid(proof: HashHopProof) -> bool:
|
||||
chain_artifacts = [Path(path) for path in proof.artifacts if path.endswith(".jsonl")]
|
||||
if len(chain_artifacts) != 1 or not chain_artifacts[0].exists():
|
||||
return False
|
||||
rows = [
|
||||
json.loads(line)
|
||||
for line in chain_artifacts[0].read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
try:
|
||||
rows = [
|
||||
json.loads(line)
|
||||
for line in chain_artifacts[0].read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False # an unreadable chain is not a valid chain, and this is a predicate
|
||||
if len(rows) != proof.hops:
|
||||
return False
|
||||
if rows[0]["from"] != proof.query_id or rows[-1]["to"] != proof.expected_target_id:
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Report shapes for the evaluation and proof layer.
|
||||
|
||||
These describe what a run claims, not what the memory holds, so they live apart from the
|
||||
memory and runtime schemas: a proof report changes when the evidence rules change, and the
|
||||
memory model should not move with it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .schemas import utc_now
|
||||
|
||||
|
||||
class BenchmarkSpec(BaseModel):
|
||||
name: str
|
||||
target: str
|
||||
dataset: str
|
||||
task_type: str
|
||||
runner: str
|
||||
metric: str
|
||||
|
||||
class BenchmarkRegistryReport(BaseModel):
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
benchmarks: list[BenchmarkSpec]
|
||||
|
||||
class BenchmarkSourceStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
status: Literal["ready", "unavailable"]
|
||||
sha: str | None = None
|
||||
gated: bool | str | None = None
|
||||
private: bool | None = None
|
||||
sibling_count: int = 0
|
||||
error: str | None = None
|
||||
|
||||
class BenchmarkSourceReport(BaseModel):
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
sources: list[BenchmarkSourceStatus]
|
||||
|
||||
class BenchmarkSampleStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
source_dataset: str | None = None
|
||||
source_sha: str | None = None
|
||||
source_note: str | None = None
|
||||
status: Literal["loaded", "gated", "failed"]
|
||||
config: str | None = None
|
||||
split: str | None = None
|
||||
sample_id: str | None = None
|
||||
fields: list[str] = Field(default_factory=list)
|
||||
question_preview: str | None = None
|
||||
answer_preview: str | None = None
|
||||
sample_sha256: str | None = None
|
||||
media_artifacts: list[str] = Field(default_factory=list)
|
||||
error: str | None = None
|
||||
|
||||
class BenchmarkSampleReport(BaseModel):
|
||||
status: Literal["ready", "partial", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
samples: list[BenchmarkSampleStatus]
|
||||
|
||||
class BenchmarkPredictionStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
status: Literal["predicted", "gated", "failed"]
|
||||
config: str | None = None
|
||||
split: str | None = None
|
||||
sample_id: str | None = None
|
||||
task_type: str
|
||||
metric: str
|
||||
prompt_sha256: str | None = None
|
||||
sample_sha256: str | None = None
|
||||
prediction: str | None = None
|
||||
answer: str | None = None
|
||||
score: float | None = None
|
||||
runtime_backend: str | None = None
|
||||
adapter_id: str | None = None
|
||||
adapter_sha256: str | None = None
|
||||
input_tokens: int | None = None
|
||||
output_tokens: int | None = None
|
||||
media_artifacts: list[str] = Field(default_factory=list)
|
||||
error: str | None = None
|
||||
|
||||
class BenchmarkPredictionReport(BaseModel):
|
||||
status: Literal["ready", "partial", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
predictions: list[BenchmarkPredictionStatus]
|
||||
|
||||
class HashHopProof(BaseModel):
|
||||
proof_id: str
|
||||
kind: Literal["text_hashhop", "visual_hashhop"]
|
||||
hops: int
|
||||
token_equivalent: int
|
||||
query_id: str
|
||||
expected_target_id: str
|
||||
model_target_id: str | None = None
|
||||
passed: bool | None = None
|
||||
artifacts: list[str]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
|
||||
class ProofCheck(BaseModel):
|
||||
name: str
|
||||
requirement: str
|
||||
status: Literal["passed", "failed"]
|
||||
evidence: list[str]
|
||||
detail: str
|
||||
|
||||
class ProofSuiteReport(BaseModel):
|
||||
proof_id: str
|
||||
cartridge_id: str
|
||||
status: Literal["passed", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
checks: list[ProofCheck]
|
||||
artifacts: dict[str, str]
|
||||
dna_before: str | None = None
|
||||
dna_after: str | None = None
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Schema DDL and versioned migrations for the memory store.
|
||||
|
||||
A memory row must answer four questions before retrieval can trust it: whose memory
|
||||
is it, where did it come from, when was it true, and in what situation may it be used.
|
||||
The first version of this table answered none of them, so every consumer guessed.
|
||||
|
||||
Versioning uses PRAGMA user_version. A database written by the first version is
|
||||
upgraded in place rather than rebuilt, because the only copy of the memory is the file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
SCHEMA_VERSION = 2
|
||||
|
||||
ATOM_COLUMNS_V2 = """
|
||||
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,
|
||||
ingested_at 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,
|
||||
holder_id TEXT NOT NULL DEFAULT '',
|
||||
origin TEXT NOT NULL DEFAULT 'artifact'
|
||||
CHECK (origin IN ('self', 'observed', 'artifact', 'third_party')),
|
||||
scenario TEXT NOT NULL DEFAULT '',
|
||||
event_time TEXT,
|
||||
event_time_end TEXT,
|
||||
event_time_precision TEXT NOT NULL DEFAULT 'unknown'
|
||||
CHECK (event_time_precision IN ('instant', 'minute', 'hour', 'day', 'month', 'year', 'unknown')),
|
||||
event_time_source TEXT NOT NULL DEFAULT '',
|
||||
valid_from TEXT,
|
||||
valid_to TEXT,
|
||||
supersedes_json TEXT NOT NULL DEFAULT '[]',
|
||||
visibility_json TEXT NOT NULL DEFAULT '{}',
|
||||
retention_until TEXT
|
||||
"""
|
||||
|
||||
_BASE_TABLES = """
|
||||
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 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
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS atom_fts USING fts5(
|
||||
atom_id UNINDEXED,
|
||||
source,
|
||||
text,
|
||||
tokenize = 'unicode61'
|
||||
);
|
||||
"""
|
||||
|
||||
# A memory is usable now when nothing has closed it, it had begun by now, and its
|
||||
# retention has not lapsed. Unknown bounds read as visible: absence of evidence about
|
||||
# when a legacy row was true is not evidence that it is false. The clock is formatted to
|
||||
# match how Python writes an aware datetime, so the comparison is exact to the millisecond.
|
||||
VIEWS = """
|
||||
CREATE VIEW IF NOT EXISTS v_atoms_current AS
|
||||
SELECT * FROM atoms
|
||||
WHERE valid_to IS NULL
|
||||
AND (valid_from IS NULL OR valid_from <= strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'))
|
||||
AND (retention_until IS NULL OR retention_until >= strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'));
|
||||
|
||||
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 INDEX IF NOT EXISTS idx_atoms_holder ON atoms(holder_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_atoms_scenario ON atoms(scenario);
|
||||
CREATE INDEX IF NOT EXISTS idx_atoms_valid_to ON atoms(valid_to);
|
||||
"""
|
||||
|
||||
# Columns the first version lacked, in the order they are added. Every one is nullable
|
||||
# or has a default, so an existing row survives the upgrade with its meaning intact.
|
||||
_V1_TO_V2_COLUMNS = (
|
||||
("holder_id", "TEXT NOT NULL DEFAULT ''"),
|
||||
("origin", "TEXT NOT NULL DEFAULT 'artifact'"),
|
||||
("scenario", "TEXT NOT NULL DEFAULT ''"),
|
||||
("event_time", "TEXT"),
|
||||
("event_time_end", "TEXT"),
|
||||
("event_time_precision", "TEXT NOT NULL DEFAULT 'unknown'"),
|
||||
("event_time_source", "TEXT NOT NULL DEFAULT ''"),
|
||||
("valid_from", "TEXT"),
|
||||
("valid_to", "TEXT"),
|
||||
("supersedes_json", "TEXT NOT NULL DEFAULT '[]'"),
|
||||
("visibility_json", "TEXT NOT NULL DEFAULT '{}'"),
|
||||
("retention_until", "TEXT"),
|
||||
)
|
||||
|
||||
|
||||
def _table_exists(connection: sqlite3.Connection, name: str) -> bool:
|
||||
row = connection.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (name,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def _column_names(connection: sqlite3.Connection, table: str) -> set[str]:
|
||||
return {str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})")}
|
||||
|
||||
|
||||
def _create_v2(connection: sqlite3.Connection) -> None:
|
||||
connection.executescript(_BASE_TABLES)
|
||||
connection.execute(f"CREATE TABLE IF NOT EXISTS atoms ({ATOM_COLUMNS_V2})")
|
||||
|
||||
|
||||
def _upgrade_v1_to_v2(connection: sqlite3.Connection) -> None:
|
||||
"""Add the four memory dimensions to a first-version atoms table.
|
||||
|
||||
The old `time` column recorded when ingestion ran, which is the only timestamp a
|
||||
legacy row honestly has, so it is renamed rather than duplicated.
|
||||
"""
|
||||
columns = _column_names(connection, "atoms")
|
||||
if "time" in columns and "ingested_at" not in columns:
|
||||
connection.execute("ALTER TABLE atoms RENAME COLUMN time TO ingested_at")
|
||||
columns = _column_names(connection, "atoms")
|
||||
if "ingested_at" not in columns:
|
||||
raise SystemExit(
|
||||
"atoms table has neither `time` nor `ingested_at`; refusing to guess which timestamp is ingestion"
|
||||
)
|
||||
for name, definition in _V1_TO_V2_COLUMNS:
|
||||
if name not in columns:
|
||||
connection.execute(f"ALTER TABLE atoms ADD COLUMN {name} {definition}")
|
||||
|
||||
|
||||
def apply(connection: sqlite3.Connection) -> None:
|
||||
"""Bring the schema to SCHEMA_VERSION, in place, without losing rows."""
|
||||
version = int(connection.execute("PRAGMA user_version").fetchone()[0])
|
||||
if version > SCHEMA_VERSION:
|
||||
raise SystemExit(f"database schema is version {version}, this build understands {SCHEMA_VERSION}")
|
||||
if version == SCHEMA_VERSION:
|
||||
return
|
||||
|
||||
if _table_exists(connection, "atoms"):
|
||||
connection.executescript(_BASE_TABLES)
|
||||
if version < 2:
|
||||
_upgrade_v1_to_v2(connection)
|
||||
else:
|
||||
_create_v2(connection)
|
||||
|
||||
connection.executescript(VIEWS)
|
||||
connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
||||
+55
-146
@@ -1,16 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
return datetime.now(UTC)
|
||||
|
||||
class RuntimeProfile(StrEnum):
|
||||
live_demo = "live_demo"
|
||||
@@ -18,7 +17,6 @@ class RuntimeProfile(StrEnum):
|
||||
library_100m = "library_100m"
|
||||
research_theater = "research_theater"
|
||||
|
||||
|
||||
class TrainingAdapterKind(StrEnum):
|
||||
hme_core_lora = "hme_core_lora"
|
||||
hme_vision_lora = "hme_vision_lora"
|
||||
@@ -27,7 +25,6 @@ class TrainingAdapterKind(StrEnum):
|
||||
hme_router = "hme_router"
|
||||
hme_retrieval = "hme_retrieval"
|
||||
|
||||
|
||||
class Modality(StrEnum):
|
||||
text = "text"
|
||||
code = "code"
|
||||
@@ -38,6 +35,36 @@ class Modality(StrEnum):
|
||||
ui_screenshot = "ui_screenshot"
|
||||
mixed = "mixed"
|
||||
|
||||
class MemoryOrigin(StrEnum):
|
||||
"""Where a memory came from, which decides how much it may be trusted.
|
||||
|
||||
A file the user handed over is not the same kind of claim as something the system
|
||||
watched happen, and neither is a statement the user made about themselves.
|
||||
"""
|
||||
|
||||
self_ = "self"
|
||||
observed = "observed"
|
||||
artifact = "artifact"
|
||||
third_party = "third_party"
|
||||
|
||||
class TimePrecision(StrEnum):
|
||||
"""How finely the event time is actually known, so a day is never read as an instant."""
|
||||
|
||||
instant = "instant"
|
||||
minute = "minute"
|
||||
hour = "hour"
|
||||
day = "day"
|
||||
month = "month"
|
||||
year = "year"
|
||||
unknown = "unknown"
|
||||
|
||||
class Visibility(BaseModel):
|
||||
"""Who may use a memory. Retention lives on the atom itself because the current
|
||||
memory view has to filter on it."""
|
||||
|
||||
scope: Literal["holder", "visible_to", "everyone"] = "holder"
|
||||
holders: list[str] = Field(default_factory=list)
|
||||
purpose: str | None = None
|
||||
|
||||
class LayoutBox(BaseModel):
|
||||
x: float
|
||||
@@ -47,19 +74,17 @@ class LayoutBox(BaseModel):
|
||||
page: int | None = None
|
||||
frame_second: float | None = None
|
||||
|
||||
|
||||
class TrustScores(BaseModel):
|
||||
parser: float = Field(ge=0.0, le=1.0)
|
||||
ocr: float = Field(default=0.0, ge=0.0, le=1.0)
|
||||
model: float = Field(default=0.0, ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class EvidenceAtom(BaseModel):
|
||||
atom_id: str
|
||||
modality: Modality
|
||||
source_id: str
|
||||
source: str
|
||||
time: datetime = Field(default_factory=utc_now)
|
||||
ingested_at: datetime = Field(default_factory=utc_now)
|
||||
text: str = ""
|
||||
image_ref: str | None = None
|
||||
embedding_refs: list[str] = Field(default_factory=list)
|
||||
@@ -68,7 +93,28 @@ class EvidenceAtom(BaseModel):
|
||||
trust: TrustScores
|
||||
token_equivalent: int = Field(default=0, ge=0)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
holder_id: str = ""
|
||||
origin: MemoryOrigin = MemoryOrigin.artifact
|
||||
scenario: str = ""
|
||||
event_time: datetime | None = None
|
||||
event_time_end: datetime | None = None
|
||||
event_time_precision: TimePrecision = TimePrecision.unknown
|
||||
event_time_source: str = ""
|
||||
valid_from: datetime | None = None
|
||||
valid_to: datetime | None = None
|
||||
supersedes: list[str] = Field(default_factory=list)
|
||||
visibility: Visibility = Field(default_factory=Visibility)
|
||||
retention_until: datetime | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _times_are_ordered(self) -> EvidenceAtom:
|
||||
if self.event_time and self.event_time_end and self.event_time_end < self.event_time:
|
||||
raise ValueError(f"{self.atom_id}: event_time_end precedes event_time")
|
||||
if self.valid_from and self.valid_to and self.valid_to < self.valid_from:
|
||||
raise ValueError(f"{self.atom_id}: valid_to precedes valid_from")
|
||||
if self.atom_id in self.supersedes:
|
||||
raise ValueError(f"{self.atom_id}: an atom cannot supersede itself")
|
||||
return self
|
||||
|
||||
class SourceRecord(BaseModel):
|
||||
source_id: str
|
||||
@@ -80,7 +126,6 @@ class SourceRecord(BaseModel):
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class IngestResponse(BaseModel):
|
||||
source_count: int
|
||||
atom_count: int
|
||||
@@ -88,7 +133,6 @@ class IngestResponse(BaseModel):
|
||||
token_equivalent: int
|
||||
sources: list[SourceRecord]
|
||||
|
||||
|
||||
class RetrievalHit(BaseModel):
|
||||
atom: EvidenceAtom
|
||||
sparse_score: float
|
||||
@@ -96,7 +140,6 @@ class RetrievalHit(BaseModel):
|
||||
graph_score: float = 0.0
|
||||
final_score: float
|
||||
|
||||
|
||||
class ContextPlan(BaseModel):
|
||||
question: str
|
||||
profile: RuntimeProfile
|
||||
@@ -107,7 +150,6 @@ class ContextPlan(BaseModel):
|
||||
compression_strategy: str
|
||||
verifier_contract: list[str]
|
||||
|
||||
|
||||
class CompiledContext(BaseModel):
|
||||
plan: ContextPlan
|
||||
hits: list[RetrievalHit]
|
||||
@@ -115,7 +157,6 @@ class CompiledContext(BaseModel):
|
||||
token_equivalent: int
|
||||
cache_trace_id: str
|
||||
|
||||
|
||||
class CitationReport(BaseModel):
|
||||
cited_atom_ids: list[str]
|
||||
missing_atom_ids: list[str]
|
||||
@@ -123,7 +164,6 @@ class CitationReport(BaseModel):
|
||||
contradiction_notes: list[str]
|
||||
confidence: float
|
||||
|
||||
|
||||
class AnswerResponse(BaseModel):
|
||||
answer: str
|
||||
citations: list[str]
|
||||
@@ -131,7 +171,6 @@ class AnswerResponse(BaseModel):
|
||||
verifier: CitationReport
|
||||
runtime_backend: str
|
||||
|
||||
|
||||
class CacheLayerStatus(BaseModel):
|
||||
layer: str
|
||||
entries: int
|
||||
@@ -140,7 +179,6 @@ class CacheLayerStatus(BaseModel):
|
||||
miss_count: int
|
||||
last_event_at: datetime | None
|
||||
|
||||
|
||||
class MemoryStatus(BaseModel):
|
||||
active_atoms: int
|
||||
active_token_equivalent: int
|
||||
@@ -148,7 +186,6 @@ class MemoryStatus(BaseModel):
|
||||
library_token_equivalent: int
|
||||
layers: list[CacheLayerStatus]
|
||||
|
||||
|
||||
class RuntimeStatus(BaseModel):
|
||||
backend: str
|
||||
model_id: str
|
||||
@@ -163,7 +200,6 @@ class RuntimeStatus(BaseModel):
|
||||
vram_total_mb: int | None = None
|
||||
vram_used_mb: int | None = None
|
||||
|
||||
|
||||
class SystemStatus(BaseModel):
|
||||
app: str = "Ely-Eye"
|
||||
version: str
|
||||
@@ -172,7 +208,6 @@ class SystemStatus(BaseModel):
|
||||
profiles: dict[str, dict[str, Any]]
|
||||
data_home: str
|
||||
|
||||
|
||||
class CartridgeManifest(BaseModel):
|
||||
cartridge_id: str
|
||||
name: str
|
||||
@@ -184,7 +219,6 @@ class CartridgeManifest(BaseModel):
|
||||
artifacts: dict[str, str]
|
||||
dna: str | None = None
|
||||
|
||||
|
||||
class AdapterManifest(BaseModel):
|
||||
adapter_id: str
|
||||
kind: TrainingAdapterKind
|
||||
@@ -248,7 +282,6 @@ class AdapterManifest(BaseModel):
|
||||
sha256: str
|
||||
cartridge_id: str | None = None
|
||||
|
||||
|
||||
class AdapterStatus(BaseModel):
|
||||
name: str
|
||||
kind: TrainingAdapterKind
|
||||
@@ -299,7 +332,6 @@ class AdapterStatus(BaseModel):
|
||||
adapter_max_abs: float | None = None
|
||||
adapter_weights_finite: bool = False
|
||||
|
||||
|
||||
class CartridgeAssetStatus(BaseModel):
|
||||
name: str
|
||||
path: str
|
||||
@@ -309,99 +341,12 @@ class CartridgeAssetStatus(BaseModel):
|
||||
dtype: str
|
||||
status: Literal["ready", "missing"]
|
||||
|
||||
|
||||
class CartridgeAssetReport(BaseModel):
|
||||
cartridge_id: str
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
assets: list[CartridgeAssetStatus]
|
||||
|
||||
|
||||
class BenchmarkSpec(BaseModel):
|
||||
name: str
|
||||
target: str
|
||||
dataset: str
|
||||
task_type: str
|
||||
runner: str
|
||||
metric: str
|
||||
|
||||
|
||||
class BenchmarkRegistryReport(BaseModel):
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
benchmarks: list[BenchmarkSpec]
|
||||
|
||||
|
||||
class BenchmarkSourceStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
status: Literal["ready", "unavailable"]
|
||||
sha: str | None = None
|
||||
gated: bool | str | None = None
|
||||
private: bool | None = None
|
||||
sibling_count: int = 0
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class BenchmarkSourceReport(BaseModel):
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
sources: list[BenchmarkSourceStatus]
|
||||
|
||||
|
||||
class BenchmarkSampleStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
source_dataset: str | None = None
|
||||
source_sha: str | None = None
|
||||
source_note: str | None = None
|
||||
status: Literal["loaded", "gated", "failed"]
|
||||
config: str | None = None
|
||||
split: str | None = None
|
||||
sample_id: str | None = None
|
||||
fields: list[str] = Field(default_factory=list)
|
||||
question_preview: str | None = None
|
||||
answer_preview: str | None = None
|
||||
sample_sha256: str | None = None
|
||||
media_artifacts: list[str] = Field(default_factory=list)
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class BenchmarkSampleReport(BaseModel):
|
||||
status: Literal["ready", "partial", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
samples: list[BenchmarkSampleStatus]
|
||||
|
||||
|
||||
class BenchmarkPredictionStatus(BaseModel):
|
||||
name: str
|
||||
dataset: str
|
||||
status: Literal["predicted", "gated", "failed"]
|
||||
config: str | None = None
|
||||
split: str | None = None
|
||||
sample_id: str | None = None
|
||||
task_type: str
|
||||
metric: str
|
||||
prompt_sha256: str | None = None
|
||||
sample_sha256: str | None = None
|
||||
prediction: str | None = None
|
||||
answer: str | None = None
|
||||
score: float | None = None
|
||||
runtime_backend: str | None = None
|
||||
adapter_id: str | None = None
|
||||
adapter_sha256: str | None = None
|
||||
input_tokens: int | None = None
|
||||
output_tokens: int | None = None
|
||||
media_artifacts: list[str] = Field(default_factory=list)
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class BenchmarkPredictionReport(BaseModel):
|
||||
status: Literal["ready", "partial", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
predictions: list[BenchmarkPredictionStatus]
|
||||
|
||||
|
||||
class RuntimeLaunchProfile(BaseModel):
|
||||
name: str
|
||||
runtime: str
|
||||
@@ -412,7 +357,6 @@ class RuntimeLaunchProfile(BaseModel):
|
||||
environment: dict[str, str] = Field(default_factory=dict)
|
||||
required_features: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RuntimeProfileValidation(BaseModel):
|
||||
name: str
|
||||
runtime: str
|
||||
@@ -428,51 +372,16 @@ class RuntimeProfileValidation(BaseModel):
|
||||
probe_output: str | None = None
|
||||
detail: str
|
||||
|
||||
|
||||
class RuntimeProfileReport(BaseModel):
|
||||
status: Literal["ready", "incomplete"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
profiles: list[RuntimeLaunchProfile]
|
||||
validations: list[RuntimeProfileValidation] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HashHopProof(BaseModel):
|
||||
proof_id: str
|
||||
kind: Literal["text_hashhop", "visual_hashhop"]
|
||||
hops: int
|
||||
token_equivalent: int
|
||||
query_id: str
|
||||
expected_target_id: str
|
||||
model_target_id: str | None = None
|
||||
passed: bool | None = None
|
||||
artifacts: list[str]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
|
||||
|
||||
class ProofCheck(BaseModel):
|
||||
name: str
|
||||
requirement: str
|
||||
status: Literal["passed", "failed"]
|
||||
evidence: list[str]
|
||||
detail: str
|
||||
|
||||
|
||||
class ProofSuiteReport(BaseModel):
|
||||
proof_id: str
|
||||
cartridge_id: str
|
||||
status: Literal["passed", "failed"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
checks: list[ProofCheck]
|
||||
artifacts: dict[str, str]
|
||||
dna_before: str | None = None
|
||||
dna_after: str | None = None
|
||||
|
||||
|
||||
class UploadIngestRequest(BaseModel):
|
||||
cartridge_name: str | None = None
|
||||
dense_embeddings: bool | None = None
|
||||
|
||||
|
||||
class PathIngestRequest(BaseModel):
|
||||
path: Path
|
||||
cartridge_name: str | None = None
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""The memory model must carry holder, origin, time and scenario, and an old database must survive."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ely_eye.config import Settings
|
||||
from ely_eye.db import Database
|
||||
from ely_eye.migrations import SCHEMA_VERSION
|
||||
from ely_eye.schemas import (
|
||||
EvidenceAtom,
|
||||
MemoryOrigin,
|
||||
Modality,
|
||||
SourceRecord,
|
||||
TimePrecision,
|
||||
TrustScores,
|
||||
Visibility,
|
||||
)
|
||||
|
||||
NOW = datetime(2026, 5, 1, 12, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def _atom(atom_id: str, **overrides: object) -> EvidenceAtom:
|
||||
fields: dict[str, object] = {
|
||||
"atom_id": atom_id,
|
||||
"modality": Modality.text,
|
||||
"source_id": "src-1",
|
||||
"source": "notes/key.txt",
|
||||
"ingested_at": NOW,
|
||||
"text": "钥匙放在玄关抽屉里",
|
||||
"trust": TrustScores(parser=1.0),
|
||||
"token_equivalent": 8,
|
||||
"holder_id": "wife",
|
||||
"origin": MemoryOrigin.self_,
|
||||
"scenario": "home",
|
||||
"event_time": NOW - timedelta(days=1),
|
||||
"event_time_precision": TimePrecision.day,
|
||||
"event_time_source": "declared",
|
||||
"valid_from": NOW - timedelta(days=1),
|
||||
}
|
||||
fields.update(overrides)
|
||||
return EvidenceAtom(**fields) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _database(home: Path) -> Database:
|
||||
return Database(Settings(home=home))
|
||||
|
||||
|
||||
def _seed_source(database: Database) -> None:
|
||||
database.upsert_sources(
|
||||
[
|
||||
SourceRecord(
|
||||
source_id="src-1",
|
||||
path="/tmp/key.txt",
|
||||
kind="text",
|
||||
mime="text/plain",
|
||||
sha256="0" * 64,
|
||||
size_bytes=12,
|
||||
created_at=NOW,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _atom_columns(database: Database) -> set[str]:
|
||||
with database.connect() as connection:
|
||||
return {row[1] for row in connection.execute("PRAGMA table_info(atoms)")}
|
||||
|
||||
|
||||
def test_a_fresh_database_carries_the_four_memory_dimensions(tmp_path: Path) -> None:
|
||||
database = _database(tmp_path)
|
||||
columns = _atom_columns(database)
|
||||
for column in (
|
||||
"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",
|
||||
"ingested_at",
|
||||
):
|
||||
assert column in columns, column
|
||||
assert "time" not in columns, "ingestion time has one name: ingested_at"
|
||||
with database.connect() as connection:
|
||||
assert int(connection.execute("PRAGMA user_version").fetchone()[0]) == SCHEMA_VERSION
|
||||
|
||||
|
||||
def test_an_atom_round_trips_every_memory_dimension(tmp_path: Path) -> None:
|
||||
database = _database(tmp_path)
|
||||
_seed_source(database)
|
||||
atom = _atom(
|
||||
"atom-1",
|
||||
event_time_end=NOW - timedelta(hours=6),
|
||||
valid_to=NOW + timedelta(days=2),
|
||||
supersedes=["atom-0"],
|
||||
visibility=Visibility(scope="visible_to", holders=["husband"], purpose="household"),
|
||||
retention_until=NOW + timedelta(days=30),
|
||||
)
|
||||
database.upsert_atoms([atom])
|
||||
stored = database.get_atoms(["atom-1"])[0]
|
||||
assert stored.holder_id == "wife"
|
||||
assert stored.origin is MemoryOrigin.self_
|
||||
assert stored.scenario == "home"
|
||||
assert stored.event_time == atom.event_time
|
||||
assert stored.event_time_end == atom.event_time_end
|
||||
assert stored.event_time_precision is TimePrecision.day
|
||||
assert stored.event_time_source == "declared"
|
||||
assert stored.valid_from == atom.valid_from
|
||||
assert stored.valid_to == atom.valid_to
|
||||
assert stored.supersedes == ["atom-0"]
|
||||
assert stored.visibility.holders == ["husband"]
|
||||
assert stored.visibility.purpose == "household"
|
||||
assert stored.retention_until == atom.retention_until
|
||||
assert stored.ingested_at == NOW
|
||||
|
||||
|
||||
def test_a_first_version_database_upgrades_in_place_and_keeps_its_rows(tmp_path: Path) -> None:
|
||||
path = tmp_path / "data" / "ely_eye.sqlite"
|
||||
path.parent.mkdir(parents=True)
|
||||
connection = sqlite3.connect(path)
|
||||
connection.executescript(
|
||||
"""
|
||||
CREATE TABLE 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 atoms (
|
||||
atom_id TEXT PRIMARY KEY, source_id TEXT NOT NULL, 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
|
||||
);
|
||||
INSERT INTO atoms VALUES (
|
||||
'legacy-1', 'src-1', 'text', 'old.txt', '2026-01-02T03:04:05+00:00', 'old memory',
|
||||
NULL, '[]', NULL, '[]', '{"parser": 1.0}', 4, '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
database = _database(tmp_path)
|
||||
assert _atom_columns(database) >= {"holder_id", "origin", "valid_from", "ingested_at"}
|
||||
legacy = database.get_atoms(["legacy-1"])[0]
|
||||
assert legacy.text == "old memory"
|
||||
assert legacy.ingested_at == datetime.fromisoformat("2026-01-02T03:04:05+00:00")
|
||||
assert legacy.holder_id == "", "a legacy row has no known holder, and must not invent one"
|
||||
assert legacy.origin is MemoryOrigin.artifact
|
||||
|
||||
|
||||
def test_v_atoms_current_hides_closed_retired_and_unbegun_memories(tmp_path: Path) -> None:
|
||||
"""The view answers 'what is usable right now', so it is written against the real clock."""
|
||||
moment = datetime.now(UTC)
|
||||
database = _database(tmp_path)
|
||||
_seed_source(database)
|
||||
database.upsert_atoms(
|
||||
[
|
||||
_atom("live"),
|
||||
_atom("closed", valid_to=moment - timedelta(days=1)),
|
||||
_atom("lapsed", retention_until=moment - timedelta(days=1)),
|
||||
_atom("future", valid_from=moment + timedelta(days=5)),
|
||||
]
|
||||
)
|
||||
with database.connect() as connection:
|
||||
visible = {row["atom_id"] for row in connection.execute("SELECT atom_id FROM v_atoms_current")}
|
||||
assert visible == {"live"}
|
||||
assert database.atom_count() == 4, "the view hides, it does not delete"
|
||||
|
||||
|
||||
def test_an_unknown_origin_is_refused_by_the_database(tmp_path: Path) -> None:
|
||||
database = _database(tmp_path)
|
||||
_seed_source(database)
|
||||
with database.connect() as connection, pytest.raises(sqlite3.IntegrityError):
|
||||
connection.execute(
|
||||
"INSERT INTO atoms (atom_id, source_id, modality, source, ingested_at, text, "
|
||||
"embedding_refs_json, relations_json, trust_json, token_equivalent, metadata_json, origin) "
|
||||
"VALUES ('x', 'src-1', 'text', 's', '2026-05-01T12:00:00+00:00', 't', '[]', '[]', "
|
||||
"'{\"parser\": 1.0}', 1, '{}', 'whispered')"
|
||||
)
|
||||
|
||||
|
||||
def test_the_schema_refuses_a_database_newer_than_this_build(tmp_path: Path) -> None:
|
||||
database = _database(tmp_path)
|
||||
with database.connect() as connection:
|
||||
connection.execute("PRAGMA user_version = 99")
|
||||
with pytest.raises(SystemExit, match="understands"):
|
||||
_database(tmp_path)
|
||||
|
||||
|
||||
def test_times_must_be_ordered_and_an_atom_cannot_supersede_itself() -> None:
|
||||
with pytest.raises(ValueError, match="valid_to precedes valid_from"):
|
||||
_atom("bad-1", valid_from=NOW, valid_to=NOW - timedelta(days=1))
|
||||
with pytest.raises(ValueError, match="event_time_end precedes event_time"):
|
||||
_atom("bad-2", event_time=NOW, event_time_end=NOW - timedelta(days=1))
|
||||
with pytest.raises(ValueError, match="cannot supersede itself"):
|
||||
_atom("bad-3", supersedes=["bad-3"])
|
||||
|
||||
|
||||
def test_atoms_as_of_walks_the_validity_timeline(tmp_path: Path) -> None:
|
||||
"""Time travel is a method because a SQLite view cannot take a parameter."""
|
||||
database = _database(tmp_path)
|
||||
_seed_source(database)
|
||||
database.upsert_atoms(
|
||||
[
|
||||
_atom("live"),
|
||||
_atom("closed", valid_from=NOW - timedelta(days=10), valid_to=NOW - timedelta(days=1)),
|
||||
_atom("future", valid_from=NOW + timedelta(days=5)),
|
||||
]
|
||||
)
|
||||
assert {atom.atom_id for atom in database.atoms_as_of(NOW)} == {"live"}
|
||||
assert {atom.atom_id for atom in database.atoms_as_of(NOW - timedelta(days=2))} == {"closed"}
|
||||
assert database.atoms_as_of(NOW, holder="nobody") == []
|
||||
assert {atom.atom_id for atom in database.atoms_as_of(NOW, holder="wife")} == {"live"}
|
||||
Reference in new issue
Block a user