superlocalmemory 3.2.1 → 3.2.3

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +61 -1
  3. package/package.json +1 -1
  4. package/pyproject.toml +26 -1
  5. package/src/superlocalmemory/attribution/signer.py +6 -1
  6. package/src/superlocalmemory/core/config.py +113 -1
  7. package/src/superlocalmemory/core/consolidation_engine.py +595 -0
  8. package/src/superlocalmemory/core/embeddings.py +0 -1
  9. package/src/superlocalmemory/core/engine.py +164 -674
  10. package/src/superlocalmemory/core/engine_wiring.py +474 -0
  11. package/src/superlocalmemory/core/graph_analyzer.py +199 -0
  12. package/src/superlocalmemory/core/recall_pipeline.py +247 -0
  13. package/src/superlocalmemory/core/store_pipeline.py +483 -0
  14. package/src/superlocalmemory/core/worker_pool.py +35 -12
  15. package/src/superlocalmemory/encoding/auto_linker.py +308 -0
  16. package/src/superlocalmemory/encoding/context_generator.py +175 -0
  17. package/src/superlocalmemory/encoding/temporal_validator.py +513 -0
  18. package/src/superlocalmemory/hooks/auto_invoker.py +484 -0
  19. package/src/superlocalmemory/retrieval/channel_registry.py +154 -0
  20. package/src/superlocalmemory/retrieval/engine.py +12 -0
  21. package/src/superlocalmemory/retrieval/semantic_channel.py +87 -3
  22. package/src/superlocalmemory/retrieval/spreading_activation.py +311 -0
  23. package/src/superlocalmemory/retrieval/strategy.py +6 -6
  24. package/src/superlocalmemory/retrieval/vector_store.py +386 -0
  25. package/src/superlocalmemory/server/routes/v3_api.py +576 -0
  26. package/src/superlocalmemory/storage/access_log.py +169 -0
  27. package/src/superlocalmemory/storage/database.py +288 -0
  28. package/src/superlocalmemory/storage/schema.py +10 -0
  29. package/src/superlocalmemory/storage/schema_v32.py +252 -0
  30. 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