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
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import logging
|
|
22
22
|
import math
|
|
23
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
|
|
@@ -71,6 +71,8 @@ class SemanticChannel:
|
|
|
71
71
|
fresh facts (low access_count) use cosine, frequently-accessed facts
|
|
72
72
|
transition to Fisher-Rao distance for uncertainty-aware similarity.
|
|
73
73
|
|
|
74
|
+
V3.2: VectorStore KNN fast path when available, falls back to full scan.
|
|
75
|
+
|
|
74
76
|
Graduated ramp: weight = min(1.2, access_count / 10 * 1.2)
|
|
75
77
|
Final sim = fisher_weight * fisher_sim + (1 - fisher_weight) * cosine_sim
|
|
76
78
|
"""
|
|
@@ -81,6 +83,7 @@ class SemanticChannel:
|
|
|
81
83
|
fisher_temperature: float = 15.0,
|
|
82
84
|
embedder: object | None = None,
|
|
83
85
|
fisher_mode: str = "simplified",
|
|
86
|
+
vector_store: Any | None = None,
|
|
84
87
|
) -> None:
|
|
85
88
|
self._db = db
|
|
86
89
|
self._temperature = fisher_temperature
|
|
@@ -88,6 +91,7 @@ class SemanticChannel:
|
|
|
88
91
|
self._fisher_mode = fisher_mode if fisher_mode in ("simplified", "full") else "simplified"
|
|
89
92
|
# Lazily instantiated full metric (avoids import cost when not needed)
|
|
90
93
|
self._full_metric: object | None = None
|
|
94
|
+
self._vector_store = vector_store
|
|
91
95
|
|
|
92
96
|
def search(
|
|
93
97
|
self,
|
|
@@ -97,8 +101,8 @@ class SemanticChannel:
|
|
|
97
101
|
) -> list[tuple[str, float]]:
|
|
98
102
|
"""Search for semantically similar facts.
|
|
99
103
|
|
|
100
|
-
Uses
|
|
101
|
-
|
|
104
|
+
Uses VectorStore KNN if available, otherwise full-table scan.
|
|
105
|
+
Fisher-Rao scoring preserved as post-KNN secondary signal.
|
|
102
106
|
|
|
103
107
|
Args:
|
|
104
108
|
query_embedding: Dense vector for the query.
|
|
@@ -114,6 +118,86 @@ class SemanticChannel:
|
|
|
114
118
|
|
|
115
119
|
q_vec = np.array(query_embedding, dtype=np.float32)
|
|
116
120
|
|
|
121
|
+
# --- FAST PATH: sqlite-vec KNN ---
|
|
122
|
+
if self._vector_store and self._vector_store.available:
|
|
123
|
+
results = self._search_via_vector_store(
|
|
124
|
+
query_embedding, q_vec, profile_id, top_k,
|
|
125
|
+
)
|
|
126
|
+
if results: # If vec0 returned results, use them
|
|
127
|
+
return results
|
|
128
|
+
# If vec0 is empty (cold start), fall through to full scan
|
|
129
|
+
|
|
130
|
+
# --- FALLBACK: full-table scan (original code, unchanged) ---
|
|
131
|
+
return self._search_full_scan(query_embedding, q_vec, profile_id, top_k)
|
|
132
|
+
|
|
133
|
+
def _search_via_vector_store(
|
|
134
|
+
self,
|
|
135
|
+
query_embedding: list[float],
|
|
136
|
+
q_vec: np.ndarray,
|
|
137
|
+
profile_id: str,
|
|
138
|
+
top_k: int,
|
|
139
|
+
) -> list[tuple[str, float]]:
|
|
140
|
+
"""KNN via VectorStore, then Fisher-Rao re-scoring on top-K subset."""
|
|
141
|
+
# Step 1: Fast KNN -- get 2x top_k candidates for Fisher re-ranking
|
|
142
|
+
knn_results = self._vector_store.search(
|
|
143
|
+
query_embedding, top_k=top_k * 2, profile_id=profile_id,
|
|
144
|
+
)
|
|
145
|
+
if not knn_results:
|
|
146
|
+
return [] # Caller falls through to full scan
|
|
147
|
+
|
|
148
|
+
# Step 2: Load only the candidate facts (NOT all facts)
|
|
149
|
+
candidate_ids = [fid for fid, _ in knn_results]
|
|
150
|
+
knn_scores = {fid: score for fid, score in knn_results}
|
|
151
|
+
facts = self._db.get_facts_by_ids(candidate_ids, profile_id)
|
|
152
|
+
|
|
153
|
+
if not facts:
|
|
154
|
+
return [(fid, score) for fid, score in knn_results[:top_k]]
|
|
155
|
+
|
|
156
|
+
# Step 3: Fisher-Rao re-scoring on the subset
|
|
157
|
+
q_mean: np.ndarray | None = None
|
|
158
|
+
q_var: np.ndarray | None = None
|
|
159
|
+
if self._embedder and hasattr(self._embedder, 'compute_fisher_params'):
|
|
160
|
+
qm, qv = self._embedder.compute_fisher_params(query_embedding)
|
|
161
|
+
q_mean = np.array(qm, dtype=np.float32)
|
|
162
|
+
q_var = np.array(qv, dtype=np.float32)
|
|
163
|
+
|
|
164
|
+
scored: list[tuple[str, float]] = []
|
|
165
|
+
for fact in facts:
|
|
166
|
+
cos_sim = knn_scores.get(fact.fact_id, 0.0)
|
|
167
|
+
|
|
168
|
+
# Graduated Fisher-Rao ramp (preserved from original)
|
|
169
|
+
fisher_weight = min(1.2, (fact.access_count or 0) / 10.0 * 1.2)
|
|
170
|
+
|
|
171
|
+
if (fisher_weight > 0.01
|
|
172
|
+
and fact.fisher_variance is not None
|
|
173
|
+
and fact.embedding is not None
|
|
174
|
+
and len(fact.fisher_variance) == len(q_vec)):
|
|
175
|
+
f_vec = np.array(fact.embedding, dtype=np.float32)
|
|
176
|
+
var_vec = np.array(fact.fisher_variance, dtype=np.float32)
|
|
177
|
+
f_sim = self._compute_fisher_sim(
|
|
178
|
+
q_vec, f_vec, var_vec, fact, q_mean, q_var,
|
|
179
|
+
)
|
|
180
|
+
capped_w = min(1.0, fisher_weight)
|
|
181
|
+
sim = capped_w * f_sim + (1.0 - capped_w) * cos_sim
|
|
182
|
+
else:
|
|
183
|
+
sim = cos_sim
|
|
184
|
+
|
|
185
|
+
if sim > 0.3:
|
|
186
|
+
scored.append((fact.fact_id, sim))
|
|
187
|
+
|
|
188
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
189
|
+
return scored[:top_k]
|
|
190
|
+
|
|
191
|
+
def _search_full_scan(
|
|
192
|
+
self,
|
|
193
|
+
query_embedding: list[float],
|
|
194
|
+
q_vec: np.ndarray,
|
|
195
|
+
profile_id: str,
|
|
196
|
+
top_k: int,
|
|
197
|
+
) -> list[tuple[str, float]]:
|
|
198
|
+
"""Original full-table-scan search. Used as fallback when VectorStore
|
|
199
|
+
is unavailable or empty (cold start).
|
|
200
|
+
"""
|
|
117
201
|
# Compute query Fisher params for Bayesian comparison (F45 fix)
|
|
118
202
|
q_mean: np.ndarray | None = None
|
|
119
203
|
q_var: np.ndarray | None = None
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3
|
|
4
|
+
|
|
5
|
+
"""SYNAPSE spreading activation -- 5th retrieval channel.
|
|
6
|
+
|
|
7
|
+
SYNAPSE (arXiv 2601.02744) 5-step algorithm adapted for SLM.
|
|
8
|
+
Pure math -- no LLM calls at query time. With M=7, T=3 the
|
|
9
|
+
computation is ~21 neighbor lookups (<5ms on SQLite with indexes).
|
|
10
|
+
|
|
11
|
+
Reads BOTH graph_edges + association_edges via UNION query (Rule 13).
|
|
12
|
+
Registered as 5th channel via ChannelRegistry (needs_embedding=True).
|
|
13
|
+
|
|
14
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
15
|
+
License: MIT
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import logging
|
|
22
|
+
import math
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from superlocalmemory.storage.models import _new_id
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Configuration (frozen dataclass, Rule 10)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SpreadingActivationConfig:
|
|
39
|
+
"""Configuration for SYNAPSE spreading activation.
|
|
40
|
+
|
|
41
|
+
All hyperparameters from the SYNAPSE paper (arXiv 2601.02744).
|
|
42
|
+
SYNAPSE tuned on 384d (all-MiniLM-L6-v2). SLM uses 768d
|
|
43
|
+
(nomic-embed-text). Phase 3 calibration test verifies convergence.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
alpha: float = 1.0 # Seed scaling factor
|
|
47
|
+
delta: float = 0.5 # Node retention / self-decay per iteration
|
|
48
|
+
spreading_factor: float = 0.8 # S: energy diffusion rate
|
|
49
|
+
theta: float = 0.5 # Activation threshold for sigmoid
|
|
50
|
+
top_m: int = 7 # Lateral inhibition: max active nodes
|
|
51
|
+
max_iterations: int = 3 # T: propagation depth
|
|
52
|
+
tau_gate: float = 0.12 # FOK confidence gate
|
|
53
|
+
enabled: bool = False # Feature flag (Rule 12)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# SpreadingActivation Channel
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class SpreadingActivation:
|
|
61
|
+
"""SYNAPSE 5-step spreading activation as 5th retrieval channel.
|
|
62
|
+
|
|
63
|
+
Algorithm:
|
|
64
|
+
Step 1: Initialization with ALPHA seed scaling
|
|
65
|
+
Step 2: Propagation with fan effect (out-degree normalization)
|
|
66
|
+
Step 3: Lateral inhibition (top-M=7 pruning)
|
|
67
|
+
Step 4: Nonlinear sigmoid gating
|
|
68
|
+
Step 5: Iterate T=3 times, then FOK gate
|
|
69
|
+
|
|
70
|
+
Registered as 5th channel via ChannelRegistry (Rule 07).
|
|
71
|
+
Reads BOTH graph_edges + association_edges via UNION query (Rule 13).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
db: Any,
|
|
77
|
+
vector_store: Any,
|
|
78
|
+
config: SpreadingActivationConfig | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
self._db = db
|
|
81
|
+
self._vector_store = vector_store
|
|
82
|
+
self._config = config or SpreadingActivationConfig()
|
|
83
|
+
|
|
84
|
+
def search(
|
|
85
|
+
self,
|
|
86
|
+
query: Any,
|
|
87
|
+
profile_id: str = "",
|
|
88
|
+
top_k: int = 7,
|
|
89
|
+
) -> list[tuple[str, float]]:
|
|
90
|
+
"""Channel-compatible interface: (query, top_k) -> [(fact_id, score)].
|
|
91
|
+
|
|
92
|
+
Matches ANNSearchable protocol (Rule 07).
|
|
93
|
+
"""
|
|
94
|
+
if not self._config.enabled:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Step 0: Get seed nodes from VectorStore KNN
|
|
99
|
+
seed_results = self._vector_store.search(
|
|
100
|
+
query, top_k=self._config.top_m,
|
|
101
|
+
)
|
|
102
|
+
if not seed_results:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
# Check cache first
|
|
106
|
+
query_hash = self._compute_query_hash(query, profile_id)
|
|
107
|
+
cached = self._get_cached_results(query_hash, profile_id)
|
|
108
|
+
if cached:
|
|
109
|
+
return cached[:top_k]
|
|
110
|
+
|
|
111
|
+
# Run 5-step spreading activation
|
|
112
|
+
activations = self._propagate(seed_results, profile_id)
|
|
113
|
+
|
|
114
|
+
# FOK gating
|
|
115
|
+
if not self._fok_check(activations):
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
# Cache results
|
|
119
|
+
self._cache_results(query_hash, profile_id, activations)
|
|
120
|
+
|
|
121
|
+
# Return top-K sorted by activation
|
|
122
|
+
results = sorted(
|
|
123
|
+
activations.items(), key=lambda x: x[1], reverse=True,
|
|
124
|
+
)
|
|
125
|
+
return results[:top_k]
|
|
126
|
+
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.debug(
|
|
129
|
+
"SpreadingActivation.search failed for profile %s: %s",
|
|
130
|
+
profile_id, exc,
|
|
131
|
+
)
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
def _propagate(
|
|
135
|
+
self,
|
|
136
|
+
seeds: list[tuple[str, float]],
|
|
137
|
+
profile_id: str,
|
|
138
|
+
) -> dict[str, float]:
|
|
139
|
+
"""Execute the 5-step SYNAPSE algorithm.
|
|
140
|
+
|
|
141
|
+
Step 1: a_i^(0) = alpha * sim(h_i, h_q) for seeds, 0 otherwise
|
|
142
|
+
Step 2: u_i^(t+1) = delta * a_i^(t) + S * SUM(w_ji/deg(j) * a_j^(t))
|
|
143
|
+
Step 3: Lateral inhibition -- keep top-M=7 only
|
|
144
|
+
Step 4: sigmoid(u - theta)
|
|
145
|
+
Step 5: Iterate T=3 times
|
|
146
|
+
"""
|
|
147
|
+
cfg = self._config
|
|
148
|
+
|
|
149
|
+
# Step 1: Initialization
|
|
150
|
+
activations: dict[str, float] = {}
|
|
151
|
+
for fact_id, similarity in seeds:
|
|
152
|
+
activations[fact_id] = cfg.alpha * similarity
|
|
153
|
+
|
|
154
|
+
# Precompute out-degrees for fan effect
|
|
155
|
+
degree_cache: dict[str, int] = {}
|
|
156
|
+
|
|
157
|
+
# Steps 2-4, repeated T times
|
|
158
|
+
for _iteration in range(cfg.max_iterations):
|
|
159
|
+
new_activations: dict[str, float] = {}
|
|
160
|
+
|
|
161
|
+
for node_id, activation in activations.items():
|
|
162
|
+
if activation < 0.001:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Get neighbors from BOTH tables (Rule 13)
|
|
166
|
+
neighbors = self._get_unified_neighbors(node_id, profile_id)
|
|
167
|
+
|
|
168
|
+
# Out-degree for fan effect normalization
|
|
169
|
+
if node_id not in degree_cache:
|
|
170
|
+
degree_cache[node_id] = max(len(neighbors), 1)
|
|
171
|
+
out_degree = degree_cache[node_id]
|
|
172
|
+
|
|
173
|
+
# Step 2: Propagation with fan effect
|
|
174
|
+
for neighbor_id, edge_weight in neighbors:
|
|
175
|
+
spread = (
|
|
176
|
+
cfg.spreading_factor
|
|
177
|
+
* (edge_weight / out_degree)
|
|
178
|
+
* activation
|
|
179
|
+
)
|
|
180
|
+
new_activations[neighbor_id] = (
|
|
181
|
+
new_activations.get(neighbor_id, 0.0) + spread
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Add self-retention (delta * current activation)
|
|
185
|
+
for node_id, activation in activations.items():
|
|
186
|
+
new_activations[node_id] = (
|
|
187
|
+
new_activations.get(node_id, 0.0) + cfg.delta * activation
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Step 3: Lateral inhibition -- keep only top-M
|
|
191
|
+
sorted_nodes = sorted(
|
|
192
|
+
new_activations.items(), key=lambda x: x[1], reverse=True,
|
|
193
|
+
)
|
|
194
|
+
top_m_nodes = sorted_nodes[: cfg.top_m]
|
|
195
|
+
|
|
196
|
+
# Step 4: Nonlinear activation (sigmoid with threshold shift)
|
|
197
|
+
activations = {}
|
|
198
|
+
for node_id, raw_activation in top_m_nodes:
|
|
199
|
+
gated = 1.0 / (1.0 + math.exp(-(raw_activation - cfg.theta)))
|
|
200
|
+
activations[node_id] = gated
|
|
201
|
+
|
|
202
|
+
return activations
|
|
203
|
+
|
|
204
|
+
def _get_unified_neighbors(
|
|
205
|
+
self, node_id: str, profile_id: str,
|
|
206
|
+
) -> list[tuple[str, float]]:
|
|
207
|
+
"""Get neighbors from BOTH graph_edges and association_edges.
|
|
208
|
+
|
|
209
|
+
Uses bidirectional UNION query (Section 4 of LLD).
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
rows = self._db.execute(
|
|
213
|
+
"""
|
|
214
|
+
SELECT target_id AS neighbor_id, weight FROM graph_edges
|
|
215
|
+
WHERE source_id = ? AND profile_id = ?
|
|
216
|
+
UNION ALL
|
|
217
|
+
SELECT target_fact_id AS neighbor_id, weight FROM association_edges
|
|
218
|
+
WHERE source_fact_id = ? AND profile_id = ?
|
|
219
|
+
UNION ALL
|
|
220
|
+
SELECT source_id AS neighbor_id, weight FROM graph_edges
|
|
221
|
+
WHERE target_id = ? AND profile_id = ?
|
|
222
|
+
UNION ALL
|
|
223
|
+
SELECT source_fact_id AS neighbor_id, weight FROM association_edges
|
|
224
|
+
WHERE target_fact_id = ? AND profile_id = ?
|
|
225
|
+
""",
|
|
226
|
+
(node_id, profile_id, node_id, profile_id,
|
|
227
|
+
node_id, profile_id, node_id, profile_id),
|
|
228
|
+
)
|
|
229
|
+
return [
|
|
230
|
+
(dict(r)["neighbor_id"], dict(r)["weight"]) for r in rows
|
|
231
|
+
]
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
logger.debug(
|
|
234
|
+
"SpreadingActivation: UNION query failed for node %s "
|
|
235
|
+
"profile %s: %s",
|
|
236
|
+
node_id, profile_id, exc,
|
|
237
|
+
)
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
def _fok_check(self, activations: dict[str, float]) -> bool:
|
|
241
|
+
"""Feeling-of-Knowing gate.
|
|
242
|
+
|
|
243
|
+
If max activation < tau_gate (0.12), reject results as noise.
|
|
244
|
+
"""
|
|
245
|
+
if not activations:
|
|
246
|
+
return False
|
|
247
|
+
return max(activations.values()) >= self._config.tau_gate
|
|
248
|
+
|
|
249
|
+
def _compute_query_hash(self, query: Any, profile_id: str) -> str:
|
|
250
|
+
"""Deterministic hash for cache key."""
|
|
251
|
+
if isinstance(query, np.ndarray):
|
|
252
|
+
data = query.tobytes() + profile_id.encode()
|
|
253
|
+
elif isinstance(query, list):
|
|
254
|
+
data = np.array(query, dtype=np.float32).tobytes() + profile_id.encode()
|
|
255
|
+
else:
|
|
256
|
+
data = str(query).encode() + profile_id.encode()
|
|
257
|
+
return hashlib.sha256(data).hexdigest()[:16]
|
|
258
|
+
|
|
259
|
+
def _get_cached_results(
|
|
260
|
+
self, query_hash: str, profile_id: str,
|
|
261
|
+
) -> list[tuple[str, float]] | None:
|
|
262
|
+
"""Check activation_cache for recent results."""
|
|
263
|
+
try:
|
|
264
|
+
rows = self._db.execute(
|
|
265
|
+
"SELECT node_id, activation_value FROM activation_cache "
|
|
266
|
+
"WHERE profile_id = ? AND query_hash = ? "
|
|
267
|
+
"AND expires_at > datetime('now') "
|
|
268
|
+
"ORDER BY activation_value DESC",
|
|
269
|
+
(profile_id, query_hash),
|
|
270
|
+
)
|
|
271
|
+
if not rows:
|
|
272
|
+
return None
|
|
273
|
+
return [
|
|
274
|
+
(dict(r)["node_id"], dict(r)["activation_value"])
|
|
275
|
+
for r in rows
|
|
276
|
+
]
|
|
277
|
+
except Exception:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _cache_results(
|
|
281
|
+
self,
|
|
282
|
+
query_hash: str,
|
|
283
|
+
profile_id: str,
|
|
284
|
+
activations: dict[str, float],
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Store results in activation_cache with 1-hour TTL."""
|
|
287
|
+
try:
|
|
288
|
+
for node_id, value in activations.items():
|
|
289
|
+
self._db.execute(
|
|
290
|
+
"INSERT OR REPLACE INTO activation_cache "
|
|
291
|
+
"(cache_id, profile_id, query_hash, node_id, "
|
|
292
|
+
" activation_value, iteration, created_at, expires_at) "
|
|
293
|
+
"VALUES (?, ?, ?, ?, ?, ?, datetime('now'), "
|
|
294
|
+
"datetime('now', '+1 hour'))",
|
|
295
|
+
(_new_id(), profile_id, query_hash, node_id, value,
|
|
296
|
+
self._config.max_iterations),
|
|
297
|
+
)
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
logger.debug("Cache write failed: %s", exc)
|
|
300
|
+
|
|
301
|
+
def cleanup_expired_cache(self) -> int:
|
|
302
|
+
"""Delete expired cache entries. Called by maintenance."""
|
|
303
|
+
try:
|
|
304
|
+
result = self._db.execute(
|
|
305
|
+
"DELETE FROM activation_cache "
|
|
306
|
+
"WHERE expires_at < datetime('now')",
|
|
307
|
+
(),
|
|
308
|
+
)
|
|
309
|
+
return len(result) if result else 0
|
|
310
|
+
except Exception:
|
|
311
|
+
return 0
|
|
@@ -16,12 +16,12 @@ import re
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
|
|
18
18
|
STRATEGY_PRESETS: dict[str, dict[str, float]] = {
|
|
19
|
-
"temporal": {"semantic": 0.8, "bm25": 1.5, "entity_graph": 0.8, "temporal": 2.0},
|
|
20
|
-
"multi_hop": {"semantic": 1.0, "bm25": 0.8, "entity_graph": 2.0, "temporal": 0.5},
|
|
21
|
-
"aggregation": {"semantic": 1.2, "bm25": 1.5, "entity_graph": 1.0, "temporal": 0.5},
|
|
22
|
-
"opinion": {"semantic": 1.8, "bm25": 0.6, "entity_graph": 0.8, "temporal": 0.3},
|
|
23
|
-
"factual": {"semantic": 1.2, "bm25": 1.4, "entity_graph": 1.0, "temporal": 0.6},
|
|
24
|
-
"entity": {"semantic": 1.0, "bm25": 1.5, "entity_graph": 1.2, "temporal": 0.5},
|
|
19
|
+
"temporal": {"semantic": 0.8, "bm25": 1.5, "entity_graph": 0.8, "temporal": 2.0, "spreading_activation": 0.5},
|
|
20
|
+
"multi_hop": {"semantic": 1.0, "bm25": 0.8, "entity_graph": 2.0, "temporal": 0.5, "spreading_activation": 2.0},
|
|
21
|
+
"aggregation": {"semantic": 1.2, "bm25": 1.5, "entity_graph": 1.0, "temporal": 0.5, "spreading_activation": 0.8},
|
|
22
|
+
"opinion": {"semantic": 1.8, "bm25": 0.6, "entity_graph": 0.8, "temporal": 0.3, "spreading_activation": 0.5},
|
|
23
|
+
"factual": {"semantic": 1.2, "bm25": 1.4, "entity_graph": 1.0, "temporal": 0.6, "spreading_activation": 0.8},
|
|
24
|
+
"entity": {"semantic": 1.0, "bm25": 1.5, "entity_graph": 1.2, "temporal": 0.5, "spreading_activation": 1.0},
|
|
25
25
|
"general": {},
|
|
26
26
|
}
|
|
27
27
|
|