superlocalmemory 3.2.1 → 3.2.2
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.
- package/CHANGELOG.md +23 -1
- package/README.md +61 -1
- package/package.json +1 -1
- package/pyproject.toml +26 -1
- package/src/superlocalmemory/attribution/signer.py +6 -1
- package/src/superlocalmemory/core/config.py +114 -1
- package/src/superlocalmemory/core/consolidation_engine.py +595 -0
- package/src/superlocalmemory/core/embeddings.py +0 -1
- package/src/superlocalmemory/core/engine.py +164 -674
- package/src/superlocalmemory/core/engine_wiring.py +474 -0
- package/src/superlocalmemory/core/graph_analyzer.py +199 -0
- package/src/superlocalmemory/core/recall_pipeline.py +247 -0
- package/src/superlocalmemory/core/store_pipeline.py +483 -0
- package/src/superlocalmemory/core/worker_pool.py +35 -12
- package/src/superlocalmemory/encoding/auto_linker.py +308 -0
- package/src/superlocalmemory/encoding/context_generator.py +175 -0
- package/src/superlocalmemory/encoding/temporal_validator.py +513 -0
- package/src/superlocalmemory/hooks/auto_invoker.py +484 -0
- package/src/superlocalmemory/retrieval/channel_registry.py +154 -0
- package/src/superlocalmemory/retrieval/engine.py +12 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +87 -3
- package/src/superlocalmemory/retrieval/spreading_activation.py +311 -0
- package/src/superlocalmemory/retrieval/strategy.py +6 -6
- package/src/superlocalmemory/retrieval/vector_store.py +386 -0
- package/src/superlocalmemory/server/routes/v3_api.py +576 -0
- package/src/superlocalmemory/storage/access_log.py +169 -0
- package/src/superlocalmemory/storage/database.py +288 -0
- package/src/superlocalmemory/storage/schema.py +10 -0
- package/src/superlocalmemory/storage/schema_v32.py +252 -0
- package/src/superlocalmemory/storage/v2_migrator.py +24 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Recall pipeline — extracted free functions for MemoryEngine.recall().
|
|
6
|
+
|
|
7
|
+
Direction: engine.py imports this module. This module NEVER imports engine.py.
|
|
8
|
+
|
|
9
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from superlocalmemory.core.config import SLMConfig
|
|
19
|
+
from superlocalmemory.core.hooks import HookRegistry
|
|
20
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
21
|
+
|
|
22
|
+
from superlocalmemory.storage.models import Mode, RecallResponse
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# apply_adaptive_ranking (was MemoryEngine._apply_adaptive_ranking)
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def apply_adaptive_ranking(
|
|
32
|
+
response: RecallResponse,
|
|
33
|
+
query: str,
|
|
34
|
+
pid: str,
|
|
35
|
+
*,
|
|
36
|
+
config: SLMConfig,
|
|
37
|
+
) -> RecallResponse:
|
|
38
|
+
"""Apply adaptive re-ranking if enough learning signals exist.
|
|
39
|
+
|
|
40
|
+
Phase 1 (< 50 signals): returns response unchanged (backward compat).
|
|
41
|
+
Phase 2 (50+): heuristic boosts from recency, access count, trust.
|
|
42
|
+
Phase 3 (200+): LightGBM ML-based reranking.
|
|
43
|
+
"""
|
|
44
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
learning_db = Path.home() / ".superlocalmemory" / "learning.db"
|
|
48
|
+
if not learning_db.exists():
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
collector = FeedbackCollector(learning_db)
|
|
52
|
+
signal_count = collector.get_feedback_count(pid)
|
|
53
|
+
|
|
54
|
+
if signal_count < 50:
|
|
55
|
+
return response # Phase 1: no change
|
|
56
|
+
|
|
57
|
+
from superlocalmemory.learning.ranker import AdaptiveRanker
|
|
58
|
+
ranker = AdaptiveRanker(signal_count=signal_count)
|
|
59
|
+
|
|
60
|
+
result_dicts = []
|
|
61
|
+
for r in response.results:
|
|
62
|
+
result_dicts.append({
|
|
63
|
+
"score": r.score,
|
|
64
|
+
"cross_encoder_score": r.score,
|
|
65
|
+
"trust_score": r.trust_score,
|
|
66
|
+
"channel_scores": r.channel_scores or {},
|
|
67
|
+
"fact": {
|
|
68
|
+
"age_days": 0,
|
|
69
|
+
"access_count": r.fact.access_count,
|
|
70
|
+
},
|
|
71
|
+
"_original": r,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
query_context = {"query_type": response.query_type}
|
|
75
|
+
reranked = ranker.rerank(result_dicts, query_context)
|
|
76
|
+
|
|
77
|
+
# Rebuild response with new ordering
|
|
78
|
+
new_results = [d["_original"] for d in reranked]
|
|
79
|
+
|
|
80
|
+
return RecallResponse(
|
|
81
|
+
query=response.query,
|
|
82
|
+
mode=response.mode,
|
|
83
|
+
results=new_results,
|
|
84
|
+
query_type=response.query_type,
|
|
85
|
+
channel_weights=response.channel_weights,
|
|
86
|
+
total_candidates=response.total_candidates,
|
|
87
|
+
retrieval_time_ms=response.retrieval_time_ms,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# run_recall (was MemoryEngine.recall)
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def run_recall(
|
|
96
|
+
query: str,
|
|
97
|
+
profile_id: str,
|
|
98
|
+
mode: Mode | None = None,
|
|
99
|
+
limit: int = 20,
|
|
100
|
+
agent_id: str = "unknown",
|
|
101
|
+
*,
|
|
102
|
+
config: SLMConfig,
|
|
103
|
+
retrieval_engine: Any,
|
|
104
|
+
trust_scorer: Any,
|
|
105
|
+
embedder: Any,
|
|
106
|
+
db: DatabaseManager,
|
|
107
|
+
llm: Any,
|
|
108
|
+
hooks: HookRegistry,
|
|
109
|
+
access_log: Any = None,
|
|
110
|
+
auto_linker: Any = None,
|
|
111
|
+
) -> RecallResponse:
|
|
112
|
+
"""Recall relevant facts for a query.
|
|
113
|
+
|
|
114
|
+
Pipeline: retrieval -> agentic sufficiency (if configured) -> post-recall updates.
|
|
115
|
+
"""
|
|
116
|
+
# Pre-operation hooks
|
|
117
|
+
hook_ctx = {
|
|
118
|
+
"operation": "recall",
|
|
119
|
+
"agent_id": agent_id,
|
|
120
|
+
"profile_id": profile_id,
|
|
121
|
+
"query_preview": query[:100],
|
|
122
|
+
}
|
|
123
|
+
hooks.run_pre("recall", hook_ctx)
|
|
124
|
+
|
|
125
|
+
m = mode or config.mode
|
|
126
|
+
|
|
127
|
+
response = retrieval_engine.recall(query, profile_id, m, limit)
|
|
128
|
+
|
|
129
|
+
# Agentic sufficiency verification
|
|
130
|
+
agentic_rounds = config.retrieval.agentic_max_rounds
|
|
131
|
+
if agentic_rounds > 0 and response.results:
|
|
132
|
+
max_score = max((r.score for r in response.results), default=0.0)
|
|
133
|
+
should_trigger = (
|
|
134
|
+
max_score < config.retrieval.agentic_confidence_threshold
|
|
135
|
+
or response.query_type == "multi_hop"
|
|
136
|
+
or len(response.results) < 3
|
|
137
|
+
)
|
|
138
|
+
if should_trigger:
|
|
139
|
+
try:
|
|
140
|
+
from superlocalmemory.retrieval.agentic import AgenticRetriever
|
|
141
|
+
agentic = AgenticRetriever(
|
|
142
|
+
confidence_threshold=config.retrieval.agentic_confidence_threshold,
|
|
143
|
+
db=db,
|
|
144
|
+
)
|
|
145
|
+
enhanced_facts = agentic.retrieve(
|
|
146
|
+
query=query, profile_id=profile_id,
|
|
147
|
+
retrieval_engine=retrieval_engine,
|
|
148
|
+
llm=llm,
|
|
149
|
+
top_k=limit,
|
|
150
|
+
query_type=response.query_type,
|
|
151
|
+
)
|
|
152
|
+
# Replace response results with enhanced facts if we got more
|
|
153
|
+
if len(enhanced_facts) > len(response.results):
|
|
154
|
+
from superlocalmemory.storage.models import RetrievalResult
|
|
155
|
+
enhanced_results = []
|
|
156
|
+
for i, f in enumerate(enhanced_facts):
|
|
157
|
+
# Look up real trust score for agentic results
|
|
158
|
+
fact_trust = 0.5
|
|
159
|
+
if trust_scorer:
|
|
160
|
+
try:
|
|
161
|
+
fact_trust = trust_scorer.get_fact_trust(
|
|
162
|
+
f.fact_id, profile_id,
|
|
163
|
+
)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
enhanced_results.append(RetrievalResult(
|
|
167
|
+
fact=f, score=1.0 / (i + 1),
|
|
168
|
+
channel_scores={"agentic": 1.0},
|
|
169
|
+
confidence=f.confidence,
|
|
170
|
+
evidence_chain=["agentic_round_2"],
|
|
171
|
+
trust_score=fact_trust,
|
|
172
|
+
))
|
|
173
|
+
response = RecallResponse(
|
|
174
|
+
query=query, mode=m, results=enhanced_results[:limit],
|
|
175
|
+
query_type=response.query_type,
|
|
176
|
+
channel_weights=response.channel_weights,
|
|
177
|
+
total_candidates=response.total_candidates + len(enhanced_facts),
|
|
178
|
+
retrieval_time_ms=response.retrieval_time_ms,
|
|
179
|
+
)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
logger.debug("Agentic sufficiency skipped: %s", exc)
|
|
182
|
+
|
|
183
|
+
# V3.2: Log access for recalled facts (Phase 1)
|
|
184
|
+
if access_log and response.results:
|
|
185
|
+
try:
|
|
186
|
+
fact_ids = [r.fact.fact_id for r in response.results]
|
|
187
|
+
access_log.store_access_batch(
|
|
188
|
+
fact_ids=fact_ids,
|
|
189
|
+
profile_id=profile_id,
|
|
190
|
+
access_type="recall",
|
|
191
|
+
)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
logger.debug("Access log batch store failed: %s", exc)
|
|
194
|
+
|
|
195
|
+
# Phase 3: Hebbian strengthening for co-accessed facts
|
|
196
|
+
if auto_linker and response.results:
|
|
197
|
+
try:
|
|
198
|
+
recalled_ids = [
|
|
199
|
+
r.fact.fact_id for r in response.results[:10]
|
|
200
|
+
]
|
|
201
|
+
auto_linker.strengthen_co_access(recalled_ids, profile_id)
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
logger.debug("Hebbian strengthening: %s", exc)
|
|
204
|
+
|
|
205
|
+
# Adaptive re-ranking (V3.1 Active Memory)
|
|
206
|
+
try:
|
|
207
|
+
response = apply_adaptive_ranking(response, query, profile_id, config=config)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
logger.debug("Adaptive ranking skipped: %s", exc)
|
|
210
|
+
|
|
211
|
+
# Reconsolidation: access updates trust + count (neuroscience principle)
|
|
212
|
+
if trust_scorer:
|
|
213
|
+
for r in response.results:
|
|
214
|
+
trust_scorer.update_on_access("fact", r.fact.fact_id, profile_id)
|
|
215
|
+
|
|
216
|
+
# Fisher Bayesian update on recall
|
|
217
|
+
q_emb = embedder.embed(query) if embedder else None
|
|
218
|
+
q_var_arr = None
|
|
219
|
+
if embedder and q_emb:
|
|
220
|
+
_, q_var_list = embedder.compute_fisher_params(q_emb)
|
|
221
|
+
import numpy as _np
|
|
222
|
+
q_var_arr = _np.array(q_var_list, dtype=_np.float64)
|
|
223
|
+
|
|
224
|
+
for r in response.results:
|
|
225
|
+
updates: dict[str, object] = {
|
|
226
|
+
"access_count": r.fact.access_count + 1,
|
|
227
|
+
}
|
|
228
|
+
# Bayesian variance narrowing after 3+ accesses
|
|
229
|
+
if (q_var_arr is not None
|
|
230
|
+
and r.fact.fisher_variance
|
|
231
|
+
and len(r.fact.fisher_variance) == len(q_var_arr)
|
|
232
|
+
and r.fact.access_count >= 3):
|
|
233
|
+
import numpy as _np
|
|
234
|
+
f_var = _np.array(r.fact.fisher_variance, dtype=_np.float64)
|
|
235
|
+
# Conjugate Gaussian update: 1/new_var = 1/f_var + 1/q_var
|
|
236
|
+
new_var = 1.0 / (1.0 / _np.maximum(f_var, 0.05) + 1.0 / _np.maximum(q_var_arr, 0.05))
|
|
237
|
+
new_var = _np.clip(new_var, 0.05, 2.0)
|
|
238
|
+
updates["fisher_variance"] = new_var.tolist()
|
|
239
|
+
|
|
240
|
+
db.update_fact(r.fact.fact_id, updates)
|
|
241
|
+
|
|
242
|
+
# Post-operation hooks (audit, trust signal, learning)
|
|
243
|
+
hook_ctx["result_count"] = len(response.results)
|
|
244
|
+
hook_ctx["query_type"] = response.query_type
|
|
245
|
+
hooks.run_post("recall", hook_ctx)
|
|
246
|
+
|
|
247
|
+
return response
|