Spaces:
Build error
Add Kompress: ModernBERT token compressor replacing LLMLingua-2
Browse filesAdds kompress_compressor.py β a self-contained ModernBERT-based token
compressor that auto-downloads from chopratejas/kompress-base on HuggingFace.
Trained on 330K structured tool outputs (JSON, diffs, logs, code, SQL,
agentic traces), achieving 82% entity preservation vs LLMLingua-2's 36%.
Changes:
- New: kompress_compressor.py β dual-head ModernBERT (token + span CNN)
with HuggingFace auto-download, no extra pip install needed
- ContentRouter: Kompress is primary ML compressor, LLMLingua-2 is fallback
- fallback_strategy changed from PASSTHROUGH to KOMPRESS β unknown/mixed
content now gets compressed instead of ignored
- No hardcoded compression ratios β model decides per-token importance,
optional target_ratio only when user explicitly sets it via API
- Version bump: 0.3.8 β 0.4.0
|
@@ -142,7 +142,7 @@ from .transforms import (
|
|
| 142 |
TransformPipeline,
|
| 143 |
)
|
| 144 |
|
| 145 |
-
__version__ = "0.
|
| 146 |
|
| 147 |
__all__ = [
|
| 148 |
# Main client
|
|
|
|
| 142 |
TransformPipeline,
|
| 143 |
)
|
| 144 |
|
| 145 |
+
__version__ = "0.4.0"
|
| 146 |
|
| 147 |
__all__ = [
|
| 148 |
# Main client
|
|
@@ -246,6 +246,7 @@ class CompressionStrategy(Enum):
|
|
| 246 |
SMART_CRUSHER = "smart_crusher"
|
| 247 |
SEARCH = "search"
|
| 248 |
LOG = "log"
|
|
|
|
| 249 |
LLMLINGUA = "llmlingua"
|
| 250 |
TEXT = "text"
|
| 251 |
DIFF = "diff"
|
|
@@ -371,6 +372,7 @@ class ContentRouterConfig:
|
|
| 371 |
|
| 372 |
# Enable/disable specific compressors
|
| 373 |
enable_code_aware: bool = True
|
|
|
|
| 374 |
enable_llmlingua: bool = True
|
| 375 |
enable_smart_crusher: bool = True
|
| 376 |
enable_search_compressor: bool = True
|
|
@@ -383,8 +385,8 @@ class ContentRouterConfig:
|
|
| 383 |
mixed_content_threshold: int = 2 # Min types to consider mixed
|
| 384 |
min_section_tokens: int = 20 # Min tokens to compress a section
|
| 385 |
|
| 386 |
-
# Fallback
|
| 387 |
-
fallback_strategy: CompressionStrategy = CompressionStrategy.
|
| 388 |
|
| 389 |
# Protection: Don't compress content that's likely the subject of analysis
|
| 390 |
skip_user_messages: bool = True # User messages contain what they want analyzed
|
|
@@ -633,6 +635,7 @@ class ContentRouter(Transform):
|
|
| 633 |
self._log_compressor: Any = None
|
| 634 |
self._diff_compressor: Any = None
|
| 635 |
self._html_extractor: Any = None
|
|
|
|
| 636 |
self._llmlingua: Any = None
|
| 637 |
self._text_compressor: Any = None
|
| 638 |
self._image_optimizer: Any = None
|
|
@@ -989,13 +992,16 @@ class ContentRouter(Transform):
|
|
| 989 |
# Estimate tokens from extracted text (simple word count)
|
| 990 |
compressed_tokens = len(compressed.split()) if compressed else 0
|
| 991 |
|
|
|
|
|
|
|
|
|
|
| 992 |
elif strategy == CompressionStrategy.LLMLINGUA:
|
| 993 |
-
compressed, compressed_tokens = self.
|
| 994 |
|
| 995 |
elif strategy == CompressionStrategy.TEXT:
|
| 996 |
-
# Prefer
|
| 997 |
-
# Falls back to heuristic TextCompressor if
|
| 998 |
-
compressed, compressed_tokens = self.
|
| 999 |
|
| 1000 |
except Exception as e:
|
| 1001 |
logger.warning("Compression with %s failed: %s", strategy.value, e)
|
|
@@ -1016,10 +1022,13 @@ class ContentRouter(Transform):
|
|
| 1016 |
# Fallback: return unchanged
|
| 1017 |
return content, original_tokens
|
| 1018 |
|
| 1019 |
-
def
|
| 1020 |
self, content: str, context: str, question: str | None = None
|
| 1021 |
) -> tuple[str, int]:
|
| 1022 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 1023 |
|
| 1024 |
Args:
|
| 1025 |
content: Content to compress.
|
|
@@ -1029,6 +1038,17 @@ class ContentRouter(Transform):
|
|
| 1029 |
Returns:
|
| 1030 |
Tuple of (compressed, token_count).
|
| 1031 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
if self.config.enable_llmlingua:
|
| 1033 |
compressor = self._get_llmlingua()
|
| 1034 |
if compressor:
|
|
@@ -1036,16 +1056,13 @@ class ContentRouter(Transform):
|
|
| 1036 |
result = compressor.compress(content, context=context, question=question)
|
| 1037 |
return result.compressed, result.compressed_tokens
|
| 1038 |
except Exception as e:
|
| 1039 |
-
logger.
|
| 1040 |
-
|
| 1041 |
-
# Fallback to text compressor
|
| 1042 |
-
compressor = self._get_text_compressor()
|
| 1043 |
-
if compressor:
|
| 1044 |
-
result = compressor.compress(content, context=context)
|
| 1045 |
-
return result.compressed, result.compressed_line_count
|
| 1046 |
|
| 1047 |
return content, len(content.split())
|
| 1048 |
|
|
|
|
|
|
|
|
|
|
| 1049 |
def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
|
| 1050 |
"""Get strategy from ContentType enum."""
|
| 1051 |
mapping = {
|
|
@@ -1069,6 +1086,7 @@ class ContentRouter(Transform):
|
|
| 1069 |
CompressionStrategy.DIFF: ContentType.GIT_DIFF,
|
| 1070 |
CompressionStrategy.HTML: ContentType.HTML,
|
| 1071 |
CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
|
|
|
|
| 1072 |
CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
|
| 1073 |
CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
|
| 1074 |
}
|
|
@@ -1154,13 +1172,19 @@ class ContentRouter(Transform):
|
|
| 1154 |
def eager_load_compressors(self) -> None:
|
| 1155 |
"""Pre-load compressors at startup to avoid first-request latency.
|
| 1156 |
|
| 1157 |
-
Call this during proxy startup to load
|
| 1158 |
before any requests arrive.
|
| 1159 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
if self.config.enable_llmlingua:
|
| 1161 |
compressor = self._get_llmlingua()
|
| 1162 |
if compressor:
|
| 1163 |
-
# Trigger the underlying model load by accessing it
|
| 1164 |
try:
|
| 1165 |
from .llmlingua_compressor import _get_llmlingua_compressor
|
| 1166 |
|
|
@@ -1170,8 +1194,20 @@ class ContentRouter(Transform):
|
|
| 1170 |
except Exception as e:
|
| 1171 |
logger.warning("Failed to pre-load LLMLingua model: %s", e)
|
| 1172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
def _get_llmlingua(self) -> Any:
|
| 1174 |
-
"""Get LLMLinguaCompressor (lazy load)."""
|
| 1175 |
if self._llmlingua is None:
|
| 1176 |
try:
|
| 1177 |
from .llmlingua_compressor import (
|
|
|
|
| 246 |
SMART_CRUSHER = "smart_crusher"
|
| 247 |
SEARCH = "search"
|
| 248 |
LOG = "log"
|
| 249 |
+
KOMPRESS = "kompress"
|
| 250 |
LLMLINGUA = "llmlingua"
|
| 251 |
TEXT = "text"
|
| 252 |
DIFF = "diff"
|
|
|
|
| 372 |
|
| 373 |
# Enable/disable specific compressors
|
| 374 |
enable_code_aware: bool = True
|
| 375 |
+
enable_kompress: bool = True # Kompress: ModernBERT token compressor (preferred over LLMLingua)
|
| 376 |
enable_llmlingua: bool = True
|
| 377 |
enable_smart_crusher: bool = True
|
| 378 |
enable_search_compressor: bool = True
|
|
|
|
| 385 |
mixed_content_threshold: int = 2 # Min types to consider mixed
|
| 386 |
min_section_tokens: int = 20 # Min tokens to compress a section
|
| 387 |
|
| 388 |
+
# Fallback: Kompress handles unknown/mixed content instead of passing through
|
| 389 |
+
fallback_strategy: CompressionStrategy = CompressionStrategy.KOMPRESS
|
| 390 |
|
| 391 |
# Protection: Don't compress content that's likely the subject of analysis
|
| 392 |
skip_user_messages: bool = True # User messages contain what they want analyzed
|
|
|
|
| 635 |
self._log_compressor: Any = None
|
| 636 |
self._diff_compressor: Any = None
|
| 637 |
self._html_extractor: Any = None
|
| 638 |
+
self._kompress: Any = None
|
| 639 |
self._llmlingua: Any = None
|
| 640 |
self._text_compressor: Any = None
|
| 641 |
self._image_optimizer: Any = None
|
|
|
|
| 992 |
# Estimate tokens from extracted text (simple word count)
|
| 993 |
compressed_tokens = len(compressed.split()) if compressed else 0
|
| 994 |
|
| 995 |
+
elif strategy == CompressionStrategy.KOMPRESS:
|
| 996 |
+
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
|
| 997 |
+
|
| 998 |
elif strategy == CompressionStrategy.LLMLINGUA:
|
| 999 |
+
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
|
| 1000 |
|
| 1001 |
elif strategy == CompressionStrategy.TEXT:
|
| 1002 |
+
# Prefer ML compressor (Kompress > LLMLingua) for text
|
| 1003 |
+
# Falls back to heuristic TextCompressor if neither available
|
| 1004 |
+
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
|
| 1005 |
|
| 1006 |
except Exception as e:
|
| 1007 |
logger.warning("Compression with %s failed: %s", strategy.value, e)
|
|
|
|
| 1022 |
# Fallback: return unchanged
|
| 1023 |
return content, original_tokens
|
| 1024 |
|
| 1025 |
+
def _try_ml_compressor(
|
| 1026 |
self, content: str, context: str, question: str | None = None
|
| 1027 |
) -> tuple[str, int]:
|
| 1028 |
+
"""ML-based compression: Kompress (primary), LLMLingua (fallback only).
|
| 1029 |
+
|
| 1030 |
+
Kompress (ModernBERT, trained on 330K structured tool outputs)
|
| 1031 |
+
auto-downloads from HuggingFace on first use. No heuristic fallback.
|
| 1032 |
|
| 1033 |
Args:
|
| 1034 |
content: Content to compress.
|
|
|
|
| 1038 |
Returns:
|
| 1039 |
Tuple of (compressed, token_count).
|
| 1040 |
"""
|
| 1041 |
+
# Primary: Kompress β downloads from chopratejas/kompress-base on first use
|
| 1042 |
+
if self.config.enable_kompress:
|
| 1043 |
+
compressor = self._get_kompress()
|
| 1044 |
+
if compressor:
|
| 1045 |
+
try:
|
| 1046 |
+
result = compressor.compress(content, context=context, question=question)
|
| 1047 |
+
return result.compressed, result.compressed_tokens
|
| 1048 |
+
except Exception as e:
|
| 1049 |
+
logger.warning("Kompress failed: %s", e)
|
| 1050 |
+
|
| 1051 |
+
# Fallback: LLMLingua (only if Kompress not installed)
|
| 1052 |
if self.config.enable_llmlingua:
|
| 1053 |
compressor = self._get_llmlingua()
|
| 1054 |
if compressor:
|
|
|
|
| 1056 |
result = compressor.compress(content, context=context, question=question)
|
| 1057 |
return result.compressed, result.compressed_tokens
|
| 1058 |
except Exception as e:
|
| 1059 |
+
logger.warning("LLMLingua failed: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
|
| 1061 |
return content, len(content.split())
|
| 1062 |
|
| 1063 |
+
# Backwards compatibility
|
| 1064 |
+
_try_llmlingua = _try_ml_compressor
|
| 1065 |
+
|
| 1066 |
def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
|
| 1067 |
"""Get strategy from ContentType enum."""
|
| 1068 |
mapping = {
|
|
|
|
| 1086 |
CompressionStrategy.DIFF: ContentType.GIT_DIFF,
|
| 1087 |
CompressionStrategy.HTML: ContentType.HTML,
|
| 1088 |
CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
|
| 1089 |
+
CompressionStrategy.KOMPRESS: ContentType.PLAIN_TEXT,
|
| 1090 |
CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
|
| 1091 |
CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
|
| 1092 |
}
|
|
|
|
| 1172 |
def eager_load_compressors(self) -> None:
|
| 1173 |
"""Pre-load compressors at startup to avoid first-request latency.
|
| 1174 |
|
| 1175 |
+
Call this during proxy startup to load models (~5s)
|
| 1176 |
before any requests arrive.
|
| 1177 |
"""
|
| 1178 |
+
# Prefer Kompress (faster, smaller, better on structured data)
|
| 1179 |
+
if self.config.enable_kompress:
|
| 1180 |
+
compressor = self._get_kompress()
|
| 1181 |
+
if compressor:
|
| 1182 |
+
logger.info("Kompress model pre-loaded at startup")
|
| 1183 |
+
return # No need to also load LLMLingua
|
| 1184 |
+
|
| 1185 |
if self.config.enable_llmlingua:
|
| 1186 |
compressor = self._get_llmlingua()
|
| 1187 |
if compressor:
|
|
|
|
| 1188 |
try:
|
| 1189 |
from .llmlingua_compressor import _get_llmlingua_compressor
|
| 1190 |
|
|
|
|
| 1194 |
except Exception as e:
|
| 1195 |
logger.warning("Failed to pre-load LLMLingua model: %s", e)
|
| 1196 |
|
| 1197 |
+
def _get_kompress(self) -> Any:
|
| 1198 |
+
"""Get KompressCompressor (lazy load). Downloads from HuggingFace on first use."""
|
| 1199 |
+
if self._kompress is None:
|
| 1200 |
+
try:
|
| 1201 |
+
from .kompress_compressor import KompressCompressor, is_kompress_available
|
| 1202 |
+
|
| 1203 |
+
if is_kompress_available():
|
| 1204 |
+
self._kompress = KompressCompressor()
|
| 1205 |
+
except ImportError:
|
| 1206 |
+
logger.debug("Kompress dependencies not available")
|
| 1207 |
+
return self._kompress
|
| 1208 |
+
|
| 1209 |
def _get_llmlingua(self) -> Any:
|
| 1210 |
+
"""Get LLMLinguaCompressor (lazy load). Fallback if Kompress unavailable."""
|
| 1211 |
if self._llmlingua is None:
|
| 1212 |
try:
|
| 1213 |
from .llmlingua_compressor import (
|
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Kompress: ModernBERT token compressor for structured tool outputs.
|
| 2 |
+
|
| 3 |
+
Drop-in replacement for LLMLingua-2. Auto-downloads the model from
|
| 4 |
+
HuggingFace (chopratejas/kompress-base) on first use.
|
| 5 |
+
|
| 6 |
+
No extra pip install needed β uses transformers + safetensors
|
| 7 |
+
which are already Headroom dependencies.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
>>> from headroom.transforms.kompress_compressor import KompressCompressor
|
| 11 |
+
>>> compressor = KompressCompressor()
|
| 12 |
+
>>> result = compressor.compress(long_tool_output)
|
| 13 |
+
>>> print(result.compressed)
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import logging
|
| 19 |
+
import threading
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn as nn
|
| 25 |
+
from transformers import AutoModel, AutoTokenizer
|
| 26 |
+
|
| 27 |
+
from ..config import TransformResult
|
| 28 |
+
from ..tokenizer import Tokenizer
|
| 29 |
+
from .base import Transform
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
# HuggingFace model ID
|
| 34 |
+
HF_MODEL_ID = "chopratejas/kompress-base"
|
| 35 |
+
|
| 36 |
+
# Lazy singleton
|
| 37 |
+
_kompress_model = None
|
| 38 |
+
_kompress_tokenizer = None
|
| 39 |
+
_kompress_lock = threading.Lock()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ββ Model Architecture (must match training) ββββββββββββββββββββββββββ
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class HeadroomCompressorModel(nn.Module):
|
| 46 |
+
"""Dual-head ModernBERT: token classification + span importance CNN."""
|
| 47 |
+
|
| 48 |
+
def __init__(self, model_name: str = "answerdotai/ModernBERT-base"):
|
| 49 |
+
super().__init__()
|
| 50 |
+
self.encoder = AutoModel.from_pretrained(model_name, attn_implementation="eager")
|
| 51 |
+
hidden_size = self.encoder.config.hidden_size # 768
|
| 52 |
+
|
| 53 |
+
# Head 1: Token keep/discard
|
| 54 |
+
self.token_dropout = nn.Dropout(0.1)
|
| 55 |
+
self.token_head = nn.Linear(hidden_size, 2)
|
| 56 |
+
|
| 57 |
+
# Head 2: Span importance (1D CNN)
|
| 58 |
+
self.span_conv = nn.Sequential(
|
| 59 |
+
nn.Conv1d(hidden_size, 256, kernel_size=5, padding=2),
|
| 60 |
+
nn.GELU(),
|
| 61 |
+
nn.Conv1d(256, 1, kernel_size=3, padding=1),
|
| 62 |
+
nn.Sigmoid(),
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def get_scores(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
|
| 66 |
+
"""Get per-token compression scores. Higher = more important."""
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
hidden = self.encoder(input_ids, attention_mask=attention_mask).last_hidden_state
|
| 69 |
+
|
| 70 |
+
token_probs = torch.softmax(self.token_head(hidden), dim=-1)[:, :, 1]
|
| 71 |
+
|
| 72 |
+
span_scores = self.span_conv(hidden.transpose(1, 2)).squeeze(1)
|
| 73 |
+
|
| 74 |
+
return token_probs * (0.5 + 0.5 * span_scores)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ββ Model Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _load_kompress(device: str = "auto") -> tuple[HeadroomCompressorModel, Any]:
|
| 81 |
+
"""Download from HuggingFace and load the Kompress model."""
|
| 82 |
+
global _kompress_model, _kompress_tokenizer
|
| 83 |
+
|
| 84 |
+
with _kompress_lock:
|
| 85 |
+
if _kompress_model is not None:
|
| 86 |
+
return _kompress_model, _kompress_tokenizer
|
| 87 |
+
|
| 88 |
+
from huggingface_hub import hf_hub_download
|
| 89 |
+
|
| 90 |
+
logger.info("Downloading Kompress model from %s ...", HF_MODEL_ID)
|
| 91 |
+
|
| 92 |
+
# Download model weights
|
| 93 |
+
weights_path = hf_hub_download(HF_MODEL_ID, "model.safetensors")
|
| 94 |
+
|
| 95 |
+
# Load architecture
|
| 96 |
+
model = HeadroomCompressorModel()
|
| 97 |
+
|
| 98 |
+
# Load trained weights
|
| 99 |
+
from safetensors.torch import load_file
|
| 100 |
+
|
| 101 |
+
state_dict = load_file(weights_path)
|
| 102 |
+
model.load_state_dict(state_dict, strict=False)
|
| 103 |
+
|
| 104 |
+
# Resolve device
|
| 105 |
+
if device == "auto":
|
| 106 |
+
if torch.cuda.is_available():
|
| 107 |
+
device = "cuda"
|
| 108 |
+
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 109 |
+
device = "mps"
|
| 110 |
+
else:
|
| 111 |
+
device = "cpu"
|
| 112 |
+
|
| 113 |
+
model.to(device)
|
| 114 |
+
model.eval()
|
| 115 |
+
logger.info("Kompress model loaded on %s (%s)", device, HF_MODEL_ID)
|
| 116 |
+
|
| 117 |
+
tokenizer = AutoTokenizer.from_pretrained("answerdotai/ModernBERT-base")
|
| 118 |
+
|
| 119 |
+
_kompress_model = model
|
| 120 |
+
_kompress_tokenizer = tokenizer
|
| 121 |
+
return model, tokenizer
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def is_kompress_available() -> bool:
|
| 125 |
+
"""Check if Kompress dependencies are available."""
|
| 126 |
+
try:
|
| 127 |
+
import huggingface_hub # noqa: F401
|
| 128 |
+
import safetensors # noqa: F401
|
| 129 |
+
|
| 130 |
+
return True
|
| 131 |
+
except ImportError:
|
| 132 |
+
return False
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def unload_kompress_model() -> bool:
|
| 136 |
+
"""Unload the Kompress model to free memory."""
|
| 137 |
+
global _kompress_model, _kompress_tokenizer
|
| 138 |
+
with _kompress_lock:
|
| 139 |
+
if _kompress_model is not None:
|
| 140 |
+
_kompress_model = None
|
| 141 |
+
_kompress_tokenizer = None
|
| 142 |
+
if torch.cuda.is_available():
|
| 143 |
+
torch.cuda.empty_cache()
|
| 144 |
+
return True
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# ββ Compressor ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@dataclass
|
| 152 |
+
class KompressConfig:
|
| 153 |
+
"""Minimal config. The model decides what's important β not us."""
|
| 154 |
+
|
| 155 |
+
device: str = "auto"
|
| 156 |
+
enable_ccr: bool = True
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@dataclass
|
| 160 |
+
class KompressResult:
|
| 161 |
+
"""Result of Kompress compression."""
|
| 162 |
+
|
| 163 |
+
compressed: str
|
| 164 |
+
original: str
|
| 165 |
+
original_tokens: int
|
| 166 |
+
compressed_tokens: int
|
| 167 |
+
compression_ratio: float
|
| 168 |
+
cache_key: str | None = None
|
| 169 |
+
model_used: str = HF_MODEL_ID
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def tokens_saved(self) -> int:
|
| 173 |
+
return max(0, self.original_tokens - self.compressed_tokens)
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def savings_percentage(self) -> float:
|
| 177 |
+
if self.original_tokens == 0:
|
| 178 |
+
return 0.0
|
| 179 |
+
return (self.tokens_saved / self.original_tokens) * 100
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class KompressCompressor(Transform):
|
| 183 |
+
"""Kompress: ModernBERT token compressor for structured tool outputs.
|
| 184 |
+
|
| 185 |
+
Auto-downloads chopratejas/kompress-base from HuggingFace on first use.
|
| 186 |
+
Drop-in replacement for LLMLinguaCompressor with identical interface.
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
name: str = "kompress_compressor"
|
| 190 |
+
|
| 191 |
+
def __init__(self, config: KompressConfig | None = None):
|
| 192 |
+
self.config = config or KompressConfig()
|
| 193 |
+
|
| 194 |
+
def compress(
|
| 195 |
+
self,
|
| 196 |
+
content: str,
|
| 197 |
+
context: str = "",
|
| 198 |
+
content_type: str | None = None,
|
| 199 |
+
question: str | None = None,
|
| 200 |
+
target_ratio: float | None = None,
|
| 201 |
+
) -> KompressResult:
|
| 202 |
+
"""Compress content using Kompress model.
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
content: Text to compress.
|
| 206 |
+
context: Optional surrounding context (unused by model, kept for interface compat).
|
| 207 |
+
content_type: Ignored β model decides importance per content type.
|
| 208 |
+
question: Ignored β kept for LLMLingua interface compat.
|
| 209 |
+
target_ratio: If None (default), model decides how much to keep using
|
| 210 |
+
score threshold. If set (e.g. 0.3), forces that keep ratio.
|
| 211 |
+
The proxy never sets this β only user-facing API does.
|
| 212 |
+
|
| 213 |
+
Returns:
|
| 214 |
+
KompressResult with compressed text.
|
| 215 |
+
"""
|
| 216 |
+
words = content.split()
|
| 217 |
+
n_words = len(words)
|
| 218 |
+
|
| 219 |
+
if n_words < 10:
|
| 220 |
+
return self._passthrough(content, n_words)
|
| 221 |
+
|
| 222 |
+
try:
|
| 223 |
+
model, tokenizer = _load_kompress(self.config.device)
|
| 224 |
+
|
| 225 |
+
# Tokenize
|
| 226 |
+
encoding = tokenizer(
|
| 227 |
+
words,
|
| 228 |
+
is_split_into_words=True,
|
| 229 |
+
truncation=True,
|
| 230 |
+
max_length=8192,
|
| 231 |
+
padding=True,
|
| 232 |
+
return_tensors="pt",
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
device = next(model.parameters()).device
|
| 236 |
+
input_ids = encoding["input_ids"].to(device)
|
| 237 |
+
attention_mask = encoding["attention_mask"].to(device)
|
| 238 |
+
|
| 239 |
+
# Get per-token importance scores from dual-head model
|
| 240 |
+
scores = model.get_scores(input_ids, attention_mask)[0].cpu()
|
| 241 |
+
|
| 242 |
+
# Map subword scores to word-level (max pooling)
|
| 243 |
+
word_ids = encoding.word_ids(batch_index=0)
|
| 244 |
+
word_scores: dict[int, float] = {}
|
| 245 |
+
for idx, wid in enumerate(word_ids):
|
| 246 |
+
if wid is None:
|
| 247 |
+
continue
|
| 248 |
+
s = scores[idx].item()
|
| 249 |
+
if wid not in word_scores or s > word_scores[wid]:
|
| 250 |
+
word_scores[wid] = s
|
| 251 |
+
|
| 252 |
+
if not word_scores:
|
| 253 |
+
return self._passthrough(content, n_words)
|
| 254 |
+
|
| 255 |
+
# Token selection
|
| 256 |
+
if target_ratio is not None:
|
| 257 |
+
# User explicitly asked for a specific ratio β use top-k
|
| 258 |
+
sorted_wids = sorted(word_scores, key=lambda w: word_scores[w], reverse=True)
|
| 259 |
+
num_keep = max(1, int(len(sorted_wids) * target_ratio))
|
| 260 |
+
kept_ids = set(sorted_wids[:num_keep])
|
| 261 |
+
else:
|
| 262 |
+
# Model decides. Trained with binary labels β score > 0.5 = keep.
|
| 263 |
+
# Dense content β most tokens score high β keeps more.
|
| 264 |
+
# Boilerplate β most score low β keeps less. That's correct.
|
| 265 |
+
kept_ids = {wid for wid, score in word_scores.items() if score > 0.5}
|
| 266 |
+
if not kept_ids:
|
| 267 |
+
# Edge case: nothing above threshold β keep the single highest
|
| 268 |
+
best = max(word_scores, key=lambda w: word_scores[w])
|
| 269 |
+
kept_ids = {best}
|
| 270 |
+
|
| 271 |
+
# Reconstruct in original word order
|
| 272 |
+
compressed_words = [words[w] for w in sorted(kept_ids) if w < n_words]
|
| 273 |
+
compressed = " ".join(compressed_words)
|
| 274 |
+
compressed_count = len(compressed_words)
|
| 275 |
+
ratio = compressed_count / n_words if n_words else 1.0
|
| 276 |
+
|
| 277 |
+
result = KompressResult(
|
| 278 |
+
compressed=compressed,
|
| 279 |
+
original=content,
|
| 280 |
+
original_tokens=n_words,
|
| 281 |
+
compressed_tokens=compressed_count,
|
| 282 |
+
compression_ratio=ratio,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# CCR marker
|
| 286 |
+
if self.config.enable_ccr and ratio < 0.8:
|
| 287 |
+
cache_key = self._store_in_ccr(content, compressed, n_words)
|
| 288 |
+
if cache_key:
|
| 289 |
+
result.cache_key = cache_key
|
| 290 |
+
result.compressed += (
|
| 291 |
+
f"\n[{n_words} items compressed to {compressed_count}."
|
| 292 |
+
f" Retrieve more: hash={cache_key}]"
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
return result
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.warning("Kompress compression failed: %s", e)
|
| 299 |
+
return self._passthrough(content, n_words)
|
| 300 |
+
|
| 301 |
+
def _passthrough(self, content: str, n_words: int) -> KompressResult:
|
| 302 |
+
return KompressResult(
|
| 303 |
+
compressed=content,
|
| 304 |
+
original=content,
|
| 305 |
+
original_tokens=n_words,
|
| 306 |
+
compressed_tokens=n_words,
|
| 307 |
+
compression_ratio=1.0,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
def apply(
|
| 311 |
+
self,
|
| 312 |
+
messages: list[dict[str, Any]],
|
| 313 |
+
tokenizer: Tokenizer,
|
| 314 |
+
**kwargs: Any,
|
| 315 |
+
) -> TransformResult:
|
| 316 |
+
"""Apply Kompress compression to messages (Transform interface)."""
|
| 317 |
+
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
|
| 318 |
+
transformed = []
|
| 319 |
+
transforms_applied = []
|
| 320 |
+
|
| 321 |
+
for message in messages:
|
| 322 |
+
role = message.get("role", "")
|
| 323 |
+
content = message.get("content", "")
|
| 324 |
+
|
| 325 |
+
if not isinstance(content, str) or len(content.split()) < 10:
|
| 326 |
+
transformed.append(message)
|
| 327 |
+
continue
|
| 328 |
+
|
| 329 |
+
# Compress tool outputs and long assistant messages
|
| 330 |
+
# Model decides how much β no hardcoded ratios
|
| 331 |
+
if role in ("tool", "assistant"):
|
| 332 |
+
result = self.compress(content)
|
| 333 |
+
if result.compression_ratio < 0.9:
|
| 334 |
+
transformed.append({**message, "content": result.compressed})
|
| 335 |
+
transforms_applied.append(f"kompress:{role}:{result.compression_ratio:.2f}")
|
| 336 |
+
else:
|
| 337 |
+
transformed.append(message)
|
| 338 |
+
else:
|
| 339 |
+
transformed.append(message)
|
| 340 |
+
|
| 341 |
+
tokens_after = sum(tokenizer.count_text(str(m.get("content", ""))) for m in transformed)
|
| 342 |
+
|
| 343 |
+
return TransformResult(
|
| 344 |
+
messages=transformed,
|
| 345 |
+
tokens_before=tokens_before,
|
| 346 |
+
tokens_after=tokens_after,
|
| 347 |
+
transforms_applied=transforms_applied or ["kompress:noop"],
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
def _store_in_ccr(self, original: str, compressed: str, original_tokens: int) -> str | None:
|
| 351 |
+
try:
|
| 352 |
+
from ..cache.compression_store import get_compression_store
|
| 353 |
+
|
| 354 |
+
store = get_compression_store()
|
| 355 |
+
return store.store(
|
| 356 |
+
original,
|
| 357 |
+
compressed,
|
| 358 |
+
original_tokens=original_tokens,
|
| 359 |
+
compressed_tokens=len(compressed.split()),
|
| 360 |
+
compression_strategy="kompress",
|
| 361 |
+
)
|
| 362 |
+
except Exception:
|
| 363 |
+
return None
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "headroom-ai"
|
| 7 |
-
version = "0.
|
| 8 |
description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
|
| 9 |
readme = "README.md"
|
| 10 |
license = "Apache-2.0"
|
|
@@ -76,7 +76,7 @@ proxy = [
|
|
| 76 |
reports = [
|
| 77 |
"jinja2>=3.0.0",
|
| 78 |
]
|
| 79 |
-
# ML-based compression (LLMLingua-2)
|
| 80 |
llmlingua = [
|
| 81 |
"llmlingua>=0.2.0",
|
| 82 |
"torch>=2.0.0",
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "headroom-ai"
|
| 7 |
+
version = "0.4.0"
|
| 8 |
description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
|
| 9 |
readme = "README.md"
|
| 10 |
license = "Apache-2.0"
|
|
|
|
| 76 |
reports = [
|
| 77 |
"jinja2>=3.0.0",
|
| 78 |
]
|
| 79 |
+
# ML-based compression (LLMLingua-2 β fallback if Kompress fails)
|
| 80 |
llmlingua = [
|
| 81 |
"llmlingua>=0.2.0",
|
| 82 |
"torch>=2.0.0",
|
|
@@ -155,12 +155,13 @@ class TestContentRouterConfig:
|
|
| 155 |
config = ContentRouterConfig()
|
| 156 |
|
| 157 |
assert config.enable_code_aware is True
|
|
|
|
| 158 |
assert config.enable_llmlingua is True
|
| 159 |
assert config.enable_smart_crusher is True
|
| 160 |
assert config.enable_search_compressor is True
|
| 161 |
assert config.enable_log_compressor is True
|
| 162 |
assert config.min_section_tokens == 20
|
| 163 |
-
assert config.fallback_strategy == CompressionStrategy.
|
| 164 |
|
| 165 |
def test_custom_values(self):
|
| 166 |
"""Custom config values are applied."""
|
|
|
|
| 155 |
config = ContentRouterConfig()
|
| 156 |
|
| 157 |
assert config.enable_code_aware is True
|
| 158 |
+
assert config.enable_kompress is True
|
| 159 |
assert config.enable_llmlingua is True
|
| 160 |
assert config.enable_smart_crusher is True
|
| 161 |
assert config.enable_search_compressor is True
|
| 162 |
assert config.enable_log_compressor is True
|
| 163 |
assert config.min_section_tokens == 20
|
| 164 |
+
assert config.fallback_strategy == CompressionStrategy.KOMPRESS
|
| 165 |
|
| 166 |
def test_custom_values(self):
|
| 167 |
"""Custom config values are applied."""
|