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.
- headroom/__init__.py +212 -0
- headroom/cache/__init__.py +76 -0
- headroom/cache/anthropic.py +517 -0
- headroom/cache/base.py +342 -0
- headroom/cache/compression_feedback.py +613 -0
- headroom/cache/compression_store.py +814 -0
- headroom/cache/dynamic_detector.py +1026 -0
- headroom/cache/google.py +884 -0
- headroom/cache/openai.py +584 -0
- headroom/cache/registry.py +175 -0
- headroom/cache/semantic.py +451 -0
- headroom/ccr/__init__.py +77 -0
- headroom/ccr/context_tracker.py +582 -0
- headroom/ccr/mcp_server.py +319 -0
- headroom/ccr/response_handler.py +772 -0
- headroom/ccr/tool_injection.py +415 -0
- headroom/cli.py +219 -0
- headroom/client.py +977 -0
- headroom/compression/__init__.py +42 -0
- headroom/compression/detector.py +424 -0
- headroom/compression/handlers/__init__.py +22 -0
- headroom/compression/handlers/base.py +219 -0
- headroom/compression/handlers/code_handler.py +506 -0
- headroom/compression/handlers/json_handler.py +418 -0
- headroom/compression/masks.py +345 -0
- headroom/compression/universal.py +465 -0
- headroom/config.py +474 -0
- headroom/exceptions.py +192 -0
- headroom/integrations/__init__.py +159 -0
- headroom/integrations/agno/__init__.py +53 -0
- headroom/integrations/agno/hooks.py +345 -0
- headroom/integrations/agno/model.py +625 -0
- headroom/integrations/agno/providers.py +154 -0
- headroom/integrations/langchain/__init__.py +106 -0
- headroom/integrations/langchain/agents.py +326 -0
- headroom/integrations/langchain/chat_model.py +1002 -0
- headroom/integrations/langchain/langsmith.py +324 -0
- headroom/integrations/langchain/memory.py +319 -0
- headroom/integrations/langchain/providers.py +200 -0
- headroom/integrations/langchain/retriever.py +371 -0
- headroom/integrations/langchain/streaming.py +341 -0
- headroom/integrations/mcp/__init__.py +37 -0
- headroom/integrations/mcp/server.py +533 -0
- headroom/memory/__init__.py +37 -0
- headroom/memory/extractor.py +390 -0
- headroom/memory/fast_store.py +621 -0
- headroom/memory/fast_wrapper.py +311 -0
- headroom/memory/inline_extractor.py +229 -0
- headroom/memory/store.py +434 -0
- headroom/memory/worker.py +260 -0
- headroom/memory/wrapper.py +321 -0
- headroom/models/__init__.py +39 -0
- headroom/models/registry.py +687 -0
- headroom/parser.py +293 -0
- headroom/pricing/__init__.py +51 -0
- headroom/pricing/anthropic_prices.py +81 -0
- headroom/pricing/litellm_pricing.py +113 -0
- headroom/pricing/openai_prices.py +91 -0
- headroom/pricing/registry.py +188 -0
- headroom/providers/__init__.py +61 -0
- headroom/providers/anthropic.py +621 -0
- headroom/providers/base.py +131 -0
- headroom/providers/cohere.py +362 -0
- headroom/providers/google.py +427 -0
- headroom/providers/litellm.py +297 -0
- headroom/providers/openai.py +566 -0
- headroom/providers/openai_compatible.py +521 -0
- headroom/proxy/__init__.py +19 -0
- headroom/proxy/server.py +2683 -0
- headroom/py.typed +0 -0
- headroom/relevance/__init__.py +124 -0
- headroom/relevance/base.py +106 -0
- headroom/relevance/bm25.py +255 -0
- headroom/relevance/embedding.py +255 -0
- headroom/relevance/hybrid.py +259 -0
- headroom/reporting/__init__.py +5 -0
- headroom/reporting/generator.py +549 -0
- headroom/storage/__init__.py +41 -0
- headroom/storage/base.py +125 -0
- headroom/storage/jsonl.py +220 -0
- headroom/storage/sqlite.py +289 -0
- headroom/telemetry/__init__.py +91 -0
- headroom/telemetry/collector.py +764 -0
- headroom/telemetry/models.py +880 -0
- headroom/telemetry/toin.py +1579 -0
- headroom/tokenizer.py +80 -0
- headroom/tokenizers/__init__.py +75 -0
- headroom/tokenizers/base.py +210 -0
- headroom/tokenizers/estimator.py +198 -0
- headroom/tokenizers/huggingface.py +317 -0
- headroom/tokenizers/mistral.py +245 -0
- headroom/tokenizers/registry.py +398 -0
- headroom/tokenizers/tiktoken_counter.py +248 -0
- headroom/transforms/__init__.py +106 -0
- headroom/transforms/base.py +57 -0
- headroom/transforms/cache_aligner.py +357 -0
- headroom/transforms/code_compressor.py +1313 -0
- headroom/transforms/content_detector.py +335 -0
- headroom/transforms/content_router.py +1158 -0
- headroom/transforms/llmlingua_compressor.py +638 -0
- headroom/transforms/log_compressor.py +529 -0
- headroom/transforms/pipeline.py +297 -0
- headroom/transforms/rolling_window.py +350 -0
- headroom/transforms/search_compressor.py +365 -0
- headroom/transforms/smart_crusher.py +2682 -0
- headroom/transforms/text_compressor.py +259 -0
- headroom/transforms/tool_crusher.py +338 -0
- headroom/utils.py +215 -0
- headroom_ai-0.2.13.dist-info/METADATA +315 -0
- headroom_ai-0.2.13.dist-info/RECORD +114 -0
- headroom_ai-0.2.13.dist-info/WHEEL +4 -0
- headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
- headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
- headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
"""Code-aware compressor using AST parsing for syntax-preserving compression.
|
|
2
|
+
|
|
3
|
+
This module provides AST-based compression for source code that guarantees
|
|
4
|
+
valid syntax output. Unlike token-level compression (LLMLingua), this
|
|
5
|
+
preserves structural elements while compressing function bodies.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Syntax validity guaranteed (output always parses)
|
|
9
|
+
- Preserves imports, signatures, type annotations, error handlers
|
|
10
|
+
- Compresses function bodies while maintaining structure
|
|
11
|
+
- Multi-language support via tree-sitter
|
|
12
|
+
|
|
13
|
+
Supported Languages (Tier 1):
|
|
14
|
+
- Python, JavaScript, TypeScript
|
|
15
|
+
|
|
16
|
+
Supported Languages (Tier 2):
|
|
17
|
+
- Go, Rust, Java, C, C++
|
|
18
|
+
|
|
19
|
+
Compression Strategy:
|
|
20
|
+
1. Parse code into AST using tree-sitter
|
|
21
|
+
2. Extract and preserve critical structures (imports, signatures, types)
|
|
22
|
+
3. Rank functions by importance (using perplexity or heuristics)
|
|
23
|
+
4. Compress function bodies while preserving signatures
|
|
24
|
+
5. Reassemble into valid code
|
|
25
|
+
|
|
26
|
+
Installation:
|
|
27
|
+
pip install headroom-ai[code]
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
>>> from headroom.transforms import CodeAwareCompressor
|
|
31
|
+
>>> compressor = CodeAwareCompressor()
|
|
32
|
+
>>> result = compressor.compress(python_code)
|
|
33
|
+
>>> print(result.compressed) # Valid Python code
|
|
34
|
+
>>> print(result.syntax_valid) # True
|
|
35
|
+
|
|
36
|
+
Reference:
|
|
37
|
+
LongCodeZip: Compress Long Context for Code Language Models
|
|
38
|
+
https://arxiv.org/abs/2510.00446
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
import re
|
|
45
|
+
import threading
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from enum import Enum
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from ..config import TransformResult
|
|
51
|
+
from ..tokenizer import Tokenizer
|
|
52
|
+
from .base import Transform
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
# Lazy import for optional dependency
|
|
57
|
+
_tree_sitter_available: bool | None = None
|
|
58
|
+
_tree_sitter_languages: dict[str, Any] = {}
|
|
59
|
+
_tree_sitter_lock = threading.Lock()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _check_tree_sitter_available() -> bool:
|
|
63
|
+
"""Check if tree-sitter packages are available."""
|
|
64
|
+
global _tree_sitter_available
|
|
65
|
+
if _tree_sitter_available is None:
|
|
66
|
+
try:
|
|
67
|
+
import tree_sitter_language_pack # noqa: F401
|
|
68
|
+
|
|
69
|
+
_tree_sitter_available = True
|
|
70
|
+
except ImportError:
|
|
71
|
+
_tree_sitter_available = False
|
|
72
|
+
return _tree_sitter_available
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_parser(language: str) -> Any:
|
|
76
|
+
"""Get a tree-sitter parser for the given language.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
language: Language name (e.g., 'python', 'javascript').
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Configured tree-sitter parser.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ImportError: If tree-sitter is not installed.
|
|
86
|
+
ValueError: If language is not supported.
|
|
87
|
+
"""
|
|
88
|
+
global _tree_sitter_languages
|
|
89
|
+
|
|
90
|
+
if not _check_tree_sitter_available():
|
|
91
|
+
raise ImportError(
|
|
92
|
+
"tree-sitter is not installed. Install with: pip install headroom-ai[code]\n"
|
|
93
|
+
"This adds ~50MB for tree-sitter grammars."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
with _tree_sitter_lock:
|
|
97
|
+
if language not in _tree_sitter_languages:
|
|
98
|
+
try:
|
|
99
|
+
from tree_sitter_language_pack import get_parser
|
|
100
|
+
|
|
101
|
+
parser = get_parser(language) # type: ignore[arg-type]
|
|
102
|
+
_tree_sitter_languages[language] = parser
|
|
103
|
+
logger.debug("Loaded tree-sitter parser for %s", language)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Language '{language}' is not supported by tree-sitter. "
|
|
107
|
+
f"Supported: python, javascript, typescript, go, rust, java, c, cpp. "
|
|
108
|
+
f"Error: {e}"
|
|
109
|
+
) from e
|
|
110
|
+
|
|
111
|
+
return _tree_sitter_languages[language]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_tree_sitter_available() -> bool:
|
|
115
|
+
"""Check if tree-sitter is installed and available.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if tree-sitter-languages package is installed.
|
|
119
|
+
"""
|
|
120
|
+
return _check_tree_sitter_available()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_tree_sitter_loaded() -> bool:
|
|
124
|
+
"""Check if any tree-sitter parsers are currently loaded.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if parsers are loaded in memory.
|
|
128
|
+
"""
|
|
129
|
+
return len(_tree_sitter_languages) > 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def unload_tree_sitter() -> bool:
|
|
133
|
+
"""Unload all tree-sitter parsers to free memory.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if parsers were unloaded, False if none were loaded.
|
|
137
|
+
"""
|
|
138
|
+
global _tree_sitter_languages
|
|
139
|
+
|
|
140
|
+
with _tree_sitter_lock:
|
|
141
|
+
if _tree_sitter_languages:
|
|
142
|
+
count = len(_tree_sitter_languages)
|
|
143
|
+
_tree_sitter_languages.clear()
|
|
144
|
+
logger.info("Unloaded %d tree-sitter parsers", count)
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CodeLanguage(Enum):
|
|
151
|
+
"""Supported programming languages."""
|
|
152
|
+
|
|
153
|
+
PYTHON = "python"
|
|
154
|
+
JAVASCRIPT = "javascript"
|
|
155
|
+
TYPESCRIPT = "typescript"
|
|
156
|
+
GO = "go"
|
|
157
|
+
RUST = "rust"
|
|
158
|
+
JAVA = "java"
|
|
159
|
+
C = "c"
|
|
160
|
+
CPP = "cpp"
|
|
161
|
+
UNKNOWN = "unknown"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class DocstringMode(Enum):
|
|
165
|
+
"""How to handle docstrings."""
|
|
166
|
+
|
|
167
|
+
FULL = "full" # Keep entire docstring
|
|
168
|
+
FIRST_LINE = "first_line" # Keep only first line
|
|
169
|
+
REMOVE = "remove" # Remove docstrings completely
|
|
170
|
+
NONE = "none" # Alias for REMOVE (deprecated)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class CodeStructure:
|
|
175
|
+
"""Extracted structure from parsed code."""
|
|
176
|
+
|
|
177
|
+
imports: list[str] = field(default_factory=list)
|
|
178
|
+
type_definitions: list[str] = field(default_factory=list)
|
|
179
|
+
class_definitions: list[str] = field(default_factory=list)
|
|
180
|
+
function_signatures: list[str] = field(default_factory=list)
|
|
181
|
+
function_bodies: list[tuple[str, str, int]] = field(
|
|
182
|
+
default_factory=list
|
|
183
|
+
) # (signature, body, line)
|
|
184
|
+
decorators: list[str] = field(default_factory=list)
|
|
185
|
+
error_handlers: list[str] = field(default_factory=list)
|
|
186
|
+
comments: list[str] = field(default_factory=list)
|
|
187
|
+
other: list[str] = field(default_factory=list)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class CodeCompressorConfig:
|
|
192
|
+
"""Configuration for code-aware compression.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
preserve_imports: Always keep import statements.
|
|
196
|
+
preserve_signatures: Always keep function/method signatures.
|
|
197
|
+
preserve_type_annotations: Keep type hints and annotations.
|
|
198
|
+
preserve_error_handlers: Keep try/except/finally blocks.
|
|
199
|
+
preserve_decorators: Keep decorators on functions/classes.
|
|
200
|
+
docstring_mode: How to handle docstrings.
|
|
201
|
+
target_compression_rate: Target compression ratio (0.2 = keep 20%).
|
|
202
|
+
max_body_lines: Maximum lines to keep per function body.
|
|
203
|
+
compress_comments: Remove non-docstring comments.
|
|
204
|
+
min_tokens_for_compression: Minimum tokens to trigger compression.
|
|
205
|
+
language_hint: Explicit language (None = auto-detect).
|
|
206
|
+
fallback_to_llmlingua: Use LLMLingua for unknown languages.
|
|
207
|
+
enable_ccr: Store originals for retrieval.
|
|
208
|
+
ccr_ttl: TTL for CCR entries in seconds.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
# Preservation settings
|
|
212
|
+
preserve_imports: bool = True
|
|
213
|
+
preserve_signatures: bool = True
|
|
214
|
+
preserve_type_annotations: bool = True
|
|
215
|
+
preserve_error_handlers: bool = True
|
|
216
|
+
preserve_decorators: bool = True
|
|
217
|
+
docstring_mode: DocstringMode = DocstringMode.FIRST_LINE
|
|
218
|
+
|
|
219
|
+
# Compression settings
|
|
220
|
+
target_compression_rate: float = 0.2
|
|
221
|
+
max_body_lines: int = 5
|
|
222
|
+
compress_comments: bool = True
|
|
223
|
+
|
|
224
|
+
# Thresholds
|
|
225
|
+
min_tokens_for_compression: int = 100
|
|
226
|
+
|
|
227
|
+
# Language handling
|
|
228
|
+
language_hint: str | None = None
|
|
229
|
+
fallback_to_llmlingua: bool = True
|
|
230
|
+
|
|
231
|
+
# CCR integration
|
|
232
|
+
enable_ccr: bool = True
|
|
233
|
+
ccr_ttl: int = 300 # 5 minutes
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class CodeCompressionResult:
|
|
238
|
+
"""Result of code-aware compression.
|
|
239
|
+
|
|
240
|
+
Attributes:
|
|
241
|
+
compressed: The compressed code (guaranteed valid syntax).
|
|
242
|
+
original: Original code before compression.
|
|
243
|
+
original_tokens: Token count before compression.
|
|
244
|
+
compressed_tokens: Token count after compression.
|
|
245
|
+
compression_ratio: Actual compression ratio achieved.
|
|
246
|
+
language: Detected or specified language.
|
|
247
|
+
language_confidence: Confidence in language detection.
|
|
248
|
+
preserved_imports: Number of import statements preserved.
|
|
249
|
+
preserved_signatures: Number of function signatures preserved.
|
|
250
|
+
compressed_bodies: Number of function bodies compressed.
|
|
251
|
+
syntax_valid: Whether output is syntactically valid.
|
|
252
|
+
cache_key: CCR cache key if stored.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
compressed: str
|
|
256
|
+
original: str
|
|
257
|
+
original_tokens: int
|
|
258
|
+
compressed_tokens: int
|
|
259
|
+
compression_ratio: float
|
|
260
|
+
|
|
261
|
+
# Code-specific metadata
|
|
262
|
+
language: CodeLanguage = CodeLanguage.UNKNOWN
|
|
263
|
+
language_confidence: float = 0.0
|
|
264
|
+
|
|
265
|
+
# Structure analysis
|
|
266
|
+
preserved_imports: int = 0
|
|
267
|
+
preserved_signatures: int = 0
|
|
268
|
+
compressed_bodies: int = 0
|
|
269
|
+
|
|
270
|
+
# Validation
|
|
271
|
+
syntax_valid: bool = True
|
|
272
|
+
|
|
273
|
+
# CCR
|
|
274
|
+
cache_key: str | None = None
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def tokens_saved(self) -> int:
|
|
278
|
+
"""Number of tokens saved by compression."""
|
|
279
|
+
return max(0, self.original_tokens - self.compressed_tokens)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def savings_percentage(self) -> float:
|
|
283
|
+
"""Percentage of tokens saved."""
|
|
284
|
+
if self.original_tokens == 0:
|
|
285
|
+
return 0.0
|
|
286
|
+
return (self.tokens_saved / self.original_tokens) * 100
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def summary(self) -> str:
|
|
290
|
+
"""Human-readable summary of compression."""
|
|
291
|
+
return (
|
|
292
|
+
f"Compressed {self.language.value} code: "
|
|
293
|
+
f"{self.original_tokens:,}→{self.compressed_tokens:,} tokens "
|
|
294
|
+
f"({self.savings_percentage:.0f}% saved). "
|
|
295
|
+
f"Kept {self.preserved_imports} imports, "
|
|
296
|
+
f"{self.preserved_signatures} signatures, "
|
|
297
|
+
f"compressed {self.compressed_bodies} bodies."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Language detection patterns
|
|
302
|
+
_LANGUAGE_PATTERNS: dict[CodeLanguage, list[re.Pattern[str]]] = {
|
|
303
|
+
CodeLanguage.PYTHON: [
|
|
304
|
+
re.compile(r"^\s*(def|class|import|from|async def)\s+\w+", re.MULTILINE),
|
|
305
|
+
re.compile(r"^\s*@\w+", re.MULTILINE), # Decorators
|
|
306
|
+
re.compile(r'^\s*"""', re.MULTILINE), # Docstrings
|
|
307
|
+
re.compile(r"^\s*if __name__\s*==", re.MULTILINE),
|
|
308
|
+
],
|
|
309
|
+
CodeLanguage.JAVASCRIPT: [
|
|
310
|
+
re.compile(r"^\s*(function|const|let|var|class|export)\s+\w+", re.MULTILINE),
|
|
311
|
+
re.compile(r"^\s*async\s+(function|=>)", re.MULTILINE),
|
|
312
|
+
re.compile(r"^\s*module\.exports", re.MULTILINE),
|
|
313
|
+
re.compile(r"^\s*(import|export)\s+.*\s+from\s+['\"]", re.MULTILINE),
|
|
314
|
+
],
|
|
315
|
+
CodeLanguage.TYPESCRIPT: [
|
|
316
|
+
re.compile(r"^\s*(interface|type|enum|namespace)\s+\w+", re.MULTILINE),
|
|
317
|
+
re.compile(r":\s*(string|number|boolean|any|void|Promise)\b", re.MULTILINE),
|
|
318
|
+
re.compile(r"<\w+>", re.MULTILINE), # Generic types
|
|
319
|
+
],
|
|
320
|
+
CodeLanguage.GO: [
|
|
321
|
+
re.compile(r"^\s*(func|type|package|import)\s+", re.MULTILINE),
|
|
322
|
+
re.compile(r"^\s*func\s+\([^)]+\)\s+\w+", re.MULTILINE), # Methods
|
|
323
|
+
re.compile(r"\bstruct\s*\{", re.MULTILINE),
|
|
324
|
+
],
|
|
325
|
+
CodeLanguage.RUST: [
|
|
326
|
+
re.compile(r"^\s*(fn|struct|enum|impl|mod|use|pub)\s+", re.MULTILINE),
|
|
327
|
+
re.compile(r"^\s*#\[", re.MULTILINE), # Attributes
|
|
328
|
+
re.compile(r"->\s*\w+", re.MULTILINE), # Return types
|
|
329
|
+
],
|
|
330
|
+
CodeLanguage.JAVA: [
|
|
331
|
+
re.compile(r"^\s*(public|private|protected)\s+(class|interface|enum)", re.MULTILINE),
|
|
332
|
+
re.compile(r"^\s*@\w+", re.MULTILINE), # Annotations
|
|
333
|
+
re.compile(r"^\s*package\s+[\w.]+;", re.MULTILINE),
|
|
334
|
+
],
|
|
335
|
+
CodeLanguage.C: [
|
|
336
|
+
re.compile(r"^\s*#include\s*[<\"]", re.MULTILINE),
|
|
337
|
+
re.compile(r"^\s*(int|void|char|float|double)\s+\w+\s*\(", re.MULTILINE),
|
|
338
|
+
re.compile(r"^\s*typedef\s+", re.MULTILINE),
|
|
339
|
+
],
|
|
340
|
+
CodeLanguage.CPP: [
|
|
341
|
+
re.compile(r"^\s*#include\s*[<\"]", re.MULTILINE),
|
|
342
|
+
re.compile(r"\bclass\s+\w+\s*[:{]", re.MULTILINE),
|
|
343
|
+
re.compile(r"\bnamespace\s+\w+", re.MULTILINE),
|
|
344
|
+
re.compile(r"::\w+", re.MULTILINE), # Scope resolution
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def detect_language(code: str) -> tuple[CodeLanguage, float]:
|
|
350
|
+
"""Detect the programming language of code.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
code: Source code to analyze.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (detected language, confidence score).
|
|
357
|
+
"""
|
|
358
|
+
if not code or not code.strip():
|
|
359
|
+
return CodeLanguage.UNKNOWN, 0.0
|
|
360
|
+
|
|
361
|
+
scores: dict[CodeLanguage, int] = {}
|
|
362
|
+
sample = code[:5000] # Analyze first 5000 chars
|
|
363
|
+
|
|
364
|
+
for lang, patterns in _LANGUAGE_PATTERNS.items():
|
|
365
|
+
score = 0
|
|
366
|
+
for pattern in patterns:
|
|
367
|
+
matches = len(pattern.findall(sample))
|
|
368
|
+
score += matches
|
|
369
|
+
if score > 0:
|
|
370
|
+
scores[lang] = score
|
|
371
|
+
|
|
372
|
+
if not scores:
|
|
373
|
+
return CodeLanguage.UNKNOWN, 0.0
|
|
374
|
+
|
|
375
|
+
# TypeScript is a superset of JavaScript - prefer TS if TS patterns found
|
|
376
|
+
if CodeLanguage.TYPESCRIPT in scores and CodeLanguage.JAVASCRIPT in scores:
|
|
377
|
+
if scores[CodeLanguage.TYPESCRIPT] >= 2:
|
|
378
|
+
scores[CodeLanguage.JAVASCRIPT] = 0
|
|
379
|
+
|
|
380
|
+
# C++ is a superset of C - prefer C++ if C++ patterns found
|
|
381
|
+
if CodeLanguage.CPP in scores and CodeLanguage.C in scores:
|
|
382
|
+
if scores[CodeLanguage.CPP] >= 2:
|
|
383
|
+
scores[CodeLanguage.C] = 0
|
|
384
|
+
|
|
385
|
+
best_lang = max(scores, key=lambda k: scores[k])
|
|
386
|
+
best_score = scores[best_lang]
|
|
387
|
+
|
|
388
|
+
# Calculate confidence (higher score = higher confidence)
|
|
389
|
+
confidence = min(1.0, 0.3 + (best_score * 0.1))
|
|
390
|
+
|
|
391
|
+
return best_lang, confidence
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class CodeAwareCompressor(Transform):
|
|
395
|
+
"""AST-preserving compression for source code.
|
|
396
|
+
|
|
397
|
+
This compressor uses tree-sitter to parse code into an AST, then
|
|
398
|
+
selectively compresses function bodies while preserving structure.
|
|
399
|
+
The output is guaranteed to be syntactically valid.
|
|
400
|
+
|
|
401
|
+
Key advantages over token-level compression:
|
|
402
|
+
- Syntax validity guaranteed
|
|
403
|
+
- Preserves imports, signatures, types, error handlers
|
|
404
|
+
- Better compression ratios for code (5-8x vs 3-5x)
|
|
405
|
+
- Lower latency (~20-50ms vs 50-200ms for LLMLingua)
|
|
406
|
+
- Smaller memory footprint (~50MB vs ~1GB)
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> compressor = CodeAwareCompressor()
|
|
410
|
+
>>> result = compressor.compress('''
|
|
411
|
+
... import os
|
|
412
|
+
... from typing import List
|
|
413
|
+
...
|
|
414
|
+
... def process_data(items: List[str]) -> List[str]:
|
|
415
|
+
... \"\"\"Process a list of items.\"\"\"
|
|
416
|
+
... results = []
|
|
417
|
+
... for item in items:
|
|
418
|
+
... # Validate item
|
|
419
|
+
... if not item:
|
|
420
|
+
... continue
|
|
421
|
+
... # Process valid item
|
|
422
|
+
... processed = item.strip().lower()
|
|
423
|
+
... results.append(processed)
|
|
424
|
+
... return results
|
|
425
|
+
... ''')
|
|
426
|
+
>>> print(result.compressed)
|
|
427
|
+
import os
|
|
428
|
+
from typing import List
|
|
429
|
+
|
|
430
|
+
def process_data(items: List[str]) -> List[str]:
|
|
431
|
+
\"\"\"Process a list of items.\"\"\"
|
|
432
|
+
# ... (body compressed: 10 lines → 2 lines)
|
|
433
|
+
pass
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
name: str = "code_aware_compressor"
|
|
437
|
+
|
|
438
|
+
def __init__(self, config: CodeCompressorConfig | None = None):
|
|
439
|
+
"""Initialize code-aware compressor.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
config: Compression configuration. If None, uses defaults.
|
|
443
|
+
|
|
444
|
+
Note:
|
|
445
|
+
Tree-sitter parsers are loaded lazily on first use to avoid
|
|
446
|
+
startup overhead when the compressor isn't used.
|
|
447
|
+
"""
|
|
448
|
+
self.config = config or CodeCompressorConfig()
|
|
449
|
+
|
|
450
|
+
def compress(
|
|
451
|
+
self,
|
|
452
|
+
code: str,
|
|
453
|
+
language: str | None = None,
|
|
454
|
+
context: str = "",
|
|
455
|
+
) -> CodeCompressionResult:
|
|
456
|
+
"""Compress code while preserving syntax validity.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
code: Source code to compress.
|
|
460
|
+
language: Language name (e.g., 'python'). Auto-detected if None.
|
|
461
|
+
context: Optional context for relevance-aware compression.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
CodeCompressionResult with compressed code and metadata.
|
|
465
|
+
"""
|
|
466
|
+
if not code or not code.strip():
|
|
467
|
+
return CodeCompressionResult(
|
|
468
|
+
compressed=code,
|
|
469
|
+
original=code,
|
|
470
|
+
original_tokens=0,
|
|
471
|
+
compressed_tokens=0,
|
|
472
|
+
compression_ratio=1.0,
|
|
473
|
+
syntax_valid=True,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Estimate tokens
|
|
477
|
+
original_tokens = len(code.split())
|
|
478
|
+
|
|
479
|
+
# Skip small content
|
|
480
|
+
if original_tokens < self.config.min_tokens_for_compression:
|
|
481
|
+
return CodeCompressionResult(
|
|
482
|
+
compressed=code,
|
|
483
|
+
original=code,
|
|
484
|
+
original_tokens=original_tokens,
|
|
485
|
+
compressed_tokens=original_tokens,
|
|
486
|
+
compression_ratio=1.0,
|
|
487
|
+
syntax_valid=True,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Detect or use specified language
|
|
491
|
+
if language:
|
|
492
|
+
detected_lang = CodeLanguage(language.lower())
|
|
493
|
+
confidence = 1.0
|
|
494
|
+
elif self.config.language_hint:
|
|
495
|
+
detected_lang = CodeLanguage(self.config.language_hint.lower())
|
|
496
|
+
confidence = 1.0
|
|
497
|
+
else:
|
|
498
|
+
detected_lang, confidence = detect_language(code)
|
|
499
|
+
|
|
500
|
+
# If language unknown and fallback enabled, try LLMLingua
|
|
501
|
+
if detected_lang == CodeLanguage.UNKNOWN:
|
|
502
|
+
if self.config.fallback_to_llmlingua:
|
|
503
|
+
return self._fallback_compress(code, original_tokens)
|
|
504
|
+
else:
|
|
505
|
+
# Pass through unchanged
|
|
506
|
+
return CodeCompressionResult(
|
|
507
|
+
compressed=code,
|
|
508
|
+
original=code,
|
|
509
|
+
original_tokens=original_tokens,
|
|
510
|
+
compressed_tokens=original_tokens,
|
|
511
|
+
compression_ratio=1.0,
|
|
512
|
+
language=CodeLanguage.UNKNOWN,
|
|
513
|
+
language_confidence=0.0,
|
|
514
|
+
syntax_valid=True,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Check if tree-sitter is available
|
|
518
|
+
if not _check_tree_sitter_available():
|
|
519
|
+
logger.warning("tree-sitter not available. Install with: pip install headroom-ai[code]")
|
|
520
|
+
if self.config.fallback_to_llmlingua:
|
|
521
|
+
return self._fallback_compress(code, original_tokens)
|
|
522
|
+
return CodeCompressionResult(
|
|
523
|
+
compressed=code,
|
|
524
|
+
original=code,
|
|
525
|
+
original_tokens=original_tokens,
|
|
526
|
+
compressed_tokens=original_tokens,
|
|
527
|
+
compression_ratio=1.0,
|
|
528
|
+
language=detected_lang,
|
|
529
|
+
language_confidence=confidence,
|
|
530
|
+
syntax_valid=True,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Parse and compress
|
|
534
|
+
try:
|
|
535
|
+
compressed, structure = self._compress_with_ast(code, detected_lang, context)
|
|
536
|
+
compressed_tokens = len(compressed.split())
|
|
537
|
+
|
|
538
|
+
# Verify syntax validity
|
|
539
|
+
syntax_valid = self._verify_syntax(compressed, detected_lang)
|
|
540
|
+
|
|
541
|
+
# If syntax invalid, fall back to original
|
|
542
|
+
if not syntax_valid:
|
|
543
|
+
logger.warning("Compression produced invalid syntax, returning original")
|
|
544
|
+
return CodeCompressionResult(
|
|
545
|
+
compressed=code,
|
|
546
|
+
original=code,
|
|
547
|
+
original_tokens=original_tokens,
|
|
548
|
+
compressed_tokens=original_tokens,
|
|
549
|
+
compression_ratio=1.0,
|
|
550
|
+
language=detected_lang,
|
|
551
|
+
language_confidence=confidence,
|
|
552
|
+
syntax_valid=True,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
ratio = compressed_tokens / max(original_tokens, 1)
|
|
556
|
+
|
|
557
|
+
# Store in CCR if significant compression
|
|
558
|
+
cache_key = None
|
|
559
|
+
if self.config.enable_ccr and ratio < 0.8:
|
|
560
|
+
cache_key = self._store_in_ccr(code, compressed, original_tokens)
|
|
561
|
+
if cache_key:
|
|
562
|
+
# Add standard CCR marker format for CCRToolInjector detection
|
|
563
|
+
compressed += (
|
|
564
|
+
f"\n# [{original_tokens} items compressed to {compressed_tokens}. "
|
|
565
|
+
f"Retrieve more: hash={cache_key}]"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return CodeCompressionResult(
|
|
569
|
+
compressed=compressed,
|
|
570
|
+
original=code,
|
|
571
|
+
original_tokens=original_tokens,
|
|
572
|
+
compressed_tokens=compressed_tokens,
|
|
573
|
+
compression_ratio=ratio,
|
|
574
|
+
language=detected_lang,
|
|
575
|
+
language_confidence=confidence,
|
|
576
|
+
preserved_imports=len(structure.imports),
|
|
577
|
+
preserved_signatures=len(structure.function_signatures),
|
|
578
|
+
compressed_bodies=len(structure.function_bodies),
|
|
579
|
+
syntax_valid=syntax_valid,
|
|
580
|
+
cache_key=cache_key,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.warning("AST compression failed: %s, falling back", e)
|
|
585
|
+
if self.config.fallback_to_llmlingua:
|
|
586
|
+
return self._fallback_compress(code, original_tokens)
|
|
587
|
+
return CodeCompressionResult(
|
|
588
|
+
compressed=code,
|
|
589
|
+
original=code,
|
|
590
|
+
original_tokens=original_tokens,
|
|
591
|
+
compressed_tokens=original_tokens,
|
|
592
|
+
compression_ratio=1.0,
|
|
593
|
+
language=detected_lang,
|
|
594
|
+
language_confidence=confidence,
|
|
595
|
+
syntax_valid=True,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _compress_with_ast(
|
|
599
|
+
self,
|
|
600
|
+
code: str,
|
|
601
|
+
language: CodeLanguage,
|
|
602
|
+
context: str,
|
|
603
|
+
) -> tuple[str, CodeStructure]:
|
|
604
|
+
"""Compress code using AST parsing.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
code: Source code.
|
|
608
|
+
language: Detected language.
|
|
609
|
+
context: User context for relevance.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Tuple of (compressed code, extracted structure).
|
|
613
|
+
"""
|
|
614
|
+
# Get parser for language
|
|
615
|
+
parser = _get_parser(language.value)
|
|
616
|
+
|
|
617
|
+
# Parse code
|
|
618
|
+
tree = parser.parse(bytes(code, "utf-8"))
|
|
619
|
+
root = tree.root_node
|
|
620
|
+
|
|
621
|
+
# Extract structure based on language
|
|
622
|
+
if language == CodeLanguage.PYTHON:
|
|
623
|
+
structure = self._extract_python_structure(root, code)
|
|
624
|
+
elif language in (CodeLanguage.JAVASCRIPT, CodeLanguage.TYPESCRIPT):
|
|
625
|
+
structure = self._extract_js_structure(root, code)
|
|
626
|
+
elif language == CodeLanguage.GO:
|
|
627
|
+
structure = self._extract_go_structure(root, code)
|
|
628
|
+
elif language == CodeLanguage.RUST:
|
|
629
|
+
structure = self._extract_rust_structure(root, code)
|
|
630
|
+
elif language == CodeLanguage.JAVA:
|
|
631
|
+
structure = self._extract_java_structure(root, code)
|
|
632
|
+
else:
|
|
633
|
+
structure = self._extract_generic_structure(root, code)
|
|
634
|
+
|
|
635
|
+
# Assemble compressed code
|
|
636
|
+
compressed = self._assemble_compressed(structure, language)
|
|
637
|
+
|
|
638
|
+
return compressed, structure
|
|
639
|
+
|
|
640
|
+
def _extract_python_structure(self, root: Any, code: str) -> CodeStructure:
|
|
641
|
+
"""Extract structure from Python AST."""
|
|
642
|
+
structure = CodeStructure()
|
|
643
|
+
lines = code.split("\n")
|
|
644
|
+
|
|
645
|
+
def visit(node: Any) -> None:
|
|
646
|
+
node_type = node.type
|
|
647
|
+
|
|
648
|
+
if node_type == "import_statement":
|
|
649
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
650
|
+
|
|
651
|
+
elif node_type == "import_from_statement":
|
|
652
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
653
|
+
|
|
654
|
+
elif node_type == "decorated_definition":
|
|
655
|
+
# Get decorator and the definition
|
|
656
|
+
decorator_text = []
|
|
657
|
+
definition_text = None
|
|
658
|
+
for child in node.children:
|
|
659
|
+
if child.type == "decorator":
|
|
660
|
+
decorator_text.append(self._get_node_text(child, code))
|
|
661
|
+
elif child.type in ("function_definition", "class_definition"):
|
|
662
|
+
definition_text = self._extract_definition(child, code, lines)
|
|
663
|
+
if decorator_text and definition_text:
|
|
664
|
+
full_def = "\n".join(decorator_text) + "\n" + definition_text
|
|
665
|
+
if "class" in definition_text:
|
|
666
|
+
structure.class_definitions.append(full_def)
|
|
667
|
+
else:
|
|
668
|
+
structure.function_signatures.append(full_def)
|
|
669
|
+
return # Don't recurse into children
|
|
670
|
+
|
|
671
|
+
elif node_type == "function_definition":
|
|
672
|
+
definition = self._extract_definition(node, code, lines)
|
|
673
|
+
structure.function_signatures.append(definition)
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
elif node_type == "class_definition":
|
|
677
|
+
definition = self._extract_definition(node, code, lines)
|
|
678
|
+
structure.class_definitions.append(definition)
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
elif node_type == "try_statement":
|
|
682
|
+
if self.config.preserve_error_handlers:
|
|
683
|
+
structure.error_handlers.append(self._get_node_text(node, code))
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
elif node_type == "type_alias_statement":
|
|
687
|
+
structure.type_definitions.append(self._get_node_text(node, code))
|
|
688
|
+
|
|
689
|
+
# Recurse into children
|
|
690
|
+
for child in node.children:
|
|
691
|
+
visit(child)
|
|
692
|
+
|
|
693
|
+
visit(root)
|
|
694
|
+
return structure
|
|
695
|
+
|
|
696
|
+
def _extract_definition(self, node: Any, code: str, lines: list[str]) -> str:
|
|
697
|
+
"""Extract a function/class definition with compressed body."""
|
|
698
|
+
node_text = self._get_node_text(node, code)
|
|
699
|
+
node_lines = node_text.split("\n")
|
|
700
|
+
|
|
701
|
+
if len(node_lines) <= self.config.max_body_lines + 2:
|
|
702
|
+
# Small enough, keep as is
|
|
703
|
+
return node_text
|
|
704
|
+
|
|
705
|
+
# Find signature (first line(s) until colon)
|
|
706
|
+
signature_lines = []
|
|
707
|
+
body_start = 0
|
|
708
|
+
paren_depth = 0
|
|
709
|
+
found_colon = False
|
|
710
|
+
|
|
711
|
+
for i, line in enumerate(node_lines):
|
|
712
|
+
signature_lines.append(line)
|
|
713
|
+
paren_depth += line.count("(") - line.count(")")
|
|
714
|
+
if ":" in line and paren_depth <= 0:
|
|
715
|
+
# Check if this is the end of signature
|
|
716
|
+
if line.rstrip().endswith(":"):
|
|
717
|
+
found_colon = True
|
|
718
|
+
body_start = i + 1
|
|
719
|
+
break
|
|
720
|
+
|
|
721
|
+
if not found_colon:
|
|
722
|
+
# Couldn't parse signature, return truncated
|
|
723
|
+
return "\n".join(node_lines[: self.config.max_body_lines]) + "\n # ..."
|
|
724
|
+
|
|
725
|
+
signature = "\n".join(signature_lines)
|
|
726
|
+
|
|
727
|
+
# Check for docstring
|
|
728
|
+
docstring = ""
|
|
729
|
+
if body_start < len(node_lines):
|
|
730
|
+
first_body_line = node_lines[body_start].strip()
|
|
731
|
+
if first_body_line.startswith(('"""', "'''")):
|
|
732
|
+
quote = first_body_line[:3]
|
|
733
|
+
docstring_lines = [node_lines[body_start]]
|
|
734
|
+
if first_body_line.count(quote) >= 2:
|
|
735
|
+
# Single line docstring
|
|
736
|
+
if self.config.docstring_mode == DocstringMode.FULL:
|
|
737
|
+
docstring = node_lines[body_start]
|
|
738
|
+
elif self.config.docstring_mode == DocstringMode.FIRST_LINE:
|
|
739
|
+
docstring = node_lines[body_start]
|
|
740
|
+
body_start += 1
|
|
741
|
+
else:
|
|
742
|
+
# Multi-line docstring
|
|
743
|
+
for j in range(body_start + 1, len(node_lines)):
|
|
744
|
+
docstring_lines.append(node_lines[j])
|
|
745
|
+
if quote in node_lines[j]:
|
|
746
|
+
break
|
|
747
|
+
body_start = body_start + len(docstring_lines)
|
|
748
|
+
if self.config.docstring_mode == DocstringMode.FULL:
|
|
749
|
+
docstring = "\n".join(docstring_lines)
|
|
750
|
+
elif self.config.docstring_mode == DocstringMode.FIRST_LINE:
|
|
751
|
+
# Keep first line of docstring
|
|
752
|
+
first_doc = docstring_lines[0].strip()
|
|
753
|
+
if first_doc == '"""' or first_doc == "'''":
|
|
754
|
+
# Opening on its own line
|
|
755
|
+
if len(docstring_lines) > 1:
|
|
756
|
+
indent_len = len(docstring_lines[0]) - len(
|
|
757
|
+
docstring_lines[0].lstrip()
|
|
758
|
+
)
|
|
759
|
+
docstring = " " * indent_len + '"""' + docstring_lines[1].strip()
|
|
760
|
+
if not docstring.rstrip().endswith('"""'):
|
|
761
|
+
docstring += '"""'
|
|
762
|
+
else:
|
|
763
|
+
docstring = docstring_lines[0]
|
|
764
|
+
if not docstring.rstrip().endswith('"""'):
|
|
765
|
+
docstring = docstring.rstrip() + '..."""'
|
|
766
|
+
|
|
767
|
+
# Build compressed output
|
|
768
|
+
body_lines = node_lines[body_start:]
|
|
769
|
+
total_body = len(body_lines)
|
|
770
|
+
|
|
771
|
+
# Determine indentation
|
|
772
|
+
indent = " "
|
|
773
|
+
if body_lines:
|
|
774
|
+
first_non_empty = next((line for line in body_lines if line.strip()), "")
|
|
775
|
+
if first_non_empty:
|
|
776
|
+
indent = first_non_empty[: len(first_non_empty) - len(first_non_empty.lstrip())]
|
|
777
|
+
|
|
778
|
+
# Keep first few lines of body and add placeholder
|
|
779
|
+
keep_lines = min(self.config.max_body_lines, len(body_lines))
|
|
780
|
+
compressed_body = body_lines[:keep_lines]
|
|
781
|
+
|
|
782
|
+
result_parts = [signature]
|
|
783
|
+
if docstring and self.config.docstring_mode not in (
|
|
784
|
+
DocstringMode.NONE,
|
|
785
|
+
DocstringMode.REMOVE,
|
|
786
|
+
):
|
|
787
|
+
result_parts.append(docstring)
|
|
788
|
+
|
|
789
|
+
if compressed_body:
|
|
790
|
+
result_parts.extend(compressed_body)
|
|
791
|
+
|
|
792
|
+
if total_body > keep_lines:
|
|
793
|
+
omitted = total_body - keep_lines
|
|
794
|
+
# Simple, honest marker - no retrieval hints (causes hallucination)
|
|
795
|
+
result_parts.append(f"{indent}# [{omitted} lines omitted]")
|
|
796
|
+
result_parts.append(f"{indent}pass")
|
|
797
|
+
|
|
798
|
+
return "\n".join(result_parts)
|
|
799
|
+
|
|
800
|
+
def _extract_js_structure(self, root: Any, code: str) -> CodeStructure:
|
|
801
|
+
"""Extract structure from JavaScript/TypeScript AST."""
|
|
802
|
+
structure = CodeStructure()
|
|
803
|
+
lines = code.split("\n")
|
|
804
|
+
|
|
805
|
+
def visit(node: Any) -> None:
|
|
806
|
+
node_type = node.type
|
|
807
|
+
|
|
808
|
+
if node_type in ("import_statement", "import_declaration"):
|
|
809
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
810
|
+
|
|
811
|
+
elif node_type == "export_statement":
|
|
812
|
+
text = self._get_node_text(node, code)
|
|
813
|
+
if "function" in text or "class" in text:
|
|
814
|
+
structure.function_signatures.append(
|
|
815
|
+
self._compress_js_function(node, code, lines)
|
|
816
|
+
)
|
|
817
|
+
else:
|
|
818
|
+
structure.imports.append(text) # export declarations
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
elif node_type in ("function_declaration", "method_definition"):
|
|
822
|
+
structure.function_signatures.append(self._compress_js_function(node, code, lines))
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
elif node_type == "class_declaration":
|
|
826
|
+
structure.class_definitions.append(self._compress_js_class(node, code, lines))
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
elif node_type in ("interface_declaration", "type_alias_declaration"):
|
|
830
|
+
structure.type_definitions.append(self._get_node_text(node, code))
|
|
831
|
+
|
|
832
|
+
elif node_type == "try_statement":
|
|
833
|
+
if self.config.preserve_error_handlers:
|
|
834
|
+
structure.error_handlers.append(self._get_node_text(node, code))
|
|
835
|
+
return
|
|
836
|
+
|
|
837
|
+
for child in node.children:
|
|
838
|
+
visit(child)
|
|
839
|
+
|
|
840
|
+
visit(root)
|
|
841
|
+
return structure
|
|
842
|
+
|
|
843
|
+
def _compress_js_function(self, node: Any, code: str, lines: list[str]) -> str:
|
|
844
|
+
"""Compress a JavaScript function."""
|
|
845
|
+
node_text = self._get_node_text(node, code)
|
|
846
|
+
node_lines = node_text.split("\n")
|
|
847
|
+
|
|
848
|
+
if len(node_lines) <= self.config.max_body_lines + 2:
|
|
849
|
+
return node_text
|
|
850
|
+
|
|
851
|
+
# Find opening brace
|
|
852
|
+
signature_lines = []
|
|
853
|
+
body_start = 0
|
|
854
|
+
for i, line in enumerate(node_lines):
|
|
855
|
+
signature_lines.append(line)
|
|
856
|
+
if "{" in line:
|
|
857
|
+
body_start = i + 1
|
|
858
|
+
break
|
|
859
|
+
|
|
860
|
+
if body_start == 0:
|
|
861
|
+
return node_text # Arrow function or other format
|
|
862
|
+
|
|
863
|
+
body_lines = node_lines[body_start:-1] # Exclude closing brace
|
|
864
|
+
total_body = len(body_lines)
|
|
865
|
+
keep_lines = min(self.config.max_body_lines, total_body)
|
|
866
|
+
|
|
867
|
+
result = signature_lines + body_lines[:keep_lines]
|
|
868
|
+
if total_body > keep_lines:
|
|
869
|
+
result.append(f" // ... ({total_body - keep_lines} lines compressed)")
|
|
870
|
+
result.append(node_lines[-1]) # Closing brace
|
|
871
|
+
|
|
872
|
+
return "\n".join(result)
|
|
873
|
+
|
|
874
|
+
def _compress_js_class(self, node: Any, code: str, lines: list[str]) -> str:
|
|
875
|
+
"""Compress a JavaScript class, keeping method signatures."""
|
|
876
|
+
# For now, use similar logic to function compression
|
|
877
|
+
return self._compress_js_function(node, code, lines)
|
|
878
|
+
|
|
879
|
+
def _extract_go_structure(self, root: Any, code: str) -> CodeStructure:
|
|
880
|
+
"""Extract structure from Go AST."""
|
|
881
|
+
structure = CodeStructure()
|
|
882
|
+
lines = code.split("\n")
|
|
883
|
+
|
|
884
|
+
def visit(node: Any) -> None:
|
|
885
|
+
node_type = node.type
|
|
886
|
+
|
|
887
|
+
if node_type == "import_declaration":
|
|
888
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
889
|
+
|
|
890
|
+
elif node_type == "package_clause":
|
|
891
|
+
structure.imports.insert(0, self._get_node_text(node, code))
|
|
892
|
+
|
|
893
|
+
elif node_type == "function_declaration":
|
|
894
|
+
structure.function_signatures.append(self._compress_go_function(node, code, lines))
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
elif node_type == "method_declaration":
|
|
898
|
+
structure.function_signatures.append(self._compress_go_function(node, code, lines))
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
elif node_type == "type_declaration":
|
|
902
|
+
structure.type_definitions.append(self._get_node_text(node, code))
|
|
903
|
+
|
|
904
|
+
for child in node.children:
|
|
905
|
+
visit(child)
|
|
906
|
+
|
|
907
|
+
visit(root)
|
|
908
|
+
return structure
|
|
909
|
+
|
|
910
|
+
def _compress_go_function(self, node: Any, code: str, lines: list[str]) -> str:
|
|
911
|
+
"""Compress a Go function."""
|
|
912
|
+
node_text = self._get_node_text(node, code)
|
|
913
|
+
node_lines = node_text.split("\n")
|
|
914
|
+
|
|
915
|
+
if len(node_lines) <= self.config.max_body_lines + 2:
|
|
916
|
+
return node_text
|
|
917
|
+
|
|
918
|
+
# Find opening brace
|
|
919
|
+
signature_lines = []
|
|
920
|
+
body_start = 0
|
|
921
|
+
for i, line in enumerate(node_lines):
|
|
922
|
+
signature_lines.append(line)
|
|
923
|
+
if "{" in line:
|
|
924
|
+
body_start = i + 1
|
|
925
|
+
break
|
|
926
|
+
|
|
927
|
+
body_lines = node_lines[body_start:-1]
|
|
928
|
+
total_body = len(body_lines)
|
|
929
|
+
keep_lines = min(self.config.max_body_lines, total_body)
|
|
930
|
+
|
|
931
|
+
result = signature_lines + body_lines[:keep_lines]
|
|
932
|
+
if total_body > keep_lines:
|
|
933
|
+
result.append(f"\t// ... ({total_body - keep_lines} lines compressed)")
|
|
934
|
+
result.append(node_lines[-1])
|
|
935
|
+
|
|
936
|
+
return "\n".join(result)
|
|
937
|
+
|
|
938
|
+
def _extract_rust_structure(self, root: Any, code: str) -> CodeStructure:
|
|
939
|
+
"""Extract structure from Rust AST."""
|
|
940
|
+
structure = CodeStructure()
|
|
941
|
+
lines = code.split("\n")
|
|
942
|
+
|
|
943
|
+
def visit(node: Any) -> None:
|
|
944
|
+
node_type = node.type
|
|
945
|
+
|
|
946
|
+
if node_type == "use_declaration":
|
|
947
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
948
|
+
|
|
949
|
+
elif node_type == "function_item":
|
|
950
|
+
structure.function_signatures.append(
|
|
951
|
+
self._compress_rust_function(node, code, lines)
|
|
952
|
+
)
|
|
953
|
+
return
|
|
954
|
+
|
|
955
|
+
elif node_type in ("struct_item", "enum_item", "type_item"):
|
|
956
|
+
structure.type_definitions.append(self._get_node_text(node, code))
|
|
957
|
+
|
|
958
|
+
elif node_type == "impl_item":
|
|
959
|
+
structure.class_definitions.append(self._compress_rust_impl(node, code, lines))
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
for child in node.children:
|
|
963
|
+
visit(child)
|
|
964
|
+
|
|
965
|
+
visit(root)
|
|
966
|
+
return structure
|
|
967
|
+
|
|
968
|
+
def _compress_rust_function(self, node: Any, code: str, lines: list[str]) -> str:
|
|
969
|
+
"""Compress a Rust function."""
|
|
970
|
+
node_text = self._get_node_text(node, code)
|
|
971
|
+
node_lines = node_text.split("\n")
|
|
972
|
+
|
|
973
|
+
if len(node_lines) <= self.config.max_body_lines + 2:
|
|
974
|
+
return node_text
|
|
975
|
+
|
|
976
|
+
# Find opening brace
|
|
977
|
+
signature_lines = []
|
|
978
|
+
body_start = 0
|
|
979
|
+
for i, line in enumerate(node_lines):
|
|
980
|
+
signature_lines.append(line)
|
|
981
|
+
if "{" in line:
|
|
982
|
+
body_start = i + 1
|
|
983
|
+
break
|
|
984
|
+
|
|
985
|
+
body_lines = node_lines[body_start:-1]
|
|
986
|
+
total_body = len(body_lines)
|
|
987
|
+
keep_lines = min(self.config.max_body_lines, total_body)
|
|
988
|
+
|
|
989
|
+
result = signature_lines + body_lines[:keep_lines]
|
|
990
|
+
if total_body > keep_lines:
|
|
991
|
+
result.append(f" // ... ({total_body - keep_lines} lines compressed)")
|
|
992
|
+
result.append(node_lines[-1])
|
|
993
|
+
|
|
994
|
+
return "\n".join(result)
|
|
995
|
+
|
|
996
|
+
def _compress_rust_impl(self, node: Any, code: str, lines: list[str]) -> str:
|
|
997
|
+
"""Compress a Rust impl block."""
|
|
998
|
+
return self._compress_rust_function(node, code, lines)
|
|
999
|
+
|
|
1000
|
+
def _extract_java_structure(self, root: Any, code: str) -> CodeStructure:
|
|
1001
|
+
"""Extract structure from Java AST."""
|
|
1002
|
+
structure = CodeStructure()
|
|
1003
|
+
lines = code.split("\n")
|
|
1004
|
+
|
|
1005
|
+
def visit(node: Any) -> None:
|
|
1006
|
+
node_type = node.type
|
|
1007
|
+
|
|
1008
|
+
if node_type == "import_declaration":
|
|
1009
|
+
structure.imports.append(self._get_node_text(node, code))
|
|
1010
|
+
|
|
1011
|
+
elif node_type == "package_declaration":
|
|
1012
|
+
structure.imports.insert(0, self._get_node_text(node, code))
|
|
1013
|
+
|
|
1014
|
+
elif node_type == "class_declaration":
|
|
1015
|
+
structure.class_definitions.append(self._compress_java_class(node, code, lines))
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
elif node_type == "method_declaration":
|
|
1019
|
+
structure.function_signatures.append(self._compress_java_method(node, code, lines))
|
|
1020
|
+
return
|
|
1021
|
+
|
|
1022
|
+
for child in node.children:
|
|
1023
|
+
visit(child)
|
|
1024
|
+
|
|
1025
|
+
visit(root)
|
|
1026
|
+
return structure
|
|
1027
|
+
|
|
1028
|
+
def _compress_java_class(self, node: Any, code: str, lines: list[str]) -> str:
|
|
1029
|
+
"""Compress a Java class."""
|
|
1030
|
+
return self._compress_js_function(node, code, lines)
|
|
1031
|
+
|
|
1032
|
+
def _compress_java_method(self, node: Any, code: str, lines: list[str]) -> str:
|
|
1033
|
+
"""Compress a Java method."""
|
|
1034
|
+
return self._compress_js_function(node, code, lines)
|
|
1035
|
+
|
|
1036
|
+
def _extract_generic_structure(self, root: Any, code: str) -> CodeStructure:
|
|
1037
|
+
"""Extract structure from generic code."""
|
|
1038
|
+
# Fallback: use line-based compression
|
|
1039
|
+
structure = CodeStructure()
|
|
1040
|
+
lines = code.split("\n")
|
|
1041
|
+
|
|
1042
|
+
# Keep imports (lines starting with import/include/use/from)
|
|
1043
|
+
for line in lines:
|
|
1044
|
+
stripped = line.strip()
|
|
1045
|
+
if any(
|
|
1046
|
+
stripped.startswith(kw)
|
|
1047
|
+
for kw in ["import ", "from ", "#include", "use ", "require("]
|
|
1048
|
+
):
|
|
1049
|
+
structure.imports.append(line)
|
|
1050
|
+
|
|
1051
|
+
# Rest goes to other
|
|
1052
|
+
structure.other = lines
|
|
1053
|
+
|
|
1054
|
+
return structure
|
|
1055
|
+
|
|
1056
|
+
def _get_node_text(self, node: Any, code: str) -> str:
|
|
1057
|
+
"""Extract text from AST node."""
|
|
1058
|
+
return code[node.start_byte : node.end_byte]
|
|
1059
|
+
|
|
1060
|
+
def _assemble_compressed(
|
|
1061
|
+
self,
|
|
1062
|
+
structure: CodeStructure,
|
|
1063
|
+
language: CodeLanguage,
|
|
1064
|
+
) -> str:
|
|
1065
|
+
"""Assemble compressed code from structure."""
|
|
1066
|
+
parts: list[str] = []
|
|
1067
|
+
|
|
1068
|
+
# Imports first
|
|
1069
|
+
if structure.imports:
|
|
1070
|
+
parts.extend(structure.imports)
|
|
1071
|
+
parts.append("") # Empty line after imports
|
|
1072
|
+
|
|
1073
|
+
# Type definitions
|
|
1074
|
+
if structure.type_definitions:
|
|
1075
|
+
parts.extend(structure.type_definitions)
|
|
1076
|
+
parts.append("")
|
|
1077
|
+
|
|
1078
|
+
# Class definitions
|
|
1079
|
+
if structure.class_definitions:
|
|
1080
|
+
parts.extend(structure.class_definitions)
|
|
1081
|
+
parts.append("")
|
|
1082
|
+
|
|
1083
|
+
# Function signatures/definitions
|
|
1084
|
+
if structure.function_signatures:
|
|
1085
|
+
parts.extend(structure.function_signatures)
|
|
1086
|
+
parts.append("")
|
|
1087
|
+
|
|
1088
|
+
# Error handlers (if preserved separately)
|
|
1089
|
+
if structure.error_handlers and self.config.preserve_error_handlers:
|
|
1090
|
+
parts.append("# Error handlers:")
|
|
1091
|
+
parts.extend(structure.error_handlers)
|
|
1092
|
+
parts.append("")
|
|
1093
|
+
|
|
1094
|
+
# Other content
|
|
1095
|
+
if structure.other:
|
|
1096
|
+
parts.extend(structure.other)
|
|
1097
|
+
|
|
1098
|
+
# Remove trailing empty lines
|
|
1099
|
+
while parts and not parts[-1].strip():
|
|
1100
|
+
parts.pop()
|
|
1101
|
+
|
|
1102
|
+
return "\n".join(parts)
|
|
1103
|
+
|
|
1104
|
+
def _verify_syntax(self, code: str, language: CodeLanguage) -> bool:
|
|
1105
|
+
"""Verify that code is syntactically valid."""
|
|
1106
|
+
try:
|
|
1107
|
+
parser = _get_parser(language.value)
|
|
1108
|
+
tree = parser.parse(bytes(code, "utf-8"))
|
|
1109
|
+
# Check for ERROR nodes in the tree
|
|
1110
|
+
return not self._has_error_nodes(tree.root_node)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
return False
|
|
1113
|
+
|
|
1114
|
+
def _has_error_nodes(self, node: Any) -> bool:
|
|
1115
|
+
"""Check if AST contains ERROR nodes."""
|
|
1116
|
+
if node.type == "ERROR":
|
|
1117
|
+
return True
|
|
1118
|
+
for child in node.children:
|
|
1119
|
+
if self._has_error_nodes(child):
|
|
1120
|
+
return True
|
|
1121
|
+
return False
|
|
1122
|
+
|
|
1123
|
+
def _fallback_compress(self, code: str, original_tokens: int) -> CodeCompressionResult:
|
|
1124
|
+
"""Fall back to LLMLingua compression."""
|
|
1125
|
+
try:
|
|
1126
|
+
from .llmlingua_compressor import LLMLinguaCompressor, _check_llmlingua_available
|
|
1127
|
+
|
|
1128
|
+
if _check_llmlingua_available():
|
|
1129
|
+
compressor = LLMLinguaCompressor()
|
|
1130
|
+
result = compressor.compress(code, content_type="code")
|
|
1131
|
+
return CodeCompressionResult(
|
|
1132
|
+
compressed=result.compressed,
|
|
1133
|
+
original=code,
|
|
1134
|
+
original_tokens=result.original_tokens,
|
|
1135
|
+
compressed_tokens=result.compressed_tokens,
|
|
1136
|
+
compression_ratio=result.compression_ratio,
|
|
1137
|
+
language=CodeLanguage.UNKNOWN,
|
|
1138
|
+
language_confidence=0.0,
|
|
1139
|
+
syntax_valid=True, # LLMLingua doesn't guarantee this
|
|
1140
|
+
)
|
|
1141
|
+
except ImportError:
|
|
1142
|
+
pass
|
|
1143
|
+
|
|
1144
|
+
# No fallback available, return original
|
|
1145
|
+
return CodeCompressionResult(
|
|
1146
|
+
compressed=code,
|
|
1147
|
+
original=code,
|
|
1148
|
+
original_tokens=original_tokens,
|
|
1149
|
+
compressed_tokens=original_tokens,
|
|
1150
|
+
compression_ratio=1.0,
|
|
1151
|
+
language=CodeLanguage.UNKNOWN,
|
|
1152
|
+
language_confidence=0.0,
|
|
1153
|
+
syntax_valid=True,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
def _store_in_ccr(
|
|
1157
|
+
self,
|
|
1158
|
+
original: str,
|
|
1159
|
+
compressed: str,
|
|
1160
|
+
original_tokens: int,
|
|
1161
|
+
) -> str | None:
|
|
1162
|
+
"""Store original in CCR for later retrieval."""
|
|
1163
|
+
try:
|
|
1164
|
+
from ..cache.compression_store import get_compression_store
|
|
1165
|
+
|
|
1166
|
+
store = get_compression_store()
|
|
1167
|
+
return store.store(
|
|
1168
|
+
original,
|
|
1169
|
+
compressed,
|
|
1170
|
+
original_tokens=original_tokens,
|
|
1171
|
+
compressed_tokens=len(compressed.split()),
|
|
1172
|
+
compression_strategy="code_aware",
|
|
1173
|
+
)
|
|
1174
|
+
except ImportError:
|
|
1175
|
+
return None
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
logger.debug("CCR storage failed: %s", e)
|
|
1178
|
+
return None
|
|
1179
|
+
|
|
1180
|
+
def apply(
|
|
1181
|
+
self,
|
|
1182
|
+
messages: list[dict[str, Any]],
|
|
1183
|
+
tokenizer: Tokenizer,
|
|
1184
|
+
**kwargs: Any,
|
|
1185
|
+
) -> TransformResult:
|
|
1186
|
+
"""Apply code-aware compression to messages.
|
|
1187
|
+
|
|
1188
|
+
This method implements the Transform interface for use in pipelines.
|
|
1189
|
+
It compresses code content in tool outputs and messages.
|
|
1190
|
+
|
|
1191
|
+
Args:
|
|
1192
|
+
messages: List of message dicts to transform.
|
|
1193
|
+
tokenizer: Tokenizer for accurate token counting.
|
|
1194
|
+
**kwargs: Additional arguments (e.g., 'context').
|
|
1195
|
+
|
|
1196
|
+
Returns:
|
|
1197
|
+
TransformResult with compressed messages and metadata.
|
|
1198
|
+
"""
|
|
1199
|
+
from .content_detector import ContentType, detect_content_type
|
|
1200
|
+
|
|
1201
|
+
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
|
|
1202
|
+
context = kwargs.get("context", "")
|
|
1203
|
+
|
|
1204
|
+
transformed_messages = []
|
|
1205
|
+
transforms_applied = []
|
|
1206
|
+
warnings: list[str] = []
|
|
1207
|
+
|
|
1208
|
+
for message in messages:
|
|
1209
|
+
content = message.get("content", "")
|
|
1210
|
+
|
|
1211
|
+
# Skip empty or non-string content (multimodal messages with images)
|
|
1212
|
+
if not content or not isinstance(content, str):
|
|
1213
|
+
transformed_messages.append(message)
|
|
1214
|
+
continue
|
|
1215
|
+
|
|
1216
|
+
# Check if content is code
|
|
1217
|
+
detection = detect_content_type(content)
|
|
1218
|
+
|
|
1219
|
+
if detection.content_type == ContentType.SOURCE_CODE:
|
|
1220
|
+
language = detection.metadata.get("language")
|
|
1221
|
+
result = self.compress(content, language=language, context=context)
|
|
1222
|
+
|
|
1223
|
+
if result.compression_ratio < 0.9:
|
|
1224
|
+
transformed_messages.append({**message, "content": result.compressed})
|
|
1225
|
+
transforms_applied.append(
|
|
1226
|
+
f"code_aware:{result.language.value}:{result.compression_ratio:.2f}"
|
|
1227
|
+
)
|
|
1228
|
+
else:
|
|
1229
|
+
transformed_messages.append(message)
|
|
1230
|
+
else:
|
|
1231
|
+
transformed_messages.append(message)
|
|
1232
|
+
|
|
1233
|
+
tokens_after = sum(
|
|
1234
|
+
tokenizer.count_text(str(m.get("content", ""))) for m in transformed_messages
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
if not _check_tree_sitter_available():
|
|
1238
|
+
warnings.append(
|
|
1239
|
+
"tree-sitter not installed. Install with: pip install headroom-ai[code]"
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
return TransformResult(
|
|
1243
|
+
messages=transformed_messages,
|
|
1244
|
+
tokens_before=tokens_before,
|
|
1245
|
+
tokens_after=tokens_after,
|
|
1246
|
+
transforms_applied=transforms_applied if transforms_applied else ["code_aware:noop"],
|
|
1247
|
+
warnings=warnings,
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
def should_apply(
|
|
1251
|
+
self,
|
|
1252
|
+
messages: list[dict[str, Any]],
|
|
1253
|
+
tokenizer: Tokenizer,
|
|
1254
|
+
**kwargs: Any,
|
|
1255
|
+
) -> bool:
|
|
1256
|
+
"""Check if code-aware compression should be applied.
|
|
1257
|
+
|
|
1258
|
+
Returns True if:
|
|
1259
|
+
- tree-sitter is available, AND
|
|
1260
|
+
- Content contains detected source code
|
|
1261
|
+
|
|
1262
|
+
Args:
|
|
1263
|
+
messages: Messages to check.
|
|
1264
|
+
tokenizer: Tokenizer for counting.
|
|
1265
|
+
**kwargs: Additional arguments.
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
True if compression should be applied.
|
|
1269
|
+
"""
|
|
1270
|
+
if not _check_tree_sitter_available():
|
|
1271
|
+
return False
|
|
1272
|
+
|
|
1273
|
+
from .content_detector import ContentType, detect_content_type
|
|
1274
|
+
|
|
1275
|
+
for message in messages:
|
|
1276
|
+
content = message.get("content", "")
|
|
1277
|
+
# Only check string content (skip multimodal)
|
|
1278
|
+
if content and isinstance(content, str):
|
|
1279
|
+
detection = detect_content_type(content)
|
|
1280
|
+
if detection.content_type == ContentType.SOURCE_CODE:
|
|
1281
|
+
return True
|
|
1282
|
+
|
|
1283
|
+
return False
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def compress_code(
|
|
1287
|
+
code: str,
|
|
1288
|
+
language: str | None = None,
|
|
1289
|
+
target_rate: float = 0.2,
|
|
1290
|
+
context: str = "",
|
|
1291
|
+
) -> str:
|
|
1292
|
+
"""Convenience function for one-off code compression.
|
|
1293
|
+
|
|
1294
|
+
Args:
|
|
1295
|
+
code: Source code to compress.
|
|
1296
|
+
language: Language hint (auto-detected if None).
|
|
1297
|
+
target_rate: Target compression rate (0.2 = keep 20%).
|
|
1298
|
+
context: Optional context for relevance.
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
Compressed code string.
|
|
1302
|
+
|
|
1303
|
+
Example:
|
|
1304
|
+
>>> compressed = compress_code(large_python_file)
|
|
1305
|
+
>>> print(compressed) # Valid Python code
|
|
1306
|
+
"""
|
|
1307
|
+
config = CodeCompressorConfig(
|
|
1308
|
+
target_compression_rate=target_rate,
|
|
1309
|
+
language_hint=language,
|
|
1310
|
+
)
|
|
1311
|
+
compressor = CodeAwareCompressor(config)
|
|
1312
|
+
result = compressor.compress(code, language=language, context=context)
|
|
1313
|
+
return result.compressed
|