Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
292 lines
10 KiB
Python
292 lines
10 KiB
Python
"""Plan 03-01 CONN-05 GREEN: memory_recall_structural via core.dispatch.
|
|
|
|
Verifies the new MCP tool branch:
|
|
- Structural query enters as a dict[str, str] of role->filler pairs.
|
|
- Pipeline ranks by Hamming similarity over the packed structure_hv.
|
|
- ZERO LLM token cost: no Embedder() instantiated, no anthropic client called.
|
|
- Budget knob honoured: smaller budget -> fewer hits returned.
|
|
- Pipeline-level structural_weight knob shifts ranking when set.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolated_keyring(monkeypatch):
|
|
import keyring as _keyring
|
|
|
|
fake_store: dict[tuple[str, str], str] = {}
|
|
monkeypatch.setattr(_keyring, "get_password", lambda s, u: fake_store.get((s, u)))
|
|
monkeypatch.setattr(_keyring, "set_password", lambda s, u, p: fake_store.__setitem__((s, u), p))
|
|
monkeypatch.setattr(_keyring, "delete_password", lambda s, u: fake_store.pop((s, u), None))
|
|
yield fake_store
|
|
|
|
|
|
def _make_record(text="x", **overrides):
|
|
from iai_mcp.types import EMBED_DIM, MemoryRecord
|
|
|
|
base = dict(
|
|
id=uuid4(),
|
|
tier="episodic",
|
|
literal_surface=text,
|
|
aaak_index="",
|
|
embedding=[0.1] * EMBED_DIM,
|
|
community_id=None,
|
|
centrality=0.0,
|
|
detail_level=2,
|
|
pinned=False,
|
|
stability=0.0,
|
|
difficulty=0.0,
|
|
last_reviewed=None,
|
|
never_decay=False,
|
|
never_merge=False,
|
|
provenance=[],
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
tags=[],
|
|
language="en",
|
|
)
|
|
base.update(overrides)
|
|
return MemoryRecord(**base)
|
|
|
|
|
|
def _seed(store, n=5):
|
|
"""Seed N records with varied tags so structure_hv differs per record."""
|
|
recs = []
|
|
for i in range(n):
|
|
rec = _make_record(text=f"text-{i}", tags=[f"topic-{i}"])
|
|
store.insert(rec)
|
|
recs.append(rec)
|
|
return recs
|
|
|
|
|
|
# ------------------------------------------------------------ dispatch happy
|
|
|
|
|
|
def test_memory_recall_structural_returns_hits(tmp_path, monkeypatch):
|
|
"""The new branch returns a hits list with score / record_id / literal_surface."""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.core import dispatch
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
store = MemoryStore()
|
|
recs = _seed(store, n=5)
|
|
|
|
resp = dispatch(
|
|
store,
|
|
"memory_recall_structural",
|
|
{
|
|
"structure_query": {"TIER": "episodic", "LANG": "en"},
|
|
"budget_tokens": 2000,
|
|
},
|
|
)
|
|
assert "hits" in resp
|
|
assert isinstance(resp["hits"], list)
|
|
assert len(resp["hits"]) >= 1
|
|
h = resp["hits"][0]
|
|
assert "record_id" in h
|
|
assert "score" in h
|
|
assert "literal_surface" in h
|
|
assert h["score"] >= 0.0
|
|
assert resp["structural_query_size"] == 2
|
|
|
|
|
|
def test_memory_recall_structural_zero_llm_cost(tmp_path, monkeypatch):
|
|
"""The branch must NOT instantiate Embedder() OR call anthropic.messages.create.
|
|
Hard guarantee for the 0-LLM-cost invariant (constitutional fit)."""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.core import dispatch
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
store = MemoryStore()
|
|
_seed(store, n=3)
|
|
|
|
# Trip-wire: replace Embedder with a sentinel that raises on instantiation.
|
|
embedder_called = {"n": 0}
|
|
|
|
class _BoomEmbedder:
|
|
def __init__(self, *a, **kw):
|
|
embedder_called["n"] += 1
|
|
raise AssertionError(
|
|
"memory_recall_structural must NOT instantiate Embedder() "
|
|
"(constitutional 0-LLM-cost invariant)"
|
|
)
|
|
|
|
import iai_mcp.embed as embed_mod
|
|
monkeypatch.setattr(embed_mod, "Embedder", _BoomEmbedder)
|
|
|
|
# Trip-wire: any anthropic client call raises immediately.
|
|
try:
|
|
import anthropic
|
|
def _boom_client(*a, **kw):
|
|
raise AssertionError("memory_recall_structural must NOT touch anthropic API")
|
|
monkeypatch.setattr(anthropic, "Anthropic", _boom_client)
|
|
except ImportError:
|
|
pass
|
|
|
|
resp = dispatch(
|
|
store,
|
|
"memory_recall_structural",
|
|
{"structure_query": {"TIER": "episodic"}, "budget_tokens": 2000},
|
|
)
|
|
assert embedder_called["n"] == 0
|
|
assert "hits" in resp
|
|
|
|
|
|
def test_memory_recall_structural_budget_honoured(tmp_path, monkeypatch):
|
|
"""A tiny budget returns fewer hits than a large budget."""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.core import dispatch
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
store = MemoryStore()
|
|
_seed(store, n=5)
|
|
|
|
big = dispatch(
|
|
store, "memory_recall_structural",
|
|
{"structure_query": {"TIER": "episodic"}, "budget_tokens": 5000},
|
|
)
|
|
small = dispatch(
|
|
store, "memory_recall_structural",
|
|
{"structure_query": {"TIER": "episodic"}, "budget_tokens": 5},
|
|
)
|
|
assert len(big["hits"]) >= len(small["hits"])
|
|
assert len(small["hits"]) >= 1 # At least one hit always returned.
|
|
|
|
|
|
# --------------------------------------------------------- pipeline peer wire
|
|
|
|
|
|
def test_pipeline_ranker_structural_weight_shifts_ordering(tmp_path, monkeypatch):
|
|
"""Setting profile_state["structural_weight"]=0.9 changes the ranker's
|
|
output relative to weight=0.0 -- proving structural is a ranking peer,
|
|
not a no-op cosmetic kwarg."""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.community import CommunityAssignment
|
|
from iai_mcp.graph import MemoryGraph
|
|
from iai_mcp.pipeline import recall_for_response
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
store = MemoryStore()
|
|
recs = []
|
|
# Seed records with distinguishable structures (different tiers).
|
|
for i, tier in enumerate(["episodic", "semantic", "episodic", "semantic"]):
|
|
rec = _make_record(text=f"row-{i}", tier=tier, tags=[f"tag-{i}"])
|
|
store.insert(rec)
|
|
recs.append(rec)
|
|
|
|
# Build a minimal graph + assignment so recall_for_response can run.
|
|
graph = MemoryGraph()
|
|
for r in recs:
|
|
graph.add_node(r.id, community_id=r.id, embedding=r.embedding)
|
|
assignment = CommunityAssignment(
|
|
node_to_community={r.id: r.id for r in recs},
|
|
community_centroids={r.id: list(r.embedding) for r in recs},
|
|
modularity=0.5,
|
|
top_communities=[r.id for r in recs],
|
|
mid_regions={r.id: [r.id] for r in recs},
|
|
)
|
|
|
|
class _StaticEmbedder:
|
|
DIM = len(recs[0].embedding)
|
|
DEFAULT_DIM = DIM
|
|
DEFAULT_MODEL_KEY = "test"
|
|
def embed(self, text): # deterministic vector keyed off length
|
|
return [0.1] * self.DIM
|
|
|
|
e = _StaticEmbedder()
|
|
|
|
baseline = recall_for_response(
|
|
store=store, graph=graph, assignment=assignment, rich_club=[],
|
|
embedder=e, cue="hello", session_id="-",
|
|
budget_tokens=5000, profile_state={"structural_weight": 0.0},
|
|
)
|
|
weighted = recall_for_response(
|
|
store=store, graph=graph, assignment=assignment, rich_club=[],
|
|
embedder=e, cue="hello", session_id="-",
|
|
budget_tokens=5000, profile_state={"structural_weight": 0.9},
|
|
)
|
|
# Both runs return some hits.
|
|
assert len(baseline.hits) >= 1
|
|
assert len(weighted.hits) >= 1
|
|
# The weighted ranker exposes structural-score reasoning in its reason
|
|
# strings (proves it's reading the new branch). Baseline does not.
|
|
assert any("structural" in h.reason for h in weighted.hits)
|
|
assert all("structural" not in h.reason for h in baseline.hits)
|
|
|
|
|
|
def test_unknown_method_does_not_match(tmp_path, monkeypatch):
|
|
"""The new branch only matches the canonical method name.
|
|
|
|
Phase 07.13-02 V3-03 fix: dispatch fall-through now raises
|
|
UnknownMethodError instead of returning {"error": ...} dict. The
|
|
bogus-suffix branch is name-gated; an exact-match-only assertion is
|
|
that the bogus method raises UnknownMethodError specifically.
|
|
"""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.core import UnknownMethodError, dispatch
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
store = MemoryStore()
|
|
# Unknown method falls through to UnknownMethodError post-Phase-07.13-02.
|
|
# We don't care about the exception details -- just that our branch is
|
|
# name-gated and a non-canonical name doesn't accidentally match.
|
|
try:
|
|
dispatch(store, "memory_recall_structural_BOGUS", {})
|
|
except (UnknownMethodError, KeyError, ValueError, TypeError):
|
|
pass # acceptable -- the bogus method failed downstream or fell through
|
|
# The valid method still works.
|
|
resp = dispatch(store, "memory_recall_structural", {"structure_query": {}})
|
|
assert "hits" in resp
|
|
|
|
|
|
def test_memory_recall_structural_max_records_caps(tmp_path, monkeypatch):
|
|
"""V3-07: max_records limits how many rows enter the O(N) scoring loop."""
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path))
|
|
from iai_mcp.core import dispatch
|
|
from iai_mcp.store import MemoryStore
|
|
from iai_mcp.types import MemoryRecord, SCHEMA_VERSION_CURRENT
|
|
|
|
store = MemoryStore()
|
|
now = datetime.now(timezone.utc)
|
|
for i in range(12):
|
|
rid = uuid4()
|
|
emb = [0.001 * (i + 1)] * 384
|
|
hv = bytes(1250)
|
|
rec = MemoryRecord(
|
|
id=rid,
|
|
tier="episodic",
|
|
literal_surface=f"row-{i}",
|
|
aaak_index="",
|
|
embedding=emb,
|
|
structure_hv=hv,
|
|
community_id=None,
|
|
centrality=0.0,
|
|
detail_level=1,
|
|
pinned=False,
|
|
stability=0.0,
|
|
difficulty=0.0,
|
|
last_reviewed=None,
|
|
never_decay=False,
|
|
never_merge=False,
|
|
provenance=[],
|
|
created_at=now,
|
|
updated_at=now,
|
|
tags=[],
|
|
language="en",
|
|
s5_trust_score=0.5,
|
|
profile_modulation_gain={},
|
|
schema_version=SCHEMA_VERSION_CURRENT,
|
|
)
|
|
store.insert(rec)
|
|
|
|
out = dispatch(
|
|
store,
|
|
"memory_recall_structural",
|
|
{"structure_query": {}, "max_records": 5},
|
|
)
|
|
assert len(out["hits"]) <= 5
|