grafomem 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aml/__init__.py +3 -0
- aml/adapter_check.py +136 -0
- aml/backends/__init__.py +1 -0
- aml/backends/bi_temporal.py +225 -0
- aml/backends/bounded_vector.py +197 -0
- aml/backends/conflict_backends.py +333 -0
- aml/backends/cross_session_backends.py +267 -0
- aml/backends/delete_backends.py +254 -0
- aml/backends/gmp_reference.py +285 -0
- aml/backends/interface.py +517 -0
- aml/backends/isolation_backends.py +524 -0
- aml/backends/persistence.py +143 -0
- aml/backends/retention_backends.py +215 -0
- aml/backends/sqlite_gmp.py +525 -0
- aml/backends/supersession_chain.py +187 -0
- aml/backends/tenant_backends.py +251 -0
- aml/backends/vector_only.py +226 -0
- aml/cli.py +627 -0
- aml/eval/__init__.py +1 -0
- aml/eval/concurrency.py +456 -0
- aml/eval/concurrency_runner.py +320 -0
- aml/eval/conformance.py +545 -0
- aml/eval/harness.py +279 -0
- aml/eval/metrics.py +268 -0
- aml/eval/report.py +307 -0
- aml/generator/__init__.py +1 -0
- aml/generator/oracle.py +502 -0
- aml/generator/trace.py +1020 -0
- aml/generator/validators.py +447 -0
- aml/generator/workloads/__init__.py +1 -0
- aml/generator/workloads/w1.py +369 -0
- aml/generator/workloads/w10.py +412 -0
- aml/generator/workloads/w2.py +280 -0
- aml/generator/workloads/w3.py +269 -0
- aml/generator/workloads/w4.py +234 -0
- aml/generator/workloads/w5.py +253 -0
- aml/generator/workloads/w6.py +254 -0
- aml/generator/workloads/w7.py +259 -0
- aml/generator/workloads/w8.py +242 -0
- aml/generator/workloads/w9.py +309 -0
- aml/provenance.py +114 -0
- aml/server/__init__.py +1 -0
- aml/server/app.py +362 -0
- aml/server/auth.py +102 -0
- aml/server/ingestion.py +244 -0
- aml/server/mcp.py +266 -0
- aml/server/stores.py +113 -0
- aml/wire.py +446 -0
- grafomem-0.2.0.dist-info/METADATA +227 -0
- grafomem-0.2.0.dist-info/RECORD +54 -0
- grafomem-0.2.0.dist-info/WHEEL +5 -0
- grafomem-0.2.0.dist-info/entry_points.txt +2 -0
- grafomem-0.2.0.dist-info/licenses/LICENSE +21 -0
- grafomem-0.2.0.dist-info/top_level.txt +1 -0
aml/__init__.py
ADDED
aml/adapter_check.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GRAFOMEM adapter pre-flight checker.
|
|
3
|
+
|
|
4
|
+
grafomem check -b my_module:MyBackend
|
|
5
|
+
|
|
6
|
+
Quick structural validation BEFORE running the full conformance suite:
|
|
7
|
+
checks Protocol compliance, method signatures, capability coherence,
|
|
8
|
+
and basic round-trip. Fails fast with actionable error messages.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_adapter(cls: type) -> list[str]:
|
|
18
|
+
"""Run pre-flight checks against a backend class. Returns a list of errors.
|
|
19
|
+
Empty list means the adapter is structurally conformant."""
|
|
20
|
+
errors: list[str] = []
|
|
21
|
+
|
|
22
|
+
# 1. Check required methods exist
|
|
23
|
+
required_methods = ["capabilities", "write", "retrieve", "delete",
|
|
24
|
+
"supersede", "audit", "flush"]
|
|
25
|
+
for method in required_methods:
|
|
26
|
+
if not hasattr(cls, method):
|
|
27
|
+
errors.append(f"Missing required method: {method}()")
|
|
28
|
+
elif not callable(getattr(cls, method)):
|
|
29
|
+
errors.append(f"{method} exists but is not callable")
|
|
30
|
+
|
|
31
|
+
if errors:
|
|
32
|
+
return errors # can't proceed without basic methods
|
|
33
|
+
|
|
34
|
+
# 2. Try to instantiate
|
|
35
|
+
instance = None
|
|
36
|
+
try:
|
|
37
|
+
# Check if __init__ needs arguments beyond self
|
|
38
|
+
sig = inspect.signature(cls.__init__)
|
|
39
|
+
params = [p for p in sig.parameters.values()
|
|
40
|
+
if p.name != "self" and p.default is inspect.Parameter.empty]
|
|
41
|
+
if params:
|
|
42
|
+
errors.append(
|
|
43
|
+
f"Constructor requires arguments: {[p.name for p in params]}. "
|
|
44
|
+
f"The conformance suite needs a zero-arg factory. Consider using "
|
|
45
|
+
f"a wrapper lambda: lambda: MyBackend(arg1, arg2)"
|
|
46
|
+
)
|
|
47
|
+
return errors
|
|
48
|
+
instance = cls()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
errors.append(f"Failed to instantiate {cls.__name__}(): {e}")
|
|
51
|
+
return errors
|
|
52
|
+
|
|
53
|
+
# 3. Check capabilities() returns set[Capability]
|
|
54
|
+
try:
|
|
55
|
+
from aml.backends.interface import Capability
|
|
56
|
+
caps = instance.capabilities()
|
|
57
|
+
if not isinstance(caps, set):
|
|
58
|
+
errors.append(f"capabilities() returned {type(caps).__name__}, expected set")
|
|
59
|
+
else:
|
|
60
|
+
for c in caps:
|
|
61
|
+
if not isinstance(c, Capability):
|
|
62
|
+
errors.append(f"capabilities() contains {c!r} which is not a Capability enum member")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
errors.append(f"capabilities() raised: {e}")
|
|
65
|
+
|
|
66
|
+
# 4. Check Protocol compliance
|
|
67
|
+
try:
|
|
68
|
+
from aml.backends.interface import MemoryBackend
|
|
69
|
+
if not isinstance(instance, MemoryBackend):
|
|
70
|
+
errors.append(
|
|
71
|
+
f"{cls.__name__} does not satisfy the MemoryBackend Protocol. "
|
|
72
|
+
f"Check method signatures match the Protocol definition."
|
|
73
|
+
)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
errors.append(f"Protocol check failed: {e}")
|
|
76
|
+
|
|
77
|
+
# 5. Basic round-trip: write + retrieve
|
|
78
|
+
try:
|
|
79
|
+
from aml.backends.interface import WriteOptions, RetrieveOptions
|
|
80
|
+
ref = instance.write("test content", WriteOptions())
|
|
81
|
+
if ref is None:
|
|
82
|
+
errors.append("write() returned None; must return a MemoryRef")
|
|
83
|
+
instance.flush()
|
|
84
|
+
mems = instance.retrieve("test", RetrieveOptions(budget_tokens=1024))
|
|
85
|
+
if not isinstance(mems, list):
|
|
86
|
+
errors.append(f"retrieve() returned {type(mems).__name__}, expected list")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
errors.append(f"Basic write/retrieve round-trip failed: {e}")
|
|
89
|
+
|
|
90
|
+
return errors
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_check(cls: type) -> bool:
|
|
94
|
+
"""Run and print pre-flight check results. Returns True if all pass."""
|
|
95
|
+
print(f"GRAFOMEM adapter check — {cls.__name__}\n")
|
|
96
|
+
|
|
97
|
+
errors = check_adapter(cls)
|
|
98
|
+
|
|
99
|
+
if not errors:
|
|
100
|
+
# Also show declared capabilities
|
|
101
|
+
try:
|
|
102
|
+
instance = cls()
|
|
103
|
+
caps = instance.capabilities()
|
|
104
|
+
print(f"✓ Protocol compliance OK")
|
|
105
|
+
print(f"✓ Method signatures OK")
|
|
106
|
+
print(f"✓ Write/retrieve round-trip OK")
|
|
107
|
+
print(f"✓ Declared capabilities: {{{', '.join(sorted(c.value for c in caps))}}}")
|
|
108
|
+
print(f"\nAdapter is structurally conformant. Run `grafomem conformance` for full suite.")
|
|
109
|
+
except Exception:
|
|
110
|
+
print(f"✓ Structural checks passed")
|
|
111
|
+
return True
|
|
112
|
+
else:
|
|
113
|
+
for e in errors:
|
|
114
|
+
print(f"✗ {e}")
|
|
115
|
+
print(f"\n{len(errors)} error(s). Fix these before running the conformance suite.")
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Smoke
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
from aml.backends.gmp_reference import GMPReferenceBackend
|
|
125
|
+
from aml.backends.vector_only import _stub_embedder, VectorOnlyBackend
|
|
126
|
+
from aml.backends.persistence import PersistenceBackend
|
|
127
|
+
|
|
128
|
+
for cls_factory in [
|
|
129
|
+
lambda: PersistenceBackend,
|
|
130
|
+
lambda: type("BadBackend", (), {}), # deliberately broken
|
|
131
|
+
]:
|
|
132
|
+
cls = cls_factory()
|
|
133
|
+
print_check(cls)
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
print("✓ Adapter check module smoke green.")
|
aml/backends/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Backend adapters + the MemoryBackend interface contract."""
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GRAFOMEM bi_temporal reference adapter.
|
|
3
|
+
|
|
4
|
+
Same pinned BGE-small vector store as vector_only / supersession_chain, but it
|
|
5
|
+
keeps a valid-time interval [valid_from, valid_until) per version and resolves
|
|
6
|
+
retrieve(as_of=t) to the version valid at t. This is a strict superset of
|
|
7
|
+
supersession_chain:
|
|
8
|
+
|
|
9
|
+
- as_of = None -> current query: candidates are open-interval heads
|
|
10
|
+
(valid_until is None) -> identical to supersession_chain.
|
|
11
|
+
- as_of = t -> historical query: candidates are versions whose interval
|
|
12
|
+
contains t (valid_from <= t < valid_until) -> one version
|
|
13
|
+
per chain, the slice that was valid then.
|
|
14
|
+
|
|
15
|
+
It learns each interval the way the trace defines it (w2.py): a version's
|
|
16
|
+
valid_from arrives on write/supersede via WriteOptions; its valid_until is the
|
|
17
|
+
NEXT version's valid_from, which the harness hands to supersede() as the new
|
|
18
|
+
version's valid_from. So supersede(old, new) closes old at opts.valid_from and
|
|
19
|
+
reconstructs the oracle's contiguous windows exactly, with no extra plumbing.
|
|
20
|
+
|
|
21
|
+
Why it matters: historical (as_of) queries are N/A for every non-BI_TEMPORAL
|
|
22
|
+
backend (the harness skips them, E1). This adapter is the only one that can
|
|
23
|
+
answer them, so on W2 those queries flip from unscored to scored — the second
|
|
24
|
+
capability axis. Claims {BI_TEMPORAL, SUPERSESSION_CHAIN, AUDIT}; it must claim
|
|
25
|
+
SUPERSESSION_CHAIN so the harness dispatches supersede() and the intervals get
|
|
26
|
+
closed. Requires grafomem[backends].
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Iterator
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
from aml.backends.interface import (
|
|
37
|
+
Capability,
|
|
38
|
+
CapabilityNotSupported,
|
|
39
|
+
Memory,
|
|
40
|
+
RetrieveOptions,
|
|
41
|
+
WriteOptions,
|
|
42
|
+
)
|
|
43
|
+
from aml.backends.vector_only import REFERENCE_MODEL, EmbedFn, _default_embedder
|
|
44
|
+
|
|
45
|
+
__grafomem_interface__ = "0.1.1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BiTemporalVectorBackend:
|
|
49
|
+
"""BGE vector store with per-version valid-time intervals + as_of resolution."""
|
|
50
|
+
|
|
51
|
+
__grafomem_interface__ = "0.1.1"
|
|
52
|
+
__grafomem_adapter_metadata__ = {
|
|
53
|
+
"underlying_system": "reference",
|
|
54
|
+
"embedding_model": REFERENCE_MODEL,
|
|
55
|
+
"vector_store": "numpy-bruteforce-cosine-exact",
|
|
56
|
+
"temporal_policy": "valid-time intervals [valid_from, valid_until); "
|
|
57
|
+
"as_of resolves to the version valid at t",
|
|
58
|
+
"notes": "Same embedder as vector_only/supersession_chain; the "
|
|
59
|
+
"difference is the BI_TEMPORAL capability, not the model.",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def __init__(self, embed_fn: EmbedFn | None = None) -> None:
|
|
63
|
+
self._embed_fn = embed_fn
|
|
64
|
+
self._store: dict[int, Memory] = {}
|
|
65
|
+
self._vec: dict[int, np.ndarray] = {}
|
|
66
|
+
self._vfrom: dict[int, datetime | None] = {}
|
|
67
|
+
self._vuntil: dict[int, datetime | None] = {} # None = open (head)
|
|
68
|
+
self._next = 0
|
|
69
|
+
|
|
70
|
+
def _embed(self, texts: list[str]) -> np.ndarray:
|
|
71
|
+
if self._embed_fn is None:
|
|
72
|
+
self._embed_fn = _default_embedder()
|
|
73
|
+
return self._embed_fn(texts)
|
|
74
|
+
|
|
75
|
+
def capabilities(self) -> set[Capability]:
|
|
76
|
+
return {Capability.BI_TEMPORAL, Capability.SUPERSESSION_CHAIN,
|
|
77
|
+
Capability.AUDIT}
|
|
78
|
+
|
|
79
|
+
def write(self, content: str, options: WriteOptions) -> int:
|
|
80
|
+
if options.tenant_id is not None:
|
|
81
|
+
raise CapabilityNotSupported(Capability.MULTI_TENANT, "write")
|
|
82
|
+
if options.signing_key is not None:
|
|
83
|
+
raise CapabilityNotSupported(
|
|
84
|
+
Capability.CRYPTOGRAPHIC_PROVENANCE, "write")
|
|
85
|
+
ref = self._next
|
|
86
|
+
self._next += 1
|
|
87
|
+
self._store[ref] = Memory(
|
|
88
|
+
ref=ref, content=content,
|
|
89
|
+
written_at=datetime.now(tz=timezone.utc),
|
|
90
|
+
metadata=dict(options.metadata),
|
|
91
|
+
)
|
|
92
|
+
self._vec[ref] = self._embed([content])[0]
|
|
93
|
+
self._vfrom[ref] = options.valid_from # None for non-temporal workloads
|
|
94
|
+
self._vuntil[ref] = None # open until superseded
|
|
95
|
+
return ref
|
|
96
|
+
|
|
97
|
+
def supersede(self, old_ref, content: str, options: WriteOptions) -> int:
|
|
98
|
+
# New version's valid_from == predecessor's valid_until (contiguous, w2.py).
|
|
99
|
+
new_ref = self.write(content, options)
|
|
100
|
+
if old_ref in self._store:
|
|
101
|
+
self._vuntil[old_ref] = options.valid_from
|
|
102
|
+
self._store[old_ref].superseded_by = new_ref
|
|
103
|
+
return new_ref
|
|
104
|
+
|
|
105
|
+
def delete(self, ref) -> bool:
|
|
106
|
+
# No deletes in W1-W6; a bi-temporal tombstone would close the interval
|
|
107
|
+
# with no successor, but no workload exercises it, so we don't claim it.
|
|
108
|
+
raise CapabilityNotSupported(Capability.HARD_DELETE, "delete")
|
|
109
|
+
|
|
110
|
+
def _valid_at(self, ref: int, t: datetime | None) -> bool:
|
|
111
|
+
if t is None: # current: open intervals only
|
|
112
|
+
return self._vuntil[ref] is None
|
|
113
|
+
vf, vu = self._vfrom[ref], self._vuntil[ref]
|
|
114
|
+
return (vf is None or vf <= t) and (vu is None or t < vu)
|
|
115
|
+
|
|
116
|
+
def retrieve(self, query: str, options: RetrieveOptions) -> list[Memory]:
|
|
117
|
+
if options.tenant_id is not None:
|
|
118
|
+
raise CapabilityNotSupported(Capability.MULTI_TENANT, "retrieve")
|
|
119
|
+
t = options.as_of
|
|
120
|
+
refs = [r for r in self._store if self._valid_at(r, t)]
|
|
121
|
+
if not refs:
|
|
122
|
+
return []
|
|
123
|
+
qv = self._embed([query])[0]
|
|
124
|
+
mat = np.stack([self._vec[r] for r in refs])
|
|
125
|
+
sims = mat @ qv
|
|
126
|
+
order = sorted(range(len(refs)),
|
|
127
|
+
key=lambda i: (-float(sims[i]), refs[i]))
|
|
128
|
+
out: list[Memory] = []
|
|
129
|
+
used = 0
|
|
130
|
+
for i in order:
|
|
131
|
+
m = self._store[refs[i]]
|
|
132
|
+
cost = len(m.content)
|
|
133
|
+
if used + cost > options.budget_tokens:
|
|
134
|
+
break
|
|
135
|
+
out.append(m)
|
|
136
|
+
used += cost
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
def audit(self) -> Iterator[Memory]:
|
|
140
|
+
return iter(list(self._store.values()))
|
|
141
|
+
|
|
142
|
+
def flush(self) -> None:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ============================================================================
|
|
147
|
+
# Smoke check — run `python -m aml.backends.bi_temporal`
|
|
148
|
+
# ============================================================================
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
from datetime import timedelta
|
|
152
|
+
|
|
153
|
+
from aml.backends.interface import MemoryBackend
|
|
154
|
+
from aml.backends.vector_only import VectorOnlyBackend, _stub_embedder
|
|
155
|
+
from aml.eval.harness import run_trace
|
|
156
|
+
from aml.generator.trace import Difficulty
|
|
157
|
+
from aml.generator.workloads.w2 import generate_w2
|
|
158
|
+
|
|
159
|
+
print("GRAFOMEM bi_temporal.py — valid-time as_of resolution (STUB embedder)\n")
|
|
160
|
+
|
|
161
|
+
b = BiTemporalVectorBackend(embed_fn=_stub_embedder())
|
|
162
|
+
|
|
163
|
+
assert isinstance(b, MemoryBackend)
|
|
164
|
+
assert b.capabilities() == {Capability.BI_TEMPORAL,
|
|
165
|
+
Capability.SUPERSESSION_CHAIN, Capability.AUDIT}
|
|
166
|
+
print("✓ Protocol + capabilities ({BI_TEMPORAL, SUPERSESSION_CHAIN, AUDIT})")
|
|
167
|
+
|
|
168
|
+
# Hand-built chain with explicit contiguous windows:
|
|
169
|
+
# Rome [t0, t1) Milan [t1, t2) Turin [t2, open)
|
|
170
|
+
t0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
|
171
|
+
t1 = t0 + timedelta(days=30)
|
|
172
|
+
t2 = t0 + timedelta(days=60)
|
|
173
|
+
r0 = b.write("Aria lives in Rome", WriteOptions(valid_from=t0))
|
|
174
|
+
r1 = b.supersede(r0, "Aria lives in Milan", WriteOptions(valid_from=t1))
|
|
175
|
+
r2 = b.supersede(r1, "Aria lives in Turin", WriteOptions(valid_from=t2))
|
|
176
|
+
b.flush()
|
|
177
|
+
|
|
178
|
+
q = "Where does Aria live?"
|
|
179
|
+
now = [m.content for m in b.retrieve(q, RetrieveOptions(as_of=None))]
|
|
180
|
+
assert now == ["Aria lives in Turin"], now
|
|
181
|
+
print("✓ as_of=None -> head (Turin)")
|
|
182
|
+
|
|
183
|
+
# Time-travel: midpoint of each window resolves to that version.
|
|
184
|
+
for label, t, expected in (
|
|
185
|
+
("inside Rome window ", t0 + timedelta(days=15), "Aria lives in Rome"),
|
|
186
|
+
("inside Milan window", t1 + timedelta(days=15), "Aria lives in Milan"),
|
|
187
|
+
("inside Turin window", t2 + timedelta(days=15), "Aria lives in Turin"),
|
|
188
|
+
):
|
|
189
|
+
got = [m.content for m in b.retrieve(q, RetrieveOptions(as_of=t))]
|
|
190
|
+
assert got == [expected], f"{label}: {got}"
|
|
191
|
+
print("✓ as_of=t -> version valid at t (Rome / Milan / Turin by window)")
|
|
192
|
+
|
|
193
|
+
audited = {m.content: m for m in b.audit()}
|
|
194
|
+
assert len(audited) == 3 and audited["Aria lives in Milan"].superseded_by == r2
|
|
195
|
+
print("✓ audit() yields full history (3 versions, superseded_by linked)")
|
|
196
|
+
|
|
197
|
+
for op, call in (
|
|
198
|
+
("tenant", lambda: b.retrieve("x", RetrieveOptions(tenant_id="t"))),
|
|
199
|
+
("delete", lambda: b.delete(r0)),
|
|
200
|
+
):
|
|
201
|
+
try:
|
|
202
|
+
call()
|
|
203
|
+
except CapabilityNotSupported:
|
|
204
|
+
pass
|
|
205
|
+
else:
|
|
206
|
+
raise AssertionError(f"{op}: expected CapabilityNotSupported")
|
|
207
|
+
print("✓ Capability guards (tenant -> no MULTI_TENANT; delete -> no HARD_DELETE)")
|
|
208
|
+
|
|
209
|
+
# Reclamation on a real W2 hard trace, through the actual harness:
|
|
210
|
+
# bi_temporal answers every query (n_na=0); vector_only skips all historical.
|
|
211
|
+
tr = generate_w2(seed=0, difficulty=Difficulty.HARD)
|
|
212
|
+
bt_run = run_trace(BiTemporalVectorBackend(embed_fn=_stub_embedder()),
|
|
213
|
+
tr, budget_tokens=512)
|
|
214
|
+
vec_run = run_trace(VectorOnlyBackend(embed_fn=_stub_embedder()),
|
|
215
|
+
tr, budget_tokens=512)
|
|
216
|
+
assert bt_run.n_na == 0, f"bi_temporal should answer all queries, n_na={bt_run.n_na}"
|
|
217
|
+
assert vec_run.n_na > 0, "vector_only should skip historical queries"
|
|
218
|
+
reclaimed = vec_run.n_na
|
|
219
|
+
answered = len(bt_run.per_query)
|
|
220
|
+
print(f"✓ Reclaims historical queries (bi_temporal n_na=0 / {answered} scored; "
|
|
221
|
+
f"vector_only n_na={reclaimed})")
|
|
222
|
+
|
|
223
|
+
print(f"\nAll bi_temporal smoke checks green. {reclaimed} historical queries that are "
|
|
224
|
+
f"N/A for every\nother backend are now answerable. Wire into run_w2 for the "
|
|
225
|
+
f"current/historical split.")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GRAFOMEM bounded_vector reference adapter.
|
|
3
|
+
|
|
4
|
+
Same pinned BGE-small vector store as vector_only, but with a fixed capacity:
|
|
5
|
+
on write, once the store exceeds `capacity`, the OLDEST memory is evicted
|
|
6
|
+
(FIFO — a recency window). This is the realistic "context window" model an agent
|
|
7
|
+
lives inside: bounded footprint and bounded per-query scan cost, at the price of
|
|
8
|
+
forgetting anything older than the window.
|
|
9
|
+
|
|
10
|
+
On W4 (Long-Horizon Dependencies) this produces the recall-vs-footprint
|
|
11
|
+
tradeoff: a query for a fact d facts back succeeds only if d < capacity (else it
|
|
12
|
+
was evicted), so recall cliffs at d = capacity — a structural fact, independent
|
|
13
|
+
of the embedder — while footprint and scan cost plateau at `capacity` instead of
|
|
14
|
+
growing linearly with the horizon like an unbounded store.
|
|
15
|
+
|
|
16
|
+
Claims {AUDIT}: no temporal, supersession, or hard-delete semantics. Eviction is
|
|
17
|
+
capacity management, not a HARD_DELETE (the caller did not request removal), and
|
|
18
|
+
is reflected in audit() — which exposes exactly the retained window.
|
|
19
|
+
Requires grafomem[backends].
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections import deque
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from aml.backends.interface import (
|
|
31
|
+
Capability,
|
|
32
|
+
CapabilityNotSupported,
|
|
33
|
+
Memory,
|
|
34
|
+
RetrieveOptions,
|
|
35
|
+
WriteOptions,
|
|
36
|
+
)
|
|
37
|
+
from aml.backends.vector_only import REFERENCE_MODEL, EmbedFn, _default_embedder
|
|
38
|
+
|
|
39
|
+
__grafomem_interface__ = "0.1.1"
|
|
40
|
+
|
|
41
|
+
DEFAULT_CAPACITY = 64
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BoundedVectorBackend:
|
|
45
|
+
"""BGE vector store with a fixed-capacity recency window (FIFO eviction)."""
|
|
46
|
+
|
|
47
|
+
__grafomem_interface__ = "0.1.1"
|
|
48
|
+
|
|
49
|
+
def __init__(self, capacity: int = DEFAULT_CAPACITY,
|
|
50
|
+
embed_fn: EmbedFn | None = None) -> None:
|
|
51
|
+
if capacity < 1:
|
|
52
|
+
raise ValueError("capacity must be >= 1")
|
|
53
|
+
self.capacity = capacity
|
|
54
|
+
self._embed_fn = embed_fn
|
|
55
|
+
self._store: dict[int, Memory] = {}
|
|
56
|
+
self._vec: dict[int, np.ndarray] = {}
|
|
57
|
+
self._order: deque[int] = deque() # FIFO eviction order
|
|
58
|
+
self._next = 0
|
|
59
|
+
self._evicted = 0 # bookkeeping (not retrievable)
|
|
60
|
+
|
|
61
|
+
__grafomem_adapter_metadata__ = {
|
|
62
|
+
"underlying_system": "reference",
|
|
63
|
+
"embedding_model": REFERENCE_MODEL,
|
|
64
|
+
"vector_store": "numpy-bruteforce-cosine-exact",
|
|
65
|
+
"retention_policy": "fixed-capacity recency window, FIFO eviction",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def _embed(self, texts: list[str]) -> np.ndarray:
|
|
69
|
+
if self._embed_fn is None:
|
|
70
|
+
self._embed_fn = _default_embedder()
|
|
71
|
+
return self._embed_fn(texts)
|
|
72
|
+
|
|
73
|
+
def capabilities(self) -> set[Capability]:
|
|
74
|
+
return {Capability.AUDIT}
|
|
75
|
+
|
|
76
|
+
def write(self, content: str, options: WriteOptions) -> int:
|
|
77
|
+
if options.tenant_id is not None:
|
|
78
|
+
raise CapabilityNotSupported(Capability.MULTI_TENANT, "write")
|
|
79
|
+
if options.signing_key is not None:
|
|
80
|
+
raise CapabilityNotSupported(
|
|
81
|
+
Capability.CRYPTOGRAPHIC_PROVENANCE, "write")
|
|
82
|
+
ref = self._next
|
|
83
|
+
self._next += 1
|
|
84
|
+
self._store[ref] = Memory(
|
|
85
|
+
ref=ref, content=content,
|
|
86
|
+
written_at=datetime.now(tz=timezone.utc),
|
|
87
|
+
metadata=dict(options.metadata),
|
|
88
|
+
)
|
|
89
|
+
self._vec[ref] = self._embed([content])[0]
|
|
90
|
+
self._order.append(ref)
|
|
91
|
+
while len(self._order) > self.capacity: # evict oldest
|
|
92
|
+
old = self._order.popleft()
|
|
93
|
+
del self._store[old]
|
|
94
|
+
del self._vec[old]
|
|
95
|
+
self._evicted += 1
|
|
96
|
+
return ref
|
|
97
|
+
|
|
98
|
+
def supersede(self, old_ref, content: str, options: WriteOptions) -> int:
|
|
99
|
+
raise CapabilityNotSupported(Capability.SUPERSESSION_CHAIN, "supersede")
|
|
100
|
+
|
|
101
|
+
def delete(self, ref) -> bool:
|
|
102
|
+
raise CapabilityNotSupported(Capability.HARD_DELETE, "delete")
|
|
103
|
+
|
|
104
|
+
def retrieve(self, query: str, options: RetrieveOptions) -> list[Memory]:
|
|
105
|
+
if options.as_of is not None:
|
|
106
|
+
raise CapabilityNotSupported(Capability.BI_TEMPORAL, "retrieve")
|
|
107
|
+
if options.tenant_id is not None:
|
|
108
|
+
raise CapabilityNotSupported(Capability.MULTI_TENANT, "retrieve")
|
|
109
|
+
refs = list(self._store.keys()) # retained window only
|
|
110
|
+
if not refs:
|
|
111
|
+
return []
|
|
112
|
+
qv = self._embed([query])[0]
|
|
113
|
+
mat = np.stack([self._vec[r] for r in refs])
|
|
114
|
+
sims = mat @ qv
|
|
115
|
+
order = sorted(range(len(refs)),
|
|
116
|
+
key=lambda i: (-float(sims[i]), refs[i]))
|
|
117
|
+
out: list[Memory] = []
|
|
118
|
+
used = 0
|
|
119
|
+
for i in order:
|
|
120
|
+
m = self._store[refs[i]]
|
|
121
|
+
cost = len(m.content)
|
|
122
|
+
if used + cost > options.budget_tokens:
|
|
123
|
+
break
|
|
124
|
+
out.append(m)
|
|
125
|
+
used += cost
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
def audit(self) -> Iterator[Memory]:
|
|
129
|
+
return iter(list(self._store.values())) # the retained window
|
|
130
|
+
|
|
131
|
+
def flush(self) -> None:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# Smoke check — run `python -m aml.backends.bounded_vector`
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
from aml.backends.interface import MemoryBackend
|
|
141
|
+
from aml.backends.vector_only import _stub_embedder
|
|
142
|
+
|
|
143
|
+
print("GRAFOMEM bounded_vector.py — fixed-capacity recency window (STUB)\n")
|
|
144
|
+
|
|
145
|
+
K = 10
|
|
146
|
+
b = BoundedVectorBackend(capacity=K, embed_fn=_stub_embedder())
|
|
147
|
+
|
|
148
|
+
assert isinstance(b, MemoryBackend)
|
|
149
|
+
assert b.capabilities() == {Capability.AUDIT}
|
|
150
|
+
print(f"✓ Protocol + capabilities ({{AUDIT}}, capacity={K})")
|
|
151
|
+
|
|
152
|
+
# Write 100 facts; only the last K survive.
|
|
153
|
+
refs = [b.write(f"Entity{i:03d} lives in City{i:03d}", WriteOptions())
|
|
154
|
+
for i in range(100)]
|
|
155
|
+
b.flush()
|
|
156
|
+
retained = {m.ref for m in b.audit()}
|
|
157
|
+
assert len(retained) == K, f"expected {K} retained, got {len(retained)}"
|
|
158
|
+
assert retained == set(refs[-K:]), "retained window is not the last K (FIFO)"
|
|
159
|
+
print(f"✓ FIFO eviction (100 written, last {K} retained)")
|
|
160
|
+
|
|
161
|
+
# A recent fact is retrievable; an evicted (old) one is not.
|
|
162
|
+
recent = b.retrieve("Where does Entity099 live?", RetrieveOptions(budget_tokens=512))
|
|
163
|
+
assert any("Entity099" in m.content for m in recent), "recent fact missing"
|
|
164
|
+
old = b.retrieve("Where does Entity001 live?", RetrieveOptions(budget_tokens=512))
|
|
165
|
+
assert not any("Entity001" in m.content for m in old), "evicted fact leaked"
|
|
166
|
+
print("✓ Recall cliff (recent retrievable; evicted absent)")
|
|
167
|
+
|
|
168
|
+
# Footprint = retained count = scan cost, plateaued at capacity.
|
|
169
|
+
assert len(list(b.audit())) == K
|
|
170
|
+
print(f"✓ Footprint plateau (audit/scan = {K}, not 100)")
|
|
171
|
+
|
|
172
|
+
# Capability guards.
|
|
173
|
+
for op, call in (
|
|
174
|
+
("supersede", lambda: b.supersede(refs[-1], "x", WriteOptions())),
|
|
175
|
+
("delete", lambda: b.delete(refs[-1])),
|
|
176
|
+
("as_of", lambda: b.retrieve("x", RetrieveOptions(
|
|
177
|
+
as_of=datetime.now(tz=timezone.utc)))),
|
|
178
|
+
):
|
|
179
|
+
try:
|
|
180
|
+
call()
|
|
181
|
+
except CapabilityNotSupported:
|
|
182
|
+
pass
|
|
183
|
+
else:
|
|
184
|
+
raise AssertionError(f"{op}: expected CapabilityNotSupported")
|
|
185
|
+
print("✓ Capability guards (supersede/delete/as_of refused)")
|
|
186
|
+
|
|
187
|
+
# Determinism: same writes -> same retained set + ranking.
|
|
188
|
+
b2 = BoundedVectorBackend(capacity=K, embed_fn=_stub_embedder())
|
|
189
|
+
for i in range(100):
|
|
190
|
+
b2.write(f"Entity{i:03d} lives in City{i:03d}", WriteOptions())
|
|
191
|
+
r1 = [m.ref for m in b.retrieve("Where does Entity095 live?", RetrieveOptions(budget_tokens=512))]
|
|
192
|
+
r2 = [m.ref for m in b2.retrieve("Where does Entity095 live?", RetrieveOptions(budget_tokens=512))]
|
|
193
|
+
assert r1 == r2
|
|
194
|
+
print("✓ Deterministic (same writes -> same window + ranking)")
|
|
195
|
+
|
|
196
|
+
print("\nAll bounded_vector smoke checks green. Next: run_w4 "
|
|
197
|
+
"(recall-by-distance + footprint vs unbounded).")
|