headroom-ai 0.2.13__py3-none-any.whl

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.
Files changed (114) hide show
  1. headroom/__init__.py +212 -0
  2. headroom/cache/__init__.py +76 -0
  3. headroom/cache/anthropic.py +517 -0
  4. headroom/cache/base.py +342 -0
  5. headroom/cache/compression_feedback.py +613 -0
  6. headroom/cache/compression_store.py +814 -0
  7. headroom/cache/dynamic_detector.py +1026 -0
  8. headroom/cache/google.py +884 -0
  9. headroom/cache/openai.py +584 -0
  10. headroom/cache/registry.py +175 -0
  11. headroom/cache/semantic.py +451 -0
  12. headroom/ccr/__init__.py +77 -0
  13. headroom/ccr/context_tracker.py +582 -0
  14. headroom/ccr/mcp_server.py +319 -0
  15. headroom/ccr/response_handler.py +772 -0
  16. headroom/ccr/tool_injection.py +415 -0
  17. headroom/cli.py +219 -0
  18. headroom/client.py +977 -0
  19. headroom/compression/__init__.py +42 -0
  20. headroom/compression/detector.py +424 -0
  21. headroom/compression/handlers/__init__.py +22 -0
  22. headroom/compression/handlers/base.py +219 -0
  23. headroom/compression/handlers/code_handler.py +506 -0
  24. headroom/compression/handlers/json_handler.py +418 -0
  25. headroom/compression/masks.py +345 -0
  26. headroom/compression/universal.py +465 -0
  27. headroom/config.py +474 -0
  28. headroom/exceptions.py +192 -0
  29. headroom/integrations/__init__.py +159 -0
  30. headroom/integrations/agno/__init__.py +53 -0
  31. headroom/integrations/agno/hooks.py +345 -0
  32. headroom/integrations/agno/model.py +625 -0
  33. headroom/integrations/agno/providers.py +154 -0
  34. headroom/integrations/langchain/__init__.py +106 -0
  35. headroom/integrations/langchain/agents.py +326 -0
  36. headroom/integrations/langchain/chat_model.py +1002 -0
  37. headroom/integrations/langchain/langsmith.py +324 -0
  38. headroom/integrations/langchain/memory.py +319 -0
  39. headroom/integrations/langchain/providers.py +200 -0
  40. headroom/integrations/langchain/retriever.py +371 -0
  41. headroom/integrations/langchain/streaming.py +341 -0
  42. headroom/integrations/mcp/__init__.py +37 -0
  43. headroom/integrations/mcp/server.py +533 -0
  44. headroom/memory/__init__.py +37 -0
  45. headroom/memory/extractor.py +390 -0
  46. headroom/memory/fast_store.py +621 -0
  47. headroom/memory/fast_wrapper.py +311 -0
  48. headroom/memory/inline_extractor.py +229 -0
  49. headroom/memory/store.py +434 -0
  50. headroom/memory/worker.py +260 -0
  51. headroom/memory/wrapper.py +321 -0
  52. headroom/models/__init__.py +39 -0
  53. headroom/models/registry.py +687 -0
  54. headroom/parser.py +293 -0
  55. headroom/pricing/__init__.py +51 -0
  56. headroom/pricing/anthropic_prices.py +81 -0
  57. headroom/pricing/litellm_pricing.py +113 -0
  58. headroom/pricing/openai_prices.py +91 -0
  59. headroom/pricing/registry.py +188 -0
  60. headroom/providers/__init__.py +61 -0
  61. headroom/providers/anthropic.py +621 -0
  62. headroom/providers/base.py +131 -0
  63. headroom/providers/cohere.py +362 -0
  64. headroom/providers/google.py +427 -0
  65. headroom/providers/litellm.py +297 -0
  66. headroom/providers/openai.py +566 -0
  67. headroom/providers/openai_compatible.py +521 -0
  68. headroom/proxy/__init__.py +19 -0
  69. headroom/proxy/server.py +2683 -0
  70. headroom/py.typed +0 -0
  71. headroom/relevance/__init__.py +124 -0
  72. headroom/relevance/base.py +106 -0
  73. headroom/relevance/bm25.py +255 -0
  74. headroom/relevance/embedding.py +255 -0
  75. headroom/relevance/hybrid.py +259 -0
  76. headroom/reporting/__init__.py +5 -0
  77. headroom/reporting/generator.py +549 -0
  78. headroom/storage/__init__.py +41 -0
  79. headroom/storage/base.py +125 -0
  80. headroom/storage/jsonl.py +220 -0
  81. headroom/storage/sqlite.py +289 -0
  82. headroom/telemetry/__init__.py +91 -0
  83. headroom/telemetry/collector.py +764 -0
  84. headroom/telemetry/models.py +880 -0
  85. headroom/telemetry/toin.py +1579 -0
  86. headroom/tokenizer.py +80 -0
  87. headroom/tokenizers/__init__.py +75 -0
  88. headroom/tokenizers/base.py +210 -0
  89. headroom/tokenizers/estimator.py +198 -0
  90. headroom/tokenizers/huggingface.py +317 -0
  91. headroom/tokenizers/mistral.py +245 -0
  92. headroom/tokenizers/registry.py +398 -0
  93. headroom/tokenizers/tiktoken_counter.py +248 -0
  94. headroom/transforms/__init__.py +106 -0
  95. headroom/transforms/base.py +57 -0
  96. headroom/transforms/cache_aligner.py +357 -0
  97. headroom/transforms/code_compressor.py +1313 -0
  98. headroom/transforms/content_detector.py +335 -0
  99. headroom/transforms/content_router.py +1158 -0
  100. headroom/transforms/llmlingua_compressor.py +638 -0
  101. headroom/transforms/log_compressor.py +529 -0
  102. headroom/transforms/pipeline.py +297 -0
  103. headroom/transforms/rolling_window.py +350 -0
  104. headroom/transforms/search_compressor.py +365 -0
  105. headroom/transforms/smart_crusher.py +2682 -0
  106. headroom/transforms/text_compressor.py +259 -0
  107. headroom/transforms/tool_crusher.py +338 -0
  108. headroom/utils.py +215 -0
  109. headroom_ai-0.2.13.dist-info/METADATA +315 -0
  110. headroom_ai-0.2.13.dist-info/RECORD +114 -0
  111. headroom_ai-0.2.13.dist-info/WHEEL +4 -0
  112. headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
  113. headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
  114. headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
