Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions plugins/memory/holographic/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ def search(

# Stage 2: Rerank with Jaccard + trust + optional decay
query_tokens = self._tokenize(query)
# The query vector is loop-invariant — encode it at most once, on
# the first candidate that actually carries an HRR vector. Lazy on
# purpose: migrated stores can have FTS candidates whose hrr_vector
# was never backfilled (MemoryStore._init_db adds the column
# without backfilling), and those must not pay for an encode
# nothing will use. encode_text is deterministic (SHA-256 counter
# blocks), so the hoisted vector is bit-identical to what the
# per-candidate calls produced.
query_vec = None
scored = []

for fact in candidates:
Expand All @@ -83,7 +92,8 @@ def search(
# HRR similarity
if self.hrr_weight > 0 and fact.get("hrr_vector"):
fact_vec = hrr.bytes_to_phases(fact["hrr_vector"])
query_vec = hrr.encode_text(query, self.hrr_dim)
if query_vec is None:
query_vec = hrr.encode_text(query, self.hrr_dim)
hrr_sim = (hrr.similarity(query_vec, fact_vec) + 1.0) / 2.0 # shift to [0,1]
else:
hrr_sim = 0.5 # neutral
Expand Down Expand Up @@ -173,14 +183,16 @@ def probe(
# Final fallback: keyword search
return self.search(entity, category=category, limit=limit)

# role_content is loop-invariant — encode it once (deterministic
# SHA-256-based atom) instead of once per fact row.
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
scored = []
for row in rows:
fact = dict(row)
fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector"))
# Unbind probe key from fact to see if entity is structurally present
residual = hrr.unbind(fact_vec, probe_key)
# Compare residual against content signal
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
content_vec = hrr.bind(hrr.encode_text(fact["content"], self.hrr_dim), role_content)
sim = hrr.similarity(residual, content_vec)
fact["score"] = (sim + 1.0) / 2.0 * fact["trust_score"]
Expand Down Expand Up @@ -234,6 +246,10 @@ def related(

# Score each fact by how much the entity's atom appears in its vector
# This catches both role-bound entity matches AND content word matches
# Both role atoms are loop-invariant — encode them once here
# (deterministic SHA-256-based atoms) instead of twice per fact row.
role_entity = hrr.encode_atom("__hrr_role_entity__", self.hrr_dim)
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
scored = []
for row in rows:
fact = dict(row)
Expand All @@ -243,8 +259,6 @@ def related(
residual = hrr.unbind(fact_vec, entity_vec)
# A high-similarity residual to ANY known role vector means this entity
# plays a structural role in the fact
role_entity = hrr.encode_atom("__hrr_role_entity__", self.hrr_dim)
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)

entity_role_sim = hrr.similarity(residual, role_entity)
content_role_sim = hrr.similarity(residual, role_content)
Expand Down
138 changes: 138 additions & 0 deletions tests/plugins/memory/test_holographic_retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,141 @@ def test_prefetch_recovers_prose_query(retriever_with_facts):
assert "deployment rollback" in results[0]["content"].lower()




# ---------------------------------------------------------------------------
# Loop-invariant encode hoists (perf) — search/probe/related must encode
# constant vectors ONCE per call, not once per candidate/row.
# encode_text/encode_atom are deterministic (SHA-256 counter blocks), so the
# hoisted vectors are bit-identical to the per-iteration values they replace.
# ---------------------------------------------------------------------------

from plugins.memory.holographic import holographic as hrr


@pytest.fixture
def hoisted_retriever():
"""30 facts with HRR vectors, default dim (smaller dims trip an
inhomogeneous-shape edge in the fact encoder)."""
store = MemoryStore(":memory:")
for i in range(30):
store.add_fact(
content=f"deploy target {i} setting alpha beta gamma option {i % 7}",
category="fact" if i % 2 else "preference",
tags=f"entity_{i % 5} deploy",
)
retriever = FactRetriever(store=store)
yield retriever
store.close()


def _counting_spy(monkeypatch, attr):
calls = []
real = getattr(hrr, attr)

def wrapper(*args, **kwargs):
calls.append(args)
return real(*args, **kwargs)

monkeypatch.setattr(hrr, attr, wrapper)
return calls


def test_encode_functions_are_deterministic():
"""Soundness premise of the hoists: same input -> identical vector."""
import numpy as np

assert np.array_equal(hrr.encode_text("deploy target", 1024),
hrr.encode_text("deploy target", 1024))
assert np.array_equal(hrr.encode_atom("__hrr_role_content__", 1024),
hrr.encode_atom("__hrr_role_content__", 1024))


def test_search_encodes_query_vector_once(hoisted_retriever, monkeypatch):
calls = _counting_spy(monkeypatch, "encode_text")
results = hoisted_retriever.search("deploy target setting")
assert results # the HRR path actually engaged
assert len(calls) == 1, (
f"query vector encoded {len(calls)}x in one search() — "
"loop-invariant hoist regressed"
)


def test_search_results_bit_identical_to_unhoisted(hoisted_retriever):
"""Parity: hoisted search() must produce the exact pre-fix results.

Replicates the pre-fix loop (query vector encoded per candidate) as the
reference and compares full scored output for exact equality.
"""
r = hoisted_retriever
query = "deploy target setting"
new_results = r.search(query)

# --- pre-fix reference ---
candidates = r._fts_candidates(query, None, 0.3, 10 * 3)
query_tokens = r._tokenize(query)
scored = []
for fact in candidates:
content_tokens = r._tokenize(fact["content"])
tag_tokens = r._tokenize(fact.get("tags", ""))
all_tokens = content_tokens | tag_tokens
jaccard = r._jaccard_similarity(query_tokens, all_tokens)
fts_score = fact.get("fts_rank", 0.0)
if r.hrr_weight > 0 and fact.get("hrr_vector"):
fact_vec = hrr.bytes_to_phases(fact["hrr_vector"])
query_vec = hrr.encode_text(query, r.hrr_dim) # per-candidate
hrr_sim = (hrr.similarity(query_vec, fact_vec) + 1.0) / 2.0
else:
hrr_sim = 0.5
relevance = (r.fts_weight * fts_score
+ r.jaccard_weight * jaccard
+ r.hrr_weight * hrr_sim)
fact["score"] = relevance * fact["trust_score"]
scored.append(fact)
scored.sort(key=lambda x: x["score"], reverse=True)
old_results = scored[:10]
for fact in old_results:
fact.pop("hrr_vector", None)

assert new_results == old_results


def test_related_encodes_role_atoms_once(hoisted_retriever, monkeypatch):
calls = _counting_spy(monkeypatch, "encode_atom")
results = hoisted_retriever.related("entity_1")
assert results
role_calls = [a for a in calls
if a and str(a[0]).startswith("__hrr_role_")]
assert len(role_calls) == 2, (
f"role atoms encoded {len(role_calls)}x in one related() — "
"expected exactly 2 (role_entity + role_content, hoisted)"
)


def test_probe_encodes_role_atom_once(hoisted_retriever, monkeypatch):
calls = _counting_spy(monkeypatch, "encode_atom")
results = hoisted_retriever.probe("entity_1")
assert results
role_content_calls = [a for a in calls
if a and a[0] == "__hrr_role_content__"]
assert len(role_content_calls) == 1, (
f"role_content atom encoded {len(role_content_calls)}x in one "
"probe() — loop-invariant hoist regressed"
)


def test_search_without_vectors_never_encodes(hoisted_retriever, monkeypatch):
"""Migrated DBs can have FTS candidates with NULL hrr_vector
(MemoryStore._init_db adds the column without backfilling existing
facts). The lazy hoist must not encode a query vector nothing will
use — pre-fix main encoded only beneath fact.get('hrr_vector')."""
store = hoisted_retriever.store
store._conn.execute("UPDATE facts SET hrr_vector = NULL")
store._conn.commit()
calls = _counting_spy(monkeypatch, "encode_text")
results = hoisted_retriever.search("deploy target setting")
assert results # candidates exist; neutral hrr_sim=0.5 path
assert calls == [], (
f"encode_text called {len(calls)}x with zero vector candidates — "
"lazy hoist regressed to eager"
)
Loading