chopratejas commited on
Commit
946ba4f
·
1 Parent(s): eac2890

Fix lint errors in text compression utilities

Browse files
headroom/transforms/__init__.py CHANGED
@@ -3,16 +3,16 @@
3
  from .base import Transform
4
  from .cache_aligner import CacheAligner
5
  from .content_detector import ContentType, DetectionResult, detect_content_type
6
- from .log_compressor import LogCompressor, LogCompressorConfig, LogCompressionResult
7
  from .pipeline import TransformPipeline
8
  from .rolling_window import RollingWindow
9
  from .search_compressor import (
 
10
  SearchCompressor,
11
  SearchCompressorConfig,
12
- SearchCompressionResult,
13
  )
14
  from .smart_crusher import SmartCrusher, SmartCrusherConfig
15
- from .text_compressor import TextCompressor, TextCompressorConfig, TextCompressionResult
16
  from .tool_crusher import ToolCrusher
17
 
18
  __all__ = [
 
3
  from .base import Transform
4
  from .cache_aligner import CacheAligner
5
  from .content_detector import ContentType, DetectionResult, detect_content_type
6
+ from .log_compressor import LogCompressionResult, LogCompressor, LogCompressorConfig
7
  from .pipeline import TransformPipeline
8
  from .rolling_window import RollingWindow
9
  from .search_compressor import (
10
+ SearchCompressionResult,
11
  SearchCompressor,
12
  SearchCompressorConfig,
 
13
  )
14
  from .smart_crusher import SmartCrusher, SmartCrusherConfig
15
+ from .text_compressor import TextCompressionResult, TextCompressor, TextCompressorConfig
16
  from .tool_crusher import ToolCrusher
17
 
18
  __all__ = [
headroom/transforms/content_detector.py CHANGED
@@ -46,9 +46,7 @@ _SEARCH_RESULT_PATTERN = re.compile(
46
  r"^[^\s:]+:\d+:" # file:line: format (grep -n style)
47
  )
48
 
49
- _DIFF_HEADER_PATTERN = re.compile(
50
- r"^(diff --git|--- a/|@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@)"
51
- )
52
 
53
  _DIFF_CHANGE_PATTERN = re.compile(r"^[+-][^+-]")
54
 
@@ -332,7 +330,6 @@ def is_json_array_of_dicts(content: str) -> bool:
332
  True if content is a JSON array where all items are dicts.
333
  """
334
  result = detect_content_type(content)
335
- return (
336
- result.content_type == ContentType.JSON_ARRAY
337
- and result.metadata.get("is_dict_array", False)
338
  )
 
46
  r"^[^\s:]+:\d+:" # file:line: format (grep -n style)
47
  )
48
 
49
+ _DIFF_HEADER_PATTERN = re.compile(r"^(diff --git|--- a/|@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@)")
 
 
50
 
51
  _DIFF_CHANGE_PATTERN = re.compile(r"^[+-][^+-]")
52
 
 
330
  True if content is a JSON array where all items are dicts.
331
  """
332
  result = detect_content_type(content)
333
+ return result.content_type == ContentType.JSON_ARRAY and result.metadata.get(
334
+ "is_dict_array", False
 
335
  )
headroom/transforms/log_compressor.py CHANGED
@@ -140,9 +140,7 @@ class LogCompressor:
140
 
141
  # Level detection patterns
142
  _LEVEL_PATTERNS = {
143
- LogLevel.ERROR: re.compile(
144
- r"\b(ERROR|error|Error|FATAL|fatal|Fatal|CRITICAL|critical)\b"
145
- ),
146
  LogLevel.FAIL: re.compile(r"\b(FAIL|FAILED|fail|failed|Fail|Failed)\b"),
147
  LogLevel.WARN: re.compile(r"\b(WARN|WARNING|warn|warning|Warn|Warning)\b"),
148
  LogLevel.INFO: re.compile(r"\b(INFO|info|Info)\b"),
@@ -178,7 +176,7 @@ class LogCompressor:
178
  """
179
  self.config = config or LogCompressorConfig()
180
 
181
- def compress(self, content: str, context: str = "") -> "LogCompressionResult":
182
  """Compress log output.
183
 
184
  Args:
@@ -354,9 +352,7 @@ class LogCompressor:
354
 
355
  # Select errors (first, last, highest scoring)
356
  if errors:
357
- selected_errors = self._select_with_first_last(
358
- errors, self.config.max_errors
359
- )
360
  selected.extend(selected_errors)
361
 
362
  # Select fails
@@ -371,7 +367,7 @@ class LogCompressor:
371
  selected.extend(warnings[: self.config.max_warnings])
372
 
373
  # Select stack traces
374
- for i, stack in enumerate(stack_traces[: self.config.max_stack_traces]):
375
  selected.extend(stack[: self.config.stack_trace_max_lines])
376
 
377
  # Always include summary lines
@@ -393,9 +389,7 @@ class LogCompressor:
393
 
394
  return selected
395
 
396
- def _select_with_first_last(
397
- self, lines: list[LogLine], max_count: int
398
- ) -> list[LogLine]:
399
  """Select lines keeping first and last."""
400
  if len(lines) <= max_count:
401
  return lines
@@ -411,7 +405,7 @@ class LogCompressor:
411
  # Fill remaining with highest scoring
412
  remaining = max_count - len(selected)
413
  if remaining > 0:
414
- candidates = [l for l in lines if l not in selected]
415
  candidates = sorted(candidates, key=lambda x: x.score, reverse=True)
416
  selected.extend(candidates[:remaining])
417
 
@@ -434,18 +428,14 @@ class LogCompressor:
434
 
435
  return deduped
436
 
437
- def _add_context(
438
- self, all_lines: list[LogLine], selected: list[LogLine]
439
- ) -> list[LogLine]:
440
  """Add context lines around selected lines."""
441
- selected_indices = {l.line_number for l in selected}
442
  context_indices: set[int] = set()
443
 
444
  for idx in selected_indices:
445
  # Add lines before
446
- for i in range(
447
- max(0, idx - self.config.error_context_lines), idx
448
- ):
449
  context_indices.add(i)
450
  # Add lines after
451
  for i in range(
@@ -467,10 +457,10 @@ class LogCompressor:
467
  """Format selected lines with summary stats."""
468
  # Count categories
469
  stats: dict[str, int] = {
470
- "errors": sum(1 for l in all_lines if l.level == LogLevel.ERROR),
471
- "fails": sum(1 for l in all_lines if l.level == LogLevel.FAIL),
472
- "warnings": sum(1 for l in all_lines if l.level == LogLevel.WARN),
473
- "info": sum(1 for l in all_lines if l.level == LogLevel.INFO),
474
  "total": len(all_lines),
475
  "selected": len(selected),
476
  }
@@ -497,9 +487,7 @@ class LogCompressor:
497
 
498
  return "\n".join(output_lines), stats
499
 
500
- def _store_in_ccr(
501
- self, original: str, compressed: str, original_count: int
502
- ) -> str | None:
503
  """Store original in CCR for later retrieval."""
504
  try:
505
  from ..cache.compression_store import get_compression_store
 
140
 
141
  # Level detection patterns
142
  _LEVEL_PATTERNS = {
143
+ LogLevel.ERROR: re.compile(r"\b(ERROR|error|Error|FATAL|fatal|Fatal|CRITICAL|critical)\b"),
 
 
144
  LogLevel.FAIL: re.compile(r"\b(FAIL|FAILED|fail|failed|Fail|Failed)\b"),
145
  LogLevel.WARN: re.compile(r"\b(WARN|WARNING|warn|warning|Warn|Warning)\b"),
146
  LogLevel.INFO: re.compile(r"\b(INFO|info|Info)\b"),
 
176
  """
177
  self.config = config or LogCompressorConfig()
178
 
179
+ def compress(self, content: str, context: str = "") -> LogCompressionResult:
180
  """Compress log output.
181
 
182
  Args:
 
352
 
353
  # Select errors (first, last, highest scoring)
354
  if errors:
355
+ selected_errors = self._select_with_first_last(errors, self.config.max_errors)
 
 
356
  selected.extend(selected_errors)
357
 
358
  # Select fails
 
367
  selected.extend(warnings[: self.config.max_warnings])
368
 
369
  # Select stack traces
370
+ for stack in stack_traces[: self.config.max_stack_traces]:
371
  selected.extend(stack[: self.config.stack_trace_max_lines])
372
 
373
  # Always include summary lines
 
389
 
390
  return selected
391
 
392
+ def _select_with_first_last(self, lines: list[LogLine], max_count: int) -> list[LogLine]:
 
 
393
  """Select lines keeping first and last."""
394
  if len(lines) <= max_count:
395
  return lines
 
405
  # Fill remaining with highest scoring
406
  remaining = max_count - len(selected)
407
  if remaining > 0:
408
+ candidates = [line for line in lines if line not in selected]
409
  candidates = sorted(candidates, key=lambda x: x.score, reverse=True)
410
  selected.extend(candidates[:remaining])
411
 
 
428
 
429
  return deduped
430
 
431
+ def _add_context(self, all_lines: list[LogLine], selected: list[LogLine]) -> list[LogLine]:
 
 
432
  """Add context lines around selected lines."""
433
+ selected_indices = {line.line_number for line in selected}
434
  context_indices: set[int] = set()
435
 
436
  for idx in selected_indices:
437
  # Add lines before
438
+ for i in range(max(0, idx - self.config.error_context_lines), idx):
 
 
439
  context_indices.add(i)
440
  # Add lines after
441
  for i in range(
 
457
  """Format selected lines with summary stats."""
458
  # Count categories
459
  stats: dict[str, int] = {
460
+ "errors": sum(1 for line in all_lines if line.level == LogLevel.ERROR),
461
+ "fails": sum(1 for line in all_lines if line.level == LogLevel.FAIL),
462
+ "warnings": sum(1 for line in all_lines if line.level == LogLevel.WARN),
463
+ "info": sum(1 for line in all_lines if line.level == LogLevel.INFO),
464
  "total": len(all_lines),
465
  "selected": len(selected),
466
  }
 
487
 
488
  return "\n".join(output_lines), stats
489
 
490
+ def _store_in_ccr(self, original: str, compressed: str, original_count: int) -> str | None:
 
 
491
  """Store original in CCR for later retrieval."""
492
  try:
493
  from ..cache.compression_store import get_compression_store
headroom/transforms/search_compressor.py CHANGED
@@ -20,7 +20,6 @@ Integrates with CCR for reversible compression.
20
 
21
  from __future__ import annotations
22
 
23
- import hashlib
24
  import re
25
  from dataclasses import dataclass, field
26
 
@@ -107,7 +106,7 @@ class SearchCompressor:
107
  self,
108
  content: str,
109
  context: str = "",
110
- ) -> "SearchCompressionResult":
111
  """Compress search results.
112
 
113
  Args:
 
20
 
21
  from __future__ import annotations
22
 
 
23
  import re
24
  from dataclasses import dataclass, field
25
 
 
106
  self,
107
  content: str,
108
  context: str = "",
109
+ ) -> SearchCompressionResult:
110
  """Compress search results.
111
 
112
  Args:
headroom/transforms/text_compressor.py CHANGED
@@ -64,7 +64,7 @@ class TextCompressor:
64
  """
65
  self.config = config or TextCompressorConfig()
66
 
67
- def compress(self, content: str, context: str = "") -> "TextCompressionResult":
68
  """Compress text content.
69
 
70
  Args:
@@ -98,11 +98,7 @@ class TextCompressor:
98
 
99
  # Store in CCR if significant compression
100
  cache_key = None
101
- if (
102
- self.config.enable_ccr
103
- and len(lines) >= self.config.min_lines_for_ccr
104
- and ratio < 0.7
105
- ):
106
  cache_key = self._store_in_ccr(content, compressed, len(lines))
107
  if cache_key:
108
  compressed += f"\n[{len(lines)} lines compressed. hash={cache_key}]"
@@ -116,13 +112,11 @@ class TextCompressor:
116
  cache_key=cache_key,
117
  )
118
 
119
- def _score_lines(
120
- self, lines: list[str], context: str
121
- ) -> list[tuple[int, str, float]]:
122
  """Score lines by importance."""
123
  context_lower = context.lower()
124
  context_words = set(context_lower.split()) if context else set()
125
- anchor_keywords = set(k.lower() for k in self.config.anchor_keywords)
126
 
127
  scored: list[tuple[int, str, float]] = []
128
 
@@ -179,7 +173,7 @@ class TextCompressor:
179
  high_score_lines.sort(key=lambda x: x[2], reverse=True)
180
 
181
  remaining_slots = self.config.max_total_lines - len(selected_indices)
182
- for idx, line, score in high_score_lines[:remaining_slots]:
183
  selected_indices.add(idx)
184
  remaining_slots -= 1
185
  if remaining_slots <= 0:
@@ -201,9 +195,7 @@ class TextCompressor:
201
  selected = sorted(selected_indices)
202
  return [(i, original_lines[i]) for i in selected]
203
 
204
- def _format_output(
205
- self, selected: list[tuple[int, str]], total_lines: int
206
- ) -> str:
207
  """Format selected lines with ellipsis markers."""
208
  if not selected:
209
  return f"[{total_lines} lines omitted]"
@@ -227,9 +219,7 @@ class TextCompressor:
227
 
228
  return "\n".join(output_lines)
229
 
230
- def _store_in_ccr(
231
- self, original: str, compressed: str, original_count: int
232
- ) -> str | None:
233
  """Store original in CCR for later retrieval."""
234
  try:
235
  from ..cache.compression_store import get_compression_store
 
64
  """
65
  self.config = config or TextCompressorConfig()
66
 
67
+ def compress(self, content: str, context: str = "") -> TextCompressionResult:
68
  """Compress text content.
69
 
70
  Args:
 
98
 
99
  # Store in CCR if significant compression
100
  cache_key = None
101
+ if self.config.enable_ccr and len(lines) >= self.config.min_lines_for_ccr and ratio < 0.7:
 
 
 
 
102
  cache_key = self._store_in_ccr(content, compressed, len(lines))
103
  if cache_key:
104
  compressed += f"\n[{len(lines)} lines compressed. hash={cache_key}]"
 
112
  cache_key=cache_key,
113
  )
114
 
115
+ def _score_lines(self, lines: list[str], context: str) -> list[tuple[int, str, float]]:
 
 
116
  """Score lines by importance."""
117
  context_lower = context.lower()
118
  context_words = set(context_lower.split()) if context else set()
119
+ anchor_keywords = {k.lower() for k in self.config.anchor_keywords}
120
 
121
  scored: list[tuple[int, str, float]] = []
122
 
 
173
  high_score_lines.sort(key=lambda x: x[2], reverse=True)
174
 
175
  remaining_slots = self.config.max_total_lines - len(selected_indices)
176
+ for idx, _line, _score in high_score_lines[:remaining_slots]:
177
  selected_indices.add(idx)
178
  remaining_slots -= 1
179
  if remaining_slots <= 0:
 
195
  selected = sorted(selected_indices)
196
  return [(i, original_lines[i]) for i in selected]
197
 
198
+ def _format_output(self, selected: list[tuple[int, str]], total_lines: int) -> str:
 
 
199
  """Format selected lines with ellipsis markers."""
200
  if not selected:
201
  return f"[{total_lines} lines omitted]"
 
219
 
220
  return "\n".join(output_lines)
221
 
222
+ def _store_in_ccr(self, original: str, compressed: str, original_count: int) -> str | None:
 
 
223
  """Store original in CCR for later retrieval."""
224
  try:
225
  from ..cache.compression_store import get_compression_store
tests/test_text_compressors.py CHANGED
@@ -3,11 +3,8 @@
3
  Tests content detection, search compressor, log compressor, and text compressor.
4
  """
5
 
6
- import pytest
7
-
8
  from headroom.transforms import (
9
  ContentType,
10
- DetectionResult,
11
  LogCompressor,
12
  LogCompressorConfig,
13
  SearchCompressor,
@@ -31,7 +28,7 @@ class TestContentDetector:
31
 
32
  def test_detect_json_array_non_dict(self):
33
  """JSON arrays of non-dicts are detected."""
34
- content = '[1, 2, 3, 4, 5]'
35
  result = detect_content_type(content)
36
  assert result.content_type == ContentType.JSON_ARRAY
37
  assert result.metadata.get("is_dict_array") is False
@@ -133,9 +130,7 @@ class TestSearchCompressor:
133
 
134
  def test_compress_search_results(self):
135
  """Search results are compressed."""
136
- content = "\n".join(
137
- [f"src/file{i}.py:{i * 10}:def function_{i}():" for i in range(100)]
138
- )
139
 
140
  compressor = SearchCompressor()
141
  result = compressor.compress(content, context="find function_50")
@@ -146,9 +141,7 @@ class TestSearchCompressor:
146
 
147
  def test_keeps_first_and_last(self):
148
  """First and last matches are preserved."""
149
- content = "\n".join(
150
- [f"src/file.py:{i}:line {i}" for i in range(1, 101)]
151
- )
152
 
153
  compressor = SearchCompressor(
154
  config=SearchCompressorConfig(
@@ -192,17 +185,19 @@ class TestLogCompressor:
192
  lines = ["=" * 40 + " test session starts " + "=" * 40]
193
  lines.append("collected 100 items")
194
  lines.extend([f"tests/test_{i}.py::test_case_{i} PASSED" for i in range(95)])
195
- lines.extend([
196
- "tests/test_fail.py::test_case_fail FAILED",
197
- "",
198
- "=" * 40 + " FAILURES " + "=" * 40,
199
- "tests/test_fail.py::test_case_fail",
200
- "AssertionError: expected True, got False",
201
- "",
202
- "=" * 40 + " short test summary " + "=" * 40,
203
- "FAILED tests/test_fail.py::test_case_fail",
204
- "1 failed, 95 passed",
205
- ])
 
 
206
  content = "\n".join(lines)
207
 
208
  compressor = LogCompressor()
@@ -241,9 +236,7 @@ INFO: Done
241
  """Small logs pass through unchanged."""
242
  content = "INFO: Starting\nINFO: Done"
243
 
244
- compressor = LogCompressor(
245
- config=LogCompressorConfig(min_lines_for_ccr=100)
246
- )
247
  result = compressor.compress(content)
248
 
249
  assert result.compression_ratio == 1.0
@@ -319,18 +312,14 @@ class TestSmartCrusherTextIntegration:
319
  from headroom.transforms import SmartCrusher, SmartCrusherConfig
320
 
321
  # Create search results content
322
- search_results = "\n".join(
323
- [f"src/file{i}.py:{i}:def function_{i}():" for i in range(100)]
324
- )
325
 
326
  messages = [
327
  {"role": "user", "content": "Find all function definitions"},
328
  {"role": "tool", "content": search_results},
329
  ]
330
 
331
- crusher = SmartCrusher(
332
- config=SmartCrusherConfig(min_tokens_to_crush=10)
333
- )
334
  tokenizer = self._get_tokenizer()
335
 
336
  result = crusher.apply(messages, tokenizer)
@@ -359,9 +348,7 @@ class TestSmartCrusherTextIntegration:
359
  {"role": "tool", "content": log_content},
360
  ]
361
 
362
- crusher = SmartCrusher(
363
- config=SmartCrusherConfig(min_tokens_to_crush=10)
364
- )
365
  tokenizer = self._get_tokenizer()
366
 
367
  result = crusher.apply(messages, tokenizer)
@@ -373,9 +360,7 @@ class TestSmartCrusherTextIntegration:
373
  def test_search_compressor_available_as_standalone(self):
374
  """SearchCompressor is available for explicit use by applications."""
375
  # Create search results content
376
- search_results = "\n".join(
377
- [f"src/file{i}.py:{i}:def function_{i}():" for i in range(100)]
378
- )
379
 
380
  # Application explicitly chooses to compress
381
  compressor = SearchCompressor()
@@ -407,13 +392,19 @@ class TestSmartCrusherTextIntegration:
407
 
408
  def test_smart_crusher_json_still_works(self):
409
  """SmartCrusher still handles JSON correctly."""
410
- from headroom.transforms import SmartCrusher, SmartCrusherConfig
411
  import json
412
  import re
413
 
 
 
414
  # Create JSON array content with larger items to trigger compression
415
  items = [
416
- {"id": i, "name": f"Item {i}", "value": i * 10, "description": f"This is item number {i}"}
 
 
 
 
 
417
  for i in range(500)
418
  ]
419
  json_content = json.dumps(items)
 
3
  Tests content detection, search compressor, log compressor, and text compressor.
4
  """
5
 
 
 
6
  from headroom.transforms import (
7
  ContentType,
 
8
  LogCompressor,
9
  LogCompressorConfig,
10
  SearchCompressor,
 
28
 
29
  def test_detect_json_array_non_dict(self):
30
  """JSON arrays of non-dicts are detected."""
31
+ content = "[1, 2, 3, 4, 5]"
32
  result = detect_content_type(content)
33
  assert result.content_type == ContentType.JSON_ARRAY
34
  assert result.metadata.get("is_dict_array") is False
 
130
 
131
  def test_compress_search_results(self):
132
  """Search results are compressed."""
133
+ content = "\n".join([f"src/file{i}.py:{i * 10}:def function_{i}():" for i in range(100)])
 
 
134
 
135
  compressor = SearchCompressor()
136
  result = compressor.compress(content, context="find function_50")
 
141
 
142
  def test_keeps_first_and_last(self):
143
  """First and last matches are preserved."""
144
+ content = "\n".join([f"src/file.py:{i}:line {i}" for i in range(1, 101)])
 
 
145
 
146
  compressor = SearchCompressor(
147
  config=SearchCompressorConfig(
 
185
  lines = ["=" * 40 + " test session starts " + "=" * 40]
186
  lines.append("collected 100 items")
187
  lines.extend([f"tests/test_{i}.py::test_case_{i} PASSED" for i in range(95)])
188
+ lines.extend(
189
+ [
190
+ "tests/test_fail.py::test_case_fail FAILED",
191
+ "",
192
+ "=" * 40 + " FAILURES " + "=" * 40,
193
+ "tests/test_fail.py::test_case_fail",
194
+ "AssertionError: expected True, got False",
195
+ "",
196
+ "=" * 40 + " short test summary " + "=" * 40,
197
+ "FAILED tests/test_fail.py::test_case_fail",
198
+ "1 failed, 95 passed",
199
+ ]
200
+ )
201
  content = "\n".join(lines)
202
 
203
  compressor = LogCompressor()
 
236
  """Small logs pass through unchanged."""
237
  content = "INFO: Starting\nINFO: Done"
238
 
239
+ compressor = LogCompressor(config=LogCompressorConfig(min_lines_for_ccr=100))
 
 
240
  result = compressor.compress(content)
241
 
242
  assert result.compression_ratio == 1.0
 
312
  from headroom.transforms import SmartCrusher, SmartCrusherConfig
313
 
314
  # Create search results content
315
+ search_results = "\n".join([f"src/file{i}.py:{i}:def function_{i}():" for i in range(100)])
 
 
316
 
317
  messages = [
318
  {"role": "user", "content": "Find all function definitions"},
319
  {"role": "tool", "content": search_results},
320
  ]
321
 
322
+ crusher = SmartCrusher(config=SmartCrusherConfig(min_tokens_to_crush=10))
 
 
323
  tokenizer = self._get_tokenizer()
324
 
325
  result = crusher.apply(messages, tokenizer)
 
348
  {"role": "tool", "content": log_content},
349
  ]
350
 
351
+ crusher = SmartCrusher(config=SmartCrusherConfig(min_tokens_to_crush=10))
 
 
352
  tokenizer = self._get_tokenizer()
353
 
354
  result = crusher.apply(messages, tokenizer)
 
360
  def test_search_compressor_available_as_standalone(self):
361
  """SearchCompressor is available for explicit use by applications."""
362
  # Create search results content
363
+ search_results = "\n".join([f"src/file{i}.py:{i}:def function_{i}():" for i in range(100)])
 
 
364
 
365
  # Application explicitly chooses to compress
366
  compressor = SearchCompressor()
 
392
 
393
  def test_smart_crusher_json_still_works(self):
394
  """SmartCrusher still handles JSON correctly."""
 
395
  import json
396
  import re
397
 
398
+ from headroom.transforms import SmartCrusher, SmartCrusherConfig
399
+
400
  # Create JSON array content with larger items to trigger compression
401
  items = [
402
+ {
403
+ "id": i,
404
+ "name": f"Item {i}",
405
+ "value": i * 10,
406
+ "description": f"This is item number {i}",
407
+ }
408
  for i in range(500)
409
  ]
410
  json_content = json.dumps(items)