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.
- 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 +113 -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,474 @@
|
|
|
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
|
+
"""Engine wiring — extracted free functions for MemoryEngine initialization.
|
|
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
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# init_embedder (was MemoryEngine._init_embedder + helpers)
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def _try_ollama_embedder(emb_cfg: Any) -> Any | None:
|
|
30
|
+
"""Try to create an OllamaEmbedder. Returns it or None."""
|
|
31
|
+
try:
|
|
32
|
+
from superlocalmemory.core.ollama_embedder import OllamaEmbedder
|
|
33
|
+
emb = OllamaEmbedder(
|
|
34
|
+
model=emb_cfg.ollama_model,
|
|
35
|
+
base_url=emb_cfg.ollama_base_url,
|
|
36
|
+
dimension=emb_cfg.dimension,
|
|
37
|
+
)
|
|
38
|
+
if emb.is_available:
|
|
39
|
+
logger.info("Using Ollama embeddings (%s)", emb_cfg.ollama_model)
|
|
40
|
+
return emb
|
|
41
|
+
logger.warning(
|
|
42
|
+
"Ollama embedder not available (model=%s). Falling back.",
|
|
43
|
+
emb_cfg.ollama_model,
|
|
44
|
+
)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
logger.warning("OllamaEmbedder init failed: %s", exc)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _try_service_embedder(cls: type, emb_cfg: Any) -> Any | None:
|
|
51
|
+
"""Try to create an EmbeddingService. Returns it or None."""
|
|
52
|
+
try:
|
|
53
|
+
emb = cls(emb_cfg)
|
|
54
|
+
if emb.is_available:
|
|
55
|
+
return emb
|
|
56
|
+
logger.warning("EmbeddingService not available. BM25-only mode. Run 'slm doctor' to diagnose.")
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
logger.warning("Embeddings unavailable (%s). BM25-only mode. Run 'slm doctor' to diagnose.", exc)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def init_embedder(config: SLMConfig) -> Any | None:
|
|
63
|
+
"""Initialize the best available embedding provider.
|
|
64
|
+
|
|
65
|
+
Priority order:
|
|
66
|
+
1. Explicit provider in config (ollama / cloud / sentence-transformers)
|
|
67
|
+
2. Auto-detect: if Ollama has embedding model -> use it
|
|
68
|
+
3. Fallback to sentence-transformers subprocess
|
|
69
|
+
4. If nothing works -> None (BM25-only mode)
|
|
70
|
+
"""
|
|
71
|
+
from superlocalmemory.core.embeddings import EmbeddingService
|
|
72
|
+
|
|
73
|
+
emb_cfg = config.embedding
|
|
74
|
+
provider = emb_cfg.provider
|
|
75
|
+
|
|
76
|
+
# --- Explicit ollama provider ---
|
|
77
|
+
if provider == "ollama":
|
|
78
|
+
return _try_ollama_embedder(emb_cfg)
|
|
79
|
+
|
|
80
|
+
# --- Explicit cloud provider ---
|
|
81
|
+
if provider == "cloud" or emb_cfg.is_cloud:
|
|
82
|
+
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
83
|
+
|
|
84
|
+
# --- Explicit sentence-transformers ---
|
|
85
|
+
if provider == "sentence-transformers":
|
|
86
|
+
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
87
|
+
|
|
88
|
+
# --- Auto-detect: try Ollama first (fast path, <1s) ---
|
|
89
|
+
ollama_emb = _try_ollama_embedder(emb_cfg)
|
|
90
|
+
if ollama_emb is not None:
|
|
91
|
+
logger.info("Auto-detected Ollama embeddings (fast path)")
|
|
92
|
+
return ollama_emb
|
|
93
|
+
|
|
94
|
+
# --- Fallback: sentence-transformers subprocess ---
|
|
95
|
+
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# init_encoding (was MemoryEngine._init_encoding)
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def init_encoding(
|
|
103
|
+
config: SLMConfig,
|
|
104
|
+
db: DatabaseManager,
|
|
105
|
+
embedder: Any,
|
|
106
|
+
llm: Any,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
"""Create all encoding components. Returns dict of components."""
|
|
109
|
+
from superlocalmemory.encoding.fact_extractor import FactExtractor
|
|
110
|
+
from superlocalmemory.encoding.entity_resolver import EntityResolver
|
|
111
|
+
from superlocalmemory.encoding.temporal_parser import TemporalParser
|
|
112
|
+
from superlocalmemory.encoding.type_router import TypeRouter
|
|
113
|
+
from superlocalmemory.encoding.graph_builder import GraphBuilder
|
|
114
|
+
from superlocalmemory.encoding.consolidator import MemoryConsolidator
|
|
115
|
+
from superlocalmemory.encoding.observation_builder import ObservationBuilder
|
|
116
|
+
from superlocalmemory.encoding.scene_builder import SceneBuilder
|
|
117
|
+
from superlocalmemory.encoding.entropy_gate import EntropyGate
|
|
118
|
+
from superlocalmemory.retrieval.ann_index import ANNIndex
|
|
119
|
+
|
|
120
|
+
ann_index = ANNIndex(dimension=config.embedding.dimension)
|
|
121
|
+
fact_extractor = FactExtractor(
|
|
122
|
+
config=config.encoding, llm=llm,
|
|
123
|
+
embedder=embedder, mode=config.mode,
|
|
124
|
+
)
|
|
125
|
+
entity_resolver = EntityResolver(db, llm)
|
|
126
|
+
temporal_parser = TemporalParser()
|
|
127
|
+
type_router = TypeRouter(
|
|
128
|
+
mode=config.mode, embedder=embedder, llm=llm,
|
|
129
|
+
)
|
|
130
|
+
graph_builder = GraphBuilder(db, ann_index)
|
|
131
|
+
consolidator = MemoryConsolidator(
|
|
132
|
+
db, embedder, llm, config.encoding,
|
|
133
|
+
)
|
|
134
|
+
observation_builder = ObservationBuilder(db)
|
|
135
|
+
scene_builder = SceneBuilder(db, embedder)
|
|
136
|
+
entropy_gate = EntropyGate(
|
|
137
|
+
embedder, config.encoding.entropy_threshold,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
sheaf_checker = None
|
|
141
|
+
if config.math.sheaf_at_encoding:
|
|
142
|
+
from superlocalmemory.math.sheaf import SheafConsistencyChecker
|
|
143
|
+
sheaf_checker = SheafConsistencyChecker(
|
|
144
|
+
db, config.math.sheaf_contradiction_threshold,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# V3.2: VectorStore (Phase 1) -- sqlite-vec KNN
|
|
148
|
+
vector_store = _init_vector_store(config)
|
|
149
|
+
|
|
150
|
+
# V3.2: AccessLog (Phase 1) -- fact access tracking
|
|
151
|
+
access_log = _init_access_log(db)
|
|
152
|
+
|
|
153
|
+
# V3.2: ContextGenerator (Phase 2) -- contextual descriptions for facts
|
|
154
|
+
context_generator = _init_context_generator(config, llm)
|
|
155
|
+
|
|
156
|
+
# V3.2: TemporalValidator (Phase 4) -- bi-temporal fact invalidation
|
|
157
|
+
temporal_validator = _init_temporal(config, db, sheaf_checker, llm)
|
|
158
|
+
|
|
159
|
+
# V3.2: Phase 3 -- Association Graph components
|
|
160
|
+
auto_linker = _init_auto_linker(db, vector_store, context_generator, config)
|
|
161
|
+
graph_analyzer = _init_graph_analyzer(db)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"ann_index": ann_index,
|
|
165
|
+
"fact_extractor": fact_extractor,
|
|
166
|
+
"entity_resolver": entity_resolver,
|
|
167
|
+
"temporal_parser": temporal_parser,
|
|
168
|
+
"type_router": type_router,
|
|
169
|
+
"graph_builder": graph_builder,
|
|
170
|
+
"consolidator": consolidator,
|
|
171
|
+
"observation_builder": observation_builder,
|
|
172
|
+
"scene_builder": scene_builder,
|
|
173
|
+
"entropy_gate": entropy_gate,
|
|
174
|
+
"sheaf_checker": sheaf_checker,
|
|
175
|
+
"vector_store": vector_store,
|
|
176
|
+
"access_log": access_log,
|
|
177
|
+
"context_generator": context_generator,
|
|
178
|
+
"temporal_validator": temporal_validator,
|
|
179
|
+
"auto_linker": auto_linker,
|
|
180
|
+
"graph_analyzer": graph_analyzer,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _init_vector_store(config: SLMConfig) -> Any | None:
|
|
185
|
+
"""Create VectorStore if sqlite-vec is available. Returns None on failure."""
|
|
186
|
+
try:
|
|
187
|
+
from superlocalmemory.retrieval.vector_store import (
|
|
188
|
+
VectorStore, VectorStoreConfig,
|
|
189
|
+
)
|
|
190
|
+
vec_config = VectorStoreConfig(
|
|
191
|
+
dimension=config.embedding.dimension,
|
|
192
|
+
enabled=True,
|
|
193
|
+
)
|
|
194
|
+
vs = VectorStore(config.db_path, vec_config)
|
|
195
|
+
if vs.available:
|
|
196
|
+
logger.info("VectorStore initialized (sqlite-vec KNN enabled)")
|
|
197
|
+
return vs
|
|
198
|
+
logger.debug("VectorStore unavailable; using ANNIndex fallback")
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.debug("VectorStore init failed: %s", exc)
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _init_access_log(db: DatabaseManager) -> Any | None:
|
|
205
|
+
"""Create AccessLog for fact access tracking."""
|
|
206
|
+
try:
|
|
207
|
+
from superlocalmemory.storage.access_log import AccessLog
|
|
208
|
+
return AccessLog(db)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
logger.debug("AccessLog init failed: %s", exc)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _init_context_generator(config: SLMConfig, llm: Any) -> Any | None:
|
|
215
|
+
"""Create ContextGenerator for Phase 2 contextual descriptions."""
|
|
216
|
+
try:
|
|
217
|
+
from superlocalmemory.encoding.context_generator import ContextGenerator
|
|
218
|
+
if hasattr(config, "auto_invoke") and config.auto_invoke.enabled:
|
|
219
|
+
return ContextGenerator(llm=llm)
|
|
220
|
+
# Still create the generator (rules-only) even when disabled,
|
|
221
|
+
# so store pipeline can generate context when called directly.
|
|
222
|
+
return ContextGenerator(llm=llm)
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.debug("ContextGenerator init failed: %s", exc)
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _init_temporal(
|
|
229
|
+
config: SLMConfig,
|
|
230
|
+
db: DatabaseManager,
|
|
231
|
+
sheaf_checker: Any,
|
|
232
|
+
llm: Any,
|
|
233
|
+
) -> Any | None:
|
|
234
|
+
"""Create TemporalValidator for Phase 4 temporal intelligence."""
|
|
235
|
+
if not config.temporal_validator.enabled:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
from superlocalmemory.encoding.temporal_validator import TemporalValidator
|
|
240
|
+
from superlocalmemory.trust.scorer import TrustScorer
|
|
241
|
+
|
|
242
|
+
trust_scorer = TrustScorer(db)
|
|
243
|
+
tv = TemporalValidator(
|
|
244
|
+
db=db,
|
|
245
|
+
sheaf_checker=sheaf_checker,
|
|
246
|
+
trust_scorer=trust_scorer,
|
|
247
|
+
llm=llm,
|
|
248
|
+
config=config.temporal_validator,
|
|
249
|
+
)
|
|
250
|
+
logger.info("TemporalValidator initialized (mode=%s)", config.temporal_validator.mode)
|
|
251
|
+
return tv
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
logger.debug("TemporalValidator init failed: %s", exc)
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _init_auto_linker(
|
|
258
|
+
db: DatabaseManager,
|
|
259
|
+
vector_store: Any,
|
|
260
|
+
context_generator: Any,
|
|
261
|
+
config: SLMConfig,
|
|
262
|
+
) -> Any | None:
|
|
263
|
+
"""Create AutoLinker for Phase 3 association graph."""
|
|
264
|
+
try:
|
|
265
|
+
from superlocalmemory.encoding.auto_linker import AutoLinker
|
|
266
|
+
return AutoLinker(
|
|
267
|
+
db=db,
|
|
268
|
+
vector_store=vector_store,
|
|
269
|
+
context_generator=context_generator,
|
|
270
|
+
config=config,
|
|
271
|
+
)
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
logger.debug("AutoLinker init failed: %s", exc)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _init_graph_analyzer(db: DatabaseManager) -> Any | None:
|
|
278
|
+
"""Create GraphAnalyzer for Phase 3 structural importance."""
|
|
279
|
+
try:
|
|
280
|
+
from superlocalmemory.core.graph_analyzer import GraphAnalyzer
|
|
281
|
+
return GraphAnalyzer(db=db)
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
logger.debug("GraphAnalyzer init failed: %s", exc)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _init_consolidation(
|
|
288
|
+
config: SLMConfig,
|
|
289
|
+
db: DatabaseManager,
|
|
290
|
+
auto_linker: Any,
|
|
291
|
+
graph_analyzer: Any,
|
|
292
|
+
temporal_validator: Any,
|
|
293
|
+
summarizer: Any,
|
|
294
|
+
behavioral_store: Any,
|
|
295
|
+
) -> Any | None:
|
|
296
|
+
"""Create ConsolidationEngine for Phase 5 sleep-time consolidation."""
|
|
297
|
+
try:
|
|
298
|
+
from superlocalmemory.core.consolidation_engine import ConsolidationEngine
|
|
299
|
+
return ConsolidationEngine(
|
|
300
|
+
db=db,
|
|
301
|
+
config=config.consolidation,
|
|
302
|
+
summarizer=summarizer,
|
|
303
|
+
behavioral_store=behavioral_store,
|
|
304
|
+
auto_linker=auto_linker,
|
|
305
|
+
graph_analyzer=graph_analyzer,
|
|
306
|
+
temporal_validator=temporal_validator,
|
|
307
|
+
slm_config=config,
|
|
308
|
+
)
|
|
309
|
+
except Exception as exc:
|
|
310
|
+
logger.debug("ConsolidationEngine init failed: %s", exc)
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _init_spreading_activation(
|
|
315
|
+
db: DatabaseManager,
|
|
316
|
+
vector_store: Any,
|
|
317
|
+
) -> Any | None:
|
|
318
|
+
"""Create SpreadingActivation for Phase 3 5th retrieval channel."""
|
|
319
|
+
try:
|
|
320
|
+
from superlocalmemory.retrieval.spreading_activation import (
|
|
321
|
+
SpreadingActivation,
|
|
322
|
+
SpreadingActivationConfig,
|
|
323
|
+
)
|
|
324
|
+
sa_config = SpreadingActivationConfig(enabled=False)
|
|
325
|
+
return SpreadingActivation(
|
|
326
|
+
db=db, vector_store=vector_store, config=sa_config,
|
|
327
|
+
)
|
|
328
|
+
except Exception as exc:
|
|
329
|
+
logger.debug("SpreadingActivation init failed: %s", exc)
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _init_auto_invoker(
|
|
334
|
+
config: SLMConfig,
|
|
335
|
+
db: DatabaseManager,
|
|
336
|
+
vector_store: Any,
|
|
337
|
+
trust_scorer: Any,
|
|
338
|
+
embedder: Any,
|
|
339
|
+
) -> Any | None:
|
|
340
|
+
"""Create AutoInvoker for Phase 2 multi-signal retrieval."""
|
|
341
|
+
if not hasattr(config, "auto_invoke") or not config.auto_invoke.enabled:
|
|
342
|
+
return None
|
|
343
|
+
try:
|
|
344
|
+
from superlocalmemory.hooks.auto_invoker import AutoInvoker
|
|
345
|
+
return AutoInvoker(
|
|
346
|
+
db=db,
|
|
347
|
+
vector_store=vector_store,
|
|
348
|
+
trust_scorer=trust_scorer,
|
|
349
|
+
embedder=embedder,
|
|
350
|
+
config=config.auto_invoke,
|
|
351
|
+
)
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
logger.debug("AutoInvoker init failed: %s", exc)
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# init_retrieval (was MemoryEngine._init_retrieval)
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def init_retrieval(
|
|
362
|
+
config: SLMConfig,
|
|
363
|
+
db: DatabaseManager,
|
|
364
|
+
embedder: Any,
|
|
365
|
+
entity_resolver: Any,
|
|
366
|
+
trust_scorer: Any,
|
|
367
|
+
vector_store: Any = None,
|
|
368
|
+
) -> Any:
|
|
369
|
+
"""Create the RetrievalEngine with 5 channels. Returns it."""
|
|
370
|
+
from superlocalmemory.retrieval.engine import RetrievalEngine
|
|
371
|
+
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
372
|
+
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
373
|
+
from superlocalmemory.retrieval.entity_channel import EntityGraphChannel
|
|
374
|
+
from superlocalmemory.retrieval.temporal_channel import TemporalChannel
|
|
375
|
+
from superlocalmemory.retrieval.reranker import CrossEncoderReranker
|
|
376
|
+
from superlocalmemory.retrieval.profile_channel import ProfileChannel
|
|
377
|
+
from superlocalmemory.retrieval.bridge_discovery import BridgeDiscovery
|
|
378
|
+
|
|
379
|
+
channels: dict = {
|
|
380
|
+
"semantic": SemanticChannel(
|
|
381
|
+
db,
|
|
382
|
+
fisher_temperature=config.math.fisher_temperature,
|
|
383
|
+
embedder=embedder,
|
|
384
|
+
fisher_mode=config.math.fisher_mode,
|
|
385
|
+
vector_store=vector_store,
|
|
386
|
+
),
|
|
387
|
+
"bm25": BM25Channel(db),
|
|
388
|
+
"entity_graph": EntityGraphChannel(db, entity_resolver),
|
|
389
|
+
"temporal": TemporalChannel(db),
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# Phase 3: Register SpreadingActivation as 5th channel
|
|
393
|
+
sa_channel = _init_spreading_activation(db, vector_store)
|
|
394
|
+
if sa_channel is not None:
|
|
395
|
+
channels["spreading_activation"] = sa_channel
|
|
396
|
+
|
|
397
|
+
reranker = None
|
|
398
|
+
if config.retrieval.use_cross_encoder:
|
|
399
|
+
reranker = CrossEncoderReranker(config.retrieval.cross_encoder_model)
|
|
400
|
+
|
|
401
|
+
profile_ch = ProfileChannel(db)
|
|
402
|
+
bridge = BridgeDiscovery(db)
|
|
403
|
+
|
|
404
|
+
return RetrievalEngine(
|
|
405
|
+
db=db, config=config.retrieval, channels=channels,
|
|
406
|
+
embedder=embedder, reranker=reranker,
|
|
407
|
+
base_weights=config.channel_weights,
|
|
408
|
+
profile_channel=profile_ch,
|
|
409
|
+
bridge_discovery=bridge,
|
|
410
|
+
trust_scorer=trust_scorer,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# wire_hooks (was MemoryEngine._wire_hooks)
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def wire_hooks(
|
|
419
|
+
hooks: HookRegistry,
|
|
420
|
+
config: SLMConfig,
|
|
421
|
+
db: DatabaseManager,
|
|
422
|
+
trust_scorer: Any,
|
|
423
|
+
profile_id: str,
|
|
424
|
+
) -> dict[str, Any]:
|
|
425
|
+
"""Wire trust, compliance, and event bus hooks into engine lifecycle.
|
|
426
|
+
|
|
427
|
+
Returns dict of created components (signal_recorder, audit_chain)
|
|
428
|
+
so engine can hold references.
|
|
429
|
+
"""
|
|
430
|
+
result: dict[str, Any] = {"signal_recorder": None, "audit_chain": None}
|
|
431
|
+
|
|
432
|
+
# -- Pre-store hooks (synchronous, can reject) --
|
|
433
|
+
if trust_scorer:
|
|
434
|
+
from superlocalmemory.trust.gate import TrustGate
|
|
435
|
+
gate = TrustGate(trust_scorer)
|
|
436
|
+
hooks.register_pre("store", lambda ctx: gate.check_write(
|
|
437
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id)))
|
|
438
|
+
hooks.register_pre("delete", lambda ctx: gate.check_delete(
|
|
439
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id)))
|
|
440
|
+
|
|
441
|
+
# -- Post-store hooks (async, never block) --
|
|
442
|
+
if trust_scorer:
|
|
443
|
+
hooks.register_post("store", lambda ctx: trust_scorer.record_signal(
|
|
444
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id), "store_success"))
|
|
445
|
+
hooks.register_post("recall", lambda ctx: trust_scorer.record_signal(
|
|
446
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id), "recall_hit"))
|
|
447
|
+
|
|
448
|
+
# -- Burst detection via SignalRecorder --
|
|
449
|
+
try:
|
|
450
|
+
from superlocalmemory.trust.signals import SignalRecorder
|
|
451
|
+
signal_recorder = SignalRecorder(db)
|
|
452
|
+
hooks.register_post("store", lambda ctx: signal_recorder.record(
|
|
453
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id), "store_success"))
|
|
454
|
+
result["signal_recorder"] = signal_recorder
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
# -- Tamper-proof audit chain (all operations logged with hash chain) --
|
|
459
|
+
try:
|
|
460
|
+
from superlocalmemory.compliance.audit import AuditChain
|
|
461
|
+
audit_path = config.db_path.parent / "audit_chain.db"
|
|
462
|
+
audit_chain = AuditChain(audit_path)
|
|
463
|
+
for op in ("store", "recall", "delete"):
|
|
464
|
+
hooks.register_post(op, lambda ctx, _op=op: audit_chain.log(
|
|
465
|
+
operation=_op,
|
|
466
|
+
agent_id=ctx.get("agent_id", "unknown"),
|
|
467
|
+
profile_id=ctx.get("profile_id", profile_id),
|
|
468
|
+
content_hash=ctx.get("content_hash", ""),
|
|
469
|
+
))
|
|
470
|
+
result["audit_chain"] = audit_chain
|
|
471
|
+
except Exception:
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
return result
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3
|
|
4
|
+
|
|
5
|
+
"""Graph structural analysis -- PageRank, community detection, centrality.
|
|
6
|
+
|
|
7
|
+
Reads BOTH graph_edges and association_edges for the full graph picture.
|
|
8
|
+
Stores results in fact_importance table.
|
|
9
|
+
Called during consolidation (Phase 5), not at query time.
|
|
10
|
+
|
|
11
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
12
|
+
License: MIT
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GraphAnalyzer:
|
|
24
|
+
"""Compute structural importance metrics for the memory graph.
|
|
25
|
+
|
|
26
|
+
- PageRank: global structural importance via networkx
|
|
27
|
+
- Community detection: Label Propagation via networkx
|
|
28
|
+
- Degree centrality: connection count normalization
|
|
29
|
+
|
|
30
|
+
Reads BOTH graph_edges and association_edges (Rule 13).
|
|
31
|
+
Stores results in fact_importance table.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, db: Any) -> None:
|
|
35
|
+
self._db = db
|
|
36
|
+
|
|
37
|
+
def compute_and_store(self, profile_id: str) -> dict[str, Any]:
|
|
38
|
+
"""Run all analyses and persist to fact_importance.
|
|
39
|
+
|
|
40
|
+
Returns summary dict with node_count, community_count, top_5_nodes.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
graph = self._build_networkx_graph(profile_id)
|
|
44
|
+
if graph.number_of_nodes() == 0:
|
|
45
|
+
return {
|
|
46
|
+
"node_count": 0,
|
|
47
|
+
"edge_count": 0,
|
|
48
|
+
"community_count": 0,
|
|
49
|
+
"top_5_nodes": [],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pagerank = self.compute_pagerank(graph)
|
|
53
|
+
communities = self.detect_communities(graph)
|
|
54
|
+
centrality = self._compute_degree_centrality(graph)
|
|
55
|
+
|
|
56
|
+
# Persist to fact_importance
|
|
57
|
+
for node_id in graph.nodes():
|
|
58
|
+
pr_score = pagerank.get(node_id, 0.0)
|
|
59
|
+
comm_id = communities.get(node_id)
|
|
60
|
+
deg_cent = centrality.get(node_id, 0.0)
|
|
61
|
+
self._db.execute(
|
|
62
|
+
"INSERT OR REPLACE INTO fact_importance "
|
|
63
|
+
"(fact_id, profile_id, pagerank_score, community_id, "
|
|
64
|
+
" degree_centrality, computed_at) "
|
|
65
|
+
"VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
|
66
|
+
(node_id, profile_id, round(pr_score, 6),
|
|
67
|
+
comm_id, round(deg_cent, 4)),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
top_5 = sorted(
|
|
71
|
+
pagerank.items(), key=lambda x: x[1], reverse=True,
|
|
72
|
+
)[:5]
|
|
73
|
+
unique_communities = len(
|
|
74
|
+
set(c for c in communities.values() if c is not None),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"node_count": graph.number_of_nodes(),
|
|
79
|
+
"edge_count": graph.number_of_edges(),
|
|
80
|
+
"community_count": unique_communities,
|
|
81
|
+
"top_5_nodes": [
|
|
82
|
+
(nid, round(score, 4)) for nid, score in top_5
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
logger.debug("GraphAnalyzer.compute_and_store failed: %s", exc)
|
|
87
|
+
return {
|
|
88
|
+
"node_count": 0,
|
|
89
|
+
"edge_count": 0,
|
|
90
|
+
"community_count": 0,
|
|
91
|
+
"top_5_nodes": [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def compute_pagerank(
|
|
95
|
+
self,
|
|
96
|
+
graph: Any = None,
|
|
97
|
+
profile_id: str = "",
|
|
98
|
+
alpha: float = 0.85,
|
|
99
|
+
) -> dict[str, float]:
|
|
100
|
+
"""Compute PageRank using networkx.
|
|
101
|
+
|
|
102
|
+
alpha = damping factor (0.85 is standard).
|
|
103
|
+
"""
|
|
104
|
+
import networkx as nx
|
|
105
|
+
|
|
106
|
+
if graph is None:
|
|
107
|
+
graph = self._build_networkx_graph(profile_id)
|
|
108
|
+
if graph.number_of_nodes() == 0:
|
|
109
|
+
return {}
|
|
110
|
+
try:
|
|
111
|
+
return nx.pagerank(graph, alpha=alpha, weight="weight")
|
|
112
|
+
except nx.PowerIterationFailedConvergence:
|
|
113
|
+
return nx.pagerank(graph, alpha=alpha, weight=None)
|
|
114
|
+
|
|
115
|
+
def detect_communities(
|
|
116
|
+
self,
|
|
117
|
+
graph: Any = None,
|
|
118
|
+
profile_id: str = "",
|
|
119
|
+
) -> dict[str, int]:
|
|
120
|
+
"""Detect communities via Label Propagation.
|
|
121
|
+
|
|
122
|
+
O(m) where m = edges (fast), no parameter tuning needed.
|
|
123
|
+
"""
|
|
124
|
+
import networkx as nx
|
|
125
|
+
from networkx.algorithms.community import (
|
|
126
|
+
label_propagation_communities,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if graph is None:
|
|
130
|
+
graph = self._build_networkx_graph(profile_id)
|
|
131
|
+
if graph.number_of_nodes() == 0:
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
# Label propagation needs undirected graph
|
|
135
|
+
undirected = graph.to_undirected()
|
|
136
|
+
communities_gen = label_propagation_communities(undirected)
|
|
137
|
+
result: dict[str, int] = {}
|
|
138
|
+
for comm_id, community in enumerate(communities_gen):
|
|
139
|
+
for node in community:
|
|
140
|
+
result[node] = comm_id
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _compute_degree_centrality(
|
|
144
|
+
self, graph: Any,
|
|
145
|
+
) -> dict[str, float]:
|
|
146
|
+
"""Degree centrality: fraction of nodes each node connects to."""
|
|
147
|
+
import networkx as nx
|
|
148
|
+
|
|
149
|
+
if graph.number_of_nodes() <= 1:
|
|
150
|
+
return {n: 0.0 for n in graph.nodes()}
|
|
151
|
+
return nx.degree_centrality(graph)
|
|
152
|
+
|
|
153
|
+
def _build_networkx_graph(self, profile_id: str) -> Any:
|
|
154
|
+
"""Build networkx DiGraph from BOTH graph_edges + association_edges."""
|
|
155
|
+
import networkx as nx
|
|
156
|
+
|
|
157
|
+
g = nx.DiGraph()
|
|
158
|
+
|
|
159
|
+
# graph_edges
|
|
160
|
+
try:
|
|
161
|
+
rows = self._db.execute(
|
|
162
|
+
"SELECT source_id, target_id, weight, edge_type "
|
|
163
|
+
"FROM graph_edges WHERE profile_id = ?",
|
|
164
|
+
(profile_id,),
|
|
165
|
+
)
|
|
166
|
+
for row in rows:
|
|
167
|
+
d = dict(row)
|
|
168
|
+
g.add_edge(
|
|
169
|
+
d["source_id"], d["target_id"],
|
|
170
|
+
weight=d["weight"], edge_type=d["edge_type"],
|
|
171
|
+
)
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
logger.debug("graph_edges read failed: %s", exc)
|
|
174
|
+
|
|
175
|
+
# association_edges
|
|
176
|
+
try:
|
|
177
|
+
rows = self._db.execute(
|
|
178
|
+
"SELECT source_fact_id, target_fact_id, weight, "
|
|
179
|
+
" association_type "
|
|
180
|
+
"FROM association_edges WHERE profile_id = ?",
|
|
181
|
+
(profile_id,),
|
|
182
|
+
)
|
|
183
|
+
for row in rows:
|
|
184
|
+
d = dict(row)
|
|
185
|
+
src, tgt = d["source_fact_id"], d["target_fact_id"]
|
|
186
|
+
if g.has_edge(src, tgt):
|
|
187
|
+
existing_w = g[src][tgt].get("weight", 0)
|
|
188
|
+
if d["weight"] > existing_w:
|
|
189
|
+
g[src][tgt]["weight"] = d["weight"]
|
|
190
|
+
else:
|
|
191
|
+
g.add_edge(
|
|
192
|
+
src, tgt,
|
|
193
|
+
weight=d["weight"],
|
|
194
|
+
edge_type=d["association_type"],
|
|
195
|
+
)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
logger.debug("association_edges read failed: %s", exc)
|
|
198
|
+
|
|
199
|
+
return g
|