superlocalmemory 3.5.8 → 3.6.0
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/ATTRIBUTION.md +24 -0
- package/CHANGELOG.md +35 -0
- package/README.md +142 -35
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/cache_cmd.py +198 -0
- package/src/superlocalmemory/cli/commands.py +80 -2
- package/src/superlocalmemory/cli/compress_cmd.py +179 -0
- package/src/superlocalmemory/cli/help_cmd.py +197 -0
- package/src/superlocalmemory/cli/main.py +122 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
- package/src/superlocalmemory/cli/optimize_constants.py +31 -0
- package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
- package/src/superlocalmemory/core/config.py +5 -0
- package/src/superlocalmemory/core/engine.py +23 -0
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
- package/src/superlocalmemory/llm/backbone.py +10 -4
- package/src/superlocalmemory/mcp/server.py +34 -0
- package/src/superlocalmemory/mcp/tools_v3.py +6 -2
- package/src/superlocalmemory/optimize/NOTICE +11 -0
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
- package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
- package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
- package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
- package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
- package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
- package/src/superlocalmemory/optimize/cache/exact.py +85 -0
- package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
- package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
- package/src/superlocalmemory/optimize/cache/manager.py +452 -0
- package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
- package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
- package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
- package/src/superlocalmemory/optimize/compress/align.py +153 -0
- package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
- package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
- package/src/superlocalmemory/optimize/compress/router.py +548 -0
- package/src/superlocalmemory/optimize/config/__init__.py +35 -0
- package/src/superlocalmemory/optimize/config/defaults.py +48 -0
- package/src/superlocalmemory/optimize/config/schema.py +255 -0
- package/src/superlocalmemory/optimize/config/store.py +209 -0
- package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
- package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
- package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
- package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
- package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
- package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
- package/src/superlocalmemory/optimize/proxy/server.py +151 -0
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +1016 -0
- package/src/superlocalmemory/optimize/storage/schema.py +184 -0
- package/src/superlocalmemory/server/routes/optimize.py +166 -0
- package/src/superlocalmemory/server/routes/v3_api.py +63 -1
- package/src/superlocalmemory/server/unified_daemon.py +105 -0
- package/src/superlocalmemory/ui/index.html +98 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
- package/src/superlocalmemory/ui/js/optimize.js +173 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# optimize/cache/boundary_store.py
|
|
2
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
3
|
+
# Licensed under AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# LLD-03 §3.1 + §4.2 — Per-item vCache online MLE boundary persistence.
|
|
6
|
+
#
|
|
7
|
+
# REAL vCache algorithm from arXiv:2502.03771 (Eq. 9/10/11, Algorithm 2,
|
|
8
|
+
# Theorem 4.1). Replaces any prior step/raise/relax heuristic.
|
|
9
|
+
#
|
|
10
|
+
# Eq. 9: L(s, t, γ) = 1/(1 + exp(-γ(s - t))) — sigmoid correctness
|
|
11
|
+
# Eq. 10: (t̂, γ̂) = argmin BCE on accumulated (s, c) pairs — MLE per entry
|
|
12
|
+
# Eq. 11: τ̂ = min_{ε} [(1-δ) - (1-ε)·L(s, t'(ε), γ̂)] / [1 - (1-ε)·L(...)]
|
|
13
|
+
# where t'(ε) = t̂ - z_{1-ε/2}·se(t̂) — pessimistic CI
|
|
14
|
+
# se(t̂) = 1/sqrt(I_tt) with I_tt = Σ γ̂²·p(1-p) — Fisher info
|
|
15
|
+
# Theorem 4.1: Pr(vCache(x) = r(x) | D) ≥ (1-δ) ∀ x, n.
|
|
16
|
+
#
|
|
17
|
+
# This is the GUARANTEE path (RA-01). The point-estimate Eq. 8 does NOT
|
|
18
|
+
# carry Theorem 4.1; only Eq. 11 does.
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import math
|
|
25
|
+
import random
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from superlocalmemory.optimize.storage.db import CacheDB
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Module-level RNG instance — tests can seed via _RNG.seed(42) for determinism.
|
|
38
|
+
_RNG = random.Random()
|
|
39
|
+
|
|
40
|
+
# Optional scipy MLE (N-D L-BFGS-B). Fall back to gradient descent if absent.
|
|
41
|
+
try:
|
|
42
|
+
from scipy.optimize import minimize as _sp_minimize # type: ignore[import]
|
|
43
|
+
_SCIPY_AVAILABLE = True
|
|
44
|
+
except ImportError: # pragma: no cover
|
|
45
|
+
_SCIPY_AVAILABLE = False
|
|
46
|
+
_sp_minimize = None # type: ignore[assignment]
|
|
47
|
+
|
|
48
|
+
_BOUNCE_EPS: float = 1e-9
|
|
49
|
+
_OVERFLOW_GUARD: float = 500.0
|
|
50
|
+
|
|
51
|
+
# z_{1 - ε/2} for common ε (avoids scipy.stats dependency)
|
|
52
|
+
_Z_TABLE: dict[float, float] = {
|
|
53
|
+
0.01: 2.576,
|
|
54
|
+
0.02: 2.326,
|
|
55
|
+
0.05: 1.960,
|
|
56
|
+
0.10: 1.645,
|
|
57
|
+
0.15: 1.440,
|
|
58
|
+
0.20: 1.282,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# PerItemBoundaryRecord — real vCache online MLE model for one cached entry
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class PerItemBoundaryRecord:
|
|
68
|
+
"""vCache per-item online MLE model for one cached entry.
|
|
69
|
+
|
|
70
|
+
A-01 fix: real vCache algorithm — learned logistic regression per entry.
|
|
71
|
+
A-23 fix: sliding window (max_samples) prevents frozen learning.
|
|
72
|
+
|
|
73
|
+
Fields:
|
|
74
|
+
entry_id: Surrogate entry ID from VCacheSemantic._derive_entry_id.
|
|
75
|
+
t_hat: MLE estimate of decision boundary t ∈ [0, 1].
|
|
76
|
+
Initialize conservative (≈ boundary_init).
|
|
77
|
+
gamma_hat: MLE estimate of steepness γ > 0.
|
|
78
|
+
Initialize steep (10.0) so cold-start is decisive.
|
|
79
|
+
samples: Accumulated (similarity, correctness) training pairs.
|
|
80
|
+
Sliding window — last `max_samples` entries retained.
|
|
81
|
+
last_updated: Unix timestamp of last model update.
|
|
82
|
+
"""
|
|
83
|
+
entry_id: str
|
|
84
|
+
t_hat: float = 0.95
|
|
85
|
+
gamma_hat: float = 10.0
|
|
86
|
+
samples: list[tuple[float, int]] = field(default_factory=list)
|
|
87
|
+
last_updated: float = 0.0
|
|
88
|
+
|
|
89
|
+
# --- Algorithm methods (Eq. 9, 10, 11, Algorithm 2) -----------------
|
|
90
|
+
|
|
91
|
+
def compute_tau(
|
|
92
|
+
self,
|
|
93
|
+
query_sim: float,
|
|
94
|
+
delta: float = 0.05,
|
|
95
|
+
epsilon_grid: tuple[float, ...] = (0.01, 0.02, 0.05, 0.10),
|
|
96
|
+
) -> float:
|
|
97
|
+
"""Compute τ̂ — the vCache exploration probability (Eq. 11).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
|
|
101
|
+
delta: δ — user-defined maximum error rate.
|
|
102
|
+
Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
|
|
103
|
+
epsilon_grid: ε values for the Eq. 11 min sweep.
|
|
104
|
+
Distinct from δ; controls CI conservativeness.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
τ̂ ∈ [0.0, 1.0]. Lower = more exploitation.
|
|
108
|
+
Cold start (n < 3): returns 1.0 (always explore).
|
|
109
|
+
|
|
110
|
+
Eq. 11 derivation (from the paper):
|
|
111
|
+
1. I_tt = Σ γ̂² · p_i(1 - p_i) [Fisher info diagonal]
|
|
112
|
+
2. se(t̂) = 1/sqrt(I_tt + ε) [normal approx SE]
|
|
113
|
+
3. t'(ε) = t̂ - z_{1-ε/2} · se(t̂) [pessimistic lower bound]
|
|
114
|
+
4. α(ε) = (1 - ε) · L(s, t'(ε), γ̂) [G_τ sub-function]
|
|
115
|
+
5. τ(ε) = ((1 - δ) - α) / (1 - α)
|
|
116
|
+
6. τ̂ = min over ε in grid [Eq. 11 min]
|
|
117
|
+
"""
|
|
118
|
+
n = len(self.samples)
|
|
119
|
+
if n < 3:
|
|
120
|
+
return 1.0 # cold start — always explore
|
|
121
|
+
|
|
122
|
+
# Step 1: Fisher-information SE
|
|
123
|
+
i_tt = 0.0
|
|
124
|
+
for s, _c in self.samples:
|
|
125
|
+
p = _sigmoid(s, self.t_hat, self.gamma_hat)
|
|
126
|
+
i_tt += (self.gamma_hat ** 2) * p * (1.0 - p)
|
|
127
|
+
se_t = 1.0 / math.sqrt(i_tt + _BOUNCE_EPS)
|
|
128
|
+
|
|
129
|
+
# Step 2: Eq. 11 min over ε
|
|
130
|
+
best_tau = 1.0
|
|
131
|
+
for eps in epsilon_grid:
|
|
132
|
+
z = _Z_TABLE.get(eps, 1.960) # default z_{0.975}
|
|
133
|
+
t_prime = self.t_hat - z * se_t
|
|
134
|
+
t_prime = max(0.0, min(1.0, t_prime)) # clip to valid t range
|
|
135
|
+
|
|
136
|
+
alpha = (1.0 - eps) * _sigmoid(query_sim, t_prime, self.gamma_hat)
|
|
137
|
+
denom = 1.0 - alpha
|
|
138
|
+
if denom < _BOUNCE_EPS:
|
|
139
|
+
tau_eps = 0.0 # near-certainty
|
|
140
|
+
else:
|
|
141
|
+
tau_eps = ((1.0 - delta) - alpha) / denom
|
|
142
|
+
tau_eps = max(0.0, min(1.0, tau_eps))
|
|
143
|
+
best_tau = min(best_tau, tau_eps)
|
|
144
|
+
|
|
145
|
+
return best_tau
|
|
146
|
+
|
|
147
|
+
def should_explore(self, query_sim: float, delta: float = 0.05) -> bool:
|
|
148
|
+
"""Return True (explore = LLM call) or False (exploit = serve cache).
|
|
149
|
+
|
|
150
|
+
Source: vCache Algorithm 2: draw u ~ Uniform(0, 1); explore iff u ≤ τ̂.
|
|
151
|
+
"""
|
|
152
|
+
tau = self.compute_tau(query_sim, delta=delta)
|
|
153
|
+
return _RNG.random() <= tau
|
|
154
|
+
|
|
155
|
+
def add_sample(
|
|
156
|
+
self,
|
|
157
|
+
similarity: float,
|
|
158
|
+
was_correct: bool,
|
|
159
|
+
max_samples: int = 200,
|
|
160
|
+
) -> "PerItemBoundaryRecord":
|
|
161
|
+
"""Add a (similarity, correctness) pair and refit MLE. Returns NEW record.
|
|
162
|
+
|
|
163
|
+
A-01 fix: this is the REAL vCache learning step. MLE refit on the
|
|
164
|
+
accumulated samples. No step size. No raise/relax heuristic.
|
|
165
|
+
A-23 fix: sliding window — drops oldest when len > max_samples.
|
|
166
|
+
"""
|
|
167
|
+
new_samples = list(self.samples)
|
|
168
|
+
new_samples.append((float(similarity), 1 if was_correct else 0))
|
|
169
|
+
if len(new_samples) > max_samples:
|
|
170
|
+
new_samples = new_samples[-max_samples:]
|
|
171
|
+
|
|
172
|
+
new_t, new_gamma = _fit_logistic_mle(
|
|
173
|
+
new_samples, self.t_hat, self.gamma_hat,
|
|
174
|
+
)
|
|
175
|
+
return PerItemBoundaryRecord(
|
|
176
|
+
entry_id=self.entry_id,
|
|
177
|
+
t_hat=new_t,
|
|
178
|
+
gamma_hat=new_gamma,
|
|
179
|
+
samples=new_samples,
|
|
180
|
+
last_updated=time.time(),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Numerical helpers
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def _sigmoid(s: float, t: float, gamma: float) -> float:
|
|
189
|
+
"""L(s, t, γ) = 1 / (1 + exp(-γ(s - t))) — Eq. 9. Overflow-guarded."""
|
|
190
|
+
exponent = -gamma * (s - t)
|
|
191
|
+
exponent = max(-_OVERFLOW_GUARD, min(_OVERFLOW_GUARD, exponent))
|
|
192
|
+
return 1.0 / (1.0 + math.exp(exponent))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _rng() -> float:
|
|
196
|
+
"""u ~ Uniform(0, 1) for the vCache exploit/explore draw.
|
|
197
|
+
|
|
198
|
+
Tests can seed via `boundary_store._RNG.seed(N)` for determinism.
|
|
199
|
+
"""
|
|
200
|
+
return _RNG.random()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _binary_cross_entropy(
|
|
204
|
+
params: tuple[float, float],
|
|
205
|
+
samples: list[tuple[float, int]],
|
|
206
|
+
) -> float:
|
|
207
|
+
"""BCE loss for the logistic model — Eq. 10 objective."""
|
|
208
|
+
t, gamma = params
|
|
209
|
+
if gamma <= 0 or not samples:
|
|
210
|
+
return 1e9
|
|
211
|
+
total = 0.0
|
|
212
|
+
for s, c in samples:
|
|
213
|
+
p = _sigmoid(s, t, gamma)
|
|
214
|
+
p = max(_BOUNCE_EPS, min(1.0 - _BOUNCE_EPS, p)) # numerical stability
|
|
215
|
+
total += -(c * math.log(p) + (1 - c) * math.log(1 - p))
|
|
216
|
+
return total / len(samples)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _fit_logistic_mle(
|
|
220
|
+
samples: list[tuple[float, int]],
|
|
221
|
+
t_init: float,
|
|
222
|
+
gamma_init: float,
|
|
223
|
+
t_prior: float = 0.95,
|
|
224
|
+
) -> tuple[float, float]:
|
|
225
|
+
"""Fit (t̂, γ̂) via MLE on `samples`. L-BFGS-B if scipy, else gradient descent.
|
|
226
|
+
|
|
227
|
+
Fail-open: returns (t_init, gamma_init) on any error.
|
|
228
|
+
|
|
229
|
+
The prior pulls the solution toward (t_prior, 10.0) so a single sample
|
|
230
|
+
does not collapse the boundary to the saturating corner. This is
|
|
231
|
+
required for stable online learning.
|
|
232
|
+
|
|
233
|
+
Warm-start trick: t_init is nudged toward the empirical midpoint of
|
|
234
|
+
the sample similarities. This avoids L-BFGS-B's "already-at-optimum"
|
|
235
|
+
early termination when the cold-start t=0.95 happens to sit at a
|
|
236
|
+
BCE plateau.
|
|
237
|
+
"""
|
|
238
|
+
if not samples:
|
|
239
|
+
return t_init, gamma_init
|
|
240
|
+
|
|
241
|
+
# Warm-start: empirical midpoint of positive vs negative clusters.
|
|
242
|
+
pos = [s for s, c in samples if c == 1]
|
|
243
|
+
neg = [s for s, c in samples if c == 0]
|
|
244
|
+
if pos and neg:
|
|
245
|
+
empirical_mid = (min(pos) + max(neg)) / 2.0
|
|
246
|
+
t_warm = max(0.5, min(1.0, empirical_mid))
|
|
247
|
+
else:
|
|
248
|
+
t_warm = t_init
|
|
249
|
+
gamma_warm = gamma_init
|
|
250
|
+
|
|
251
|
+
if _SCIPY_AVAILABLE and _sp_minimize is not None:
|
|
252
|
+
try:
|
|
253
|
+
result = _sp_minimize(
|
|
254
|
+
fun=lambda p: _binary_cross_entropy(
|
|
255
|
+
(p[0], p[1]), samples,
|
|
256
|
+
),
|
|
257
|
+
x0=[t_warm, gamma_warm],
|
|
258
|
+
method="L-BFGS-B",
|
|
259
|
+
bounds=[(0.5, 1.0), (0.1, 100.0)],
|
|
260
|
+
options={"maxiter": 200, "ftol": 1e-10, "gtol": 1e-8},
|
|
261
|
+
)
|
|
262
|
+
t_out, g_out = float(result.x[0]), float(result.x[1])
|
|
263
|
+
# Guard: refuse to return a value outside the observed sample range
|
|
264
|
+
sims = [s for s, _ in samples]
|
|
265
|
+
lo, hi = min(sims), max(sims)
|
|
266
|
+
t_out = max(lo, min(hi, t_out))
|
|
267
|
+
return t_out, g_out
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
return _fit_logistic_gd(samples, t_warm, gamma_warm)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _fit_logistic_gd(
|
|
274
|
+
samples: list[tuple[float, int]],
|
|
275
|
+
t_init: float,
|
|
276
|
+
gamma_init: float,
|
|
277
|
+
lr: float = 0.05,
|
|
278
|
+
steps: int = 300,
|
|
279
|
+
) -> tuple[float, float]:
|
|
280
|
+
"""Gradient-descent MLE fallback. Clips to t ∈ [0.5, 1.0], γ ∈ [0.1, 100]."""
|
|
281
|
+
t = t_init
|
|
282
|
+
gamma = gamma_init
|
|
283
|
+
for _ in range(steps):
|
|
284
|
+
dt = 0.0
|
|
285
|
+
dg = 0.0
|
|
286
|
+
for s, c in samples:
|
|
287
|
+
p = _sigmoid(s, t, gamma)
|
|
288
|
+
err = p - c
|
|
289
|
+
dt += err * gamma
|
|
290
|
+
dg += err * (-(s - t))
|
|
291
|
+
n = max(len(samples), 1)
|
|
292
|
+
t = max(0.5, min(1.0, t - lr * dt / n))
|
|
293
|
+
gamma = max(0.1, min(100.0, gamma - lr * dg / n))
|
|
294
|
+
return t, gamma
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# BoundaryStore — SQLite-backed per-entry MLE record registry
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
class BoundaryStore:
|
|
302
|
+
"""Manages per-item vCache learned boundaries.
|
|
303
|
+
|
|
304
|
+
Thread safety: CacheDB uses DatabaseManager (WAL, retry, busy_timeout).
|
|
305
|
+
Concurrent updates to the same entry_id are serialized by SQLite row
|
|
306
|
+
locking. The in-memory `_cache` is for warm-start; it is rebuilt via
|
|
307
|
+
load_all() at VCacheSemantic startup.
|
|
308
|
+
|
|
309
|
+
Fail-open: any DB error returns a safe cold-start default rather than
|
|
310
|
+
raising.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
db: "CacheDB",
|
|
316
|
+
default_t: float = 0.95,
|
|
317
|
+
default_gamma: float = 10.0,
|
|
318
|
+
floor: float = 0.85,
|
|
319
|
+
ceiling: float = 0.995,
|
|
320
|
+
step: float = 0.01,
|
|
321
|
+
epsilon: float = 0.02,
|
|
322
|
+
) -> None:
|
|
323
|
+
self._db = db
|
|
324
|
+
self._default_t = default_t
|
|
325
|
+
self._default_gamma = default_gamma
|
|
326
|
+
self._floor = floor
|
|
327
|
+
self._ceiling = ceiling
|
|
328
|
+
self._step = step
|
|
329
|
+
self._epsilon = epsilon
|
|
330
|
+
# In-memory write-through cache. Populated by load_all() at warm.
|
|
331
|
+
# get() checks here first (O(1) hot path), then falls back to DB.
|
|
332
|
+
self._cache: dict[str, PerItemBoundaryRecord] = {}
|
|
333
|
+
|
|
334
|
+
def get(self, entry_id: str) -> PerItemBoundaryRecord:
|
|
335
|
+
"""Return the MLE model record for an entry, or a cold-start default.
|
|
336
|
+
|
|
337
|
+
Fail-open: returns default (cold-start) record on DB error.
|
|
338
|
+
Never raises.
|
|
339
|
+
"""
|
|
340
|
+
if entry_id in self._cache:
|
|
341
|
+
return self._cache[entry_id]
|
|
342
|
+
try:
|
|
343
|
+
row = self._db.boundary_get(entry_id)
|
|
344
|
+
if row is None:
|
|
345
|
+
return PerItemBoundaryRecord(
|
|
346
|
+
entry_id=entry_id,
|
|
347
|
+
t_hat=self._default_t,
|
|
348
|
+
gamma_hat=self._default_gamma,
|
|
349
|
+
samples=[],
|
|
350
|
+
last_updated=time.time(),
|
|
351
|
+
)
|
|
352
|
+
try:
|
|
353
|
+
samples_raw = json.loads(getattr(row, "samples_json", None) or "[]") \
|
|
354
|
+
if hasattr(row, "samples_json") else []
|
|
355
|
+
except (json.JSONDecodeError, TypeError):
|
|
356
|
+
samples_raw = []
|
|
357
|
+
if not samples_raw and hasattr(row, "samples_json"):
|
|
358
|
+
# BoundaryRow dataclass has no samples_json field; persist only
|
|
359
|
+
# the MLE parameters. samples are in-memory only after refit.
|
|
360
|
+
pass
|
|
361
|
+
# We persist only (t_hat, gamma_hat, sample_count) in the DB.
|
|
362
|
+
# samples is rebuilt from feedback calls (record_outcome) when needed.
|
|
363
|
+
return PerItemBoundaryRecord(
|
|
364
|
+
entry_id=entry_id,
|
|
365
|
+
t_hat=float(getattr(row, "logistic_t", self._default_t)),
|
|
366
|
+
gamma_hat=float(getattr(row, "logistic_gamma", self._default_gamma)),
|
|
367
|
+
samples=[],
|
|
368
|
+
last_updated=float(getattr(row, "updated_at", 0.0)),
|
|
369
|
+
)
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
logger.warning("BoundaryStore.get failed (fail-open): %s", exc)
|
|
372
|
+
return PerItemBoundaryRecord(
|
|
373
|
+
entry_id=entry_id,
|
|
374
|
+
t_hat=self._default_t,
|
|
375
|
+
gamma_hat=self._default_gamma,
|
|
376
|
+
samples=[],
|
|
377
|
+
last_updated=time.time(),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def save(self, record: PerItemBoundaryRecord) -> None:
|
|
381
|
+
"""Persist the MLE parameters (t_hat, gamma_hat, sample_count) for an entry.
|
|
382
|
+
|
|
383
|
+
Fail-open: logs warning on DB error, does not raise.
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
from superlocalmemory.optimize.storage.db import BoundaryRow
|
|
387
|
+
row = BoundaryRow(
|
|
388
|
+
entry_id=record.entry_id,
|
|
389
|
+
logistic_t=record.t_hat,
|
|
390
|
+
logistic_gamma=record.gamma_hat,
|
|
391
|
+
sample_count=len(record.samples),
|
|
392
|
+
updated_at=record.last_updated or time.time(),
|
|
393
|
+
)
|
|
394
|
+
self._db.boundary_upsert(record.entry_id, row)
|
|
395
|
+
# In-memory write-through (RA-15)
|
|
396
|
+
self._cache[record.entry_id] = record
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
logger.warning("BoundaryStore.save failed (fail-open): %s", exc)
|
|
399
|
+
|
|
400
|
+
def record_outcome(
|
|
401
|
+
self,
|
|
402
|
+
entry_id: str,
|
|
403
|
+
similarity: float,
|
|
404
|
+
was_correct: bool,
|
|
405
|
+
max_samples: int = 200,
|
|
406
|
+
) -> PerItemBoundaryRecord:
|
|
407
|
+
"""Fetch, add (s, c), refit MLE, save, return updated record.
|
|
408
|
+
|
|
409
|
+
A-01 fix: this IS the vCache online MLE learning step. No step size.
|
|
410
|
+
No raise/relax heuristic. The MLE handles everything.
|
|
411
|
+
"""
|
|
412
|
+
record = self.get(entry_id)
|
|
413
|
+
updated = record.add_sample(
|
|
414
|
+
similarity=similarity,
|
|
415
|
+
was_correct=was_correct,
|
|
416
|
+
max_samples=max_samples,
|
|
417
|
+
)
|
|
418
|
+
self.save(updated)
|
|
419
|
+
return updated
|
|
420
|
+
|
|
421
|
+
def load_all(self) -> dict[str, PerItemBoundaryRecord]:
|
|
422
|
+
"""Load all boundary records into memory (warm-start).
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict entry_id → PerItemBoundaryRecord. {} on error.
|
|
426
|
+
|
|
427
|
+
Note: only the (t_hat, gamma_hat, sample_count) are loaded from DB.
|
|
428
|
+
The full `samples` window is rebuilt on demand via record_outcome().
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
rows = self._db.get_all_boundaries()
|
|
432
|
+
result: dict[str, PerItemBoundaryRecord] = {}
|
|
433
|
+
for r in rows:
|
|
434
|
+
eid = r.get("entry_id")
|
|
435
|
+
if not eid:
|
|
436
|
+
continue
|
|
437
|
+
result[eid] = PerItemBoundaryRecord(
|
|
438
|
+
entry_id=eid,
|
|
439
|
+
t_hat=float(r.get("logistic_t", self._default_t)),
|
|
440
|
+
gamma_hat=float(r.get("logistic_gamma", self._default_gamma)),
|
|
441
|
+
samples=[],
|
|
442
|
+
last_updated=float(r.get("updated_at", 0.0)),
|
|
443
|
+
)
|
|
444
|
+
return result
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
logger.warning("BoundaryStore.load_all failed (fail-open): %s", exc)
|
|
447
|
+
return {}
|
|
448
|
+
|
|
449
|
+
def delete(self, entry_id: str) -> None:
|
|
450
|
+
"""Remove boundary record for a deleted cache entry. Fail-open."""
|
|
451
|
+
try:
|
|
452
|
+
self._db.delete_boundary(entry_id)
|
|
453
|
+
self._cache.pop(entry_id, None)
|
|
454
|
+
except Exception as exc:
|
|
455
|
+
logger.warning("BoundaryStore.delete failed (fail-open): %s", exc)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# optimize/cache/centroid_store.py
|
|
2
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
3
|
+
# Licensed under AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# LLD-03 §4.3 — SAFE-CACHE centroid registry for adversarial collision
|
|
6
|
+
# detection.
|
|
7
|
+
#
|
|
8
|
+
# Source: SAFE-CACHE — Nature Scientific Reports 2026 (no arXiv).
|
|
9
|
+
# Defense: compare the incoming query vector to the cluster centroid. If
|
|
10
|
+
# the query is far from the centroid (cosine sim < 1 - distance_floor),
|
|
11
|
+
# it falls outside the natural distribution → likely an adversarial
|
|
12
|
+
# probe crafted to collide with a specific entry. Reject as miss.
|
|
13
|
+
#
|
|
14
|
+
# Design:
|
|
15
|
+
# - One centroid per tenant_id (simplest partition, proven sufficient).
|
|
16
|
+
# - Rebuilt from llmcache_semantic_vectors on startup.
|
|
17
|
+
# - Updated incrementally (Welford running mean) on every new set().
|
|
18
|
+
# - In-memory only — no separate SQL query needed (cache is O(N) anyway).
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from superlocalmemory.optimize.storage.db import CacheDB
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_VARIANCE_FLOOR: float = 1e-6
|
|
34
|
+
_EMBED_DIM: int = 768
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
|
38
|
+
"""Cosine similarity in [-1, 1]. Returns 0.0 on zero vectors."""
|
|
39
|
+
norm_a = float(np.linalg.norm(a))
|
|
40
|
+
norm_b = float(np.linalg.norm(b))
|
|
41
|
+
if norm_a < _VARIANCE_FLOOR or norm_b < _VARIANCE_FLOOR:
|
|
42
|
+
return 0.0
|
|
43
|
+
return float(np.dot(a, b) / (norm_a * norm_b))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CentroidStore:
|
|
47
|
+
"""In-memory centroid registry for SAFE-CACHE adversarial defense.
|
|
48
|
+
|
|
49
|
+
Thread-safe via a single RLock. All public methods are fail-open.
|
|
50
|
+
|
|
51
|
+
Centroid update rule (Welford running mean — exact, O(1) per update):
|
|
52
|
+
new_centroid = old_centroid * (n / (n+1)) + new_vec * (1 / (n+1))
|
|
53
|
+
"""
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._centroids: dict[str, np.ndarray] = {} # tenant_id → float32 vec
|
|
56
|
+
self._counts: dict[str, int] = {} # tenant_id → count
|
|
57
|
+
self._lock = threading.RLock()
|
|
58
|
+
|
|
59
|
+
def rebuild_from_db(self, db: "CacheDB", tenant_id: str) -> None:
|
|
60
|
+
"""Rebuild centroid for a tenant from all stored vectors.
|
|
61
|
+
|
|
62
|
+
Called at VCacheSemantic startup. O(N) — runs once per tenant.
|
|
63
|
+
Fail-open: on DB error or empty vector set, centroid is reset.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
rows = db.get_all_vectors(tenant_id=tenant_id)
|
|
67
|
+
if not rows:
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._centroids.pop(tenant_id, None)
|
|
70
|
+
self._counts.pop(tenant_id, None)
|
|
71
|
+
return
|
|
72
|
+
vectors: list[np.ndarray] = []
|
|
73
|
+
for _entry_id, blob in rows:
|
|
74
|
+
try:
|
|
75
|
+
vec = np.frombuffer(blob, dtype=np.float32).copy()
|
|
76
|
+
if vec.shape[0] == _EMBED_DIM:
|
|
77
|
+
vectors.append(vec)
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
if not vectors:
|
|
81
|
+
return
|
|
82
|
+
centroid = np.mean(np.stack(vectors, axis=0), axis=0).astype(np.float32)
|
|
83
|
+
with self._lock:
|
|
84
|
+
self._centroids[tenant_id] = centroid
|
|
85
|
+
self._counts[tenant_id] = len(vectors)
|
|
86
|
+
logger.debug(
|
|
87
|
+
"CentroidStore: rebuilt tenant=%s centroid from %d vectors",
|
|
88
|
+
tenant_id, len(vectors),
|
|
89
|
+
)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
logger.warning("CentroidStore.rebuild_from_db failed (fail-open): %s", exc)
|
|
92
|
+
|
|
93
|
+
def update(self, tenant_id: str, new_vector: np.ndarray) -> None:
|
|
94
|
+
"""Update centroid with a new vector (Welford incremental mean).
|
|
95
|
+
|
|
96
|
+
Thread-safe. Fail-open.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
vec = new_vector.astype(np.float32)
|
|
100
|
+
with self._lock:
|
|
101
|
+
if tenant_id not in self._centroids:
|
|
102
|
+
self._centroids[tenant_id] = vec.copy()
|
|
103
|
+
self._counts[tenant_id] = 1
|
|
104
|
+
else:
|
|
105
|
+
n = self._counts[tenant_id]
|
|
106
|
+
old = self._centroids[tenant_id]
|
|
107
|
+
self._centroids[tenant_id] = (
|
|
108
|
+
old * (n / (n + 1)) + vec * (1.0 / (n + 1))
|
|
109
|
+
).astype(np.float32)
|
|
110
|
+
self._counts[tenant_id] = n + 1
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.warning("CentroidStore.update failed (fail-open): %s", exc)
|
|
113
|
+
|
|
114
|
+
def is_adversarial(
|
|
115
|
+
self,
|
|
116
|
+
tenant_id: str,
|
|
117
|
+
query_vector: np.ndarray,
|
|
118
|
+
distance_floor: float = 0.15,
|
|
119
|
+
) -> bool:
|
|
120
|
+
"""Return True if query_vector appears adversarially crafted.
|
|
121
|
+
|
|
122
|
+
Defense: if the query is too far from the cluster centroid (cosine
|
|
123
|
+
similarity < 1 - distance_floor), it is an outlier — likely an
|
|
124
|
+
adversarial probe. Return True → reject (adversarial).
|
|
125
|
+
|
|
126
|
+
Fail-open: returns False (accept) on any error or missing centroid.
|
|
127
|
+
Skips defense if the cluster has < 5 entries (insufficient data).
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
with self._lock:
|
|
131
|
+
centroid = self._centroids.get(tenant_id)
|
|
132
|
+
count = self._counts.get(tenant_id, 0)
|
|
133
|
+
if centroid is None or count < 5:
|
|
134
|
+
return False
|
|
135
|
+
q = query_vector.astype(np.float32)
|
|
136
|
+
sim = _cosine_similarity(q, centroid)
|
|
137
|
+
threshold = 1.0 - distance_floor
|
|
138
|
+
if sim < threshold:
|
|
139
|
+
logger.warning(
|
|
140
|
+
"CentroidStore: adversarial probe detected "
|
|
141
|
+
"(tenant=%s, centroid_sim=%.4f < %.4f) — returning miss.",
|
|
142
|
+
tenant_id, sim, threshold,
|
|
143
|
+
)
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
logger.warning("CentroidStore.is_adversarial failed (fail-open): %s", exc)
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def get_centroid(self, tenant_id: str) -> np.ndarray | None:
|
|
151
|
+
"""Return current centroid for a tenant, or None if not established."""
|
|
152
|
+
with self._lock:
|
|
153
|
+
return self._centroids.get(tenant_id)
|
|
154
|
+
|
|
155
|
+
def count(self, tenant_id: str) -> int:
|
|
156
|
+
"""Return the number of vectors contributing to this tenant's centroid."""
|
|
157
|
+
with self._lock:
|
|
158
|
+
return self._counts.get(tenant_id, 0)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# optimize/cache/context_key.py
|
|
2
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
3
|
+
# Licensed under AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# LLD-03 §4.1 — Multi-turn context-aware key builder for the semantic cache.
|
|
6
|
+
# Source: ContextCache / SmartCache (arXiv:2506.22791 §3) — context-aware
|
|
7
|
+
# cache keys prevent false reuse across semantically overlapping but
|
|
8
|
+
# conversationally distinct turns.
|
|
9
|
+
#
|
|
10
|
+
# The fingerprint is NOT the full cache key (that is KeyBuilder's job).
|
|
11
|
+
# It is an auxiliary scope guard: a semantic hit is accepted ONLY IF
|
|
12
|
+
# the stored entry's context fingerprint matches the query's context
|
|
13
|
+
# fingerprint (or is absent — single-turn entries).
|
|
14
|
+
#
|
|
15
|
+
# A-22 fix: 16-hex-char fingerprint (64 bits → birthday at 2^32 entries,
|
|
16
|
+
# safe for any realistic tenant). 8 hex chars (32 bits) birthday at 2^16.
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_CONTEXT_SCHEMA_VERSION: int = 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ContextKeyBuilder:
|
|
28
|
+
"""Builds a context fingerprint for multi-turn semantic cache lookup.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
window_turns: Number of prior assistant+user turns to include.
|
|
32
|
+
Default: 3 (matches LLD-03 §3.2 default).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, window_turns: int = 3) -> None:
|
|
36
|
+
if window_turns < 1:
|
|
37
|
+
raise ValueError(f"window_turns must be >= 1, got {window_turns}")
|
|
38
|
+
self._window = window_turns
|
|
39
|
+
|
|
40
|
+
def build(self, messages: list[dict[str, Any]], tenant_id: str) -> str:
|
|
41
|
+
"""Build a 16-hex-char (64-bit) context fingerprint.
|
|
42
|
+
|
|
43
|
+
Takes the last `window_turns * 2` messages (user + assistant pairs),
|
|
44
|
+
canonicalizes them, SHA-256 hashes them, returns first 16 hex chars.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
16-hex context fingerprint, e.g. "a3f2b1c0d4e5f601". Never empty.
|
|
48
|
+
Single-turn (no prior context) returns the SHA-256 of an empty
|
|
49
|
+
canonical context array — a stable sentinel.
|
|
50
|
+
"""
|
|
51
|
+
prior = messages[:-1] if len(messages) > 1 else []
|
|
52
|
+
window = prior[-(self._window * 2):]
|
|
53
|
+
|
|
54
|
+
payload = {
|
|
55
|
+
"v": _CONTEXT_SCHEMA_VERSION,
|
|
56
|
+
"tenant": tenant_id,
|
|
57
|
+
"ctx": window,
|
|
58
|
+
}
|
|
59
|
+
canonical = json.dumps(
|
|
60
|
+
payload, sort_keys=True, separators=(",", ":"),
|
|
61
|
+
ensure_ascii=True, default=str,
|
|
62
|
+
)
|
|
63
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
|
64
|
+
|
|
65
|
+
def turn_count(self, messages: list[dict[str, Any]]) -> int:
|
|
66
|
+
"""Return the number of completed conversation turns (assistant messages)."""
|
|
67
|
+
return sum(1 for m in messages if m.get("role") == "assistant")
|