superlocalmemory 3.5.7 → 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 +40 -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 +100 -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/infra/process_reaper.py +12 -1
- 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,77 @@
|
|
|
1
|
+
# compress/prose_llmlingua.py
|
|
2
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
3
|
+
# Licensed under AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# LLMLingua-2 paper: Microsoft Research + MIT (2024).
|
|
6
|
+
# Model: XLM-RoBERTa fine-tuned as token binary classifier.
|
|
7
|
+
# License: MIT (github.com/microsoft/LLMLingua)
|
|
8
|
+
|
|
9
|
+
"""LLMLinguaCompressor — opt-in LLMLingua-2 prose compressor.
|
|
10
|
+
|
|
11
|
+
SAFETY RULES (NON-NEGOTIABLE):
|
|
12
|
+
1. ONLY called for prose/narrative content — NEVER JSON or code.
|
|
13
|
+
2. compress_mode MUST be "aggressive" in optimize.json.
|
|
14
|
+
3. Lossy — CCR stores original before this runs.
|
|
15
|
+
4. Import errors caught — returns original on failure.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("slm.optimize.compress.llmlingua")
|
|
24
|
+
|
|
25
|
+
_DEFAULT_RATE: float = 0.5
|
|
26
|
+
_MODEL_BERT: str = "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank"
|
|
27
|
+
_MODEL_XLM: str = "microsoft/llmlingua-2-xlm-roberta-large-meetingbank"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LLMLinguaCompressor:
|
|
31
|
+
"""Opt-in LLMLingua-2 prose compressor."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
model_name: str = _MODEL_BERT,
|
|
36
|
+
device_map: str = "cpu",
|
|
37
|
+
rate: float = _DEFAULT_RATE,
|
|
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
|
+
try:
|
|
46
|
+
from llmlingua import PromptCompressor # type: ignore[import]
|
|
47
|
+
except ImportError as e:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"llmlingua package not installed. Install: pip install llmlingua."
|
|
50
|
+
) from e
|
|
51
|
+
|
|
52
|
+
logger.info("Loading LLMLingua-2 model=%s device=%s", model_name, device_map)
|
|
53
|
+
self._compressor: Any = PromptCompressor(
|
|
54
|
+
model_name=model_name,
|
|
55
|
+
use_llmlingua2=True,
|
|
56
|
+
device_map=device_map,
|
|
57
|
+
)
|
|
58
|
+
self._rate = rate
|
|
59
|
+
logger.info("LLMLingua-2 loaded successfully model=%s", model_name)
|
|
60
|
+
|
|
61
|
+
def compress(self, text: str, rate: float | None = None) -> str:
|
|
62
|
+
"""Compress prose text using LLMLingua-2. Fail-open: returns original on error."""
|
|
63
|
+
effective_rate = rate if rate is not None else self._rate
|
|
64
|
+
try:
|
|
65
|
+
result = self._compressor.compress_prompt(
|
|
66
|
+
[text],
|
|
67
|
+
rate=effective_rate,
|
|
68
|
+
use_token_level_filter=True,
|
|
69
|
+
)
|
|
70
|
+
compressed = result.get("compressed_prompt", text)
|
|
71
|
+
if not isinstance(compressed, str) or not compressed:
|
|
72
|
+
logger.warning("LLMLingua-2 returned unexpected output — passthrough")
|
|
73
|
+
return text
|
|
74
|
+
return compressed
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
logger.warning("LLMLingua-2 compress failed — passthrough: %s", exc)
|
|
77
|
+
return text
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# compress/router.py
|
|
2
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
3
|
+
# Licensed under AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Routing pattern adapted from:
|
|
6
|
+
# headroom/transforms/content_router.py (Apache-2.0, Headroom contributors)
|
|
7
|
+
# Specifically: ContentRouter._determine_strategy(), _strategy_from_detection()
|
|
8
|
+
# Attribution: See ATTRIBUTION.md.
|
|
9
|
+
|
|
10
|
+
"""CompressRouter — implements CompressHook, dispatches to sub-compressors.
|
|
11
|
+
|
|
12
|
+
INTERFACE-CONTRACT v2.2 §3 defines: compress(ProxyRequest) → ProxyRequest.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from superlocalmemory.optimize.proxy.lifecycle import ProxyRequest, CompressHook
|
|
24
|
+
from superlocalmemory.optimize.config.store import ConfigStore
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("slm.optimize.compress.router")
|
|
27
|
+
|
|
28
|
+
_MIN_CHARS_FOR_COMPRESSION: int = 500
|
|
29
|
+
_MIN_RATIO_STRUCTURED: float = 0.60
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CompressRouter:
|
|
33
|
+
"""Implements CompressHook Protocol per INTERFACE-CONTRACT §3.
|
|
34
|
+
|
|
35
|
+
compress(req: ProxyRequest) → ProxyRequest
|
|
36
|
+
on_compress(before_tokens, after_tokens, lossy) — metrics callback
|
|
37
|
+
|
|
38
|
+
Singleton per daemon instance.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_instance: "CompressRouter | None" = None
|
|
42
|
+
_lock: threading.Lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_instance(cls) -> "CompressRouter":
|
|
46
|
+
if cls._instance is None:
|
|
47
|
+
with cls._lock:
|
|
48
|
+
if cls._instance is None:
|
|
49
|
+
cls._instance = cls()
|
|
50
|
+
return cls._instance
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._json_compressor: "JSONCompressor | None" = None
|
|
54
|
+
self._code_compressor: "CodeCompressor | None" = None
|
|
55
|
+
self._llmlingua_compressor: "LLMLinguaCompressor | None" = None
|
|
56
|
+
self._ccr_store: "CCRStore | None" = None
|
|
57
|
+
self._aligner: "CacheAligner | None" = None
|
|
58
|
+
self._config_store: ConfigStore | None = None
|
|
59
|
+
self._metrics_counters: Any = None
|
|
60
|
+
|
|
61
|
+
# ── CompressHook Protocol (INTERFACE-CONTRACT §3) ──────────────────────
|
|
62
|
+
|
|
63
|
+
def compress(self, req: ProxyRequest) -> ProxyRequest:
|
|
64
|
+
"""Compress request body. Called by proxy after cache miss.
|
|
65
|
+
|
|
66
|
+
CONTRACT §3: compress(req: ProxyRequest) -> ProxyRequest (NOT ctx → CompressResult).
|
|
67
|
+
Returns req unchanged on error / no-op (fail-open).
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
cfg = self._get_config()
|
|
71
|
+
|
|
72
|
+
if not cfg.compress_enabled:
|
|
73
|
+
return req
|
|
74
|
+
if req.stream:
|
|
75
|
+
return req
|
|
76
|
+
if req.has_tools:
|
|
77
|
+
return req # §6.5 safety rule
|
|
78
|
+
|
|
79
|
+
body = dict(req.body)
|
|
80
|
+
messages = body.get("messages", [])
|
|
81
|
+
if not isinstance(messages, list):
|
|
82
|
+
return req
|
|
83
|
+
|
|
84
|
+
# Step 5: CacheAligner on system prompt (detection only)
|
|
85
|
+
system_text = body.get("system", "")
|
|
86
|
+
if isinstance(system_text, str) and system_text:
|
|
87
|
+
aligner = self._get_aligner()
|
|
88
|
+
align_result = aligner.detect(system_text)
|
|
89
|
+
if align_result.findings:
|
|
90
|
+
logger.warning(
|
|
91
|
+
"[%s] CacheAligner: %d volatile tokens — "
|
|
92
|
+
"provider prefix cache may be unstable: %s",
|
|
93
|
+
req.request_id,
|
|
94
|
+
len(align_result.findings),
|
|
95
|
+
[f.label for f in align_result.findings[:5]],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Step 6: compress each content block
|
|
99
|
+
aggressive = cfg.compress_mode == "aggressive"
|
|
100
|
+
protect_recent = cfg.compress_protect_recent
|
|
101
|
+
new_messages, tokens_before, tokens_after, strategy = self._compress_messages(
|
|
102
|
+
messages=messages,
|
|
103
|
+
aggressive=aggressive,
|
|
104
|
+
protect_recent=protect_recent,
|
|
105
|
+
request_id=req.request_id,
|
|
106
|
+
model=body.get("model", ""),
|
|
107
|
+
tenant_id="default",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if tokens_after >= tokens_before:
|
|
111
|
+
return req # no improvement
|
|
112
|
+
|
|
113
|
+
body["messages"] = new_messages
|
|
114
|
+
new_bytes = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
|
|
115
|
+
|
|
116
|
+
# CONTRACT §3: fire on_compress metrics callback
|
|
117
|
+
lossy = strategy == "llmlingua2_prose"
|
|
118
|
+
self.on_compress(tokens_before, tokens_after, lossy)
|
|
119
|
+
|
|
120
|
+
return ProxyRequest(
|
|
121
|
+
provider=req.provider,
|
|
122
|
+
method=req.method,
|
|
123
|
+
path=req.path,
|
|
124
|
+
headers=req.headers,
|
|
125
|
+
body=body,
|
|
126
|
+
body_bytes=new_bytes,
|
|
127
|
+
request_id=req.request_id,
|
|
128
|
+
stream=req.stream,
|
|
129
|
+
has_tools=req.has_tools,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.warning("[%s] CompressRouter.compress failed (fail-open): %s",
|
|
134
|
+
req.request_id if hasattr(req, 'request_id') else '?', exc)
|
|
135
|
+
return req
|
|
136
|
+
|
|
137
|
+
def on_compress(self, before_tokens: int, after_tokens: int, lossy: bool) -> None:
|
|
138
|
+
"""Metrics callback — CONTRACT §3. MUST NOT raise."""
|
|
139
|
+
try:
|
|
140
|
+
saved = max(0, before_tokens - after_tokens)
|
|
141
|
+
if self._metrics_counters is not None:
|
|
142
|
+
# M-02: Call proper method instead of accessing private attribute
|
|
143
|
+
self._metrics_counters.on_compress(saved, lossy)
|
|
144
|
+
logger.debug("on_compress: saved=%d tokens lossy=%s", saved, lossy)
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
logger.debug("on_compress metrics update failed (non-fatal): %s", exc)
|
|
147
|
+
|
|
148
|
+
def set_metrics(self, counters: Any) -> None:
|
|
149
|
+
self._metrics_counters = counters
|
|
150
|
+
|
|
151
|
+
# ── Internal routing ──────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def _compress_messages(
|
|
154
|
+
self,
|
|
155
|
+
messages: list[dict[str, Any]],
|
|
156
|
+
aggressive: bool,
|
|
157
|
+
protect_recent: int,
|
|
158
|
+
request_id: str,
|
|
159
|
+
model: str,
|
|
160
|
+
tenant_id: str,
|
|
161
|
+
) -> tuple[list[dict[str, Any]], int, int, str]:
|
|
162
|
+
total_before = 0
|
|
163
|
+
total_after = 0
|
|
164
|
+
primary_strategy = "none"
|
|
165
|
+
new_messages: list[dict[str, Any]] = []
|
|
166
|
+
|
|
167
|
+
protect_indices = set(range(max(0, len(messages) - protect_recent), len(messages)))
|
|
168
|
+
|
|
169
|
+
for idx, msg in enumerate(messages):
|
|
170
|
+
role = msg.get("role", "")
|
|
171
|
+
is_tool_msg = (
|
|
172
|
+
role == "tool"
|
|
173
|
+
or role == "user"
|
|
174
|
+
or _msg_has_tool_result(msg)
|
|
175
|
+
)
|
|
176
|
+
if idx in protect_indices or is_tool_msg:
|
|
177
|
+
new_messages.append(msg)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
new_content, before, after, strat = self._compress_content_block(
|
|
181
|
+
content=msg.get("content", ""),
|
|
182
|
+
aggressive=aggressive,
|
|
183
|
+
request_id=request_id,
|
|
184
|
+
model=model,
|
|
185
|
+
tenant_id=tenant_id,
|
|
186
|
+
)
|
|
187
|
+
total_before += before
|
|
188
|
+
total_after += after
|
|
189
|
+
if strat != "none":
|
|
190
|
+
primary_strategy = strat
|
|
191
|
+
|
|
192
|
+
new_msg = dict(msg)
|
|
193
|
+
new_msg["content"] = new_content
|
|
194
|
+
new_messages.append(new_msg)
|
|
195
|
+
|
|
196
|
+
return new_messages, total_before, total_after, primary_strategy
|
|
197
|
+
|
|
198
|
+
def _compress_content_block(
|
|
199
|
+
self,
|
|
200
|
+
content: Any,
|
|
201
|
+
aggressive: bool,
|
|
202
|
+
request_id: str,
|
|
203
|
+
model: str,
|
|
204
|
+
tenant_id: str,
|
|
205
|
+
) -> tuple[Any, int, int, str]:
|
|
206
|
+
if isinstance(content, str):
|
|
207
|
+
return self._compress_text(content, aggressive, request_id, model, tenant_id)
|
|
208
|
+
|
|
209
|
+
if isinstance(content, list):
|
|
210
|
+
new_blocks: list[Any] = []
|
|
211
|
+
total_before = 0
|
|
212
|
+
total_after = 0
|
|
213
|
+
primary = "none"
|
|
214
|
+
for block in content:
|
|
215
|
+
if not isinstance(block, dict):
|
|
216
|
+
new_blocks.append(block)
|
|
217
|
+
continue
|
|
218
|
+
block_type = block.get("type", "")
|
|
219
|
+
if block_type in ("text", "tool_result"):
|
|
220
|
+
text = block.get("text", "") or _tool_result_text(block)
|
|
221
|
+
if len(text) < _MIN_CHARS_FOR_COMPRESSION:
|
|
222
|
+
new_blocks.append(block)
|
|
223
|
+
continue
|
|
224
|
+
new_text, before, after, strat = self._compress_text(
|
|
225
|
+
text, aggressive, request_id, model, tenant_id
|
|
226
|
+
)
|
|
227
|
+
total_before += before
|
|
228
|
+
total_after += after
|
|
229
|
+
if strat != "none":
|
|
230
|
+
primary = strat
|
|
231
|
+
new_block = dict(block)
|
|
232
|
+
if block_type == "text":
|
|
233
|
+
new_block["text"] = new_text
|
|
234
|
+
else:
|
|
235
|
+
new_block = _set_tool_result_text(new_block, new_text)
|
|
236
|
+
new_blocks.append(new_block)
|
|
237
|
+
else:
|
|
238
|
+
new_blocks.append(block)
|
|
239
|
+
return new_blocks, total_before, total_after, primary
|
|
240
|
+
|
|
241
|
+
return content, 0, 0, "none"
|
|
242
|
+
|
|
243
|
+
def _compress_text(
|
|
244
|
+
self,
|
|
245
|
+
text: str,
|
|
246
|
+
aggressive: bool,
|
|
247
|
+
request_id: str,
|
|
248
|
+
model: str,
|
|
249
|
+
tenant_id: str,
|
|
250
|
+
) -> tuple[str, int, int, str]:
|
|
251
|
+
tokens_before = _token_estimate(text)
|
|
252
|
+
|
|
253
|
+
# JSON detection
|
|
254
|
+
stripped = text.strip()
|
|
255
|
+
if stripped.startswith(("{", "[")):
|
|
256
|
+
try:
|
|
257
|
+
parsed = json.loads(stripped)
|
|
258
|
+
compressor = self._get_json_compressor()
|
|
259
|
+
compressed = compressor.compress(parsed)
|
|
260
|
+
tokens_after = _token_estimate_structured(compressed)
|
|
261
|
+
tokens_before_adj = _token_estimate_structured(text)
|
|
262
|
+
ratio = tokens_after / tokens_before_adj if tokens_before_adj else 1.0
|
|
263
|
+
if ratio < _MIN_RATIO_STRUCTURED:
|
|
264
|
+
# B-03: store-before-compress
|
|
265
|
+
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
266
|
+
if ccr_id:
|
|
267
|
+
try:
|
|
268
|
+
obj = json.loads(compressed)
|
|
269
|
+
if isinstance(obj, dict):
|
|
270
|
+
obj["__slm_ccr__"] = ccr_id
|
|
271
|
+
compressed = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
272
|
+
elif isinstance(obj, list):
|
|
273
|
+
# RB-02: list-root embedding
|
|
274
|
+
wrapper = {"__slm_ccr__": ccr_id, "__slm_data__": obj}
|
|
275
|
+
compressed = json.dumps(wrapper, ensure_ascii=False, separators=(",", ":"))
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
279
|
+
logger.debug("[%s] JSON compressed %.2f ratio ccr_id=%s", request_id, ratio, ccr_id)
|
|
280
|
+
return compressed, tokens_before_adj, tokens_after, "extractive_json"
|
|
281
|
+
return text, tokens_before, tokens_before, "none"
|
|
282
|
+
except (json.JSONDecodeError, Exception):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
# Code detection
|
|
286
|
+
lang = _detect_language(text)
|
|
287
|
+
if lang is not None:
|
|
288
|
+
compressor = self._get_code_compressor()
|
|
289
|
+
# RB-03: compress first, compute ratio, then store CCR only if beneficial
|
|
290
|
+
compressed_probe = compressor.compress(text, language=lang, ccr_id="")
|
|
291
|
+
tokens_after = _token_estimate(compressed_probe)
|
|
292
|
+
ratio = tokens_after / tokens_before if tokens_before else 1.0
|
|
293
|
+
if ratio < _MIN_RATIO_STRUCTURED:
|
|
294
|
+
# B-03: store-before-compress
|
|
295
|
+
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
296
|
+
compressed = compressor.compress(text, language=lang, ccr_id=ccr_id)
|
|
297
|
+
if ccr_id:
|
|
298
|
+
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
299
|
+
logger.debug("[%s] Code compressed lang=%s ratio=%.2f ccr_id=%s",
|
|
300
|
+
request_id, lang, ratio, ccr_id)
|
|
301
|
+
return compressed, tokens_before, tokens_after, "extractive_code"
|
|
302
|
+
return text, tokens_before, tokens_before, "none"
|
|
303
|
+
|
|
304
|
+
# Prose: only if aggressive mode AND compress_prose is enabled
|
|
305
|
+
# (Phase 3 — opt-in prose tier; off by default; gated by config
|
|
306
|
+
# field compress_prose added in LLD-04 INTERFACE-CONTRACT v2.)
|
|
307
|
+
cfg = self._get_config()
|
|
308
|
+
prose_enabled = bool(getattr(cfg, "compress_prose", False))
|
|
309
|
+
if aggressive and prose_enabled:
|
|
310
|
+
compressor = self._get_llmlingua_compressor()
|
|
311
|
+
if compressor is not None:
|
|
312
|
+
# B-03: store-before-compress
|
|
313
|
+
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
314
|
+
compressed = compressor.compress(text)
|
|
315
|
+
if ccr_id:
|
|
316
|
+
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
317
|
+
tokens_after = _token_estimate(compressed)
|
|
318
|
+
logger.info(
|
|
319
|
+
"[%s] LLMLingua-2 prose compressed rate=%.2f ccr_id=%s (LOSSY)",
|
|
320
|
+
request_id,
|
|
321
|
+
tokens_after / tokens_before if tokens_before else 1.0,
|
|
322
|
+
ccr_id,
|
|
323
|
+
)
|
|
324
|
+
return compressed, tokens_before, tokens_after, "llmlingua2_prose"
|
|
325
|
+
|
|
326
|
+
return text, tokens_before, tokens_before, "none"
|
|
327
|
+
|
|
328
|
+
# ── Lazy loaders ─────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
def _get_json_compressor(self) -> "JSONCompressor":
|
|
331
|
+
if self._json_compressor is None:
|
|
332
|
+
from superlocalmemory.optimize.compress.extractive_json import JSONCompressor
|
|
333
|
+
self._json_compressor = JSONCompressor()
|
|
334
|
+
return self._json_compressor
|
|
335
|
+
|
|
336
|
+
def _get_code_compressor(self) -> "CodeCompressor":
|
|
337
|
+
if self._code_compressor is None:
|
|
338
|
+
from superlocalmemory.optimize.compress.extractive_code import CodeCompressor
|
|
339
|
+
self._code_compressor = CodeCompressor()
|
|
340
|
+
return self._code_compressor
|
|
341
|
+
|
|
342
|
+
def _get_llmlingua_compressor(self) -> "LLMLinguaCompressor | None":
|
|
343
|
+
if self._llmlingua_compressor is None:
|
|
344
|
+
try:
|
|
345
|
+
from superlocalmemory.optimize.compress.prose_llmlingua import LLMLinguaCompressor
|
|
346
|
+
self._llmlingua_compressor = LLMLinguaCompressor()
|
|
347
|
+
except ImportError:
|
|
348
|
+
logger.warning("LLMLinguaCompressor not available — prose compression disabled")
|
|
349
|
+
return None
|
|
350
|
+
return self._llmlingua_compressor
|
|
351
|
+
|
|
352
|
+
def _get_ccr_store(self) -> "CCRStore":
|
|
353
|
+
if self._ccr_store is None:
|
|
354
|
+
from superlocalmemory.optimize.compress.ccr import CCRStore
|
|
355
|
+
self._ccr_store = CCRStore()
|
|
356
|
+
return self._ccr_store
|
|
357
|
+
|
|
358
|
+
def _get_aligner(self) -> "CacheAligner":
|
|
359
|
+
if self._aligner is None:
|
|
360
|
+
from superlocalmemory.optimize.compress.align import CacheAligner
|
|
361
|
+
self._aligner = CacheAligner()
|
|
362
|
+
return self._aligner
|
|
363
|
+
|
|
364
|
+
def _get_config(self):
|
|
365
|
+
if self._config_store is None:
|
|
366
|
+
self._config_store = ConfigStore()
|
|
367
|
+
return self._config_store.get()
|
|
368
|
+
|
|
369
|
+
# ── CCR helpers ───────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def _ccr_store_original(self, original_bytes: bytes, model: str, tenant_id: str) -> str:
|
|
372
|
+
"""B-03: Store original BEFORE compression. Returns ccr_id or '' on failure."""
|
|
373
|
+
try:
|
|
374
|
+
store = self._get_ccr_store()
|
|
375
|
+
return store.store(original=original_bytes, model=model, tenant_id=tenant_id)
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
logger.warning("CCR store failed (non-fatal): %s", exc)
|
|
378
|
+
return ""
|
|
379
|
+
|
|
380
|
+
def _ccr_update_compressed(self, ccr_id: str, compressed_bytes: bytes) -> None:
|
|
381
|
+
"""B-03: Update CCR row with compressed bytes after compression completes."""
|
|
382
|
+
try:
|
|
383
|
+
store = self._get_ccr_store()
|
|
384
|
+
store.update_compressed(ccr_id, compressed_bytes)
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
logger.debug("CCR update_compressed failed (non-fatal): %s", exc)
|
|
387
|
+
|
|
388
|
+
# ── Public convenience method (M-06) ──────────────────────────────────
|
|
389
|
+
|
|
390
|
+
def compress_text(self, text: str, strategy: str = "auto") -> "CompressTextResult":
|
|
391
|
+
"""Convenience method for test harness. NEVER raises."""
|
|
392
|
+
try:
|
|
393
|
+
cfg = self._get_config()
|
|
394
|
+
aggressive = cfg.compress_mode == "aggressive"
|
|
395
|
+
compressed, tb, ta, strat = self._compress_text(
|
|
396
|
+
text, aggressive, request_id="eval", model="", tenant_id="default"
|
|
397
|
+
)
|
|
398
|
+
return CompressTextResult(
|
|
399
|
+
compressed_text=compressed, strategy=strat,
|
|
400
|
+
tokens_before=tb, tokens_after=ta,
|
|
401
|
+
)
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
logger.debug("compress_text failed (non-fatal): %s", exc)
|
|
404
|
+
return CompressTextResult(
|
|
405
|
+
compressed_text=text, strategy="none",
|
|
406
|
+
tokens_before=len(text.split()), tokens_after=len(text.split()),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass
|
|
411
|
+
class CompressTextResult:
|
|
412
|
+
compressed_text: str
|
|
413
|
+
strategy: str # "extractive_json" | "extractive_code" | "llmlingua2_prose" | "none"
|
|
414
|
+
tokens_before: int
|
|
415
|
+
tokens_after: int
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ── Module-level helpers ──────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
def _token_estimate(text: str) -> int:
|
|
421
|
+
return len(text.split()) if text else 0
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _token_estimate_structured(text: str) -> int:
|
|
425
|
+
return max(1, len(text) // 4) if text else 0
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _msg_has_tool_result(msg: dict) -> bool:
|
|
429
|
+
"""B-09: Detect historical tool_result blocks in messages."""
|
|
430
|
+
content = msg.get("content", "")
|
|
431
|
+
if isinstance(content, list):
|
|
432
|
+
for block in content:
|
|
433
|
+
if not isinstance(block, dict):
|
|
434
|
+
continue
|
|
435
|
+
if block.get("type") == "tool_result":
|
|
436
|
+
return True
|
|
437
|
+
if "tool_use_id" in block:
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _detect_language(text: str) -> str | None:
|
|
443
|
+
"""Detect programming language. Returns None if ambiguous.
|
|
444
|
+
|
|
445
|
+
B-11: Line-start anchored regex. Threshold = 3 signal lines.
|
|
446
|
+
"""
|
|
447
|
+
import re
|
|
448
|
+
|
|
449
|
+
if not text or len(text) < 50:
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
lines = text.split("\n", 30)
|
|
453
|
+
|
|
454
|
+
if lines and lines[0].startswith("#!"):
|
|
455
|
+
shebang = lines[0].lower()
|
|
456
|
+
if "python" in shebang:
|
|
457
|
+
return "python"
|
|
458
|
+
if "node" in shebang or "javascript" in shebang:
|
|
459
|
+
return "javascript"
|
|
460
|
+
|
|
461
|
+
if lines and lines[0].startswith("```"):
|
|
462
|
+
lang_hint = lines[0][3:].strip().lower()
|
|
463
|
+
_KNOWN = {"python", "javascript", "js", "typescript", "ts", "go", "rust", "java", "cpp", "c++", "c"}
|
|
464
|
+
if lang_hint in _KNOWN:
|
|
465
|
+
if lang_hint in ("cpp", "c++"):
|
|
466
|
+
return "cpp"
|
|
467
|
+
if lang_hint in ("js", "typescript", "ts"):
|
|
468
|
+
return "javascript"
|
|
469
|
+
return lang_hint
|
|
470
|
+
|
|
471
|
+
_LANG_PATTERNS: dict[str, list[str]] = {
|
|
472
|
+
"python": [
|
|
473
|
+
r"^\s*(async\s+)?def\s+\w+\s*\(",
|
|
474
|
+
r"^\s*class\s+\w+[\s:(]",
|
|
475
|
+
r"^\s*import\s+\w+",
|
|
476
|
+
r"^\s*from\s+\w+\s+import\s+",
|
|
477
|
+
],
|
|
478
|
+
"javascript": [
|
|
479
|
+
r"^\s*(async\s+)?function\s+\w+\s*\(",
|
|
480
|
+
r"^\s*class\s+\w+(\s+extends)?",
|
|
481
|
+
r"^\s*const\s+\w+\s*=",
|
|
482
|
+
r"^\s*import\s+.+from\s+['\"]",
|
|
483
|
+
r"^\s*export\s+(default\s+|const\s+|class\s+)",
|
|
484
|
+
],
|
|
485
|
+
"go": [
|
|
486
|
+
r"^\s*func\s+(\(\w[\w\s\*]*\)\s+)?\w+\s*\(",
|
|
487
|
+
r"^\s*package\s+\w+",
|
|
488
|
+
r"^\s*import\s+[\(\"\`]",
|
|
489
|
+
r"^\s*type\s+\w+\s+(struct|interface)\s*\{",
|
|
490
|
+
],
|
|
491
|
+
"rust": [
|
|
492
|
+
r"^\s*(pub\s+)?(async\s+)?fn\s+\w+",
|
|
493
|
+
r"^\s*use\s+\w+",
|
|
494
|
+
r"^\s*(pub\s+)?struct\s+\w+",
|
|
495
|
+
r"^\s*impl(\s+\w+)?\s+",
|
|
496
|
+
],
|
|
497
|
+
"java": [
|
|
498
|
+
r"^\s*(public|private|protected)\s+(static\s+)?\w+\s+\w+\s*\(",
|
|
499
|
+
r"^\s*import\s+[\w\.]+;",
|
|
500
|
+
r"^\s*(public|private|protected)?\s*class\s+\w+",
|
|
501
|
+
],
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
def _count_signals(patterns: list[str]) -> int:
|
|
505
|
+
count = 0
|
|
506
|
+
for line in lines:
|
|
507
|
+
for pat in patterns:
|
|
508
|
+
if re.search(pat, line):
|
|
509
|
+
count += 1
|
|
510
|
+
break
|
|
511
|
+
return count
|
|
512
|
+
|
|
513
|
+
scores: dict[str, int] = {
|
|
514
|
+
lang: _count_signals(patterns)
|
|
515
|
+
for lang, patterns in _LANG_PATTERNS.items()
|
|
516
|
+
}
|
|
517
|
+
best_lang, best_score = max(scores.items(), key=lambda kv: kv[1])
|
|
518
|
+
if best_score >= 3:
|
|
519
|
+
return best_lang
|
|
520
|
+
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _tool_result_text(block: dict) -> str:
|
|
525
|
+
content = block.get("content", "")
|
|
526
|
+
if isinstance(content, str):
|
|
527
|
+
return content
|
|
528
|
+
if isinstance(content, list):
|
|
529
|
+
parts = [b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"]
|
|
530
|
+
return "\n".join(parts)
|
|
531
|
+
return ""
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _set_tool_result_text(block: dict, new_text: str) -> dict:
|
|
535
|
+
content = block.get("content", "")
|
|
536
|
+
if isinstance(content, str):
|
|
537
|
+
return {**block, "content": new_text}
|
|
538
|
+
if isinstance(content, list):
|
|
539
|
+
new_content = []
|
|
540
|
+
replaced = False
|
|
541
|
+
for b in content:
|
|
542
|
+
if not replaced and isinstance(b, dict) and b.get("type") == "text":
|
|
543
|
+
new_content.append({**b, "text": new_text})
|
|
544
|
+
replaced = True
|
|
545
|
+
else:
|
|
546
|
+
new_content.append(b)
|
|
547
|
+
return {**block, "content": new_content}
|
|
548
|
+
return block
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Module-level accessor for OptimizeConfig (INTERFACE-CONTRACT §2).
|
|
2
|
+
|
|
3
|
+
LLD-01 (and all other Optimize LLDs) import get_optimize_config() from this
|
|
4
|
+
module. They NEVER construct ConfigStore themselves or read optimize.json directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from superlocalmemory.optimize.config.schema import OptimizeConfig
|
|
10
|
+
|
|
11
|
+
_store: "ConfigStore | None" = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_optimize_config() -> OptimizeConfig:
|
|
15
|
+
"""Return the current active OptimizeConfig (thread-safe, no I/O)."""
|
|
16
|
+
if _store is None:
|
|
17
|
+
from superlocalmemory.optimize.config.defaults import DEFAULT_OPTIMIZE_CONFIG
|
|
18
|
+
return DEFAULT_OPTIMIZE_CONFIG
|
|
19
|
+
return _store.get()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_optimize_config() -> OptimizeConfig:
|
|
23
|
+
"""Alias of get_optimize_config() — INTERFACE-CONTRACT v2 §2."""
|
|
24
|
+
return get_optimize_config()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _set_config_store(store: "ConfigStore") -> None:
|
|
28
|
+
global _store
|
|
29
|
+
_store = store
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _reset_config_store() -> None:
|
|
33
|
+
"""Reset the module-level store (testing only)."""
|
|
34
|
+
global _store
|
|
35
|
+
_store = None
|