Spaces:
Build error
Build error
Commit ·
e723f5f
1
Parent(s): f60956b
Remove hardcoded source hint system from ContentRouter
Browse filesContentRouter now routes purely based on content analysis instead of
relying on hardcoded tool name mappings. This makes the router work
with any MCP tool regardless of naming convention.
Changes:
- Remove generate_source_hint() function and _strategy_from_hint() method
- Remove source_hint parameter from compress() method
- Remove _get_tool_source_hint() from IntelligentContextManager
- Update tests to remove source hint test cases
- Update docs to document content detection approach
docs/transforms.md
CHANGED
|
@@ -516,15 +516,14 @@ router = ContentRouter(config)
|
|
| 516 |
### Example
|
| 517 |
|
| 518 |
```python
|
| 519 |
-
from headroom.transforms import ContentRouter
|
| 520 |
|
| 521 |
router = ContentRouter()
|
| 522 |
|
| 523 |
-
#
|
| 524 |
-
|
| 525 |
-
result = router.compress(content, source_hint=hint)
|
| 526 |
|
| 527 |
-
print(result.
|
| 528 |
print(result.routing_log) # List of routing decisions
|
| 529 |
```
|
| 530 |
|
|
@@ -540,22 +539,17 @@ print(result.routing_log) # List of routing decisions
|
|
| 540 |
| LLMLINGUA | Any (max compression) | LLMLinguaCompressor |
|
| 541 |
| PASSTHROUGH | Small content | None |
|
| 542 |
|
| 543 |
-
###
|
| 544 |
|
| 545 |
-
|
| 546 |
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
|
| 553 |
-
|
| 554 |
-
hint = generate_source_hint(file_path="components/Button.tsx")
|
| 555 |
-
|
| 556 |
-
# From explicit tool
|
| 557 |
-
hint = generate_source_hint(tool_name="Grep") # Routes to SEARCH
|
| 558 |
-
```
|
| 559 |
|
| 560 |
---
|
| 561 |
|
|
|
|
| 516 |
### Example
|
| 517 |
|
| 518 |
```python
|
| 519 |
+
from headroom.transforms import ContentRouter
|
| 520 |
|
| 521 |
router = ContentRouter()
|
| 522 |
|
| 523 |
+
# Router auto-detects content type and routes to optimal compressor
|
| 524 |
+
result = router.compress(content)
|
|
|
|
| 525 |
|
| 526 |
+
print(result.strategy_used) # CompressionStrategy.CODE_AWARE, SMART_CRUSHER, etc.
|
| 527 |
print(result.routing_log) # List of routing decisions
|
| 528 |
```
|
| 529 |
|
|
|
|
| 539 |
| LLMLINGUA | Any (max compression) | LLMLinguaCompressor |
|
| 540 |
| PASSTHROUGH | Small content | None |
|
| 541 |
|
| 542 |
+
### Content Detection
|
| 543 |
|
| 544 |
+
The router automatically detects content types by analyzing the content itself:
|
| 545 |
|
| 546 |
+
- **Source code**: Detected by syntax patterns, indentation, keywords
|
| 547 |
+
- **JSON arrays**: Detected by JSON structure with array elements
|
| 548 |
+
- **Search results**: Detected by `file:line:` patterns
|
| 549 |
+
- **Log output**: Detected by timestamp and log level patterns
|
| 550 |
+
- **Plain text**: Fallback for prose content
|
| 551 |
|
| 552 |
+
No manual hints required - the router inspects content directly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
|
| 554 |
---
|
| 555 |
|
headroom/transforms/__init__.py
CHANGED
|
@@ -57,7 +57,6 @@ from .content_router import (
|
|
| 57 |
ContentRouter,
|
| 58 |
ContentRouterConfig,
|
| 59 |
RouterCompressionResult,
|
| 60 |
-
generate_source_hint,
|
| 61 |
)
|
| 62 |
|
| 63 |
__all__ = [
|
|
@@ -101,7 +100,6 @@ __all__ = [
|
|
| 101 |
"ContentRouterConfig",
|
| 102 |
"RouterCompressionResult",
|
| 103 |
"CompressionStrategy",
|
| 104 |
-
"generate_source_hint",
|
| 105 |
# Other transforms
|
| 106 |
"CacheAligner",
|
| 107 |
"RollingWindow",
|
|
|
|
| 57 |
ContentRouter,
|
| 58 |
ContentRouterConfig,
|
| 59 |
RouterCompressionResult,
|
|
|
|
| 60 |
)
|
| 61 |
|
| 62 |
__all__ = [
|
|
|
|
| 100 |
"ContentRouterConfig",
|
| 101 |
"RouterCompressionResult",
|
| 102 |
"CompressionStrategy",
|
|
|
|
| 103 |
# Other transforms
|
| 104 |
"CacheAligner",
|
| 105 |
"RollingWindow",
|
headroom/transforms/content_router.py
CHANGED
|
@@ -360,51 +360,6 @@ def _extract_json_block(lines: list[str], start: int) -> tuple[str | None, int]:
|
|
| 360 |
return None, start
|
| 361 |
|
| 362 |
|
| 363 |
-
def generate_source_hint(tool_name: str, tool_input: dict[str, Any]) -> str:
|
| 364 |
-
"""Generate a source hint from tool metadata.
|
| 365 |
-
|
| 366 |
-
This enables higher-confidence routing decisions.
|
| 367 |
-
|
| 368 |
-
Args:
|
| 369 |
-
tool_name: Name of the tool that produced the output.
|
| 370 |
-
tool_input: Input parameters to the tool.
|
| 371 |
-
|
| 372 |
-
Returns:
|
| 373 |
-
Source hint string (e.g., "file:auth.py", "tool:grep").
|
| 374 |
-
"""
|
| 375 |
-
# File read operations
|
| 376 |
-
if tool_name in ("Read", "read_file", "cat", "ReadFile"):
|
| 377 |
-
file_path = tool_input.get("file_path", tool_input.get("path", ""))
|
| 378 |
-
if file_path:
|
| 379 |
-
return f"file:{file_path}"
|
| 380 |
-
|
| 381 |
-
# Search operations
|
| 382 |
-
if tool_name in ("Grep", "grep", "ripgrep", "rg", "search", "Search"):
|
| 383 |
-
return "tool:grep"
|
| 384 |
-
|
| 385 |
-
# Glob operations
|
| 386 |
-
if tool_name in ("Glob", "glob", "find"):
|
| 387 |
-
return "tool:glob"
|
| 388 |
-
|
| 389 |
-
# Build/test operations
|
| 390 |
-
if tool_name == "Bash":
|
| 391 |
-
command = str(tool_input.get("command", ""))
|
| 392 |
-
if any(cmd in command for cmd in ["pytest", "npm test", "cargo test", "go test"]):
|
| 393 |
-
return "tool:pytest"
|
| 394 |
-
if any(cmd in command for cmd in ["npm run build", "cargo build", "make"]):
|
| 395 |
-
return "tool:build"
|
| 396 |
-
if "git diff" in command:
|
| 397 |
-
return "tool:git-diff"
|
| 398 |
-
if "git log" in command:
|
| 399 |
-
return "tool:git-log"
|
| 400 |
-
|
| 401 |
-
# Web fetch
|
| 402 |
-
if tool_name in ("WebFetch", "fetch", "curl", "WebSearch"):
|
| 403 |
-
return "tool:web"
|
| 404 |
-
|
| 405 |
-
return ""
|
| 406 |
-
|
| 407 |
-
|
| 408 |
class ContentRouter(Transform):
|
| 409 |
"""Intelligent router that selects optimal compression strategy.
|
| 410 |
|
|
@@ -462,15 +417,12 @@ class ContentRouter(Transform):
|
|
| 462 |
def compress(
|
| 463 |
self,
|
| 464 |
content: str,
|
| 465 |
-
source_hint: str | None = None,
|
| 466 |
context: str = "",
|
| 467 |
) -> RouterCompressionResult:
|
| 468 |
-
"""Compress content using optimal strategy.
|
| 469 |
|
| 470 |
Args:
|
| 471 |
content: Content to compress.
|
| 472 |
-
source_hint: Optional hint about content source.
|
| 473 |
-
Examples: "file:auth.py", "tool:grep", "tool:pytest"
|
| 474 |
context: Optional context for relevance-aware compression.
|
| 475 |
|
| 476 |
Returns:
|
|
@@ -484,87 +436,31 @@ class ContentRouter(Transform):
|
|
| 484 |
routing_log=[],
|
| 485 |
)
|
| 486 |
|
| 487 |
-
# Determine strategy
|
| 488 |
-
strategy = self._determine_strategy(content
|
| 489 |
|
| 490 |
if strategy == CompressionStrategy.MIXED:
|
| 491 |
return self._compress_mixed(content, context)
|
| 492 |
else:
|
| 493 |
return self._compress_pure(content, strategy, context)
|
| 494 |
|
| 495 |
-
def _determine_strategy(
|
| 496 |
-
|
| 497 |
-
content: str,
|
| 498 |
-
source_hint: str | None,
|
| 499 |
-
) -> CompressionStrategy:
|
| 500 |
-
"""Determine the compression strategy.
|
| 501 |
|
| 502 |
Args:
|
| 503 |
content: Content to analyze.
|
| 504 |
-
source_hint: Optional source hint.
|
| 505 |
|
| 506 |
Returns:
|
| 507 |
Selected compression strategy.
|
| 508 |
"""
|
| 509 |
-
# 1.
|
| 510 |
-
if source_hint:
|
| 511 |
-
strategy = self._strategy_from_hint(source_hint)
|
| 512 |
-
if strategy:
|
| 513 |
-
return strategy
|
| 514 |
-
|
| 515 |
-
# 2. Check for mixed content
|
| 516 |
if is_mixed_content(content):
|
| 517 |
return CompressionStrategy.MIXED
|
| 518 |
|
| 519 |
-
#
|
| 520 |
detection = detect_content_type(content)
|
| 521 |
return self._strategy_from_detection(detection)
|
| 522 |
|
| 523 |
-
def _strategy_from_hint(self, hint: str) -> CompressionStrategy | None:
|
| 524 |
-
"""Get strategy from source hint.
|
| 525 |
-
|
| 526 |
-
Args:
|
| 527 |
-
hint: Source hint string.
|
| 528 |
-
|
| 529 |
-
Returns:
|
| 530 |
-
Strategy if determinable, None otherwise.
|
| 531 |
-
"""
|
| 532 |
-
hint_lower = hint.lower()
|
| 533 |
-
|
| 534 |
-
# File hints
|
| 535 |
-
if hint_lower.startswith("file:"):
|
| 536 |
-
file_path = hint_lower[5:]
|
| 537 |
-
if file_path.endswith((".py", ".pyw")):
|
| 538 |
-
return CompressionStrategy.CODE_AWARE
|
| 539 |
-
if file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
|
| 540 |
-
return CompressionStrategy.CODE_AWARE
|
| 541 |
-
if file_path.endswith((".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp")):
|
| 542 |
-
return CompressionStrategy.CODE_AWARE
|
| 543 |
-
if file_path.endswith(".json"):
|
| 544 |
-
return CompressionStrategy.SMART_CRUSHER
|
| 545 |
-
if file_path.endswith((".md", ".txt", ".rst")):
|
| 546 |
-
return CompressionStrategy.TEXT
|
| 547 |
-
if file_path.endswith((".log", ".out")):
|
| 548 |
-
return CompressionStrategy.LOG
|
| 549 |
-
|
| 550 |
-
# Tool hints
|
| 551 |
-
if hint_lower.startswith("tool:"):
|
| 552 |
-
tool = hint_lower[5:]
|
| 553 |
-
if tool in ("grep", "rg", "ripgrep", "ag", "search"):
|
| 554 |
-
return CompressionStrategy.SEARCH
|
| 555 |
-
if tool in ("pytest", "jest", "cargo-test", "go-test", "npm-test"):
|
| 556 |
-
return CompressionStrategy.LOG
|
| 557 |
-
if tool in ("build", "make", "cargo-build", "npm-build"):
|
| 558 |
-
return CompressionStrategy.LOG
|
| 559 |
-
if tool in ("git-diff", "diff"):
|
| 560 |
-
return CompressionStrategy.DIFF
|
| 561 |
-
|
| 562 |
-
# Direct strategy hints (used by _process_content_blocks for tool_result)
|
| 563 |
-
if hint_lower == "json_array":
|
| 564 |
-
return CompressionStrategy.SMART_CRUSHER
|
| 565 |
-
|
| 566 |
-
return None
|
| 567 |
-
|
| 568 |
def _strategy_from_detection(self, detection: Any) -> CompressionStrategy:
|
| 569 |
"""Get strategy from content detection result.
|
| 570 |
|
|
@@ -897,14 +793,13 @@ class ContentRouter(Transform):
|
|
| 897 |
Args:
|
| 898 |
messages: Messages to transform.
|
| 899 |
tokenizer: Tokenizer for counting.
|
| 900 |
-
**kwargs: Additional arguments (context
|
| 901 |
|
| 902 |
Returns:
|
| 903 |
TransformResult with routed and compressed messages.
|
| 904 |
"""
|
| 905 |
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
|
| 906 |
context = kwargs.get("context", "")
|
| 907 |
-
source_hints = kwargs.get("source_hints", {}) # message_id -> hint
|
| 908 |
|
| 909 |
transformed_messages: list[dict[str, Any]] = []
|
| 910 |
transforms_applied: list[str] = []
|
|
@@ -945,9 +840,6 @@ class ContentRouter(Transform):
|
|
| 945 |
transformed_messages.append(message)
|
| 946 |
continue
|
| 947 |
|
| 948 |
-
# Get source hint if available
|
| 949 |
-
source_hint = source_hints.get(i) or source_hints.get(str(i))
|
| 950 |
-
|
| 951 |
# Detect content type for protection decisions
|
| 952 |
detection = detect_content_type(content)
|
| 953 |
is_code = detection.content_type == ContentType.SOURCE_CODE
|
|
@@ -969,8 +861,8 @@ class ContentRouter(Transform):
|
|
| 969 |
transforms_applied.append("router:protected:analysis_context")
|
| 970 |
continue
|
| 971 |
|
| 972 |
-
# Route and compress
|
| 973 |
-
result = self.compress(content,
|
| 974 |
|
| 975 |
if result.compression_ratio < 0.9:
|
| 976 |
transformed_messages.append({**message, "content": result.compressed})
|
|
@@ -1013,8 +905,6 @@ class ContentRouter(Transform):
|
|
| 1013 |
Returns:
|
| 1014 |
Transformed message with compressed content blocks.
|
| 1015 |
"""
|
| 1016 |
-
import json
|
| 1017 |
-
|
| 1018 |
new_blocks = []
|
| 1019 |
any_compressed = False
|
| 1020 |
|
|
@@ -1031,33 +921,7 @@ class ContentRouter(Transform):
|
|
| 1031 |
|
| 1032 |
# Only process string content
|
| 1033 |
if isinstance(tool_content, str) and len(tool_content) > 500:
|
| 1034 |
-
#
|
| 1035 |
-
try:
|
| 1036 |
-
parsed = json.loads(tool_content)
|
| 1037 |
-
if isinstance(parsed, list) and len(parsed) > 10:
|
| 1038 |
-
# Route to SmartCrusher for arrays
|
| 1039 |
-
result = self.compress(
|
| 1040 |
-
tool_content,
|
| 1041 |
-
source_hint="json_array",
|
| 1042 |
-
context=context,
|
| 1043 |
-
)
|
| 1044 |
-
if result.compression_ratio < 0.9:
|
| 1045 |
-
new_blocks.append(
|
| 1046 |
-
{
|
| 1047 |
-
**block,
|
| 1048 |
-
"content": result.compressed,
|
| 1049 |
-
}
|
| 1050 |
-
)
|
| 1051 |
-
transforms_applied.append(
|
| 1052 |
-
f"router:tool_result:{result.strategy_used.value}"
|
| 1053 |
-
)
|
| 1054 |
-
any_compressed = True
|
| 1055 |
-
continue
|
| 1056 |
-
except (json.JSONDecodeError, TypeError):
|
| 1057 |
-
# Not JSON, try general compression
|
| 1058 |
-
pass
|
| 1059 |
-
|
| 1060 |
-
# Try general compression for large non-JSON content
|
| 1061 |
result = self.compress(tool_content, context=context)
|
| 1062 |
if result.compression_ratio < 0.9:
|
| 1063 |
new_blocks.append({**block, "content": result.compressed})
|
|
@@ -1140,15 +1004,13 @@ class ContentRouter(Transform):
|
|
| 1140 |
|
| 1141 |
def route_and_compress(
|
| 1142 |
content: str,
|
| 1143 |
-
source_hint: str | None = None,
|
| 1144 |
context: str = "",
|
| 1145 |
) -> str:
|
| 1146 |
"""Convenience function for one-off routing and compression.
|
| 1147 |
|
| 1148 |
Args:
|
| 1149 |
content: Content to compress.
|
| 1150 |
-
|
| 1151 |
-
context: Optional context.
|
| 1152 |
|
| 1153 |
Returns:
|
| 1154 |
Compressed content.
|
|
@@ -1157,5 +1019,5 @@ def route_and_compress(
|
|
| 1157 |
>>> compressed = route_and_compress(mixed_content)
|
| 1158 |
"""
|
| 1159 |
router = ContentRouter()
|
| 1160 |
-
result = router.compress(content,
|
| 1161 |
return result.compressed
|
|
|
|
| 360 |
return None, start
|
| 361 |
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
class ContentRouter(Transform):
|
| 364 |
"""Intelligent router that selects optimal compression strategy.
|
| 365 |
|
|
|
|
| 417 |
def compress(
|
| 418 |
self,
|
| 419 |
content: str,
|
|
|
|
| 420 |
context: str = "",
|
| 421 |
) -> RouterCompressionResult:
|
| 422 |
+
"""Compress content using optimal strategy based on content detection.
|
| 423 |
|
| 424 |
Args:
|
| 425 |
content: Content to compress.
|
|
|
|
|
|
|
| 426 |
context: Optional context for relevance-aware compression.
|
| 427 |
|
| 428 |
Returns:
|
|
|
|
| 436 |
routing_log=[],
|
| 437 |
)
|
| 438 |
|
| 439 |
+
# Determine strategy from content analysis
|
| 440 |
+
strategy = self._determine_strategy(content)
|
| 441 |
|
| 442 |
if strategy == CompressionStrategy.MIXED:
|
| 443 |
return self._compress_mixed(content, context)
|
| 444 |
else:
|
| 445 |
return self._compress_pure(content, strategy, context)
|
| 446 |
|
| 447 |
+
def _determine_strategy(self, content: str) -> CompressionStrategy:
|
| 448 |
+
"""Determine the compression strategy from content analysis.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
Args:
|
| 451 |
content: Content to analyze.
|
|
|
|
| 452 |
|
| 453 |
Returns:
|
| 454 |
Selected compression strategy.
|
| 455 |
"""
|
| 456 |
+
# 1. Check for mixed content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
if is_mixed_content(content):
|
| 458 |
return CompressionStrategy.MIXED
|
| 459 |
|
| 460 |
+
# 2. Detect content type from content itself
|
| 461 |
detection = detect_content_type(content)
|
| 462 |
return self._strategy_from_detection(detection)
|
| 463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
def _strategy_from_detection(self, detection: Any) -> CompressionStrategy:
|
| 465 |
"""Get strategy from content detection result.
|
| 466 |
|
|
|
|
| 793 |
Args:
|
| 794 |
messages: Messages to transform.
|
| 795 |
tokenizer: Tokenizer for counting.
|
| 796 |
+
**kwargs: Additional arguments (context).
|
| 797 |
|
| 798 |
Returns:
|
| 799 |
TransformResult with routed and compressed messages.
|
| 800 |
"""
|
| 801 |
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
|
| 802 |
context = kwargs.get("context", "")
|
|
|
|
| 803 |
|
| 804 |
transformed_messages: list[dict[str, Any]] = []
|
| 805 |
transforms_applied: list[str] = []
|
|
|
|
| 840 |
transformed_messages.append(message)
|
| 841 |
continue
|
| 842 |
|
|
|
|
|
|
|
|
|
|
| 843 |
# Detect content type for protection decisions
|
| 844 |
detection = detect_content_type(content)
|
| 845 |
is_code = detection.content_type == ContentType.SOURCE_CODE
|
|
|
|
| 861 |
transforms_applied.append("router:protected:analysis_context")
|
| 862 |
continue
|
| 863 |
|
| 864 |
+
# Route and compress based on content detection
|
| 865 |
+
result = self.compress(content, context=context)
|
| 866 |
|
| 867 |
if result.compression_ratio < 0.9:
|
| 868 |
transformed_messages.append({**message, "content": result.compressed})
|
|
|
|
| 905 |
Returns:
|
| 906 |
Transformed message with compressed content blocks.
|
| 907 |
"""
|
|
|
|
|
|
|
| 908 |
new_blocks = []
|
| 909 |
any_compressed = False
|
| 910 |
|
|
|
|
| 921 |
|
| 922 |
# Only process string content
|
| 923 |
if isinstance(tool_content, str) and len(tool_content) > 500:
|
| 924 |
+
# Compress using content detection (will auto-detect JSON arrays, etc.)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
result = self.compress(tool_content, context=context)
|
| 926 |
if result.compression_ratio < 0.9:
|
| 927 |
new_blocks.append({**block, "content": result.compressed})
|
|
|
|
| 1004 |
|
| 1005 |
def route_and_compress(
|
| 1006 |
content: str,
|
|
|
|
| 1007 |
context: str = "",
|
| 1008 |
) -> str:
|
| 1009 |
"""Convenience function for one-off routing and compression.
|
| 1010 |
|
| 1011 |
Args:
|
| 1012 |
content: Content to compress.
|
| 1013 |
+
context: Optional context for relevance-aware compression.
|
|
|
|
| 1014 |
|
| 1015 |
Returns:
|
| 1016 |
Compressed content.
|
|
|
|
| 1019 |
>>> compressed = route_and_compress(mixed_content)
|
| 1020 |
"""
|
| 1021 |
router = ContentRouter()
|
| 1022 |
+
result = router.compress(content, context=context)
|
| 1023 |
return result.compressed
|
headroom/transforms/intelligent_context.py
CHANGED
|
@@ -444,16 +444,8 @@ class IntelligentContextManager(Transform):
|
|
| 444 |
# Focus on tool messages (highest compression potential)
|
| 445 |
if role == "tool" and isinstance(content, str) and len(content) > 100:
|
| 446 |
try:
|
| 447 |
-
#
|
| 448 |
-
|
| 449 |
-
source_hint = self._get_tool_source_hint(messages, tool_call_id)
|
| 450 |
-
|
| 451 |
-
# Compress using ContentRouter
|
| 452 |
-
result = router.compress(
|
| 453 |
-
content,
|
| 454 |
-
source_hint=source_hint,
|
| 455 |
-
context="", # No specific context in compress-first mode
|
| 456 |
-
)
|
| 457 |
|
| 458 |
# Check if compression was effective
|
| 459 |
if result.compression_ratio < 0.9: # At least 10% savings
|
|
@@ -500,46 +492,6 @@ class IntelligentContextManager(Transform):
|
|
| 500 |
|
| 501 |
return compressed_messages, transforms_applied, total_tokens_saved
|
| 502 |
|
| 503 |
-
def _get_tool_source_hint(self, messages: list[dict[str, Any]], tool_call_id: str) -> str:
|
| 504 |
-
"""Extract source hint from the tool call that produced this result.
|
| 505 |
-
|
| 506 |
-
Args:
|
| 507 |
-
messages: List of all messages.
|
| 508 |
-
tool_call_id: The ID of the tool call to find.
|
| 509 |
-
|
| 510 |
-
Returns:
|
| 511 |
-
Source hint string (e.g., "tool:grep", "file:main.py").
|
| 512 |
-
"""
|
| 513 |
-
if not tool_call_id:
|
| 514 |
-
return ""
|
| 515 |
-
|
| 516 |
-
# Find the assistant message with this tool call
|
| 517 |
-
for msg in messages:
|
| 518 |
-
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
| 519 |
-
for tc in msg.get("tool_calls", []):
|
| 520 |
-
if tc.get("id") == tool_call_id:
|
| 521 |
-
func = tc.get("function", {})
|
| 522 |
-
tool_name = func.get("name", "")
|
| 523 |
-
|
| 524 |
-
# Import here to avoid circular imports
|
| 525 |
-
try:
|
| 526 |
-
import json
|
| 527 |
-
|
| 528 |
-
from .content_router import generate_source_hint
|
| 529 |
-
|
| 530 |
-
args_str = func.get("arguments", "{}")
|
| 531 |
-
try:
|
| 532 |
-
args = (
|
| 533 |
-
json.loads(args_str) if isinstance(args_str, str) else args_str
|
| 534 |
-
)
|
| 535 |
-
except json.JSONDecodeError:
|
| 536 |
-
args = {}
|
| 537 |
-
|
| 538 |
-
return generate_source_hint(tool_name, args)
|
| 539 |
-
except ImportError:
|
| 540 |
-
return f"tool:{tool_name}" if tool_name else ""
|
| 541 |
-
return ""
|
| 542 |
-
|
| 543 |
def _compress_content_blocks(
|
| 544 |
self,
|
| 545 |
content_blocks: list[Any],
|
|
|
|
| 444 |
# Focus on tool messages (highest compression potential)
|
| 445 |
if role == "tool" and isinstance(content, str) and len(content) > 100:
|
| 446 |
try:
|
| 447 |
+
# Compress using ContentRouter (auto-detects content type)
|
| 448 |
+
result = router.compress(content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
# Check if compression was effective
|
| 451 |
if result.compression_ratio < 0.9: # At least 10% savings
|
|
|
|
| 492 |
|
| 493 |
return compressed_messages, transforms_applied, total_tokens_saved
|
| 494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
def _compress_content_blocks(
|
| 496 |
self,
|
| 497 |
content_blocks: list[Any],
|
tests/test_transforms/test_content_router.py
CHANGED
|
@@ -4,7 +4,6 @@ Comprehensive tests covering:
|
|
| 4 |
- ContentRouterConfig: Configuration validation and defaults
|
| 5 |
- ContentRouter: Core routing functionality
|
| 6 |
- Strategy detection: Code, JSON, search, logs, text
|
| 7 |
-
- Source hint parsing: File paths, tool names
|
| 8 |
- Mixed content handling: Split, route, reassemble
|
| 9 |
- Transform interface: apply(), should_apply() methods
|
| 10 |
"""
|
|
@@ -18,7 +17,6 @@ from headroom.transforms.content_router import (
|
|
| 18 |
ContentRouterConfig,
|
| 19 |
RouterCompressionResult,
|
| 20 |
RoutingDecision,
|
| 21 |
-
generate_source_hint,
|
| 22 |
)
|
| 23 |
|
| 24 |
# =============================================================================
|
|
@@ -279,51 +277,6 @@ class TestRouterCompressionResult:
|
|
| 279 |
assert result.savings_percentage == 0.0
|
| 280 |
|
| 281 |
|
| 282 |
-
# =============================================================================
|
| 283 |
-
# TestSourceHintGeneration
|
| 284 |
-
# =============================================================================
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
class TestSourceHintGeneration:
|
| 288 |
-
"""Tests for generate_source_hint function."""
|
| 289 |
-
|
| 290 |
-
def test_file_path_hint_with_file_path(self):
|
| 291 |
-
"""File path generates file: hint."""
|
| 292 |
-
hint = generate_source_hint("Read", {"file_path": "/src/auth.py"})
|
| 293 |
-
assert hint == "file:/src/auth.py"
|
| 294 |
-
|
| 295 |
-
def test_file_path_hint_with_path(self):
|
| 296 |
-
"""File path with 'path' key also works."""
|
| 297 |
-
hint = generate_source_hint("read_file", {"path": "/src/auth.py"})
|
| 298 |
-
assert hint == "file:/src/auth.py"
|
| 299 |
-
|
| 300 |
-
def test_grep_tool_hint(self):
|
| 301 |
-
"""Grep tool generates tool:grep hint."""
|
| 302 |
-
hint = generate_source_hint("Grep", {"pattern": "def.*"})
|
| 303 |
-
assert hint == "tool:grep"
|
| 304 |
-
|
| 305 |
-
def test_glob_tool_hint(self):
|
| 306 |
-
"""Glob tool generates tool:glob hint."""
|
| 307 |
-
hint = generate_source_hint("Glob", {"pattern": "*.py"})
|
| 308 |
-
assert hint == "tool:glob"
|
| 309 |
-
|
| 310 |
-
def test_bash_test_command(self):
|
| 311 |
-
"""Bash with test command generates tool:pytest hint."""
|
| 312 |
-
hint = generate_source_hint("Bash", {"command": "pytest tests/"})
|
| 313 |
-
assert hint == "tool:pytest"
|
| 314 |
-
|
| 315 |
-
def test_bash_build_command(self):
|
| 316 |
-
"""Bash with build command generates tool:build hint."""
|
| 317 |
-
hint = generate_source_hint("Bash", {"command": "npm run build"})
|
| 318 |
-
assert "tool:" in hint
|
| 319 |
-
|
| 320 |
-
def test_unknown_tool(self):
|
| 321 |
-
"""Unknown tool returns generic tool hint."""
|
| 322 |
-
hint = generate_source_hint("CustomTool", {"arg": "value"})
|
| 323 |
-
# Should return some hint, not crash
|
| 324 |
-
assert hint is not None
|
| 325 |
-
|
| 326 |
-
|
| 327 |
# =============================================================================
|
| 328 |
# TestStrategyDetection
|
| 329 |
# =============================================================================
|
|
@@ -335,54 +288,32 @@ class TestStrategyDetection:
|
|
| 335 |
def test_detect_python_code(self, router):
|
| 336 |
"""Python code is detected."""
|
| 337 |
code = generate_python_code(5)
|
| 338 |
-
strategy = router._determine_strategy(code
|
| 339 |
# Should be either CODE_AWARE or fallback
|
| 340 |
assert strategy in CompressionStrategy
|
| 341 |
|
| 342 |
def test_detect_json_content(self, router):
|
| 343 |
"""JSON content is detected."""
|
| 344 |
json_data = generate_json_data(20)
|
| 345 |
-
strategy = router._determine_strategy(json_data
|
| 346 |
assert strategy in CompressionStrategy
|
| 347 |
|
| 348 |
def test_detect_search_results(self, router):
|
| 349 |
"""Search/grep results are detected."""
|
| 350 |
search_results = generate_search_results(10)
|
| 351 |
-
strategy = router._determine_strategy(search_results
|
| 352 |
assert strategy in CompressionStrategy
|
| 353 |
|
| 354 |
def test_detect_log_output(self, router):
|
| 355 |
"""Build/test logs are detected."""
|
| 356 |
logs = generate_log_output(30)
|
| 357 |
-
strategy = router._determine_strategy(logs
|
| 358 |
assert strategy in CompressionStrategy
|
| 359 |
|
| 360 |
def test_detect_plain_text(self, router):
|
| 361 |
"""Plain text detection."""
|
| 362 |
text = "This is just plain text without any special formatting."
|
| 363 |
-
strategy = router._determine_strategy(text
|
| 364 |
-
assert strategy in CompressionStrategy
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
# =============================================================================
|
| 368 |
-
# TestSourceHintRouting
|
| 369 |
-
# =============================================================================
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
class TestSourceHintRouting:
|
| 373 |
-
"""Tests for source hint-based routing."""
|
| 374 |
-
|
| 375 |
-
def test_file_hint_routes_appropriately(self, router):
|
| 376 |
-
"""file:*.py hint influences routing."""
|
| 377 |
-
content = generate_python_code(3)
|
| 378 |
-
strategy = router._determine_strategy(content, "file:/src/auth.py")
|
| 379 |
-
# Should return a valid strategy
|
| 380 |
-
assert strategy in CompressionStrategy
|
| 381 |
-
|
| 382 |
-
def test_grep_hint_routes_appropriately(self, router):
|
| 383 |
-
"""tool:grep hint influences routing."""
|
| 384 |
-
content = generate_search_results(5)
|
| 385 |
-
strategy = router._determine_strategy(content, "tool:grep")
|
| 386 |
assert strategy in CompressionStrategy
|
| 387 |
|
| 388 |
|
|
@@ -430,14 +361,6 @@ class TestContentRouter:
|
|
| 430 |
assert result.original == content
|
| 431 |
assert result.strategy_used is not None
|
| 432 |
|
| 433 |
-
def test_compress_with_source_hint(self, router):
|
| 434 |
-
"""Source hint influences routing decision."""
|
| 435 |
-
content = generate_python_code(5)
|
| 436 |
-
result = router.compress(content, source_hint="file:/src/auth.py")
|
| 437 |
-
|
| 438 |
-
# Should return a valid result
|
| 439 |
-
assert isinstance(result, RouterCompressionResult)
|
| 440 |
-
|
| 441 |
def test_name_property(self, router):
|
| 442 |
"""Router has correct name."""
|
| 443 |
assert router.name == "content_router"
|
|
@@ -515,7 +438,7 @@ class TestCompressorDisabling:
|
|
| 515 |
code = generate_python_code(10)
|
| 516 |
|
| 517 |
# Should not crash
|
| 518 |
-
result = router.compress(code
|
| 519 |
assert result is not None
|
| 520 |
|
| 521 |
def test_config_accepts_disable_search_compression(self):
|
|
@@ -528,7 +451,7 @@ class TestCompressorDisabling:
|
|
| 528 |
search_results = generate_search_results(10)
|
| 529 |
|
| 530 |
# Should not crash
|
| 531 |
-
result = router.compress(search_results
|
| 532 |
assert result is not None
|
| 533 |
|
| 534 |
def test_config_accepts_disable_log_compression(self):
|
|
@@ -541,7 +464,7 @@ class TestCompressorDisabling:
|
|
| 541 |
logs = generate_log_output(30)
|
| 542 |
|
| 543 |
# Should not crash
|
| 544 |
-
result = router.compress(logs
|
| 545 |
assert result is not None
|
| 546 |
|
| 547 |
|
|
@@ -570,18 +493,6 @@ class TestEdgeCases:
|
|
| 570 |
result = router.compress(content)
|
| 571 |
assert result is not None
|
| 572 |
|
| 573 |
-
def test_null_source_hint(self, router):
|
| 574 |
-
"""Null source hint doesn't crash."""
|
| 575 |
-
content = generate_python_code(5)
|
| 576 |
-
result = router.compress(content, source_hint=None)
|
| 577 |
-
assert result is not None
|
| 578 |
-
|
| 579 |
-
def test_empty_source_hint(self, router):
|
| 580 |
-
"""Empty source hint doesn't crash."""
|
| 581 |
-
content = generate_python_code(5)
|
| 582 |
-
result = router.compress(content, source_hint="")
|
| 583 |
-
assert result is not None
|
| 584 |
-
|
| 585 |
|
| 586 |
# =============================================================================
|
| 587 |
# TestRoutingLog
|
|
|
|
| 4 |
- ContentRouterConfig: Configuration validation and defaults
|
| 5 |
- ContentRouter: Core routing functionality
|
| 6 |
- Strategy detection: Code, JSON, search, logs, text
|
|
|
|
| 7 |
- Mixed content handling: Split, route, reassemble
|
| 8 |
- Transform interface: apply(), should_apply() methods
|
| 9 |
"""
|
|
|
|
| 17 |
ContentRouterConfig,
|
| 18 |
RouterCompressionResult,
|
| 19 |
RoutingDecision,
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
# =============================================================================
|
|
|
|
| 277 |
assert result.savings_percentage == 0.0
|
| 278 |
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
# =============================================================================
|
| 281 |
# TestStrategyDetection
|
| 282 |
# =============================================================================
|
|
|
|
| 288 |
def test_detect_python_code(self, router):
|
| 289 |
"""Python code is detected."""
|
| 290 |
code = generate_python_code(5)
|
| 291 |
+
strategy = router._determine_strategy(code)
|
| 292 |
# Should be either CODE_AWARE or fallback
|
| 293 |
assert strategy in CompressionStrategy
|
| 294 |
|
| 295 |
def test_detect_json_content(self, router):
|
| 296 |
"""JSON content is detected."""
|
| 297 |
json_data = generate_json_data(20)
|
| 298 |
+
strategy = router._determine_strategy(json_data)
|
| 299 |
assert strategy in CompressionStrategy
|
| 300 |
|
| 301 |
def test_detect_search_results(self, router):
|
| 302 |
"""Search/grep results are detected."""
|
| 303 |
search_results = generate_search_results(10)
|
| 304 |
+
strategy = router._determine_strategy(search_results)
|
| 305 |
assert strategy in CompressionStrategy
|
| 306 |
|
| 307 |
def test_detect_log_output(self, router):
|
| 308 |
"""Build/test logs are detected."""
|
| 309 |
logs = generate_log_output(30)
|
| 310 |
+
strategy = router._determine_strategy(logs)
|
| 311 |
assert strategy in CompressionStrategy
|
| 312 |
|
| 313 |
def test_detect_plain_text(self, router):
|
| 314 |
"""Plain text detection."""
|
| 315 |
text = "This is just plain text without any special formatting."
|
| 316 |
+
strategy = router._determine_strategy(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
assert strategy in CompressionStrategy
|
| 318 |
|
| 319 |
|
|
|
|
| 361 |
assert result.original == content
|
| 362 |
assert result.strategy_used is not None
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
def test_name_property(self, router):
|
| 365 |
"""Router has correct name."""
|
| 366 |
assert router.name == "content_router"
|
|
|
|
| 438 |
code = generate_python_code(10)
|
| 439 |
|
| 440 |
# Should not crash
|
| 441 |
+
result = router.compress(code)
|
| 442 |
assert result is not None
|
| 443 |
|
| 444 |
def test_config_accepts_disable_search_compression(self):
|
|
|
|
| 451 |
search_results = generate_search_results(10)
|
| 452 |
|
| 453 |
# Should not crash
|
| 454 |
+
result = router.compress(search_results)
|
| 455 |
assert result is not None
|
| 456 |
|
| 457 |
def test_config_accepts_disable_log_compression(self):
|
|
|
|
| 464 |
logs = generate_log_output(30)
|
| 465 |
|
| 466 |
# Should not crash
|
| 467 |
+
result = router.compress(logs)
|
| 468 |
assert result is not None
|
| 469 |
|
| 470 |
|
|
|
|
| 493 |
result = router.compress(content)
|
| 494 |
assert result is not None
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
|
| 497 |
# =============================================================================
|
| 498 |
# TestRoutingLog
|
tests/test_transforms/test_intelligent_context.py
CHANGED
|
@@ -1017,49 +1017,6 @@ class TestCompressFirstStrategy:
|
|
| 1017 |
router2 = manager._get_content_router()
|
| 1018 |
assert router is router2
|
| 1019 |
|
| 1020 |
-
def test_source_hint_extraction(self):
|
| 1021 |
-
"""Source hints should be extracted from tool calls."""
|
| 1022 |
-
manager = IntelligentContextManager()
|
| 1023 |
-
|
| 1024 |
-
messages = [
|
| 1025 |
-
{
|
| 1026 |
-
"role": "assistant",
|
| 1027 |
-
"tool_calls": [
|
| 1028 |
-
{
|
| 1029 |
-
"id": "call_1",
|
| 1030 |
-
"function": {
|
| 1031 |
-
"name": "Read",
|
| 1032 |
-
"arguments": '{"file_path": "/src/main.py"}',
|
| 1033 |
-
},
|
| 1034 |
-
}
|
| 1035 |
-
],
|
| 1036 |
-
},
|
| 1037 |
-
{
|
| 1038 |
-
"role": "assistant",
|
| 1039 |
-
"tool_calls": [
|
| 1040 |
-
{
|
| 1041 |
-
"id": "call_2",
|
| 1042 |
-
"function": {
|
| 1043 |
-
"name": "Grep",
|
| 1044 |
-
"arguments": '{"pattern": "def"}',
|
| 1045 |
-
},
|
| 1046 |
-
}
|
| 1047 |
-
],
|
| 1048 |
-
},
|
| 1049 |
-
]
|
| 1050 |
-
|
| 1051 |
-
# Test file read hint
|
| 1052 |
-
hint1 = manager._get_tool_source_hint(messages, "call_1")
|
| 1053 |
-
assert "file:" in hint1 or hint1 == "" # May not have content_router import
|
| 1054 |
-
|
| 1055 |
-
# Test grep hint
|
| 1056 |
-
hint2 = manager._get_tool_source_hint(messages, "call_2")
|
| 1057 |
-
assert "grep" in hint2.lower() or hint2 == ""
|
| 1058 |
-
|
| 1059 |
-
# Test unknown tool call
|
| 1060 |
-
hint3 = manager._get_tool_source_hint(messages, "unknown")
|
| 1061 |
-
assert hint3 == ""
|
| 1062 |
-
|
| 1063 |
|
| 1064 |
class TestCompressFirstWithContentBlocks:
|
| 1065 |
"""Tests for COMPRESS_FIRST with Anthropic-style content blocks."""
|
|
|
|
| 1017 |
router2 = manager._get_content_router()
|
| 1018 |
assert router is router2
|
| 1019 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1020 |
|
| 1021 |
class TestCompressFirstWithContentBlocks:
|
| 1022 |
"""Tests for COMPRESS_FIRST with Anthropic-style content blocks."""
|