Initial release: iai-mcp v0.1.0
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
This commit is contained in:
commit
f6b876fbe7
332 changed files with 97258 additions and 0 deletions
292
tests/test_memory_recall_structural.py
Normal file
292
tests/test_memory_recall_structural.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue