superlocalmemory 3.6.9 → 3.6.10
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 +78 -10
- package/README.md +8 -4
- package/package.json +1 -1
- package/pyproject.toml +6 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/compress_cmd.py +32 -70
- package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
- package/src/superlocalmemory/cli/setup_wizard.py +49 -0
- package/src/superlocalmemory/mcp/agent_context.py +111 -0
- package/src/superlocalmemory/mcp/tools_active.py +7 -8
- package/src/superlocalmemory/mcp/tools_core.py +16 -0
- package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
- package/src/superlocalmemory/optimize/cache/exact.py +7 -4
- package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
- package/src/superlocalmemory/optimize/cache/manager.py +70 -8
- package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
- package/src/superlocalmemory/optimize/compress/router.py +82 -87
- package/src/superlocalmemory/optimize/config/__init__.py +16 -0
- package/src/superlocalmemory/optimize/config/defaults.py +1 -6
- package/src/superlocalmemory/optimize/config/schema.py +2 -19
- package/src/superlocalmemory/optimize/config/store.py +15 -1
- package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
- package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/server.py +29 -0
- package/src/superlocalmemory/optimize/storage/db.py +78 -11
- package/src/superlocalmemory/optimize/storage/schema.py +11 -0
- package/src/superlocalmemory/server/routes/optimize.py +6 -8
- package/src/superlocalmemory/server/unified_daemon.py +26 -5
- package/src/superlocalmemory/ui/index.html +18 -14
- package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
- package/src/superlocalmemory/ui/js/optimize.js +9 -9
- package/src/superlocalmemory.egg-info/PKG-INFO +10 -5
- package/src/superlocalmemory.egg-info/SOURCES.txt +2 -2
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
- package/src/superlocalmemory/optimize/compress/extractive_json.py +0 -72
|
@@ -93,19 +93,23 @@ class PerItemBoundaryRecord:
|
|
|
93
93
|
query_sim: float,
|
|
94
94
|
delta: float = 0.05,
|
|
95
95
|
epsilon_grid: tuple[float, ...] = (0.01, 0.02, 0.05, 0.10),
|
|
96
|
+
return_threshold: float = 1.0,
|
|
96
97
|
) -> float:
|
|
97
98
|
"""Compute τ̂ — the vCache exploration probability (Eq. 11).
|
|
98
99
|
|
|
99
100
|
Args:
|
|
100
|
-
query_sim:
|
|
101
|
-
delta:
|
|
102
|
-
|
|
103
|
-
epsilon_grid:
|
|
104
|
-
|
|
101
|
+
query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
|
|
102
|
+
delta: δ — user-defined maximum error rate.
|
|
103
|
+
Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
|
|
104
|
+
epsilon_grid: ε values for the Eq. 11 min sweep.
|
|
105
|
+
Distinct from δ; controls CI conservativeness.
|
|
106
|
+
return_threshold: Semantic return threshold from config (semantic_return_threshold).
|
|
107
|
+
C-03 fix: during cold start, exploit directly when
|
|
108
|
+
query_sim >= return_threshold instead of always exploring.
|
|
105
109
|
|
|
106
110
|
Returns:
|
|
107
111
|
τ̂ ∈ [0.0, 1.0]. Lower = more exploitation.
|
|
108
|
-
Cold start (n < 3):
|
|
112
|
+
Cold start (n < 3): 0.0 if query_sim >= return_threshold, else 1.0.
|
|
109
113
|
|
|
110
114
|
Eq. 11 derivation (from the paper):
|
|
111
115
|
1. I_tt = Σ γ̂² · p_i(1 - p_i) [Fisher info diagonal]
|
|
@@ -117,7 +121,12 @@ class PerItemBoundaryRecord:
|
|
|
117
121
|
"""
|
|
118
122
|
n = len(self.samples)
|
|
119
123
|
if n < 3:
|
|
120
|
-
|
|
124
|
+
# ARCH-02 note: this function serves dual purpose — (a) warm-phase vCache
|
|
125
|
+
# Eq. 11 tau computation and (b) cold-start similarity gate. The cold-start
|
|
126
|
+
# branch (n < 3) is intentionally simple: if the query is already above the
|
|
127
|
+
# return threshold, serve it (tau=0.0 → exploit); otherwise explore (tau=1.0).
|
|
128
|
+
# C-03: honor return_threshold — avoids 100% miss until 3 samples are accumulated.
|
|
129
|
+
return 0.0 if query_sim >= return_threshold else 1.0
|
|
121
130
|
|
|
122
131
|
# Step 1: Fisher-information SE
|
|
123
132
|
i_tt = 0.0
|
|
@@ -144,12 +153,17 @@ class PerItemBoundaryRecord:
|
|
|
144
153
|
|
|
145
154
|
return best_tau
|
|
146
155
|
|
|
147
|
-
def should_explore(
|
|
156
|
+
def should_explore(
|
|
157
|
+
self,
|
|
158
|
+
query_sim: float,
|
|
159
|
+
delta: float = 0.05,
|
|
160
|
+
return_threshold: float = 1.0,
|
|
161
|
+
) -> bool:
|
|
148
162
|
"""Return True (explore = LLM call) or False (exploit = serve cache).
|
|
149
163
|
|
|
150
164
|
Source: vCache Algorithm 2: draw u ~ Uniform(0, 1); explore iff u ≤ τ̂.
|
|
151
165
|
"""
|
|
152
|
-
tau = self.compute_tau(query_sim, delta=delta)
|
|
166
|
+
tau = self.compute_tau(query_sim, delta=delta, return_threshold=return_threshold)
|
|
153
167
|
return _RNG.random() <= tau
|
|
154
168
|
|
|
155
169
|
def add_sample(
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import time
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from superlocalmemory.optimize.cache.key_builder import CacheConfig
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
_NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
|
|
13
15
|
"tool_use",
|
|
@@ -20,17 +22,21 @@ _NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
|
|
|
20
22
|
def _is_cacheable_response(response: dict) -> bool:
|
|
21
23
|
finish = response.get("stop_reason") or response.get("finish_reason") or ""
|
|
22
24
|
if finish in _NON_CACHEABLE_FINISH_REASONS:
|
|
25
|
+
logger.debug("exact: skip cache (finish_reason=%r)", finish)
|
|
23
26
|
return False
|
|
24
27
|
choices = response.get("choices") or []
|
|
25
28
|
for choice in choices:
|
|
26
29
|
fr = (choice.get("finish_reason") or "")
|
|
27
30
|
if fr in _NON_CACHEABLE_FINISH_REASONS:
|
|
31
|
+
logger.debug("exact: skip cache (choice.finish_reason=%r)", fr)
|
|
28
32
|
return False
|
|
29
33
|
msg = choice.get("message") or {}
|
|
30
34
|
if msg.get("tool_calls"):
|
|
35
|
+
logger.debug("exact: skip cache (choice.message.tool_calls present)")
|
|
31
36
|
return False
|
|
32
37
|
for block in response.get("content") or []:
|
|
33
38
|
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
39
|
+
logger.debug("exact: skip cache (tool_use content block)")
|
|
34
40
|
return False
|
|
35
41
|
return True
|
|
36
42
|
|
|
@@ -46,9 +52,6 @@ class ExactCache:
|
|
|
46
52
|
row = self._db.get(key, tenant_id)
|
|
47
53
|
if row is None:
|
|
48
54
|
return None
|
|
49
|
-
if row.ttl_expires is not None and row.ttl_expires < time.time():
|
|
50
|
-
self._db.delete(key, tenant_id)
|
|
51
|
-
return None
|
|
52
55
|
return json.loads(row.value.decode("utf-8"))
|
|
53
56
|
|
|
54
57
|
def set(
|
|
@@ -74,7 +77,7 @@ class ExactCache:
|
|
|
74
77
|
value=encoded,
|
|
75
78
|
model=model,
|
|
76
79
|
ttl_expires=expires_at,
|
|
77
|
-
tags=
|
|
80
|
+
tags=tags,
|
|
78
81
|
)
|
|
79
82
|
return True
|
|
80
83
|
|
|
@@ -4,10 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
DETERMINISTIC_PARAMS: frozenset[str] = frozenset({
|
|
12
15
|
"max_tokens", "stop", "stop_sequences", "top_p", "top_k",
|
|
13
16
|
"response_format", "tools", "tool_choice",
|
|
@@ -62,6 +65,16 @@ class KeyBuilder:
|
|
|
62
65
|
temperature = 0.0
|
|
63
66
|
|
|
64
67
|
if temperature != 0 and not self._config.allow_nonzero_temperature_cache:
|
|
68
|
+
logger.debug(
|
|
69
|
+
"cache skip: temperature=%.2f allow_nonzero=%s",
|
|
70
|
+
temperature,
|
|
71
|
+
self._config.allow_nonzero_temperature_cache,
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
from superlocalmemory.optimize.metrics.counters import MetricsCollector
|
|
75
|
+
MetricsCollector.get_instance().increment_skipped_temperature()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
65
78
|
return None
|
|
66
79
|
|
|
67
80
|
deterministic_params: dict[str, Any] = {}
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib as _hashlib
|
|
6
|
+
import json as _json_mod
|
|
5
7
|
import logging
|
|
6
8
|
import threading
|
|
7
9
|
import time
|
|
8
10
|
from abc import ABC, abstractmethod
|
|
9
11
|
from typing import Any, Callable
|
|
10
12
|
|
|
13
|
+
# C-08: precomputed hash for the common "default" tenant — avoids sha256 on every request
|
|
14
|
+
_DEFAULT_TENANT_HASH: str = _hashlib.sha256(b"default").hexdigest()
|
|
15
|
+
|
|
11
16
|
from superlocalmemory.optimize.cache.exact import ExactCache
|
|
12
17
|
from superlocalmemory.optimize.cache.invalidation import InvalidationEngine
|
|
13
18
|
from superlocalmemory.optimize.cache.key_builder import CacheConfig, KeyBuilder
|
|
@@ -230,6 +235,26 @@ class CacheManager:
|
|
|
230
235
|
ttl_seconds=0,
|
|
231
236
|
)
|
|
232
237
|
self._metrics.exact_misses += 1
|
|
238
|
+
|
|
239
|
+
# C-01: semantic fallback on exact miss (only when explicitly enabled)
|
|
240
|
+
if self._semantic.is_enabled():
|
|
241
|
+
try:
|
|
242
|
+
sem_result = self._semantic.lookup(req, tenant_id, None)
|
|
243
|
+
if sem_result is not None:
|
|
244
|
+
self._metrics.semantic_hits += 1
|
|
245
|
+
if isinstance(sem_result, CachedResponse):
|
|
246
|
+
return sem_result
|
|
247
|
+
# VCacheSemantic may return a response dict — wrap it
|
|
248
|
+
return CachedResponse(
|
|
249
|
+
hit=True,
|
|
250
|
+
data=_json_mod.dumps(sem_result, ensure_ascii=False).encode(),
|
|
251
|
+
cache_key=key,
|
|
252
|
+
ttl_seconds=0,
|
|
253
|
+
)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
logger.warning("SemanticTier.lookup raised (fail-open): %s", exc)
|
|
256
|
+
self._metrics.semantic_misses += 1
|
|
257
|
+
|
|
233
258
|
# Return miss WITH the key so callers can use it for cache storage.
|
|
234
259
|
return CachedResponse(hit=False, data=None, cache_key=key, ttl_seconds=0)
|
|
235
260
|
|
|
@@ -258,6 +283,13 @@ class CacheManager:
|
|
|
258
283
|
self._invalidation.register(key, tenant_id, tags)
|
|
259
284
|
self._metrics.sets += 1
|
|
260
285
|
|
|
286
|
+
# C-02: index in semantic tier after exact write (fail-open)
|
|
287
|
+
if self._semantic.is_enabled():
|
|
288
|
+
try:
|
|
289
|
+
self._semantic.index_entry(req, tenant_id, None, response_dict)
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
logger.warning("SemanticTier.index_entry raised (fail-open): %s", exc)
|
|
292
|
+
|
|
261
293
|
# ---- CacheHook protocol implementation (INTERFACE-CONTRACT §3) ----
|
|
262
294
|
|
|
263
295
|
def check(self, req: ProxyRequest) -> "CachedResponse | None":
|
|
@@ -269,7 +301,7 @@ class CacheManager:
|
|
|
269
301
|
cache-miss result.
|
|
270
302
|
"""
|
|
271
303
|
try:
|
|
272
|
-
result = self.get(req, tenant_id=
|
|
304
|
+
result = self.get(req, tenant_id=_DEFAULT_TENANT_HASH)
|
|
273
305
|
if result is not None and not result.hit:
|
|
274
306
|
MetricsCollector.get_instance().on_miss()
|
|
275
307
|
return result
|
|
@@ -280,21 +312,51 @@ class CacheManager:
|
|
|
280
312
|
def store(self, req: ProxyRequest, resp: ProviderResponse) -> None:
|
|
281
313
|
"""CacheHook.store() — persist response; fail-open on error."""
|
|
282
314
|
try:
|
|
283
|
-
self.set(req, resp, tenant_id=
|
|
315
|
+
self.set(req, resp, tenant_id=_DEFAULT_TENANT_HASH)
|
|
284
316
|
except Exception as exc:
|
|
285
317
|
logger.warning("CacheManager.store raised (fail-open): %s", exc)
|
|
286
318
|
|
|
287
319
|
def on_hit(self, req: ProxyRequest, resp: bytes, tokens_saved: int) -> None:
|
|
288
320
|
"""CacheHook.on_hit() — forward token savings to MetricsCollector.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
321
|
+
|
|
322
|
+
M-01: compute input tokens from request body when caller passes 0.
|
|
323
|
+
M-02: parse real output tokens from cached response usage field.
|
|
324
|
+
All counts are estimates for display — not billing-accurate.
|
|
293
325
|
"""
|
|
326
|
+
import json as _json
|
|
327
|
+
|
|
328
|
+
# M-01: estimate input tokens from message content
|
|
329
|
+
if tokens_saved == 0 and isinstance(req, ProxyRequest):
|
|
330
|
+
try:
|
|
331
|
+
body = req.body or {}
|
|
332
|
+
total_chars = 0
|
|
333
|
+
for m in (body.get("messages") or []):
|
|
334
|
+
c = m.get("content", "")
|
|
335
|
+
if isinstance(c, str):
|
|
336
|
+
total_chars += len(c)
|
|
337
|
+
elif isinstance(c, list):
|
|
338
|
+
for blk in c:
|
|
339
|
+
if isinstance(blk, dict):
|
|
340
|
+
total_chars += len(blk.get("text", "") or "")
|
|
341
|
+
total_chars += len(body.get("system", "") or "")
|
|
342
|
+
tokens_saved = max(0, total_chars // 4)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# M-02: parse real output tokens from stored response
|
|
294
347
|
output_tokens = 0
|
|
295
348
|
if resp:
|
|
296
|
-
|
|
297
|
-
|
|
349
|
+
try:
|
|
350
|
+
data = _json.loads(resp)
|
|
351
|
+
usage = data.get("usage") or {}
|
|
352
|
+
output_tokens = (
|
|
353
|
+
usage.get("output_tokens")
|
|
354
|
+
or usage.get("completion_tokens")
|
|
355
|
+
or 0
|
|
356
|
+
)
|
|
357
|
+
except Exception:
|
|
358
|
+
output_tokens = len(resp) // 4 # fallback byte-estimate
|
|
359
|
+
|
|
298
360
|
MetricsCollector.get_instance().on_hit(
|
|
299
361
|
tokens_saved_input=tokens_saved,
|
|
300
362
|
tokens_saved_output=output_tokens,
|
|
@@ -294,7 +294,8 @@ class VCacheSemantic(SemanticTier):
|
|
|
294
294
|
# Step 5: vCache exploit/explore (MLE τ̂ via record_outcome updates)
|
|
295
295
|
record = self._boundary_store.get(best_entry_id)
|
|
296
296
|
delta = float(getattr(cfg, "semantic_error_target", _DEFAULT_ERROR_TARGET))
|
|
297
|
-
|
|
297
|
+
sem_return_threshold = float(getattr(cfg, "semantic_return_threshold", _DEFAULT_RETURN_THRESHOLD))
|
|
298
|
+
if record.should_explore(best_score, delta=delta, return_threshold=sem_return_threshold):
|
|
298
299
|
logger.debug(
|
|
299
300
|
"VCacheSemantic: explore (score=%.4f entry=%s t_hat=%.4f)",
|
|
300
301
|
best_score, best_entry_id, record.t_hat,
|
|
@@ -363,11 +364,11 @@ class VCacheSemantic(SemanticTier):
|
|
|
363
364
|
self._index[tenant_id] = []
|
|
364
365
|
return
|
|
365
366
|
entries: list[tuple[str, str, np.ndarray]] = []
|
|
366
|
-
for entry_id, blob in rows:
|
|
367
|
+
for entry_id, blob, ctx_fp in rows: # C-10: unpack persisted context_fp
|
|
367
368
|
try:
|
|
368
369
|
v = np.frombuffer(blob, dtype=np.float32).copy()
|
|
369
370
|
if v.shape[0] == _EMBED_DIM:
|
|
370
|
-
entries.append((entry_id,
|
|
371
|
+
entries.append((entry_id, ctx_fp, v))
|
|
371
372
|
except Exception:
|
|
372
373
|
continue
|
|
373
374
|
with self._index_lock:
|
|
@@ -422,13 +423,17 @@ class VCacheSemantic(SemanticTier):
|
|
|
422
423
|
)
|
|
423
424
|
return
|
|
424
425
|
|
|
425
|
-
# Store vector in DB
|
|
426
|
+
# Store vector in DB — C-10: persist context_fp alongside the vector
|
|
426
427
|
vec_bytes = vec.tobytes()
|
|
427
428
|
self._db.vec_add(
|
|
428
429
|
entry_id=entry_id,
|
|
429
430
|
tenant_id=tenant_id,
|
|
430
431
|
vector=vec_bytes,
|
|
431
|
-
meta={
|
|
432
|
+
meta={
|
|
433
|
+
"model": "nomic-ai/nomic-embed-text-v1.5",
|
|
434
|
+
"dim": _EMBED_DIM,
|
|
435
|
+
"context_fp": context_fp,
|
|
436
|
+
},
|
|
432
437
|
)
|
|
433
438
|
|
|
434
439
|
# Initialize boundary record if new
|
|
@@ -32,16 +32,10 @@ class LLMLinguaCompressor:
|
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
34
|
self,
|
|
35
|
-
model_name: str =
|
|
35
|
+
model_name: str = _MODEL_XLM,
|
|
36
36
|
device_map: str = "cpu",
|
|
37
37
|
rate: float = _DEFAULT_RATE,
|
|
38
38
|
) -> None:
|
|
39
|
-
import os
|
|
40
|
-
if os.environ.get("SLM_DISABLE_HF_DOWNLOAD", "0") == "1":
|
|
41
|
-
raise ImportError(
|
|
42
|
-
"LLMLingua-2 model download blocked: SLM_DISABLE_HF_DOWNLOAD=1. "
|
|
43
|
-
"Set compress_llmlingua_allow_download=true in optimize.json."
|
|
44
|
-
)
|
|
45
39
|
try:
|
|
46
40
|
from llmlingua import PromptCompressor # type: ignore[import]
|
|
47
41
|
except ImportError as e:
|
|
@@ -26,7 +26,6 @@ from superlocalmemory.optimize.config.store import ConfigStore
|
|
|
26
26
|
logger = logging.getLogger("slm.optimize.compress.router")
|
|
27
27
|
|
|
28
28
|
_MIN_CHARS_FOR_COMPRESSION: int = 500
|
|
29
|
-
_MIN_RATIO_STRUCTURED: float = 0.60
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
class CompressRouter:
|
|
@@ -50,8 +49,6 @@ class CompressRouter:
|
|
|
50
49
|
return cls._instance
|
|
51
50
|
|
|
52
51
|
def __init__(self) -> None:
|
|
53
|
-
self._json_compressor: "JSONCompressor | None" = None
|
|
54
|
-
self._code_compressor: "CodeCompressor | None" = None
|
|
55
52
|
self._llmlingua_compressor: "LLMLinguaCompressor | None" = None
|
|
56
53
|
self._ccr_store: "CCRStore | None" = None
|
|
57
54
|
self._aligner: "CacheAligner | None" = None
|
|
@@ -103,12 +100,17 @@ class CompressRouter:
|
|
|
103
100
|
tenant_id="default",
|
|
104
101
|
)
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
# S-01/Stage-9 fix: build new_bytes BEFORE the improvement guard.
|
|
104
|
+
# Layer 1 normalization saves characters (not word-count tokens), so
|
|
105
|
+
# tokens_after == tokens_before for "normalize" strategy. Checking byte
|
|
106
|
+
# length of the serialized body correctly detects Layer 1 savings.
|
|
109
107
|
body["messages"] = new_messages
|
|
110
108
|
new_bytes = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
|
|
111
109
|
|
|
110
|
+
bytes_saved = len(req.body_bytes) - len(new_bytes)
|
|
111
|
+
if bytes_saved <= 0 and tokens_after >= tokens_before:
|
|
112
|
+
return req # neither bytes nor tokens improved
|
|
113
|
+
|
|
112
114
|
# CONTRACT §3: fire on_compress metrics callback
|
|
113
115
|
lossy = strategy == "llmlingua2_prose"
|
|
114
116
|
self.on_compress(tokens_before, tokens_after, lossy)
|
|
@@ -160,7 +162,12 @@ class CompressRouter:
|
|
|
160
162
|
primary_strategy = "none"
|
|
161
163
|
new_messages: list[dict[str, Any]] = []
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
# K-05: protect last N *user* turns, not last N messages of any role
|
|
166
|
+
user_indices = [i for i, m in enumerate(messages) if m.get("role") == "user"]
|
|
167
|
+
protect_indices: set[int] = set(user_indices[-protect_recent:]) if protect_recent > 0 else set()
|
|
168
|
+
# Always protect the very last message (current turn, any role)
|
|
169
|
+
if messages:
|
|
170
|
+
protect_indices.add(len(messages) - 1)
|
|
164
171
|
|
|
165
172
|
for idx, msg in enumerate(messages):
|
|
166
173
|
role = msg.get("role", "")
|
|
@@ -248,96 +255,77 @@ class CompressRouter:
|
|
|
248
255
|
) -> tuple[str, int, int, str]:
|
|
249
256
|
tokens_before = _token_estimate(text)
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
if len(text) < _MIN_CHARS_FOR_COMPRESSION:
|
|
259
|
+
return text, tokens_before, tokens_before, "none"
|
|
260
|
+
|
|
261
|
+
# K-01/K-02/K-03: NEVER compress structured content (JSON or code)
|
|
252
262
|
stripped = text.strip()
|
|
253
263
|
if stripped.startswith(("{", "[")):
|
|
264
|
+
# PERF-02: for large content, structural bracket-match avoids O(n) json.loads().
|
|
265
|
+
# Conservative: matching outer brackets → treat as JSON and skip compression.
|
|
266
|
+
# K-01 mandate is safety-first: false-positive (non-JSON treated as JSON) is
|
|
267
|
+
# safe; false-negative (JSON compressed) would be a correctness violation.
|
|
268
|
+
_last = stripped[-1] if stripped else ""
|
|
269
|
+
if len(stripped) > 8192 and (
|
|
270
|
+
(stripped[0] == "{" and _last == "}") or (stripped[0] == "[" and _last == "]")
|
|
271
|
+
):
|
|
272
|
+
return text, tokens_before, tokens_before, "none" # large JSON → passthrough
|
|
254
273
|
try:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
obj["__slm_ccr__"] = ccr_id
|
|
269
|
-
compressed = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
270
|
-
elif isinstance(obj, list):
|
|
271
|
-
# RB-02: list-root embedding
|
|
272
|
-
wrapper = {"__slm_ccr__": ccr_id, "__slm_data__": obj}
|
|
273
|
-
compressed = json.dumps(wrapper, ensure_ascii=False, separators=(",", ":"))
|
|
274
|
-
except Exception:
|
|
275
|
-
pass
|
|
276
|
-
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
277
|
-
logger.debug("[%s] JSON compressed %.2f ratio ccr_id=%s", request_id, ratio, ccr_id)
|
|
278
|
-
return compressed, tokens_before_adj, tokens_after, "extractive_json"
|
|
279
|
-
return text, tokens_before, tokens_before, "none"
|
|
280
|
-
except (json.JSONDecodeError, Exception):
|
|
281
|
-
pass
|
|
282
|
-
|
|
283
|
-
# Code detection
|
|
284
|
-
lang = _detect_language(text)
|
|
285
|
-
if lang is not None:
|
|
286
|
-
compressor = self._get_code_compressor()
|
|
287
|
-
# RB-03: compress first, compute ratio, then store CCR only if beneficial
|
|
288
|
-
compressed_probe = compressor.compress(text, language=lang, ccr_id="")
|
|
289
|
-
tokens_after = _token_estimate(compressed_probe)
|
|
290
|
-
ratio = tokens_after / tokens_before if tokens_before else 1.0
|
|
291
|
-
if ratio < _MIN_RATIO_STRUCTURED:
|
|
292
|
-
# B-03: store-before-compress
|
|
293
|
-
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
294
|
-
compressed = compressor.compress(text, language=lang, ccr_id=ccr_id)
|
|
295
|
-
if ccr_id:
|
|
296
|
-
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
297
|
-
logger.debug("[%s] Code compressed lang=%s ratio=%.2f ccr_id=%s",
|
|
298
|
-
request_id, lang, ratio, ccr_id)
|
|
299
|
-
return compressed, tokens_before, tokens_after, "extractive_code"
|
|
300
|
-
return text, tokens_before, tokens_before, "none"
|
|
274
|
+
json.loads(stripped)
|
|
275
|
+
return text, tokens_before, tokens_before, "none" # valid JSON → passthrough
|
|
276
|
+
except json.JSONDecodeError:
|
|
277
|
+
pass # not valid JSON — treat as prose
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
logger.warning("compress: unexpected error probing JSON content: %s", exc)
|
|
280
|
+
|
|
281
|
+
if _detect_language(text) is not None:
|
|
282
|
+
return text, tokens_before, tokens_before, "none" # code → passthrough
|
|
283
|
+
|
|
284
|
+
# Layer 1 — lossless whitespace normalization (always-on, safe)
|
|
285
|
+
normalized = self._normalize_whitespace(text)
|
|
286
|
+
tokens_after_l1 = _token_estimate(normalized)
|
|
301
287
|
|
|
302
|
-
#
|
|
303
|
-
# (Phase 3 — opt-in prose tier; off by default; gated by config
|
|
304
|
-
# field compress_prose added in LLD-04 INTERFACE-CONTRACT v2.)
|
|
288
|
+
# Layer 2 — LLMLingua-2 prose compression (aggressive + opt-in only)
|
|
305
289
|
cfg = self._get_config()
|
|
306
290
|
prose_enabled = bool(getattr(cfg, "compress_prose", False))
|
|
307
|
-
if aggressive and prose_enabled:
|
|
291
|
+
if aggressive and prose_enabled: # pragma: no cover — LLMLingua optional dep
|
|
308
292
|
compressor = self._get_llmlingua_compressor()
|
|
309
293
|
if compressor is not None:
|
|
310
|
-
# B-03: store
|
|
294
|
+
# B-03: store original BEFORE lossy compression
|
|
311
295
|
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
312
|
-
compressed = compressor.compress(
|
|
296
|
+
compressed = compressor.compress(normalized)
|
|
313
297
|
if ccr_id:
|
|
314
298
|
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
299
|
+
tokens_after_l2 = _token_estimate(compressed)
|
|
300
|
+
if tokens_after_l2 < tokens_before:
|
|
301
|
+
logger.info(
|
|
302
|
+
"[%s] LLMLingua-2 prose compressed rate=%.2f ccr_id=%s (LOSSY)",
|
|
303
|
+
request_id,
|
|
304
|
+
tokens_after_l2 / tokens_before if tokens_before else 1.0,
|
|
305
|
+
ccr_id,
|
|
306
|
+
)
|
|
307
|
+
return compressed, tokens_before, tokens_after_l2, "llmlingua2_prose"
|
|
323
308
|
|
|
324
|
-
|
|
309
|
+
# S-01 fix: compare character length, not word count.
|
|
310
|
+
# _token_estimate() is word-count — whitespace normalization saves characters/bytes
|
|
311
|
+
# but never removes words, so token counts are identical before and after Layer 1.
|
|
312
|
+
# Character comparison correctly detects when normalization reduced the body size.
|
|
313
|
+
if len(normalized) < len(text):
|
|
314
|
+
return normalized, tokens_before, tokens_after_l1, "normalize"
|
|
325
315
|
|
|
326
|
-
|
|
316
|
+
return text, tokens_before, tokens_before, "none"
|
|
327
317
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _normalize_whitespace(text: str) -> str:
|
|
320
|
+
"""Layer 1 lossless: collapse excess blank lines, strip trailing spaces per line."""
|
|
321
|
+
import re
|
|
322
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
323
|
+
lines = [line.rstrip() for line in text.split("\n")]
|
|
324
|
+
return "\n".join(lines)
|
|
333
325
|
|
|
334
|
-
|
|
335
|
-
if self._code_compressor is None:
|
|
336
|
-
from superlocalmemory.optimize.compress.extractive_code import CodeCompressor
|
|
337
|
-
self._code_compressor = CodeCompressor()
|
|
338
|
-
return self._code_compressor
|
|
326
|
+
# ── Lazy loaders ─────────────────────────────────────────────────────
|
|
339
327
|
|
|
340
|
-
def _get_llmlingua_compressor(self) -> "LLMLinguaCompressor | None":
|
|
328
|
+
def _get_llmlingua_compressor(self) -> "LLMLinguaCompressor | None": # pragma: no cover
|
|
341
329
|
if self._llmlingua_compressor is None:
|
|
342
330
|
try:
|
|
343
331
|
from superlocalmemory.optimize.compress.prose_llmlingua import LLMLinguaCompressor
|
|
@@ -396,21 +384,32 @@ class CompressRouter:
|
|
|
396
384
|
return CompressTextResult(
|
|
397
385
|
compressed_text=compressed, strategy=strat,
|
|
398
386
|
tokens_before=tb, tokens_after=ta,
|
|
387
|
+
lossy=strat == "llmlingua2_prose",
|
|
399
388
|
)
|
|
400
389
|
except Exception as exc:
|
|
401
390
|
logger.debug("compress_text failed (non-fatal): %s", exc)
|
|
391
|
+
t = _token_estimate(text)
|
|
402
392
|
return CompressTextResult(
|
|
403
393
|
compressed_text=text, strategy="none",
|
|
404
|
-
tokens_before=
|
|
394
|
+
tokens_before=t, tokens_after=t,
|
|
395
|
+
lossy=False,
|
|
405
396
|
)
|
|
406
397
|
|
|
407
398
|
|
|
408
399
|
@dataclass
|
|
409
400
|
class CompressTextResult:
|
|
401
|
+
"""Result of a compress_text() call.
|
|
402
|
+
|
|
403
|
+
UX-02 note: lossy=True only when strategy="llmlingua2_prose" (Layer 2).
|
|
404
|
+
In the default install (LLMLingua optional dep not installed), lossy is
|
|
405
|
+
always False — install `llmlingua>=0.2.0` and set compress_prose=True +
|
|
406
|
+
compress_mode="aggressive" to activate lossy compression.
|
|
407
|
+
"""
|
|
410
408
|
compressed_text: str
|
|
411
|
-
strategy: str # "
|
|
409
|
+
strategy: str # "normalize" | "llmlingua2_prose" | "none"
|
|
412
410
|
tokens_before: int
|
|
413
411
|
tokens_after: int
|
|
412
|
+
lossy: bool = False # K-10: True only for llmlingua2_prose (Layer 2)
|
|
414
413
|
|
|
415
414
|
|
|
416
415
|
# ── Module-level helpers ──────────────────────────────────────────────────────
|
|
@@ -419,10 +418,6 @@ def _token_estimate(text: str) -> int:
|
|
|
419
418
|
return len(text.split()) if text else 0
|
|
420
419
|
|
|
421
420
|
|
|
422
|
-
def _token_estimate_structured(text: str) -> int:
|
|
423
|
-
return max(1, len(text) // 4) if text else 0
|
|
424
|
-
|
|
425
|
-
|
|
426
421
|
def _msg_has_tool_result(msg: dict) -> bool:
|
|
427
422
|
"""B-09: Detect historical tool_result blocks in messages."""
|
|
428
423
|
content = msg.get("content", "")
|
|
@@ -24,6 +24,22 @@ def load_optimize_config() -> OptimizeConfig:
|
|
|
24
24
|
return get_optimize_config()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def get_shared_store() -> "ConfigStore":
|
|
28
|
+
"""Return the process-wide ConfigStore singleton, creating it on first use.
|
|
29
|
+
|
|
30
|
+
This is the SINGLE instance the daemon, the /api/optimize routes, the proxy
|
|
31
|
+
hook-reload callback, and the hot-reload watchdog must all share so that a UI
|
|
32
|
+
config change reaches the live proxy (fixes W-02 hot-reload + W-05 fresh-store
|
|
33
|
+
-per-request). The daemon calls this at startup, registers a change callback,
|
|
34
|
+
and starts the watchdog.
|
|
35
|
+
"""
|
|
36
|
+
global _store
|
|
37
|
+
if _store is None:
|
|
38
|
+
from superlocalmemory.optimize.config.store import ConfigStore
|
|
39
|
+
_store = ConfigStore()
|
|
40
|
+
return _store
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
def _set_config_store(store: "ConfigStore") -> None:
|
|
28
44
|
global _store
|
|
29
45
|
_store = store
|
|
@@ -23,15 +23,10 @@ DEFAULT_OPTIMIZE_CONFIG = OptimizeConfig(
|
|
|
23
23
|
semantic_boundary_floor=0.85,
|
|
24
24
|
semantic_pad_latency_ms=0.0,
|
|
25
25
|
semantic_centroid_min_similarity=0.85,
|
|
26
|
-
compress_enabled=
|
|
26
|
+
compress_enabled=False,
|
|
27
27
|
compress_mode="safe",
|
|
28
|
-
compress_code=True,
|
|
29
|
-
compress_json=True,
|
|
30
28
|
compress_prose=False,
|
|
31
|
-
compress_ccr=True,
|
|
32
|
-
compress_align=True,
|
|
33
29
|
compress_protect_recent=4,
|
|
34
|
-
compress_llmlingua_allow_download=False,
|
|
35
30
|
ttl_seconds=86400,
|
|
36
31
|
ttl=TTLConfig(),
|
|
37
32
|
providers={
|