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
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
# Licensed under the MIT License - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
4
|
|
|
5
|
-
"""SuperLocalMemory V3 — Main Memory Engine.
|
|
5
|
+
"""SuperLocalMemory V3 — Main Memory Engine (Facade).
|
|
6
|
+
|
|
7
|
+
Thin orchestrator that delegates to extracted pipeline modules:
|
|
8
|
+
- store_pipeline (store, store_fact_direct, close_session, enrich_fact)
|
|
9
|
+
- recall_pipeline (recall, adaptive ranking)
|
|
10
|
+
- engine_wiring (embedder init, encoding init, retrieval init, hooks)
|
|
6
11
|
|
|
7
|
-
Orchestrates the full memory lifecycle: store, encode, retrieve.
|
|
8
12
|
Single entry point for all memory operations.
|
|
9
13
|
Profile-scoped. Mode-aware (A/B/C).
|
|
10
14
|
|
|
@@ -19,7 +23,7 @@ from typing import Any
|
|
|
19
23
|
from superlocalmemory.core.config import SLMConfig
|
|
20
24
|
from superlocalmemory.core.modes import get_capabilities
|
|
21
25
|
from superlocalmemory.storage.models import (
|
|
22
|
-
AtomicFact,
|
|
26
|
+
AtomicFact, MemoryRecord, Mode, RecallResponse,
|
|
23
27
|
)
|
|
24
28
|
|
|
25
29
|
logger = logging.getLogger(__name__)
|
|
@@ -67,8 +71,35 @@ class MemoryEngine:
|
|
|
67
71
|
self._provenance = None
|
|
68
72
|
self._adaptive_learner = None
|
|
69
73
|
self._compliance_checker = None
|
|
74
|
+
self._vector_store = None
|
|
75
|
+
self._access_log = None
|
|
76
|
+
self._context_generator = None
|
|
77
|
+
self._temporal_validator = None
|
|
78
|
+
self._auto_invoker = None
|
|
79
|
+
self._auto_linker = None
|
|
80
|
+
self._graph_analyzer = None
|
|
81
|
+
self._consolidation_engine = None
|
|
70
82
|
self._hooks = HookRegistry()
|
|
71
83
|
|
|
84
|
+
# -- Public properties (Phase 2+ access) --------------------------------
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def db(self):
|
|
88
|
+
"""Database manager (read-only access for Phase 2+)."""
|
|
89
|
+
return self._db
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def trust_scorer(self):
|
|
93
|
+
"""Trust scorer (read-only access for Phase 2+)."""
|
|
94
|
+
return self._trust_scorer
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def embedder(self):
|
|
98
|
+
"""Embedding service (read-only access for Phase 2+)."""
|
|
99
|
+
return self._embedder
|
|
100
|
+
|
|
101
|
+
# -- Initialization -----------------------------------------------------
|
|
102
|
+
|
|
72
103
|
def initialize(self) -> None:
|
|
73
104
|
"""Initialize all components. Call once before use."""
|
|
74
105
|
if self._initialized:
|
|
@@ -76,17 +107,22 @@ class MemoryEngine:
|
|
|
76
107
|
|
|
77
108
|
from superlocalmemory.storage import schema
|
|
78
109
|
from superlocalmemory.storage.database import DatabaseManager
|
|
79
|
-
from superlocalmemory.core.embeddings import EmbeddingService
|
|
80
110
|
from superlocalmemory.llm.backbone import LLMBackbone
|
|
111
|
+
from superlocalmemory.core.engine_wiring import (
|
|
112
|
+
init_embedder, init_encoding, init_retrieval, wire_hooks,
|
|
113
|
+
_init_auto_invoker, _init_consolidation,
|
|
114
|
+
)
|
|
81
115
|
|
|
82
116
|
self._db = DatabaseManager(self._config.db_path)
|
|
83
117
|
self._db.initialize(schema)
|
|
84
|
-
self._embedder = self.
|
|
118
|
+
self._embedder = init_embedder(self._config)
|
|
85
119
|
|
|
86
120
|
if self._caps.llm_fact_extraction:
|
|
87
121
|
self._llm = LLMBackbone(self._config.llm)
|
|
88
122
|
if not self._llm.is_available():
|
|
89
|
-
logger.warning(
|
|
123
|
+
logger.warning(
|
|
124
|
+
"LLM not available. Falling back to Mode A extraction.",
|
|
125
|
+
)
|
|
90
126
|
self._llm = None
|
|
91
127
|
|
|
92
128
|
from superlocalmemory.trust.scorer import TrustScorer
|
|
@@ -96,89 +132,72 @@ class MemoryEngine:
|
|
|
96
132
|
|
|
97
133
|
self._trust_scorer = TrustScorer(self._db)
|
|
98
134
|
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
# Encoding components
|
|
136
|
+
enc = init_encoding(
|
|
137
|
+
self._config, self._db, self._embedder, self._llm,
|
|
138
|
+
)
|
|
139
|
+
self._ann_index = enc["ann_index"]
|
|
140
|
+
self._fact_extractor = enc["fact_extractor"]
|
|
141
|
+
self._entity_resolver = enc["entity_resolver"]
|
|
142
|
+
self._temporal_parser = enc["temporal_parser"]
|
|
143
|
+
self._type_router = enc["type_router"]
|
|
144
|
+
self._graph_builder = enc["graph_builder"]
|
|
145
|
+
self._consolidator = enc["consolidator"]
|
|
146
|
+
self._observation_builder = enc["observation_builder"]
|
|
147
|
+
self._scene_builder = enc["scene_builder"]
|
|
148
|
+
self._entropy_gate = enc["entropy_gate"]
|
|
149
|
+
self._sheaf_checker = enc["sheaf_checker"]
|
|
150
|
+
self._vector_store = enc.get("vector_store")
|
|
151
|
+
self._access_log = enc.get("access_log")
|
|
152
|
+
self._context_generator = enc.get("context_generator")
|
|
153
|
+
self._temporal_validator = enc.get("temporal_validator")
|
|
154
|
+
self._auto_linker = enc.get("auto_linker")
|
|
155
|
+
self._graph_analyzer = enc.get("graph_analyzer")
|
|
156
|
+
|
|
157
|
+
# Retrieval engine
|
|
158
|
+
self._retrieval_engine = init_retrieval(
|
|
159
|
+
self._config, self._db, self._embedder,
|
|
160
|
+
self._entity_resolver, self._trust_scorer,
|
|
161
|
+
vector_store=self._vector_store,
|
|
162
|
+
)
|
|
101
163
|
|
|
102
164
|
self._provenance = ProvenanceTracker(self._db)
|
|
103
165
|
self._adaptive_learner = AdaptiveLearner(self._db)
|
|
104
166
|
self._compliance_checker = EUAIActChecker()
|
|
105
167
|
|
|
106
168
|
# Wire lifecycle hooks
|
|
107
|
-
|
|
169
|
+
hook_result = wire_hooks(
|
|
170
|
+
self._hooks, self._config, self._db,
|
|
171
|
+
self._trust_scorer, self._profile_id,
|
|
172
|
+
)
|
|
173
|
+
self._signal_recorder = hook_result["signal_recorder"]
|
|
174
|
+
self._audit_chain = hook_result["audit_chain"]
|
|
175
|
+
|
|
176
|
+
# V3.2: AutoInvoker (Phase 2) -- multi-signal auto-recall
|
|
177
|
+
self._auto_invoker = _init_auto_invoker(
|
|
178
|
+
self._config, self._db, self._vector_store,
|
|
179
|
+
self._trust_scorer, self._embedder,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# V3.2: ConsolidationEngine (Phase 5) -- sleep-time consolidation
|
|
183
|
+
from superlocalmemory.core.summarizer import Summarizer
|
|
184
|
+
summarizer = Summarizer(self._config)
|
|
185
|
+
self._consolidation_engine = _init_consolidation(
|
|
186
|
+
self._config, self._db,
|
|
187
|
+
auto_linker=self._auto_linker,
|
|
188
|
+
graph_analyzer=self._graph_analyzer,
|
|
189
|
+
temporal_validator=self._temporal_validator,
|
|
190
|
+
summarizer=summarizer,
|
|
191
|
+
behavioral_store=None,
|
|
192
|
+
)
|
|
108
193
|
|
|
109
194
|
self._initialized = True
|
|
110
|
-
logger.info(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Priority order:
|
|
117
|
-
1. Explicit provider in config (ollama / cloud / sentence-transformers)
|
|
118
|
-
2. Auto-detect: if LLM provider=ollama and Ollama has embedding model → use it
|
|
119
|
-
3. Fallback to sentence-transformers subprocess
|
|
120
|
-
4. If nothing works → None (BM25-only mode)
|
|
121
|
-
"""
|
|
122
|
-
from superlocalmemory.core.embeddings import EmbeddingService
|
|
123
|
-
|
|
124
|
-
emb_cfg = self._config.embedding
|
|
125
|
-
provider = emb_cfg.provider
|
|
126
|
-
|
|
127
|
-
# --- Explicit ollama provider ---
|
|
128
|
-
if provider == "ollama":
|
|
129
|
-
return self._try_ollama_embedder(emb_cfg)
|
|
130
|
-
|
|
131
|
-
# --- Explicit cloud provider ---
|
|
132
|
-
if provider == "cloud" or emb_cfg.is_cloud:
|
|
133
|
-
return self._try_service_embedder(EmbeddingService, emb_cfg)
|
|
134
|
-
|
|
135
|
-
# --- Explicit sentence-transformers ---
|
|
136
|
-
if provider == "sentence-transformers":
|
|
137
|
-
return self._try_service_embedder(EmbeddingService, emb_cfg)
|
|
138
|
-
|
|
139
|
-
# --- Auto-detect: try Ollama first (fast path, <1s) ---
|
|
140
|
-
# Check regardless of LLM provider — if Ollama is running and has
|
|
141
|
-
# the embedding model, use it. This avoids the 30s cold start of
|
|
142
|
-
# sentence-transformers subprocess.
|
|
143
|
-
ollama_emb = self._try_ollama_embedder(emb_cfg)
|
|
144
|
-
if ollama_emb is not None:
|
|
145
|
-
logger.info("Auto-detected Ollama embeddings (fast path)")
|
|
146
|
-
return ollama_emb
|
|
147
|
-
|
|
148
|
-
# --- Fallback: sentence-transformers subprocess ---
|
|
149
|
-
return self._try_service_embedder(EmbeddingService, emb_cfg)
|
|
150
|
-
|
|
151
|
-
def _try_ollama_embedder(self, emb_cfg):
|
|
152
|
-
"""Try to create an OllamaEmbedder. Returns it or None."""
|
|
153
|
-
try:
|
|
154
|
-
from superlocalmemory.core.ollama_embedder import OllamaEmbedder
|
|
155
|
-
emb = OllamaEmbedder(
|
|
156
|
-
model=emb_cfg.ollama_model,
|
|
157
|
-
base_url=emb_cfg.ollama_base_url,
|
|
158
|
-
dimension=emb_cfg.dimension,
|
|
159
|
-
)
|
|
160
|
-
if emb.is_available:
|
|
161
|
-
logger.info("Using Ollama embeddings (%s)", emb_cfg.ollama_model)
|
|
162
|
-
return emb
|
|
163
|
-
logger.warning(
|
|
164
|
-
"Ollama embedder not available (model=%s). Falling back.",
|
|
165
|
-
emb_cfg.ollama_model,
|
|
166
|
-
)
|
|
167
|
-
except Exception as exc:
|
|
168
|
-
logger.warning("OllamaEmbedder init failed: %s", exc)
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
@staticmethod
|
|
172
|
-
def _try_service_embedder(cls, emb_cfg):
|
|
173
|
-
"""Try to create an EmbeddingService. Returns it or None."""
|
|
174
|
-
try:
|
|
175
|
-
emb = cls(emb_cfg)
|
|
176
|
-
if emb.is_available:
|
|
177
|
-
return emb
|
|
178
|
-
logger.warning("EmbeddingService not available. BM25-only mode. Run 'slm doctor' to diagnose.")
|
|
179
|
-
except Exception as exc:
|
|
180
|
-
logger.warning("Embeddings unavailable (%s). BM25-only mode. Run 'slm doctor' to diagnose.", exc)
|
|
181
|
-
return None
|
|
195
|
+
logger.info(
|
|
196
|
+
"MemoryEngine initialized: mode=%s profile=%s",
|
|
197
|
+
self._config.mode.value, self._profile_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# -- Store operations ---------------------------------------------------
|
|
182
201
|
|
|
183
202
|
def store(
|
|
184
203
|
self,
|
|
@@ -192,364 +211,79 @@ class MemoryEngine:
|
|
|
192
211
|
"""Store content and extract structured facts. Returns fact_ids."""
|
|
193
212
|
self._ensure_init()
|
|
194
213
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
from superlocalmemory.core.store_pipeline import run_store
|
|
215
|
+
return run_store(
|
|
216
|
+
content, self._profile_id,
|
|
217
|
+
session_id=session_id, session_date=session_date,
|
|
218
|
+
speaker=speaker, role=role, metadata=metadata,
|
|
219
|
+
config=self._config, db=self._db,
|
|
220
|
+
embedder=self._embedder,
|
|
221
|
+
fact_extractor=self._fact_extractor,
|
|
222
|
+
entity_resolver=self._entity_resolver,
|
|
223
|
+
temporal_parser=self._temporal_parser,
|
|
224
|
+
type_router=self._type_router,
|
|
225
|
+
graph_builder=self._graph_builder,
|
|
226
|
+
consolidator=self._consolidator,
|
|
227
|
+
observation_builder=self._observation_builder,
|
|
228
|
+
scene_builder=self._scene_builder,
|
|
229
|
+
entropy_gate=self._entropy_gate,
|
|
230
|
+
ann_index=self._ann_index,
|
|
231
|
+
sheaf_checker=self._sheaf_checker,
|
|
232
|
+
retrieval_engine=self._retrieval_engine,
|
|
233
|
+
provenance=self._provenance,
|
|
234
|
+
hooks=self._hooks,
|
|
235
|
+
vector_store=self._vector_store,
|
|
236
|
+
context_generator=self._context_generator,
|
|
237
|
+
temporal_validator=self._temporal_validator,
|
|
238
|
+
auto_linker=self._auto_linker,
|
|
239
|
+
consolidation_engine=self._consolidation_engine,
|
|
240
|
+
)
|
|
202
241
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
242
|
+
def store_fact_direct(self, fact: AtomicFact) -> str:
|
|
243
|
+
"""Store a pre-built fact with full enrichment."""
|
|
244
|
+
self._ensure_init()
|
|
206
245
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
from superlocalmemory.core.store_pipeline import run_store_fact_direct
|
|
247
|
+
return run_store_fact_direct(
|
|
248
|
+
fact, self._profile_id,
|
|
249
|
+
db=self._db, embedder=self._embedder,
|
|
250
|
+
entity_resolver=self._entity_resolver,
|
|
251
|
+
ann_index=self._ann_index,
|
|
252
|
+
graph_builder=self._graph_builder,
|
|
253
|
+
retrieval_engine=self._retrieval_engine,
|
|
254
|
+
vector_store=self._vector_store,
|
|
211
255
|
)
|
|
212
|
-
self._db.store_memory(record)
|
|
213
256
|
|
|
214
|
-
|
|
215
|
-
turns=[content], session_id=session_id,
|
|
216
|
-
session_date=parsed_date, speaker_a=speaker,
|
|
217
|
-
)
|
|
218
|
-
if not facts:
|
|
219
|
-
return []
|
|
220
|
-
|
|
221
|
-
if self._type_router:
|
|
222
|
-
facts = self._type_router.route_facts(facts)
|
|
223
|
-
|
|
224
|
-
stored_ids: list[str] = []
|
|
225
|
-
for fact in facts:
|
|
226
|
-
fact = self._enrich_fact(fact, record)
|
|
227
|
-
|
|
228
|
-
if self._consolidator:
|
|
229
|
-
action = self._consolidator.consolidate(fact, self._profile_id)
|
|
230
|
-
if action.action_type.value == "noop":
|
|
231
|
-
continue
|
|
232
|
-
|
|
233
|
-
# Opinion confidence tracking: reinforce or decay
|
|
234
|
-
# When a new opinion aligns with existing, boost confidence.
|
|
235
|
-
# When contradicted (supersede), reduce old fact's confidence.
|
|
236
|
-
if fact.fact_type == FactType.OPINION and action.action_type.value == "update":
|
|
237
|
-
try:
|
|
238
|
-
existing = self._db.get_fact(action.new_fact_id)
|
|
239
|
-
if existing and existing.fact_type == FactType.OPINION:
|
|
240
|
-
new_conf = min(1.0, existing.confidence + 0.1)
|
|
241
|
-
self._db.update_fact(action.new_fact_id, {"confidence": new_conf})
|
|
242
|
-
except Exception:
|
|
243
|
-
pass
|
|
244
|
-
elif fact.fact_type == FactType.OPINION and action.action_type.value == "supersede":
|
|
245
|
-
try:
|
|
246
|
-
old_id = getattr(action, "old_fact_id", None)
|
|
247
|
-
if old_id:
|
|
248
|
-
old_fact = self._db.get_fact(old_id)
|
|
249
|
-
if old_fact:
|
|
250
|
-
new_conf = max(0.0, old_fact.confidence - 0.2)
|
|
251
|
-
self._db.update_fact(old_id, {"confidence": new_conf})
|
|
252
|
-
except Exception:
|
|
253
|
-
pass
|
|
254
|
-
|
|
255
|
-
if action.action_type.value in ("update", "supersede"):
|
|
256
|
-
# Run post-processing on updated facts
|
|
257
|
-
updated_fact = self._db.get_fact(action.new_fact_id)
|
|
258
|
-
if updated_fact:
|
|
259
|
-
if self._graph_builder:
|
|
260
|
-
self._graph_builder.build_edges(updated_fact, self._profile_id)
|
|
261
|
-
if self._observation_builder:
|
|
262
|
-
for eid in updated_fact.canonical_entities:
|
|
263
|
-
self._observation_builder.update_profile(
|
|
264
|
-
eid, updated_fact, self._profile_id,
|
|
265
|
-
)
|
|
266
|
-
stored_ids.append(action.new_fact_id)
|
|
267
|
-
continue
|
|
268
|
-
# ADD case: consolidator already stored the fact (F8 fix)
|
|
269
|
-
# Fall through to post-processing below
|
|
270
|
-
else:
|
|
271
|
-
self._db.store_fact(fact)
|
|
272
|
-
|
|
273
|
-
stored_ids.append(fact.fact_id)
|
|
274
|
-
|
|
275
|
-
if fact.embedding and self._ann_index:
|
|
276
|
-
self._ann_index.add(fact.fact_id, fact.embedding)
|
|
277
|
-
if self._graph_builder:
|
|
278
|
-
self._graph_builder.build_edges(fact, self._profile_id)
|
|
279
|
-
|
|
280
|
-
# Sheaf consistency check (runs after edges exist)
|
|
281
|
-
# Cap edge check to prevent O(N^2) hang on large graphs
|
|
282
|
-
if (self._sheaf_checker
|
|
283
|
-
and fact.embedding
|
|
284
|
-
and fact.canonical_entities):
|
|
285
|
-
from superlocalmemory.storage.models import EdgeType, GraphEdge
|
|
286
|
-
try:
|
|
287
|
-
edges_for_fact = self._db.get_edges_for_node(
|
|
288
|
-
fact.fact_id, self._profile_id,
|
|
289
|
-
)
|
|
290
|
-
# Only run sheaf if edge count is manageable
|
|
291
|
-
# At 18K+ edges, the coboundary computation becomes O(N*dim^2)
|
|
292
|
-
if len(edges_for_fact) < self._config.math.sheaf_max_edges_per_check:
|
|
293
|
-
contradictions = self._sheaf_checker.check_consistency(
|
|
294
|
-
fact, self._profile_id,
|
|
295
|
-
)
|
|
296
|
-
for c in contradictions:
|
|
297
|
-
if c.severity > 0.45:
|
|
298
|
-
edge = GraphEdge(
|
|
299
|
-
profile_id=self._profile_id,
|
|
300
|
-
source_id=fact.fact_id,
|
|
301
|
-
target_id=c.fact_id_b,
|
|
302
|
-
edge_type=EdgeType.SUPERSEDES,
|
|
303
|
-
weight=c.severity,
|
|
304
|
-
)
|
|
305
|
-
self._db.store_edge(edge)
|
|
306
|
-
except Exception as exc:
|
|
307
|
-
logger.debug("Sheaf check skipped: %s", exc)
|
|
308
|
-
|
|
309
|
-
if self._observation_builder:
|
|
310
|
-
for eid in fact.canonical_entities:
|
|
311
|
-
self._observation_builder.update_profile(eid, fact, self._profile_id)
|
|
312
|
-
|
|
313
|
-
# Increment fact_count for each linked canonical entity
|
|
314
|
-
for eid in fact.canonical_entities:
|
|
315
|
-
try:
|
|
316
|
-
self._db.increment_entity_fact_count(eid)
|
|
317
|
-
except Exception:
|
|
318
|
-
pass # Non-critical — entity may have been deleted
|
|
319
|
-
if self._scene_builder:
|
|
320
|
-
self._scene_builder.assign_to_scene(fact, self._profile_id)
|
|
321
|
-
|
|
322
|
-
# Populate temporal_events for temporal retrieval
|
|
323
|
-
has_dates = (fact.observation_date or fact.referenced_date
|
|
324
|
-
or fact.interval_start)
|
|
325
|
-
if fact.canonical_entities and has_dates:
|
|
326
|
-
from superlocalmemory.storage.models import TemporalEvent
|
|
327
|
-
for eid in fact.canonical_entities:
|
|
328
|
-
event = TemporalEvent(
|
|
329
|
-
profile_id=self._profile_id, entity_id=eid,
|
|
330
|
-
fact_id=fact.fact_id,
|
|
331
|
-
observation_date=fact.observation_date,
|
|
332
|
-
referenced_date=fact.referenced_date,
|
|
333
|
-
interval_start=fact.interval_start,
|
|
334
|
-
interval_end=fact.interval_end,
|
|
335
|
-
description=fact.content[:200],
|
|
336
|
-
)
|
|
337
|
-
self._db.store_temporal_event(event)
|
|
338
|
-
|
|
339
|
-
# Foresight: extract time-bounded predictions
|
|
340
|
-
try:
|
|
341
|
-
from superlocalmemory.encoding.foresight import extract_foresight_signals
|
|
342
|
-
from superlocalmemory.storage.models import TemporalEvent as _TE
|
|
343
|
-
foresight_signals = extract_foresight_signals(fact)
|
|
344
|
-
for sig in foresight_signals:
|
|
345
|
-
f_event = _TE(
|
|
346
|
-
profile_id=self._profile_id,
|
|
347
|
-
entity_id=sig.get("entity_id", ""),
|
|
348
|
-
fact_id=fact.fact_id,
|
|
349
|
-
interval_start=sig.get("start_time"),
|
|
350
|
-
interval_end=sig.get("end_time"),
|
|
351
|
-
description=sig.get("description", ""),
|
|
352
|
-
)
|
|
353
|
-
self._db.store_temporal_event(f_event)
|
|
354
|
-
except Exception as exc:
|
|
355
|
-
logger.debug("Foresight extraction: %s", exc)
|
|
356
|
-
|
|
357
|
-
# Persist BM25 tokens at ingestion
|
|
358
|
-
bm25 = getattr(self._retrieval_engine, '_bm25', None) if self._retrieval_engine else None
|
|
359
|
-
if bm25:
|
|
360
|
-
bm25.add(fact.fact_id, fact.content, self._profile_id)
|
|
361
|
-
|
|
362
|
-
# Record provenance for data lineage (EU AI Act Art. 10)
|
|
363
|
-
if self._provenance:
|
|
364
|
-
try:
|
|
365
|
-
self._provenance.record(
|
|
366
|
-
fact_id=fact.fact_id,
|
|
367
|
-
profile_id=self._profile_id,
|
|
368
|
-
source_type="store",
|
|
369
|
-
source_id=session_id,
|
|
370
|
-
created_by=speaker or "unknown",
|
|
371
|
-
)
|
|
372
|
-
except Exception:
|
|
373
|
-
pass
|
|
374
|
-
|
|
375
|
-
logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
|
|
376
|
-
|
|
377
|
-
# Post-operation hooks (audit, trust signal, event bus)
|
|
378
|
-
hook_ctx["fact_ids"] = stored_ids
|
|
379
|
-
hook_ctx["fact_count"] = len(stored_ids)
|
|
380
|
-
self._hooks.run_post("store", hook_ctx)
|
|
381
|
-
|
|
382
|
-
return stored_ids
|
|
257
|
+
# -- Recall operations --------------------------------------------------
|
|
383
258
|
|
|
384
259
|
def recall(
|
|
385
260
|
self, query: str, profile_id: str | None = None,
|
|
386
261
|
mode: Mode | None = None, limit: int = 20,
|
|
387
262
|
agent_id: str = "unknown",
|
|
388
263
|
) -> RecallResponse:
|
|
389
|
-
"""Recall relevant facts for a query.
|
|
390
|
-
|
|
391
|
-
Pipeline: retrieval → agentic sufficiency (if configured) → post-recall updates.
|
|
392
|
-
Agentic sufficiency (sufficiency check): triggers 2-round re-retrieval when
|
|
393
|
-
initial results are insufficient. Mode C uses LLM judgment; Mode A uses
|
|
394
|
-
heuristic alias expansion.
|
|
395
|
-
"""
|
|
264
|
+
"""Recall relevant facts for a query."""
|
|
396
265
|
self._ensure_init()
|
|
397
266
|
|
|
398
|
-
# Pre-operation hooks
|
|
399
|
-
hook_ctx = {"operation": "recall", "agent_id": agent_id,
|
|
400
|
-
"profile_id": profile_id or self._profile_id, "query_preview": query[:100]}
|
|
401
|
-
self._hooks.run_pre("recall", hook_ctx)
|
|
402
|
-
|
|
403
267
|
pid = profile_id or self._profile_id
|
|
404
|
-
m = mode or self._config.mode
|
|
405
|
-
|
|
406
|
-
response = self._retrieval_engine.recall(query, pid, m, limit)
|
|
407
|
-
|
|
408
|
-
# Agentic sufficiency verification
|
|
409
|
-
# Only trigger when: (a) configured rounds > 0, (b) results look weak
|
|
410
|
-
agentic_rounds = self._config.retrieval.agentic_max_rounds
|
|
411
|
-
if agentic_rounds > 0 and response.results:
|
|
412
|
-
max_score = max((r.score for r in response.results), default=0.0)
|
|
413
|
-
should_trigger = (
|
|
414
|
-
max_score < self._config.retrieval.agentic_confidence_threshold
|
|
415
|
-
or response.query_type == "multi_hop"
|
|
416
|
-
or len(response.results) < 3
|
|
417
|
-
)
|
|
418
|
-
if should_trigger:
|
|
419
|
-
try:
|
|
420
|
-
from superlocalmemory.retrieval.agentic import AgenticRetriever
|
|
421
|
-
agentic = AgenticRetriever(
|
|
422
|
-
confidence_threshold=self._config.retrieval.agentic_confidence_threshold,
|
|
423
|
-
db=self._db,
|
|
424
|
-
)
|
|
425
|
-
enhanced_facts = agentic.retrieve(
|
|
426
|
-
query=query, profile_id=pid,
|
|
427
|
-
retrieval_engine=self._retrieval_engine,
|
|
428
|
-
llm=self._llm,
|
|
429
|
-
top_k=limit,
|
|
430
|
-
query_type=response.query_type,
|
|
431
|
-
)
|
|
432
|
-
# Replace response results with enhanced facts if we got more
|
|
433
|
-
if len(enhanced_facts) > len(response.results):
|
|
434
|
-
from superlocalmemory.storage.models import RetrievalResult
|
|
435
|
-
enhanced_results = []
|
|
436
|
-
for i, f in enumerate(enhanced_facts):
|
|
437
|
-
# Look up real trust score for agentic results
|
|
438
|
-
fact_trust = 0.5
|
|
439
|
-
if self._trust_scorer:
|
|
440
|
-
try:
|
|
441
|
-
fact_trust = self._trust_scorer.get_fact_trust(
|
|
442
|
-
f.fact_id, pid,
|
|
443
|
-
)
|
|
444
|
-
except Exception:
|
|
445
|
-
pass
|
|
446
|
-
enhanced_results.append(RetrievalResult(
|
|
447
|
-
fact=f, score=1.0 / (i + 1),
|
|
448
|
-
channel_scores={"agentic": 1.0},
|
|
449
|
-
confidence=f.confidence,
|
|
450
|
-
evidence_chain=["agentic_round_2"],
|
|
451
|
-
trust_score=fact_trust,
|
|
452
|
-
))
|
|
453
|
-
response = RecallResponse(
|
|
454
|
-
query=query, mode=m, results=enhanced_results[:limit],
|
|
455
|
-
query_type=response.query_type,
|
|
456
|
-
channel_weights=response.channel_weights,
|
|
457
|
-
total_candidates=response.total_candidates + len(enhanced_facts),
|
|
458
|
-
retrieval_time_ms=response.retrieval_time_ms,
|
|
459
|
-
)
|
|
460
|
-
except Exception as exc:
|
|
461
|
-
logger.debug("Agentic sufficiency skipped: %s", exc)
|
|
462
|
-
|
|
463
|
-
# Adaptive re-ranking (V3.1 Active Memory)
|
|
464
|
-
# Phase 1 (< 50 signals): no change (cross-encoder order preserved)
|
|
465
|
-
# Phase 2 (50+): heuristic boosts (recency, access, trust)
|
|
466
|
-
# Phase 3 (200+): LightGBM ML ranking
|
|
467
|
-
try:
|
|
468
|
-
response = self._apply_adaptive_ranking(response, query, pid)
|
|
469
|
-
except Exception as exc:
|
|
470
|
-
logger.debug("Adaptive ranking skipped: %s", exc)
|
|
471
|
-
|
|
472
|
-
# Reconsolidation: access updates trust + count (neuroscience principle)
|
|
473
|
-
if self._trust_scorer:
|
|
474
|
-
for r in response.results:
|
|
475
|
-
self._trust_scorer.update_on_access("fact", r.fact.fact_id, pid)
|
|
476
|
-
|
|
477
|
-
# Fisher Bayesian update on recall
|
|
478
|
-
q_emb = self._embedder.embed(query) if self._embedder else None
|
|
479
|
-
q_var_arr = None
|
|
480
|
-
if self._embedder and q_emb:
|
|
481
|
-
_, q_var_list = self._embedder.compute_fisher_params(q_emb)
|
|
482
|
-
import numpy as _np
|
|
483
|
-
q_var_arr = _np.array(q_var_list, dtype=_np.float64)
|
|
484
|
-
|
|
485
|
-
for r in response.results:
|
|
486
|
-
updates: dict[str, object] = {
|
|
487
|
-
"access_count": r.fact.access_count + 1,
|
|
488
|
-
}
|
|
489
|
-
# Bayesian variance narrowing after 3+ accesses
|
|
490
|
-
if (q_var_arr is not None
|
|
491
|
-
and r.fact.fisher_variance
|
|
492
|
-
and len(r.fact.fisher_variance) == len(q_var_arr)
|
|
493
|
-
and r.fact.access_count >= 3):
|
|
494
|
-
import numpy as _np
|
|
495
|
-
f_var = _np.array(r.fact.fisher_variance, dtype=_np.float64)
|
|
496
|
-
# Conjugate Gaussian update: 1/new_var = 1/f_var + 1/q_var
|
|
497
|
-
new_var = 1.0 / (1.0 / _np.maximum(f_var, 0.05) + 1.0 / _np.maximum(q_var_arr, 0.05))
|
|
498
|
-
new_var = _np.clip(new_var, 0.05, 2.0)
|
|
499
|
-
updates["fisher_variance"] = new_var.tolist()
|
|
500
|
-
|
|
501
|
-
self._db.update_fact(r.fact.fact_id, updates)
|
|
502
|
-
|
|
503
|
-
# Post-operation hooks (audit, trust signal, learning)
|
|
504
|
-
hook_ctx["result_count"] = len(response.results)
|
|
505
|
-
hook_ctx["query_type"] = response.query_type
|
|
506
|
-
self._hooks.run_post("recall", hook_ctx)
|
|
507
|
-
|
|
508
|
-
return response
|
|
509
268
|
|
|
510
|
-
|
|
511
|
-
|
|
269
|
+
from superlocalmemory.core.recall_pipeline import run_recall
|
|
270
|
+
return run_recall(
|
|
271
|
+
query, pid, mode=mode, limit=limit, agent_id=agent_id,
|
|
272
|
+
config=self._config,
|
|
273
|
+
retrieval_engine=self._retrieval_engine,
|
|
274
|
+
trust_scorer=self._trust_scorer,
|
|
275
|
+
embedder=self._embedder,
|
|
276
|
+
db=self._db, llm=self._llm,
|
|
277
|
+
hooks=self._hooks,
|
|
278
|
+
access_log=self._access_log,
|
|
279
|
+
auto_linker=self._auto_linker,
|
|
280
|
+
)
|
|
512
281
|
|
|
513
|
-
|
|
514
|
-
and graph edges are all populated — even for auxiliary data.
|
|
515
|
-
Creates a parent memory record to satisfy FK constraint.
|
|
516
|
-
"""
|
|
517
|
-
self._ensure_init()
|
|
282
|
+
# -- Session operations -------------------------------------------------
|
|
518
283
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
record = MemoryRecord(
|
|
523
|
-
profile_id=self._profile_id,
|
|
524
|
-
content=fact.content[:500],
|
|
525
|
-
session_id=fact.session_id,
|
|
526
|
-
)
|
|
527
|
-
self._db.store_memory(record)
|
|
528
|
-
fact.memory_id = record.memory_id
|
|
529
|
-
|
|
530
|
-
if not fact.embedding and self._embedder:
|
|
531
|
-
fact.embedding = self._embedder.embed(fact.content)
|
|
532
|
-
if fact.embedding:
|
|
533
|
-
fact.fisher_mean, fact.fisher_variance = (
|
|
534
|
-
self._embedder.compute_fisher_params(fact.embedding)
|
|
535
|
-
)
|
|
536
|
-
if self._entity_resolver and fact.entities:
|
|
537
|
-
canonical = self._entity_resolver.resolve(
|
|
538
|
-
fact.entities, self._profile_id,
|
|
539
|
-
)
|
|
540
|
-
fact.canonical_entities = list(canonical.values())
|
|
541
|
-
self._db.store_fact(fact)
|
|
542
|
-
if fact.embedding and self._ann_index:
|
|
543
|
-
self._ann_index.add(fact.fact_id, fact.embedding)
|
|
544
|
-
if self._graph_builder:
|
|
545
|
-
self._graph_builder.build_edges(fact, self._profile_id)
|
|
546
|
-
# BM25 indexing
|
|
547
|
-
bm25 = getattr(self._retrieval_engine, '_bm25', None) if self._retrieval_engine else None
|
|
548
|
-
if bm25:
|
|
549
|
-
bm25.add(fact.fact_id, fact.content, self._profile_id)
|
|
550
|
-
return fact.fact_id
|
|
551
|
-
|
|
552
|
-
def create_speaker_entities(self, speaker_a: str, speaker_b: str) -> None:
|
|
284
|
+
def create_speaker_entities(
|
|
285
|
+
self, speaker_a: str, speaker_b: str,
|
|
286
|
+
) -> None:
|
|
553
287
|
"""Pre-create canonical entities for conversation speakers."""
|
|
554
288
|
self._ensure_init()
|
|
555
289
|
if self._entity_resolver:
|
|
@@ -558,48 +292,15 @@ class MemoryEngine:
|
|
|
558
292
|
)
|
|
559
293
|
|
|
560
294
|
def close_session(self, session_id: str) -> int:
|
|
561
|
-
"""Create session-level temporal summary
|
|
562
|
-
|
|
563
|
-
Aggregates facts from a completed session into temporal_events
|
|
564
|
-
with session scope. Enables temporal queries like "What happened
|
|
565
|
-
in session 3?"
|
|
566
|
-
|
|
567
|
-
Returns number of session summary events created.
|
|
568
|
-
"""
|
|
295
|
+
"""Create session-level temporal summary."""
|
|
569
296
|
self._ensure_init()
|
|
570
|
-
from superlocalmemory.storage.models import TemporalEvent
|
|
571
|
-
|
|
572
|
-
facts = self._db.get_all_facts(self._profile_id)
|
|
573
|
-
session_facts = [f for f in facts if f.session_id == session_id]
|
|
574
|
-
if not session_facts:
|
|
575
|
-
return 0
|
|
576
|
-
|
|
577
|
-
# Group by entity for session-level summaries
|
|
578
|
-
entity_facts: dict[str, list[AtomicFact]] = {}
|
|
579
|
-
for f in session_facts:
|
|
580
|
-
for eid in f.canonical_entities:
|
|
581
|
-
entity_facts.setdefault(eid, []).append(f)
|
|
582
|
-
|
|
583
|
-
count = 0
|
|
584
|
-
session_date = session_facts[0].observation_date or ""
|
|
585
|
-
for eid, efacts in entity_facts.items():
|
|
586
|
-
summary_parts = [f.content[:80] for f in efacts[:5]]
|
|
587
|
-
summary = f"Session {session_id}: " + "; ".join(summary_parts)
|
|
588
|
-
event = TemporalEvent(
|
|
589
|
-
profile_id=self._profile_id,
|
|
590
|
-
entity_id=eid,
|
|
591
|
-
fact_id=efacts[0].fact_id,
|
|
592
|
-
observation_date=session_date,
|
|
593
|
-
description=summary[:500],
|
|
594
|
-
)
|
|
595
|
-
self._db.store_temporal_event(event)
|
|
596
|
-
count += 1
|
|
597
297
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
session_id,
|
|
298
|
+
from superlocalmemory.core.store_pipeline import run_close_session
|
|
299
|
+
return run_close_session(
|
|
300
|
+
session_id, self._profile_id, db=self._db,
|
|
601
301
|
)
|
|
602
|
-
|
|
302
|
+
|
|
303
|
+
# -- Lifecycle ----------------------------------------------------------
|
|
603
304
|
|
|
604
305
|
def close(self) -> None:
|
|
605
306
|
self._initialized = False
|
|
@@ -617,219 +318,8 @@ class MemoryEngine:
|
|
|
617
318
|
self._ensure_init()
|
|
618
319
|
return self._db.get_fact_count(self._profile_id)
|
|
619
320
|
|
|
620
|
-
# -- Internal
|
|
321
|
+
# -- Internal -----------------------------------------------------------
|
|
621
322
|
|
|
622
323
|
def _ensure_init(self) -> None:
|
|
623
324
|
if not self._initialized:
|
|
624
325
|
self.initialize()
|
|
625
|
-
|
|
626
|
-
def _apply_adaptive_ranking(self, response, query: str, pid: str):
|
|
627
|
-
"""Apply adaptive re-ranking if enough learning signals exist.
|
|
628
|
-
|
|
629
|
-
Phase 1 (< 50 signals): returns response unchanged (backward compat).
|
|
630
|
-
Phase 2 (50+): heuristic boosts from recency, access count, trust.
|
|
631
|
-
Phase 3 (200+): LightGBM ML-based reranking.
|
|
632
|
-
"""
|
|
633
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
634
|
-
from pathlib import Path
|
|
635
|
-
|
|
636
|
-
learning_db = Path.home() / ".superlocalmemory" / "learning.db"
|
|
637
|
-
if not learning_db.exists():
|
|
638
|
-
return response
|
|
639
|
-
|
|
640
|
-
collector = FeedbackCollector(learning_db)
|
|
641
|
-
signal_count = collector.get_feedback_count(pid)
|
|
642
|
-
|
|
643
|
-
if signal_count < 50:
|
|
644
|
-
return response # Phase 1: no change
|
|
645
|
-
|
|
646
|
-
from superlocalmemory.learning.ranker import AdaptiveRanker
|
|
647
|
-
ranker = AdaptiveRanker(signal_count=signal_count)
|
|
648
|
-
|
|
649
|
-
result_dicts = []
|
|
650
|
-
for r in response.results:
|
|
651
|
-
result_dicts.append({
|
|
652
|
-
"score": r.score,
|
|
653
|
-
"cross_encoder_score": r.score,
|
|
654
|
-
"trust_score": r.trust_score,
|
|
655
|
-
"channel_scores": r.channel_scores or {},
|
|
656
|
-
"fact": {
|
|
657
|
-
"age_days": 0,
|
|
658
|
-
"access_count": r.fact.access_count,
|
|
659
|
-
},
|
|
660
|
-
"_original": r,
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
query_context = {"query_type": response.query_type}
|
|
664
|
-
reranked = ranker.rerank(result_dicts, query_context)
|
|
665
|
-
|
|
666
|
-
# Rebuild response with new ordering
|
|
667
|
-
new_results = [d["_original"] for d in reranked]
|
|
668
|
-
|
|
669
|
-
from superlocalmemory.storage.models import RecallResponse
|
|
670
|
-
return RecallResponse(
|
|
671
|
-
query=response.query,
|
|
672
|
-
mode=response.mode,
|
|
673
|
-
results=new_results,
|
|
674
|
-
query_type=response.query_type,
|
|
675
|
-
channel_weights=response.channel_weights,
|
|
676
|
-
total_candidates=response.total_candidates,
|
|
677
|
-
retrieval_time_ms=response.retrieval_time_ms,
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
def _init_encoding(self) -> None:
|
|
681
|
-
from superlocalmemory.encoding.fact_extractor import FactExtractor
|
|
682
|
-
from superlocalmemory.encoding.entity_resolver import EntityResolver
|
|
683
|
-
from superlocalmemory.encoding.temporal_parser import TemporalParser
|
|
684
|
-
from superlocalmemory.encoding.type_router import TypeRouter
|
|
685
|
-
from superlocalmemory.encoding.graph_builder import GraphBuilder
|
|
686
|
-
from superlocalmemory.encoding.consolidator import MemoryConsolidator
|
|
687
|
-
from superlocalmemory.encoding.observation_builder import ObservationBuilder
|
|
688
|
-
from superlocalmemory.encoding.scene_builder import SceneBuilder
|
|
689
|
-
from superlocalmemory.encoding.entropy_gate import EntropyGate
|
|
690
|
-
from superlocalmemory.retrieval.ann_index import ANNIndex
|
|
691
|
-
|
|
692
|
-
self._ann_index = ANNIndex(dimension=self._config.embedding.dimension)
|
|
693
|
-
self._fact_extractor = FactExtractor(
|
|
694
|
-
config=self._config.encoding, llm=self._llm,
|
|
695
|
-
embedder=self._embedder, mode=self._config.mode,
|
|
696
|
-
)
|
|
697
|
-
self._entity_resolver = EntityResolver(self._db, self._llm)
|
|
698
|
-
self._temporal_parser = TemporalParser()
|
|
699
|
-
self._type_router = TypeRouter(
|
|
700
|
-
mode=self._config.mode, embedder=self._embedder, llm=self._llm,
|
|
701
|
-
)
|
|
702
|
-
self._graph_builder = GraphBuilder(self._db, self._ann_index)
|
|
703
|
-
self._consolidator = MemoryConsolidator(
|
|
704
|
-
self._db, self._embedder, self._llm, self._config.encoding,
|
|
705
|
-
)
|
|
706
|
-
self._observation_builder = ObservationBuilder(self._db)
|
|
707
|
-
self._scene_builder = SceneBuilder(self._db, self._embedder)
|
|
708
|
-
self._entropy_gate = EntropyGate(
|
|
709
|
-
self._embedder, self._config.encoding.entropy_threshold,
|
|
710
|
-
)
|
|
711
|
-
|
|
712
|
-
# Wire Sheaf consistency checker
|
|
713
|
-
if self._config.math.sheaf_at_encoding:
|
|
714
|
-
from superlocalmemory.math.sheaf import SheafConsistencyChecker
|
|
715
|
-
self._sheaf_checker = SheafConsistencyChecker(
|
|
716
|
-
self._db, self._config.math.sheaf_contradiction_threshold,
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
def _init_retrieval(self) -> None:
|
|
720
|
-
from superlocalmemory.retrieval.engine import RetrievalEngine
|
|
721
|
-
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
722
|
-
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
723
|
-
from superlocalmemory.retrieval.entity_channel import EntityGraphChannel
|
|
724
|
-
from superlocalmemory.retrieval.temporal_channel import TemporalChannel
|
|
725
|
-
from superlocalmemory.retrieval.reranker import CrossEncoderReranker
|
|
726
|
-
from superlocalmemory.retrieval.profile_channel import ProfileChannel
|
|
727
|
-
from superlocalmemory.retrieval.bridge_discovery import BridgeDiscovery
|
|
728
|
-
|
|
729
|
-
channels: dict = {
|
|
730
|
-
"semantic": SemanticChannel(
|
|
731
|
-
self._db,
|
|
732
|
-
fisher_temperature=self._config.math.fisher_temperature,
|
|
733
|
-
embedder=self._embedder,
|
|
734
|
-
fisher_mode=self._config.math.fisher_mode,
|
|
735
|
-
),
|
|
736
|
-
"bm25": BM25Channel(self._db),
|
|
737
|
-
"entity_graph": EntityGraphChannel(self._db, self._entity_resolver),
|
|
738
|
-
"temporal": TemporalChannel(self._db),
|
|
739
|
-
}
|
|
740
|
-
reranker = None
|
|
741
|
-
if self._config.retrieval.use_cross_encoder:
|
|
742
|
-
reranker = CrossEncoderReranker(self._config.retrieval.cross_encoder_model)
|
|
743
|
-
|
|
744
|
-
profile_ch = ProfileChannel(self._db)
|
|
745
|
-
bridge = BridgeDiscovery(self._db)
|
|
746
|
-
|
|
747
|
-
self._retrieval_engine = RetrievalEngine(
|
|
748
|
-
db=self._db, config=self._config.retrieval, channels=channels,
|
|
749
|
-
embedder=self._embedder, reranker=reranker,
|
|
750
|
-
base_weights=self._config.channel_weights,
|
|
751
|
-
profile_channel=profile_ch,
|
|
752
|
-
bridge_discovery=bridge,
|
|
753
|
-
trust_scorer=self._trust_scorer,
|
|
754
|
-
)
|
|
755
|
-
|
|
756
|
-
def _wire_hooks(self) -> None:
|
|
757
|
-
"""Wire trust, compliance, and event bus hooks into engine lifecycle."""
|
|
758
|
-
# -- Pre-store hooks (synchronous, can reject) --
|
|
759
|
-
if self._trust_scorer:
|
|
760
|
-
from superlocalmemory.trust.gate import TrustGate
|
|
761
|
-
gate = TrustGate(self._trust_scorer)
|
|
762
|
-
self._hooks.register_pre("store", lambda ctx: gate.check_write(
|
|
763
|
-
ctx.get("agent_id", "unknown"), ctx.get("profile_id", self._profile_id)))
|
|
764
|
-
self._hooks.register_pre("delete", lambda ctx: gate.check_delete(
|
|
765
|
-
ctx.get("agent_id", "unknown"), ctx.get("profile_id", self._profile_id)))
|
|
766
|
-
|
|
767
|
-
# -- Post-store hooks (async, never block) --
|
|
768
|
-
if self._trust_scorer:
|
|
769
|
-
self._hooks.register_post("store", lambda ctx: self._trust_scorer.record_signal(
|
|
770
|
-
ctx.get("agent_id", "unknown"), ctx.get("profile_id", self._profile_id), "store_success"))
|
|
771
|
-
self._hooks.register_post("recall", lambda ctx: self._trust_scorer.record_signal(
|
|
772
|
-
ctx.get("agent_id", "unknown"), ctx.get("profile_id", self._profile_id), "recall_hit"))
|
|
773
|
-
|
|
774
|
-
# -- Burst detection via SignalRecorder --
|
|
775
|
-
try:
|
|
776
|
-
from superlocalmemory.trust.signals import SignalRecorder
|
|
777
|
-
self._signal_recorder = SignalRecorder(self._db)
|
|
778
|
-
self._hooks.register_post("store", lambda ctx: self._signal_recorder.record(
|
|
779
|
-
ctx.get("agent_id", "unknown"), ctx.get("profile_id", self._profile_id), "store_success"))
|
|
780
|
-
except Exception:
|
|
781
|
-
self._signal_recorder = None
|
|
782
|
-
|
|
783
|
-
# -- Tamper-proof audit chain (all operations logged with hash chain) --
|
|
784
|
-
try:
|
|
785
|
-
from superlocalmemory.compliance.audit import AuditChain
|
|
786
|
-
audit_path = self._config.db_path.parent / "audit_chain.db"
|
|
787
|
-
self._audit_chain = AuditChain(audit_path)
|
|
788
|
-
for op in ("store", "recall", "delete"):
|
|
789
|
-
self._hooks.register_post(op, lambda ctx, _op=op: self._audit_chain.log(
|
|
790
|
-
operation=_op,
|
|
791
|
-
agent_id=ctx.get("agent_id", "unknown"),
|
|
792
|
-
profile_id=ctx.get("profile_id", self._profile_id),
|
|
793
|
-
content_hash=ctx.get("content_hash", ""),
|
|
794
|
-
))
|
|
795
|
-
except Exception:
|
|
796
|
-
self._audit_chain = None
|
|
797
|
-
|
|
798
|
-
def _enrich_fact(self, fact: AtomicFact, record: MemoryRecord) -> AtomicFact:
|
|
799
|
-
"""Enrich fact with embeddings, entities, temporal, emotional data."""
|
|
800
|
-
from superlocalmemory.encoding.emotional import tag_emotion, emotional_importance_boost
|
|
801
|
-
from superlocalmemory.encoding.signal_inference import infer_signal
|
|
802
|
-
|
|
803
|
-
embedding = self._embedder.embed(fact.content) if self._embedder else None
|
|
804
|
-
fisher_mean, fisher_variance = (None, None)
|
|
805
|
-
if self._embedder and embedding:
|
|
806
|
-
fisher_mean, fisher_variance = self._embedder.compute_fisher_params(embedding)
|
|
807
|
-
|
|
808
|
-
canonical = {}
|
|
809
|
-
if self._entity_resolver and fact.entities:
|
|
810
|
-
canonical = self._entity_resolver.resolve(fact.entities, self._profile_id)
|
|
811
|
-
|
|
812
|
-
temporal = {}
|
|
813
|
-
if self._temporal_parser:
|
|
814
|
-
temporal = self._temporal_parser.extract_dates_from_text(fact.content)
|
|
815
|
-
|
|
816
|
-
emotion = tag_emotion(fact.content)
|
|
817
|
-
signal = infer_signal(fact.content)
|
|
818
|
-
|
|
819
|
-
return AtomicFact(
|
|
820
|
-
fact_id=fact.fact_id, memory_id=record.memory_id,
|
|
821
|
-
profile_id=self._profile_id, content=fact.content,
|
|
822
|
-
fact_type=fact.fact_type, entities=fact.entities,
|
|
823
|
-
canonical_entities=list(canonical.values()),
|
|
824
|
-
observation_date=fact.observation_date or record.session_date,
|
|
825
|
-
referenced_date=fact.referenced_date or temporal.get("referenced_date"),
|
|
826
|
-
interval_start=fact.interval_start or temporal.get("interval_start"),
|
|
827
|
-
interval_end=fact.interval_end or temporal.get("interval_end"),
|
|
828
|
-
confidence=fact.confidence,
|
|
829
|
-
importance=min(1.0, fact.importance + emotional_importance_boost(emotion)),
|
|
830
|
-
evidence_count=fact.evidence_count,
|
|
831
|
-
source_turn_ids=fact.source_turn_ids, session_id=record.session_id,
|
|
832
|
-
embedding=embedding, fisher_mean=fisher_mean, fisher_variance=fisher_variance,
|
|
833
|
-
emotional_valence=emotion.valence, emotional_arousal=emotion.arousal,
|
|
834
|
-
signal_type=signal, created_at=fact.created_at,
|
|
835
|
-
)
|