Files
Krystie dc5dc94d79 Initial commit: Adaptive Recall sidecar for UMP (Phase 5)
Multi-channel retrieval sidecar over Universal Memory Protocol:
- 3-channel RRF (UMP FTS5 + Qdrant vector + knowledge graph)
- ACT-R re-ranking (Anderson 1983) with access tracking
- Co-occurrence graph edges (Phase 6) for dense traversal
- Memory lifecycle decay (Phase 4) with per-kind confidence
- MCP shim routes recall through sidecar, falls back to canonical UMP

Architecture:
- src/server.js      HTTP sidecar on port 4380
- src/graph.js       2592-node / 111-edge graph from UMP (or +cooccur: 13k+)
- src/actr.js        A_i = -d*ln(age) + beta*log1p(freq) + epsilon*conf
- src/access_log.js  per-URN counter + last_accessed_at
- src/ump-recall-mcp.js  MCP shim (recall via sidecar, others passthrough)

Eval results (851-record UMP corpus):
- 2ch RRF over baseline: +50pp recall@10
- 3ch RRF (+graph): +60pp, 12 unique wins
- ACT-R re-rank: 4/20 #1 changes, 84% top-5 retention

Tests: 76/76 passing across graph (27), actr (27), access_log (28),
decay (20), mcp-shim (sidecar + fallback). Run with: npm test

Inspired by AIAppsAPI/adaptive-recall but built from scratch against
existing DNS2 infrastructure (UMP at :4317, Qdrant at :6333,
Ollama at :11434). No paid SaaS, MIT-licensed.
2026-07-12 19:24:47 -07:00

37 lines
1.4 KiB
Python

#!/usr/bin/env python3
"""Smoke-test the 3-channel RRF sidecar."""
import json
import urllib.request
def call(query, limit=5):
req = urllib.request.Request(
"http://127.0.0.1:4380/recall",
data=json.dumps({"query": query, "limit": limit}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
def show(label, query):
print(f"\n=== {label}: query={query!r} ===")
try:
r = call(query, 5)
except Exception as e:
print(f"ERROR: {e}")
return
print(f"phase={r.get('phase')} fused_count={r.get('fused_count')} returned={r.get('returned')}")
print(f"channels={r.get('channels')}")
if r.get('channel_errors'):
print(f"channel_errors={r.get('channel_errors')}")
for i, h in enumerate(r.get('hits', [])[:5]):
urn = h['urn'][:60]
bc = h.get('by_channel', {})
ranks = {k: v.get('rank') for k, v in bc.items()}
scores = {k: round(v.get('score', 0), 3) if v.get('score') is not None else None for k, v in bc.items()}
score = round(h['score'], 4)
print(f" #{h['rrf_rank']} score={score} urn={urn}... via={ranks} scores={scores}")
show("Test 1 (entity-rich)", "DNS2 ollama")
show("Test 2 (graph-hostile)", "supercalifragilistic")
show("Test 3 (single entity)", "Triangles")
show("Test 4 (zero entity)", "the and of")