@@ -0,0 +1,465 @@
1
+ """Universal compressor with ML-based detection and structure preservation.
2
+
3
+ This is the main entry point for compression. It:
4
+ 1. Detects content type using Magika (ML)
5
+ 2. Extracts structure using appropriate handler
6
+ 3. Compresses non-structural content with LLMLingua
7
+ 4. Optionally stores original in CCR for retrieval
8
+
9
+ Usage:
10
+ compressor = UniversalCompressor()
11
+ result = compressor.compress(content)
12
+
13
+ # Result contains:
14
+ # - compressed: The compressed content
15
+ # - compression_ratio: original_tokens / compressed_tokens
16
+ # - content_type: Detected content type
17
+ # - preservation_ratio: Fraction of content preserved as structure
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from collections.abc import Callable
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+ from headroom.compression.detector import (
28
+ ContentType,
29
+ DetectionResult,
30
+ FallbackDetector,
31
+ get_detector,
32
+ )
33
+ from headroom.compression.handlers.base import (
34
+ NoOpHandler,
35
+ StructureHandler,
36
+ )
37
+ from headroom.compression.handlers.code_handler import CodeStructureHandler
38
+ from headroom.compression.handlers.json_handler import JSONStructureHandler
39
+ from headroom.compression.masks import (
40
+ StructureMask,
41
+ compute_entropy_mask,
42
+ mask_to_spans,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ @dataclass
49
+ class UniversalCompressorConfig:
50
+ """Configuration for UniversalCompressor.
51
+
52
+ Attributes:
53
+ use_magika: Use ML-based detection (requires magika package).
54
+ use_llmlingua: Use LLMLingua for content compression.
55
+ use_entropy_preservation: Preserve high-entropy tokens (UUIDs, etc.).
56
+ entropy_threshold: Threshold for entropy-based preservation.
57
+ min_content_length: Minimum content length to compress.
58
+ compression_ratio_target: Target compression ratio (0.0-1.0).
59
+ ccr_enabled: Store originals in CCR for retrieval.
60
+ """
61
+
62
+ use_magika: bool = True
63
+ use_llmlingua: bool = True
64
+ use_entropy_preservation: bool = True
65
+ entropy_threshold: float = 0.85
66
+ min_content_length: int = 100
67
+ compression_ratio_target: float = 0.3 # Target 70% reduction
68
+ ccr_enabled: bool = True
69
+
70
+
71
+ @dataclass
72
+ class CompressionResult:
73
+ """Result from compression.
74
+
75
+ Attributes:
76
+ compressed: The compressed content.
77
+ original: The original content (for reference).
78
+ compression_ratio: compressed_length / original_length.
79
+ tokens_before: Estimated token count before compression.
80
+ tokens_after: Estimated token count after compression.
81
+ content_type: Detected content type.
82
+ detection_confidence: Confidence of content type detection.
83
+ handler_used: Name of structure handler used.
84
+ preservation_ratio: Fraction of content marked as structural.
85
+ ccr_key: CCR storage key (if CCR enabled).
86
+ metadata: Additional metadata.
87
+ """
88
+
89
+ compressed: str
90
+ original: str
91
+ compression_ratio: float
92
+ tokens_before: int
93
+ tokens_after: int
94
+ content_type: ContentType
95
+ detection_confidence: float
96
+ handler_used: str
97
+ preservation_ratio: float
98
+ ccr_key: str | None = None
99
+ metadata: dict = field(default_factory=dict)
100
+
101
+ @property
102
+ def tokens_saved(self) -> int:
103
+ """Number of tokens saved."""
104
+ return max(0, self.tokens_before - self.tokens_after)
105
+
106
+ @property
107
+ def savings_percentage(self) -> float:
108
+ """Percentage of tokens saved."""
109
+ if self.tokens_before == 0:
110
+ return 0.0
111
+ return (self.tokens_saved / self.tokens_before) * 100
112
+
113
+
114
+ class UniversalCompressor:
115
+ """Universal compressor with ML detection and structure preservation.
116
+
117
+ This compressor automatically:
118
+ 1. Detects content type (JSON, code, logs, text) using ML
119
+ 2. Extracts structure (keys, signatures, templates)
120
+ 3. Preserves structure while compressing content
121
+ 4. Stores original for CCR retrieval
122
+
123
+ Example:
124
+ >>> compressor = UniversalCompressor()
125
+ >>> result = compressor.compress('{"users": [{"id": 1, "name": "Alice"}]}')
126
+ >>> print(result.content_type) # ContentType.JSON
127
+ >>> print(result.compressed) # Structure preserved, values compressed
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ config: UniversalCompressorConfig | None = None,
133
+ handlers: dict[ContentType, StructureHandler] | None = None,
134
+ compress_fn: Callable[[str], str] | None = None,
135
+ ):
136
+ """Initialize the compressor.
137
+
138
+ Args:
139
+ config: Compression configuration.
140
+ handlers: Custom handlers for content types.
141
+ compress_fn: Custom compression function. If None, uses
142
+ LLMLingua when available, else simple truncation.
143
+ """
144
+ self.config = config or UniversalCompressorConfig()
145
+
146
+ # Initialize detector
147
+ if self.config.use_magika:
148
+ self._detector = get_detector(prefer_magika=True)
149
+ else:
150
+ self._detector = FallbackDetector()
151
+
152
+ # Initialize handlers
153
+ self._handlers: dict[ContentType, StructureHandler] = handlers or {
154
+ ContentType.JSON: JSONStructureHandler(),
155
+ ContentType.CODE: CodeStructureHandler(),
156
+ }
157
+ self._noop_handler = NoOpHandler()
158
+
159
+ # Initialize compression function
160
+ self._compress_fn = compress_fn or self._get_default_compress_fn()
161
+
162
+ # CCR store (lazy initialized)
163
+ self._ccr_store: Any | None = None
164
+
165
+ def _get_default_compress_fn(self) -> Callable[[str], str]:
166
+ """Get default compression function.
167
+
168
+ Returns LLMLingua wrapper if available, else simple truncation.
169
+ """
170
+ if self.config.use_llmlingua:
171
+ try:
172
+ return self._llmlingua_compress
173
+ except ImportError:
174
+ logger.info("LLMLingua not available, using simple compression")
175
+
176
+ return self._simple_compress
177
+
178
+ def _llmlingua_compress(self, text: str) -> str:
179
+ """Compress using LLMLingua.
180
+
181
+ Args:
182
+ text: Text to compress.
183
+
184
+ Returns:
185
+ Compressed text.
186
+ """
187
+ try:
188
+ from headroom.transforms.llmlingua_compressor import compress_with_llmlingua
189
+
190
+ return compress_with_llmlingua(
191
+ text,
192
+ compression_rate=self.config.compression_ratio_target,
193
+ )
194
+ except ImportError:
195
+ return self._simple_compress(text)
196
+ except Exception as e:
197
+ logger.warning("LLMLingua compression failed: %s", e)
198
+ return self._simple_compress(text)
199
+
200
+ def _simple_compress(self, text: str) -> str:
201
+ """Simple compression fallback (truncation with indicator).
202
+
203
+ Args:
204
+ text: Text to compress.
205
+
206
+ Returns:
207
+ Truncated text with indicator.
208
+ """
209
+ target_len = int(len(text) * self.config.compression_ratio_target)
210
+ if len(text) <= target_len:
211
+ return text
212
+
213
+ # Keep first and last portions
214
+ keep_start = target_len * 2 // 3
215
+ keep_end = target_len // 3
216
+
217
+ return text[:keep_start] + "\n...[compressed]...\n" + text[-keep_end:]
218
+
219
+ def compress(
220
+ self,
221
+ content: str,
222
+ content_type: ContentType | None = None,
223
+ **kwargs: Any,
224
+ ) -> CompressionResult:
225
+ """Compress content with structure preservation.
226
+
227
+ Args:
228
+ content: Content to compress.
229
+ content_type: Override content type detection.
230
+ **kwargs: Handler-specific options.
231
+
232
+ Returns:
233
+ CompressionResult with compressed content and metadata.
234
+ """
235
+ # Handle empty/short content
236
+ if not content or len(content) < self.config.min_content_length:
237
+ return CompressionResult(
238
+ compressed=content,
239
+ original=content,
240
+ compression_ratio=1.0,
241
+ tokens_before=self._estimate_tokens(content),
242
+ tokens_after=self._estimate_tokens(content),
243
+ content_type=ContentType.UNKNOWN,
244
+ detection_confidence=0.0,
245
+ handler_used="none",
246
+ preservation_ratio=1.0,
247
+ metadata={"skipped": "content too short"},
248
+ )
249
+
250
+ # Detect content type
251
+ if content_type is None:
252
+ detection = self._detector.detect(content)
253
+ else:
254
+ detection = DetectionResult(
255
+ content_type=content_type,
256
+ confidence=1.0,
257
+ raw_label="override",
258
+ )
259
+
260
+ # Get handler for content type
261
+ handler = self._handlers.get(detection.content_type, self._noop_handler)
262
+
263
+ # Tokenize content (character-level for masks)
264
+ tokens = list(content)
265
+
266
+ # Get structure mask from handler
267
+ handler_result = handler.get_mask(content, tokens, **kwargs)
268
+ structure_mask = handler_result.mask
269
+
270
+ # Optionally add entropy-based preservation
271
+ if self.config.use_entropy_preservation:
272
+ entropy_mask = compute_entropy_mask(
273
+ tokens,
274
+ threshold=self.config.entropy_threshold,
275
+ )
276
+ # Union: preserve if either mask says preserve
277
+ structure_mask = structure_mask.union(entropy_mask)
278
+
279
+ # Apply compression to non-structural parts
280
+ compressed = self._compress_with_mask(content, structure_mask)
281
+
282
+ # Estimate tokens
283
+ tokens_before = self._estimate_tokens(content)
284
+ tokens_after = self._estimate_tokens(compressed)
285
+
286
+ # Store in CCR if enabled
287
+ ccr_key = None
288
+ if self.config.ccr_enabled:
289
+ ccr_key = self._store_in_ccr(content, compressed)
290
+
291
+ return CompressionResult(
292
+ compressed=compressed,
293
+ original=content,
294
+ compression_ratio=len(compressed) / len(content) if content else 1.0,
295
+ tokens_before=tokens_before,
296
+ tokens_after=tokens_after,
297
+ content_type=detection.content_type,
298
+ detection_confidence=detection.confidence,
299
+ handler_used=handler_result.handler_name,
300
+ preservation_ratio=structure_mask.preservation_ratio,
301
+ ccr_key=ccr_key,
302
+ metadata={
303
+ "detection": {
304
+ "raw_label": detection.raw_label,
305
+ "language": detection.language,
306
+ },
307
+ "handler": handler_result.metadata,
308
+ },
309
+ )
310
+
311
+ def _compress_with_mask(self, content: str, mask: StructureMask) -> str:
312
+ """Apply compression respecting structure mask.
313
+
314
+ Args:
315
+ content: Original content.
316
+ mask: Structure mask.
317
+
318
+ Returns:
319
+ Compressed content with structure preserved.
320
+ """
321
+ spans = mask_to_spans(mask)
322
+ result_parts: list[str] = []
323
+
324
+ for span in spans:
325
+ span_content = content[span.start : span.end]
326
+
327
+ if span.is_structural:
328
+ # Preserve structural content
329
+ result_parts.append(span_content)
330
+ else:
331
+ # Compress non-structural content
332
+ if len(span_content) > 50: # Only compress if substantial
333
+ compressed = self._compress_fn(span_content)
334
+ result_parts.append(compressed)
335
+ else:
336
+ result_parts.append(span_content)
337
+
338
+ return "".join(result_parts)
339
+
340
+ def _estimate_tokens(self, text: str) -> int:
341
+ """Estimate token count.
342
+
343
+ Uses simple heuristic: ~4 characters per token.
344
+
345
+ Args:
346
+ text: Text to estimate.
347
+
348
+ Returns:
349
+ Estimated token count.
350
+ """
351
+ if not text:
352
+ return 0
353
+ # Simple estimation: ~4 chars per token on average
354
+ return len(text) // 4
355
+
356
+ def _store_in_ccr(self, original: str, compressed: str) -> str | None:
357
+ """Store original in CCR for retrieval.
358
+
359
+ Args:
360
+ original: Original content.
361
+ compressed: Compressed content.
362
+
363
+ Returns:
364
+ CCR key if stored, None otherwise.
365
+ """
366
+ try:
367
+ if self._ccr_store is None:
368
+ from headroom.cache.compression_store import CompressionStore
369
+
370
+ self._ccr_store = CompressionStore()
371
+
372
+ key = self._ccr_store.store(
373
+ original,
374
+ compressed,
375
+ original_tokens=self._estimate_tokens(original),
376
+ compressed_tokens=self._estimate_tokens(compressed),
377
+ )
378
+ return key
379
+ except ImportError:
380
+ logger.debug("CCR store not available")
381
+ return None
382
+ except Exception as e:
383
+ logger.warning("Failed to store in CCR: %s", e)
384
+ return None
385
+
386
+ def compress_batch(
387
+ self,
388
+ contents: list[str],
389
+ **kwargs: Any,
390
+ ) -> list[CompressionResult]:
391
+ """Compress multiple contents.
392
+
393
+ More efficient than calling compress() in a loop for
394
+ ML detection.
395
+
396
+ Args:
397
+ contents: List of contents to compress.
398
+ **kwargs: Handler-specific options.
399
+
400
+ Returns:
401
+ List of CompressionResults.
402
+ """
403
+ if not contents:
404
+ return []
405
+
406
+ # Batch detection
407
+ if hasattr(self._detector, "detect_batch"):
408
+ detections = self._detector.detect_batch(contents)
409
+ else:
410
+ detections = [self._detector.detect(c) for c in contents]
411
+
412
+ # Compress each with detected type
413
+ results = []
414
+ for content, detection in zip(contents, detections):
415
+ result = self.compress(
416
+ content,
417
+ content_type=detection.content_type,
418
+ **kwargs,
419
+ )
420
+ results.append(result)
421
+
422
+ return results
423
+
424
+ def get_handler(self, content_type: ContentType) -> StructureHandler:
425
+ """Get handler for content type.
426
+
427
+ Args:
428
+ content_type: Content type.
429
+
430
+ Returns:
431
+ Handler for the content type.
432
+ """
433
+ return self._handlers.get(content_type, self._noop_handler)
434
+
435
+ def register_handler(
436
+ self,
437
+ content_type: ContentType,
438
+ handler: StructureHandler,
439
+ ) -> None:
440
+ """Register a custom handler for a content type.
441
+
442
+ Args:
443
+ content_type: Content type to handle.
444
+ handler: Handler instance.
445
+ """
446
+ self._handlers[content_type] = handler
447
+
448
+
449
+ def compress(content: str, **kwargs: Any) -> CompressionResult:
450
+ """Convenience function for one-off compression.
451
+
452
+ Args:
453
+ content: Content to compress.
454
+ **kwargs: Passed to UniversalCompressor.compress().
455
+
456
+ Returns:
457
+ CompressionResult.
458
+
459
+ Example:
460
+ >>> from headroom.compression import compress
461
+ >>> result = compress('{"users": [{"id": 1}, {"id": 2}]}')
462
+ >>> print(result.compressed)
463
+ """
464
+ compressor = UniversalCompressor()
465
+ return compressor.compress(content, **kwargs)