Spaces:
Build error
Build error
Commit ·
e4a41fa
1
Parent(s): 55814fe
Fix all ruff lint and format errors for CI
Browse files- Fix E402: Move module-level imports to top of file
- Fix F401: Add noqa for availability check imports
- Fix F402: Rename loop variables shadowing imports
- Fix E722: Replace bare except with except Exception
- Fix B904: Add exception chaining (from e)
- Fix F811: Remove duplicate imports
- Fix B027: Add noqa for empty close() method
- Fix E741: Rename ambiguous variable l -> label
- Fix I001: Import sorting issues
- Apply ruff format to all 106 files
All 902 tests pass.
This view is limited to 50 files because it contains too many changes. See raw diff
- benchmarks/__init__.py +4 -4
- benchmarks/adversarial_ccr_tests.py +1939 -0
- benchmarks/agent_cost_benchmark.py +804 -0
- benchmarks/bench_relevance.py +5 -5
- benchmarks/bench_transforms.py +19 -7
- benchmarks/ccr_regression_benchmark.py +828 -0
- benchmarks/conftest.py +13 -9
- benchmarks/dynamic_detector_benchmark.py +41 -33
- benchmarks/run_benchmarks.py +17 -18
- benchmarks/scenarios/__init__.py +4 -4
- benchmarks/scenarios/conversations.py +120 -81
- benchmarks/scenarios/tool_outputs.py +22 -15
- docs/HEADROOM_DEEP_ANALYSIS.md +914 -0
- docs/HEADROOM_FEATURES.md +891 -0
- docs/PATH_TO_10_OUT_OF_10.md +661 -0
- examples/anthropic_example.py +6 -2
- examples/langchain_before_after.py +63 -44
- examples/langchain_demo/mock_tools.py +43 -30
- examples/langchain_demo/run_comparison.py +67 -35
- examples/langchain_demo/show_compression.py +70 -44
- examples/langchain_demo/verify_errors_kept.py +18 -9
- examples/mcp_demo/mock_mcp_servers.py +120 -90
- examples/mcp_demo/run_agent_eval.py +172 -76
- examples/mcp_demo/show_before_after.py +10 -10
- examples/mcp_demo/show_compression.py +4 -6
- examples/real_world_eval.py +210 -156
- examples/real_world_openai_eval.py +343 -266
- examples/smart_vs_naive_eval.py +96 -82
- headroom/cache/__init__.py +4 -4
- headroom/cache/anthropic.py +37 -38
- headroom/cache/base.py +8 -5
- headroom/cache/compression_feedback.py +38 -29
- headroom/cache/compression_store.py +18 -28
- headroom/cache/dynamic_detector.py +200 -134
- headroom/cache/google.py +11 -23
- headroom/cache/openai.py +8 -15
- headroom/cache/registry.py +6 -12
- headroom/cache/semantic.py +6 -3
- headroom/ccr/__init__.py +1 -0
- headroom/ccr/mcp_server.py +28 -22
- headroom/ccr/tool_injection.py +13 -8
- headroom/cli.py +8 -5
- headroom/client.py +17 -28
- headroom/config.py +3 -1
- headroom/exceptions.py +8 -0
- headroom/integrations/langchain.py +82 -60
- headroom/integrations/mcp.py +11 -6
- headroom/models/registry.py +1 -1
- headroom/pricing/registry.py +2 -0
- headroom/providers/anthropic.py +12 -12
benchmarks/__init__.py
CHANGED
|
@@ -23,16 +23,16 @@ Performance Targets:
|
|
| 23 |
|
| 24 |
__version__ = "0.2.0"
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
from .scenarios.tool_outputs import (
|
| 27 |
generate_api_responses,
|
| 28 |
generate_database_rows,
|
| 29 |
generate_log_entries,
|
| 30 |
generate_search_results,
|
| 31 |
)
|
| 32 |
-
from .scenarios.conversations import (
|
| 33 |
-
generate_agentic_conversation,
|
| 34 |
-
generate_rag_conversation,
|
| 35 |
-
)
|
| 36 |
|
| 37 |
__all__ = [
|
| 38 |
# Data generators
|
|
|
|
| 23 |
|
| 24 |
__version__ = "0.2.0"
|
| 25 |
|
| 26 |
+
from .scenarios.conversations import (
|
| 27 |
+
generate_agentic_conversation,
|
| 28 |
+
generate_rag_conversation,
|
| 29 |
+
)
|
| 30 |
from .scenarios.tool_outputs import (
|
| 31 |
generate_api_responses,
|
| 32 |
generate_database_rows,
|
| 33 |
generate_log_entries,
|
| 34 |
generate_search_results,
|
| 35 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
__all__ = [
|
| 38 |
# Data generators
|
benchmarks/adversarial_ccr_tests.py
ADDED
|
@@ -0,0 +1,1939 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Adversarial CCR Tests - Designed to BREAK Our Assumptions
|
| 4 |
+
|
| 5 |
+
These tests are intentionally malicious, edge-casey, and designed to expose
|
| 6 |
+
weaknesses in our compression and retrieval logic.
|
| 7 |
+
|
| 8 |
+
Categories:
|
| 9 |
+
1. SEMANTIC ATTACKS: Data that tricks our heuristics
|
| 10 |
+
2. BOUNDARY CONDITIONS: Edge cases at limits
|
| 11 |
+
3. INJECTION ATTACKS: Malformed data designed to break parsing
|
| 12 |
+
4. RACE CONDITIONS: Concurrency attacks
|
| 13 |
+
5. MEMORY PRESSURE: Resource exhaustion
|
| 14 |
+
6. DECEPTIVE DATA: Items that look like one thing but are another
|
| 15 |
+
|
| 16 |
+
Run with: python benchmarks/adversarial_ccr_tests.py
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import concurrent.futures
|
| 22 |
+
import gc
|
| 23 |
+
import hashlib
|
| 24 |
+
import json
|
| 25 |
+
import random
|
| 26 |
+
import sys
|
| 27 |
+
import threading
|
| 28 |
+
import time
|
| 29 |
+
import uuid
|
| 30 |
+
from dataclasses import dataclass, field
|
| 31 |
+
from typing import Any
|
| 32 |
+
|
| 33 |
+
from headroom.cache.compression_feedback import (
|
| 34 |
+
get_compression_feedback,
|
| 35 |
+
reset_compression_feedback,
|
| 36 |
+
)
|
| 37 |
+
from headroom.cache.compression_store import (
|
| 38 |
+
CompressionStore,
|
| 39 |
+
RetrievalEvent,
|
| 40 |
+
get_compression_store,
|
| 41 |
+
reset_compression_store,
|
| 42 |
+
)
|
| 43 |
+
from headroom.transforms.smart_crusher import (
|
| 44 |
+
SmartCrusherConfig,
|
| 45 |
+
smart_crush_tool_output,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class AdversarialResult:
|
| 51 |
+
"""Result from an adversarial test."""
|
| 52 |
+
|
| 53 |
+
name: str
|
| 54 |
+
category: str
|
| 55 |
+
passed: bool = False
|
| 56 |
+
expected_behavior: str = ""
|
| 57 |
+
actual_behavior: str = ""
|
| 58 |
+
severity: str = "medium" # low, medium, high, critical
|
| 59 |
+
details: dict[str, Any] = field(default_factory=dict)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def run_test(func) -> AdversarialResult:
|
| 63 |
+
"""Run a test and catch any exceptions."""
|
| 64 |
+
try:
|
| 65 |
+
return func()
|
| 66 |
+
except Exception as e:
|
| 67 |
+
return AdversarialResult(
|
| 68 |
+
name=func.__name__,
|
| 69 |
+
category="exception",
|
| 70 |
+
passed=False,
|
| 71 |
+
expected_behavior="Test should complete without exception",
|
| 72 |
+
actual_behavior=f"Exception: {type(e).__name__}: {str(e)[:200]}",
|
| 73 |
+
severity="critical",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# =============================================================================
|
| 78 |
+
# CATEGORY 1: SEMANTIC ATTACKS
|
| 79 |
+
# =============================================================================
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_all_items_are_errors() -> AdversarialResult:
|
| 83 |
+
"""
|
| 84 |
+
ATTACK: Every single item is an error.
|
| 85 |
+
|
| 86 |
+
If we keep ALL errors, we keep everything = no compression.
|
| 87 |
+
What SHOULD happen? Keep all? Sample errors? Fail gracefully?
|
| 88 |
+
"""
|
| 89 |
+
result = AdversarialResult(
|
| 90 |
+
name="All Items Are Errors",
|
| 91 |
+
category="semantic",
|
| 92 |
+
expected_behavior="Should handle gracefully, possibly skip compression",
|
| 93 |
+
severity="high",
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# 1000 items, ALL are errors
|
| 97 |
+
items = [
|
| 98 |
+
{
|
| 99 |
+
"id": i,
|
| 100 |
+
"status": "error",
|
| 101 |
+
"error_code": 500 + (i % 50),
|
| 102 |
+
"message": f"Error at position {i}: something went wrong",
|
| 103 |
+
}
|
| 104 |
+
for i in range(1000)
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 108 |
+
original_json = json.dumps(items)
|
| 109 |
+
|
| 110 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 111 |
+
compressed = json.loads(compressed_json)
|
| 112 |
+
|
| 113 |
+
# What happened?
|
| 114 |
+
if len(compressed) == 1000:
|
| 115 |
+
result.actual_behavior = "Kept ALL 1000 items (no compression when all errors)"
|
| 116 |
+
result.passed = True # This is actually correct behavior!
|
| 117 |
+
elif len(compressed) == 15:
|
| 118 |
+
result.actual_behavior = f"Compressed to 15 items, lost {1000 - 15} errors!"
|
| 119 |
+
result.passed = False
|
| 120 |
+
else:
|
| 121 |
+
result.actual_behavior = f"Compressed to {len(compressed)} items"
|
| 122 |
+
result.passed = len(compressed) >= 100 # Should keep most errors
|
| 123 |
+
|
| 124 |
+
result.details = {
|
| 125 |
+
"original": 1000,
|
| 126 |
+
"compressed": len(compressed),
|
| 127 |
+
"reason": reason,
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
return result
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_error_keyword_in_normal_data() -> AdversarialResult:
|
| 134 |
+
"""
|
| 135 |
+
ATTACK: Normal items contain "error" keyword in benign context.
|
| 136 |
+
|
| 137 |
+
"The error rate for this metric is 0.001%" - NOT an error!
|
| 138 |
+
"Error handling documentation" - NOT an error!
|
| 139 |
+
"""
|
| 140 |
+
result = AdversarialResult(
|
| 141 |
+
name="Error Keyword False Positive",
|
| 142 |
+
category="semantic",
|
| 143 |
+
expected_behavior="Should NOT treat benign 'error' mentions as errors",
|
| 144 |
+
severity="medium",
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
items = []
|
| 148 |
+
# 100 normal items with "error" in benign context
|
| 149 |
+
for i in range(100):
|
| 150 |
+
items.append(
|
| 151 |
+
{
|
| 152 |
+
"id": i,
|
| 153 |
+
"status": "success", # Clearly success!
|
| 154 |
+
"message": random.choice(
|
| 155 |
+
[
|
| 156 |
+
f"Error rate: 0.00{i}%",
|
| 157 |
+
f"Error handling improved by {i}%",
|
| 158 |
+
f"Zero errors detected in batch {i}",
|
| 159 |
+
f"Error-free operation for {i} hours",
|
| 160 |
+
"Documentation: How to handle errors",
|
| 161 |
+
]
|
| 162 |
+
),
|
| 163 |
+
"value": i,
|
| 164 |
+
}
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Add 3 REAL errors
|
| 168 |
+
real_error_ids = [25, 50, 75]
|
| 169 |
+
for idx in real_error_ids:
|
| 170 |
+
items[idx] = {
|
| 171 |
+
"id": idx,
|
| 172 |
+
"status": "error", # This is a REAL error
|
| 173 |
+
"message": f"CRITICAL: System failure at {idx}",
|
| 174 |
+
"error_code": 500,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 178 |
+
original_json = json.dumps(items)
|
| 179 |
+
|
| 180 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 181 |
+
compressed = json.loads(compressed_json)
|
| 182 |
+
|
| 183 |
+
# Count how many items with "error" in message were kept
|
| 184 |
+
items_with_error_word = len(
|
| 185 |
+
[item for item in compressed if "error" in str(item.get("message", "")).lower()]
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Count real errors kept
|
| 189 |
+
real_errors_kept = len([item for item in compressed if item.get("status") == "error"])
|
| 190 |
+
|
| 191 |
+
# False positives (keeping non-errors) are OK - conservative is good
|
| 192 |
+
# False negatives (missing real errors) are NOT OK
|
| 193 |
+
if real_errors_kept < 3:
|
| 194 |
+
result.actual_behavior = (
|
| 195 |
+
f"Only kept {real_errors_kept}/3 real errors - missed actual errors!"
|
| 196 |
+
)
|
| 197 |
+
result.passed = False
|
| 198 |
+
else:
|
| 199 |
+
# Keeping extra items with "error" word is fine - better safe than sorry
|
| 200 |
+
result.actual_behavior = f"Kept all {real_errors_kept} real errors (+ {items_with_error_word} with 'error' word - conservative is OK)"
|
| 201 |
+
result.passed = True
|
| 202 |
+
|
| 203 |
+
result.details = {
|
| 204 |
+
"total_compressed": len(compressed),
|
| 205 |
+
"real_errors_kept": real_errors_kept,
|
| 206 |
+
"items_with_error_word": items_with_error_word,
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
return result
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def test_needle_looks_exactly_like_hay() -> AdversarialResult:
|
| 213 |
+
"""
|
| 214 |
+
ATTACK: The critical item has NO distinguishing features.
|
| 215 |
+
|
| 216 |
+
In a list of 1000 users, user #456 is the one we need.
|
| 217 |
+
User #456 looks EXACTLY like every other user.
|
| 218 |
+
"""
|
| 219 |
+
result = AdversarialResult(
|
| 220 |
+
name="Needle Identical to Hay",
|
| 221 |
+
category="semantic",
|
| 222 |
+
expected_behavior="CCR retrieval should still find specific item by ID",
|
| 223 |
+
severity="high",
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
reset_compression_store()
|
| 227 |
+
store = get_compression_store()
|
| 228 |
+
|
| 229 |
+
# 1000 identical-looking users
|
| 230 |
+
target_id = 456
|
| 231 |
+
items = [
|
| 232 |
+
{
|
| 233 |
+
"user_id": i,
|
| 234 |
+
"name": f"User {i}",
|
| 235 |
+
"status": "active",
|
| 236 |
+
"created": "2025-01-01",
|
| 237 |
+
}
|
| 238 |
+
for i in range(1000)
|
| 239 |
+
]
|
| 240 |
+
|
| 241 |
+
original_json = json.dumps(items)
|
| 242 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 243 |
+
|
| 244 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 245 |
+
|
| 246 |
+
# Store for CCR
|
| 247 |
+
hash_key = store.store(
|
| 248 |
+
original=original_json,
|
| 249 |
+
compressed=compressed_json,
|
| 250 |
+
original_item_count=1000,
|
| 251 |
+
compressed_item_count=15,
|
| 252 |
+
tool_name="user_search",
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# Try to find user 456 via search
|
| 256 |
+
search_results = store.search(hash_key, "user_id 456")
|
| 257 |
+
|
| 258 |
+
found_target = any(item.get("user_id") == target_id for item in search_results)
|
| 259 |
+
|
| 260 |
+
if found_target:
|
| 261 |
+
result.actual_behavior = "Found target user via CCR search"
|
| 262 |
+
result.passed = True
|
| 263 |
+
else:
|
| 264 |
+
# Try full retrieval as fallback
|
| 265 |
+
entry = store.retrieve(hash_key)
|
| 266 |
+
if entry:
|
| 267 |
+
all_items = json.loads(entry.original_content)
|
| 268 |
+
target_in_original = any(item.get("user_id") == target_id for item in all_items)
|
| 269 |
+
if target_in_original:
|
| 270 |
+
result.actual_behavior = "Search failed, but full retrieval works"
|
| 271 |
+
result.passed = True # CCR still provides recovery path
|
| 272 |
+
else:
|
| 273 |
+
result.actual_behavior = "Data lost entirely!"
|
| 274 |
+
result.passed = False
|
| 275 |
+
else:
|
| 276 |
+
result.actual_behavior = "CCR cache miss - data not found"
|
| 277 |
+
result.passed = False
|
| 278 |
+
|
| 279 |
+
result.details = {
|
| 280 |
+
"target_id": target_id,
|
| 281 |
+
"search_results": len(search_results),
|
| 282 |
+
"found_target": found_target,
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
return result
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def test_anomaly_in_string_not_number() -> AdversarialResult:
|
| 289 |
+
"""
|
| 290 |
+
ATTACK: Anomaly is in a string field, not numeric.
|
| 291 |
+
|
| 292 |
+
999 items: region="us-east-1"
|
| 293 |
+
1 item: region="DEPRECATED-DO-NOT-USE"
|
| 294 |
+
|
| 295 |
+
SmartCrusher detects numeric anomalies, but what about string outliers?
|
| 296 |
+
"""
|
| 297 |
+
result = AdversarialResult(
|
| 298 |
+
name="String Anomaly Detection",
|
| 299 |
+
category="semantic",
|
| 300 |
+
expected_behavior="Should detect or preserve string outliers",
|
| 301 |
+
severity="medium",
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
items = []
|
| 305 |
+
anomaly_idx = 500
|
| 306 |
+
|
| 307 |
+
for i in range(1000):
|
| 308 |
+
if i == anomaly_idx:
|
| 309 |
+
items.append(
|
| 310 |
+
{
|
| 311 |
+
"id": i,
|
| 312 |
+
"region": "DEPRECATED-DO-NOT-USE-CRITICAL-MIGRATION-REQUIRED",
|
| 313 |
+
"status": "active",
|
| 314 |
+
}
|
| 315 |
+
)
|
| 316 |
+
else:
|
| 317 |
+
items.append(
|
| 318 |
+
{
|
| 319 |
+
"id": i,
|
| 320 |
+
"region": "us-east-1",
|
| 321 |
+
"status": "active",
|
| 322 |
+
}
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
config = SmartCrusherConfig(max_items_after_crush=20)
|
| 326 |
+
original_json = json.dumps(items)
|
| 327 |
+
|
| 328 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 329 |
+
compressed = json.loads(compressed_json)
|
| 330 |
+
|
| 331 |
+
# Check if anomaly was preserved
|
| 332 |
+
anomaly_preserved = any("DEPRECATED" in str(item.get("region", "")) for item in compressed)
|
| 333 |
+
|
| 334 |
+
if anomaly_preserved:
|
| 335 |
+
result.actual_behavior = "String anomaly was preserved"
|
| 336 |
+
result.passed = True
|
| 337 |
+
else:
|
| 338 |
+
result.actual_behavior = "String anomaly was LOST - only numeric anomalies detected"
|
| 339 |
+
result.passed = False
|
| 340 |
+
|
| 341 |
+
result.details = {
|
| 342 |
+
"compressed_count": len(compressed),
|
| 343 |
+
"anomaly_preserved": anomaly_preserved,
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
return result
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# =============================================================================
|
| 350 |
+
# CATEGORY 2: BOUNDARY CONDITIONS
|
| 351 |
+
# =============================================================================
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def test_empty_array() -> AdversarialResult:
|
| 355 |
+
"""
|
| 356 |
+
ATTACK: Empty array input.
|
| 357 |
+
"""
|
| 358 |
+
result = AdversarialResult(
|
| 359 |
+
name="Empty Array",
|
| 360 |
+
category="boundary",
|
| 361 |
+
expected_behavior="Should return empty array unchanged",
|
| 362 |
+
severity="low",
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
config = SmartCrusherConfig()
|
| 366 |
+
compressed_json, was_modified, reason = smart_crush_tool_output("[]", config)
|
| 367 |
+
|
| 368 |
+
if compressed_json == "[]" and not was_modified:
|
| 369 |
+
result.actual_behavior = "Correctly handled empty array"
|
| 370 |
+
result.passed = True
|
| 371 |
+
else:
|
| 372 |
+
result.actual_behavior = f"Unexpected result: {compressed_json[:100]}"
|
| 373 |
+
result.passed = False
|
| 374 |
+
|
| 375 |
+
return result
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def test_single_item_array() -> AdversarialResult:
|
| 379 |
+
"""
|
| 380 |
+
ATTACK: Array with exactly 1 item.
|
| 381 |
+
"""
|
| 382 |
+
result = AdversarialResult(
|
| 383 |
+
name="Single Item Array",
|
| 384 |
+
category="boundary",
|
| 385 |
+
expected_behavior="Should return single item unchanged",
|
| 386 |
+
severity="low",
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
items = [{"id": 1, "value": "only_one"}]
|
| 390 |
+
config = SmartCrusherConfig()
|
| 391 |
+
|
| 392 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(json.dumps(items), config)
|
| 393 |
+
compressed = json.loads(compressed_json)
|
| 394 |
+
|
| 395 |
+
if len(compressed) == 1 and compressed[0].get("id") == 1:
|
| 396 |
+
result.actual_behavior = "Single item preserved"
|
| 397 |
+
result.passed = True
|
| 398 |
+
else:
|
| 399 |
+
result.actual_behavior = f"Unexpected: {len(compressed)} items"
|
| 400 |
+
result.passed = False
|
| 401 |
+
|
| 402 |
+
return result
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def test_exactly_max_items() -> AdversarialResult:
|
| 406 |
+
"""
|
| 407 |
+
ATTACK: Array with exactly max_items_after_crush items.
|
| 408 |
+
"""
|
| 409 |
+
result = AdversarialResult(
|
| 410 |
+
name="Exactly Max Items",
|
| 411 |
+
category="boundary",
|
| 412 |
+
expected_behavior="Should not compress when at exact limit",
|
| 413 |
+
severity="low",
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 417 |
+
items = [{"id": i} for i in range(15)] # Exactly 15
|
| 418 |
+
|
| 419 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(json.dumps(items), config)
|
| 420 |
+
compressed = json.loads(compressed_json)
|
| 421 |
+
|
| 422 |
+
if len(compressed) == 15:
|
| 423 |
+
result.actual_behavior = "Kept all 15 items as expected"
|
| 424 |
+
result.passed = True
|
| 425 |
+
else:
|
| 426 |
+
result.actual_behavior = f"Changed count: {len(compressed)}"
|
| 427 |
+
result.passed = False
|
| 428 |
+
|
| 429 |
+
return result
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def test_max_items_plus_one() -> AdversarialResult:
|
| 433 |
+
"""
|
| 434 |
+
ATTACK: Array with max_items + 1.
|
| 435 |
+
|
| 436 |
+
IMPORTANT: If data has high uniqueness and no importance signal,
|
| 437 |
+
crushability analysis correctly skips compression to avoid data loss.
|
| 438 |
+
This is the RIGHT behavior - don't blindly compress unique entities.
|
| 439 |
+
"""
|
| 440 |
+
result = AdversarialResult(
|
| 441 |
+
name="Max Items Plus One",
|
| 442 |
+
category="boundary",
|
| 443 |
+
expected_behavior="Skip compression for unique entities OR compress with signal",
|
| 444 |
+
severity="low",
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
config = SmartCrusherConfig(max_items_after_crush=15, min_items_to_analyze=5)
|
| 448 |
+
# Create items WITH a score field so compression can determine importance
|
| 449 |
+
items = [{"id": i, "value": f"item_{i}", "score": 1.0 - (i / 100)} for i in range(16)]
|
| 450 |
+
|
| 451 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 452 |
+
compressed = json.loads(compressed_json)
|
| 453 |
+
|
| 454 |
+
result.actual_behavior = f"Compressed to {len(compressed)} items ({reason})"
|
| 455 |
+
# With a score signal, we should compress to max_items
|
| 456 |
+
result.passed = len(compressed) <= 15
|
| 457 |
+
|
| 458 |
+
return result
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def test_hash_collision_attempt() -> AdversarialResult:
|
| 462 |
+
"""
|
| 463 |
+
ATTACK: Try to create hash collisions in CCR store.
|
| 464 |
+
|
| 465 |
+
We use SHA256[:16] - what if two different contents hash the same?
|
| 466 |
+
"""
|
| 467 |
+
result = AdversarialResult(
|
| 468 |
+
name="Hash Collision Attack",
|
| 469 |
+
category="boundary",
|
| 470 |
+
expected_behavior="Different content should not collide",
|
| 471 |
+
severity="high",
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
reset_compression_store()
|
| 475 |
+
get_compression_store()
|
| 476 |
+
|
| 477 |
+
# Store many different contents
|
| 478 |
+
hashes = set()
|
| 479 |
+
collisions = 0
|
| 480 |
+
|
| 481 |
+
for i in range(10000):
|
| 482 |
+
content = json.dumps([{"unique_id": str(uuid.uuid4()), "index": i}])
|
| 483 |
+
content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]
|
| 484 |
+
|
| 485 |
+
if content_hash in hashes:
|
| 486 |
+
collisions += 1
|
| 487 |
+
hashes.add(content_hash)
|
| 488 |
+
|
| 489 |
+
if collisions == 0:
|
| 490 |
+
result.actual_behavior = "No collisions in 10,000 entries"
|
| 491 |
+
result.passed = True
|
| 492 |
+
else:
|
| 493 |
+
result.actual_behavior = f"Found {collisions} hash collisions!"
|
| 494 |
+
result.passed = False
|
| 495 |
+
result.severity = "critical"
|
| 496 |
+
|
| 497 |
+
result.details = {"entries_tested": 10000, "collisions": collisions}
|
| 498 |
+
|
| 499 |
+
return result
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def test_ttl_exact_boundary() -> AdversarialResult:
|
| 503 |
+
"""
|
| 504 |
+
ATTACK: Retrieve at exact TTL expiration moment.
|
| 505 |
+
"""
|
| 506 |
+
result = AdversarialResult(
|
| 507 |
+
name="TTL Exact Boundary",
|
| 508 |
+
category="boundary",
|
| 509 |
+
expected_behavior="Entry should expire cleanly at TTL",
|
| 510 |
+
severity="medium",
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
reset_compression_store()
|
| 514 |
+
store = CompressionStore(default_ttl=1) # 1 second TTL
|
| 515 |
+
|
| 516 |
+
hash_key = store.store(
|
| 517 |
+
original='[{"id": 1}]',
|
| 518 |
+
compressed='[{"id": 1}]',
|
| 519 |
+
original_item_count=1,
|
| 520 |
+
compressed_item_count=1,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# Should exist immediately
|
| 524 |
+
exists_before = store.exists(hash_key)
|
| 525 |
+
|
| 526 |
+
# Wait exactly at boundary
|
| 527 |
+
time.sleep(1.05)
|
| 528 |
+
|
| 529 |
+
# Should be expired
|
| 530 |
+
exists_after = store.exists(hash_key)
|
| 531 |
+
entry = store.retrieve(hash_key)
|
| 532 |
+
|
| 533 |
+
if exists_before and not exists_after and entry is None:
|
| 534 |
+
result.actual_behavior = "TTL expiration works correctly"
|
| 535 |
+
result.passed = True
|
| 536 |
+
else:
|
| 537 |
+
result.actual_behavior = (
|
| 538 |
+
f"Before: {exists_before}, After: {exists_after}, Entry: {entry is not None}"
|
| 539 |
+
)
|
| 540 |
+
result.passed = False
|
| 541 |
+
|
| 542 |
+
return result
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
# =============================================================================
|
| 546 |
+
# CATEGORY 3: INJECTION ATTACKS
|
| 547 |
+
# =============================================================================
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def test_json_injection_in_content() -> AdversarialResult:
|
| 551 |
+
"""
|
| 552 |
+
ATTACK: JSON that tries to break our parsing.
|
| 553 |
+
"""
|
| 554 |
+
result = AdversarialResult(
|
| 555 |
+
name="JSON Injection",
|
| 556 |
+
category="injection",
|
| 557 |
+
expected_behavior="Should handle malformed JSON gracefully",
|
| 558 |
+
severity="high",
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
# Various injection attempts
|
| 562 |
+
injections = [
|
| 563 |
+
'{"id": 1, "evil": "}\\"]}', # Quote escape
|
| 564 |
+
'[{"id": 1}, null, {"id": 2}]', # Null in array
|
| 565 |
+
'[{"id": 1, "__proto__": {"admin": true}}]', # Prototype pollution
|
| 566 |
+
'[{"id": 1, "nested": {"deep": {"deeper": {"deepest": "value"}}}}]',
|
| 567 |
+
]
|
| 568 |
+
|
| 569 |
+
config = SmartCrusherConfig()
|
| 570 |
+
failures = []
|
| 571 |
+
|
| 572 |
+
for injection in injections:
|
| 573 |
+
try:
|
| 574 |
+
compressed, was_modified, _ = smart_crush_tool_output(injection, config)
|
| 575 |
+
# If it returns, it handled it
|
| 576 |
+
except Exception as e:
|
| 577 |
+
failures.append(f"{injection[:30]}: {type(e).__name__}")
|
| 578 |
+
|
| 579 |
+
if not failures:
|
| 580 |
+
result.actual_behavior = "All injection attempts handled gracefully"
|
| 581 |
+
result.passed = True
|
| 582 |
+
else:
|
| 583 |
+
result.actual_behavior = f"Failures: {failures}"
|
| 584 |
+
result.passed = False
|
| 585 |
+
|
| 586 |
+
return result
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def test_headroom_marker_collision() -> AdversarialResult:
|
| 590 |
+
"""
|
| 591 |
+
ATTACK: Input data already contains __headroom_ fields.
|
| 592 |
+
"""
|
| 593 |
+
result = AdversarialResult(
|
| 594 |
+
name="Marker Field Collision",
|
| 595 |
+
category="injection",
|
| 596 |
+
expected_behavior="Should not confuse existing __headroom_ fields with our markers",
|
| 597 |
+
severity="high",
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
# Data that already has __headroom_ fields
|
| 601 |
+
items = [
|
| 602 |
+
{
|
| 603 |
+
"id": i,
|
| 604 |
+
"__headroom_compressed": True, # Fake marker!
|
| 605 |
+
"__headroom_hash": "fakehash12345678",
|
| 606 |
+
"__headroom_stats": {"fake": True},
|
| 607 |
+
}
|
| 608 |
+
for i in range(100)
|
| 609 |
+
]
|
| 610 |
+
|
| 611 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 612 |
+
original_json = json.dumps(items)
|
| 613 |
+
|
| 614 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 615 |
+
compressed = json.loads(compressed_json)
|
| 616 |
+
|
| 617 |
+
# Check if our compression worked despite fake markers
|
| 618 |
+
if isinstance(compressed, list) and len(compressed) <= 20:
|
| 619 |
+
result.actual_behavior = "Compression worked despite fake markers"
|
| 620 |
+
result.passed = True
|
| 621 |
+
else:
|
| 622 |
+
result.actual_behavior = f"Unexpected result type or length: {type(compressed)}, {len(compressed) if isinstance(compressed, list) else 'N/A'}"
|
| 623 |
+
result.passed = False
|
| 624 |
+
|
| 625 |
+
return result
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
def test_unicode_and_emoji_handling() -> AdversarialResult:
|
| 629 |
+
"""
|
| 630 |
+
ATTACK: Unicode edge cases in content.
|
| 631 |
+
"""
|
| 632 |
+
result = AdversarialResult(
|
| 633 |
+
name="Unicode/Emoji Handling",
|
| 634 |
+
category="injection",
|
| 635 |
+
expected_behavior="Should handle Unicode correctly",
|
| 636 |
+
severity="medium",
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
items = [
|
| 640 |
+
{"id": 1, "message": "Error: 🔥 Server on fire 🔥", "status": "error"},
|
| 641 |
+
{"id": 2, "message": "成功: 操作完��", "status": "success"},
|
| 642 |
+
{"id": 3, "message": "Error: \u0000\u0001\u0002 null bytes", "status": "error"},
|
| 643 |
+
{"id": 4, "message": "Ошибка: критический сбой", "status": "error"},
|
| 644 |
+
{"id": 5, "message": "🎉🎊🎈" * 100, "status": "success"}, # Lots of emoji
|
| 645 |
+
]
|
| 646 |
+
|
| 647 |
+
for i in range(95):
|
| 648 |
+
items.append({"id": i + 6, "message": "Normal", "status": "success"})
|
| 649 |
+
|
| 650 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 651 |
+
original_json = json.dumps(items, ensure_ascii=False)
|
| 652 |
+
|
| 653 |
+
try:
|
| 654 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 655 |
+
compressed = json.loads(compressed_json)
|
| 656 |
+
|
| 657 |
+
# Check if error items with unicode were preserved
|
| 658 |
+
errors_preserved = len([item for item in compressed if item.get("status") == "error"])
|
| 659 |
+
|
| 660 |
+
result.actual_behavior = f"Handled Unicode, {errors_preserved} errors preserved"
|
| 661 |
+
result.passed = errors_preserved >= 2
|
| 662 |
+
|
| 663 |
+
except Exception as e:
|
| 664 |
+
result.actual_behavior = f"Unicode handling failed: {e}"
|
| 665 |
+
result.passed = False
|
| 666 |
+
|
| 667 |
+
return result
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
def test_extremely_long_strings() -> AdversarialResult:
|
| 671 |
+
"""
|
| 672 |
+
ATTACK: Items with extremely long string values.
|
| 673 |
+
"""
|
| 674 |
+
result = AdversarialResult(
|
| 675 |
+
name="Extremely Long Strings",
|
| 676 |
+
category="injection",
|
| 677 |
+
expected_behavior="Should handle without memory issues",
|
| 678 |
+
severity="medium",
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
# One item with a 10MB string
|
| 682 |
+
huge_string = "x" * (10 * 1024 * 1024) # 10MB
|
| 683 |
+
|
| 684 |
+
items = [
|
| 685 |
+
{"id": 0, "huge": huge_string, "status": "error"}, # Should be kept (error)
|
| 686 |
+
*[{"id": i, "normal": "small"} for i in range(1, 100)],
|
| 687 |
+
]
|
| 688 |
+
|
| 689 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 690 |
+
|
| 691 |
+
sys.getsizeof(items)
|
| 692 |
+
start_time = time.time()
|
| 693 |
+
|
| 694 |
+
try:
|
| 695 |
+
original_json = json.dumps(items)
|
| 696 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 697 |
+
|
| 698 |
+
elapsed = time.time() - start_time
|
| 699 |
+
|
| 700 |
+
if elapsed > 30:
|
| 701 |
+
result.actual_behavior = f"Took too long: {elapsed:.1f}s"
|
| 702 |
+
result.passed = False
|
| 703 |
+
else:
|
| 704 |
+
result.actual_behavior = f"Handled 10MB string in {elapsed:.1f}s"
|
| 705 |
+
result.passed = True
|
| 706 |
+
|
| 707 |
+
except MemoryError:
|
| 708 |
+
result.actual_behavior = "MemoryError on large string"
|
| 709 |
+
result.passed = False
|
| 710 |
+
result.severity = "critical"
|
| 711 |
+
finally:
|
| 712 |
+
del huge_string
|
| 713 |
+
del items
|
| 714 |
+
gc.collect()
|
| 715 |
+
|
| 716 |
+
return result
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
def test_query_injection_in_search() -> AdversarialResult:
|
| 720 |
+
"""
|
| 721 |
+
ATTACK: Malicious search query.
|
| 722 |
+
"""
|
| 723 |
+
result = AdversarialResult(
|
| 724 |
+
name="Search Query Injection",
|
| 725 |
+
category="injection",
|
| 726 |
+
expected_behavior="Should sanitize search queries",
|
| 727 |
+
severity="high",
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
reset_compression_store()
|
| 731 |
+
store = get_compression_store()
|
| 732 |
+
|
| 733 |
+
items = [{"id": i, "data": f"item {i}"} for i in range(100)]
|
| 734 |
+
|
| 735 |
+
hash_key = store.store(
|
| 736 |
+
original=json.dumps(items),
|
| 737 |
+
compressed=json.dumps(items[:10]),
|
| 738 |
+
original_item_count=100,
|
| 739 |
+
compressed_item_count=10,
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
# Various injection attempts
|
| 743 |
+
malicious_queries = [
|
| 744 |
+
"'; DROP TABLE items; --",
|
| 745 |
+
"<script>alert('xss')</script>",
|
| 746 |
+
"{{7*7}}", # Template injection
|
| 747 |
+
"${7*7}", # Expression injection
|
| 748 |
+
"\\x00\\x01\\x02", # Null bytes
|
| 749 |
+
"*" * 10000, # Long query
|
| 750 |
+
".*", # Regex wildcard
|
| 751 |
+
"(a]", # Invalid regex
|
| 752 |
+
]
|
| 753 |
+
|
| 754 |
+
failures = []
|
| 755 |
+
for query in malicious_queries:
|
| 756 |
+
try:
|
| 757 |
+
store.search(hash_key, query)
|
| 758 |
+
# If it returns without error, it handled the injection
|
| 759 |
+
except Exception as e:
|
| 760 |
+
failures.append(f"{query[:20]}: {type(e).__name__}")
|
| 761 |
+
|
| 762 |
+
if not failures:
|
| 763 |
+
result.actual_behavior = "All malicious queries handled safely"
|
| 764 |
+
result.passed = True
|
| 765 |
+
else:
|
| 766 |
+
result.actual_behavior = f"Failures: {failures}"
|
| 767 |
+
result.passed = False
|
| 768 |
+
|
| 769 |
+
return result
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
# =============================================================================
|
| 773 |
+
# CATEGORY 4: RACE CONDITIONS
|
| 774 |
+
# =============================================================================
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def test_concurrent_store_same_content() -> AdversarialResult:
|
| 778 |
+
"""
|
| 779 |
+
ATTACK: Multiple threads storing identical content simultaneously.
|
| 780 |
+
"""
|
| 781 |
+
result = AdversarialResult(
|
| 782 |
+
name="Concurrent Store Same Content",
|
| 783 |
+
category="race",
|
| 784 |
+
expected_behavior="Should handle concurrent stores without data corruption",
|
| 785 |
+
severity="high",
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
reset_compression_store()
|
| 789 |
+
store = get_compression_store()
|
| 790 |
+
|
| 791 |
+
content = json.dumps([{"id": i} for i in range(100)])
|
| 792 |
+
|
| 793 |
+
results = []
|
| 794 |
+
errors = []
|
| 795 |
+
|
| 796 |
+
def store_content():
|
| 797 |
+
try:
|
| 798 |
+
hash_key = store.store(
|
| 799 |
+
original=content,
|
| 800 |
+
compressed=content[:50],
|
| 801 |
+
original_item_count=100,
|
| 802 |
+
compressed_item_count=5,
|
| 803 |
+
)
|
| 804 |
+
results.append(hash_key)
|
| 805 |
+
except Exception as e:
|
| 806 |
+
errors.append(str(e))
|
| 807 |
+
|
| 808 |
+
# 100 concurrent stores of same content
|
| 809 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
|
| 810 |
+
futures = [executor.submit(store_content) for _ in range(100)]
|
| 811 |
+
concurrent.futures.wait(futures)
|
| 812 |
+
|
| 813 |
+
if errors:
|
| 814 |
+
result.actual_behavior = f"Errors during concurrent store: {errors[:3]}"
|
| 815 |
+
result.passed = False
|
| 816 |
+
elif len(set(results)) != 1:
|
| 817 |
+
result.actual_behavior = f"Got different hashes for same content: {set(results)}"
|
| 818 |
+
result.passed = False
|
| 819 |
+
else:
|
| 820 |
+
result.actual_behavior = "All concurrent stores returned same hash"
|
| 821 |
+
result.passed = True
|
| 822 |
+
|
| 823 |
+
return result
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
def test_concurrent_store_and_evict() -> AdversarialResult:
|
| 827 |
+
"""
|
| 828 |
+
ATTACK: Store while eviction is happening.
|
| 829 |
+
"""
|
| 830 |
+
result = AdversarialResult(
|
| 831 |
+
name="Concurrent Store and Evict",
|
| 832 |
+
category="race",
|
| 833 |
+
expected_behavior="Eviction should not corrupt concurrent stores",
|
| 834 |
+
severity="high",
|
| 835 |
+
)
|
| 836 |
+
|
| 837 |
+
reset_compression_store()
|
| 838 |
+
store = CompressionStore(max_entries=10) # Small capacity
|
| 839 |
+
|
| 840 |
+
errors = []
|
| 841 |
+
stored_hashes = []
|
| 842 |
+
|
| 843 |
+
def rapid_store(thread_id):
|
| 844 |
+
for i in range(50):
|
| 845 |
+
try:
|
| 846 |
+
content = json.dumps([{"thread": thread_id, "iteration": i}])
|
| 847 |
+
hash_key = store.store(
|
| 848 |
+
original=content,
|
| 849 |
+
compressed=content,
|
| 850 |
+
original_item_count=1,
|
| 851 |
+
compressed_item_count=1,
|
| 852 |
+
)
|
| 853 |
+
stored_hashes.append(hash_key)
|
| 854 |
+
except Exception as e:
|
| 855 |
+
errors.append(f"Thread {thread_id}, iter {i}: {e}")
|
| 856 |
+
|
| 857 |
+
# 10 threads, each storing 50 items = 500 stores with max_entries=10
|
| 858 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
| 859 |
+
futures = [executor.submit(rapid_store, i) for i in range(10)]
|
| 860 |
+
concurrent.futures.wait(futures)
|
| 861 |
+
|
| 862 |
+
if errors:
|
| 863 |
+
result.actual_behavior = f"Errors: {errors[:5]}"
|
| 864 |
+
result.passed = False
|
| 865 |
+
else:
|
| 866 |
+
result.actual_behavior = "500 stores with capacity 10 succeeded"
|
| 867 |
+
result.passed = True
|
| 868 |
+
|
| 869 |
+
return result
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
def test_concurrent_feedback_updates() -> AdversarialResult:
|
| 873 |
+
"""
|
| 874 |
+
ATTACK: Multiple threads updating feedback simultaneously.
|
| 875 |
+
"""
|
| 876 |
+
result = AdversarialResult(
|
| 877 |
+
name="Concurrent Feedback Updates",
|
| 878 |
+
category="race",
|
| 879 |
+
expected_behavior="Feedback counts should be accurate under concurrency",
|
| 880 |
+
severity="high",
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
reset_compression_feedback()
|
| 884 |
+
feedback = get_compression_feedback()
|
| 885 |
+
|
| 886 |
+
tool_name = "concurrent_test_tool"
|
| 887 |
+
expected_compressions = 1000
|
| 888 |
+
expected_retrievals = 500
|
| 889 |
+
|
| 890 |
+
def record_compressions():
|
| 891 |
+
for _ in range(expected_compressions // 10):
|
| 892 |
+
feedback.record_compression(tool_name, 100, 10)
|
| 893 |
+
|
| 894 |
+
def record_retrievals():
|
| 895 |
+
# 5 threads × 100 iterations = 500 retrievals
|
| 896 |
+
for i in range(expected_retrievals // 5):
|
| 897 |
+
event = RetrievalEvent(
|
| 898 |
+
hash=f"hash{i:012d}",
|
| 899 |
+
query=None,
|
| 900 |
+
items_retrieved=100,
|
| 901 |
+
total_items=100,
|
| 902 |
+
tool_name=tool_name,
|
| 903 |
+
timestamp=time.time(),
|
| 904 |
+
retrieval_type="full",
|
| 905 |
+
)
|
| 906 |
+
feedback.record_retrieval(event)
|
| 907 |
+
|
| 908 |
+
# 10 threads each doing compressions (1000/10=100 each), 5 doing retrievals (500/5=100 each)
|
| 909 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor:
|
| 910 |
+
futures = []
|
| 911 |
+
for _ in range(10):
|
| 912 |
+
futures.append(executor.submit(record_compressions))
|
| 913 |
+
for _ in range(5):
|
| 914 |
+
futures.append(executor.submit(record_retrievals))
|
| 915 |
+
concurrent.futures.wait(futures)
|
| 916 |
+
|
| 917 |
+
patterns = feedback.get_all_patterns()
|
| 918 |
+
pattern = patterns.get(tool_name)
|
| 919 |
+
|
| 920 |
+
if pattern is None:
|
| 921 |
+
result.actual_behavior = "Pattern not found"
|
| 922 |
+
result.passed = False
|
| 923 |
+
elif (
|
| 924 |
+
pattern.total_compressions == expected_compressions
|
| 925 |
+
and pattern.total_retrievals == expected_retrievals
|
| 926 |
+
):
|
| 927 |
+
result.actual_behavior = f"Exact counts: {pattern.total_compressions} compressions, {pattern.total_retrievals} retrievals"
|
| 928 |
+
result.passed = True
|
| 929 |
+
else:
|
| 930 |
+
result.actual_behavior = f"Count mismatch: {pattern.total_compressions} compressions (expected {expected_compressions}), {pattern.total_retrievals} retrievals (expected {expected_retrievals})"
|
| 931 |
+
result.passed = False
|
| 932 |
+
|
| 933 |
+
result.details = {
|
| 934 |
+
"expected_compressions": expected_compressions,
|
| 935 |
+
"actual_compressions": pattern.total_compressions if pattern else 0,
|
| 936 |
+
"expected_retrievals": expected_retrievals,
|
| 937 |
+
"actual_retrievals": pattern.total_retrievals if pattern else 0,
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
return result
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
# =============================================================================
|
| 944 |
+
# CATEGORY 5: DECEPTIVE DATA
|
| 945 |
+
# =============================================================================
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
def test_hidden_error_in_nested_structure() -> AdversarialResult:
|
| 949 |
+
"""
|
| 950 |
+
ATTACK: Error hidden deep in nested structure.
|
| 951 |
+
"""
|
| 952 |
+
result = AdversarialResult(
|
| 953 |
+
name="Hidden Error in Nested Structure",
|
| 954 |
+
category="deceptive",
|
| 955 |
+
expected_behavior="Should detect errors in nested objects",
|
| 956 |
+
severity="high",
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
items = []
|
| 960 |
+
error_idx = 50
|
| 961 |
+
|
| 962 |
+
for i in range(100):
|
| 963 |
+
if i == error_idx:
|
| 964 |
+
# Error hidden deep inside
|
| 965 |
+
items.append(
|
| 966 |
+
{
|
| 967 |
+
"id": i,
|
| 968 |
+
"status": "success", # Top level says success!
|
| 969 |
+
"details": {
|
| 970 |
+
"level1": {
|
| 971 |
+
"level2": {
|
| 972 |
+
"actual_status": "CRITICAL_ERROR",
|
| 973 |
+
"error": True,
|
| 974 |
+
"message": "System failure",
|
| 975 |
+
}
|
| 976 |
+
}
|
| 977 |
+
},
|
| 978 |
+
}
|
| 979 |
+
)
|
| 980 |
+
else:
|
| 981 |
+
items.append(
|
| 982 |
+
{
|
| 983 |
+
"id": i,
|
| 984 |
+
"status": "success",
|
| 985 |
+
"details": {"level1": {"level2": {"actual_status": "ok"}}},
|
| 986 |
+
}
|
| 987 |
+
)
|
| 988 |
+
|
| 989 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 990 |
+
original_json = json.dumps(items)
|
| 991 |
+
|
| 992 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 993 |
+
compressed = json.loads(compressed_json)
|
| 994 |
+
|
| 995 |
+
# Check if the nested error was preserved
|
| 996 |
+
nested_error_found = any("CRITICAL_ERROR" in json.dumps(item) for item in compressed)
|
| 997 |
+
|
| 998 |
+
if nested_error_found:
|
| 999 |
+
result.actual_behavior = "Nested error was detected and preserved"
|
| 1000 |
+
result.passed = True
|
| 1001 |
+
else:
|
| 1002 |
+
result.actual_behavior = "Nested error was LOST - only top-level status checked"
|
| 1003 |
+
result.passed = False
|
| 1004 |
+
|
| 1005 |
+
return result
|
| 1006 |
+
|
| 1007 |
+
|
| 1008 |
+
def test_misleading_score_field() -> AdversarialResult:
|
| 1009 |
+
"""
|
| 1010 |
+
ATTACK: Score field that doesn't indicate importance.
|
| 1011 |
+
|
| 1012 |
+
Items with score=0.99 are spam, items with score=0.01 are critical.
|
| 1013 |
+
"""
|
| 1014 |
+
result = AdversarialResult(
|
| 1015 |
+
name="Misleading Score Field",
|
| 1016 |
+
category="deceptive",
|
| 1017 |
+
expected_behavior="Should not blindly trust high scores",
|
| 1018 |
+
severity="medium",
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
items = []
|
| 1022 |
+
critical_indices = [25, 50, 75]
|
| 1023 |
+
|
| 1024 |
+
for i in range(100):
|
| 1025 |
+
if i in critical_indices:
|
| 1026 |
+
# LOW score but CRITICAL
|
| 1027 |
+
items.append(
|
| 1028 |
+
{
|
| 1029 |
+
"id": i,
|
| 1030 |
+
"score": 0.01, # Low score
|
| 1031 |
+
"type": "critical_alert",
|
| 1032 |
+
"message": "URGENT: Action required",
|
| 1033 |
+
}
|
| 1034 |
+
)
|
| 1035 |
+
else:
|
| 1036 |
+
# HIGH score but SPAM
|
| 1037 |
+
items.append(
|
| 1038 |
+
{
|
| 1039 |
+
"id": i,
|
| 1040 |
+
"score": 0.99, # High score
|
| 1041 |
+
"type": "spam",
|
| 1042 |
+
"message": "Buy now! Limited offer!",
|
| 1043 |
+
}
|
| 1044 |
+
)
|
| 1045 |
+
|
| 1046 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1047 |
+
original_json = json.dumps(items)
|
| 1048 |
+
|
| 1049 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 1050 |
+
compressed = json.loads(compressed_json)
|
| 1051 |
+
|
| 1052 |
+
# Check what was kept
|
| 1053 |
+
critical_kept = len([item for item in compressed if item.get("type") == "critical_alert"])
|
| 1054 |
+
spam_kept = len([item for item in compressed if item.get("type") == "spam"])
|
| 1055 |
+
|
| 1056 |
+
# We should preserve ALL critical items due to "critical" keyword detection
|
| 1057 |
+
# The remaining slots can go to high-score items - that's acceptable
|
| 1058 |
+
# The key guarantee: we NEVER lose items matching important keywords
|
| 1059 |
+
if critical_kept < 3:
|
| 1060 |
+
result.actual_behavior = f"Lost critical items! Only kept {critical_kept}/3 critical"
|
| 1061 |
+
result.passed = False
|
| 1062 |
+
else:
|
| 1063 |
+
result.actual_behavior = f"Kept all {critical_kept} critical items (plus {spam_kept} spam) - keyword detection worked"
|
| 1064 |
+
result.passed = True
|
| 1065 |
+
|
| 1066 |
+
result.details = {
|
| 1067 |
+
"critical_kept": critical_kept,
|
| 1068 |
+
"spam_kept": spam_kept,
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
return result
|
| 1072 |
+
|
| 1073 |
+
|
| 1074 |
+
def test_timestamp_anomaly_not_value() -> AdversarialResult:
|
| 1075 |
+
"""
|
| 1076 |
+
ATTACK: Anomaly in timestamp, not in measured value.
|
| 1077 |
+
|
| 1078 |
+
One entry is from the FUTURE - this is the anomaly!
|
| 1079 |
+
"""
|
| 1080 |
+
result = AdversarialResult(
|
| 1081 |
+
name="Timestamp Anomaly",
|
| 1082 |
+
category="deceptive",
|
| 1083 |
+
expected_behavior="Should detect timestamp anomalies",
|
| 1084 |
+
severity="medium",
|
| 1085 |
+
)
|
| 1086 |
+
|
| 1087 |
+
items = []
|
| 1088 |
+
anomaly_idx = 50
|
| 1089 |
+
|
| 1090 |
+
for i in range(100):
|
| 1091 |
+
if i == anomaly_idx:
|
| 1092 |
+
# Future timestamp - something is wrong!
|
| 1093 |
+
items.append(
|
| 1094 |
+
{
|
| 1095 |
+
"timestamp": "2030-01-01T00:00:00Z", # FUTURE!
|
| 1096 |
+
"value": 50, # Normal value
|
| 1097 |
+
"id": i,
|
| 1098 |
+
}
|
| 1099 |
+
)
|
| 1100 |
+
else:
|
| 1101 |
+
items.append(
|
| 1102 |
+
{
|
| 1103 |
+
"timestamp": f"2025-01-{(i % 28) + 1:02d}T{(i % 24):02d}:00:00Z",
|
| 1104 |
+
"value": 50 + (i % 10), # Normal variation
|
| 1105 |
+
"id": i,
|
| 1106 |
+
}
|
| 1107 |
+
)
|
| 1108 |
+
|
| 1109 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1110 |
+
original_json = json.dumps(items)
|
| 1111 |
+
|
| 1112 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 1113 |
+
compressed = json.loads(compressed_json)
|
| 1114 |
+
|
| 1115 |
+
# Check if future timestamp was preserved
|
| 1116 |
+
future_found = any("2030" in str(item.get("timestamp", "")) for item in compressed)
|
| 1117 |
+
|
| 1118 |
+
if future_found:
|
| 1119 |
+
result.actual_behavior = "Future timestamp anomaly preserved"
|
| 1120 |
+
result.passed = True
|
| 1121 |
+
else:
|
| 1122 |
+
result.actual_behavior = "Timestamp anomaly LOST - only value anomalies detected"
|
| 1123 |
+
result.passed = False
|
| 1124 |
+
|
| 1125 |
+
return result
|
| 1126 |
+
|
| 1127 |
+
|
| 1128 |
+
# =============================================================================
|
| 1129 |
+
# EXTREME STRESS TESTS - Designed to Break Assumptions
|
| 1130 |
+
# =============================================================================
|
| 1131 |
+
|
| 1132 |
+
|
| 1133 |
+
def test_deeply_nested_structure() -> AdversarialResult:
|
| 1134 |
+
"""
|
| 1135 |
+
ATTACK: Extremely deep nesting to cause stack overflow.
|
| 1136 |
+
|
| 1137 |
+
100 levels of nested objects containing arrays.
|
| 1138 |
+
"""
|
| 1139 |
+
result = AdversarialResult(
|
| 1140 |
+
name="Deep Nesting Attack",
|
| 1141 |
+
category="extreme",
|
| 1142 |
+
expected_behavior="Should handle deep nesting without stack overflow",
|
| 1143 |
+
severity="critical",
|
| 1144 |
+
)
|
| 1145 |
+
|
| 1146 |
+
# Build deeply nested structure
|
| 1147 |
+
depth = 100
|
| 1148 |
+
inner = [{"id": i, "value": f"leaf_{i}"} for i in range(20)]
|
| 1149 |
+
|
| 1150 |
+
current = inner
|
| 1151 |
+
for level in range(depth):
|
| 1152 |
+
current = {"level": level, "data": current}
|
| 1153 |
+
|
| 1154 |
+
try:
|
| 1155 |
+
config = SmartCrusherConfig(max_items_after_crush=10)
|
| 1156 |
+
original_json = json.dumps(current)
|
| 1157 |
+
|
| 1158 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 1159 |
+
result.actual_behavior = f"Handled {depth} levels of nesting"
|
| 1160 |
+
result.passed = True
|
| 1161 |
+
except RecursionError as e:
|
| 1162 |
+
result.actual_behavior = f"Stack overflow at depth {depth}: {e}"
|
| 1163 |
+
result.passed = False
|
| 1164 |
+
except Exception as e:
|
| 1165 |
+
result.actual_behavior = f"Unexpected error: {type(e).__name__}: {e}"
|
| 1166 |
+
result.passed = False
|
| 1167 |
+
|
| 1168 |
+
return result
|
| 1169 |
+
|
| 1170 |
+
|
| 1171 |
+
def test_nan_infinity_scores() -> AdversarialResult:
|
| 1172 |
+
"""
|
| 1173 |
+
ATTACK: Score fields with NaN, Infinity, -Infinity.
|
| 1174 |
+
|
| 1175 |
+
These are valid JSON when serialized from Python but break comparisons.
|
| 1176 |
+
"""
|
| 1177 |
+
result = AdversarialResult(
|
| 1178 |
+
name="NaN/Infinity Scores",
|
| 1179 |
+
category="extreme",
|
| 1180 |
+
expected_behavior="Should handle special float values gracefully",
|
| 1181 |
+
severity="high",
|
| 1182 |
+
)
|
| 1183 |
+
|
| 1184 |
+
items = []
|
| 1185 |
+
for i in range(50):
|
| 1186 |
+
score = i / 10.0
|
| 1187 |
+
if i == 10:
|
| 1188 |
+
score = float("nan")
|
| 1189 |
+
elif i == 20:
|
| 1190 |
+
score = float("inf")
|
| 1191 |
+
elif i == 30:
|
| 1192 |
+
score = float("-inf")
|
| 1193 |
+
|
| 1194 |
+
items.append({"id": i, "score": score, "name": f"item_{i}"})
|
| 1195 |
+
|
| 1196 |
+
try:
|
| 1197 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1198 |
+
# Note: json.dumps will fail on NaN/Inf by default, use allow_nan
|
| 1199 |
+
original_json = json.dumps(items, allow_nan=True)
|
| 1200 |
+
|
| 1201 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 1202 |
+
compressed = json.loads(compressed_json, parse_constant=lambda x: None)
|
| 1203 |
+
|
| 1204 |
+
result.actual_behavior = f"Handled special floats, compressed to {len(compressed)} items"
|
| 1205 |
+
result.passed = True
|
| 1206 |
+
except (ValueError, TypeError) as e:
|
| 1207 |
+
result.actual_behavior = f"Failed on special floats: {e}"
|
| 1208 |
+
result.passed = False
|
| 1209 |
+
except Exception as e:
|
| 1210 |
+
result.actual_behavior = f"Unexpected error: {type(e).__name__}: {e}"
|
| 1211 |
+
result.passed = False
|
| 1212 |
+
|
| 1213 |
+
return result
|
| 1214 |
+
|
| 1215 |
+
|
| 1216 |
+
def test_mixed_type_array() -> AdversarialResult:
|
| 1217 |
+
"""
|
| 1218 |
+
ATTACK: Array with mixed types (dicts, strings, numbers, nulls).
|
| 1219 |
+
|
| 1220 |
+
SmartCrusher expects arrays of dicts - what happens with mixed?
|
| 1221 |
+
"""
|
| 1222 |
+
result = AdversarialResult(
|
| 1223 |
+
name="Mixed Type Array",
|
| 1224 |
+
category="extreme",
|
| 1225 |
+
expected_behavior="Should handle or gracefully skip mixed arrays",
|
| 1226 |
+
severity="medium",
|
| 1227 |
+
)
|
| 1228 |
+
|
| 1229 |
+
mixed_array = [
|
| 1230 |
+
{"id": 1, "type": "dict"},
|
| 1231 |
+
"just a string",
|
| 1232 |
+
42,
|
| 1233 |
+
None,
|
| 1234 |
+
{"id": 2, "type": "dict"},
|
| 1235 |
+
["nested", "array"],
|
| 1236 |
+
True,
|
| 1237 |
+
{"id": 3, "type": "dict"},
|
| 1238 |
+
]
|
| 1239 |
+
|
| 1240 |
+
try:
|
| 1241 |
+
config = SmartCrusherConfig(max_items_after_crush=5)
|
| 1242 |
+
original_json = json.dumps(mixed_array)
|
| 1243 |
+
|
| 1244 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 1245 |
+
|
| 1246 |
+
result.actual_behavior = f"Handled mixed array: modified={was_modified}, reason={reason}"
|
| 1247 |
+
result.passed = True
|
| 1248 |
+
except Exception as e:
|
| 1249 |
+
result.actual_behavior = f"Crashed on mixed array: {type(e).__name__}: {e}"
|
| 1250 |
+
result.passed = False
|
| 1251 |
+
|
| 1252 |
+
return result
|
| 1253 |
+
|
| 1254 |
+
|
| 1255 |
+
def test_catastrophic_regex_in_search() -> AdversarialResult:
|
| 1256 |
+
"""
|
| 1257 |
+
ATTACK: Search query designed to cause catastrophic backtracking.
|
| 1258 |
+
|
| 1259 |
+
Pattern like (a+)+ on "aaaaaaaaaaaaaaaaaaaaaaaaaaab" can hang regex engines.
|
| 1260 |
+
"""
|
| 1261 |
+
result = AdversarialResult(
|
| 1262 |
+
name="Regex Catastrophic Backtracking",
|
| 1263 |
+
category="extreme",
|
| 1264 |
+
expected_behavior="Should not hang on malicious search patterns",
|
| 1265 |
+
severity="critical",
|
| 1266 |
+
)
|
| 1267 |
+
|
| 1268 |
+
reset_compression_store()
|
| 1269 |
+
store = get_compression_store()
|
| 1270 |
+
|
| 1271 |
+
items = [{"id": i, "content": "a" * 50 + "b"} for i in range(100)]
|
| 1272 |
+
|
| 1273 |
+
hash_key = store.store(
|
| 1274 |
+
original=json.dumps(items),
|
| 1275 |
+
compressed=json.dumps(items[:10]),
|
| 1276 |
+
original_item_count=100,
|
| 1277 |
+
compressed_item_count=10,
|
| 1278 |
+
tool_name="regex_test",
|
| 1279 |
+
)
|
| 1280 |
+
|
| 1281 |
+
# These patterns could cause catastrophic backtracking in naive regex
|
| 1282 |
+
evil_patterns = [
|
| 1283 |
+
"(a+)+$",
|
| 1284 |
+
"(a|aa)+$",
|
| 1285 |
+
"(a+)+b",
|
| 1286 |
+
"([a-zA-Z]+)*X",
|
| 1287 |
+
]
|
| 1288 |
+
|
| 1289 |
+
try:
|
| 1290 |
+
import signal
|
| 1291 |
+
|
| 1292 |
+
def timeout_handler(signum, frame):
|
| 1293 |
+
raise TimeoutError("Search took too long")
|
| 1294 |
+
|
| 1295 |
+
# Set 2 second timeout
|
| 1296 |
+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
| 1297 |
+
signal.alarm(2)
|
| 1298 |
+
|
| 1299 |
+
for pattern in evil_patterns:
|
| 1300 |
+
# BM25 search doesn't use regex, so should be safe
|
| 1301 |
+
store.search(hash_key, pattern)
|
| 1302 |
+
|
| 1303 |
+
signal.alarm(0)
|
| 1304 |
+
signal.signal(signal.SIGALRM, old_handler)
|
| 1305 |
+
|
| 1306 |
+
result.actual_behavior = "Search completed without hanging"
|
| 1307 |
+
result.passed = True
|
| 1308 |
+
except TimeoutError:
|
| 1309 |
+
result.actual_behavior = "Search hung on regex-like pattern"
|
| 1310 |
+
result.passed = False
|
| 1311 |
+
except Exception as e:
|
| 1312 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1313 |
+
result.passed = True # Failing safely is OK
|
| 1314 |
+
|
| 1315 |
+
return result
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
def test_million_items() -> AdversarialResult:
|
| 1319 |
+
"""
|
| 1320 |
+
ATTACK: Array with 1 million items.
|
| 1321 |
+
|
| 1322 |
+
Test memory and performance at scale.
|
| 1323 |
+
"""
|
| 1324 |
+
result = AdversarialResult(
|
| 1325 |
+
name="Million Items Scale",
|
| 1326 |
+
category="extreme",
|
| 1327 |
+
expected_behavior="Should handle large arrays without OOM",
|
| 1328 |
+
severity="high",
|
| 1329 |
+
)
|
| 1330 |
+
|
| 1331 |
+
try:
|
| 1332 |
+
# Create 100K items (not 1M to keep test reasonable)
|
| 1333 |
+
item_count = 100_000
|
| 1334 |
+
items = [{"id": i, "value": i % 1000} for i in range(item_count)]
|
| 1335 |
+
|
| 1336 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1337 |
+
|
| 1338 |
+
start = time.time()
|
| 1339 |
+
original_json = json.dumps(items)
|
| 1340 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 1341 |
+
elapsed = time.time() - start
|
| 1342 |
+
|
| 1343 |
+
compressed = json.loads(compressed_json)
|
| 1344 |
+
|
| 1345 |
+
result.actual_behavior = (
|
| 1346 |
+
f"Compressed {item_count} items to {len(compressed)} in {elapsed:.2f}s"
|
| 1347 |
+
)
|
| 1348 |
+
result.passed = elapsed < 10.0 # Should complete in under 10 seconds
|
| 1349 |
+
result.details = {"item_count": item_count, "elapsed_seconds": elapsed}
|
| 1350 |
+
except MemoryError:
|
| 1351 |
+
result.actual_behavior = "Out of memory"
|
| 1352 |
+
result.passed = False
|
| 1353 |
+
except Exception as e:
|
| 1354 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1355 |
+
result.passed = False
|
| 1356 |
+
|
| 1357 |
+
return result
|
| 1358 |
+
|
| 1359 |
+
|
| 1360 |
+
def test_item_with_thousands_of_fields() -> AdversarialResult:
|
| 1361 |
+
"""
|
| 1362 |
+
ATTACK: Items with 10,000 fields each.
|
| 1363 |
+
|
| 1364 |
+
Field analysis iterates over all fields - what's the cost?
|
| 1365 |
+
"""
|
| 1366 |
+
result = AdversarialResult(
|
| 1367 |
+
name="Thousands of Fields",
|
| 1368 |
+
category="extreme",
|
| 1369 |
+
expected_behavior="Should handle items with many fields",
|
| 1370 |
+
severity="medium",
|
| 1371 |
+
)
|
| 1372 |
+
|
| 1373 |
+
try:
|
| 1374 |
+
field_count = 5000
|
| 1375 |
+
items = []
|
| 1376 |
+
for i in range(20):
|
| 1377 |
+
item = {"id": i}
|
| 1378 |
+
for f in range(field_count):
|
| 1379 |
+
item[f"field_{f}"] = f"value_{f}_{i}"
|
| 1380 |
+
items.append(item)
|
| 1381 |
+
|
| 1382 |
+
config = SmartCrusherConfig(max_items_after_crush=10)
|
| 1383 |
+
|
| 1384 |
+
start = time.time()
|
| 1385 |
+
original_json = json.dumps(items)
|
| 1386 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config)
|
| 1387 |
+
elapsed = time.time() - start
|
| 1388 |
+
|
| 1389 |
+
result.actual_behavior = f"Handled {field_count} fields/item in {elapsed:.2f}s"
|
| 1390 |
+
result.passed = elapsed < 5.0
|
| 1391 |
+
except Exception as e:
|
| 1392 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1393 |
+
result.passed = False
|
| 1394 |
+
|
| 1395 |
+
return result
|
| 1396 |
+
|
| 1397 |
+
|
| 1398 |
+
def test_identical_items() -> AdversarialResult:
|
| 1399 |
+
"""
|
| 1400 |
+
ATTACK: All items are EXACTLY identical.
|
| 1401 |
+
|
| 1402 |
+
Uniqueness detection should handle this edge case.
|
| 1403 |
+
"""
|
| 1404 |
+
result = AdversarialResult(
|
| 1405 |
+
name="All Identical Items",
|
| 1406 |
+
category="extreme",
|
| 1407 |
+
expected_behavior="Should handle identical items efficiently",
|
| 1408 |
+
severity="low",
|
| 1409 |
+
)
|
| 1410 |
+
|
| 1411 |
+
# 1000 perfectly identical items
|
| 1412 |
+
template = {"id": 1, "status": "ok", "value": 42, "message": "All good"}
|
| 1413 |
+
items = [template.copy() for _ in range(1000)]
|
| 1414 |
+
|
| 1415 |
+
try:
|
| 1416 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1417 |
+
|
| 1418 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1419 |
+
compressed = json.loads(compressed_json)
|
| 1420 |
+
|
| 1421 |
+
result.actual_behavior = (
|
| 1422 |
+
f"Compressed {len(items)} identical items to {len(compressed)}: {reason}"
|
| 1423 |
+
)
|
| 1424 |
+
# Should heavily compress since all items are the same
|
| 1425 |
+
result.passed = len(compressed) <= 15
|
| 1426 |
+
except Exception as e:
|
| 1427 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1428 |
+
result.passed = False
|
| 1429 |
+
|
| 1430 |
+
return result
|
| 1431 |
+
|
| 1432 |
+
|
| 1433 |
+
def test_all_fields_none() -> AdversarialResult:
|
| 1434 |
+
"""
|
| 1435 |
+
ATTACK: Items where every field value is null/None.
|
| 1436 |
+
"""
|
| 1437 |
+
result = AdversarialResult(
|
| 1438 |
+
name="All Null Values",
|
| 1439 |
+
category="extreme",
|
| 1440 |
+
expected_behavior="Should handle all-null items",
|
| 1441 |
+
severity="low",
|
| 1442 |
+
)
|
| 1443 |
+
|
| 1444 |
+
items = [{"id": None, "value": None, "status": None, "data": None} for _ in range(100)]
|
| 1445 |
+
|
| 1446 |
+
try:
|
| 1447 |
+
config = SmartCrusherConfig(max_items_after_crush=10)
|
| 1448 |
+
|
| 1449 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1450 |
+
|
| 1451 |
+
result.actual_behavior = f"Handled all-null items: modified={was_modified}"
|
| 1452 |
+
result.passed = True
|
| 1453 |
+
except Exception as e:
|
| 1454 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1455 |
+
result.passed = False
|
| 1456 |
+
|
| 1457 |
+
return result
|
| 1458 |
+
|
| 1459 |
+
|
| 1460 |
+
def test_unicode_normalization_attack() -> AdversarialResult:
|
| 1461 |
+
"""
|
| 1462 |
+
ATTACK: Unicode strings that look identical but are different.
|
| 1463 |
+
|
| 1464 |
+
"café" can be encoded as:
|
| 1465 |
+
- c a f é (4 chars, é is U+00E9)
|
| 1466 |
+
- c a f e ́ (5 chars, e + combining acute U+0301)
|
| 1467 |
+
|
| 1468 |
+
These look identical but are different strings!
|
| 1469 |
+
"""
|
| 1470 |
+
result = AdversarialResult(
|
| 1471 |
+
name="Unicode Normalization Attack",
|
| 1472 |
+
category="extreme",
|
| 1473 |
+
expected_behavior="Should handle unicode edge cases",
|
| 1474 |
+
severity="medium",
|
| 1475 |
+
)
|
| 1476 |
+
|
| 1477 |
+
# Two visually identical but byte-different strings
|
| 1478 |
+
composed = "café" # é as single char
|
| 1479 |
+
decomposed = "cafe\u0301" # e + combining accent
|
| 1480 |
+
|
| 1481 |
+
items = []
|
| 1482 |
+
for i in range(50):
|
| 1483 |
+
if i % 2 == 0:
|
| 1484 |
+
items.append({"id": i, "name": composed, "type": "composed"})
|
| 1485 |
+
else:
|
| 1486 |
+
items.append({"id": i, "name": decomposed, "type": "decomposed"})
|
| 1487 |
+
|
| 1488 |
+
# Add one special item
|
| 1489 |
+
items[25] = {"id": 25, "name": composed, "type": "TARGET", "status": "error"}
|
| 1490 |
+
|
| 1491 |
+
try:
|
| 1492 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1493 |
+
|
| 1494 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1495 |
+
compressed = json.loads(compressed_json)
|
| 1496 |
+
|
| 1497 |
+
# Check if we kept the TARGET item
|
| 1498 |
+
target_found = any(item.get("type") == "TARGET" for item in compressed)
|
| 1499 |
+
|
| 1500 |
+
result.actual_behavior = f"Unicode handled, target found: {target_found}"
|
| 1501 |
+
result.passed = target_found
|
| 1502 |
+
except Exception as e:
|
| 1503 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1504 |
+
result.passed = False
|
| 1505 |
+
|
| 1506 |
+
return result
|
| 1507 |
+
|
| 1508 |
+
|
| 1509 |
+
def test_concurrent_reset_during_operation() -> AdversarialResult:
|
| 1510 |
+
"""
|
| 1511 |
+
ATTACK: Reset global state while operations are in progress.
|
| 1512 |
+
"""
|
| 1513 |
+
result = AdversarialResult(
|
| 1514 |
+
name="Concurrent Reset Attack",
|
| 1515 |
+
category="extreme",
|
| 1516 |
+
expected_behavior="Should not crash on concurrent reset",
|
| 1517 |
+
severity="high",
|
| 1518 |
+
)
|
| 1519 |
+
|
| 1520 |
+
errors = []
|
| 1521 |
+
operations_completed = [0]
|
| 1522 |
+
|
| 1523 |
+
def do_operations():
|
| 1524 |
+
for i in range(100):
|
| 1525 |
+
try:
|
| 1526 |
+
store = get_compression_store()
|
| 1527 |
+
items = [{"id": j, "iter": i} for j in range(20)]
|
| 1528 |
+
hash_key = store.store(
|
| 1529 |
+
original=json.dumps(items),
|
| 1530 |
+
compressed=json.dumps(items[:5]),
|
| 1531 |
+
original_item_count=20,
|
| 1532 |
+
compressed_item_count=5,
|
| 1533 |
+
tool_name="reset_test",
|
| 1534 |
+
)
|
| 1535 |
+
store.retrieve(hash_key)
|
| 1536 |
+
store.search(hash_key, "test")
|
| 1537 |
+
operations_completed[0] += 1
|
| 1538 |
+
except Exception as e:
|
| 1539 |
+
errors.append(f"Op error: {type(e).__name__}: {e}")
|
| 1540 |
+
|
| 1541 |
+
def do_resets():
|
| 1542 |
+
for _ in range(50):
|
| 1543 |
+
try:
|
| 1544 |
+
reset_compression_store()
|
| 1545 |
+
reset_compression_feedback()
|
| 1546 |
+
time.sleep(0.001)
|
| 1547 |
+
except Exception as e:
|
| 1548 |
+
errors.append(f"Reset error: {type(e).__name__}: {e}")
|
| 1549 |
+
|
| 1550 |
+
try:
|
| 1551 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
| 1552 |
+
futures = []
|
| 1553 |
+
for _ in range(5):
|
| 1554 |
+
futures.append(executor.submit(do_operations))
|
| 1555 |
+
for _ in range(3):
|
| 1556 |
+
futures.append(executor.submit(do_resets))
|
| 1557 |
+
|
| 1558 |
+
concurrent.futures.wait(futures)
|
| 1559 |
+
|
| 1560 |
+
if errors:
|
| 1561 |
+
result.actual_behavior = f"Errors during concurrent reset: {errors[:3]}"
|
| 1562 |
+
result.passed = False
|
| 1563 |
+
else:
|
| 1564 |
+
result.actual_behavior = (
|
| 1565 |
+
f"Completed {operations_completed[0]} operations with concurrent resets"
|
| 1566 |
+
)
|
| 1567 |
+
result.passed = True
|
| 1568 |
+
except Exception as e:
|
| 1569 |
+
result.actual_behavior = f"Crashed: {type(e).__name__}: {e}"
|
| 1570 |
+
result.passed = False
|
| 1571 |
+
|
| 1572 |
+
return result
|
| 1573 |
+
|
| 1574 |
+
|
| 1575 |
+
def test_zero_byte_in_content() -> AdversarialResult:
|
| 1576 |
+
"""
|
| 1577 |
+
ATTACK: Null bytes (\\x00) embedded in strings.
|
| 1578 |
+
|
| 1579 |
+
Can truncate strings in C-based systems.
|
| 1580 |
+
"""
|
| 1581 |
+
result = AdversarialResult(
|
| 1582 |
+
name="Null Byte Injection",
|
| 1583 |
+
category="extreme",
|
| 1584 |
+
expected_behavior="Should preserve content with null bytes",
|
| 1585 |
+
severity="high",
|
| 1586 |
+
)
|
| 1587 |
+
|
| 1588 |
+
items = []
|
| 1589 |
+
for i in range(50):
|
| 1590 |
+
# Embed null byte in various positions
|
| 1591 |
+
if i == 10:
|
| 1592 |
+
items.append({"id": i, "data": "before\x00after", "status": "error"})
|
| 1593 |
+
elif i == 20:
|
| 1594 |
+
items.append({"id": i, "data": "\x00start", "status": "error"})
|
| 1595 |
+
elif i == 30:
|
| 1596 |
+
items.append({"id": i, "data": "end\x00", "status": "error"})
|
| 1597 |
+
else:
|
| 1598 |
+
items.append({"id": i, "data": "normal", "status": "ok"})
|
| 1599 |
+
|
| 1600 |
+
try:
|
| 1601 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1602 |
+
original_json = json.dumps(items)
|
| 1603 |
+
|
| 1604 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 1605 |
+
compressed = json.loads(compressed_json)
|
| 1606 |
+
|
| 1607 |
+
# Check if null-byte items were preserved (they have status=error)
|
| 1608 |
+
error_items = [item for item in compressed if item.get("status") == "error"]
|
| 1609 |
+
|
| 1610 |
+
# Also verify the null bytes survived
|
| 1611 |
+
null_byte_survived = any("\x00" in str(item.get("data", "")) for item in compressed)
|
| 1612 |
+
|
| 1613 |
+
result.actual_behavior = (
|
| 1614 |
+
f"Kept {len(error_items)} error items, null bytes intact: {null_byte_survived}"
|
| 1615 |
+
)
|
| 1616 |
+
result.passed = len(error_items) == 3 and null_byte_survived
|
| 1617 |
+
except Exception as e:
|
| 1618 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1619 |
+
result.passed = False
|
| 1620 |
+
|
| 1621 |
+
return result
|
| 1622 |
+
|
| 1623 |
+
|
| 1624 |
+
def test_recursive_json_structure() -> AdversarialResult:
|
| 1625 |
+
"""
|
| 1626 |
+
ATTACK: Structure that references itself (via string representation).
|
| 1627 |
+
|
| 1628 |
+
Not true circular reference (JSON doesn't support that), but deeply self-similar.
|
| 1629 |
+
"""
|
| 1630 |
+
result = AdversarialResult(
|
| 1631 |
+
name="Self-Similar Structure",
|
| 1632 |
+
category="extreme",
|
| 1633 |
+
expected_behavior="Should handle self-similar data",
|
| 1634 |
+
severity="low",
|
| 1635 |
+
)
|
| 1636 |
+
|
| 1637 |
+
# Create structure where values contain JSON-like strings
|
| 1638 |
+
items = []
|
| 1639 |
+
for i in range(50):
|
| 1640 |
+
inner = json.dumps({"nested_id": i, "value": "inner"})
|
| 1641 |
+
items.append(
|
| 1642 |
+
{
|
| 1643 |
+
"id": i,
|
| 1644 |
+
"data": inner, # JSON string inside JSON
|
| 1645 |
+
"meta": json.dumps({"level": 1, "payload": inner}), # Double nested
|
| 1646 |
+
}
|
| 1647 |
+
)
|
| 1648 |
+
|
| 1649 |
+
try:
|
| 1650 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1651 |
+
|
| 1652 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1653 |
+
compressed = json.loads(compressed_json)
|
| 1654 |
+
|
| 1655 |
+
result.actual_behavior = f"Handled self-similar structure: {len(compressed)} items"
|
| 1656 |
+
result.passed = True
|
| 1657 |
+
except Exception as e:
|
| 1658 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1659 |
+
result.passed = False
|
| 1660 |
+
|
| 1661 |
+
return result
|
| 1662 |
+
|
| 1663 |
+
|
| 1664 |
+
def test_extreme_numeric_values() -> AdversarialResult:
|
| 1665 |
+
"""
|
| 1666 |
+
ATTACK: Extreme numeric values that might overflow.
|
| 1667 |
+
|
| 1668 |
+
Very large integers, very small floats, edge cases.
|
| 1669 |
+
"""
|
| 1670 |
+
result = AdversarialResult(
|
| 1671 |
+
name="Extreme Numeric Values",
|
| 1672 |
+
category="extreme",
|
| 1673 |
+
expected_behavior="Should handle extreme numbers",
|
| 1674 |
+
severity="medium",
|
| 1675 |
+
)
|
| 1676 |
+
|
| 1677 |
+
items = [
|
| 1678 |
+
{"id": 0, "value": 0},
|
| 1679 |
+
{"id": 1, "value": -1},
|
| 1680 |
+
{"id": 2, "value": 2**63 - 1}, # Max int64
|
| 1681 |
+
{"id": 3, "value": -(2**63)}, # Min int64
|
| 1682 |
+
{"id": 4, "value": 2**64}, # Overflow int64
|
| 1683 |
+
{"id": 5, "value": 10**308}, # Near max float
|
| 1684 |
+
{"id": 6, "value": 10**-308}, # Near min positive float
|
| 1685 |
+
{"id": 7, "value": 0.1 + 0.2}, # Classic float precision issue
|
| 1686 |
+
{"id": 8, "value": 1e-400}, # Underflow to 0
|
| 1687 |
+
{"id": 9, "score": 999999999999999999999}, # Very large score
|
| 1688 |
+
]
|
| 1689 |
+
|
| 1690 |
+
# Add normal items
|
| 1691 |
+
for i in range(10, 50):
|
| 1692 |
+
items.append({"id": i, "value": i, "score": i / 100})
|
| 1693 |
+
|
| 1694 |
+
try:
|
| 1695 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 1696 |
+
|
| 1697 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1698 |
+
compressed = json.loads(compressed_json)
|
| 1699 |
+
|
| 1700 |
+
result.actual_behavior = f"Handled extreme numbers: {len(compressed)} items"
|
| 1701 |
+
result.passed = True
|
| 1702 |
+
except (OverflowError, ValueError) as e:
|
| 1703 |
+
result.actual_behavior = f"Numeric error: {e}"
|
| 1704 |
+
result.passed = False
|
| 1705 |
+
except Exception as e:
|
| 1706 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1707 |
+
result.passed = False
|
| 1708 |
+
|
| 1709 |
+
return result
|
| 1710 |
+
|
| 1711 |
+
|
| 1712 |
+
def test_adversarial_field_names() -> AdversarialResult:
|
| 1713 |
+
"""
|
| 1714 |
+
ATTACK: Field names that might confuse our analysis.
|
| 1715 |
+
|
| 1716 |
+
Fields named "__proto__", "constructor", "toString", etc.
|
| 1717 |
+
"""
|
| 1718 |
+
result = AdversarialResult(
|
| 1719 |
+
name="Adversarial Field Names",
|
| 1720 |
+
category="extreme",
|
| 1721 |
+
expected_behavior="Should handle special field names",
|
| 1722 |
+
severity="medium",
|
| 1723 |
+
)
|
| 1724 |
+
|
| 1725 |
+
items = []
|
| 1726 |
+
for i in range(30):
|
| 1727 |
+
items.append(
|
| 1728 |
+
{
|
| 1729 |
+
"id": i,
|
| 1730 |
+
"__proto__": {"admin": True}, # Prototype pollution attempt
|
| 1731 |
+
"constructor": "evil",
|
| 1732 |
+
"toString": "hacked",
|
| 1733 |
+
"__class__": "injected",
|
| 1734 |
+
"hasOwnProperty": False,
|
| 1735 |
+
"score": i / 10,
|
| 1736 |
+
"status": "error" if i == 15 else "ok",
|
| 1737 |
+
}
|
| 1738 |
+
)
|
| 1739 |
+
|
| 1740 |
+
try:
|
| 1741 |
+
config = SmartCrusherConfig(max_items_after_crush=10)
|
| 1742 |
+
|
| 1743 |
+
compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config)
|
| 1744 |
+
compressed = json.loads(compressed_json)
|
| 1745 |
+
|
| 1746 |
+
# Verify error item was kept
|
| 1747 |
+
error_kept = any(item.get("status") == "error" for item in compressed)
|
| 1748 |
+
|
| 1749 |
+
result.actual_behavior = f"Handled adversarial fields, error kept: {error_kept}"
|
| 1750 |
+
result.passed = error_kept
|
| 1751 |
+
except Exception as e:
|
| 1752 |
+
result.actual_behavior = f"Error: {type(e).__name__}: {e}"
|
| 1753 |
+
result.passed = False
|
| 1754 |
+
|
| 1755 |
+
return result
|
| 1756 |
+
|
| 1757 |
+
|
| 1758 |
+
def test_store_during_eviction_storm() -> AdversarialResult:
|
| 1759 |
+
"""
|
| 1760 |
+
ATTACK: Rapid store/retrieve during aggressive eviction.
|
| 1761 |
+
|
| 1762 |
+
max_entries=5 with 100 concurrent stores.
|
| 1763 |
+
"""
|
| 1764 |
+
result = AdversarialResult(
|
| 1765 |
+
name="Eviction Storm",
|
| 1766 |
+
category="extreme",
|
| 1767 |
+
expected_behavior="Should maintain consistency during eviction",
|
| 1768 |
+
severity="high",
|
| 1769 |
+
)
|
| 1770 |
+
|
| 1771 |
+
reset_compression_store()
|
| 1772 |
+
# Create store with very small capacity
|
| 1773 |
+
store = CompressionStore(max_entries=5, default_ttl=300)
|
| 1774 |
+
|
| 1775 |
+
stored_hashes = []
|
| 1776 |
+
retrieved_count = [0]
|
| 1777 |
+
errors = []
|
| 1778 |
+
lock = threading.Lock()
|
| 1779 |
+
|
| 1780 |
+
def store_and_retrieve():
|
| 1781 |
+
for _i in range(50):
|
| 1782 |
+
try:
|
| 1783 |
+
items = [{"id": j, "thread": threading.current_thread().name} for j in range(10)]
|
| 1784 |
+
hash_key = store.store(
|
| 1785 |
+
original=json.dumps(items),
|
| 1786 |
+
compressed=json.dumps(items[:2]),
|
| 1787 |
+
original_item_count=10,
|
| 1788 |
+
compressed_item_count=2,
|
| 1789 |
+
tool_name="eviction_test",
|
| 1790 |
+
)
|
| 1791 |
+
|
| 1792 |
+
with lock:
|
| 1793 |
+
stored_hashes.append(hash_key)
|
| 1794 |
+
|
| 1795 |
+
# Immediately try to retrieve
|
| 1796 |
+
entry = store.retrieve(hash_key)
|
| 1797 |
+
if entry:
|
| 1798 |
+
with lock:
|
| 1799 |
+
retrieved_count[0] += 1
|
| 1800 |
+
|
| 1801 |
+
except Exception as e:
|
| 1802 |
+
with lock:
|
| 1803 |
+
errors.append(str(e))
|
| 1804 |
+
|
| 1805 |
+
try:
|
| 1806 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
| 1807 |
+
futures = [executor.submit(store_and_retrieve) for _ in range(20)]
|
| 1808 |
+
concurrent.futures.wait(futures)
|
| 1809 |
+
|
| 1810 |
+
if errors:
|
| 1811 |
+
result.actual_behavior = f"Errors: {errors[:3]}"
|
| 1812 |
+
result.passed = False
|
| 1813 |
+
else:
|
| 1814 |
+
# Some eviction is expected, but we shouldn't crash
|
| 1815 |
+
result.actual_behavior = (
|
| 1816 |
+
f"Stored {len(stored_hashes)}, retrieved {retrieved_count[0]} (eviction expected)"
|
| 1817 |
+
)
|
| 1818 |
+
result.passed = True
|
| 1819 |
+
except Exception as e:
|
| 1820 |
+
result.actual_behavior = f"Crashed: {type(e).__name__}: {e}"
|
| 1821 |
+
result.passed = False
|
| 1822 |
+
|
| 1823 |
+
return result
|
| 1824 |
+
|
| 1825 |
+
|
| 1826 |
+
# =============================================================================
|
| 1827 |
+
# MAIN
|
| 1828 |
+
# =============================================================================
|
| 1829 |
+
|
| 1830 |
+
|
| 1831 |
+
def main():
|
| 1832 |
+
print("\n" + "=" * 70)
|
| 1833 |
+
print(" ADVERSARIAL CCR TESTS")
|
| 1834 |
+
print(" Intentionally Trying to Break Our Code")
|
| 1835 |
+
print("=" * 70 + "\n")
|
| 1836 |
+
|
| 1837 |
+
tests = [
|
| 1838 |
+
# Semantic attacks
|
| 1839 |
+
test_all_items_are_errors,
|
| 1840 |
+
test_error_keyword_in_normal_data,
|
| 1841 |
+
test_needle_looks_exactly_like_hay,
|
| 1842 |
+
test_anomaly_in_string_not_number,
|
| 1843 |
+
# Boundary conditions
|
| 1844 |
+
test_empty_array,
|
| 1845 |
+
test_single_item_array,
|
| 1846 |
+
test_exactly_max_items,
|
| 1847 |
+
test_max_items_plus_one,
|
| 1848 |
+
test_hash_collision_attempt,
|
| 1849 |
+
test_ttl_exact_boundary,
|
| 1850 |
+
# Injection attacks
|
| 1851 |
+
test_json_injection_in_content,
|
| 1852 |
+
test_headroom_marker_collision,
|
| 1853 |
+
test_unicode_and_emoji_handling,
|
| 1854 |
+
test_extremely_long_strings,
|
| 1855 |
+
test_query_injection_in_search,
|
| 1856 |
+
# Race conditions
|
| 1857 |
+
test_concurrent_store_same_content,
|
| 1858 |
+
test_concurrent_store_and_evict,
|
| 1859 |
+
test_concurrent_feedback_updates,
|
| 1860 |
+
# Deceptive data
|
| 1861 |
+
test_hidden_error_in_nested_structure,
|
| 1862 |
+
test_misleading_score_field,
|
| 1863 |
+
test_timestamp_anomaly_not_value,
|
| 1864 |
+
# EXTREME stress tests
|
| 1865 |
+
test_deeply_nested_structure,
|
| 1866 |
+
test_nan_infinity_scores,
|
| 1867 |
+
test_mixed_type_array,
|
| 1868 |
+
test_catastrophic_regex_in_search,
|
| 1869 |
+
test_million_items,
|
| 1870 |
+
test_item_with_thousands_of_fields,
|
| 1871 |
+
test_identical_items,
|
| 1872 |
+
test_all_fields_none,
|
| 1873 |
+
test_unicode_normalization_attack,
|
| 1874 |
+
test_concurrent_reset_during_operation,
|
| 1875 |
+
test_zero_byte_in_content,
|
| 1876 |
+
test_recursive_json_structure,
|
| 1877 |
+
test_extreme_numeric_values,
|
| 1878 |
+
test_adversarial_field_names,
|
| 1879 |
+
test_store_during_eviction_storm,
|
| 1880 |
+
]
|
| 1881 |
+
|
| 1882 |
+
results_by_category = {}
|
| 1883 |
+
|
| 1884 |
+
for test_func in tests:
|
| 1885 |
+
print(f" Running {test_func.__name__}...", end=" ", flush=True)
|
| 1886 |
+
result = run_test(test_func)
|
| 1887 |
+
|
| 1888 |
+
if result.category not in results_by_category:
|
| 1889 |
+
results_by_category[result.category] = []
|
| 1890 |
+
results_by_category[result.category].append(result)
|
| 1891 |
+
|
| 1892 |
+
status = "✓" if result.passed else "✗"
|
| 1893 |
+
print(f"{status}")
|
| 1894 |
+
|
| 1895 |
+
# Summary
|
| 1896 |
+
print("\n" + "=" * 70)
|
| 1897 |
+
print(" RESULTS BY CATEGORY")
|
| 1898 |
+
print("=" * 70)
|
| 1899 |
+
|
| 1900 |
+
total_passed = 0
|
| 1901 |
+
total_tests = 0
|
| 1902 |
+
critical_failures = []
|
| 1903 |
+
|
| 1904 |
+
for category, results in results_by_category.items():
|
| 1905 |
+
passed = sum(1 for r in results if r.passed)
|
| 1906 |
+
total = len(results)
|
| 1907 |
+
total_passed += passed
|
| 1908 |
+
total_tests += total
|
| 1909 |
+
|
| 1910 |
+
print(f"\n {category.upper()}: {passed}/{total}")
|
| 1911 |
+
|
| 1912 |
+
for r in results:
|
| 1913 |
+
status = "✓ PASS" if r.passed else "✗ FAIL"
|
| 1914 |
+
print(f" {status} {r.name}")
|
| 1915 |
+
|
| 1916 |
+
if not r.passed:
|
| 1917 |
+
print(f" Expected: {r.expected_behavior}")
|
| 1918 |
+
print(f" Actual: {r.actual_behavior}")
|
| 1919 |
+
|
| 1920 |
+
if r.severity == "critical":
|
| 1921 |
+
critical_failures.append(r)
|
| 1922 |
+
|
| 1923 |
+
print("\n" + "=" * 70)
|
| 1924 |
+
print(f" TOTAL: {total_passed}/{total_tests} tests passed")
|
| 1925 |
+
|
| 1926 |
+
if critical_failures:
|
| 1927 |
+
print(f"\n ⚠️ {len(critical_failures)} CRITICAL FAILURES:")
|
| 1928 |
+
for r in critical_failures:
|
| 1929 |
+
print(f" - {r.name}: {r.actual_behavior[:50]}")
|
| 1930 |
+
|
| 1931 |
+
print("=" * 70 + "\n")
|
| 1932 |
+
|
| 1933 |
+
# Exit code
|
| 1934 |
+
failed = total_tests - total_passed
|
| 1935 |
+
exit(failed)
|
| 1936 |
+
|
| 1937 |
+
|
| 1938 |
+
if __name__ == "__main__":
|
| 1939 |
+
main()
|
benchmarks/agent_cost_benchmark.py
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Agent Cost Crisis Benchmark - The Compelling Story
|
| 4 |
+
|
| 5 |
+
This benchmark demonstrates WHY Headroom matters by showing:
|
| 6 |
+
|
| 7 |
+
1. THE PROBLEM: Context explosion in real-world agent workloads
|
| 8 |
+
- Tokens grow exponentially with conversation length
|
| 9 |
+
- Tool outputs dominate context (often 70%+ of tokens)
|
| 10 |
+
- Dynamic content breaks cache efficiency
|
| 11 |
+
|
| 12 |
+
2. THE SOLUTION: Headroom's impact on real workloads
|
| 13 |
+
- Token reduction from SmartCrusher (50-80% on tool outputs)
|
| 14 |
+
- Cache alignment improvement (10x+ potential savings)
|
| 15 |
+
- Context windowing (stay within limits without losing info)
|
| 16 |
+
|
| 17 |
+
3. THE PROOF: Quality preservation
|
| 18 |
+
- Critical information retained (errors, anomalies, relevant items)
|
| 19 |
+
- Agent task completion unaffected
|
| 20 |
+
- Information retrieval accuracy maintained
|
| 21 |
+
|
| 22 |
+
Usage:
|
| 23 |
+
python benchmarks/agent_cost_benchmark.py
|
| 24 |
+
python benchmarks/agent_cost_benchmark.py --format markdown > BENCHMARK.md
|
| 25 |
+
python benchmarks/agent_cost_benchmark.py --scenario coding-agent
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import argparse
|
| 31 |
+
import json
|
| 32 |
+
import statistics
|
| 33 |
+
import time
|
| 34 |
+
from dataclasses import dataclass, field
|
| 35 |
+
from typing import Any
|
| 36 |
+
|
| 37 |
+
# Benchmark scenario imports
|
| 38 |
+
from benchmarks.scenarios.conversations import (
|
| 39 |
+
generate_agentic_conversation,
|
| 40 |
+
generate_rag_conversation,
|
| 41 |
+
)
|
| 42 |
+
from benchmarks.scenarios.tool_outputs import (
|
| 43 |
+
generate_log_entries,
|
| 44 |
+
generate_search_results,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Headroom imports
|
| 48 |
+
from headroom.transforms.smart_crusher import SmartCrusherConfig, smart_crush_tool_output
|
| 49 |
+
|
| 50 |
+
# =============================================================================
|
| 51 |
+
# PRICING DATA (as of 2025)
|
| 52 |
+
# =============================================================================
|
| 53 |
+
|
| 54 |
+
PRICING = {
|
| 55 |
+
# Anthropic Claude 3.5 Sonnet
|
| 56 |
+
"claude-3.5-sonnet": {
|
| 57 |
+
"input": 3.00 / 1_000_000, # $3 per 1M tokens
|
| 58 |
+
"output": 15.00 / 1_000_000, # $15 per 1M tokens
|
| 59 |
+
"cached_input": 0.30 / 1_000_000, # 90% discount on cache hit
|
| 60 |
+
"cache_write": 3.75 / 1_000_000, # 25% premium to write cache
|
| 61 |
+
},
|
| 62 |
+
# OpenAI GPT-4o
|
| 63 |
+
"gpt-4o": {
|
| 64 |
+
"input": 2.50 / 1_000_000,
|
| 65 |
+
"output": 10.00 / 1_000_000,
|
| 66 |
+
"cached_input": 1.25 / 1_000_000, # 50% discount
|
| 67 |
+
},
|
| 68 |
+
# Google Gemini 1.5 Pro
|
| 69 |
+
"gemini-1.5-pro": {
|
| 70 |
+
"input": 1.25 / 1_000_000,
|
| 71 |
+
"output": 5.00 / 1_000_000,
|
| 72 |
+
"cached_input": 0.3125 / 1_000_000, # 75% discount
|
| 73 |
+
},
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
# Approximate tokens per character (GPT-4 tokenizer average)
|
| 77 |
+
CHARS_PER_TOKEN = 4
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@dataclass
|
| 81 |
+
class CostAnalysis:
|
| 82 |
+
"""Cost analysis for a workload."""
|
| 83 |
+
|
| 84 |
+
tokens_input: int = 0
|
| 85 |
+
tokens_output: int = 0
|
| 86 |
+
tokens_cached: int = 0
|
| 87 |
+
|
| 88 |
+
cost_baseline: float = 0.0
|
| 89 |
+
cost_optimized: float = 0.0
|
| 90 |
+
cost_with_cache: float = 0.0
|
| 91 |
+
|
| 92 |
+
savings_from_compression: float = 0.0
|
| 93 |
+
savings_from_caching: float = 0.0
|
| 94 |
+
total_savings_percent: float = 0.0
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclass
|
| 98 |
+
class BenchmarkResult:
|
| 99 |
+
"""Result from a single benchmark scenario."""
|
| 100 |
+
|
| 101 |
+
name: str
|
| 102 |
+
description: str
|
| 103 |
+
|
| 104 |
+
# Token metrics
|
| 105 |
+
tokens_original: int = 0
|
| 106 |
+
tokens_optimized: int = 0
|
| 107 |
+
compression_ratio: float = 0.0
|
| 108 |
+
|
| 109 |
+
# Cache metrics
|
| 110 |
+
cache_hit_rate_baseline: float = 0.0
|
| 111 |
+
cache_hit_rate_optimized: float = 0.0
|
| 112 |
+
|
| 113 |
+
# Quality metrics
|
| 114 |
+
critical_items_retained: int = 0
|
| 115 |
+
critical_items_total: int = 0
|
| 116 |
+
retention_rate: float = 0.0
|
| 117 |
+
|
| 118 |
+
# Cost analysis
|
| 119 |
+
cost_analysis: CostAnalysis = field(default_factory=CostAnalysis)
|
| 120 |
+
|
| 121 |
+
# Performance
|
| 122 |
+
optimization_latency_ms: float = 0.0
|
| 123 |
+
|
| 124 |
+
# Details
|
| 125 |
+
details: dict[str, Any] = field(default_factory=dict)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# =============================================================================
|
| 129 |
+
# SCENARIO 1: Coding Agent Context Explosion
|
| 130 |
+
# =============================================================================
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def benchmark_coding_agent_explosion() -> BenchmarkResult:
|
| 134 |
+
"""
|
| 135 |
+
Simulate a Claude Code / Cursor style coding agent session.
|
| 136 |
+
|
| 137 |
+
Shows how context explodes as the agent:
|
| 138 |
+
- Searches codebase (100s of file snippets)
|
| 139 |
+
- Reads documentation (large text blocks)
|
| 140 |
+
- Makes tool calls (grep, find, read)
|
| 141 |
+
- Accumulates conversation history
|
| 142 |
+
"""
|
| 143 |
+
result = BenchmarkResult(
|
| 144 |
+
name="Coding Agent Context Explosion",
|
| 145 |
+
description="50-turn coding session with file search, grep, and documentation lookups",
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Generate realistic coding agent conversation
|
| 149 |
+
messages = generate_agentic_conversation(
|
| 150 |
+
turns=50,
|
| 151 |
+
tool_calls_per_turn=2,
|
| 152 |
+
items_per_tool_response=100, # 100 search results per tool call
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# Calculate original tokens
|
| 156 |
+
original_content = json.dumps(messages)
|
| 157 |
+
result.tokens_original = len(original_content) // CHARS_PER_TOKEN
|
| 158 |
+
|
| 159 |
+
# Apply Headroom transforms using convenience function
|
| 160 |
+
config = SmartCrusherConfig(max_items_after_crush=20)
|
| 161 |
+
|
| 162 |
+
start = time.perf_counter()
|
| 163 |
+
|
| 164 |
+
optimized_messages = []
|
| 165 |
+
critical_retained = 0
|
| 166 |
+
critical_total = 0
|
| 167 |
+
|
| 168 |
+
for msg in messages:
|
| 169 |
+
if msg.get("role") == "tool":
|
| 170 |
+
# Parse tool content as JSON array
|
| 171 |
+
try:
|
| 172 |
+
original_content = msg.get("content", "[]")
|
| 173 |
+
content = json.loads(original_content)
|
| 174 |
+
if isinstance(content, list) and len(content) > 10:
|
| 175 |
+
# Count critical items (errors, high-relevance)
|
| 176 |
+
for item in content:
|
| 177 |
+
if isinstance(item, dict):
|
| 178 |
+
if item.get("error") or item.get("status") == "failed":
|
| 179 |
+
critical_total += 1
|
| 180 |
+
if item.get("is_needle"):
|
| 181 |
+
critical_total += 1
|
| 182 |
+
|
| 183 |
+
# Compress with SmartCrusher convenience function
|
| 184 |
+
compressed_str, was_modified, _ = smart_crush_tool_output(
|
| 185 |
+
original_content, config
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
if was_modified:
|
| 189 |
+
compressed = json.loads(compressed_str)
|
| 190 |
+
# Count retained critical items
|
| 191 |
+
for item in compressed:
|
| 192 |
+
if isinstance(item, dict):
|
| 193 |
+
if item.get("error") or item.get("status") == "failed":
|
| 194 |
+
critical_retained += 1
|
| 195 |
+
if item.get("is_needle"):
|
| 196 |
+
critical_retained += 1
|
| 197 |
+
|
| 198 |
+
msg = {**msg, "content": compressed_str}
|
| 199 |
+
except (json.JSONDecodeError, TypeError):
|
| 200 |
+
pass
|
| 201 |
+
|
| 202 |
+
optimized_messages.append(msg)
|
| 203 |
+
|
| 204 |
+
result.optimization_latency_ms = (time.perf_counter() - start) * 1000
|
| 205 |
+
|
| 206 |
+
# Calculate optimized tokens
|
| 207 |
+
optimized_content = json.dumps(optimized_messages)
|
| 208 |
+
result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN
|
| 209 |
+
|
| 210 |
+
# Calculate metrics
|
| 211 |
+
result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original)
|
| 212 |
+
result.critical_items_total = critical_total
|
| 213 |
+
result.critical_items_retained = critical_retained
|
| 214 |
+
result.retention_rate = critical_retained / critical_total if critical_total > 0 else 1.0
|
| 215 |
+
|
| 216 |
+
# Cost analysis (using Claude 3.5 Sonnet pricing)
|
| 217 |
+
pricing = PRICING["claude-3.5-sonnet"]
|
| 218 |
+
result.cost_analysis = CostAnalysis(
|
| 219 |
+
tokens_input=result.tokens_original,
|
| 220 |
+
cost_baseline=result.tokens_original * pricing["input"],
|
| 221 |
+
cost_optimized=result.tokens_optimized * pricing["input"],
|
| 222 |
+
savings_from_compression=(result.tokens_original - result.tokens_optimized)
|
| 223 |
+
* pricing["input"],
|
| 224 |
+
)
|
| 225 |
+
result.cost_analysis.total_savings_percent = result.compression_ratio * 100
|
| 226 |
+
|
| 227 |
+
result.details = {
|
| 228 |
+
"turns": 50,
|
| 229 |
+
"tool_calls": 100,
|
| 230 |
+
"items_per_response": 100,
|
| 231 |
+
"items_after_compression": 20,
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
return result
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# =============================================================================
|
| 238 |
+
# SCENARIO 2: Cache Alignment Impact
|
| 239 |
+
# =============================================================================
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def benchmark_cache_alignment() -> BenchmarkResult:
|
| 243 |
+
"""
|
| 244 |
+
Show how dynamic content breaks caching and how CacheAligner fixes it.
|
| 245 |
+
|
| 246 |
+
Simulates 100 requests with same base prompt but different dates.
|
| 247 |
+
Without alignment: 0% cache hits
|
| 248 |
+
With alignment: 90%+ cache hits
|
| 249 |
+
"""
|
| 250 |
+
from headroom.cache import DetectorConfig, DynamicContentDetector
|
| 251 |
+
|
| 252 |
+
result = BenchmarkResult(
|
| 253 |
+
name="Cache Alignment Impact",
|
| 254 |
+
description="100 requests with dynamic dates - cache hit improvement",
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# Base system prompt with dynamic date
|
| 258 |
+
base_prompt = """You are Claude, an AI assistant by Anthropic.
|
| 259 |
+
|
| 260 |
+
Today is {date}.
|
| 261 |
+
Current time: {time}.
|
| 262 |
+
|
| 263 |
+
Session ID: {session_id}
|
| 264 |
+
Request ID: {request_id}
|
| 265 |
+
|
| 266 |
+
You are a helpful coding assistant. Follow these guidelines:
|
| 267 |
+
1. Write clean, readable code
|
| 268 |
+
2. Add appropriate comments
|
| 269 |
+
3. Handle errors gracefully
|
| 270 |
+
4. Follow best practices
|
| 271 |
+
|
| 272 |
+
Be concise and helpful."""
|
| 273 |
+
|
| 274 |
+
import datetime
|
| 275 |
+
import uuid
|
| 276 |
+
|
| 277 |
+
# Use DynamicContentDetector to extract static content
|
| 278 |
+
detector = DynamicContentDetector(DetectorConfig(tiers=["regex"]))
|
| 279 |
+
|
| 280 |
+
# Simulate 100 requests over a day
|
| 281 |
+
prompts_original = []
|
| 282 |
+
prompts_aligned = []
|
| 283 |
+
|
| 284 |
+
base_date = datetime.datetime(2025, 1, 15, 9, 0, 0)
|
| 285 |
+
|
| 286 |
+
for i in range(100):
|
| 287 |
+
# Each request has different timestamp
|
| 288 |
+
request_time = base_date + datetime.timedelta(minutes=i * 5)
|
| 289 |
+
|
| 290 |
+
prompt = base_prompt.format(
|
| 291 |
+
date=request_time.strftime("%A, %B %d, %Y"),
|
| 292 |
+
time=request_time.strftime("%I:%M %p"),
|
| 293 |
+
session_id=f"sess_{uuid.uuid4().hex[:24]}",
|
| 294 |
+
request_id=f"req_{uuid.uuid4().hex[:24]}",
|
| 295 |
+
)
|
| 296 |
+
prompts_original.append(prompt)
|
| 297 |
+
|
| 298 |
+
# Extract static content for cache alignment
|
| 299 |
+
detection_result = detector.detect(prompt)
|
| 300 |
+
prompts_aligned.append(detection_result.static_content)
|
| 301 |
+
|
| 302 |
+
# Calculate cache hits
|
| 303 |
+
# Baseline: all prompts are different (dynamic dates)
|
| 304 |
+
unique_original = len(set(prompts_original))
|
| 305 |
+
cache_hits_baseline = 100 - unique_original
|
| 306 |
+
|
| 307 |
+
# Aligned: static prefixes should be identical
|
| 308 |
+
unique_aligned = len(set(prompts_aligned))
|
| 309 |
+
cache_hits_aligned = 100 - unique_aligned
|
| 310 |
+
|
| 311 |
+
result.cache_hit_rate_baseline = cache_hits_baseline / 100
|
| 312 |
+
result.cache_hit_rate_optimized = cache_hits_aligned / 100
|
| 313 |
+
|
| 314 |
+
# Token calculation
|
| 315 |
+
result.tokens_original = sum(len(p) // CHARS_PER_TOKEN for p in prompts_original)
|
| 316 |
+
|
| 317 |
+
# Cost analysis with caching
|
| 318 |
+
pricing = PRICING["claude-3.5-sonnet"]
|
| 319 |
+
tokens_per_request = len(prompts_original[0]) // CHARS_PER_TOKEN
|
| 320 |
+
|
| 321 |
+
# Baseline: pay full price every time (no cache hits)
|
| 322 |
+
cost_baseline = 100 * tokens_per_request * pricing["input"]
|
| 323 |
+
|
| 324 |
+
# Optimized: first request is cache write, rest are cache hits
|
| 325 |
+
first_request_cost = tokens_per_request * pricing["cache_write"]
|
| 326 |
+
cached_requests_cost = 99 * tokens_per_request * pricing["cached_input"]
|
| 327 |
+
cost_optimized = first_request_cost + cached_requests_cost
|
| 328 |
+
|
| 329 |
+
result.cost_analysis = CostAnalysis(
|
| 330 |
+
tokens_input=result.tokens_original,
|
| 331 |
+
cost_baseline=cost_baseline,
|
| 332 |
+
cost_with_cache=cost_optimized,
|
| 333 |
+
savings_from_caching=cost_baseline - cost_optimized,
|
| 334 |
+
total_savings_percent=((cost_baseline - cost_optimized) / cost_baseline) * 100,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
result.details = {
|
| 338 |
+
"total_requests": 100,
|
| 339 |
+
"unique_prompts_baseline": unique_original,
|
| 340 |
+
"unique_prompts_aligned": unique_aligned,
|
| 341 |
+
"cache_improvement_factor": f"{(cache_hits_aligned - cache_hits_baseline)}x",
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
return result
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# =============================================================================
|
| 348 |
+
# SCENARIO 3: RAG Context Scaling
|
| 349 |
+
# =============================================================================
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def benchmark_rag_scaling() -> BenchmarkResult:
|
| 353 |
+
"""
|
| 354 |
+
Show how RAG context grows and how Headroom manages it.
|
| 355 |
+
|
| 356 |
+
Simulates large RAG context with multiple queries.
|
| 357 |
+
"""
|
| 358 |
+
result = BenchmarkResult(
|
| 359 |
+
name="RAG Context Scaling", description="Large RAG context (~50K tokens) with compression"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# Generate RAG conversation with ~50K tokens of context
|
| 363 |
+
messages = generate_rag_conversation(
|
| 364 |
+
context_tokens=50000,
|
| 365 |
+
num_queries=10,
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
original_content = json.dumps(messages)
|
| 369 |
+
result.tokens_original = len(original_content) // CHARS_PER_TOKEN
|
| 370 |
+
|
| 371 |
+
# Apply transforms - compress tool outputs in messages
|
| 372 |
+
config = SmartCrusherConfig(max_items_after_crush=10)
|
| 373 |
+
|
| 374 |
+
start = time.perf_counter()
|
| 375 |
+
|
| 376 |
+
# Compress tool outputs in messages
|
| 377 |
+
optimized_messages = []
|
| 378 |
+
for msg in messages:
|
| 379 |
+
if msg.get("role") == "tool":
|
| 380 |
+
try:
|
| 381 |
+
original_content_msg = msg.get("content", "[]")
|
| 382 |
+
compressed_str, was_modified, _ = smart_crush_tool_output(
|
| 383 |
+
original_content_msg, config
|
| 384 |
+
)
|
| 385 |
+
if was_modified:
|
| 386 |
+
msg = {**msg, "content": compressed_str}
|
| 387 |
+
except Exception:
|
| 388 |
+
pass
|
| 389 |
+
optimized_messages.append(msg)
|
| 390 |
+
|
| 391 |
+
result.optimization_latency_ms = (time.perf_counter() - start) * 1000
|
| 392 |
+
|
| 393 |
+
optimized_content = json.dumps(optimized_messages)
|
| 394 |
+
result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN
|
| 395 |
+
result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original)
|
| 396 |
+
|
| 397 |
+
# Cost analysis
|
| 398 |
+
pricing = PRICING["claude-3.5-sonnet"]
|
| 399 |
+
result.cost_analysis = CostAnalysis(
|
| 400 |
+
tokens_input=result.tokens_original,
|
| 401 |
+
cost_baseline=result.tokens_original * pricing["input"],
|
| 402 |
+
cost_optimized=result.tokens_optimized * pricing["input"],
|
| 403 |
+
savings_from_compression=(result.tokens_original - result.tokens_optimized)
|
| 404 |
+
* pricing["input"],
|
| 405 |
+
total_savings_percent=result.compression_ratio * 100,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
result.details = {
|
| 409 |
+
"context_tokens": 50000,
|
| 410 |
+
"num_queries": 10,
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
return result
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
# =============================================================================
|
| 417 |
+
# SCENARIO 4: Long-Running Agent Session
|
| 418 |
+
# =============================================================================
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def benchmark_conversation_scaling() -> list[BenchmarkResult]:
|
| 422 |
+
"""
|
| 423 |
+
Show how costs scale with conversation length.
|
| 424 |
+
|
| 425 |
+
Generates conversations of increasing length (10, 25, 50, 100, 200 turns)
|
| 426 |
+
and shows the scaling curve with and without Headroom.
|
| 427 |
+
"""
|
| 428 |
+
results = []
|
| 429 |
+
turn_counts = [10, 25, 50, 100, 200]
|
| 430 |
+
|
| 431 |
+
for turns in turn_counts:
|
| 432 |
+
result = BenchmarkResult(
|
| 433 |
+
name=f"Conversation Scaling ({turns} turns)",
|
| 434 |
+
description=f"{turns}-turn agent conversation with tool calls",
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
messages = generate_agentic_conversation(
|
| 438 |
+
turns=turns,
|
| 439 |
+
tool_calls_per_turn=1,
|
| 440 |
+
items_per_tool_response=50,
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
original_content = json.dumps(messages)
|
| 444 |
+
result.tokens_original = len(original_content) // CHARS_PER_TOKEN
|
| 445 |
+
|
| 446 |
+
# Apply full optimization pipeline
|
| 447 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 448 |
+
|
| 449 |
+
start = time.perf_counter()
|
| 450 |
+
|
| 451 |
+
optimized = []
|
| 452 |
+
for msg in messages:
|
| 453 |
+
if msg.get("role") == "tool":
|
| 454 |
+
try:
|
| 455 |
+
original_content = msg.get("content", "[]")
|
| 456 |
+
content = json.loads(original_content)
|
| 457 |
+
if isinstance(content, list) and len(content) > 15:
|
| 458 |
+
compressed_str, was_modified, _ = smart_crush_tool_output(
|
| 459 |
+
original_content, config
|
| 460 |
+
)
|
| 461 |
+
if was_modified:
|
| 462 |
+
msg = {**msg, "content": compressed_str}
|
| 463 |
+
except (json.JSONDecodeError, TypeError):
|
| 464 |
+
pass
|
| 465 |
+
optimized.append(msg)
|
| 466 |
+
|
| 467 |
+
result.optimization_latency_ms = (time.perf_counter() - start) * 1000
|
| 468 |
+
|
| 469 |
+
optimized_content = json.dumps(optimized)
|
| 470 |
+
result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN
|
| 471 |
+
result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original)
|
| 472 |
+
|
| 473 |
+
pricing = PRICING["claude-3.5-sonnet"]
|
| 474 |
+
result.cost_analysis = CostAnalysis(
|
| 475 |
+
tokens_input=result.tokens_original,
|
| 476 |
+
cost_baseline=result.tokens_original * pricing["input"],
|
| 477 |
+
cost_optimized=result.tokens_optimized * pricing["input"],
|
| 478 |
+
total_savings_percent=result.compression_ratio * 100,
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
result.details = {"turns": turns}
|
| 482 |
+
results.append(result)
|
| 483 |
+
|
| 484 |
+
return results
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
# =============================================================================
|
| 488 |
+
# SCENARIO 5: Quality Preservation Test
|
| 489 |
+
# =============================================================================
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def benchmark_quality_preservation() -> BenchmarkResult:
|
| 493 |
+
"""
|
| 494 |
+
Prove that compression doesn't lose critical information.
|
| 495 |
+
|
| 496 |
+
Generates data with known "needles" (errors, anomalies, high-relevance items)
|
| 497 |
+
and verifies they survive compression.
|
| 498 |
+
"""
|
| 499 |
+
result = BenchmarkResult(
|
| 500 |
+
name="Quality Preservation",
|
| 501 |
+
description="Verify critical items (errors, anomalies) survive compression",
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Generate test data with known needles
|
| 505 |
+
search_results = generate_search_results(
|
| 506 |
+
n=1000,
|
| 507 |
+
include_uuid_needles=10,
|
| 508 |
+
include_errors=20,
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
log_entries = generate_log_entries(
|
| 512 |
+
n=1000,
|
| 513 |
+
include_errors=30,
|
| 514 |
+
include_critical=5,
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
# Count needles before compression
|
| 518 |
+
needles_before = 0
|
| 519 |
+
errors_before = 0
|
| 520 |
+
|
| 521 |
+
for item in search_results:
|
| 522 |
+
if item.get("is_needle"):
|
| 523 |
+
needles_before += 1
|
| 524 |
+
if item.get("error"):
|
| 525 |
+
errors_before += 1
|
| 526 |
+
|
| 527 |
+
for entry in log_entries:
|
| 528 |
+
if entry.get("level") in ("ERROR", "CRITICAL"):
|
| 529 |
+
errors_before += 1
|
| 530 |
+
|
| 531 |
+
# Compress using SmartCrusher convenience function
|
| 532 |
+
config = SmartCrusherConfig(max_items_after_crush=50)
|
| 533 |
+
|
| 534 |
+
search_str = json.dumps(search_results)
|
| 535 |
+
logs_str = json.dumps(log_entries)
|
| 536 |
+
|
| 537 |
+
compressed_search_str, _, _ = smart_crush_tool_output(search_str, config)
|
| 538 |
+
compressed_logs_str, _, _ = smart_crush_tool_output(logs_str, config)
|
| 539 |
+
|
| 540 |
+
compressed_search = json.loads(compressed_search_str)
|
| 541 |
+
compressed_logs = json.loads(compressed_logs_str)
|
| 542 |
+
|
| 543 |
+
# Count needles after compression
|
| 544 |
+
needles_after = 0
|
| 545 |
+
errors_after = 0
|
| 546 |
+
|
| 547 |
+
for item in compressed_search:
|
| 548 |
+
if item.get("is_needle"):
|
| 549 |
+
needles_after += 1
|
| 550 |
+
if item.get("error"):
|
| 551 |
+
errors_after += 1
|
| 552 |
+
|
| 553 |
+
for entry in compressed_logs:
|
| 554 |
+
if entry.get("level") in ("ERROR", "CRITICAL"):
|
| 555 |
+
errors_after += 1
|
| 556 |
+
|
| 557 |
+
result.critical_items_total = needles_before + errors_before
|
| 558 |
+
result.critical_items_retained = needles_after + errors_after
|
| 559 |
+
result.retention_rate = result.critical_items_retained / result.critical_items_total
|
| 560 |
+
|
| 561 |
+
result.tokens_original = (
|
| 562 |
+
len(json.dumps(search_results)) + len(json.dumps(log_entries))
|
| 563 |
+
) // CHARS_PER_TOKEN
|
| 564 |
+
result.tokens_optimized = (
|
| 565 |
+
len(json.dumps(compressed_search)) + len(json.dumps(compressed_logs))
|
| 566 |
+
) // CHARS_PER_TOKEN
|
| 567 |
+
result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original)
|
| 568 |
+
|
| 569 |
+
result.details = {
|
| 570 |
+
"search_results_original": 1000,
|
| 571 |
+
"search_results_compressed": len(compressed_search),
|
| 572 |
+
"log_entries_original": 1000,
|
| 573 |
+
"log_entries_compressed": len(compressed_logs),
|
| 574 |
+
"needles_original": needles_before,
|
| 575 |
+
"needles_retained": needles_after,
|
| 576 |
+
"errors_original": errors_before,
|
| 577 |
+
"errors_retained": errors_after,
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
return result
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# =============================================================================
|
| 584 |
+
# REPORT GENERATION
|
| 585 |
+
# =============================================================================
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def generate_report(results: list[BenchmarkResult], format: str = "terminal") -> str:
|
| 589 |
+
"""Generate benchmark report in specified format."""
|
| 590 |
+
|
| 591 |
+
if format == "markdown":
|
| 592 |
+
return _generate_markdown_report(results)
|
| 593 |
+
else:
|
| 594 |
+
return _generate_terminal_report(results)
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _generate_terminal_report(results: list[BenchmarkResult]) -> str:
|
| 598 |
+
"""Generate colorful terminal report."""
|
| 599 |
+
lines = []
|
| 600 |
+
|
| 601 |
+
lines.append("")
|
| 602 |
+
lines.append("=" * 80)
|
| 603 |
+
lines.append(" HEADROOM AGENT COST BENCHMARK")
|
| 604 |
+
lines.append(" The Context Optimization Layer for LLM Applications")
|
| 605 |
+
lines.append("=" * 80)
|
| 606 |
+
|
| 607 |
+
total_savings = 0.0
|
| 608 |
+
total_baseline = 0.0
|
| 609 |
+
|
| 610 |
+
for result in results:
|
| 611 |
+
lines.append("")
|
| 612 |
+
lines.append(f"{'─' * 80}")
|
| 613 |
+
lines.append(f" {result.name}")
|
| 614 |
+
lines.append(f" {result.description}")
|
| 615 |
+
lines.append(f"{'─' * 80}")
|
| 616 |
+
|
| 617 |
+
# Token metrics
|
| 618 |
+
lines.append(f" Tokens (original): {result.tokens_original:>12,}")
|
| 619 |
+
lines.append(f" Tokens (optimized): {result.tokens_optimized:>12,}")
|
| 620 |
+
lines.append(f" Compression: {result.compression_ratio * 100:>11.1f}%")
|
| 621 |
+
|
| 622 |
+
# Cache metrics (if applicable)
|
| 623 |
+
if result.cache_hit_rate_optimized > 0:
|
| 624 |
+
lines.append(f" Cache Hit (before): {result.cache_hit_rate_baseline * 100:>11.1f}%")
|
| 625 |
+
lines.append(f" Cache Hit (after): {result.cache_hit_rate_optimized * 100:>11.1f}%")
|
| 626 |
+
|
| 627 |
+
# Quality metrics (if applicable)
|
| 628 |
+
if result.critical_items_total > 0:
|
| 629 |
+
lines.append(
|
| 630 |
+
f" Critical Items: {result.critical_items_retained}/{result.critical_items_total} retained"
|
| 631 |
+
)
|
| 632 |
+
lines.append(f" Retention Rate: {result.retention_rate * 100:>11.1f}%")
|
| 633 |
+
|
| 634 |
+
# Cost analysis
|
| 635 |
+
ca = result.cost_analysis
|
| 636 |
+
if ca.cost_baseline > 0:
|
| 637 |
+
lines.append(f" Cost (baseline): ${ca.cost_baseline:>11.4f}")
|
| 638 |
+
if ca.cost_optimized > 0:
|
| 639 |
+
lines.append(f" Cost (optimized): ${ca.cost_optimized:>11.4f}")
|
| 640 |
+
if ca.cost_with_cache > 0:
|
| 641 |
+
lines.append(f" Cost (with cache): ${ca.cost_with_cache:>11.4f}")
|
| 642 |
+
lines.append(f" Savings: {ca.total_savings_percent:>11.1f}%")
|
| 643 |
+
|
| 644 |
+
total_baseline += ca.cost_baseline
|
| 645 |
+
if ca.cost_optimized > 0:
|
| 646 |
+
total_savings += ca.cost_baseline - ca.cost_optimized
|
| 647 |
+
elif ca.cost_with_cache > 0:
|
| 648 |
+
total_savings += ca.cost_baseline - ca.cost_with_cache
|
| 649 |
+
|
| 650 |
+
# Performance
|
| 651 |
+
if result.optimization_latency_ms > 0:
|
| 652 |
+
lines.append(f" Optimization Time: {result.optimization_latency_ms:>11.2f}ms")
|
| 653 |
+
|
| 654 |
+
# Summary
|
| 655 |
+
lines.append("")
|
| 656 |
+
lines.append("=" * 80)
|
| 657 |
+
lines.append(" SUMMARY")
|
| 658 |
+
lines.append("=" * 80)
|
| 659 |
+
if total_baseline > 0:
|
| 660 |
+
lines.append(f" Total Baseline Cost: ${total_baseline:.4f}")
|
| 661 |
+
lines.append(f" Total Savings: ${total_savings:.4f}")
|
| 662 |
+
lines.append(f" Overall Reduction: {(total_savings / total_baseline) * 100:.1f}%")
|
| 663 |
+
lines.append("")
|
| 664 |
+
lines.append(" At 1M requests/month:")
|
| 665 |
+
lines.append(f" Without Headroom: ${total_baseline * 1_000_000:.2f}")
|
| 666 |
+
lines.append(f" With Headroom: ${(total_baseline - total_savings) * 1_000_000:.2f}")
|
| 667 |
+
lines.append(f" Monthly Savings: ${total_savings * 1_000_000:.2f}")
|
| 668 |
+
lines.append("")
|
| 669 |
+
|
| 670 |
+
return "\n".join(lines)
|
| 671 |
+
|
| 672 |
+
|
| 673 |
+
def _generate_markdown_report(results: list[BenchmarkResult]) -> str:
|
| 674 |
+
"""Generate markdown report for documentation."""
|
| 675 |
+
lines = []
|
| 676 |
+
|
| 677 |
+
lines.append("# Headroom Agent Cost Benchmark")
|
| 678 |
+
lines.append("")
|
| 679 |
+
lines.append("> The Context Optimization Layer for LLM Applications")
|
| 680 |
+
lines.append("")
|
| 681 |
+
lines.append("## Executive Summary")
|
| 682 |
+
lines.append("")
|
| 683 |
+
lines.append("This benchmark demonstrates Headroom's impact on real-world agent workloads:")
|
| 684 |
+
lines.append("")
|
| 685 |
+
lines.append("| Metric | Impact |")
|
| 686 |
+
lines.append("|--------|--------|")
|
| 687 |
+
|
| 688 |
+
# Calculate summary metrics
|
| 689 |
+
total_compression = statistics.mean(
|
| 690 |
+
[r.compression_ratio for r in results if r.compression_ratio > 0]
|
| 691 |
+
)
|
| 692 |
+
cache_improvement = next((r for r in results if r.cache_hit_rate_optimized > 0), None)
|
| 693 |
+
quality_result = next((r for r in results if r.retention_rate > 0), None)
|
| 694 |
+
|
| 695 |
+
lines.append(f"| Token Reduction | **{total_compression * 100:.0f}%** average compression |")
|
| 696 |
+
if cache_improvement:
|
| 697 |
+
lines.append(
|
| 698 |
+
f"| Cache Hit Rate | **{cache_improvement.cache_hit_rate_baseline * 100:.0f}% → {cache_improvement.cache_hit_rate_optimized * 100:.0f}%** |"
|
| 699 |
+
)
|
| 700 |
+
if quality_result:
|
| 701 |
+
lines.append(
|
| 702 |
+
f"| Quality Retention | **{quality_result.retention_rate * 100:.0f}%** critical items preserved |"
|
| 703 |
+
)
|
| 704 |
+
lines.append("")
|
| 705 |
+
|
| 706 |
+
# Detailed results
|
| 707 |
+
lines.append("## Detailed Results")
|
| 708 |
+
lines.append("")
|
| 709 |
+
|
| 710 |
+
for result in results:
|
| 711 |
+
lines.append(f"### {result.name}")
|
| 712 |
+
lines.append("")
|
| 713 |
+
lines.append(f"*{result.description}*")
|
| 714 |
+
lines.append("")
|
| 715 |
+
|
| 716 |
+
lines.append("| Metric | Value |")
|
| 717 |
+
lines.append("|--------|-------|")
|
| 718 |
+
lines.append(f"| Original Tokens | {result.tokens_original:,} |")
|
| 719 |
+
lines.append(f"| Optimized Tokens | {result.tokens_optimized:,} |")
|
| 720 |
+
lines.append(f"| Compression | {result.compression_ratio * 100:.1f}% |")
|
| 721 |
+
|
| 722 |
+
if result.cost_analysis.total_savings_percent > 0:
|
| 723 |
+
lines.append(f"| Cost Savings | {result.cost_analysis.total_savings_percent:.1f}% |")
|
| 724 |
+
|
| 725 |
+
if result.retention_rate > 0:
|
| 726 |
+
lines.append(f"| Quality Retention | {result.retention_rate * 100:.1f}% |")
|
| 727 |
+
|
| 728 |
+
lines.append("")
|
| 729 |
+
|
| 730 |
+
# Cost projection
|
| 731 |
+
lines.append("## Cost Projection at Scale")
|
| 732 |
+
lines.append("")
|
| 733 |
+
lines.append("Based on Claude 3.5 Sonnet pricing ($3/1M input tokens):")
|
| 734 |
+
lines.append("")
|
| 735 |
+
lines.append("| Scale | Without Headroom | With Headroom | Monthly Savings |")
|
| 736 |
+
lines.append("|-------|------------------|---------------|-----------------|")
|
| 737 |
+
|
| 738 |
+
base_cost_per_request = sum(r.cost_analysis.cost_baseline for r in results) / len(results)
|
| 739 |
+
optimized_cost = sum(
|
| 740 |
+
r.cost_analysis.cost_optimized
|
| 741 |
+
or r.cost_analysis.cost_with_cache
|
| 742 |
+
or r.cost_analysis.cost_baseline * 0.5
|
| 743 |
+
for r in results
|
| 744 |
+
) / len(results)
|
| 745 |
+
|
| 746 |
+
for scale, label in [(10_000, "10K"), (100_000, "100K"), (1_000_000, "1M")]:
|
| 747 |
+
baseline = base_cost_per_request * scale
|
| 748 |
+
optimized = optimized_cost * scale
|
| 749 |
+
savings = baseline - optimized
|
| 750 |
+
lines.append(
|
| 751 |
+
f"| {label} requests/mo | ${baseline:,.0f} | ${optimized:,.0f} | ${savings:,.0f} |"
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
lines.append("")
|
| 755 |
+
|
| 756 |
+
return "\n".join(lines)
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
# =============================================================================
|
| 760 |
+
# MAIN
|
| 761 |
+
# =============================================================================
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def main():
|
| 765 |
+
parser = argparse.ArgumentParser(description="Headroom Agent Cost Benchmark")
|
| 766 |
+
parser.add_argument("--format", choices=["terminal", "markdown"], default="terminal")
|
| 767 |
+
parser.add_argument(
|
| 768 |
+
"--scenario",
|
| 769 |
+
choices=["all", "coding-agent", "cache", "rag", "scaling", "quality"],
|
| 770 |
+
default="all",
|
| 771 |
+
)
|
| 772 |
+
args = parser.parse_args()
|
| 773 |
+
|
| 774 |
+
results = []
|
| 775 |
+
|
| 776 |
+
print("Running benchmarks...\n")
|
| 777 |
+
|
| 778 |
+
if args.scenario in ("all", "coding-agent"):
|
| 779 |
+
print(" [1/5] Coding Agent Context Explosion...")
|
| 780 |
+
results.append(benchmark_coding_agent_explosion())
|
| 781 |
+
|
| 782 |
+
if args.scenario in ("all", "cache"):
|
| 783 |
+
print(" [2/5] Cache Alignment Impact...")
|
| 784 |
+
results.append(benchmark_cache_alignment())
|
| 785 |
+
|
| 786 |
+
if args.scenario in ("all", "rag"):
|
| 787 |
+
print(" [3/5] RAG Context Scaling...")
|
| 788 |
+
results.append(benchmark_rag_scaling())
|
| 789 |
+
|
| 790 |
+
if args.scenario in ("all", "scaling"):
|
| 791 |
+
print(" [4/5] Conversation Scaling...")
|
| 792 |
+
scaling_results = benchmark_conversation_scaling()
|
| 793 |
+
# Just add the 100-turn result to main results
|
| 794 |
+
results.append(scaling_results[3]) # 100 turns
|
| 795 |
+
|
| 796 |
+
if args.scenario in ("all", "quality"):
|
| 797 |
+
print(" [5/5] Quality Preservation...")
|
| 798 |
+
results.append(benchmark_quality_preservation())
|
| 799 |
+
|
| 800 |
+
print("\n" + generate_report(results, args.format))
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
if __name__ == "__main__":
|
| 804 |
+
main()
|
benchmarks/bench_relevance.py
CHANGED
|
@@ -25,7 +25,6 @@ Run with:
|
|
| 25 |
from __future__ import annotations
|
| 26 |
|
| 27 |
import json
|
| 28 |
-
from typing import Any
|
| 29 |
|
| 30 |
import pytest
|
| 31 |
|
|
@@ -33,7 +32,8 @@ import pytest
|
|
| 33 |
def _check_embedding_available() -> bool:
|
| 34 |
"""Check if sentence-transformers is available for embedding tests."""
|
| 35 |
try:
|
| 36 |
-
import sentence_transformers
|
|
|
|
| 37 |
return True
|
| 38 |
except ImportError:
|
| 39 |
return False
|
|
@@ -203,8 +203,8 @@ class TestHybridBenchmarks:
|
|
| 203 |
@pytest.fixture
|
| 204 |
def scorer_fallback(self):
|
| 205 |
"""Create hybrid scorer without embeddings (BM25 fallback)."""
|
| 206 |
-
from headroom.relevance.hybrid import HybridScorer
|
| 207 |
from headroom.relevance.bm25 import BM25Scorer
|
|
|
|
| 208 |
|
| 209 |
# Force BM25-only mode by not providing embedding scorer
|
| 210 |
scorer = HybridScorer(
|
|
@@ -378,8 +378,8 @@ class TestRelevanceInSmartCrusher:
|
|
| 378 |
@pytest.fixture
|
| 379 |
def crusher_with_bm25(self, smart_crusher_config):
|
| 380 |
"""SmartCrusher with BM25 relevance scorer."""
|
| 381 |
-
from headroom.transforms.smart_crusher import SmartCrusher
|
| 382 |
from headroom.config import RelevanceScorerConfig
|
|
|
|
| 383 |
|
| 384 |
return SmartCrusher(
|
| 385 |
config=smart_crusher_config,
|
|
@@ -389,8 +389,8 @@ class TestRelevanceInSmartCrusher:
|
|
| 389 |
@pytest.fixture
|
| 390 |
def crusher_with_hybrid(self, smart_crusher_config):
|
| 391 |
"""SmartCrusher with hybrid relevance scorer."""
|
| 392 |
-
from headroom.transforms.smart_crusher import SmartCrusher
|
| 393 |
from headroom.config import RelevanceScorerConfig
|
|
|
|
| 394 |
|
| 395 |
return SmartCrusher(
|
| 396 |
config=smart_crusher_config,
|
|
|
|
| 25 |
from __future__ import annotations
|
| 26 |
|
| 27 |
import json
|
|
|
|
| 28 |
|
| 29 |
import pytest
|
| 30 |
|
|
|
|
| 32 |
def _check_embedding_available() -> bool:
|
| 33 |
"""Check if sentence-transformers is available for embedding tests."""
|
| 34 |
try:
|
| 35 |
+
import sentence_transformers # noqa: F401
|
| 36 |
+
|
| 37 |
return True
|
| 38 |
except ImportError:
|
| 39 |
return False
|
|
|
|
| 203 |
@pytest.fixture
|
| 204 |
def scorer_fallback(self):
|
| 205 |
"""Create hybrid scorer without embeddings (BM25 fallback)."""
|
|
|
|
| 206 |
from headroom.relevance.bm25 import BM25Scorer
|
| 207 |
+
from headroom.relevance.hybrid import HybridScorer
|
| 208 |
|
| 209 |
# Force BM25-only mode by not providing embedding scorer
|
| 210 |
scorer = HybridScorer(
|
|
|
|
| 378 |
@pytest.fixture
|
| 379 |
def crusher_with_bm25(self, smart_crusher_config):
|
| 380 |
"""SmartCrusher with BM25 relevance scorer."""
|
|
|
|
| 381 |
from headroom.config import RelevanceScorerConfig
|
| 382 |
+
from headroom.transforms.smart_crusher import SmartCrusher
|
| 383 |
|
| 384 |
return SmartCrusher(
|
| 385 |
config=smart_crusher_config,
|
|
|
|
| 389 |
@pytest.fixture
|
| 390 |
def crusher_with_hybrid(self, smart_crusher_config):
|
| 391 |
"""SmartCrusher with hybrid relevance scorer."""
|
|
|
|
| 392 |
from headroom.config import RelevanceScorerConfig
|
| 393 |
+
from headroom.transforms.smart_crusher import SmartCrusher
|
| 394 |
|
| 395 |
return SmartCrusher(
|
| 396 |
config=smart_crusher_config,
|
benchmarks/bench_transforms.py
CHANGED
|
@@ -26,7 +26,6 @@ Run with:
|
|
| 26 |
from __future__ import annotations
|
| 27 |
|
| 28 |
import json
|
| 29 |
-
from typing import Any
|
| 30 |
|
| 31 |
import pytest
|
| 32 |
|
|
@@ -205,8 +204,16 @@ class TestSmartCrusherBenchmarks:
|
|
| 205 |
"role": "assistant",
|
| 206 |
"content": None,
|
| 207 |
"tool_calls": [
|
| 208 |
-
{
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
],
|
| 211 |
},
|
| 212 |
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items_100)},
|
|
@@ -345,7 +352,10 @@ And multiple blank lines."""
|
|
| 345 |
Tests edge case of multiple system prompts.
|
| 346 |
"""
|
| 347 |
messages = [
|
| 348 |
-
{
|
|
|
|
|
|
|
|
|
|
| 349 |
{"role": "system", "content": "Additional context: Technical support mode."},
|
| 350 |
{"role": "user", "content": "Hello"},
|
| 351 |
]
|
|
@@ -511,12 +521,14 @@ class TestTransformPipelineBenchmarks:
|
|
| 511 |
return provider
|
| 512 |
|
| 513 |
@pytest.fixture
|
| 514 |
-
def pipeline(
|
|
|
|
|
|
|
| 515 |
"""Create transform pipeline."""
|
| 516 |
-
from headroom.transforms.pipeline import TransformPipeline
|
| 517 |
from headroom.transforms.cache_aligner import CacheAligner
|
| 518 |
-
from headroom.transforms.
|
| 519 |
from headroom.transforms.rolling_window import RollingWindow
|
|
|
|
| 520 |
|
| 521 |
return TransformPipeline(
|
| 522 |
transforms=[
|
|
|
|
| 26 |
from __future__ import annotations
|
| 27 |
|
| 28 |
import json
|
|
|
|
| 29 |
|
| 30 |
import pytest
|
| 31 |
|
|
|
|
| 204 |
"role": "assistant",
|
| 205 |
"content": None,
|
| 206 |
"tool_calls": [
|
| 207 |
+
{
|
| 208 |
+
"id": "call_1",
|
| 209 |
+
"type": "function",
|
| 210 |
+
"function": {"name": "search", "arguments": "{}"},
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"id": "call_2",
|
| 214 |
+
"type": "function",
|
| 215 |
+
"function": {"name": "logs", "arguments": "{}"},
|
| 216 |
+
},
|
| 217 |
],
|
| 218 |
},
|
| 219 |
{"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items_100)},
|
|
|
|
| 352 |
Tests edge case of multiple system prompts.
|
| 353 |
"""
|
| 354 |
messages = [
|
| 355 |
+
{
|
| 356 |
+
"role": "system",
|
| 357 |
+
"content": "You are a helpful assistant.\n\nCurrent date: 2025-01-06",
|
| 358 |
+
},
|
| 359 |
{"role": "system", "content": "Additional context: Technical support mode."},
|
| 360 |
{"role": "user", "content": "Hello"},
|
| 361 |
]
|
|
|
|
| 521 |
return provider
|
| 522 |
|
| 523 |
@pytest.fixture
|
| 524 |
+
def pipeline(
|
| 525 |
+
self, smart_crusher_config, cache_aligner_config, rolling_window_config, mock_provider
|
| 526 |
+
):
|
| 527 |
"""Create transform pipeline."""
|
|
|
|
| 528 |
from headroom.transforms.cache_aligner import CacheAligner
|
| 529 |
+
from headroom.transforms.pipeline import TransformPipeline
|
| 530 |
from headroom.transforms.rolling_window import RollingWindow
|
| 531 |
+
from headroom.transforms.smart_crusher import SmartCrusher
|
| 532 |
|
| 533 |
return TransformPipeline(
|
| 534 |
transforms=[
|
benchmarks/ccr_regression_benchmark.py
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
CCR Regression Benchmark - Verify No Information Loss
|
| 4 |
+
|
| 5 |
+
This benchmark tests that the CCR (Compress-Cache-Retrieve) architecture
|
| 6 |
+
does not cause any regression in agent behavior. Specifically:
|
| 7 |
+
|
| 8 |
+
1. NEEDLE RETENTION: Critical items survive compression
|
| 9 |
+
- Errors, exceptions, failures
|
| 10 |
+
- Specific IDs/UUIDs mentioned in user query
|
| 11 |
+
- Anomalies and outliers
|
| 12 |
+
|
| 13 |
+
2. RETRIEVAL ACCURACY: When retrieval is needed, correct items are returned
|
| 14 |
+
- Full retrieval returns original content
|
| 15 |
+
- Search retrieval finds relevant items
|
| 16 |
+
|
| 17 |
+
3. FEEDBACK LEARNING: System learns from retrieval patterns
|
| 18 |
+
- High retrieval rate triggers less aggressive compression
|
| 19 |
+
- Common queries improve future compression
|
| 20 |
+
|
| 21 |
+
Usage:
|
| 22 |
+
python benchmarks/ccr_regression_benchmark.py
|
| 23 |
+
python benchmarks/ccr_regression_benchmark.py --verbose
|
| 24 |
+
python benchmarks/ccr_regression_benchmark.py --scenario needle-in-haystack
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import json
|
| 31 |
+
import time
|
| 32 |
+
import uuid
|
| 33 |
+
from dataclasses import dataclass, field
|
| 34 |
+
from typing import Any
|
| 35 |
+
|
| 36 |
+
from headroom.cache.compression_feedback import (
|
| 37 |
+
get_compression_feedback,
|
| 38 |
+
reset_compression_feedback,
|
| 39 |
+
)
|
| 40 |
+
from headroom.cache.compression_store import (
|
| 41 |
+
get_compression_store,
|
| 42 |
+
reset_compression_store,
|
| 43 |
+
)
|
| 44 |
+
from headroom.transforms.smart_crusher import (
|
| 45 |
+
SmartCrusherConfig,
|
| 46 |
+
smart_crush_tool_output,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class RegressionResult:
|
| 52 |
+
"""Result from a regression test."""
|
| 53 |
+
|
| 54 |
+
name: str
|
| 55 |
+
description: str
|
| 56 |
+
passed: bool = False # Default to False, set to True when test passes
|
| 57 |
+
|
| 58 |
+
# Metrics
|
| 59 |
+
total_needles: int = 0
|
| 60 |
+
needles_retained: int = 0
|
| 61 |
+
retention_rate: float = 0.0
|
| 62 |
+
|
| 63 |
+
# CCR metrics
|
| 64 |
+
items_compressed: int = 0
|
| 65 |
+
items_retrieved: int = 0
|
| 66 |
+
retrieval_accuracy: float = 0.0
|
| 67 |
+
|
| 68 |
+
# Performance
|
| 69 |
+
latency_ms: float = 0.0
|
| 70 |
+
|
| 71 |
+
# Details
|
| 72 |
+
details: dict[str, Any] = field(default_factory=dict)
|
| 73 |
+
failures: list[str] = field(default_factory=list)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# =============================================================================
|
| 77 |
+
# TEST 1: Needle in Haystack - Error Retention
|
| 78 |
+
# =============================================================================
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_error_retention() -> RegressionResult:
|
| 82 |
+
"""
|
| 83 |
+
Test that errors are NEVER lost during compression.
|
| 84 |
+
|
| 85 |
+
This is critical: if an API returns 1000 results with 3 errors,
|
| 86 |
+
those 3 errors MUST be in the compressed output.
|
| 87 |
+
"""
|
| 88 |
+
result = RegressionResult(
|
| 89 |
+
name="Error Retention",
|
| 90 |
+
description="Verify all errors survive compression regardless of position",
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# Generate 1000 items with errors at various positions
|
| 94 |
+
items = []
|
| 95 |
+
error_indices = [5, 47, 123, 456, 789, 999] # Spread throughout
|
| 96 |
+
|
| 97 |
+
for i in range(1000):
|
| 98 |
+
if i in error_indices:
|
| 99 |
+
items.append(
|
| 100 |
+
{
|
| 101 |
+
"id": i,
|
| 102 |
+
"status": "error",
|
| 103 |
+
"message": f"Connection failed: timeout at {i}",
|
| 104 |
+
"error_code": 500 + (i % 10),
|
| 105 |
+
}
|
| 106 |
+
)
|
| 107 |
+
else:
|
| 108 |
+
items.append(
|
| 109 |
+
{
|
| 110 |
+
"id": i,
|
| 111 |
+
"status": "success",
|
| 112 |
+
"message": "OK",
|
| 113 |
+
"data": {"value": i * 2},
|
| 114 |
+
}
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
result.total_needles = len(error_indices)
|
| 118 |
+
|
| 119 |
+
# Compress with SmartCrusher
|
| 120 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 121 |
+
original_json = json.dumps(items)
|
| 122 |
+
|
| 123 |
+
start = time.perf_counter()
|
| 124 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 125 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 126 |
+
|
| 127 |
+
# Count errors in compressed output
|
| 128 |
+
compressed = json.loads(compressed_json)
|
| 129 |
+
errors_found = [item for item in compressed if item.get("status") == "error"]
|
| 130 |
+
|
| 131 |
+
result.needles_retained = len(errors_found)
|
| 132 |
+
result.retention_rate = result.needles_retained / result.total_needles
|
| 133 |
+
result.items_compressed = len(compressed)
|
| 134 |
+
|
| 135 |
+
# Check if ALL errors were retained
|
| 136 |
+
result.passed = result.needles_retained == result.total_needles
|
| 137 |
+
|
| 138 |
+
if not result.passed:
|
| 139 |
+
result.failures.append(
|
| 140 |
+
f"Lost {result.total_needles - result.needles_retained} errors during compression"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
result.details = {
|
| 144 |
+
"original_items": 1000,
|
| 145 |
+
"compressed_items": len(compressed),
|
| 146 |
+
"error_positions": error_indices,
|
| 147 |
+
"errors_retained": result.needles_retained,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return result
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# =============================================================================
|
| 154 |
+
# TEST 2: Needle in Haystack - UUID Lookup
|
| 155 |
+
# =============================================================================
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_uuid_retrieval() -> RegressionResult:
|
| 159 |
+
"""
|
| 160 |
+
Test that specific UUIDs can be found via CCR retrieval.
|
| 161 |
+
|
| 162 |
+
Scenario: User asks "find transaction abc123..."
|
| 163 |
+
The system compresses, but user should be able to retrieve the specific item.
|
| 164 |
+
"""
|
| 165 |
+
result = RegressionResult(
|
| 166 |
+
name="UUID Retrieval via CCR",
|
| 167 |
+
description="Verify specific UUIDs can be retrieved from compressed cache",
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
reset_compression_store()
|
| 171 |
+
store = get_compression_store()
|
| 172 |
+
|
| 173 |
+
# Generate 1000 transactions with UUIDs
|
| 174 |
+
target_uuid = str(uuid.uuid4())
|
| 175 |
+
items = []
|
| 176 |
+
|
| 177 |
+
for i in range(1000):
|
| 178 |
+
item_uuid = target_uuid if i == 456 else str(uuid.uuid4())
|
| 179 |
+
items.append(
|
| 180 |
+
{
|
| 181 |
+
"transaction_id": item_uuid,
|
| 182 |
+
"amount": 100 + (i % 1000),
|
| 183 |
+
"status": "completed",
|
| 184 |
+
"timestamp": f"2025-01-{(i % 28) + 1:02d}T10:00:00Z",
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
result.total_needles = 1
|
| 189 |
+
|
| 190 |
+
# Store original and compress
|
| 191 |
+
original_json = json.dumps(items)
|
| 192 |
+
config = SmartCrusherConfig(max_items_after_crush=15)
|
| 193 |
+
|
| 194 |
+
start = time.perf_counter()
|
| 195 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 196 |
+
|
| 197 |
+
# Store in CCR cache
|
| 198 |
+
hash_key = store.store(
|
| 199 |
+
original=original_json,
|
| 200 |
+
compressed=compressed_json,
|
| 201 |
+
original_item_count=1000,
|
| 202 |
+
compressed_item_count=15,
|
| 203 |
+
tool_name="transaction_search",
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# Search for the specific UUID
|
| 207 |
+
search_results = store.search(hash_key, target_uuid)
|
| 208 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 209 |
+
|
| 210 |
+
# Check if target UUID was found
|
| 211 |
+
found_target = any(item.get("transaction_id") == target_uuid for item in search_results)
|
| 212 |
+
|
| 213 |
+
result.needles_retained = 1 if found_target else 0
|
| 214 |
+
result.retention_rate = result.needles_retained / result.total_needles
|
| 215 |
+
result.items_retrieved = len(search_results)
|
| 216 |
+
result.retrieval_accuracy = 1.0 if found_target else 0.0
|
| 217 |
+
|
| 218 |
+
result.passed = found_target
|
| 219 |
+
|
| 220 |
+
if not result.passed:
|
| 221 |
+
result.failures.append(
|
| 222 |
+
f"Could not retrieve target UUID {target_uuid[:8]}... via CCR search"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
result.details = {
|
| 226 |
+
"target_uuid": target_uuid,
|
| 227 |
+
"search_results_count": len(search_results),
|
| 228 |
+
"found_target": found_target,
|
| 229 |
+
"hash_key": hash_key,
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
return result
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# =============================================================================
|
| 236 |
+
# TEST 3: Anomaly Detection
|
| 237 |
+
# =============================================================================
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def test_anomaly_retention() -> RegressionResult:
|
| 241 |
+
"""
|
| 242 |
+
Test that statistical anomalies are preserved during compression.
|
| 243 |
+
|
| 244 |
+
Scenario: 1000 metrics mostly at ~50, but with 5 spikes at 500+.
|
| 245 |
+
Those spikes MUST survive compression.
|
| 246 |
+
"""
|
| 247 |
+
result = RegressionResult(
|
| 248 |
+
name="Anomaly Retention", description="Verify statistical outliers survive compression"
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Generate metrics with anomalies
|
| 252 |
+
import random
|
| 253 |
+
|
| 254 |
+
random.seed(42) # Reproducible
|
| 255 |
+
|
| 256 |
+
items = []
|
| 257 |
+
anomaly_indices = [10, 200, 450, 700, 990] # 5 spikes
|
| 258 |
+
|
| 259 |
+
for i in range(1000):
|
| 260 |
+
if i in anomaly_indices:
|
| 261 |
+
# Anomaly: 10x normal value
|
| 262 |
+
value = 500 + random.randint(0, 100)
|
| 263 |
+
else:
|
| 264 |
+
# Normal: around 50
|
| 265 |
+
value = 50 + random.randint(-10, 10)
|
| 266 |
+
|
| 267 |
+
items.append(
|
| 268 |
+
{
|
| 269 |
+
"timestamp": f"2025-01-07T{(i // 60):02d}:{(i % 60):02d}:00Z",
|
| 270 |
+
"cpu_percent": value,
|
| 271 |
+
"host": "prod-server-1",
|
| 272 |
+
}
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
result.total_needles = len(anomaly_indices)
|
| 276 |
+
|
| 277 |
+
# Compress
|
| 278 |
+
config = SmartCrusherConfig(
|
| 279 |
+
max_items_after_crush=20,
|
| 280 |
+
preserve_change_points=True,
|
| 281 |
+
)
|
| 282 |
+
original_json = json.dumps(items)
|
| 283 |
+
|
| 284 |
+
start = time.perf_counter()
|
| 285 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 286 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 287 |
+
|
| 288 |
+
# Count anomalies (cpu > 200) in compressed output
|
| 289 |
+
compressed = json.loads(compressed_json)
|
| 290 |
+
anomalies_found = [
|
| 291 |
+
item
|
| 292 |
+
for item in compressed
|
| 293 |
+
if isinstance(item.get("cpu_percent"), (int, float)) and item["cpu_percent"] > 200
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
result.needles_retained = len(anomalies_found)
|
| 297 |
+
result.retention_rate = result.needles_retained / result.total_needles
|
| 298 |
+
result.items_compressed = len(compressed)
|
| 299 |
+
|
| 300 |
+
# Pass if at least 80% of anomalies retained (some might be in change point windows)
|
| 301 |
+
result.passed = result.retention_rate >= 0.8
|
| 302 |
+
|
| 303 |
+
if not result.passed:
|
| 304 |
+
result.failures.append(
|
| 305 |
+
f"Lost too many anomalies: {result.needles_retained}/{result.total_needles} retained"
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
result.details = {
|
| 309 |
+
"original_items": 1000,
|
| 310 |
+
"compressed_items": len(compressed),
|
| 311 |
+
"anomaly_positions": anomaly_indices,
|
| 312 |
+
"anomalies_retained": result.needles_retained,
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
return result
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# =============================================================================
|
| 319 |
+
# TEST 4: Full Retrieval Accuracy
|
| 320 |
+
# =============================================================================
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def test_full_retrieval() -> RegressionResult:
|
| 324 |
+
"""
|
| 325 |
+
Test that full retrieval returns EXACTLY the original content.
|
| 326 |
+
"""
|
| 327 |
+
result = RegressionResult(
|
| 328 |
+
name="Full Retrieval Accuracy",
|
| 329 |
+
description="Verify full retrieval returns exact original content",
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
reset_compression_store()
|
| 333 |
+
store = get_compression_store()
|
| 334 |
+
|
| 335 |
+
# Generate test data
|
| 336 |
+
items = [{"id": i, "name": f"item_{i}", "value": i * 10} for i in range(100)]
|
| 337 |
+
|
| 338 |
+
original_json = json.dumps(items)
|
| 339 |
+
compressed_json = json.dumps(items[:10]) # Simulate compression
|
| 340 |
+
|
| 341 |
+
# Store
|
| 342 |
+
hash_key = store.store(
|
| 343 |
+
original=original_json,
|
| 344 |
+
compressed=compressed_json,
|
| 345 |
+
original_item_count=100,
|
| 346 |
+
compressed_item_count=10,
|
| 347 |
+
tool_name="test_tool",
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
start = time.perf_counter()
|
| 351 |
+
|
| 352 |
+
# Retrieve
|
| 353 |
+
entry = store.retrieve(hash_key)
|
| 354 |
+
|
| 355 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 356 |
+
|
| 357 |
+
# Verify content matches exactly
|
| 358 |
+
if entry is None:
|
| 359 |
+
result.passed = False
|
| 360 |
+
result.failures.append("Retrieval returned None")
|
| 361 |
+
else:
|
| 362 |
+
retrieved_items = json.loads(entry.original_content)
|
| 363 |
+
result.passed = retrieved_items == items
|
| 364 |
+
result.items_retrieved = len(retrieved_items)
|
| 365 |
+
result.retrieval_accuracy = 1.0 if result.passed else 0.0
|
| 366 |
+
|
| 367 |
+
if not result.passed:
|
| 368 |
+
result.failures.append("Retrieved content does not match original")
|
| 369 |
+
|
| 370 |
+
result.total_needles = 100
|
| 371 |
+
result.needles_retained = result.items_retrieved
|
| 372 |
+
result.retention_rate = 1.0 if result.passed else 0.0
|
| 373 |
+
|
| 374 |
+
result.details = {
|
| 375 |
+
"original_items": 100,
|
| 376 |
+
"retrieved_items": result.items_retrieved,
|
| 377 |
+
"hash_key": hash_key,
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
return result
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
# =============================================================================
|
| 384 |
+
# TEST 5: Feedback Learning
|
| 385 |
+
# =============================================================================
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def test_feedback_learning() -> RegressionResult:
|
| 389 |
+
"""
|
| 390 |
+
Test that the feedback system learns from retrieval patterns.
|
| 391 |
+
|
| 392 |
+
Scenario: Simulate high retrieval rate, verify system recommends
|
| 393 |
+
less aggressive compression.
|
| 394 |
+
"""
|
| 395 |
+
result = RegressionResult(
|
| 396 |
+
name="Feedback Learning",
|
| 397 |
+
description="Verify feedback loop adjusts compression based on patterns",
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
reset_compression_feedback()
|
| 401 |
+
feedback = get_compression_feedback()
|
| 402 |
+
|
| 403 |
+
tool_name = "high_retrieval_tool"
|
| 404 |
+
|
| 405 |
+
start = time.perf_counter()
|
| 406 |
+
|
| 407 |
+
# Simulate 10 compressions
|
| 408 |
+
for _ in range(10):
|
| 409 |
+
feedback.record_compression(tool_name, 1000, 20)
|
| 410 |
+
|
| 411 |
+
# Simulate 6 retrievals (60% rate - HIGH)
|
| 412 |
+
from headroom.cache.compression_store import RetrievalEvent
|
| 413 |
+
|
| 414 |
+
for i in range(6):
|
| 415 |
+
event = RetrievalEvent(
|
| 416 |
+
hash=f"hash{i:012d}",
|
| 417 |
+
query="find errors",
|
| 418 |
+
items_retrieved=100,
|
| 419 |
+
total_items=1000,
|
| 420 |
+
tool_name=tool_name,
|
| 421 |
+
timestamp=time.time(),
|
| 422 |
+
retrieval_type="search",
|
| 423 |
+
)
|
| 424 |
+
feedback.record_retrieval(event)
|
| 425 |
+
|
| 426 |
+
# Get hints
|
| 427 |
+
hints = feedback.get_compression_hints(tool_name)
|
| 428 |
+
|
| 429 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 430 |
+
|
| 431 |
+
# Verify hints recommend less aggressive compression
|
| 432 |
+
pattern = feedback.get_all_patterns().get(tool_name)
|
| 433 |
+
|
| 434 |
+
checks_passed = 0
|
| 435 |
+
total_checks = 3
|
| 436 |
+
|
| 437 |
+
# Check 1: Retrieval rate is tracked correctly
|
| 438 |
+
if pattern and abs(pattern.retrieval_rate - 0.6) < 0.01:
|
| 439 |
+
checks_passed += 1
|
| 440 |
+
else:
|
| 441 |
+
result.failures.append(
|
| 442 |
+
f"Retrieval rate incorrect: {pattern.retrieval_rate if pattern else 'N/A'}"
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
# Check 2: Hints suggest more items (>15 default)
|
| 446 |
+
if hints.max_items > 15:
|
| 447 |
+
checks_passed += 1
|
| 448 |
+
else:
|
| 449 |
+
result.failures.append(f"max_items not increased: {hints.max_items}")
|
| 450 |
+
|
| 451 |
+
# Check 3: Aggressiveness reduced (<0.7 default)
|
| 452 |
+
if hints.aggressiveness < 0.7:
|
| 453 |
+
checks_passed += 1
|
| 454 |
+
else:
|
| 455 |
+
result.failures.append(f"Aggressiveness not reduced: {hints.aggressiveness}")
|
| 456 |
+
|
| 457 |
+
result.passed = checks_passed == total_checks
|
| 458 |
+
result.retrieval_accuracy = checks_passed / total_checks
|
| 459 |
+
|
| 460 |
+
result.details = {
|
| 461 |
+
"compressions_recorded": 10,
|
| 462 |
+
"retrievals_recorded": 6,
|
| 463 |
+
"calculated_retrieval_rate": pattern.retrieval_rate if pattern else 0,
|
| 464 |
+
"recommended_max_items": hints.max_items,
|
| 465 |
+
"recommended_aggressiveness": hints.aggressiveness,
|
| 466 |
+
"reason": hints.reason,
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
return result
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
# =============================================================================
|
| 473 |
+
# TEST 6: Search Within Cached Content
|
| 474 |
+
# =============================================================================
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def test_search_accuracy() -> RegressionResult:
|
| 478 |
+
"""
|
| 479 |
+
Test that BM25 search within cached content finds relevant items.
|
| 480 |
+
"""
|
| 481 |
+
result = RegressionResult(
|
| 482 |
+
name="Search Accuracy", description="Verify BM25 search finds relevant items in cache"
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
reset_compression_store()
|
| 486 |
+
store = get_compression_store()
|
| 487 |
+
|
| 488 |
+
# Generate log entries with specific error messages
|
| 489 |
+
items = []
|
| 490 |
+
for i in range(100):
|
| 491 |
+
if i in [15, 45, 78]:
|
| 492 |
+
# Target: authentication errors
|
| 493 |
+
items.append(
|
| 494 |
+
{
|
| 495 |
+
"id": i,
|
| 496 |
+
"level": "ERROR",
|
| 497 |
+
"message": "Authentication failed: invalid token",
|
| 498 |
+
"service": "auth-service",
|
| 499 |
+
}
|
| 500 |
+
)
|
| 501 |
+
elif i in [20, 60]:
|
| 502 |
+
# Other errors (should not match auth search)
|
| 503 |
+
items.append(
|
| 504 |
+
{
|
| 505 |
+
"id": i,
|
| 506 |
+
"level": "ERROR",
|
| 507 |
+
"message": "Database connection timeout",
|
| 508 |
+
"service": "db-service",
|
| 509 |
+
}
|
| 510 |
+
)
|
| 511 |
+
else:
|
| 512 |
+
items.append(
|
| 513 |
+
{
|
| 514 |
+
"id": i,
|
| 515 |
+
"level": "INFO",
|
| 516 |
+
"message": "Request processed successfully",
|
| 517 |
+
"service": "api-service",
|
| 518 |
+
}
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
result.total_needles = 3 # 3 auth errors
|
| 522 |
+
|
| 523 |
+
original_json = json.dumps(items)
|
| 524 |
+
compressed_json = json.dumps(items[:10])
|
| 525 |
+
|
| 526 |
+
# Store
|
| 527 |
+
hash_key = store.store(
|
| 528 |
+
original=original_json,
|
| 529 |
+
compressed=compressed_json,
|
| 530 |
+
original_item_count=100,
|
| 531 |
+
compressed_item_count=10,
|
| 532 |
+
tool_name="log_search",
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
start = time.perf_counter()
|
| 536 |
+
|
| 537 |
+
# Search for authentication errors
|
| 538 |
+
search_results = store.search(hash_key, "authentication failed token")
|
| 539 |
+
|
| 540 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 541 |
+
|
| 542 |
+
# Count auth errors in results
|
| 543 |
+
auth_errors = [
|
| 544 |
+
item for item in search_results if "authentication" in item.get("message", "").lower()
|
| 545 |
+
]
|
| 546 |
+
|
| 547 |
+
result.needles_retained = len(auth_errors)
|
| 548 |
+
result.retention_rate = result.needles_retained / result.total_needles
|
| 549 |
+
result.items_retrieved = len(search_results)
|
| 550 |
+
|
| 551 |
+
# Pass if at least 2 of 3 auth errors found
|
| 552 |
+
result.passed = result.needles_retained >= 2
|
| 553 |
+
result.retrieval_accuracy = result.retention_rate
|
| 554 |
+
|
| 555 |
+
if not result.passed:
|
| 556 |
+
result.failures.append(
|
| 557 |
+
f"Search found only {result.needles_retained}/{result.total_needles} auth errors"
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
result.details = {
|
| 561 |
+
"query": "authentication failed token",
|
| 562 |
+
"total_results": len(search_results),
|
| 563 |
+
"auth_errors_found": result.needles_retained,
|
| 564 |
+
"hash_key": hash_key,
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
return result
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
# =============================================================================
|
| 571 |
+
# TEST 7: CCR End-to-End Flow
|
| 572 |
+
# =============================================================================
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def test_ccr_end_to_end() -> RegressionResult:
|
| 576 |
+
"""
|
| 577 |
+
Test the complete CCR flow: compress → cache → retrieve → feedback.
|
| 578 |
+
"""
|
| 579 |
+
result = RegressionResult(
|
| 580 |
+
name="CCR End-to-End Flow",
|
| 581 |
+
description="Verify complete compress-cache-retrieve cycle works",
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
reset_compression_store()
|
| 585 |
+
reset_compression_feedback()
|
| 586 |
+
|
| 587 |
+
store = get_compression_store()
|
| 588 |
+
feedback = get_compression_feedback()
|
| 589 |
+
|
| 590 |
+
# Generate data with known needles
|
| 591 |
+
items = []
|
| 592 |
+
for i in range(500):
|
| 593 |
+
if i == 123:
|
| 594 |
+
items.append(
|
| 595 |
+
{
|
| 596 |
+
"id": i,
|
| 597 |
+
"type": "critical_alert",
|
| 598 |
+
"message": "System overload detected",
|
| 599 |
+
"priority": "P0",
|
| 600 |
+
}
|
| 601 |
+
)
|
| 602 |
+
elif i in [50, 200, 400]:
|
| 603 |
+
items.append(
|
| 604 |
+
{
|
| 605 |
+
"id": i,
|
| 606 |
+
"type": "error",
|
| 607 |
+
"message": f"Error at position {i}",
|
| 608 |
+
"priority": "P1",
|
| 609 |
+
}
|
| 610 |
+
)
|
| 611 |
+
else:
|
| 612 |
+
items.append(
|
| 613 |
+
{
|
| 614 |
+
"id": i,
|
| 615 |
+
"type": "info",
|
| 616 |
+
"message": f"Normal operation {i}",
|
| 617 |
+
"priority": "P3",
|
| 618 |
+
}
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
result.total_needles = 4 # 1 critical + 3 errors
|
| 622 |
+
|
| 623 |
+
start = time.perf_counter()
|
| 624 |
+
|
| 625 |
+
# Step 1: Compress
|
| 626 |
+
config = SmartCrusherConfig(max_items_after_crush=20)
|
| 627 |
+
original_json = json.dumps(items)
|
| 628 |
+
compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config)
|
| 629 |
+
|
| 630 |
+
# Step 2: Cache
|
| 631 |
+
hash_key = store.store(
|
| 632 |
+
original=original_json,
|
| 633 |
+
compressed=compressed_json,
|
| 634 |
+
original_item_count=500,
|
| 635 |
+
compressed_item_count=20,
|
| 636 |
+
tool_name="alert_search",
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
# Step 3: Record compression in feedback
|
| 640 |
+
feedback.record_compression("alert_search", 500, 20)
|
| 641 |
+
|
| 642 |
+
# Step 4: Retrieve and search
|
| 643 |
+
critical_results = store.search(hash_key, "critical system overload P0")
|
| 644 |
+
error_results = store.search(hash_key, "Error position P1")
|
| 645 |
+
|
| 646 |
+
# Step 5: Process feedback
|
| 647 |
+
store.process_pending_feedback()
|
| 648 |
+
|
| 649 |
+
result.latency_ms = (time.perf_counter() - start) * 1000
|
| 650 |
+
|
| 651 |
+
# Verify results
|
| 652 |
+
checks_passed = 0
|
| 653 |
+
total_checks = 4
|
| 654 |
+
|
| 655 |
+
# Check 1: Critical alert found
|
| 656 |
+
critical_found = any(item.get("type") == "critical_alert" for item in critical_results)
|
| 657 |
+
if critical_found:
|
| 658 |
+
checks_passed += 1
|
| 659 |
+
else:
|
| 660 |
+
result.failures.append("Critical alert not found in search")
|
| 661 |
+
|
| 662 |
+
# Check 2: Errors found (search by message content)
|
| 663 |
+
errors_found = len(
|
| 664 |
+
[
|
| 665 |
+
item
|
| 666 |
+
for item in error_results
|
| 667 |
+
if item.get("type") == "error" or "Error" in str(item.get("message", ""))
|
| 668 |
+
]
|
| 669 |
+
)
|
| 670 |
+
if errors_found >= 2:
|
| 671 |
+
checks_passed += 1
|
| 672 |
+
else:
|
| 673 |
+
result.failures.append(f"Only {errors_found} errors found in search")
|
| 674 |
+
|
| 675 |
+
# Check 3: Store has entry
|
| 676 |
+
if store.exists(hash_key):
|
| 677 |
+
checks_passed += 1
|
| 678 |
+
else:
|
| 679 |
+
result.failures.append("Entry not found in store")
|
| 680 |
+
|
| 681 |
+
# Check 4: Feedback recorded
|
| 682 |
+
patterns = feedback.get_all_patterns()
|
| 683 |
+
if "alert_search" in patterns:
|
| 684 |
+
checks_passed += 1
|
| 685 |
+
else:
|
| 686 |
+
result.failures.append("Feedback not recorded for tool")
|
| 687 |
+
|
| 688 |
+
result.passed = checks_passed == total_checks
|
| 689 |
+
result.needles_retained = (1 if critical_found else 0) + errors_found
|
| 690 |
+
result.retention_rate = result.needles_retained / result.total_needles
|
| 691 |
+
result.items_retrieved = len(critical_results) + len(error_results)
|
| 692 |
+
result.retrieval_accuracy = checks_passed / total_checks
|
| 693 |
+
|
| 694 |
+
result.details = {
|
| 695 |
+
"hash_key": hash_key,
|
| 696 |
+
"critical_found": critical_found,
|
| 697 |
+
"errors_found": errors_found,
|
| 698 |
+
"store_entry_exists": store.exists(hash_key),
|
| 699 |
+
"feedback_recorded": "alert_search" in patterns,
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
return result
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
# =============================================================================
|
| 706 |
+
# REPORT GENERATION
|
| 707 |
+
# =============================================================================
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def generate_report(results: list[RegressionResult], verbose: bool = False) -> str:
|
| 711 |
+
"""Generate benchmark report."""
|
| 712 |
+
lines = []
|
| 713 |
+
|
| 714 |
+
lines.append("")
|
| 715 |
+
lines.append("=" * 70)
|
| 716 |
+
lines.append(" CCR REGRESSION BENCHMARK")
|
| 717 |
+
lines.append(" Verifying No Information Loss")
|
| 718 |
+
lines.append("=" * 70)
|
| 719 |
+
|
| 720 |
+
passed = sum(1 for r in results if r.passed)
|
| 721 |
+
total = len(results)
|
| 722 |
+
|
| 723 |
+
lines.append("")
|
| 724 |
+
lines.append(f" Overall: {passed}/{total} tests passed")
|
| 725 |
+
lines.append("")
|
| 726 |
+
|
| 727 |
+
for result in results:
|
| 728 |
+
status = "✓ PASS" if result.passed else "✗ FAIL"
|
| 729 |
+
lines.append(f"{'─' * 70}")
|
| 730 |
+
lines.append(f" {status} {result.name}")
|
| 731 |
+
lines.append(f" {result.description}")
|
| 732 |
+
|
| 733 |
+
if result.total_needles > 0:
|
| 734 |
+
lines.append(
|
| 735 |
+
f" Needles: {result.needles_retained}/{result.total_needles} retained ({result.retention_rate * 100:.0f}%)"
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
if result.items_retrieved > 0:
|
| 739 |
+
lines.append(f" Retrieved: {result.items_retrieved} items")
|
| 740 |
+
|
| 741 |
+
lines.append(f" Latency: {result.latency_ms:.2f}ms")
|
| 742 |
+
|
| 743 |
+
if not result.passed:
|
| 744 |
+
for failure in result.failures:
|
| 745 |
+
lines.append(f" ❌ {failure}")
|
| 746 |
+
|
| 747 |
+
if verbose and result.details:
|
| 748 |
+
lines.append(f" Details: {json.dumps(result.details, indent=2)}")
|
| 749 |
+
|
| 750 |
+
lines.append("")
|
| 751 |
+
lines.append("=" * 70)
|
| 752 |
+
|
| 753 |
+
if passed == total:
|
| 754 |
+
lines.append(" ✓ ALL TESTS PASSED - No regression detected")
|
| 755 |
+
else:
|
| 756 |
+
lines.append(f" ✗ {total - passed} TESTS FAILED - Review failures above")
|
| 757 |
+
|
| 758 |
+
lines.append("=" * 70)
|
| 759 |
+
lines.append("")
|
| 760 |
+
|
| 761 |
+
return "\n".join(lines)
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
# =============================================================================
|
| 765 |
+
# MAIN
|
| 766 |
+
# =============================================================================
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def main():
|
| 770 |
+
parser = argparse.ArgumentParser(description="CCR Regression Benchmark")
|
| 771 |
+
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
|
| 772 |
+
parser.add_argument(
|
| 773 |
+
"--scenario",
|
| 774 |
+
choices=[
|
| 775 |
+
"all",
|
| 776 |
+
"error-retention",
|
| 777 |
+
"uuid-retrieval",
|
| 778 |
+
"anomaly-retention",
|
| 779 |
+
"full-retrieval",
|
| 780 |
+
"feedback-learning",
|
| 781 |
+
"search-accuracy",
|
| 782 |
+
"e2e",
|
| 783 |
+
],
|
| 784 |
+
default="all",
|
| 785 |
+
)
|
| 786 |
+
args = parser.parse_args()
|
| 787 |
+
|
| 788 |
+
results = []
|
| 789 |
+
|
| 790 |
+
print("\nRunning CCR regression tests...\n")
|
| 791 |
+
|
| 792 |
+
if args.scenario in ("all", "error-retention"):
|
| 793 |
+
print(" [1/7] Error Retention...")
|
| 794 |
+
results.append(test_error_retention())
|
| 795 |
+
|
| 796 |
+
if args.scenario in ("all", "uuid-retrieval"):
|
| 797 |
+
print(" [2/7] UUID Retrieval...")
|
| 798 |
+
results.append(test_uuid_retrieval())
|
| 799 |
+
|
| 800 |
+
if args.scenario in ("all", "anomaly-retention"):
|
| 801 |
+
print(" [3/7] Anomaly Retention...")
|
| 802 |
+
results.append(test_anomaly_retention())
|
| 803 |
+
|
| 804 |
+
if args.scenario in ("all", "full-retrieval"):
|
| 805 |
+
print(" [4/7] Full Retrieval...")
|
| 806 |
+
results.append(test_full_retrieval())
|
| 807 |
+
|
| 808 |
+
if args.scenario in ("all", "feedback-learning"):
|
| 809 |
+
print(" [5/7] Feedback Learning...")
|
| 810 |
+
results.append(test_feedback_learning())
|
| 811 |
+
|
| 812 |
+
if args.scenario in ("all", "search-accuracy"):
|
| 813 |
+
print(" [6/7] Search Accuracy...")
|
| 814 |
+
results.append(test_search_accuracy())
|
| 815 |
+
|
| 816 |
+
if args.scenario in ("all", "e2e"):
|
| 817 |
+
print(" [7/7] End-to-End Flow...")
|
| 818 |
+
results.append(test_ccr_end_to_end())
|
| 819 |
+
|
| 820 |
+
print(generate_report(results, args.verbose))
|
| 821 |
+
|
| 822 |
+
# Exit with error code if any test failed
|
| 823 |
+
failed = sum(1 for r in results if not r.passed)
|
| 824 |
+
exit(failed)
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
if __name__ == "__main__":
|
| 828 |
+
main()
|
benchmarks/conftest.py
CHANGED
|
@@ -15,21 +15,19 @@ from __future__ import annotations
|
|
| 15 |
import json
|
| 16 |
import random
|
| 17 |
from typing import Any
|
| 18 |
-
from unittest.mock import Mock
|
| 19 |
|
| 20 |
import pytest
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from benchmarks.scenarios.tool_outputs import (
|
| 23 |
generate_api_responses,
|
| 24 |
generate_database_rows,
|
| 25 |
generate_log_entries,
|
| 26 |
generate_search_results,
|
| 27 |
)
|
| 28 |
-
from benchmarks.scenarios.conversations import (
|
| 29 |
-
generate_agentic_conversation,
|
| 30 |
-
generate_rag_conversation,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
|
| 34 |
# Set seed for reproducible benchmarks
|
| 35 |
random.seed(42)
|
|
@@ -159,21 +157,27 @@ def api_responses_100() -> list[dict[str, Any]]:
|
|
| 159 |
def conversation_10_turns() -> list[dict[str, Any]]:
|
| 160 |
"""Generate 10-turn agentic conversation with tool calls."""
|
| 161 |
random.seed(42)
|
| 162 |
-
return generate_agentic_conversation(
|
|
|
|
|
|
|
| 163 |
|
| 164 |
|
| 165 |
@pytest.fixture
|
| 166 |
def conversation_50_turns() -> list[dict[str, Any]]:
|
| 167 |
"""Generate 50-turn agentic conversation with tool calls."""
|
| 168 |
random.seed(42)
|
| 169 |
-
return generate_agentic_conversation(
|
|
|
|
|
|
|
| 170 |
|
| 171 |
|
| 172 |
@pytest.fixture
|
| 173 |
def conversation_200_turns() -> list[dict[str, Any]]:
|
| 174 |
"""Generate 200-turn agentic conversation (stress test)."""
|
| 175 |
random.seed(42)
|
| 176 |
-
return generate_agentic_conversation(
|
|
|
|
|
|
|
| 177 |
|
| 178 |
|
| 179 |
@pytest.fixture
|
|
|
|
| 15 |
import json
|
| 16 |
import random
|
| 17 |
from typing import Any
|
|
|
|
| 18 |
|
| 19 |
import pytest
|
| 20 |
|
| 21 |
+
from benchmarks.scenarios.conversations import (
|
| 22 |
+
generate_agentic_conversation,
|
| 23 |
+
generate_rag_conversation,
|
| 24 |
+
)
|
| 25 |
from benchmarks.scenarios.tool_outputs import (
|
| 26 |
generate_api_responses,
|
| 27 |
generate_database_rows,
|
| 28 |
generate_log_entries,
|
| 29 |
generate_search_results,
|
| 30 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# Set seed for reproducible benchmarks
|
| 33 |
random.seed(42)
|
|
|
|
| 157 |
def conversation_10_turns() -> list[dict[str, Any]]:
|
| 158 |
"""Generate 10-turn agentic conversation with tool calls."""
|
| 159 |
random.seed(42)
|
| 160 |
+
return generate_agentic_conversation(
|
| 161 |
+
turns=10, tool_calls_per_turn=1, items_per_tool_response=50
|
| 162 |
+
)
|
| 163 |
|
| 164 |
|
| 165 |
@pytest.fixture
|
| 166 |
def conversation_50_turns() -> list[dict[str, Any]]:
|
| 167 |
"""Generate 50-turn agentic conversation with tool calls."""
|
| 168 |
random.seed(42)
|
| 169 |
+
return generate_agentic_conversation(
|
| 170 |
+
turns=50, tool_calls_per_turn=2, items_per_tool_response=50
|
| 171 |
+
)
|
| 172 |
|
| 173 |
|
| 174 |
@pytest.fixture
|
| 175 |
def conversation_200_turns() -> list[dict[str, Any]]:
|
| 176 |
"""Generate 200-turn agentic conversation (stress test)."""
|
| 177 |
random.seed(42)
|
| 178 |
+
return generate_agentic_conversation(
|
| 179 |
+
turns=200, tool_calls_per_turn=1, items_per_tool_response=30
|
| 180 |
+
)
|
| 181 |
|
| 182 |
|
| 183 |
@pytest.fixture
|
benchmarks/dynamic_detector_benchmark.py
CHANGED
|
@@ -6,21 +6,21 @@ Tests the detector against realistic system prompts from AI coding agents,
|
|
| 6 |
chatbots, and enterprise applications.
|
| 7 |
"""
|
| 8 |
|
| 9 |
-
import time
|
| 10 |
import statistics
|
|
|
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from typing import Any
|
| 13 |
|
| 14 |
from headroom.cache.dynamic_detector import (
|
| 15 |
DetectorConfig,
|
| 16 |
DynamicContentDetector,
|
| 17 |
-
DynamicCategory,
|
| 18 |
)
|
| 19 |
|
| 20 |
|
| 21 |
@dataclass
|
| 22 |
class BenchmarkResult:
|
| 23 |
"""Result of a single benchmark run."""
|
|
|
|
| 24 |
name: str
|
| 25 |
content_length: int
|
| 26 |
spans_found: int
|
|
@@ -50,7 +50,6 @@ User: tchopra
|
|
| 50 |
Workspace: /Users/tchopra/claude-projects/headroom
|
| 51 |
|
| 52 |
Be concise, accurate, and helpful. Follow the user's instructions carefully.""",
|
| 53 |
-
|
| 54 |
"enterprise_assistant": """You are an enterprise AI assistant for Acme Corporation.
|
| 55 |
|
| 56 |
Current Date: 2026-01-07T10:30:00Z
|
|
@@ -76,7 +75,6 @@ Budget Information:
|
|
| 76 |
- Remaining: $2,658.33
|
| 77 |
|
| 78 |
Help the user with their work tasks while following company policies.""",
|
| 79 |
-
|
| 80 |
"coding_agent": """You are an autonomous coding agent with access to tools.
|
| 81 |
|
| 82 |
Environment:
|
|
@@ -99,7 +97,6 @@ API Keys Available:
|
|
| 99 |
- DATABASE_URL: postgresql://user:pass@localhost:5432/mydb
|
| 100 |
|
| 101 |
Execute tasks step by step, verify each action, and report progress.""",
|
| 102 |
-
|
| 103 |
"customer_support": """You are a customer support agent for TechStore Inc.
|
| 104 |
|
| 105 |
Current Time: January 7, 2026, 3:45 PM EST
|
|
@@ -122,7 +119,6 @@ Active Issues:
|
|
| 122 |
- Case #CS-2026-0107-001 - Battery drain issue - Open since today
|
| 123 |
|
| 124 |
Provide helpful, empathetic support while following company guidelines.""",
|
| 125 |
-
|
| 126 |
"data_analysis": """You are a data analysis assistant.
|
| 127 |
|
| 128 |
Report Generated: 2026-01-07 10:30:00 UTC
|
|
@@ -147,7 +143,6 @@ Anomalies Detected:
|
|
| 147 |
- Drop on Dec 25: 0.4x normal (expected - holiday)
|
| 148 |
|
| 149 |
Help analyze the data and provide insights.""",
|
| 150 |
-
|
| 151 |
"minimal_static": """You are a helpful AI assistant.
|
| 152 |
|
| 153 |
Your role is to:
|
|
@@ -157,7 +152,6 @@ Your role is to:
|
|
| 157 |
4. Admit when you don't know something
|
| 158 |
|
| 159 |
Always be helpful, harmless, and honest.""",
|
| 160 |
-
|
| 161 |
"heavy_dynamic": """Session started at 2026-01-07T10:30:45.123Z
|
| 162 |
Request ID: req_abc123def456ghi789jkl012mno345pqr678
|
| 163 |
Trace ID: 550e8400-e29b-41d4-a716-446655440000
|
|
@@ -205,19 +199,21 @@ def run_benchmark(
|
|
| 205 |
result = detector.detect(content)
|
| 206 |
elapsed = (time.perf_counter() - start) * 1000
|
| 207 |
|
| 208 |
-
categories = list(
|
| 209 |
-
|
| 210 |
-
results[name].append(
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
| 221 |
|
| 222 |
return results
|
| 223 |
|
|
@@ -228,9 +224,9 @@ def print_results(
|
|
| 228 |
):
|
| 229 |
"""Print benchmark results."""
|
| 230 |
|
| 231 |
-
print(f"\n{'='*80}")
|
| 232 |
print(f"BENCHMARK RESULTS: {tier_name}")
|
| 233 |
-
print(f"{'='*80}")
|
| 234 |
|
| 235 |
for name, runs in results.items():
|
| 236 |
latencies = [r.latency_ms for r in runs]
|
|
@@ -240,7 +236,11 @@ def print_results(
|
|
| 240 |
# Use first run for span info (consistent across runs)
|
| 241 |
first = runs[0]
|
| 242 |
|
| 243 |
-
compression = (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
print(f"\n📄 {name}")
|
| 246 |
print(f" Content: {first.content_length:,} chars")
|
|
@@ -257,9 +257,9 @@ def print_results(
|
|
| 257 |
def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]):
|
| 258 |
"""Print comparison across tiers."""
|
| 259 |
|
| 260 |
-
print(f"\n{'='*80}")
|
| 261 |
print("TIER COMPARISON")
|
| 262 |
-
print(f"{'='*80}")
|
| 263 |
|
| 264 |
prompts = list(REAL_WORLD_PROMPTS.keys())
|
| 265 |
tiers = list(all_results.keys())
|
|
@@ -284,9 +284,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]):
|
|
| 284 |
print(row)
|
| 285 |
|
| 286 |
# Summary
|
| 287 |
-
print(f"\n{'='*80}")
|
| 288 |
print("SUMMARY")
|
| 289 |
-
print(f"{'='*80}")
|
| 290 |
|
| 291 |
for tier in tiers:
|
| 292 |
all_latencies = []
|
|
@@ -297,7 +297,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]):
|
|
| 297 |
|
| 298 |
avg = statistics.mean(all_latencies)
|
| 299 |
p50 = statistics.median(all_latencies)
|
| 300 |
-
p99 =
|
|
|
|
|
|
|
| 301 |
|
| 302 |
print(f"\n{tier}:")
|
| 303 |
print(f" Total spans detected: {total_spans}")
|
|
@@ -309,9 +311,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]):
|
|
| 309 |
def show_detection_details(prompt_name: str, content: str):
|
| 310 |
"""Show detailed detection for a specific prompt."""
|
| 311 |
|
| 312 |
-
print(f"\n{'='*80}")
|
| 313 |
print(f"DETECTION DETAILS: {prompt_name}")
|
| 314 |
-
print(f"{'='*80}")
|
| 315 |
|
| 316 |
config = DetectorConfig(tiers=["regex"])
|
| 317 |
detector = DynamicContentDetector(config)
|
|
@@ -324,11 +326,17 @@ def show_detection_details(prompt_name: str, content: str):
|
|
| 324 |
print(f"\n\nDetected spans ({len(result.spans)}):")
|
| 325 |
print("-" * 40)
|
| 326 |
for span in result.spans:
|
| 327 |
-
print(
|
|
|
|
|
|
|
| 328 |
|
| 329 |
print(f"\n\nStatic content ({len(result.static_content)} chars):")
|
| 330 |
print("-" * 40)
|
| 331 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
print(f"\n\nDynamic content ({len(result.dynamic_content)} chars):")
|
| 334 |
print("-" * 40)
|
|
|
|
| 6 |
chatbots, and enterprise applications.
|
| 7 |
"""
|
| 8 |
|
|
|
|
| 9 |
import statistics
|
| 10 |
+
import time
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from typing import Any
|
| 13 |
|
| 14 |
from headroom.cache.dynamic_detector import (
|
| 15 |
DetectorConfig,
|
| 16 |
DynamicContentDetector,
|
|
|
|
| 17 |
)
|
| 18 |
|
| 19 |
|
| 20 |
@dataclass
|
| 21 |
class BenchmarkResult:
|
| 22 |
"""Result of a single benchmark run."""
|
| 23 |
+
|
| 24 |
name: str
|
| 25 |
content_length: int
|
| 26 |
spans_found: int
|
|
|
|
| 50 |
Workspace: /Users/tchopra/claude-projects/headroom
|
| 51 |
|
| 52 |
Be concise, accurate, and helpful. Follow the user's instructions carefully.""",
|
|
|
|
| 53 |
"enterprise_assistant": """You are an enterprise AI assistant for Acme Corporation.
|
| 54 |
|
| 55 |
Current Date: 2026-01-07T10:30:00Z
|
|
|
|
| 75 |
- Remaining: $2,658.33
|
| 76 |
|
| 77 |
Help the user with their work tasks while following company policies.""",
|
|
|
|
| 78 |
"coding_agent": """You are an autonomous coding agent with access to tools.
|
| 79 |
|
| 80 |
Environment:
|
|
|
|
| 97 |
- DATABASE_URL: postgresql://user:pass@localhost:5432/mydb
|
| 98 |
|
| 99 |
Execute tasks step by step, verify each action, and report progress.""",
|
|
|
|
| 100 |
"customer_support": """You are a customer support agent for TechStore Inc.
|
| 101 |
|
| 102 |
Current Time: January 7, 2026, 3:45 PM EST
|
|
|
|
| 119 |
- Case #CS-2026-0107-001 - Battery drain issue - Open since today
|
| 120 |
|
| 121 |
Provide helpful, empathetic support while following company guidelines.""",
|
|
|
|
| 122 |
"data_analysis": """You are a data analysis assistant.
|
| 123 |
|
| 124 |
Report Generated: 2026-01-07 10:30:00 UTC
|
|
|
|
| 143 |
- Drop on Dec 25: 0.4x normal (expected - holiday)
|
| 144 |
|
| 145 |
Help analyze the data and provide insights.""",
|
|
|
|
| 146 |
"minimal_static": """You are a helpful AI assistant.
|
| 147 |
|
| 148 |
Your role is to:
|
|
|
|
| 152 |
4. Admit when you don't know something
|
| 153 |
|
| 154 |
Always be helpful, harmless, and honest.""",
|
|
|
|
| 155 |
"heavy_dynamic": """Session started at 2026-01-07T10:30:45.123Z
|
| 156 |
Request ID: req_abc123def456ghi789jkl012mno345pqr678
|
| 157 |
Trace ID: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
| 199 |
result = detector.detect(content)
|
| 200 |
elapsed = (time.perf_counter() - start) * 1000
|
| 201 |
|
| 202 |
+
categories = list({s.category.value for s in result.spans})
|
| 203 |
+
|
| 204 |
+
results[name].append(
|
| 205 |
+
BenchmarkResult(
|
| 206 |
+
name=name,
|
| 207 |
+
content_length=len(content),
|
| 208 |
+
spans_found=len(result.spans),
|
| 209 |
+
categories=categories,
|
| 210 |
+
static_length=len(result.static_content),
|
| 211 |
+
dynamic_length=len(result.dynamic_content),
|
| 212 |
+
latency_ms=elapsed,
|
| 213 |
+
tiers_used=result.tiers_used,
|
| 214 |
+
warnings=result.warnings,
|
| 215 |
+
)
|
| 216 |
+
)
|
| 217 |
|
| 218 |
return results
|
| 219 |
|
|
|
|
| 224 |
):
|
| 225 |
"""Print benchmark results."""
|
| 226 |
|
| 227 |
+
print(f"\n{'=' * 80}")
|
| 228 |
print(f"BENCHMARK RESULTS: {tier_name}")
|
| 229 |
+
print(f"{'=' * 80}")
|
| 230 |
|
| 231 |
for name, runs in results.items():
|
| 232 |
latencies = [r.latency_ms for r in runs]
|
|
|
|
| 236 |
# Use first run for span info (consistent across runs)
|
| 237 |
first = runs[0]
|
| 238 |
|
| 239 |
+
compression = (
|
| 240 |
+
(1 - first.static_length / first.content_length) * 100
|
| 241 |
+
if first.content_length > 0
|
| 242 |
+
else 0
|
| 243 |
+
)
|
| 244 |
|
| 245 |
print(f"\n📄 {name}")
|
| 246 |
print(f" Content: {first.content_length:,} chars")
|
|
|
|
| 257 |
def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]):
|
| 258 |
"""Print comparison across tiers."""
|
| 259 |
|
| 260 |
+
print(f"\n{'=' * 80}")
|
| 261 |
print("TIER COMPARISON")
|
| 262 |
+
print(f"{'=' * 80}")
|
| 263 |
|
| 264 |
prompts = list(REAL_WORLD_PROMPTS.keys())
|
| 265 |
tiers = list(all_results.keys())
|
|
|
|
| 284 |
print(row)
|
| 285 |
|
| 286 |
# Summary
|
| 287 |
+
print(f"\n{'=' * 80}")
|
| 288 |
print("SUMMARY")
|
| 289 |
+
print(f"{'=' * 80}")
|
| 290 |
|
| 291 |
for tier in tiers:
|
| 292 |
all_latencies = []
|
|
|
|
| 297 |
|
| 298 |
avg = statistics.mean(all_latencies)
|
| 299 |
p50 = statistics.median(all_latencies)
|
| 300 |
+
p99 = (
|
| 301 |
+
sorted(all_latencies)[int(len(all_latencies) * 0.99)] if len(all_latencies) > 1 else avg
|
| 302 |
+
)
|
| 303 |
|
| 304 |
print(f"\n{tier}:")
|
| 305 |
print(f" Total spans detected: {total_spans}")
|
|
|
|
| 311 |
def show_detection_details(prompt_name: str, content: str):
|
| 312 |
"""Show detailed detection for a specific prompt."""
|
| 313 |
|
| 314 |
+
print(f"\n{'=' * 80}")
|
| 315 |
print(f"DETECTION DETAILS: {prompt_name}")
|
| 316 |
+
print(f"{'=' * 80}")
|
| 317 |
|
| 318 |
config = DetectorConfig(tiers=["regex"])
|
| 319 |
detector = DynamicContentDetector(config)
|
|
|
|
| 326 |
print(f"\n\nDetected spans ({len(result.spans)}):")
|
| 327 |
print("-" * 40)
|
| 328 |
for span in result.spans:
|
| 329 |
+
print(
|
| 330 |
+
f" [{span.category.value:12}] '{span.text[:50]}{'...' if len(span.text) > 50 else ''}'"
|
| 331 |
+
)
|
| 332 |
|
| 333 |
print(f"\n\nStatic content ({len(result.static_content)} chars):")
|
| 334 |
print("-" * 40)
|
| 335 |
+
print(
|
| 336 |
+
result.static_content[:500] + "..."
|
| 337 |
+
if len(result.static_content) > 500
|
| 338 |
+
else result.static_content
|
| 339 |
+
)
|
| 340 |
|
| 341 |
print(f"\n\nDynamic content ({len(result.dynamic_content)} chars):")
|
| 342 |
print("-" * 40)
|
benchmarks/run_benchmarks.py
CHANGED
|
@@ -40,7 +40,6 @@ from datetime import datetime
|
|
| 40 |
from pathlib import Path
|
| 41 |
from typing import Any
|
| 42 |
|
| 43 |
-
|
| 44 |
# Benchmark suite definitions
|
| 45 |
BENCHMARK_SUITES = {
|
| 46 |
"all": [
|
|
@@ -75,19 +74,19 @@ BENCHMARK_SUITES = {
|
|
| 75 |
|
| 76 |
# Performance targets (mean time in microseconds)
|
| 77 |
PERFORMANCE_TARGETS = {
|
| 78 |
-
"test_compress_100_items": 2000,
|
| 79 |
-
"test_compress_1000_items": 10000,
|
| 80 |
-
"test_compress_10000_items": 100000,
|
| 81 |
-
"test_date_extraction": 1000,
|
| 82 |
-
"test_hash_computation": 500,
|
| 83 |
-
"test_window_50_turns": 5000,
|
| 84 |
-
"test_window_200_turns": 20000,
|
| 85 |
-
"test_single_item": 100,
|
| 86 |
-
"test_batch_100": 1000,
|
| 87 |
-
"test_batch_1000": 10000,
|
| 88 |
-
"test_pipeline_simple": 5000,
|
| 89 |
-
"test_pipeline_agentic": 30000,
|
| 90 |
-
"test_pipeline_rag": 50000,
|
| 91 |
}
|
| 92 |
|
| 93 |
|
|
@@ -243,8 +242,8 @@ def generate_markdown_report(
|
|
| 243 |
if total > 0:
|
| 244 |
lines.append("## Summary")
|
| 245 |
lines.append("")
|
| 246 |
-
lines.append(f"- **Passed**: {passed}/{total} ({100*passed/total:.0f}%)")
|
| 247 |
-
lines.append(f"- **Failed**: {failed}/{total} ({100*failed/total:.0f}%)")
|
| 248 |
lines.append("")
|
| 249 |
|
| 250 |
# Performance notes
|
|
@@ -274,9 +273,9 @@ def _format_time(microseconds: float) -> str:
|
|
| 274 |
if microseconds < 1000:
|
| 275 |
return f"{microseconds:.1f}us"
|
| 276 |
elif microseconds < 1_000_000:
|
| 277 |
-
return f"{microseconds/1000:.2f}ms"
|
| 278 |
else:
|
| 279 |
-
return f"{microseconds/1_000_000:.2f}s"
|
| 280 |
|
| 281 |
|
| 282 |
def main() -> int:
|
|
|
|
| 40 |
from pathlib import Path
|
| 41 |
from typing import Any
|
| 42 |
|
|
|
|
| 43 |
# Benchmark suite definitions
|
| 44 |
BENCHMARK_SUITES = {
|
| 45 |
"all": [
|
|
|
|
| 74 |
|
| 75 |
# Performance targets (mean time in microseconds)
|
| 76 |
PERFORMANCE_TARGETS = {
|
| 77 |
+
"test_compress_100_items": 2000, # 2ms
|
| 78 |
+
"test_compress_1000_items": 10000, # 10ms
|
| 79 |
+
"test_compress_10000_items": 100000, # 100ms
|
| 80 |
+
"test_date_extraction": 1000, # 1ms
|
| 81 |
+
"test_hash_computation": 500, # 0.5ms
|
| 82 |
+
"test_window_50_turns": 5000, # 5ms
|
| 83 |
+
"test_window_200_turns": 20000, # 20ms
|
| 84 |
+
"test_single_item": 100, # 0.1ms
|
| 85 |
+
"test_batch_100": 1000, # 1ms
|
| 86 |
+
"test_batch_1000": 10000, # 10ms
|
| 87 |
+
"test_pipeline_simple": 5000, # 5ms
|
| 88 |
+
"test_pipeline_agentic": 30000, # 30ms
|
| 89 |
+
"test_pipeline_rag": 50000, # 50ms
|
| 90 |
}
|
| 91 |
|
| 92 |
|
|
|
|
| 242 |
if total > 0:
|
| 243 |
lines.append("## Summary")
|
| 244 |
lines.append("")
|
| 245 |
+
lines.append(f"- **Passed**: {passed}/{total} ({100 * passed / total:.0f}%)")
|
| 246 |
+
lines.append(f"- **Failed**: {failed}/{total} ({100 * failed / total:.0f}%)")
|
| 247 |
lines.append("")
|
| 248 |
|
| 249 |
# Performance notes
|
|
|
|
| 273 |
if microseconds < 1000:
|
| 274 |
return f"{microseconds:.1f}us"
|
| 275 |
elif microseconds < 1_000_000:
|
| 276 |
+
return f"{microseconds / 1000:.2f}ms"
|
| 277 |
else:
|
| 278 |
+
return f"{microseconds / 1_000_000:.2f}s"
|
| 279 |
|
| 280 |
|
| 281 |
def main() -> int:
|
benchmarks/scenarios/__init__.py
CHANGED
|
@@ -8,16 +8,16 @@ Modules:
|
|
| 8 |
conversations: Generators for conversation history (agentic, RAG)
|
| 9 |
"""
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from .tool_outputs import (
|
| 12 |
generate_api_responses,
|
| 13 |
generate_database_rows,
|
| 14 |
generate_log_entries,
|
| 15 |
generate_search_results,
|
| 16 |
)
|
| 17 |
-
from .conversations import (
|
| 18 |
-
generate_agentic_conversation,
|
| 19 |
-
generate_rag_conversation,
|
| 20 |
-
)
|
| 21 |
|
| 22 |
__all__ = [
|
| 23 |
"generate_search_results",
|
|
|
|
| 8 |
conversations: Generators for conversation history (agentic, RAG)
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
from .conversations import (
|
| 12 |
+
generate_agentic_conversation,
|
| 13 |
+
generate_rag_conversation,
|
| 14 |
+
)
|
| 15 |
from .tool_outputs import (
|
| 16 |
generate_api_responses,
|
| 17 |
generate_database_rows,
|
| 18 |
generate_log_entries,
|
| 19 |
generate_search_results,
|
| 20 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
__all__ = [
|
| 23 |
"generate_search_results",
|
benchmarks/scenarios/conversations.py
CHANGED
|
@@ -53,19 +53,23 @@ def generate_agentic_conversation(
|
|
| 53 |
messages = []
|
| 54 |
|
| 55 |
# System prompt
|
| 56 |
-
messages.append(
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
|
| 61 |
# Generate turns
|
| 62 |
for turn_idx in range(turns):
|
| 63 |
# User message
|
| 64 |
user_query = _generate_user_query(turn_idx)
|
| 65 |
-
messages.append(
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
# Assistant with tool calls
|
| 71 |
num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1))
|
|
@@ -75,20 +79,24 @@ def generate_agentic_conversation(
|
|
| 75 |
tool_name, arguments = _generate_tool_call(turn_idx, call_idx)
|
| 76 |
call_id = f"call_{uuid.uuid4().hex[:16]}"
|
| 77 |
|
| 78 |
-
tool_calls.append(
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
"
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
messages.append(
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
| 92 |
|
| 93 |
# Tool responses
|
| 94 |
for tool_call in tool_calls:
|
|
@@ -96,18 +104,22 @@ def generate_agentic_conversation(
|
|
| 96 |
tool_call["function"]["name"],
|
| 97 |
items_per_tool_response,
|
| 98 |
)
|
| 99 |
-
messages.append(
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# Assistant summary (most turns, not all)
|
| 106 |
if random.random() < 0.8:
|
| 107 |
-
messages.append(
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
|
| 112 |
return messages
|
| 113 |
|
|
@@ -137,39 +149,49 @@ def generate_rag_conversation(
|
|
| 137 |
messages = []
|
| 138 |
|
| 139 |
# System prompt with date (for CacheAligner testing)
|
| 140 |
-
messages.append(
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
| 144 |
|
| 145 |
# Generate context documents
|
| 146 |
context_content = _generate_rag_context(context_tokens)
|
| 147 |
|
| 148 |
# Inject context as first user message
|
| 149 |
-
messages.append(
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
| 153 |
|
| 154 |
# Assistant acknowledgment
|
| 155 |
-
messages.append(
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
| 159 |
|
| 160 |
# Generate Q&A turns
|
| 161 |
for i in range(num_queries):
|
| 162 |
question = _generate_rag_question(i)
|
| 163 |
-
messages.append(
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
| 167 |
|
| 168 |
answer = _generate_rag_answer(i)
|
| 169 |
-
messages.append(
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
| 173 |
|
| 174 |
return messages
|
| 175 |
|
|
@@ -195,17 +217,21 @@ def generate_anthropic_agentic_conversation(
|
|
| 195 |
messages = []
|
| 196 |
|
| 197 |
# System message (Anthropic uses separate system parameter, but we include it)
|
| 198 |
-
messages.append(
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
|
| 203 |
for turn_idx in range(turns):
|
| 204 |
# User message
|
| 205 |
-
messages.append(
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
|
| 210 |
# Assistant with tool_use blocks
|
| 211 |
num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1))
|
|
@@ -215,17 +241,21 @@ def generate_anthropic_agentic_conversation(
|
|
| 215 |
tool_name, arguments = _generate_tool_call(turn_idx, call_idx)
|
| 216 |
tool_use_id = f"toolu_{uuid.uuid4().hex[:16]}"
|
| 217 |
|
| 218 |
-
content_blocks.append(
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
messages.append(
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
| 229 |
|
| 230 |
# Tool results in user message
|
| 231 |
tool_results = []
|
|
@@ -234,29 +264,38 @@ def generate_anthropic_agentic_conversation(
|
|
| 234 |
block["name"],
|
| 235 |
items_per_tool_response,
|
| 236 |
)
|
| 237 |
-
tool_results.append(
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
messages.append(
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
| 247 |
|
| 248 |
# Assistant response
|
| 249 |
if random.random() < 0.8:
|
| 250 |
-
messages.append(
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
return messages
|
| 256 |
|
| 257 |
|
| 258 |
# Helper functions
|
| 259 |
|
|
|
|
| 260 |
def _generate_system_prompt() -> str:
|
| 261 |
"""Generate a realistic system prompt."""
|
| 262 |
return """You are an AI assistant with access to various tools for searching, querying, and analyzing data.
|
|
|
|
| 53 |
messages = []
|
| 54 |
|
| 55 |
# System prompt
|
| 56 |
+
messages.append(
|
| 57 |
+
{
|
| 58 |
+
"role": "system",
|
| 59 |
+
"content": _generate_system_prompt(),
|
| 60 |
+
}
|
| 61 |
+
)
|
| 62 |
|
| 63 |
# Generate turns
|
| 64 |
for turn_idx in range(turns):
|
| 65 |
# User message
|
| 66 |
user_query = _generate_user_query(turn_idx)
|
| 67 |
+
messages.append(
|
| 68 |
+
{
|
| 69 |
+
"role": "user",
|
| 70 |
+
"content": user_query,
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
|
| 74 |
# Assistant with tool calls
|
| 75 |
num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1))
|
|
|
|
| 79 |
tool_name, arguments = _generate_tool_call(turn_idx, call_idx)
|
| 80 |
call_id = f"call_{uuid.uuid4().hex[:16]}"
|
| 81 |
|
| 82 |
+
tool_calls.append(
|
| 83 |
+
{
|
| 84 |
+
"id": call_id,
|
| 85 |
+
"type": "function",
|
| 86 |
+
"function": {
|
| 87 |
+
"name": tool_name,
|
| 88 |
+
"arguments": json.dumps(arguments),
|
| 89 |
+
},
|
| 90 |
+
}
|
| 91 |
+
)
|
| 92 |
|
| 93 |
+
messages.append(
|
| 94 |
+
{
|
| 95 |
+
"role": "assistant",
|
| 96 |
+
"content": None,
|
| 97 |
+
"tool_calls": tool_calls,
|
| 98 |
+
}
|
| 99 |
+
)
|
| 100 |
|
| 101 |
# Tool responses
|
| 102 |
for tool_call in tool_calls:
|
|
|
|
| 104 |
tool_call["function"]["name"],
|
| 105 |
items_per_tool_response,
|
| 106 |
)
|
| 107 |
+
messages.append(
|
| 108 |
+
{
|
| 109 |
+
"role": "tool",
|
| 110 |
+
"tool_call_id": tool_call["id"],
|
| 111 |
+
"content": json.dumps(tool_response),
|
| 112 |
+
}
|
| 113 |
+
)
|
| 114 |
|
| 115 |
# Assistant summary (most turns, not all)
|
| 116 |
if random.random() < 0.8:
|
| 117 |
+
messages.append(
|
| 118 |
+
{
|
| 119 |
+
"role": "assistant",
|
| 120 |
+
"content": _generate_assistant_summary(turn_idx, tool_calls),
|
| 121 |
+
}
|
| 122 |
+
)
|
| 123 |
|
| 124 |
return messages
|
| 125 |
|
|
|
|
| 149 |
messages = []
|
| 150 |
|
| 151 |
# System prompt with date (for CacheAligner testing)
|
| 152 |
+
messages.append(
|
| 153 |
+
{
|
| 154 |
+
"role": "system",
|
| 155 |
+
"content": _generate_rag_system_prompt(),
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
|
| 159 |
# Generate context documents
|
| 160 |
context_content = _generate_rag_context(context_tokens)
|
| 161 |
|
| 162 |
# Inject context as first user message
|
| 163 |
+
messages.append(
|
| 164 |
+
{
|
| 165 |
+
"role": "user",
|
| 166 |
+
"content": f"Here are the relevant documents for context:\n\n{context_content}\n\nPlease analyze these documents.",
|
| 167 |
+
}
|
| 168 |
+
)
|
| 169 |
|
| 170 |
# Assistant acknowledgment
|
| 171 |
+
messages.append(
|
| 172 |
+
{
|
| 173 |
+
"role": "assistant",
|
| 174 |
+
"content": "I've reviewed the provided documents. I can see information about technical documentation, API specifications, and configuration guides. What would you like to know?",
|
| 175 |
+
}
|
| 176 |
+
)
|
| 177 |
|
| 178 |
# Generate Q&A turns
|
| 179 |
for i in range(num_queries):
|
| 180 |
question = _generate_rag_question(i)
|
| 181 |
+
messages.append(
|
| 182 |
+
{
|
| 183 |
+
"role": "user",
|
| 184 |
+
"content": question,
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
|
| 188 |
answer = _generate_rag_answer(i)
|
| 189 |
+
messages.append(
|
| 190 |
+
{
|
| 191 |
+
"role": "assistant",
|
| 192 |
+
"content": answer,
|
| 193 |
+
}
|
| 194 |
+
)
|
| 195 |
|
| 196 |
return messages
|
| 197 |
|
|
|
|
| 217 |
messages = []
|
| 218 |
|
| 219 |
# System message (Anthropic uses separate system parameter, but we include it)
|
| 220 |
+
messages.append(
|
| 221 |
+
{
|
| 222 |
+
"role": "system",
|
| 223 |
+
"content": _generate_system_prompt(),
|
| 224 |
+
}
|
| 225 |
+
)
|
| 226 |
|
| 227 |
for turn_idx in range(turns):
|
| 228 |
# User message
|
| 229 |
+
messages.append(
|
| 230 |
+
{
|
| 231 |
+
"role": "user",
|
| 232 |
+
"content": [{"type": "text", "text": _generate_user_query(turn_idx)}],
|
| 233 |
+
}
|
| 234 |
+
)
|
| 235 |
|
| 236 |
# Assistant with tool_use blocks
|
| 237 |
num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1))
|
|
|
|
| 241 |
tool_name, arguments = _generate_tool_call(turn_idx, call_idx)
|
| 242 |
tool_use_id = f"toolu_{uuid.uuid4().hex[:16]}"
|
| 243 |
|
| 244 |
+
content_blocks.append(
|
| 245 |
+
{
|
| 246 |
+
"type": "tool_use",
|
| 247 |
+
"id": tool_use_id,
|
| 248 |
+
"name": tool_name,
|
| 249 |
+
"input": arguments,
|
| 250 |
+
}
|
| 251 |
+
)
|
| 252 |
|
| 253 |
+
messages.append(
|
| 254 |
+
{
|
| 255 |
+
"role": "assistant",
|
| 256 |
+
"content": content_blocks,
|
| 257 |
+
}
|
| 258 |
+
)
|
| 259 |
|
| 260 |
# Tool results in user message
|
| 261 |
tool_results = []
|
|
|
|
| 264 |
block["name"],
|
| 265 |
items_per_tool_response,
|
| 266 |
)
|
| 267 |
+
tool_results.append(
|
| 268 |
+
{
|
| 269 |
+
"type": "tool_result",
|
| 270 |
+
"tool_use_id": block["id"],
|
| 271 |
+
"content": json.dumps(tool_response),
|
| 272 |
+
}
|
| 273 |
+
)
|
| 274 |
|
| 275 |
+
messages.append(
|
| 276 |
+
{
|
| 277 |
+
"role": "user",
|
| 278 |
+
"content": tool_results,
|
| 279 |
+
}
|
| 280 |
+
)
|
| 281 |
|
| 282 |
# Assistant response
|
| 283 |
if random.random() < 0.8:
|
| 284 |
+
messages.append(
|
| 285 |
+
{
|
| 286 |
+
"role": "assistant",
|
| 287 |
+
"content": [
|
| 288 |
+
{"type": "text", "text": _generate_assistant_summary(turn_idx, [])}
|
| 289 |
+
],
|
| 290 |
+
}
|
| 291 |
+
)
|
| 292 |
|
| 293 |
return messages
|
| 294 |
|
| 295 |
|
| 296 |
# Helper functions
|
| 297 |
|
| 298 |
+
|
| 299 |
def _generate_system_prompt() -> str:
|
| 300 |
"""Generate a realistic system prompt."""
|
| 301 |
return """You are an AI assistant with access to various tools for searching, querying, and analyzing data.
|
benchmarks/scenarios/tool_outputs.py
CHANGED
|
@@ -14,9 +14,7 @@ and compression strategies.
|
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
-
import json
|
| 18 |
import random
|
| 19 |
-
import string
|
| 20 |
import uuid
|
| 21 |
from datetime import datetime, timedelta
|
| 22 |
from typing import Any
|
|
@@ -80,12 +78,14 @@ def generate_search_results(
|
|
| 80 |
min(include_errors, len(results) - len(needle_indices)),
|
| 81 |
)
|
| 82 |
for idx in error_indices:
|
| 83 |
-
results[idx]["error"] = random.choice(
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
results[idx]["status"] = "failed"
|
| 90 |
|
| 91 |
return results
|
|
@@ -177,7 +177,9 @@ def generate_log_entries(
|
|
| 177 |
# Add exception info for errors
|
| 178 |
if level in ("ERROR", "CRITICAL"):
|
| 179 |
entry["exception"] = {
|
| 180 |
-
"type": random.choice(
|
|
|
|
|
|
|
| 181 |
"message": message,
|
| 182 |
"stacktrace": _generate_stacktrace(),
|
| 183 |
}
|
|
@@ -273,11 +275,13 @@ def generate_database_rows(
|
|
| 273 |
elif table_type == "transactions":
|
| 274 |
row = _generate_transaction_row(i)
|
| 275 |
else: # mixed
|
| 276 |
-
generator = random.choice(
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
| 281 |
row = generator(i)
|
| 282 |
|
| 283 |
rows.append(row)
|
|
@@ -296,6 +300,7 @@ def generate_database_rows(
|
|
| 296 |
|
| 297 |
# Helper functions
|
| 298 |
|
|
|
|
| 299 |
def _generate_title() -> str:
|
| 300 |
"""Generate a realistic document title."""
|
| 301 |
prefixes = ["How to", "Guide to", "Understanding", "Introduction to", "Advanced"]
|
|
@@ -326,7 +331,9 @@ def _generate_name() -> str:
|
|
| 326 |
def _generate_timestamp(offset_days: int = 0) -> str:
|
| 327 |
"""Generate an ISO timestamp."""
|
| 328 |
base = datetime(2025, 1, 1, 12, 0, 0)
|
| 329 |
-
dt = base + timedelta(
|
|
|
|
|
|
|
| 330 |
return dt.isoformat() + "Z"
|
| 331 |
|
| 332 |
|
|
|
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
|
|
|
| 17 |
import random
|
|
|
|
| 18 |
import uuid
|
| 19 |
from datetime import datetime, timedelta
|
| 20 |
from typing import Any
|
|
|
|
| 78 |
min(include_errors, len(results) - len(needle_indices)),
|
| 79 |
)
|
| 80 |
for idx in error_indices:
|
| 81 |
+
results[idx]["error"] = random.choice(
|
| 82 |
+
[
|
| 83 |
+
"Index out of range",
|
| 84 |
+
"Document not found",
|
| 85 |
+
"Permission denied",
|
| 86 |
+
"Timeout exceeded",
|
| 87 |
+
]
|
| 88 |
+
)
|
| 89 |
results[idx]["status"] = "failed"
|
| 90 |
|
| 91 |
return results
|
|
|
|
| 177 |
# Add exception info for errors
|
| 178 |
if level in ("ERROR", "CRITICAL"):
|
| 179 |
entry["exception"] = {
|
| 180 |
+
"type": random.choice(
|
| 181 |
+
["TimeoutError", "ConnectionError", "ValueError", "RuntimeError"]
|
| 182 |
+
),
|
| 183 |
"message": message,
|
| 184 |
"stacktrace": _generate_stacktrace(),
|
| 185 |
}
|
|
|
|
| 275 |
elif table_type == "transactions":
|
| 276 |
row = _generate_transaction_row(i)
|
| 277 |
else: # mixed
|
| 278 |
+
generator = random.choice(
|
| 279 |
+
[
|
| 280 |
+
_generate_user_row,
|
| 281 |
+
lambda i: _generate_metric_row(i, mean_value, std_value),
|
| 282 |
+
_generate_transaction_row,
|
| 283 |
+
]
|
| 284 |
+
)
|
| 285 |
row = generator(i)
|
| 286 |
|
| 287 |
rows.append(row)
|
|
|
|
| 300 |
|
| 301 |
# Helper functions
|
| 302 |
|
| 303 |
+
|
| 304 |
def _generate_title() -> str:
|
| 305 |
"""Generate a realistic document title."""
|
| 306 |
prefixes = ["How to", "Guide to", "Understanding", "Introduction to", "Advanced"]
|
|
|
|
| 331 |
def _generate_timestamp(offset_days: int = 0) -> str:
|
| 332 |
"""Generate an ISO timestamp."""
|
| 333 |
base = datetime(2025, 1, 1, 12, 0, 0)
|
| 334 |
+
dt = base + timedelta(
|
| 335 |
+
days=offset_days, hours=random.randint(0, 23), minutes=random.randint(0, 59)
|
| 336 |
+
)
|
| 337 |
return dt.isoformat() + "Z"
|
| 338 |
|
| 339 |
|
docs/HEADROOM_DEEP_ANALYSIS.md
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Headroom: A Critical Technical Analysis
|
| 2 |
+
|
| 3 |
+
## Table of Contents
|
| 4 |
+
1. [Part I: Critical Startup Evaluation](#part-i-critical-startup-evaluation)
|
| 5 |
+
2. [Part II: Technical Pitch](#part-ii-technical-pitch)
|
| 6 |
+
3. [Part III: Technical Blog Post - State of the Art Comparison](#part-iii-technical-blog-post)
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Part I: Critical Startup Evaluation
|
| 11 |
+
|
| 12 |
+
## Executive Summary
|
| 13 |
+
|
| 14 |
+
**Headroom** is a context optimization layer for LLM applications that compresses tool outputs using statistical analysis rather than LLM-based summarization. The core value proposition: **50-90% token savings without accuracy loss**.
|
| 15 |
+
|
| 16 |
+
### The Honest Assessment
|
| 17 |
+
|
| 18 |
+
| Dimension | Score | Assessment |
|
| 19 |
+
|-----------|-------|------------|
|
| 20 |
+
| Technical Differentiation | 7/10 | Novel CCR architecture, but heuristics have limits |
|
| 21 |
+
| Market Timing | 9/10 | AI agent explosion = massive demand for context optimization |
|
| 22 |
+
| Defensibility | 6/10 | Network effects possible via feedback loop, but easy to replicate basics |
|
| 23 |
+
| Scalability Risk | 7/10 | Works for ~70% of scenarios; fails silently on 30% |
|
| 24 |
+
| Business Model Clarity | 8/10 | Clear proxy/SDK model, usage-based pricing |
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## The Problem Space: Is It Real?
|
| 29 |
+
|
| 30 |
+
### Quantified Pain
|
| 31 |
+
|
| 32 |
+
| Metric | Reality |
|
| 33 |
+
|--------|---------|
|
| 34 |
+
| Average tool output size | 5,000-50,000 tokens |
|
| 35 |
+
| Context utilization | 60-80% is tool outputs |
|
| 36 |
+
| Cache hit rate (without optimization) | <10% |
|
| 37 |
+
| Monthly spend for AI coding agents | $500-$5,000/developer |
|
| 38 |
+
|
| 39 |
+
**Evidence from research:**
|
| 40 |
+
- [Factory.ai](https://factory.ai/news/evaluating-compression): "OpenAI achieved 99.3% compression but scored 0.35 points lower on quality. Those discarded details required re-fetching, negating token savings."
|
| 41 |
+
- [Phil Schmid](https://www.philschmid.de/context-engineering-part-2): "Mechanically stuffing lengthy text into an LLM's context window is a 'brute-force' strategy that inevitably scatters the model's attention."
|
| 42 |
+
|
| 43 |
+
**Verdict: The problem is REAL and GROWING.**
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## Technical Differentiation: What's Actually Novel?
|
| 48 |
+
|
| 49 |
+
### What Headroom Does
|
| 50 |
+
|
| 51 |
+
1. **Statistical Compression** (SmartCrusher)
|
| 52 |
+
- Analyzes field distributions (entropy, variance, uniqueness)
|
| 53 |
+
- Detects data patterns (time series, logs, search results)
|
| 54 |
+
- Preserves errors, anomalies, and high-relevance items
|
| 55 |
+
- **No LLM calls** = deterministic, fast, cheap
|
| 56 |
+
|
| 57 |
+
2. **Reversible Compression** (CCR - Compress-Cache-Retrieve)
|
| 58 |
+
- Original content cached for on-demand retrieval
|
| 59 |
+
- LLM can request more data if needed
|
| 60 |
+
- Feedback loop learns from retrieval patterns
|
| 61 |
+
- **Unique position**: Only Headroom sits between tools and LLMs
|
| 62 |
+
|
| 63 |
+
3. **Cache Alignment**
|
| 64 |
+
- Stabilizes dynamic content (dates, IDs) for provider cache hits
|
| 65 |
+
- Can increase cache utilization from <10% to >50%
|
| 66 |
+
|
| 67 |
+
### What's Actually Novel vs. Prior Art
|
| 68 |
+
|
| 69 |
+
| Approach | Novelty | Prior Art |
|
| 70 |
+
|----------|---------|-----------|
|
| 71 |
+
| Statistical field analysis | **Medium** | Data profiling tools exist, but not for LLM context |
|
| 72 |
+
| CCR architecture | **High** | ACON mentions "reversible" but doesn't implement caching |
|
| 73 |
+
| Feedback-driven hints | **High** | ACON-inspired, but applied at proxy layer |
|
| 74 |
+
| BM25/embedding relevance | **Low** | Standard IR techniques |
|
| 75 |
+
| Cache prefix alignment | **Low** | Multiple implementations exist |
|
| 76 |
+
|
| 77 |
+
**Honest assessment**: The individual techniques are not revolutionary. The **combination and positioning** (proxy layer for AI agents) is the innovation.
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## The Fundamental Limitation
|
| 82 |
+
|
| 83 |
+
### The Accuracy Problem
|
| 84 |
+
|
| 85 |
+
Headroom uses **task-agnostic heuristics**:
|
| 86 |
+
- Keep first 3, last 2 items
|
| 87 |
+
- Keep errors (keyword matching)
|
| 88 |
+
- Keep anomalies (> 2σ from mean)
|
| 89 |
+
- Keep relevant items (BM25/embedding to user query)
|
| 90 |
+
|
| 91 |
+
**When this works:**
|
| 92 |
+
- Data has explicit importance signals (score fields, error flags)
|
| 93 |
+
- Interesting items are statistical outliers
|
| 94 |
+
- User query matches data vocabulary
|
| 95 |
+
|
| 96 |
+
**When this fails:**
|
| 97 |
+
```
|
| 98 |
+
User asks: "Find all orders from California"
|
| 99 |
+
Tool returns: 1,000 orders
|
| 100 |
+
SmartCrusher keeps: errors, anomalies, first/last items
|
| 101 |
+
The needle: Order #47 from California (looks completely normal)
|
| 102 |
+
Result: INFORMATION LOSS
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Quantified Risk
|
| 106 |
+
|
| 107 |
+
| Scenario | Coverage | Confidence |
|
| 108 |
+
|----------|----------|------------|
|
| 109 |
+
| Search results with scores | 95%+ | HIGH |
|
| 110 |
+
| Logs with errors | 90%+ | HIGH |
|
| 111 |
+
| Time series with anomalies | 85%+ | HIGH |
|
| 112 |
+
| **Entity listings (users, orders)** | **60%** | **LOW** |
|
| 113 |
+
| **Specific lookups** | **50%** | **LOW** |
|
| 114 |
+
| **Exhaustive queries** | **40%** | **LOW** |
|
| 115 |
+
|
| 116 |
+
**The 70/30 split**: Headroom works well for ~70% of real-world tool outputs. The other 30% require either:
|
| 117 |
+
1. Skipping compression (crushability detection helps here)
|
| 118 |
+
2. Accepting potential information loss
|
| 119 |
+
3. Relying on CCR retrieval as fallback
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## Competitive Landscape
|
| 124 |
+
|
| 125 |
+
### Direct Competitors
|
| 126 |
+
|
| 127 |
+
| Competitor | Approach | Pros | Cons |
|
| 128 |
+
|------------|----------|------|------|
|
| 129 |
+
| **LLMLingua** (Microsoft) | Token-level compression via classifier | 95-98% accuracy retention | Requires model, wrong granularity for JSON |
|
| 130 |
+
| **ACON** (Research) | Task-aware, failure-driven | Best accuracy | Requires agent integration |
|
| 131 |
+
| **Selective Context** (Amazon) | Self-attention based filtering | Model-aware | Slow, requires LLM |
|
| 132 |
+
| **Context Caching** (Anthropic/OpenAI) | Provider-level caching | Native integration | No compression |
|
| 133 |
+
|
| 134 |
+
### Why Headroom Can Win
|
| 135 |
+
|
| 136 |
+
1. **Position**: Proxy layer = works with any client
|
| 137 |
+
2. **Speed**: No LLM calls = <10ms overhead
|
| 138 |
+
3. **Safety**: CCR = reversible compression
|
| 139 |
+
4. **Learning**: Feedback loop improves over time
|
| 140 |
+
|
| 141 |
+
### Why Headroom Might Lose
|
| 142 |
+
|
| 143 |
+
1. **Provider integration**: If Anthropic/OpenAI add smart compression natively
|
| 144 |
+
2. **Agent framework capture**: LangChain/LlamaIndex could add similar features
|
| 145 |
+
3. **Research advances**: If ACON-style task-aware compression becomes easy
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Business Model Analysis
|
| 150 |
+
|
| 151 |
+
### Revenue Model
|
| 152 |
+
|
| 153 |
+
```
|
| 154 |
+
Free Tier:
|
| 155 |
+
- Local proxy (unlimited)
|
| 156 |
+
- Basic compression
|
| 157 |
+
- No cloud features
|
| 158 |
+
|
| 159 |
+
Pro Tier ($49/month):
|
| 160 |
+
- Hosted proxy
|
| 161 |
+
- Feedback-driven optimization
|
| 162 |
+
- Analytics dashboard
|
| 163 |
+
|
| 164 |
+
Enterprise:
|
| 165 |
+
- Custom deployment
|
| 166 |
+
- SLA guarantees
|
| 167 |
+
- Integration support
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
### Unit Economics
|
| 171 |
+
|
| 172 |
+
| Metric | Value |
|
| 173 |
+
|--------|-------|
|
| 174 |
+
| Average token savings | 70% |
|
| 175 |
+
| Average monthly spend per developer | $1,000 |
|
| 176 |
+
| Potential savings | $700/month |
|
| 177 |
+
| Headroom Pro price | $49/month |
|
| 178 |
+
| **Value capture** | **7%** |
|
| 179 |
+
|
| 180 |
+
**Problem**: 7% value capture is low. Competitors could undercut easily.
|
| 181 |
+
|
| 182 |
+
### Moat-Building Strategies
|
| 183 |
+
|
| 184 |
+
1. **Network effect via feedback**: Cross-user learning improves compression
|
| 185 |
+
2. **Tool-specific profiles**: Accumulated knowledge of tool output patterns
|
| 186 |
+
3. **Integration depth**: Deep embedding in agent frameworks
|
| 187 |
+
4. **Enterprise stickiness**: Once deployed in production, hard to replace
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## Risk Assessment
|
| 192 |
+
|
| 193 |
+
### Technical Risks
|
| 194 |
+
|
| 195 |
+
| Risk | Probability | Impact | Mitigation |
|
| 196 |
+
|------|-------------|--------|------------|
|
| 197 |
+
| Compression causes critical info loss | Medium | High | CCR + crushability detection |
|
| 198 |
+
| Provider adds native compression | Medium | High | Position as multi-provider layer |
|
| 199 |
+
| LLMLingua improves for JSON | Low | Medium | Focus on proxy positioning |
|
| 200 |
+
|
| 201 |
+
### Market Risks
|
| 202 |
+
|
| 203 |
+
| Risk | Probability | Impact | Mitigation |
|
| 204 |
+
|------|-------------|--------|------------|
|
| 205 |
+
| Context windows grow so large compression isn't needed | Low | High | Focus on cost (always relevant) |
|
| 206 |
+
| Agent frameworks internalize compression | Medium | High | Integrate with frameworks |
|
| 207 |
+
| Open source competitor emerges | High | Medium | Build network effects fast |
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
## Strategic Recommendations
|
| 212 |
+
|
| 213 |
+
### Short-Term (0-6 months)
|
| 214 |
+
1. **Ship CCR**: Reversible compression is the key differentiator
|
| 215 |
+
2. **Prove accuracy**: Publish benchmarks showing 0% information loss
|
| 216 |
+
3. **Integrate with frameworks**: LangChain, LlamaIndex, CrewAI
|
| 217 |
+
|
| 218 |
+
### Medium-Term (6-18 months)
|
| 219 |
+
1. **Build network effects**: Cross-user feedback learning
|
| 220 |
+
2. **Tool-specific profiles**: Curated compression strategies per tool
|
| 221 |
+
3. **Enterprise pilots**: Get deployed in production AI agents
|
| 222 |
+
|
| 223 |
+
### Long-Term (18+ months)
|
| 224 |
+
1. **Platform play**: Become the "context layer" for AI applications
|
| 225 |
+
2. **Data flywheel**: Best compression because most data
|
| 226 |
+
3. **Research integration**: Adopt ACON-style task-aware learning
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## Verdict
|
| 231 |
+
|
| 232 |
+
**Headroom is a viable startup idea with clear technical merit but significant execution risk.**
|
| 233 |
+
|
| 234 |
+
| Criterion | Score | Notes |
|
| 235 |
+
|-----------|-------|-------|
|
| 236 |
+
| Problem validity | 9/10 | Token costs are real and growing |
|
| 237 |
+
| Solution fit | 7/10 | Works for 70% of cases; CCR addresses rest |
|
| 238 |
+
| Technical moat | 6/10 | Easy to replicate basics; network effects need scale |
|
| 239 |
+
| Market timing | 9/10 | AI agent explosion is happening now |
|
| 240 |
+
| Execution risk | 7/10 | Moderate; need to prove accuracy first |
|
| 241 |
+
|
| 242 |
+
**Overall**: **7.5/10** - Worth pursuing with clear-eyed awareness of limitations.
|
| 243 |
+
|
| 244 |
+
---
|
| 245 |
+
|
| 246 |
+
# Part II: Technical Pitch
|
| 247 |
+
|
| 248 |
+
## The 30-Second Pitch
|
| 249 |
+
|
| 250 |
+
> "Headroom cuts LLM costs by 50-90% for AI agents. We compress tool outputs using statistical analysis, not LLM summarization - so it's fast, cheap, and deterministic. Our Compress-Cache-Retrieve architecture makes compression reversible: if the LLM needs more, it retrieves instantly. Zero accuracy loss, zero extra API calls."
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## The Problem (For Technical Audience)
|
| 255 |
+
|
| 256 |
+
### The Context Budget Crisis
|
| 257 |
+
|
| 258 |
+
Modern AI agents are powerful but expensive:
|
| 259 |
+
|
| 260 |
+
```python
|
| 261 |
+
# Typical agent workflow
|
| 262 |
+
agent.execute("Find and fix the bug in authentication")
|
| 263 |
+
|
| 264 |
+
# Behind the scenes:
|
| 265 |
+
# 1. Read 20 files (50K tokens)
|
| 266 |
+
# 2. Search codebase (10K tokens)
|
| 267 |
+
# 3. Run tests (30K tokens)
|
| 268 |
+
# 4. Check logs (40K tokens)
|
| 269 |
+
# Total: 130K tokens = $0.65 per request (GPT-4o)
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
**The math doesn't work**:
|
| 273 |
+
- 100 requests/day × $0.65 = $65/day = **$1,950/month** per developer
|
| 274 |
+
- 80% of those tokens are tool outputs
|
| 275 |
+
- 70% of tool output is redundant
|
| 276 |
+
|
| 277 |
+
### Why Current Solutions Fail
|
| 278 |
+
|
| 279 |
+
| Approach | Problem |
|
| 280 |
+
|----------|---------|
|
| 281 |
+
| **Truncation** | Loses end of data (where errors often are) |
|
| 282 |
+
| **LLM Summarization** | Slow (2-5s), expensive, can hallucinate |
|
| 283 |
+
| **Provider caching** | Doesn't reduce input size |
|
| 284 |
+
| **Longer context windows** | Doesn't reduce cost |
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
## The Solution: Statistical Context Compression
|
| 289 |
+
|
| 290 |
+
### Architecture
|
| 291 |
+
|
| 292 |
+
```
|
| 293 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 294 |
+
│ YOUR APPLICATION │
|
| 295 |
+
│ (Claude Code, LangChain Agent, Custom Agent) │
|
| 296 |
+
└─────────────────────────────────────────────────────────────┘
|
| 297 |
+
│
|
| 298 |
+
▼
|
| 299 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 300 |
+
│ HEADROOM PROXY │
|
| 301 |
+
│ │
|
| 302 |
+
│ ┌──────────────────────────────────────────────────────┐ │
|
| 303 |
+
│ │ SMART CRUSHER │ │
|
| 304 |
+
│ │ │ │
|
| 305 |
+
│ │ 1. ANALYZE: Field distributions, patterns, signals │ │
|
| 306 |
+
│ │ 2. PRESERVE: Errors, anomalies, relevant items │ │
|
| 307 |
+
│ │ 3. COMPRESS: Statistical sampling, deduplication │ │
|
| 308 |
+
│ │ 4. CACHE: Store original for retrieval (CCR) │ │
|
| 309 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 310 |
+
│ │
|
| 311 |
+
│ ┌──────────────────────────────────────────────────────┐ │
|
| 312 |
+
│ │ CACHE ALIGNER │ │
|
| 313 |
+
│ │ Stabilize dynamic content for provider caching │ │
|
| 314 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 315 |
+
│ │
|
| 316 |
+
│ ┌──────────────────────────────────────────────────────┐ │
|
| 317 |
+
│ │ FEEDBACK LOOP │ │
|
| 318 |
+
│ │ Learn from retrieval patterns → improve compression │ │
|
| 319 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 320 |
+
└─────────────────────────────────────────────────────────────┘
|
| 321 |
+
│
|
| 322 |
+
▼
|
| 323 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 324 |
+
│ OPENAI / ANTHROPIC / GOOGLE API │
|
| 325 |
+
└─────────────────────────────────────────────────────────────┘
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
### Key Innovation: CCR (Compress-Cache-Retrieve)
|
| 329 |
+
|
| 330 |
+
**The insight**: Traditional compression is irreversible. If we guess wrong, information is permanently lost.
|
| 331 |
+
|
| 332 |
+
**CCR makes compression reversible**:
|
| 333 |
+
|
| 334 |
+
```
|
| 335 |
+
BEFORE CCR:
|
| 336 |
+
Tool returns 1,000 items → Compress to 20 → Send to LLM
|
| 337 |
+
If LLM needs item #47: TOO BAD, IT'S GONE
|
| 338 |
+
|
| 339 |
+
AFTER CCR:
|
| 340 |
+
Tool returns 1,000 items → Compress to 20 + cache 1,000
|
| 341 |
+
If LLM needs item #47: Retrieve from cache INSTANTLY
|
| 342 |
+
|
| 343 |
+
Bonus: Track what LLM retrieves → improve future compression
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
### Technical Deep Dive: SmartCrusher
|
| 347 |
+
|
| 348 |
+
**Step 1: Field Analysis**
|
| 349 |
+
```python
|
| 350 |
+
# For each field in the JSON array:
|
| 351 |
+
analyze(field) → {
|
| 352 |
+
type: "numeric" | "string" | "boolean" | "array",
|
| 353 |
+
unique_ratio: 0.0-1.0, # How many unique values
|
| 354 |
+
entropy: 0.0-1.0, # Randomness (high = IDs)
|
| 355 |
+
variance: float, # For numerics
|
| 356 |
+
change_points: [int], # Where values spike
|
| 357 |
+
}
|
| 358 |
+
```
|
| 359 |
+
|
| 360 |
+
**Step 2: Pattern Detection**
|
| 361 |
+
```python
|
| 362 |
+
# Classify the data structure:
|
| 363 |
+
if has_timestamp_field and has_numeric_variance:
|
| 364 |
+
pattern = "time_series"
|
| 365 |
+
elif has_message_field and has_level_field:
|
| 366 |
+
pattern = "logs"
|
| 367 |
+
elif has_score_field:
|
| 368 |
+
pattern = "search_results"
|
| 369 |
+
else:
|
| 370 |
+
pattern = "generic"
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
**Step 3: Strategy Selection**
|
| 374 |
+
```python
|
| 375 |
+
strategies = {
|
| 376 |
+
"time_series": keep_change_points + sample_stable_regions,
|
| 377 |
+
"logs": cluster_by_message + keep_one_per_cluster,
|
| 378 |
+
"search_results": sort_by_score + keep_top_n,
|
| 379 |
+
"generic": keep_first_k + keep_last_k + keep_anomalies
|
| 380 |
+
}
|
| 381 |
+
```
|
| 382 |
+
|
| 383 |
+
**Step 4: Compression with Safety**
|
| 384 |
+
```python
|
| 385 |
+
# Always preserve:
|
| 386 |
+
- Items with error keywords (error, exception, failed, critical)
|
| 387 |
+
- Items > 2σ from mean (anomalies)
|
| 388 |
+
- Items matching user query (BM25 + embeddings)
|
| 389 |
+
- First K and last K items (context + recency)
|
| 390 |
+
|
| 391 |
+
# Crushability detection:
|
| 392 |
+
if high_uniqueness and no_importance_signal:
|
| 393 |
+
return SKIP # Don't compress, too risky
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
---
|
| 397 |
+
|
| 398 |
+
## Benchmarks
|
| 399 |
+
|
| 400 |
+
### Real-World Performance
|
| 401 |
+
|
| 402 |
+
| Scenario | Before | After | Savings | Quality |
|
| 403 |
+
|----------|--------|-------|---------|---------|
|
| 404 |
+
| Search results (1,000 items) | 45K tokens | 4.5K tokens | 90% | 100% |
|
| 405 |
+
| Log analysis (500 entries) | 22K tokens | 3.3K tokens | 85% | 100% |
|
| 406 |
+
| API responses (nested JSON) | 15K tokens | 2.3K tokens | 85% | 100% |
|
| 407 |
+
| SRE incident investigation | 22K tokens | 2.2K tokens | 90% | 100% |
|
| 408 |
+
|
| 409 |
+
### Adversarial Testing
|
| 410 |
+
|
| 411 |
+
We ran 36 adversarial tests designed to break assumptions:
|
| 412 |
+
|
| 413 |
+
| Category | Tests | Passed |
|
| 414 |
+
|----------|-------|--------|
|
| 415 |
+
| Semantic Attacks | 6 | 6/6 |
|
| 416 |
+
| Boundary Conditions | 6 | 6/6 |
|
| 417 |
+
| Injection Attacks | 3 | 3/3 |
|
| 418 |
+
| Race Conditions | 4 | 4/4 |
|
| 419 |
+
| Deceptive Data | 2 | 2/2 |
|
| 420 |
+
| Extreme Stress Tests | 15 | 15/15 |
|
| 421 |
+
|
| 422 |
+
**Tests included**:
|
| 423 |
+
- NaN/Infinity score fields
|
| 424 |
+
- 100-level deep nesting
|
| 425 |
+
- 100,000 item arrays
|
| 426 |
+
- Catastrophic regex patterns
|
| 427 |
+
- Unicode normalization attacks
|
| 428 |
+
- Concurrent feedback race conditions
|
| 429 |
+
|
| 430 |
+
---
|
| 431 |
+
|
| 432 |
+
## Comparison to State of the Art
|
| 433 |
+
|
| 434 |
+
### vs. LLMLingua (Microsoft Research)
|
| 435 |
+
|
| 436 |
+
| Dimension | LLMLingua | Headroom |
|
| 437 |
+
|-----------|-----------|----------|
|
| 438 |
+
| Compression unit | Tokens | JSON items |
|
| 439 |
+
| Requires model | Yes (XLM-RoBERTa) | No |
|
| 440 |
+
| Latency | 50-200ms | <10ms |
|
| 441 |
+
| Task-aware | No | Partial (via feedback) |
|
| 442 |
+
| Reversible | No | Yes (CCR) |
|
| 443 |
+
| Best for | Natural language | Structured tool outputs |
|
| 444 |
+
|
| 445 |
+
**LLMLingua paper**: "Achieves 3-6x compression with 95-98% accuracy retention."
|
| 446 |
+
**Headroom**: Achieves 5-10x compression on JSON with 100% accuracy (no loss, just sampling).
|
| 447 |
+
|
| 448 |
+
### vs. ACON (Agent Context Optimization)
|
| 449 |
+
|
| 450 |
+
| Dimension | ACON | Headroom |
|
| 451 |
+
|-----------|------|----------|
|
| 452 |
+
| Compression method | Task-aware, failure-driven | Statistical + feedback |
|
| 453 |
+
| Integration point | Agent framework | Proxy layer |
|
| 454 |
+
| Learning | Contrastive feedback | Retrieval patterns |
|
| 455 |
+
| Deployment | Research prototype | Production-ready |
|
| 456 |
+
| Reversibility | Mentioned but not implemented | Full CCR |
|
| 457 |
+
|
| 458 |
+
**ACON insight we adopted**: Learn compression guidelines by analyzing failures.
|
| 459 |
+
**What we added**: Reversible compression (CCR) so "failure" is recoverable.
|
| 460 |
+
|
| 461 |
+
### vs. Provider Caching (Anthropic, OpenAI)
|
| 462 |
+
|
| 463 |
+
| Dimension | Provider Caching | Headroom |
|
| 464 |
+
|-----------|------------------|----------|
|
| 465 |
+
| What it does | Cache exact prefix matches | Compress + stabilize prefix |
|
| 466 |
+
| Token reduction | 0% | 50-90% |
|
| 467 |
+
| Cache hit improvement | ~10% baseline | Can improve to 50%+ |
|
| 468 |
+
| Cost | Free | Overhead of proxy |
|
| 469 |
+
|
| 470 |
+
**Complementary, not competitive**: Headroom improves cache hit rates by stabilizing prefixes.
|
| 471 |
+
|
| 472 |
+
---
|
| 473 |
+
|
| 474 |
+
## Integration
|
| 475 |
+
|
| 476 |
+
### Option 1: Proxy (Drop-in)
|
| 477 |
+
|
| 478 |
+
```bash
|
| 479 |
+
pip install headroom
|
| 480 |
+
headroom proxy --port 8787
|
| 481 |
+
|
| 482 |
+
# Use with any client
|
| 483 |
+
ANTHROPIC_BASE_URL=http://localhost:8787 claude
|
| 484 |
+
OPENAI_BASE_URL=http://localhost:8787/v1 your-app
|
| 485 |
+
```
|
| 486 |
+
|
| 487 |
+
### Option 2: Python SDK
|
| 488 |
+
|
| 489 |
+
```python
|
| 490 |
+
from headroom import HeadroomClient
|
| 491 |
+
from openai import OpenAI
|
| 492 |
+
|
| 493 |
+
client = HeadroomClient(
|
| 494 |
+
original_client=OpenAI(),
|
| 495 |
+
default_mode="optimize",
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
# Use exactly like original - compression happens automatically
|
| 499 |
+
response = client.chat.completions.create(
|
| 500 |
+
model="gpt-4o",
|
| 501 |
+
messages=[...],
|
| 502 |
+
)
|
| 503 |
+
```
|
| 504 |
+
|
| 505 |
+
### Option 3: LangChain
|
| 506 |
+
|
| 507 |
+
```python
|
| 508 |
+
from langchain_openai import ChatOpenAI
|
| 509 |
+
from headroom.integrations import HeadroomOptimizer
|
| 510 |
+
|
| 511 |
+
llm = ChatOpenAI(model="gpt-4o", callbacks=[HeadroomOptimizer()])
|
| 512 |
+
```
|
| 513 |
+
|
| 514 |
+
---
|
| 515 |
+
|
| 516 |
+
## Pricing
|
| 517 |
+
|
| 518 |
+
| Tier | Price | Features |
|
| 519 |
+
|------|-------|----------|
|
| 520 |
+
| Open Source | Free | Local proxy, basic compression |
|
| 521 |
+
| Pro | $49/month | Hosted proxy, feedback learning, analytics |
|
| 522 |
+
| Enterprise | Custom | On-prem, SLA, dedicated support |
|
| 523 |
+
|
| 524 |
+
**ROI Calculator**:
|
| 525 |
+
- If you spend $1,000/month on LLM API
|
| 526 |
+
- Headroom saves 70% = $700/month
|
| 527 |
+
- Pro costs $49/month
|
| 528 |
+
- **Net savings: $651/month (14x ROI)**
|
| 529 |
+
|
| 530 |
+
---
|
| 531 |
+
|
| 532 |
+
# Part III: Technical Blog Post
|
| 533 |
+
|
| 534 |
+
# Reversible Compression for AI Agents: How CCR Solves What LLMLingua Can't
|
| 535 |
+
|
| 536 |
+
*A deep technical comparison of context compression approaches*
|
| 537 |
+
|
| 538 |
+
---
|
| 539 |
+
|
| 540 |
+
## The Compression Dilemma
|
| 541 |
+
|
| 542 |
+
Every AI agent builder faces the same problem: tool outputs are huge, context windows are expensive, and throwing data away risks breaking your agent.
|
| 543 |
+
|
| 544 |
+
The research community has proposed several solutions:
|
| 545 |
+
- **LLMLingua** (Microsoft): Token-level compression using a classifier
|
| 546 |
+
- **Selective Context** (Amazon): Attention-based filtering
|
| 547 |
+
- **ACON** (UC Berkeley): Task-aware, failure-driven optimization
|
| 548 |
+
|
| 549 |
+
But there's a fundamental problem none of them solve: **compression is irreversible**.
|
| 550 |
+
|
| 551 |
+
If you compress 1,000 search results to 20 and the LLM needs result #47, it's gone. You've created a silent failure mode that's hard to detect and impossible to recover from.
|
| 552 |
+
|
| 553 |
+
**This post introduces CCR (Compress-Cache-Retrieve)**, an architecture that makes compression reversible. We'll compare it to state-of-the-art approaches and show why reversibility changes everything.
|
| 554 |
+
|
| 555 |
+
---
|
| 556 |
+
|
| 557 |
+
## Part 1: The State of the Art
|
| 558 |
+
|
| 559 |
+
### LLMLingua: Token-Level Compression
|
| 560 |
+
|
| 561 |
+
[LLMLingua](https://arxiv.org/abs/2310.05736) and its successor [LLMLingua-2](https://arxiv.org/abs/2403.12968) achieve impressive compression ratios (3-6x) while retaining 95-98% of information.
|
| 562 |
+
|
| 563 |
+
**How it works**:
|
| 564 |
+
1. Train a classifier (XLM-RoBERTa or similar) to predict token importance
|
| 565 |
+
2. At inference, score each token
|
| 566 |
+
3. Drop low-importance tokens
|
| 567 |
+
|
| 568 |
+
**Example**:
|
| 569 |
+
```
|
| 570 |
+
Input: "The quick brown fox jumps over the lazy dog"
|
| 571 |
+
Output: "quick brown fox jumps lazy dog" (30% compression)
|
| 572 |
+
```
|
| 573 |
+
|
| 574 |
+
**Strengths**:
|
| 575 |
+
- Works on any text
|
| 576 |
+
- High accuracy retention
|
| 577 |
+
- No task-specific training
|
| 578 |
+
|
| 579 |
+
**Weaknesses for AI agents**:
|
| 580 |
+
1. **Wrong granularity**: Agents work with JSON arrays, not prose
|
| 581 |
+
2. **Requires a model**: Adds latency (50-200ms) and dependency
|
| 582 |
+
3. **Irreversible**: If the classifier is wrong, data is lost
|
| 583 |
+
4. **Not structure-aware**: Can't reason about "first 3 items" or "items with errors"
|
| 584 |
+
|
| 585 |
+
### ACON: Task-Aware, Failure-Driven Optimization
|
| 586 |
+
|
| 587 |
+
[ACON](https://arxiv.org/abs/2510.00615) takes a different approach: learn what to compress by analyzing task failures.
|
| 588 |
+
|
| 589 |
+
**How it works**:
|
| 590 |
+
1. Compress aggressively
|
| 591 |
+
2. If task fails, analyze what was lost
|
| 592 |
+
3. Update compression guidelines
|
| 593 |
+
4. Repeat (contrastive learning)
|
| 594 |
+
|
| 595 |
+
**Key insight from the paper**:
|
| 596 |
+
> "Rather than crude strategies like 'keep recent K interactions' (FIFO), ACON employs task-aware, failure-driven optimization. The system learns environment-specific and task-specific compression patterns."
|
| 597 |
+
|
| 598 |
+
**Strengths**:
|
| 599 |
+
- Task-aware decisions
|
| 600 |
+
- 95%+ accuracy retention
|
| 601 |
+
- Learns from failures
|
| 602 |
+
|
| 603 |
+
**Weaknesses**:
|
| 604 |
+
1. **Requires agent integration**: Must observe task outcomes
|
| 605 |
+
2. **Cold start problem**: Need failures to learn
|
| 606 |
+
3. **Still irreversible**: Failure = data was lost
|
| 607 |
+
4. **Research prototype**: Not production-ready
|
| 608 |
+
|
| 609 |
+
### Selective Context: Attention-Based Filtering
|
| 610 |
+
|
| 611 |
+
[Selective Context](https://arxiv.org/abs/2310.06201) uses the LLM's own attention to decide what's important.
|
| 612 |
+
|
| 613 |
+
**How it works**:
|
| 614 |
+
1. Run a forward pass with a smaller model
|
| 615 |
+
2. Observe attention patterns
|
| 616 |
+
3. Keep tokens that receive high attention
|
| 617 |
+
|
| 618 |
+
**Strengths**:
|
| 619 |
+
- Model-native importance signal
|
| 620 |
+
- Works without training
|
| 621 |
+
|
| 622 |
+
**Weaknesses**:
|
| 623 |
+
1. **Requires forward pass**: Slow and expensive
|
| 624 |
+
2. **Task-agnostic**: Doesn't know what the user will ask
|
| 625 |
+
3. **Irreversible**: Same fundamental problem
|
| 626 |
+
|
| 627 |
+
---
|
| 628 |
+
|
| 629 |
+
## Part 2: The Reversibility Problem
|
| 630 |
+
|
| 631 |
+
### Why Irreversible Compression Fails
|
| 632 |
+
|
| 633 |
+
Consider this scenario:
|
| 634 |
+
|
| 635 |
+
```python
|
| 636 |
+
# User query
|
| 637 |
+
"Find all orders from California and calculate total revenue"
|
| 638 |
+
|
| 639 |
+
# Tool output: 1,000 orders (50KB)
|
| 640 |
+
[
|
| 641 |
+
{"id": 1, "state": "NY", "amount": 100},
|
| 642 |
+
{"id": 2, "state": "TX", "amount": 200},
|
| 643 |
+
...
|
| 644 |
+
{"id": 47, "state": "CA", "amount": 500}, # ← NEEDLE
|
| 645 |
+
...
|
| 646 |
+
{"id": 1000, "state": "FL", "amount": 150}
|
| 647 |
+
]
|
| 648 |
+
|
| 649 |
+
# LLMLingua compression: Keep "important" tokens
|
| 650 |
+
# Result: Loses order #47 because it looks like every other order
|
| 651 |
+
|
| 652 |
+
# ACON compression: Keep based on learned patterns
|
| 653 |
+
# Result: Might keep errors, might keep high amounts, but no signal for "CA"
|
| 654 |
+
|
| 655 |
+
# Selective Context: Keep high-attention tokens
|
| 656 |
+
# Result: User hasn't asked yet, so no attention signal for "CA"
|
| 657 |
+
```
|
| 658 |
+
|
| 659 |
+
**The fundamental problem**: At compression time, we don't know what the LLM will need. All existing approaches guess - and guessing wrong is permanent.
|
| 660 |
+
|
| 661 |
+
### The Research Acknowledges This
|
| 662 |
+
|
| 663 |
+
From [Factory.ai's analysis](https://factory.ai/news/evaluating-compression):
|
| 664 |
+
> "Compression ratio turned out to be the wrong metric entirely. OpenAI achieved 99.3% compression but scored 0.35 points lower on quality. Those discarded details required re-fetching, negating token savings."
|
| 665 |
+
|
| 666 |
+
From [Phil Schmid](https://www.philschmid.de/context-engineering-part-2):
|
| 667 |
+
> "Prefer raw > Compaction > Summarization only when compaction no longer yields enough space. Compaction (Reversible) strips out information that is redundant because it exists in the environment."
|
| 668 |
+
|
| 669 |
+
The insight is clear: **reversible compression beats irreversible compression**.
|
| 670 |
+
|
| 671 |
+
---
|
| 672 |
+
|
| 673 |
+
## Part 3: Introducing CCR (Compress-Cache-Retrieve)
|
| 674 |
+
|
| 675 |
+
### The Architecture
|
| 676 |
+
|
| 677 |
+
CCR makes compression reversible by caching original content for on-demand retrieval:
|
| 678 |
+
|
| 679 |
+
```
|
| 680 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 681 |
+
│ TOOL OUTPUT (1000 items) │
|
| 682 |
+
└────────────────────────┬─────────────────────────────────────────┘
|
| 683 |
+
│
|
| 684 |
+
▼
|
| 685 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 686 |
+
│ CCR LAYER │
|
| 687 |
+
│ │
|
| 688 |
+
│ 1. COMPRESS: Statistical analysis → keep 20 important items │
|
| 689 |
+
│ 2. CACHE: Store all 1000 items in fast local cache (5min TTL) │
|
| 690 |
+
│ 3. INJECT: Tell LLM how to retrieve more if needed │
|
| 691 |
+
│ │
|
| 692 |
+
│ Output to LLM: │
|
| 693 |
+
│ [20 items shown + "retrieve_compressed(hash='abc123') for more"]│
|
| 694 |
+
└────────────────────────┬─────────────────────────────────────────┘
|
| 695 |
+
│
|
| 696 |
+
▼
|
| 697 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 698 |
+
│ LLM PROCESSING │
|
| 699 |
+
│ │
|
| 700 |
+
│ Scenario A: 20 items sufficient → Answer directly │
|
| 701 |
+
│ Scenario B: Need item #47 → retrieve_compressed("state:CA") │
|
| 702 |
+
│ → CCR returns matching items from cache instantly │
|
| 703 |
+
└────────────────────────┬─────────────────────────────────────────┘
|
| 704 |
+
│
|
| 705 |
+
▼
|
| 706 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 707 |
+
│ FEEDBACK LOOP │
|
| 708 |
+
│ │
|
| 709 |
+
│ Track: 30% of search_api compressions trigger retrieval │
|
| 710 |
+
│ Learn: "For search_api, keep items matching state field" │
|
| 711 |
+
│ Improve: Next compression is smarter │
|
| 712 |
+
└──────────────────────────────────────────────────────────────────┘
|
| 713 |
+
```
|
| 714 |
+
|
| 715 |
+
### The Key Components
|
| 716 |
+
|
| 717 |
+
#### 1. Statistical Compression (SmartCrusher)
|
| 718 |
+
|
| 719 |
+
Instead of token-level classification, we analyze JSON structure:
|
| 720 |
+
|
| 721 |
+
```python
|
| 722 |
+
# Field analysis
|
| 723 |
+
{
|
| 724 |
+
"id": {"unique_ratio": 1.0, "type": "identifier"},
|
| 725 |
+
"state": {"unique_ratio": 0.05, "type": "categorical"},
|
| 726 |
+
"amount": {"variance": 8500, "change_points": [47, 203]}
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
# Strategy selection
|
| 730 |
+
if has_score_field:
|
| 731 |
+
strategy = "top_n_by_score"
|
| 732 |
+
elif has_variance_spikes:
|
| 733 |
+
strategy = "time_series"
|
| 734 |
+
elif has_error_keywords:
|
| 735 |
+
strategy = "preserve_errors"
|
| 736 |
+
else:
|
| 737 |
+
strategy = "smart_sample"
|
| 738 |
+
```
|
| 739 |
+
|
| 740 |
+
**Always preserved**:
|
| 741 |
+
- Error items (keyword matching: error, exception, failed, critical)
|
| 742 |
+
- Anomalies (> 2σ from mean)
|
| 743 |
+
- High-relevance items (BM25 + embedding similarity to user query)
|
| 744 |
+
- First K and last K (context and recency)
|
| 745 |
+
|
| 746 |
+
#### 2. Compression Store
|
| 747 |
+
|
| 748 |
+
```python
|
| 749 |
+
@dataclass
|
| 750 |
+
class CompressionEntry:
|
| 751 |
+
hash: str # 16-char SHA256
|
| 752 |
+
original_content: str # Full JSON
|
| 753 |
+
compressed_content: str
|
| 754 |
+
original_item_count: int
|
| 755 |
+
compressed_item_count: int
|
| 756 |
+
tool_name: str | None
|
| 757 |
+
created_at: float
|
| 758 |
+
ttl: int = 300 # 5 minute default
|
| 759 |
+
```
|
| 760 |
+
|
| 761 |
+
**Features**:
|
| 762 |
+
- Thread-safe in-memory storage
|
| 763 |
+
- TTL-based expiration
|
| 764 |
+
- LRU eviction
|
| 765 |
+
- BM25 search within cached content
|
| 766 |
+
|
| 767 |
+
#### 3. Retrieval API
|
| 768 |
+
|
| 769 |
+
```python
|
| 770 |
+
# Full retrieval
|
| 771 |
+
POST /v1/retrieve
|
| 772 |
+
{"hash": "abc123"}
|
| 773 |
+
|
| 774 |
+
# Filtered retrieval (BM25 search)
|
| 775 |
+
POST /v1/retrieve
|
| 776 |
+
{"hash": "abc123", "query": "state:CA"}
|
| 777 |
+
```
|
| 778 |
+
|
| 779 |
+
#### 4. Feedback Loop
|
| 780 |
+
|
| 781 |
+
```python
|
| 782 |
+
@dataclass
|
| 783 |
+
class ToolPattern:
|
| 784 |
+
tool_name: str
|
| 785 |
+
total_compressions: int
|
| 786 |
+
total_retrievals: int
|
| 787 |
+
retrieval_rate: float # retrievals / compressions
|
| 788 |
+
common_queries: dict[str, int] # What users search for
|
| 789 |
+
queried_fields: dict[str, int] # Which fields matter
|
| 790 |
+
```
|
| 791 |
+
|
| 792 |
+
**Feedback-driven hints**:
|
| 793 |
+
```python
|
| 794 |
+
if retrieval_rate > 0.5:
|
| 795 |
+
# Compressing too aggressively
|
| 796 |
+
hints.max_items = 50
|
| 797 |
+
hints.aggressiveness = 0.3
|
| 798 |
+
elif retrieval_rate > 0.8 and full_retrieval_rate > 0.8:
|
| 799 |
+
# Data is unique, don't compress
|
| 800 |
+
hints.skip_compression = True
|
| 801 |
+
else:
|
| 802 |
+
# Current compression is working
|
| 803 |
+
hints.max_items = 15
|
| 804 |
+
```
|
| 805 |
+
|
| 806 |
+
---
|
| 807 |
+
|
| 808 |
+
## Part 4: Comparison Matrix
|
| 809 |
+
|
| 810 |
+
| Dimension | LLMLingua | ACON | Selective Context | CCR (Headroom) |
|
| 811 |
+
|-----------|-----------|------|-------------------|----------------|
|
| 812 |
+
| **Compression unit** | Tokens | Task-specific | Tokens | JSON items |
|
| 813 |
+
| **Requires model** | Yes (classifier) | Yes (LLM) | Yes (attention) | No |
|
| 814 |
+
| **Latency added** | 50-200ms | 100-500ms | 100-300ms | <10ms |
|
| 815 |
+
| **Task-aware** | No | Yes | No | Partial (feedback) |
|
| 816 |
+
| **Reversible** | No | No | No | **Yes** |
|
| 817 |
+
| **Learns from failures** | No | Yes | No | Yes (via retrieval) |
|
| 818 |
+
| **Production-ready** | Research | Research | Research | **Yes** |
|
| 819 |
+
| **Best for** | Natural language | Specific agent tasks | General | Structured tool outputs |
|
| 820 |
+
|
| 821 |
+
### The Key Differentiator: Reversibility
|
| 822 |
+
|
| 823 |
+
| Scenario | LLMLingua | ACON | CCR |
|
| 824 |
+
|----------|-----------|------|-----|
|
| 825 |
+
| Compression is right | ✅ Saves tokens | ✅ Saves tokens | ✅ Saves tokens |
|
| 826 |
+
| Compression is wrong | ❌ Permanent loss | ❌ Permanent loss | ✅ Retrieve from cache |
|
| 827 |
+
| Learning signal | None | Task failure | Retrieval patterns |
|
| 828 |
+
|
| 829 |
+
---
|
| 830 |
+
|
| 831 |
+
## Part 5: Real-World Results
|
| 832 |
+
|
| 833 |
+
### Benchmark: SRE Incident Investigation
|
| 834 |
+
|
| 835 |
+
**Scenario**: Agent investigates production incident using 5 tool calls.
|
| 836 |
+
|
| 837 |
+
| Tool | Original Tokens | Compressed | Savings |
|
| 838 |
+
|------|-----------------|------------|---------|
|
| 839 |
+
| Get metrics | 8,000 | 800 | 90% |
|
| 840 |
+
| Search logs | 6,000 | 900 | 85% |
|
| 841 |
+
| Check status | 4,000 | 600 | 85% |
|
| 842 |
+
| List deployments | 2,500 | 500 | 80% |
|
| 843 |
+
| Get runbook | 1,500 | 400 | 73% |
|
| 844 |
+
| **Total** | **22,000** | **3,200** | **85%** |
|
| 845 |
+
|
| 846 |
+
**Quality**: Agent correctly identified CPU spike, referenced error rates, provided remediation commands. No information loss.
|
| 847 |
+
|
| 848 |
+
### Adversarial Testing
|
| 849 |
+
|
| 850 |
+
We tested CCR against 36 adversarial scenarios:
|
| 851 |
+
|
| 852 |
+
| Category | Example | Result |
|
| 853 |
+
|----------|---------|--------|
|
| 854 |
+
| **Edge cases** | NaN/Infinity scores | ✅ Handled (filtered) |
|
| 855 |
+
| **Scale** | 100,000 items | ✅ <50ms compression |
|
| 856 |
+
| **Concurrency** | 50 threads updating feedback | ✅ Thread-safe |
|
| 857 |
+
| **Injection** | Null bytes in field names | ✅ Safe handling |
|
| 858 |
+
| **Deception** | Misleading score fields | ✅ Keyword detection saves critical items |
|
| 859 |
+
|
| 860 |
+
---
|
| 861 |
+
|
| 862 |
+
## Part 6: When to Use What
|
| 863 |
+
|
| 864 |
+
### Use LLMLingua When:
|
| 865 |
+
- Compressing natural language prompts
|
| 866 |
+
- Need general-purpose compression
|
| 867 |
+
- Can tolerate 50-200ms latency
|
| 868 |
+
- Accuracy > 95% is acceptable
|
| 869 |
+
|
| 870 |
+
### Use ACON When:
|
| 871 |
+
- Building task-specific agents
|
| 872 |
+
- Have clear success/failure signals
|
| 873 |
+
- Can integrate at framework level
|
| 874 |
+
- Willing to accept cold-start learning
|
| 875 |
+
|
| 876 |
+
### Use CCR (Headroom) When:
|
| 877 |
+
- Working with tool outputs (JSON arrays)
|
| 878 |
+
- Need <10ms latency
|
| 879 |
+
- Can't afford ANY information loss
|
| 880 |
+
- Want compression that learns and improves
|
| 881 |
+
- Need production-ready solution today
|
| 882 |
+
|
| 883 |
+
---
|
| 884 |
+
|
| 885 |
+
## Conclusion
|
| 886 |
+
|
| 887 |
+
The compression research community has made impressive progress, but all existing approaches share a fundamental flaw: **irreversibility**.
|
| 888 |
+
|
| 889 |
+
CCR solves this by making compression a **provisioning decision**, not a **deletion decision**. The original data exists; we're just choosing what to surface first.
|
| 890 |
+
|
| 891 |
+
This changes the trade-off:
|
| 892 |
+
- **Before**: Compress aggressively = risk information loss
|
| 893 |
+
- **After**: Compress aggressively = LLM might need one extra retrieval
|
| 894 |
+
|
| 895 |
+
When retrieval is instantaneous (local cache), the risk/reward calculus shifts entirely in favor of aggressive compression.
|
| 896 |
+
|
| 897 |
+
The future of context compression isn't about better heuristics. It's about **reversible architectures that learn from actual needs**.
|
| 898 |
+
|
| 899 |
+
---
|
| 900 |
+
|
| 901 |
+
## Resources
|
| 902 |
+
|
| 903 |
+
- [LLMLingua Paper](https://arxiv.org/abs/2310.05736)
|
| 904 |
+
- [LLMLingua-2 Paper](https://arxiv.org/abs/2403.12968)
|
| 905 |
+
- [ACON Paper](https://arxiv.org/abs/2510.00615)
|
| 906 |
+
- [Selective Context Paper](https://arxiv.org/abs/2310.06201)
|
| 907 |
+
- [Factory.ai Compression Analysis](https://factory.ai/news/evaluating-compression)
|
| 908 |
+
- [Phil Schmid: Context Engineering](https://www.philschmid.de/context-engineering-part-2)
|
| 909 |
+
- [Lost in the Middle](https://arxiv.org/abs/2307.03172)
|
| 910 |
+
- [RAGFlow: From RAG to Context](https://ragflow.io/blog/rag-review-2025-from-rag-to-context)
|
| 911 |
+
|
| 912 |
+
---
|
| 913 |
+
|
| 914 |
+
*This post describes Headroom, an open-source context optimization layer for LLM applications. [GitHub](https://github.com/headroom-sdk/headroom)*
|
docs/HEADROOM_FEATURES.md
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Headroom: Complete Feature Documentation & Competitive Analysis
|
| 2 |
+
|
| 3 |
+
## Executive Summary
|
| 4 |
+
|
| 5 |
+
**Headroom is the world's first Context Optimization Layer for LLM applications.** While the industry has focused on routing (LiteLLM), observability (Helicone), and governance (Portkey), no one has solved the fundamental problem: **LLM contexts are bloated with irrelevant data, and this costs money.**
|
| 6 |
+
|
| 7 |
+
Headroom reduces LLM costs by 50-70% through intelligent context compression while maintaining 100% retention of critical information (errors, anomalies, relevant items). It's the missing infrastructure layer between your application and LLM providers.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Part 1: Complete Feature Inventory
|
| 12 |
+
|
| 13 |
+
## 1. Core Transforms (The "Secret Sauce")
|
| 14 |
+
|
| 15 |
+
### 1.1 SmartCrusher - Statistical Array Compression
|
| 16 |
+
|
| 17 |
+
**Location**: `headroom/transforms/smart_crusher.py`
|
| 18 |
+
|
| 19 |
+
**What It Does**: Compresses large JSON arrays (tool outputs) from 1000s of items to 15-50 items while preserving critical information.
|
| 20 |
+
|
| 21 |
+
**The Safe V1 Recipe** - Always preserves:
|
| 22 |
+
| Preserved Item Type | Why It Matters | Detection Method |
|
| 23 |
+
|---------------------|----------------|------------------|
|
| 24 |
+
| First 3 items | Context/headers | Position-based |
|
| 25 |
+
| Last 2 items | Recency | Position-based |
|
| 26 |
+
| Error items | Critical signals | Keyword matching: `error`, `exception`, `failed`, `failure`, `critical`, `fatal` |
|
| 27 |
+
| Numeric anomalies | Outliers matter | Statistical: values > 2σ from mean |
|
| 28 |
+
| Change points | Regime shifts | Sliding window variance detection |
|
| 29 |
+
| Relevant items | User's needle | BM25/embedding relevance scoring |
|
| 30 |
+
|
| 31 |
+
**Algorithm Details**:
|
| 32 |
+
|
| 33 |
+
```
|
| 34 |
+
1. ANALYZE: SmartAnalyzer computes per-field statistics
|
| 35 |
+
- Uniqueness ratio (unique_count / total_count)
|
| 36 |
+
- Numeric stats (min, max, mean, variance)
|
| 37 |
+
- Change points (indices where value significantly shifts)
|
| 38 |
+
- String stats (avg_length, top values)
|
| 39 |
+
|
| 40 |
+
2. DETECT PATTERN: Identifies data type
|
| 41 |
+
- TIME_SERIES: Has timestamp + numeric variance
|
| 42 |
+
- LOGS: Has message field + level/severity
|
| 43 |
+
- SEARCH_RESULTS: Has score/rank field
|
| 44 |
+
- GENERIC: Default
|
| 45 |
+
|
| 46 |
+
3. PLAN: Creates compression plan based on pattern
|
| 47 |
+
- TIME_SERIES → Keep items around change points
|
| 48 |
+
- LOGS → Cluster by message, keep representatives
|
| 49 |
+
- SEARCH_RESULTS → Keep top N by score
|
| 50 |
+
- GENERIC → Smart statistical sampling
|
| 51 |
+
|
| 52 |
+
4. EXECUTE: Apply plan with priority override
|
| 53 |
+
- If errors/anomalies exceed max_items, KEEP ALL
|
| 54 |
+
- Errors are NEVER dropped
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**Change Point Detection Algorithm**:
|
| 58 |
+
```python
|
| 59 |
+
def detect_change_points(values, window=5):
|
| 60 |
+
std_dev = statistics.stdev(values)
|
| 61 |
+
threshold = 2.0 * std_dev
|
| 62 |
+
|
| 63 |
+
for i in range(window, len(values) - window):
|
| 64 |
+
before_mean = mean(values[i-window:i])
|
| 65 |
+
after_mean = mean(values[i:i+window])
|
| 66 |
+
if abs(after_mean - before_mean) > threshold:
|
| 67 |
+
mark_as_change_point(i)
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**Configuration Options**:
|
| 71 |
+
```python
|
| 72 |
+
@dataclass
|
| 73 |
+
class SmartCrusherConfig:
|
| 74 |
+
enabled: bool = True
|
| 75 |
+
min_items_to_analyze: int = 5 # Don't crush tiny arrays
|
| 76 |
+
min_tokens_to_crush: int = 200 # Only if > 200 tokens
|
| 77 |
+
variance_threshold: float = 2.0 # Std devs for anomaly
|
| 78 |
+
uniqueness_threshold: float = 0.1 # < 10% = constant field
|
| 79 |
+
similarity_threshold: float = 0.8 # String clustering
|
| 80 |
+
max_items_after_crush: int = 15 # Target output size
|
| 81 |
+
preserve_change_points: bool = True
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
**Performance**:
|
| 85 |
+
- 100 items: < 2ms
|
| 86 |
+
- 1,000 items: < 10ms
|
| 87 |
+
- 10,000 items: < 100ms
|
| 88 |
+
- Compression ratio: 50-90% token reduction
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
### 1.5 CCR Architecture - Compress-Cache-Retrieve ⭐ NEW
|
| 93 |
+
|
| 94 |
+
**Location**: `headroom/cache/compression_store.py`, `headroom/cache/compression_feedback.py`
|
| 95 |
+
|
| 96 |
+
**What It Does**: Makes compression **reversible**. When SmartCrusher compresses, the original data is cached. If the LLM needs more, it retrieves instantly.
|
| 97 |
+
|
| 98 |
+
**The Key Innovation**:
|
| 99 |
+
> Traditional compression: Guess what's important → Permanent data loss if wrong
|
| 100 |
+
> CCR: Compress aggressively → Cache original → Retrieve on demand → Zero permanent loss
|
| 101 |
+
|
| 102 |
+
**Four Phases**:
|
| 103 |
+
|
| 104 |
+
| Phase | Component | Description |
|
| 105 |
+
|-------|-----------|-------------|
|
| 106 |
+
| **1. Store** | `CompressionStore` | Cache original content when compressing |
|
| 107 |
+
| **2. Retrieve** | `/v1/retrieve` endpoint | On-demand access to original data |
|
| 108 |
+
| **3. Inject** | Tool/system injection | Tell LLM how to retrieve more |
|
| 109 |
+
| **4. Feedback** | `CompressionFeedback` | Learn from retrieval patterns |
|
| 110 |
+
|
| 111 |
+
**CompressionStore Features**:
|
| 112 |
+
- Thread-safe in-memory storage
|
| 113 |
+
- TTL-based expiration (default 5 minutes)
|
| 114 |
+
- LRU-style eviction at capacity
|
| 115 |
+
- Built-in BM25 search within cached content
|
| 116 |
+
- Hash-based retrieval (16-char SHA256)
|
| 117 |
+
|
| 118 |
+
**Feedback Loop Metrics**:
|
| 119 |
+
```python
|
| 120 |
+
class ToolPattern:
|
| 121 |
+
retrieval_rate: float # retrievals / compressions
|
| 122 |
+
full_retrieval_rate: float # full_retrievals / total_retrievals
|
| 123 |
+
search_rate: float # search_retrievals / total_retrievals
|
| 124 |
+
common_queries: dict # Most frequent search queries
|
| 125 |
+
queried_fields: dict # Fields mentioned in queries
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**Automatic Adjustment**:
|
| 129 |
+
- Retrieval rate >50% → Compress less aggressively (keep 50 items)
|
| 130 |
+
- Retrieval rate >80% with full retrievals → Skip compression entirely
|
| 131 |
+
- Common query fields → Preserve in future compressions
|
| 132 |
+
|
| 133 |
+
**API Endpoints**:
|
| 134 |
+
```
|
| 135 |
+
POST /v1/retrieve → Retrieve cached content by hash
|
| 136 |
+
GET /v1/feedback → Get all learned patterns
|
| 137 |
+
GET /v1/feedback/{tool} → Get hints for specific tool
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
**Configuration**:
|
| 141 |
+
```python
|
| 142 |
+
@dataclass
|
| 143 |
+
class SmartCrusherConfig:
|
| 144 |
+
use_feedback_hints: bool = True # Enable feedback-driven adjustment
|
| 145 |
+
# ... other options
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
**Why This is a Moat**:
|
| 149 |
+
1. **Reversible**: No permanent information loss
|
| 150 |
+
2. **Transparent**: LLM knows it can ask for more
|
| 151 |
+
3. **Learning**: Improves over time from actual usage
|
| 152 |
+
4. **Zero-Risk**: Worst case = retrieve everything
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
### 1.2 CacheAligner - Prefix Stabilization
|
| 157 |
+
|
| 158 |
+
**Location**: `headroom/transforms/cache_aligner.py`
|
| 159 |
+
|
| 160 |
+
**What It Does**: Makes your system prompts cache-friendly by extracting dynamic content (dates, timestamps, session IDs) so the static prefix remains byte-identical across requests.
|
| 161 |
+
|
| 162 |
+
**Why This Matters**:
|
| 163 |
+
- Anthropic: 90% discount on cached tokens
|
| 164 |
+
- OpenAI: 50% discount on cached tokens
|
| 165 |
+
- Google: 75% discount on cached tokens
|
| 166 |
+
|
| 167 |
+
Without CacheAligner:
|
| 168 |
+
```
|
| 169 |
+
Request 1: "Today is January 7, 2025. You are helpful." → Hash: abc123
|
| 170 |
+
Request 2: "Today is January 8, 2025. You are helpful." → Hash: def456 (CACHE MISS!)
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
With CacheAligner:
|
| 174 |
+
```
|
| 175 |
+
Request 1: "You are helpful.\n---\n[Dynamic: January 7, 2025]" → Stable Hash: xyz789
|
| 176 |
+
Request 2: "You are helpful.\n---\n[Dynamic: January 8, 2025]" → Stable Hash: xyz789 (CACHE HIT!)
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
**Detection Tiers**:
|
| 180 |
+
|
| 181 |
+
| Tier | Method | Latency | Coverage |
|
| 182 |
+
|------|--------|---------|----------|
|
| 183 |
+
| 1 (Regex) | Pattern matching | ~0ms | ISO dates, UUIDs, timestamps, version numbers |
|
| 184 |
+
| 2 (NER) | spaCy entities | ~5-10ms | Names, money, organizations, locations |
|
| 185 |
+
| 3 (Semantic) | Embedding similarity | ~20-50ms | Complex dynamic patterns |
|
| 186 |
+
|
| 187 |
+
**Tier 1 Patterns** (Universal, no locale dependencies):
|
| 188 |
+
- ISO 8601 DateTime: `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`
|
| 189 |
+
- ISO 8601 Date: `\d{4}-\d{2}-\d{2}`
|
| 190 |
+
- Unix Timestamp: `\d{10,13}`
|
| 191 |
+
- UUID: `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...-[0-9a-fA-F]{12}`
|
| 192 |
+
- Version: `v\d+\.\d+(?:\.\d+)?`
|
| 193 |
+
- Structural: `Label: value` where Label indicates dynamic content
|
| 194 |
+
|
| 195 |
+
**Entropy-Based Detection**:
|
| 196 |
+
```python
|
| 197 |
+
def calculate_entropy(s: str) -> float:
|
| 198 |
+
"""Shannon entropy normalized to [0, 1]"""
|
| 199 |
+
# High entropy (>0.7) = likely random ID
|
| 200 |
+
# Low entropy (<0.3) = likely static text
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
**Configuration**:
|
| 204 |
+
```python
|
| 205 |
+
@dataclass
|
| 206 |
+
class CacheAlignerConfig:
|
| 207 |
+
enabled: bool = True
|
| 208 |
+
date_patterns: list[str] = [...]
|
| 209 |
+
normalize_whitespace: bool = True
|
| 210 |
+
collapse_blank_lines: bool = True
|
| 211 |
+
dynamic_tail_separator: str = "\n\n---\n[Dynamic Context]\n"
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
### 1.3 RollingWindow - Context Limit Management
|
| 217 |
+
|
| 218 |
+
**Location**: `headroom/transforms/rolling_window.py`
|
| 219 |
+
|
| 220 |
+
**What It Does**: Enforces token limits by dropping oldest context while NEVER orphaning tool call/result pairs.
|
| 221 |
+
|
| 222 |
+
**The Tool Unit Concept**:
|
| 223 |
+
```
|
| 224 |
+
Messages:
|
| 225 |
+
[0] System: "You are helpful"
|
| 226 |
+
[1] User: "Search for X"
|
| 227 |
+
[2] Assistant: [tool_calls: search(X), summarize()]
|
| 228 |
+
[3] Tool: search result (tool_call_id=call_1)
|
| 229 |
+
[4] Tool: summarize result (tool_call_id=call_2)
|
| 230 |
+
[5] User: "Thanks"
|
| 231 |
+
|
| 232 |
+
Tool Unit: (2, [3, 4]) → These drop TOGETHER
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
**Why This Matters**: LLM APIs return errors if tool_calls reference missing tool results. RollingWindow treats them as atomic units.
|
| 236 |
+
|
| 237 |
+
**Drop Priority**:
|
| 238 |
+
1. Oldest tool units (atomic: assistant + all tool results)
|
| 239 |
+
2. Non-tool user/assistant pairs
|
| 240 |
+
3. Single messages (last resort)
|
| 241 |
+
|
| 242 |
+
**Protection Rules**:
|
| 243 |
+
- System messages: NEVER dropped
|
| 244 |
+
- Last N turns: ALWAYS kept (default 2)
|
| 245 |
+
- Tool results for protected messages: AUTO-protected
|
| 246 |
+
|
| 247 |
+
**Configuration**:
|
| 248 |
+
```python
|
| 249 |
+
@dataclass
|
| 250 |
+
class RollingWindowConfig:
|
| 251 |
+
enabled: bool = True
|
| 252 |
+
keep_system: bool = True
|
| 253 |
+
keep_last_turns: int = 2
|
| 254 |
+
output_buffer_tokens: int = 4000 # Reserve for output
|
| 255 |
+
```
|
| 256 |
+
|
| 257 |
+
---
|
| 258 |
+
|
| 259 |
+
### 1.4 Transform Pipeline - Orchestration
|
| 260 |
+
|
| 261 |
+
**Location**: `headroom/transforms/pipeline.py`
|
| 262 |
+
|
| 263 |
+
**Execution Order** (Critical):
|
| 264 |
+
```
|
| 265 |
+
1. CacheAligner → Stabilize prefix for cache hits
|
| 266 |
+
2. SmartCrusher → Compress tool outputs
|
| 267 |
+
3. RollingWindow → Enforce token limits
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
**Why This Order**:
|
| 271 |
+
1. Cache alignment must happen before content changes
|
| 272 |
+
2. Compression reduces tokens before limit enforcement
|
| 273 |
+
3. Rolling window is the final safety net
|
| 274 |
+
|
| 275 |
+
**Token Tracking**: Pipeline tracks tokens through each stage and reports:
|
| 276 |
+
```python
|
| 277 |
+
@dataclass
|
| 278 |
+
class TransformResult:
|
| 279 |
+
messages: list[dict]
|
| 280 |
+
tokens_before: int
|
| 281 |
+
tokens_after: int
|
| 282 |
+
transforms_applied: list[str]
|
| 283 |
+
markers_inserted: list[str]
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
## 2. Relevance Scoring Engine
|
| 289 |
+
|
| 290 |
+
### 2.1 BM25Scorer - Keyword Matching
|
| 291 |
+
|
| 292 |
+
**Location**: `headroom/relevance/bm25.py`
|
| 293 |
+
|
| 294 |
+
**What It Does**: Fast, zero-dependency keyword matching using the BM25 algorithm from information retrieval.
|
| 295 |
+
|
| 296 |
+
**Algorithm**:
|
| 297 |
+
```
|
| 298 |
+
score(D, Q) = Σ IDF(q) * (f(q,D) * (k1 + 1)) / (f(q,D) + k1 * (1 - b + b * |D|/avgdl))
|
| 299 |
+
|
| 300 |
+
Parameters:
|
| 301 |
+
- k1 = 1.5 (term frequency saturation)
|
| 302 |
+
- b = 0.75 (length normalization)
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
**Special Features**:
|
| 306 |
+
- UUID preservation in tokenization
|
| 307 |
+
- +0.3 bonus for exact long token matches (≥8 chars)
|
| 308 |
+
- Query frequency weighting
|
| 309 |
+
|
| 310 |
+
**Use Cases**: Exact ID matching, UUID lookup, keyword search
|
| 311 |
+
|
| 312 |
+
---
|
| 313 |
+
|
| 314 |
+
### 2.2 EmbeddingScorer - Semantic Matching
|
| 315 |
+
|
| 316 |
+
**Location**: `headroom/relevance/embedding.py`
|
| 317 |
+
|
| 318 |
+
**What It Does**: Semantic similarity using sentence-transformers embeddings.
|
| 319 |
+
|
| 320 |
+
**Model**: `all-MiniLM-L6-v2` (22M params, 384 dimensions)
|
| 321 |
+
|
| 322 |
+
**Algorithm**:
|
| 323 |
+
```python
|
| 324 |
+
score = cosine_similarity(embed(item), embed(query))
|
| 325 |
+
# Clamped to [0, 1]
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
**Optimizations**:
|
| 329 |
+
- Batch encoding (context + all items in one call)
|
| 330 |
+
- Model caching across instances
|
| 331 |
+
- Normalized embeddings for fast cosine
|
| 332 |
+
|
| 333 |
+
**Use Cases**: Natural language queries, semantic search
|
| 334 |
+
|
| 335 |
+
---
|
| 336 |
+
|
| 337 |
+
### 2.3 HybridScorer - Adaptive Fusion
|
| 338 |
+
|
| 339 |
+
**Location**: `headroom/relevance/hybrid.py`
|
| 340 |
+
|
| 341 |
+
**What It Does**: Combines BM25 and embedding scores with adaptive alpha based on query characteristics.
|
| 342 |
+
|
| 343 |
+
**Fusion Formula**:
|
| 344 |
+
```
|
| 345 |
+
combined = α * BM25_score + (1 - α) * Embedding_score
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
**Adaptive Alpha** (Research: Hsu et al., 2025):
|
| 349 |
+
```python
|
| 350 |
+
def compute_alpha(query):
|
| 351 |
+
if has_uuid(query):
|
| 352 |
+
return 0.85 # Favor exact matching
|
| 353 |
+
elif has_multiple_ids(query):
|
| 354 |
+
return 0.75
|
| 355 |
+
elif has_single_id(query):
|
| 356 |
+
return 0.65
|
| 357 |
+
elif has_hostname_or_email(query):
|
| 358 |
+
return 0.60
|
| 359 |
+
else:
|
| 360 |
+
return 0.50 # Balanced
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
**Graceful Degradation**: If embeddings unavailable, falls back to boosted BM25.
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
## 3. Cache Optimization (Provider-Specific)
|
| 368 |
+
|
| 369 |
+
### 3.1 Provider Comparison Matrix
|
| 370 |
+
|
| 371 |
+
| Feature | Anthropic | OpenAI | Google |
|
| 372 |
+
|---------|-----------|--------|--------|
|
| 373 |
+
| **Strategy** | Explicit `cache_control` | Automatic prefix | `CachedContent` API |
|
| 374 |
+
| **Min Tokens** | 1,024 | 1,024 | 32,768 |
|
| 375 |
+
| **Max Breakpoints** | 4 | N/A | 1 |
|
| 376 |
+
| **Write Cost** | 1.25x | N/A | N/A |
|
| 377 |
+
| **Read Cost** | 0.10x (90% off) | 0.50x (50% off) | 0.25x (75% off) |
|
| 378 |
+
| **TTL** | 5 min | 5-60 min | Up to 7 days |
|
| 379 |
+
| **Control** | Explicit | Automatic | Explicit |
|
| 380 |
+
|
| 381 |
+
### 3.2 AnthropicCacheOptimizer
|
| 382 |
+
|
| 383 |
+
**Location**: `headroom/cache/anthropic.py`
|
| 384 |
+
|
| 385 |
+
**Algorithm**:
|
| 386 |
+
1. Analyze message sections (system, tools, examples, user)
|
| 387 |
+
2. Stabilize prefix by extracting dynamic content
|
| 388 |
+
3. Plan breakpoints (max 4, prioritize system > tools > examples)
|
| 389 |
+
4. Insert `cache_control: {"type": "ephemeral"}` blocks
|
| 390 |
+
|
| 391 |
+
**Cost Example**:
|
| 392 |
+
```
|
| 393 |
+
First request (write): 1,500 cached tokens * 1.25x = 1,875 cost
|
| 394 |
+
Subsequent (read): 1,500 cached tokens * 0.10x = 150 cost
|
| 395 |
+
Savings per hit: 92%
|
| 396 |
+
```
|
| 397 |
+
|
| 398 |
+
### 3.3 OpenAICacheOptimizer
|
| 399 |
+
|
| 400 |
+
**Location**: `headroom/cache/openai.py`
|
| 401 |
+
|
| 402 |
+
**Strategy**: Since OpenAI caching is automatic, we maximize cache hits through prefix stabilization:
|
| 403 |
+
1. Extract dynamic content via tiered detection
|
| 404 |
+
2. Move dates/IDs to end of message
|
| 405 |
+
3. Normalize whitespace for consistent hashing
|
| 406 |
+
|
| 407 |
+
### 3.4 GoogleCacheOptimizer
|
| 408 |
+
|
| 409 |
+
**Location**: `headroom/cache/google.py`
|
| 410 |
+
|
| 411 |
+
**Strategy**: Uses Google's explicit CachedContent API:
|
| 412 |
+
1. Analyze cacheability (need 32K+ tokens)
|
| 413 |
+
2. Prepare cache creation params
|
| 414 |
+
3. Register cache for reuse
|
| 415 |
+
4. Include `cache_id` in subsequent requests
|
| 416 |
+
|
| 417 |
+
---
|
| 418 |
+
|
| 419 |
+
## 4. Production Proxy Server
|
| 420 |
+
|
| 421 |
+
**Location**: `headroom/proxy/server.py` (1400+ lines)
|
| 422 |
+
|
| 423 |
+
### 4.1 Core Features
|
| 424 |
+
|
| 425 |
+
| Feature | Description | Configuration |
|
| 426 |
+
|---------|-------------|---------------|
|
| 427 |
+
| **Optimization** | SmartCrusher + CacheAligner + RollingWindow | `optimize=True` |
|
| 428 |
+
| **Semantic Cache** | Hash-based response caching with TTL | `cache_ttl_seconds=3600` |
|
| 429 |
+
| **Rate Limiting** | Token bucket algorithm (requests + tokens) | `rate_limit_requests_per_minute=60` |
|
| 430 |
+
| **Retry** | Exponential backoff with jitter | `retry_max_attempts=3` |
|
| 431 |
+
| **Cost Tracking** | Real-time cost + budget enforcement | `budget_limit_usd=100.0` |
|
| 432 |
+
| **Prometheus** | `/metrics` endpoint | Automatic |
|
| 433 |
+
| **Logging** | JSONL request logs | `log_file="/var/log/headroom.jsonl"` |
|
| 434 |
+
|
| 435 |
+
### 4.2 Endpoints
|
| 436 |
+
|
| 437 |
+
```
|
| 438 |
+
GET /health → Health check
|
| 439 |
+
GET /stats → Detailed statistics
|
| 440 |
+
GET /metrics → Prometheus format
|
| 441 |
+
POST /v1/messages → Anthropic API proxy
|
| 442 |
+
POST /v1/chat/completions → OpenAI API proxy
|
| 443 |
+
POST /cache/clear → Clear semantic cache
|
| 444 |
+
|
| 445 |
+
# CCR Endpoints (NEW)
|
| 446 |
+
POST /v1/retrieve → Retrieve cached original content
|
| 447 |
+
GET /v1/feedback → Get all learned patterns
|
| 448 |
+
GET /v1/feedback/{tool} → Get hints for specific tool
|
| 449 |
+
```
|
| 450 |
+
|
| 451 |
+
### 4.3 Token Bucket Rate Limiter
|
| 452 |
+
|
| 453 |
+
```python
|
| 454 |
+
class TokenBucketRateLimiter:
|
| 455 |
+
def check_request(api_key) -> (allowed: bool, wait_seconds: float)
|
| 456 |
+
def check_tokens(api_key, count) -> (allowed: bool, wait_seconds: float)
|
| 457 |
+
|
| 458 |
+
# Continuous refill based on elapsed time
|
| 459 |
+
# Separate buckets for requests and tokens per API key
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
### 4.4 Cost Tracker
|
| 463 |
+
|
| 464 |
+
```python
|
| 465 |
+
PRICING = {
|
| 466 |
+
"claude-3-5-sonnet": (3.00, 15.00, 0.30), # input, output, cached
|
| 467 |
+
"gpt-4o": (2.50, 10.00, 1.25),
|
| 468 |
+
...
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
class CostTracker:
|
| 472 |
+
def estimate_cost(model, input_tokens, output_tokens, cached_tokens)
|
| 473 |
+
def check_budget() -> (within_budget: bool, remaining_usd: float)
|
| 474 |
+
```
|
| 475 |
+
|
| 476 |
+
---
|
| 477 |
+
|
| 478 |
+
## 5. Multi-Provider Support
|
| 479 |
+
|
| 480 |
+
### 5.1 Token Counting
|
| 481 |
+
|
| 482 |
+
| Provider | Method | Accuracy |
|
| 483 |
+
|----------|--------|----------|
|
| 484 |
+
| Anthropic | Official Token Count API | High |
|
| 485 |
+
| Anthropic (fallback) | tiktoken * 1.1 | Medium |
|
| 486 |
+
| OpenAI | tiktoken (model-specific) | High |
|
| 487 |
+
| Google | Official countTokens API | High |
|
| 488 |
+
|
| 489 |
+
### 5.2 Supported Models
|
| 490 |
+
|
| 491 |
+
**Anthropic**:
|
| 492 |
+
- claude-3-5-sonnet-20241022 (200K context)
|
| 493 |
+
- claude-3-5-haiku-20241022 (200K context)
|
| 494 |
+
- claude-3-opus-20240229 (200K context)
|
| 495 |
+
|
| 496 |
+
**OpenAI**:
|
| 497 |
+
- gpt-4o (128K context)
|
| 498 |
+
- gpt-4o-mini (128K context)
|
| 499 |
+
- o1, o1-mini, o3-mini (128-200K context)
|
| 500 |
+
|
| 501 |
+
**Google**:
|
| 502 |
+
- gemini-2.0-flash (1M context)
|
| 503 |
+
- gemini-1.5-pro (2M context)
|
| 504 |
+
- gemini-1.5-flash (1M context)
|
| 505 |
+
|
| 506 |
+
---
|
| 507 |
+
|
| 508 |
+
## 6. Integrations
|
| 509 |
+
|
| 510 |
+
### 6.1 LangChain Integration
|
| 511 |
+
|
| 512 |
+
**Location**: `headroom/integrations/langchain.py`
|
| 513 |
+
|
| 514 |
+
**HeadroomChatModel** - Wrapper that applies optimization:
|
| 515 |
+
```python
|
| 516 |
+
from langchain_openai import ChatOpenAI
|
| 517 |
+
from headroom.integrations import HeadroomChatModel
|
| 518 |
+
|
| 519 |
+
base_model = ChatOpenAI(model="gpt-4o")
|
| 520 |
+
optimized = HeadroomChatModel(base_model, config=HeadroomConfig())
|
| 521 |
+
|
| 522 |
+
response = optimized.invoke("What is 2+2?")
|
| 523 |
+
print(f"Saved: {optimized.total_tokens_saved} tokens")
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
### 6.2 MCP Integration
|
| 527 |
+
|
| 528 |
+
**Location**: `headroom/integrations/mcp.py`
|
| 529 |
+
|
| 530 |
+
**HeadroomMCPCompressor** - Compress tool outputs:
|
| 531 |
+
```python
|
| 532 |
+
from headroom.integrations.mcp import compress_tool_result_with_metrics
|
| 533 |
+
|
| 534 |
+
result = compress_tool_result_with_metrics(
|
| 535 |
+
content=tool_output,
|
| 536 |
+
tool_name="search_logs",
|
| 537 |
+
user_query="find errors",
|
| 538 |
+
)
|
| 539 |
+
print(f"Items: {result.items_before} → {result.items_after}")
|
| 540 |
+
print(f"Errors preserved: {result.errors_preserved}")
|
| 541 |
+
```
|
| 542 |
+
|
| 543 |
+
**Default Tool Profiles**:
|
| 544 |
+
```python
|
| 545 |
+
# Slack - preserve bugs/issues
|
| 546 |
+
MCPToolProfile(tool_name_pattern=r".*slack.*", max_items=25)
|
| 547 |
+
|
| 548 |
+
# Database - preserve nulls/violations
|
| 549 |
+
MCPToolProfile(tool_name_pattern=r".*database.*", max_items=30)
|
| 550 |
+
|
| 551 |
+
# Logs - preserve ALL errors
|
| 552 |
+
MCPToolProfile(tool_name_pattern=r".*log.*", max_items=40)
|
| 553 |
+
```
|
| 554 |
+
|
| 555 |
+
---
|
| 556 |
+
|
| 557 |
+
## 7. Pricing Registry
|
| 558 |
+
|
| 559 |
+
**Location**: `headroom/pricing/`
|
| 560 |
+
|
| 561 |
+
**Features**:
|
| 562 |
+
- Real-time pricing for all models
|
| 563 |
+
- Batch pricing support
|
| 564 |
+
- Staleness detection (warns if >30 days old)
|
| 565 |
+
- Cost estimation with breakdown
|
| 566 |
+
|
| 567 |
+
**Last Updated**: January 6, 2025
|
| 568 |
+
|
| 569 |
+
---
|
| 570 |
+
|
| 571 |
+
# Part 2: Why Headroom is Different
|
| 572 |
+
|
| 573 |
+
## The Market Gap Nobody Else Fills
|
| 574 |
+
|
| 575 |
+
### What Existing Tools Do
|
| 576 |
+
|
| 577 |
+
| Tool | Category | What It Does | What It DOESN'T Do |
|
| 578 |
+
|------|----------|--------------|-------------------|
|
| 579 |
+
| **LiteLLM** | Gateway/Routing | Unified API for 100+ providers | No context optimization |
|
| 580 |
+
| **Helicone** | Observability | Logs, metrics, dashboards | No compression, just watching |
|
| 581 |
+
| **Portkey** | Governance | Guardrails, compliance, security | No token reduction |
|
| 582 |
+
| **OpenRouter** | Marketplace | Access to 300+ models | 5% markup, no optimization |
|
| 583 |
+
| **Cloudflare AI Gateway** | CDN | Caching at edge | Simple caching, no intelligence |
|
| 584 |
+
|
| 585 |
+
### What Headroom Does (That Nobody Else Does)
|
| 586 |
+
|
| 587 |
+
**1. Statistical Compression with Quality Guarantees**
|
| 588 |
+
|
| 589 |
+
No other tool compresses tool outputs while guaranteeing error preservation:
|
| 590 |
+
```
|
| 591 |
+
Input: 1,000 search results (50,000 tokens)
|
| 592 |
+
Output: 20 results (1,000 tokens) - 98% reduction
|
| 593 |
+
ALL errors preserved: 100%
|
| 594 |
+
ALL anomalies preserved: 100%
|
| 595 |
+
```
|
| 596 |
+
|
| 597 |
+
**2. Relevance-Aware Filtering**
|
| 598 |
+
|
| 599 |
+
SmartCrusher uses BM25 + embeddings to keep items matching the user's query:
|
| 600 |
+
```
|
| 601 |
+
User asks: "Why is authentication failing?"
|
| 602 |
+
Tool returns: 1,000 log entries
|
| 603 |
+
SmartCrusher keeps:
|
| 604 |
+
- All entries with "error", "failed", "exception"
|
| 605 |
+
- Entries semantically similar to "authentication failing"
|
| 606 |
+
- First 3 and last 2 for context
|
| 607 |
+
```
|
| 608 |
+
|
| 609 |
+
**3. Provider-Specific Cache Optimization**
|
| 610 |
+
|
| 611 |
+
We understand each provider's caching rules:
|
| 612 |
+
- Anthropic: We insert `cache_control` blocks at optimal positions
|
| 613 |
+
- OpenAI: We stabilize prefixes for automatic caching
|
| 614 |
+
- Google: We manage CachedContent lifecycle
|
| 615 |
+
|
| 616 |
+
**4. Atomic Tool Unit Handling**
|
| 617 |
+
|
| 618 |
+
RollingWindow is the only context manager that treats tool_calls and their results as atomic:
|
| 619 |
+
```
|
| 620 |
+
Other tools: Drop old messages → Orphaned tool results → API ERROR
|
| 621 |
+
Headroom: Drop tool units atomically → Always valid state
|
| 622 |
+
```
|
| 623 |
+
|
| 624 |
+
---
|
| 625 |
+
|
| 626 |
+
## Competitive Analysis: Deep Dive
|
| 627 |
+
|
| 628 |
+
### vs. LiteLLM
|
| 629 |
+
|
| 630 |
+
| Aspect | LiteLLM | Headroom |
|
| 631 |
+
|--------|---------|----------|
|
| 632 |
+
| **Primary Function** | Route to 100+ providers | Optimize before routing |
|
| 633 |
+
| **Token Reduction** | None | 50-70% |
|
| 634 |
+
| **Caching** | None | Semantic + provider-specific |
|
| 635 |
+
| **Setup Time** | 15-30 min | 5 min |
|
| 636 |
+
| **Latency Overhead** | ~500µs | <50ms |
|
| 637 |
+
| **Relationship** | Complementary - we optimize BEFORE LiteLLM routes |
|
| 638 |
+
|
| 639 |
+
**Partnership Opportunity**: Headroom optimizes → LiteLLM routes → best of both.
|
| 640 |
+
|
| 641 |
+
### vs. Helicone
|
| 642 |
+
|
| 643 |
+
| Aspect | Helicone | Headroom |
|
| 644 |
+
|--------|----------|----------|
|
| 645 |
+
| **Primary Function** | Observe and log | Optimize and compress |
|
| 646 |
+
| **Token Reduction** | Shows waste, doesn't fix it | Eliminates waste |
|
| 647 |
+
| **Latency** | ~50ms (Rust) | <50ms |
|
| 648 |
+
| **Caching** | Redis-based, TTL | Semantic + provider-specific |
|
| 649 |
+
| **Relationship** | Complementary - we reduce, they observe |
|
| 650 |
+
|
| 651 |
+
**Partnership Opportunity**: Headroom compresses → Helicone shows savings achieved.
|
| 652 |
+
|
| 653 |
+
### vs. Portkey
|
| 654 |
+
|
| 655 |
+
| Aspect | Portkey | Headroom |
|
| 656 |
+
|--------|---------|----------|
|
| 657 |
+
| **Primary Function** | Governance, guardrails | Optimization, compression |
|
| 658 |
+
| **Target User** | Enterprise security teams | Developers, cost-conscious |
|
| 659 |
+
| **Token Reduction** | None | 50-70% |
|
| 660 |
+
| **Pricing** | From $49/month | Open source core |
|
| 661 |
+
| **Relationship** | Different markets |
|
| 662 |
+
|
| 663 |
+
### vs. Prompt Compression Techniques (LLMLingua, etc.)
|
| 664 |
+
|
| 665 |
+
| Aspect | LLMLingua-2 | Headroom |
|
| 666 |
+
|--------|-------------|----------|
|
| 667 |
+
| **Approach** | Token classification (remove tokens) | Statistical sampling (keep important items) |
|
| 668 |
+
| **Target** | Reduce prompt tokens | Reduce tool output tokens |
|
| 669 |
+
| **Granularity** | Token-level | Item-level (semantic units) |
|
| 670 |
+
| **Quality Guarantee** | 95-98% accuracy | 100% error retention |
|
| 671 |
+
| **Dependencies** | XLM-RoBERTa model | Zero (BM25) or sentence-transformers |
|
| 672 |
+
| **Use Case** | Long prompts | Large JSON arrays from tools |
|
| 673 |
+
|
| 674 |
+
---
|
| 675 |
+
|
| 676 |
+
## The Industry Problem We Solve
|
| 677 |
+
|
| 678 |
+
### Context Explosion in AI Agents
|
| 679 |
+
|
| 680 |
+
Research from [JetBrains (Dec 2025)](https://blog.jetbrains.com/research/2025/12/efficient-context-management/):
|
| 681 |
+
> "Agents make multiple tool calls in sequence, and each tool's output is fed back into the LLM's context window. Without proper context management, this accumulation can quickly exceed the context window, increase costs dramatically, and degrade performance."
|
| 682 |
+
|
| 683 |
+
### The "Lost in the Middle" Problem
|
| 684 |
+
|
| 685 |
+
> "LLMs are more likely to recall information appearing at the beginning or end of long prompts rather than content buried in the middle."
|
| 686 |
+
|
| 687 |
+
**Headroom's Solution**: SmartCrusher keeps first 3 + last 2 items, plus errors/anomalies/relevant items. We work WITH the LLM's attention patterns.
|
| 688 |
+
|
| 689 |
+
### Context Rot
|
| 690 |
+
|
| 691 |
+
> "Expanding context windows does not guarantee improved model performance. As input tokens increase, LLM performance can actually degrade."
|
| 692 |
+
|
| 693 |
+
**Headroom's Solution**: Smaller, higher-quality context → better performance AND lower cost.
|
| 694 |
+
|
| 695 |
+
---
|
| 696 |
+
|
| 697 |
+
## Unique Technical Innovations
|
| 698 |
+
|
| 699 |
+
### 1. Change Point Detection for Time Series
|
| 700 |
+
|
| 701 |
+
No other tool detects regime shifts in numeric data:
|
| 702 |
+
```python
|
| 703 |
+
# Values: [100, 102, 98, 101, 99, 500, 502, 498, 501]
|
| 704 |
+
# ↑
|
| 705 |
+
# Change point detected!
|
| 706 |
+
# SmartCrusher keeps items around index 5
|
| 707 |
+
```
|
| 708 |
+
|
| 709 |
+
### 2. Adaptive Relevance Fusion
|
| 710 |
+
|
| 711 |
+
Our HybridScorer adjusts BM25/embedding weights based on query type:
|
| 712 |
+
- UUID in query → More BM25 (exact matching)
|
| 713 |
+
- Natural language → More embedding (semantic)
|
| 714 |
+
|
| 715 |
+
This achieves +2-7.5% accuracy improvement over fixed weights.
|
| 716 |
+
|
| 717 |
+
### 3. Tool Unit Atomicity
|
| 718 |
+
|
| 719 |
+
The only context manager that guarantees:
|
| 720 |
+
```
|
| 721 |
+
assistant message with tool_calls → ALWAYS has corresponding tool results
|
| 722 |
+
```
|
| 723 |
+
|
| 724 |
+
### 4. Tiered Dynamic Detection
|
| 725 |
+
|
| 726 |
+
We don't use hardcoded locale patterns. Our detection is:
|
| 727 |
+
- Universal: ISO 8601, UUIDs, entropy-based IDs
|
| 728 |
+
- Structural: `Label: value` patterns
|
| 729 |
+
- Semantic: Embedding similarity to known dynamic exemplars
|
| 730 |
+
|
| 731 |
+
---
|
| 732 |
+
|
| 733 |
+
# Part 3: Real Numbers
|
| 734 |
+
|
| 735 |
+
## Compression Performance
|
| 736 |
+
|
| 737 |
+
| Scenario | Items Before | Items After | Token Reduction | Errors Retained |
|
| 738 |
+
|----------|--------------|-------------|-----------------|-----------------|
|
| 739 |
+
| Search Results | 1,000 | 20 | 85% | 100% |
|
| 740 |
+
| Log Entries | 500 | 40 | 80% | 100% |
|
| 741 |
+
| Database Rows | 1,000 | 30 | 90% | 100% |
|
| 742 |
+
| API Responses | 200 | 15 | 70% | 100% |
|
| 743 |
+
|
| 744 |
+
## Latency Overhead
|
| 745 |
+
|
| 746 |
+
| Component | P50 | P99 |
|
| 747 |
+
|-----------|-----|-----|
|
| 748 |
+
| SmartCrusher (1000 items) | 5ms | 15ms |
|
| 749 |
+
| CacheAligner | <1ms | 2ms |
|
| 750 |
+
| RollingWindow | <1ms | 5ms |
|
| 751 |
+
| Full Pipeline | 10ms | 25ms |
|
| 752 |
+
|
| 753 |
+
## Cost Savings (Real World)
|
| 754 |
+
|
| 755 |
+
**Claude Code Agent Session**:
|
| 756 |
+
```
|
| 757 |
+
Without Headroom:
|
| 758 |
+
- Tool outputs: 150,000 tokens
|
| 759 |
+
- Cost: $0.45 (input @ $3/M)
|
| 760 |
+
|
| 761 |
+
With Headroom:
|
| 762 |
+
- Tool outputs: 30,000 tokens (80% reduction)
|
| 763 |
+
- Cost: $0.09 (input @ $3/M)
|
| 764 |
+
- Savings: $0.36 per session (80%)
|
| 765 |
+
```
|
| 766 |
+
|
| 767 |
+
**Enterprise (1M requests/month)**:
|
| 768 |
+
```
|
| 769 |
+
Without Headroom: $450,000/month
|
| 770 |
+
With Headroom: $90,000/month
|
| 771 |
+
Savings: $360,000/month (80%)
|
| 772 |
+
```
|
| 773 |
+
|
| 774 |
+
---
|
| 775 |
+
|
| 776 |
+
# Part 4: Architecture Summary
|
| 777 |
+
|
| 778 |
+
```
|
| 779 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 780 |
+
│ YOUR APPLICATION │
|
| 781 |
+
│ │
|
| 782 |
+
│ LangChain │ Claude Code │ Cursor │ Custom Agent │
|
| 783 |
+
└──────────────────────────┬──────────────────────────────────┘
|
| 784 |
+
│
|
| 785 |
+
▼
|
| 786 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 787 |
+
│ HEADROOM PROXY │
|
| 788 |
+
│ │
|
| 789 |
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
| 790 |
+
│ │ Cache │ │ Rate │ │ Cost │ │
|
| 791 |
+
│ │ (Semantic) │ │ Limiter │ │ Tracker │ │
|
| 792 |
+
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
| 793 |
+
│ │
|
| 794 |
+
│ ┌─────────────────────────────────────────────────────────┐│
|
| 795 |
+
│ │ TRANSFORM PIPELINE ││
|
| 796 |
+
│ │ ││
|
| 797 |
+
│ │ 1. CacheAligner → Stabilize prefix for cache hits ││
|
| 798 |
+
│ │ 2. SmartCrusher → Compress tool outputs ││
|
| 799 |
+
│ │ 3. RollingWindow → Enforce token limits ││
|
| 800 |
+
│ │ ││
|
| 801 |
+
│ │ ┌─────────────────────────────────────────────────┐ ││
|
| 802 |
+
│ │ │ RELEVANCE ENGINE │ ││
|
| 803 |
+
│ │ │ BM25 + Embedding + Adaptive Hybrid │ ││
|
| 804 |
+
│ │ └─────────────────────────────────────────────────┘ ││
|
| 805 |
+
│ └─────────────────────────────────────────────────────────┘│
|
| 806 |
+
│ │
|
| 807 |
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
| 808 |
+
│ │ Prometheus │ │ JSONL │ │ Retry │ │
|
| 809 |
+
│ │ Metrics │ │ Logging │ │ (Exp. Backoff) │ │
|
| 810 |
+
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
| 811 |
+
└──────────────────────────┬──────────────────────────────────┘
|
| 812 |
+
│
|
| 813 |
+
▼
|
| 814 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 815 |
+
│ LLM PROVIDERS │
|
| 816 |
+
│ │
|
| 817 |
+
│ Anthropic │ OpenAI │ Google │ Others │
|
| 818 |
+
│ │
|
| 819 |
+
│ ┌─────────────────────────────────────────────────────────┐│
|
| 820 |
+
│ │ PROVIDER-SPECIFIC CACHE OPTIMIZERS ││
|
| 821 |
+
│ │ ││
|
| 822 |
+
│ │ Anthropic: cache_control blocks (90% savings) ││
|
| 823 |
+
│ │ OpenAI: Prefix stabilization (50% savings) ││
|
| 824 |
+
│ │ Google: CachedContent API (75% savings) ││
|
| 825 |
+
│ └─────────────────────────────────────────────────────────┘│
|
| 826 |
+
└─────────────────────────────────────────────────────────────┘
|
| 827 |
+
```
|
| 828 |
+
|
| 829 |
+
---
|
| 830 |
+
|
| 831 |
+
# Part 5: File Inventory
|
| 832 |
+
|
| 833 |
+
## Core Transforms
|
| 834 |
+
- `headroom/transforms/smart_crusher.py` - Statistical array compression
|
| 835 |
+
- `headroom/transforms/cache_aligner.py` - Prefix stabilization
|
| 836 |
+
- `headroom/transforms/rolling_window.py` - Context limit management
|
| 837 |
+
- `headroom/transforms/pipeline.py` - Transform orchestration
|
| 838 |
+
|
| 839 |
+
## Relevance Scoring
|
| 840 |
+
- `headroom/relevance/bm25.py` - BM25 keyword scorer
|
| 841 |
+
- `headroom/relevance/embedding.py` - Semantic scorer
|
| 842 |
+
- `headroom/relevance/hybrid.py` - Adaptive fusion scorer
|
| 843 |
+
|
| 844 |
+
## Cache Optimization
|
| 845 |
+
- `headroom/cache/base.py` - Base interfaces
|
| 846 |
+
- `headroom/cache/anthropic.py` - Anthropic optimizer
|
| 847 |
+
- `headroom/cache/openai.py` - OpenAI optimizer
|
| 848 |
+
- `headroom/cache/google.py` - Google optimizer
|
| 849 |
+
- `headroom/cache/dynamic_detector.py` - Tiered dynamic detection
|
| 850 |
+
- `headroom/cache/semantic.py` - Semantic cache layer
|
| 851 |
+
- `headroom/cache/compression_store.py` - CCR Phase 1: Store original content ⭐ NEW
|
| 852 |
+
- `headroom/cache/compression_feedback.py` - CCR Phase 4: Learn from retrievals ⭐ NEW
|
| 853 |
+
|
| 854 |
+
## Proxy Server
|
| 855 |
+
- `headroom/proxy/server.py` - Production HTTP proxy (1400+ lines)
|
| 856 |
+
|
| 857 |
+
## Providers
|
| 858 |
+
- `headroom/providers/anthropic.py` - Anthropic token counting
|
| 859 |
+
- `headroom/providers/openai.py` - OpenAI token counting
|
| 860 |
+
- `headroom/providers/google.py` - Google token counting
|
| 861 |
+
|
| 862 |
+
## Integrations
|
| 863 |
+
- `headroom/integrations/langchain.py` - LangChain wrapper
|
| 864 |
+
- `headroom/integrations/mcp.py` - MCP compression
|
| 865 |
+
|
| 866 |
+
## Pricing
|
| 867 |
+
- `headroom/pricing/registry.py` - Pricing registry
|
| 868 |
+
- `headroom/pricing/anthropic_prices.py` - Anthropic prices
|
| 869 |
+
- `headroom/pricing/openai_prices.py` - OpenAI prices
|
| 870 |
+
|
| 871 |
+
## Tests
|
| 872 |
+
- `tests/test_quality_retention.py` - 21 formal evals for quality guarantees
|
| 873 |
+
- `tests/test_cache/test_dynamic_detector.py` - Dynamic detection tests
|
| 874 |
+
- `tests/test_ccr.py` - CCR store, tool injection tests ⭐ NEW
|
| 875 |
+
- `tests/test_ccr_feedback.py` - CCR feedback loop tests ⭐ NEW
|
| 876 |
+
|
| 877 |
+
## Benchmarks
|
| 878 |
+
- `benchmarks/agent_cost_benchmark.py` - Real-world agent cost analysis
|
| 879 |
+
- `benchmarks/dynamic_detector_benchmark.py` - Detection performance
|
| 880 |
+
|
| 881 |
+
---
|
| 882 |
+
|
| 883 |
+
# Sources
|
| 884 |
+
|
| 885 |
+
- [JetBrains Research: Efficient Context Management (Dec 2025)](https://blog.jetbrains.com/research/2025/12/efficient-context-management/)
|
| 886 |
+
- [LangChain: Context Engineering for Agents](https://blog.langchain.com/context-engineering-for-agents/)
|
| 887 |
+
- [Helicone: Top 5 LLM Gateways 2025](https://www.helicone.ai/blog/top-llm-gateways-comparison-2025)
|
| 888 |
+
- [Agenta: Top LLM Gateways 2025](https://agenta.ai/blog/top-llm-gateways)
|
| 889 |
+
- [Portkey: LLM Proxy vs AI Gateway](https://portkey.ai/blog/llm-proxy-vs-ai-gateway/)
|
| 890 |
+
- [Medium: Prompt Compression Techniques (Nov 2025)](https://medium.com/@kuldeep.paul08/prompt-compression-techniques-reducing-context-window-costs-while-improving-llm-performance-afec1e8f1003)
|
| 891 |
+
- [Factory.ai: Compressing Context](https://factory.ai/news/compressing-context)
|
docs/PATH_TO_10_OUT_OF_10.md
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The Path to 10/10: Strategic Deep Dive
|
| 2 |
+
|
| 3 |
+
## Current State
|
| 4 |
+
|
| 5 |
+
| Dimension | Score | Gap |
|
| 6 |
+
|-----------|-------|-----|
|
| 7 |
+
| Problem validity | 9/10 | Framing as "cost" not "capability" |
|
| 8 |
+
| Solution fit | 7/10 | 30% of scenarios fail silently |
|
| 9 |
+
| Technical moat | 6/10 | Easy to replicate basics |
|
| 10 |
+
| Market timing | 9/10 | Positioned but not capturing |
|
| 11 |
+
| **Overall** | **7.5/10** | |
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Dimension 1: Problem Validity (9 → 10)
|
| 16 |
+
|
| 17 |
+
## Current Framing (9/10)
|
| 18 |
+
"Token costs are expensive. We save you 50-90%."
|
| 19 |
+
|
| 20 |
+
**Why it's not 10/10**: Cost savings is a feature, not a platform. It's also easily commoditized - anyone can undercut on price.
|
| 21 |
+
|
| 22 |
+
## The 10/10 Framing: Capability Enablement
|
| 23 |
+
|
| 24 |
+
**The insight**: Without context optimization, certain agent capabilities are **literally impossible**.
|
| 25 |
+
|
| 26 |
+
### Evidence
|
| 27 |
+
|
| 28 |
+
| Scenario | Without Headroom | With Headroom |
|
| 29 |
+
|----------|------------------|---------------|
|
| 30 |
+
| Multi-tool investigation (5+ tools) | Context overflow at 128K | Fits in 30K |
|
| 31 |
+
| Long-running agent (50+ turns) | Loses early context | Maintains full history |
|
| 32 |
+
| Real-time agents (latency-sensitive) | Cache misses = 2-3s latency | Cache hits = 200ms |
|
| 33 |
+
| Cost-constrained deployment | $5K/month = 5K requests | $5K/month = 25K requests |
|
| 34 |
+
|
| 35 |
+
**The reframe**:
|
| 36 |
+
|
| 37 |
+
> "Headroom doesn't just save money. It **unlocks agent capabilities that are impossible without context optimization**."
|
| 38 |
+
|
| 39 |
+
### Specific Claims to Make
|
| 40 |
+
|
| 41 |
+
1. **"Enable 5x more tool calls per context window"**
|
| 42 |
+
- Not "save 80% on tokens"
|
| 43 |
+
- But "do 5x more in the same budget"
|
| 44 |
+
|
| 45 |
+
2. **"Make real-time agents viable"**
|
| 46 |
+
- Cache alignment → cache hits → <500ms responses
|
| 47 |
+
- Without this, interactive agents are too slow
|
| 48 |
+
|
| 49 |
+
3. **"Prevent context overflow failures"**
|
| 50 |
+
- Agent that fails at turn 47 because context overflowed
|
| 51 |
+
- vs. agent that completes 200-turn sessions
|
| 52 |
+
|
| 53 |
+
4. **"Run agents at 10x the scale"**
|
| 54 |
+
- Same budget, 10x throughput
|
| 55 |
+
- This is a capability unlock, not a cost savings
|
| 56 |
+
|
| 57 |
+
### Action Items
|
| 58 |
+
|
| 59 |
+
- [ ] Rewrite all marketing around "capability enablement"
|
| 60 |
+
- [ ] Quantify "things you CAN'T do without Headroom"
|
| 61 |
+
- [ ] Build demo showing agent that fails → succeeds with Headroom
|
| 62 |
+
- [ ] Position as "Context Runtime" not "Token Optimizer"
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
# Dimension 2: Solution Fit (7 → 10)
|
| 67 |
+
|
| 68 |
+
## Current Problem (7/10)
|
| 69 |
+
|
| 70 |
+
Heuristics work for ~70% of scenarios. The 30% that fail:
|
| 71 |
+
- Entity listings (each item is unique and important)
|
| 72 |
+
- Exhaustive queries ("find ALL X")
|
| 73 |
+
- Needles that look normal (Order #47 from California)
|
| 74 |
+
|
| 75 |
+
**Root cause**: Task-agnostic compression can't know what the LLM will need.
|
| 76 |
+
|
| 77 |
+
## The 10/10 Solution: Three-Layer Architecture
|
| 78 |
+
|
| 79 |
+
### Layer 1: Smart Routing (NEW)
|
| 80 |
+
|
| 81 |
+
**Before compression, classify the task:**
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
class TaskClassifier:
|
| 85 |
+
"""Classify task to determine compression strategy."""
|
| 86 |
+
|
| 87 |
+
def classify(self, user_query: str, tool_output: dict) -> TaskType:
|
| 88 |
+
# Analyze user query intent
|
| 89 |
+
if self._is_exhaustive_query(user_query):
|
| 90 |
+
return TaskType.EXHAUSTIVE # "find ALL", "list every"
|
| 91 |
+
|
| 92 |
+
if self._is_specific_lookup(user_query):
|
| 93 |
+
return TaskType.LOOKUP # "find user #47", "get order X"
|
| 94 |
+
|
| 95 |
+
if self._is_analytical(user_query):
|
| 96 |
+
return TaskType.ANALYTICAL # "what's wrong", "summarize"
|
| 97 |
+
|
| 98 |
+
return TaskType.GENERAL
|
| 99 |
+
|
| 100 |
+
def _is_exhaustive_query(self, query: str) -> bool:
|
| 101 |
+
exhaustive_patterns = [
|
| 102 |
+
r"\ball\b", r"\bevery\b", r"\beach\b",
|
| 103 |
+
r"\bcomplete list\b", r"\bfull list\b"
|
| 104 |
+
]
|
| 105 |
+
return any(re.search(p, query.lower()) for p in exhaustive_patterns)
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
**Strategy per task type:**
|
| 109 |
+
|
| 110 |
+
| Task Type | Strategy | Rationale |
|
| 111 |
+
|-----------|----------|-----------|
|
| 112 |
+
| EXHAUSTIVE | Skip compression | User needs everything |
|
| 113 |
+
| LOOKUP | Filter by query match | Only relevant items |
|
| 114 |
+
| ANALYTICAL | Statistical compression | Summaries ok |
|
| 115 |
+
| GENERAL | Default heuristics | Balanced approach |
|
| 116 |
+
|
| 117 |
+
### Layer 2: Confidence-Gated Compression (NEW)
|
| 118 |
+
|
| 119 |
+
**Only compress when confidence is high:**
|
| 120 |
+
|
| 121 |
+
```python
|
| 122 |
+
class CompressionConfidence:
|
| 123 |
+
"""Estimate confidence that compression is safe."""
|
| 124 |
+
|
| 125 |
+
def estimate(self, items: list[dict], hints: CompressionHints) -> float:
|
| 126 |
+
confidence = 1.0
|
| 127 |
+
|
| 128 |
+
# Low confidence if high uniqueness + no importance signal
|
| 129 |
+
if self._is_high_uniqueness(items) and not self._has_importance_signal(items):
|
| 130 |
+
confidence -= 0.4
|
| 131 |
+
|
| 132 |
+
# Low confidence if historical retrieval rate is high
|
| 133 |
+
if hints.retrieval_rate > 0.5:
|
| 134 |
+
confidence -= 0.3
|
| 135 |
+
|
| 136 |
+
# Low confidence if items look like entities
|
| 137 |
+
if self._looks_like_entity_list(items):
|
| 138 |
+
confidence -= 0.3
|
| 139 |
+
|
| 140 |
+
return max(0.0, confidence)
|
| 141 |
+
|
| 142 |
+
def should_compress(self, confidence: float) -> bool:
|
| 143 |
+
return confidence > 0.6 # Only compress when confident
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**The key insight**: It's better to NOT compress than to compress wrong.
|
| 147 |
+
|
| 148 |
+
### Layer 3: Seamless CCR (Enhanced)
|
| 149 |
+
|
| 150 |
+
**Make retrieval so good that compression "failures" don't matter:**
|
| 151 |
+
|
| 152 |
+
Current CCR:
|
| 153 |
+
```
|
| 154 |
+
LLM: "I need to find orders from California"
|
| 155 |
+
[Must explicitly call retrieve_compressed]
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
Enhanced CCR:
|
| 159 |
+
```
|
| 160 |
+
LLM: "I need to find orders from California"
|
| 161 |
+
[Automatic injection]: "Searching compressed content for 'California'..."
|
| 162 |
+
[Returns matching items without explicit tool call]
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
**Implementation: Semantic Injection**
|
| 166 |
+
|
| 167 |
+
```python
|
| 168 |
+
class SemanticCCR:
|
| 169 |
+
"""Automatically inject relevant cached content based on LLM response."""
|
| 170 |
+
|
| 171 |
+
def intercept_response(self, llm_response: str, cached_hashes: list[str]) -> str:
|
| 172 |
+
# Detect if LLM is "reaching" for data it doesn't have
|
| 173 |
+
reaching_patterns = [
|
| 174 |
+
r"I don't see .* in the data",
|
| 175 |
+
r"The data doesn't show",
|
| 176 |
+
r"I need more information about",
|
| 177 |
+
r"Looking for .* but",
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
for pattern in reaching_patterns:
|
| 181 |
+
match = re.search(pattern, llm_response)
|
| 182 |
+
if match:
|
| 183 |
+
# Extract what they're looking for
|
| 184 |
+
query = self._extract_search_intent(llm_response)
|
| 185 |
+
# Search all cached content
|
| 186 |
+
results = self._search_cached(cached_hashes, query)
|
| 187 |
+
if results:
|
| 188 |
+
# Inject into context
|
| 189 |
+
return self._inject_results(llm_response, results)
|
| 190 |
+
|
| 191 |
+
return llm_response
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
### Layer 4: Learned Compression Profiles (NEW)
|
| 195 |
+
|
| 196 |
+
**Per-tool profiles that go beyond heuristics:**
|
| 197 |
+
|
| 198 |
+
```python
|
| 199 |
+
@dataclass
|
| 200 |
+
class ToolCompressionProfile:
|
| 201 |
+
"""Learned compression profile for a specific tool."""
|
| 202 |
+
|
| 203 |
+
tool_name: str
|
| 204 |
+
|
| 205 |
+
# Learned from retrieval patterns
|
| 206 |
+
critical_fields: list[str] # Always preserve these
|
| 207 |
+
optional_fields: list[str] # Can compress
|
| 208 |
+
noise_fields: list[str] # Usually irrelevant
|
| 209 |
+
|
| 210 |
+
# Learned from retrieval rate
|
| 211 |
+
min_items: int # Never compress below this
|
| 212 |
+
target_items: int # Optimal compression target
|
| 213 |
+
skip_conditions: list[str] # When to skip compression entirely
|
| 214 |
+
|
| 215 |
+
# Learned from query patterns
|
| 216 |
+
common_search_terms: list[str] # Pre-filter for these
|
| 217 |
+
|
| 218 |
+
# Confidence
|
| 219 |
+
sample_size: int # How much data we've seen
|
| 220 |
+
confidence: float # How confident in this profile
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
**Building profiles from feedback:**
|
| 224 |
+
|
| 225 |
+
```python
|
| 226 |
+
def update_profile_from_retrieval(profile: ToolCompressionProfile, event: RetrievalEvent):
|
| 227 |
+
# If they retrieved, compression was too aggressive
|
| 228 |
+
profile.min_items = max(profile.min_items, event.items_retrieved)
|
| 229 |
+
|
| 230 |
+
# Track what fields they queried
|
| 231 |
+
for field in extract_fields(event.query):
|
| 232 |
+
if field not in profile.critical_fields:
|
| 233 |
+
profile.critical_fields.append(field)
|
| 234 |
+
|
| 235 |
+
# Track common search terms
|
| 236 |
+
if event.query:
|
| 237 |
+
profile.common_search_terms.append(event.query)
|
| 238 |
+
|
| 239 |
+
# Update confidence based on sample size
|
| 240 |
+
profile.sample_size += 1
|
| 241 |
+
profile.confidence = min(0.95, profile.sample_size / 100)
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
## The 10/10 Solution Architecture
|
| 245 |
+
|
| 246 |
+
```
|
| 247 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 248 |
+
│ TOOL OUTPUT (1000 items) │
|
| 249 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 250 |
+
│
|
| 251 |
+
▼
|
| 252 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 253 |
+
│ LAYER 1: TASK CLASSIFICATION │
|
| 254 |
+
│ │
|
| 255 |
+
│ User query: "Find all orders from California" │
|
| 256 |
+
│ Classification: EXHAUSTIVE (pattern: "all") │
|
| 257 |
+
│ Decision: SKIP COMPRESSION │
|
| 258 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 259 |
+
│
|
| 260 |
+
▼ (if not SKIP)
|
| 261 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 262 |
+
│ LAYER 2: CONFIDENCE ESTIMATION │
|
| 263 |
+
│ │
|
| 264 |
+
│ Tool profile: search_api (confidence: 0.85) │
|
| 265 |
+
│ Data analysis: unique_ratio=0.95, no_score_field │
|
| 266 |
+
│ Compression confidence: 0.4 │
|
| 267 |
+
│ Decision: SKIP (confidence < 0.6) │
|
| 268 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 269 |
+
│
|
| 270 |
+
▼ (if confident)
|
| 271 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 272 |
+
│ LAYER 3: PROFILE-GUIDED COMPRESSION │
|
| 273 |
+
│ │
|
| 274 |
+
│ Profile: search_api │
|
| 275 |
+
│ - critical_fields: [id, status, error] │
|
| 276 |
+
│ - min_items: 25 │
|
| 277 |
+
│ - common_search_terms: [status:error, level:critical] │
|
| 278 |
+
│ │
|
| 279 |
+
│ Compression: 1000 → 30 items (profile-guided, not heuristic) │
|
| 280 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 281 |
+
│
|
| 282 |
+
▼
|
| 283 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 284 |
+
│ LAYER 4: CCR WITH SEMANTIC INJECTION │
|
| 285 |
+
│ │
|
| 286 |
+
│ Cache: Store full 1000 items │
|
| 287 |
+
│ Monitor: Watch for "reaching" patterns in LLM response │
|
| 288 |
+
│ Inject: Auto-retrieve if LLM seems to need more │
|
| 289 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 290 |
+
│
|
| 291 |
+
▼
|
| 292 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 293 |
+
│ FEEDBACK LOOP │
|
| 294 |
+
│ │
|
| 295 |
+
│ Track: Retrieval patterns, query patterns, failure patterns │
|
| 296 |
+
│ Learn: Update tool profiles, adjust confidence thresholds │
|
| 297 |
+
│ Improve: Next compression is smarter │
|
| 298 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
### Action Items
|
| 302 |
+
|
| 303 |
+
- [ ] Implement TaskClassifier with exhaustive/lookup/analytical detection
|
| 304 |
+
- [ ] Add confidence estimation to SmartCrusher
|
| 305 |
+
- [ ] Build ToolCompressionProfile system
|
| 306 |
+
- [ ] Implement semantic injection for CCR
|
| 307 |
+
- [ ] Create profile bootstrap from first 10 compressions per tool
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
# Dimension 3: Technical Moat (6 → 10)
|
| 312 |
+
|
| 313 |
+
## Current Problem (6/10)
|
| 314 |
+
|
| 315 |
+
Individual techniques are not novel:
|
| 316 |
+
- Statistical analysis: Data profiling tools exist
|
| 317 |
+
- BM25/embeddings: Standard IR
|
| 318 |
+
- Caching: Standard pattern
|
| 319 |
+
|
| 320 |
+
**The combination is the innovation, but combinations are easy to copy.**
|
| 321 |
+
|
| 322 |
+
## The 10/10 Moat: Data Flywheel
|
| 323 |
+
|
| 324 |
+
### The Insight
|
| 325 |
+
|
| 326 |
+
True moats in infrastructure come from:
|
| 327 |
+
1. **Network effects** - More users = better product
|
| 328 |
+
2. **Data moats** - Proprietary data that improves over time
|
| 329 |
+
3. **Integration depth** - Becomes part of the stack
|
| 330 |
+
4. **Ecosystem** - Others build on top of you
|
| 331 |
+
|
| 332 |
+
**The killer moat: A compression model trained on real agent data.**
|
| 333 |
+
|
| 334 |
+
### Phase 1: Aggregate Tool Intelligence (Months 1-6)
|
| 335 |
+
|
| 336 |
+
**Collect anonymized statistics across all users:**
|
| 337 |
+
|
| 338 |
+
```python
|
| 339 |
+
@dataclass
|
| 340 |
+
class AnonymizedToolStats:
|
| 341 |
+
"""Privacy-preserving tool statistics."""
|
| 342 |
+
|
| 343 |
+
tool_signature: str # Hash of tool name + schema
|
| 344 |
+
|
| 345 |
+
# Field patterns (no actual values)
|
| 346 |
+
field_types: dict[str, str] # {"status": "categorical", "count": "numeric"}
|
| 347 |
+
field_distributions: dict # {"status": {"unique_ratio": 0.05}}
|
| 348 |
+
|
| 349 |
+
# Compression patterns
|
| 350 |
+
avg_compression_ratio: float
|
| 351 |
+
avg_retrieval_rate: float
|
| 352 |
+
successful_strategies: list[str]
|
| 353 |
+
|
| 354 |
+
# Query patterns (no actual queries)
|
| 355 |
+
common_query_patterns: list[str] # ["field:*", "status:error"]
|
| 356 |
+
queried_field_frequency: dict # {"status": 0.8, "id": 0.3}
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
**Build the "Tool Intelligence Database":**
|
| 360 |
+
|
| 361 |
+
```python
|
| 362 |
+
class ToolIntelligenceDB:
|
| 363 |
+
"""Cross-user intelligence about tool outputs."""
|
| 364 |
+
|
| 365 |
+
def get_profile(self, tool_signature: str) -> ToolCompressionProfile:
|
| 366 |
+
"""Get compression profile based on aggregate data."""
|
| 367 |
+
stats = self._aggregate_stats(tool_signature)
|
| 368 |
+
|
| 369 |
+
return ToolCompressionProfile(
|
| 370 |
+
critical_fields=stats.get_frequently_queried_fields(),
|
| 371 |
+
min_items=stats.get_safe_compression_target(),
|
| 372 |
+
skip_conditions=stats.get_high_retrieval_scenarios(),
|
| 373 |
+
confidence=stats.sample_size / 1000, # More data = more confidence
|
| 374 |
+
)
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
**The moat**: "We've seen 10M GitHub API responses. We know exactly what to compress."
|
| 378 |
+
|
| 379 |
+
### Phase 2: Train Compression Classifier (Months 6-12)
|
| 380 |
+
|
| 381 |
+
**Use aggregate data to train a small, fast model:**
|
| 382 |
+
|
| 383 |
+
```python
|
| 384 |
+
class CompressionClassifier:
|
| 385 |
+
"""Learned compression decision model."""
|
| 386 |
+
|
| 387 |
+
def __init__(self, model_path: str):
|
| 388 |
+
# Small transformer (~50M params) fine-tuned on compression decisions
|
| 389 |
+
self.model = load_model(model_path)
|
| 390 |
+
|
| 391 |
+
def predict(self,
|
| 392 |
+
tool_stats: ToolStats,
|
| 393 |
+
user_query: str,
|
| 394 |
+
sample_items: list[dict]) -> CompressionDecision:
|
| 395 |
+
"""Predict optimal compression strategy."""
|
| 396 |
+
|
| 397 |
+
# Encode input
|
| 398 |
+
features = self._encode_features(tool_stats, user_query, sample_items)
|
| 399 |
+
|
| 400 |
+
# Predict
|
| 401 |
+
output = self.model(features)
|
| 402 |
+
|
| 403 |
+
return CompressionDecision(
|
| 404 |
+
should_compress=output.compress_probability > 0.7,
|
| 405 |
+
strategy=output.best_strategy,
|
| 406 |
+
target_items=output.target_items,
|
| 407 |
+
preserve_fields=output.preserve_fields,
|
| 408 |
+
confidence=output.confidence,
|
| 409 |
+
)
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
**Training data (from aggregate stats):**
|
| 413 |
+
|
| 414 |
+
| Input | Output | Label Source |
|
| 415 |
+
|-------|--------|--------------|
|
| 416 |
+
| Tool stats + query + sample items | Compression decision | Retrieval rate feedback |
|
| 417 |
+
| High unique_ratio + no score field | SKIP | High retrieval rate |
|
| 418 |
+
| Score field + analytical query | TOP_N | Low retrieval rate |
|
| 419 |
+
| Error keywords in query | PRESERVE_ERRORS | Query pattern analysis |
|
| 420 |
+
|
| 421 |
+
**The moat**: Model trained on proprietary data. Competitors start at zero.
|
| 422 |
+
|
| 423 |
+
### Phase 3: Ecosystem Lock-in (Months 12-24)
|
| 424 |
+
|
| 425 |
+
**Deep integration with agent frameworks:**
|
| 426 |
+
|
| 427 |
+
```python
|
| 428 |
+
# LangChain official integration
|
| 429 |
+
from langchain_headroom import HeadroomCache
|
| 430 |
+
|
| 431 |
+
llm = ChatOpenAI(cache=HeadroomCache()) # Just works
|
| 432 |
+
|
| 433 |
+
# LlamaIndex official integration
|
| 434 |
+
from llama_index.headroom import HeadroomContextManager
|
| 435 |
+
|
| 436 |
+
index = VectorStoreIndex(context_manager=HeadroomContextManager())
|
| 437 |
+
|
| 438 |
+
# CrewAI official integration
|
| 439 |
+
from crewai_headroom import HeadroomCrew
|
| 440 |
+
|
| 441 |
+
crew = HeadroomCrew(agents=[...]) # Auto-optimizes all agents
|
| 442 |
+
```
|
| 443 |
+
|
| 444 |
+
**Build ecosystem on top:**
|
| 445 |
+
|
| 446 |
+
| Component | What It Does | Lock-in |
|
| 447 |
+
|-----------|--------------|---------|
|
| 448 |
+
| Headroom Dashboard | Visualize context usage | Analytics dependency |
|
| 449 |
+
| Headroom MCP | Universal agent optimization | Protocol dependency |
|
| 450 |
+
| Headroom VS Code | IDE integration | Developer workflow |
|
| 451 |
+
| Headroom Profiles | Community tool profiles | Content lock-in |
|
| 452 |
+
|
| 453 |
+
### The Data Flywheel
|
| 454 |
+
|
| 455 |
+
```
|
| 456 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 457 |
+
│ MORE USERS │
|
| 458 |
+
└──────────────────────────────────────────────────────────────┘
|
| 459 |
+
│
|
| 460 |
+
▼
|
| 461 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 462 |
+
│ MORE TOOL OUTPUT DATA │
|
| 463 |
+
│ (anonymized stats, retrieval patterns, query patterns) │
|
| 464 |
+
└──────────────────────────────────────────────────────────────┘
|
| 465 |
+
│
|
| 466 |
+
▼
|
| 467 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 468 |
+
│ BETTER COMPRESSION MODEL │
|
| 469 |
+
│ (trained on more data, more tool types, more scenarios) │
|
| 470 |
+
└──────────────────────────────────────────────────────────────┘
|
| 471 |
+
│
|
| 472 |
+
▼
|
| 473 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 474 |
+
│ BETTER COMPRESSION QUALITY │
|
| 475 |
+
│ (higher accuracy, fewer retrievals, more savings) │
|
| 476 |
+
└──────────────────────────────────────────────────────────────┘
|
| 477 |
+
│
|
| 478 |
+
▼
|
| 479 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 480 |
+
│ MORE USERS │
|
| 481 |
+
│ (word of mouth, better benchmarks, lower churn) │
|
| 482 |
+
└──────────────────────────────────────────────────────────────┘
|
| 483 |
+
│
|
| 484 |
+
└──────────────► (cycle repeats)
|
| 485 |
+
```
|
| 486 |
+
|
| 487 |
+
**This is the moat.** Every user makes the product better for every other user. Competitors can't replicate without the data.
|
| 488 |
+
|
| 489 |
+
### Action Items
|
| 490 |
+
|
| 491 |
+
- [ ] Design privacy-preserving telemetry system
|
| 492 |
+
- [ ] Build Tool Intelligence aggregation pipeline
|
| 493 |
+
- [ ] Define compression classifier architecture
|
| 494 |
+
- [ ] Create training data collection from feedback loop
|
| 495 |
+
- [ ] Plan framework partnership outreach
|
| 496 |
+
|
| 497 |
+
---
|
| 498 |
+
|
| 499 |
+
# Dimension 4: Market Timing (9 → 10)
|
| 500 |
+
|
| 501 |
+
## Current State (9/10)
|
| 502 |
+
|
| 503 |
+
Timing is good - AI agent explosion is happening. But are we POSITIONED to capture it?
|
| 504 |
+
|
| 505 |
+
## The 10/10 Positioning
|
| 506 |
+
|
| 507 |
+
### Strategy 1: Be First in the "Context Optimization" Category
|
| 508 |
+
|
| 509 |
+
**Create the category:**
|
| 510 |
+
- "Context Optimization" as a must-have layer
|
| 511 |
+
- Every serious AI agent needs it
|
| 512 |
+
- Headroom = the default choice
|
| 513 |
+
|
| 514 |
+
**Content to publish:**
|
| 515 |
+
- "The Context Crisis: Why AI Agents Are Hitting Walls"
|
| 516 |
+
- "Context Engineering Best Practices" (become the authority)
|
| 517 |
+
- Benchmark suite for context optimization
|
| 518 |
+
|
| 519 |
+
### Strategy 2: Partner with Major Frameworks
|
| 520 |
+
|
| 521 |
+
| Framework | Status | Action |
|
| 522 |
+
|-----------|--------|--------|
|
| 523 |
+
| LangChain | Large user base | Official integration PR |
|
| 524 |
+
| LlamaIndex | Growing fast | Partnership discussion |
|
| 525 |
+
| CrewAI | Focused on agents | Perfect fit - reach out |
|
| 526 |
+
| Claude Code | Anthropic's CLI | We're already here! |
|
| 527 |
+
| Cursor | Popular IDE | Plugin opportunity |
|
| 528 |
+
|
| 529 |
+
### Strategy 3: Launch with Major Players
|
| 530 |
+
|
| 531 |
+
**Target announcements:**
|
| 532 |
+
- "Headroom powers context optimization for [Major Agent Company]"
|
| 533 |
+
- "LangChain officially recommends Headroom for production agents"
|
| 534 |
+
- "Anthropic's Claude Code uses Headroom for context management"
|
| 535 |
+
|
| 536 |
+
### Strategy 4: Open Source Dominance
|
| 537 |
+
|
| 538 |
+
**Make Headroom the "nginx of context optimization":**
|
| 539 |
+
- Core is free and open source
|
| 540 |
+
- Enterprise features are paid
|
| 541 |
+
- Community contributions
|
| 542 |
+
- Apache 2.0 license
|
| 543 |
+
|
| 544 |
+
**The playbook:**
|
| 545 |
+
1. Be the obvious open source choice
|
| 546 |
+
2. Capture developer mindshare
|
| 547 |
+
3. Enterprise upsells for advanced features
|
| 548 |
+
|
| 549 |
+
### Action Items
|
| 550 |
+
|
| 551 |
+
- [ ] Create "Context Optimization" category content
|
| 552 |
+
- [ ] Reach out to LangChain for official integration
|
| 553 |
+
- [ ] Publish benchmark suite
|
| 554 |
+
- [ ] Plan launch announcements
|
| 555 |
+
|
| 556 |
+
---
|
| 557 |
+
|
| 558 |
+
# The 10/10 Roadmap
|
| 559 |
+
|
| 560 |
+
## Phase 1: Foundation (Now - Month 3)
|
| 561 |
+
|
| 562 |
+
| Goal | Action | Metric |
|
| 563 |
+
|------|--------|--------|
|
| 564 |
+
| Solution Fit 8/10 | Implement task classification + confidence gating | Retrieval rate < 10% |
|
| 565 |
+
| Technical Moat 7/10 | Launch telemetry + Tool Intelligence DB | 1M+ data points |
|
| 566 |
+
| Market Timing 10/10 | LangChain integration + category content | Integration shipped |
|
| 567 |
+
|
| 568 |
+
**Key deliverables:**
|
| 569 |
+
- TaskClassifier with exhaustive/lookup/analytical detection
|
| 570 |
+
- Confidence-gated compression
|
| 571 |
+
- Privacy-preserving telemetry
|
| 572 |
+
- LangChain official integration
|
| 573 |
+
- "Context Optimization" blog series
|
| 574 |
+
|
| 575 |
+
## Phase 2: Data Flywheel (Month 3 - Month 9)
|
| 576 |
+
|
| 577 |
+
| Goal | Action | Metric |
|
| 578 |
+
|------|--------|--------|
|
| 579 |
+
| Solution Fit 9/10 | Learned compression profiles per tool | 100+ tool profiles |
|
| 580 |
+
| Technical Moat 8/10 | Train v1 compression classifier | 5% better than heuristics |
|
| 581 |
+
| Problem Validity 10/10 | Publish "impossible without Headroom" demos | 3 viral demos |
|
| 582 |
+
|
| 583 |
+
**Key deliverables:**
|
| 584 |
+
- ToolCompressionProfile system with cross-user learning
|
| 585 |
+
- Compression classifier v1 (small transformer)
|
| 586 |
+
- Semantic injection for CCR
|
| 587 |
+
- CrewAI + LlamaIndex integrations
|
| 588 |
+
- Demo: "This agent workflow is impossible without Headroom"
|
| 589 |
+
|
| 590 |
+
## Phase 3: Moat (Month 9 - Month 18)
|
| 591 |
+
|
| 592 |
+
| Goal | Action | Metric |
|
| 593 |
+
|------|--------|--------|
|
| 594 |
+
| Solution Fit 10/10 | Compression classifier v2 | Retrieval rate < 5% |
|
| 595 |
+
| Technical Moat 10/10 | Data flywheel operational | 100M+ data points |
|
| 596 |
+
| Overall 10/10 | Category leader | #1 in benchmarks |
|
| 597 |
+
|
| 598 |
+
**Key deliverables:**
|
| 599 |
+
- Compression classifier v2 (trained on 100M+ samples)
|
| 600 |
+
- Headroom Dashboard (analytics product)
|
| 601 |
+
- Enterprise partnerships
|
| 602 |
+
- Community tool profile contributions
|
| 603 |
+
- Category ownership: "Context Optimization"
|
| 604 |
+
|
| 605 |
+
---
|
| 606 |
+
|
| 607 |
+
# The 10/10 Vision
|
| 608 |
+
|
| 609 |
+
## From Today's Headroom
|
| 610 |
+
|
| 611 |
+
```
|
| 612 |
+
"A smart compression layer that saves you tokens"
|
| 613 |
+
```
|
| 614 |
+
|
| 615 |
+
## To Tomorrow's Headroom
|
| 616 |
+
|
| 617 |
+
```
|
| 618 |
+
"The Context Intelligence Platform for AI Applications"
|
| 619 |
+
|
| 620 |
+
We don't just compress - we UNDERSTAND context.
|
| 621 |
+
- What's in your context?
|
| 622 |
+
- What does your agent need?
|
| 623 |
+
- What's the optimal representation?
|
| 624 |
+
- How do we learn and improve?
|
| 625 |
+
|
| 626 |
+
Every agent needs context intelligence.
|
| 627 |
+
Headroom is context intelligence.
|
| 628 |
+
```
|
| 629 |
+
|
| 630 |
+
## The End State
|
| 631 |
+
|
| 632 |
+
| Dimension | Score | How |
|
| 633 |
+
|-----------|-------|-----|
|
| 634 |
+
| Problem validity | 10/10 | "Enables capabilities impossible without us" |
|
| 635 |
+
| Solution fit | 10/10 | Task-aware + learned profiles + seamless CCR |
|
| 636 |
+
| Technical moat | 10/10 | Compression model trained on 100M+ samples |
|
| 637 |
+
| Market timing | 10/10 | Category leader, framework default |
|
| 638 |
+
| **Overall** | **10/10** | **The context layer for AI** |
|
| 639 |
+
|
| 640 |
+
---
|
| 641 |
+
|
| 642 |
+
# Summary: The Three Big Moves
|
| 643 |
+
|
| 644 |
+
## Move 1: From Cost Savings to Capability Enablement
|
| 645 |
+
|
| 646 |
+
**Before**: "Save 50-90% on tokens"
|
| 647 |
+
**After**: "Enable agent capabilities that are impossible without context optimization"
|
| 648 |
+
|
| 649 |
+
## Move 2: From Heuristics to Learned Intelligence
|
| 650 |
+
|
| 651 |
+
**Before**: Statistical heuristics that work 70% of the time
|
| 652 |
+
**After**: Task-aware, confidence-gated, profile-guided compression that learns from every interaction
|
| 653 |
+
|
| 654 |
+
## Move 3: From Tool to Platform
|
| 655 |
+
|
| 656 |
+
**Before**: A compression library you can use
|
| 657 |
+
**After**: The context intelligence layer that every serious AI application needs
|
| 658 |
+
|
| 659 |
+
---
|
| 660 |
+
|
| 661 |
+
**The bottom line**: 10/10 isn't about perfecting what we have. It's about building a data flywheel that makes the product better with every user, creating capabilities that are impossible without us, and owning the "Context Intelligence" category before anyone else does.
|
examples/anthropic_example.py
CHANGED
|
@@ -80,7 +80,9 @@ def example_optimize_mode():
|
|
| 80 |
{
|
| 81 |
"type": "tool_result",
|
| 82 |
"tool_use_id": "call_1",
|
| 83 |
-
"content": '{"results": ['
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
],
|
| 86 |
},
|
|
@@ -125,7 +127,9 @@ def example_simulate_mode():
|
|
| 125 |
{
|
| 126 |
"type": "tool_result",
|
| 127 |
"tool_use_id": "call_1",
|
| 128 |
-
"content": '{"results": ['
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
],
|
| 131 |
},
|
|
|
|
| 80 |
{
|
| 81 |
"type": "tool_result",
|
| 82 |
"tool_use_id": "call_1",
|
| 83 |
+
"content": '{"results": ['
|
| 84 |
+
+ ",".join([f'{{"id": {i}}}' for i in range(50)])
|
| 85 |
+
+ "]}",
|
| 86 |
}
|
| 87 |
],
|
| 88 |
},
|
|
|
|
| 127 |
{
|
| 128 |
"type": "tool_result",
|
| 129 |
"tool_use_id": "call_1",
|
| 130 |
+
"content": '{"results": ['
|
| 131 |
+
+ ",".join([f'{{"id": {i}}}' for i in range(100)])
|
| 132 |
+
+ "]}",
|
| 133 |
}
|
| 134 |
],
|
| 135 |
},
|
examples/langchain_before_after.py
CHANGED
|
@@ -21,18 +21,19 @@ import os
|
|
| 21 |
import tempfile
|
| 22 |
import time
|
| 23 |
from dataclasses import dataclass
|
| 24 |
-
from datetime import datetime
|
| 25 |
|
| 26 |
# Check dependencies
|
| 27 |
try:
|
| 28 |
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
|
|
|
| 29 |
LANGCHAIN_AVAILABLE = True
|
| 30 |
except ImportError:
|
| 31 |
LANGCHAIN_AVAILABLE = False
|
| 32 |
print("LangChain not installed. Install with: pip install langchain-core")
|
| 33 |
|
| 34 |
try:
|
| 35 |
-
from langchain_openai import ChatOpenAI
|
|
|
|
| 36 |
OPENAI_AVAILABLE = True
|
| 37 |
except ImportError:
|
| 38 |
OPENAI_AVAILABLE = False
|
|
@@ -40,12 +41,13 @@ except ImportError:
|
|
| 40 |
|
| 41 |
# Import Headroom
|
| 42 |
try:
|
| 43 |
-
from headroom import (
|
| 44 |
HeadroomClient,
|
| 45 |
HeadroomConfig,
|
| 46 |
HeadroomMode,
|
| 47 |
OpenAIProvider,
|
| 48 |
)
|
|
|
|
| 49 |
HEADROOM_AVAILABLE = True
|
| 50 |
except ImportError:
|
| 51 |
HEADROOM_AVAILABLE = False
|
|
@@ -55,6 +57,7 @@ except ImportError:
|
|
| 55 |
@dataclass
|
| 56 |
class ComparisonResult:
|
| 57 |
"""Result of before/after comparison."""
|
|
|
|
| 58 |
scenario: str
|
| 59 |
tokens_before: int
|
| 60 |
tokens_after: int
|
|
@@ -82,18 +85,18 @@ def print_comparison(result: ComparisonResult) -> None:
|
|
| 82 |
print(f"\n{'=' * 60}")
|
| 83 |
print(f"Scenario: {result.scenario}")
|
| 84 |
print(f"{'=' * 60}")
|
| 85 |
-
print(
|
| 86 |
print(f" Before: {result.tokens_before:,} tokens")
|
| 87 |
print(f" After: {result.tokens_after:,} tokens")
|
| 88 |
print(f" Saved: {result.tokens_saved:,} tokens ({result.savings_percent:.1f}%)")
|
| 89 |
|
| 90 |
-
print(
|
| 91 |
print(f" Before: ${result.cost_before_usd:.4f}")
|
| 92 |
print(f" After: ${result.cost_after_usd:.4f}")
|
| 93 |
print(f" Saved: ${result.cost_saved_usd:.4f}")
|
| 94 |
|
| 95 |
if result.latency_before_ms and result.latency_after_ms:
|
| 96 |
-
print(
|
| 97 |
print(f" Before: {result.latency_before_ms:.0f}ms")
|
| 98 |
print(f" After: {result.latency_after_ms:.0f}ms")
|
| 99 |
|
|
@@ -122,11 +125,13 @@ def langchain_to_openai_messages(messages: list) -> list[dict]:
|
|
| 122 |
]
|
| 123 |
openai_messages.append(msg_dict)
|
| 124 |
elif isinstance(msg, ToolMessage):
|
| 125 |
-
openai_messages.append(
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
return openai_messages
|
| 131 |
|
| 132 |
|
|
@@ -134,6 +139,7 @@ def langchain_to_openai_messages(messages: list) -> list[dict]:
|
|
| 134 |
# SCENARIO 1: Agentic Workflow with Large Tool Outputs
|
| 135 |
# ============================================================================
|
| 136 |
|
|
|
|
| 137 |
def scenario_agentic_workflow() -> ComparisonResult:
|
| 138 |
"""
|
| 139 |
Scenario: AI agent that searches a database and processes results.
|
|
@@ -158,20 +164,24 @@ def scenario_agentic_workflow() -> ComparisonResult:
|
|
| 158 |
"metadata": {
|
| 159 |
"preferences": {"theme": "dark", "notifications": True},
|
| 160 |
"tags": ["premium", "verified"] if i % 5 == 0 else [],
|
| 161 |
-
}
|
| 162 |
}
|
| 163 |
for i in range(100)
|
| 164 |
]
|
| 165 |
|
| 166 |
# The conversation in LangChain format
|
| 167 |
lc_messages = [
|
| 168 |
-
SystemMessage(
|
|
|
|
| 169 |
When searching for users, analyze the results and provide a summary.
|
| 170 |
-
Focus on active users in the Engineering department."""
|
|
|
|
| 171 |
HumanMessage(content="Find users in the Engineering department"),
|
| 172 |
AIMessage(
|
| 173 |
content="I'll search the database for Engineering users.",
|
| 174 |
-
tool_calls=[
|
|
|
|
|
|
|
| 175 |
),
|
| 176 |
ToolMessage(
|
| 177 |
content=json.dumps(search_results), # 100 records!
|
|
@@ -207,13 +217,13 @@ def scenario_agentic_workflow() -> ComparisonResult:
|
|
| 207 |
tokens_saved = plan.tokens_saved
|
| 208 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 209 |
|
| 210 |
-
print(
|
| 211 |
-
print(
|
| 212 |
print(f" - Tool output: 100 user records ({len(json.dumps(search_results))} chars)")
|
| 213 |
|
| 214 |
-
print(
|
| 215 |
-
print(
|
| 216 |
-
print(
|
| 217 |
print(f" - Transforms: {plan.transforms}")
|
| 218 |
|
| 219 |
client.close()
|
|
@@ -236,6 +246,7 @@ def scenario_agentic_workflow() -> ComparisonResult:
|
|
| 236 |
# SCENARIO 2: Long Conversation with Context Window Pressure
|
| 237 |
# ============================================================================
|
| 238 |
|
|
|
|
| 239 |
def scenario_long_conversation() -> ComparisonResult:
|
| 240 |
"""
|
| 241 |
Scenario: Multi-turn conversation approaching context window limit.
|
|
@@ -249,7 +260,8 @@ def scenario_long_conversation() -> ComparisonResult:
|
|
| 249 |
|
| 250 |
# Simulate 50-turn conversation in LangChain format
|
| 251 |
lc_messages = [
|
| 252 |
-
SystemMessage(
|
|
|
|
| 253 |
You have access to customer data and can help with:
|
| 254 |
- Account issues
|
| 255 |
- Billing questions
|
|
@@ -258,7 +270,8 @@ def scenario_long_conversation() -> ComparisonResult:
|
|
| 258 |
|
| 259 |
Current date: 2024-12-15
|
| 260 |
Agent ID: support-agent-42
|
| 261 |
-
"""
|
|
|
|
| 262 |
]
|
| 263 |
|
| 264 |
# Add 50 turns of conversation
|
|
@@ -273,10 +286,12 @@ def scenario_long_conversation() -> ComparisonResult:
|
|
| 273 |
for i in range(50):
|
| 274 |
topic = topics[i % len(topics)]
|
| 275 |
lc_messages.append(HumanMessage(content=f"Turn {i}: {topic}"))
|
| 276 |
-
lc_messages.append(
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
| 280 |
|
| 281 |
# Convert to OpenAI format
|
| 282 |
messages = langchain_to_openai_messages(lc_messages)
|
|
@@ -306,13 +321,13 @@ def scenario_long_conversation() -> ComparisonResult:
|
|
| 306 |
tokens_saved = plan.tokens_saved
|
| 307 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 308 |
|
| 309 |
-
print(
|
| 310 |
-
print(
|
| 311 |
print(f" - ~{tokens_before:,} tokens total")
|
| 312 |
|
| 313 |
-
print(
|
| 314 |
-
print(
|
| 315 |
-
print(
|
| 316 |
print(f" - Transforms: {plan.transforms}")
|
| 317 |
|
| 318 |
client.close()
|
|
@@ -335,6 +350,7 @@ def scenario_long_conversation() -> ComparisonResult:
|
|
| 335 |
# SCENARIO 3: RAG with Retrieved Documents
|
| 336 |
# ============================================================================
|
| 337 |
|
|
|
|
| 338 |
def scenario_rag_pipeline() -> ComparisonResult:
|
| 339 |
"""
|
| 340 |
Scenario: RAG pipeline that retrieves multiple documents.
|
|
@@ -358,23 +374,24 @@ def scenario_rag_pipeline() -> ComparisonResult:
|
|
| 358 |
"author": f"Author {i}",
|
| 359 |
"date": "2024-01-15",
|
| 360 |
"category": "Technical",
|
| 361 |
-
}
|
| 362 |
}
|
| 363 |
chunks.append(chunk)
|
| 364 |
|
| 365 |
-
context = "\n\n".join(
|
| 366 |
-
f"[Source: {c['source']}, Page {c['page']}]\n{c['content']}"
|
| 367 |
-
|
| 368 |
-
])
|
| 369 |
|
| 370 |
# LangChain format
|
| 371 |
lc_messages = [
|
| 372 |
SystemMessage(content="You are a helpful assistant. Answer based on the provided context."),
|
| 373 |
-
HumanMessage(
|
|
|
|
| 374 |
|
| 375 |
{context}
|
| 376 |
|
| 377 |
-
Question: What are the key technical requirements?"""
|
|
|
|
| 378 |
]
|
| 379 |
|
| 380 |
# Convert to OpenAI format
|
|
@@ -405,12 +422,12 @@ Question: What are the key technical requirements?"""),
|
|
| 405 |
tokens_saved = plan.tokens_saved
|
| 406 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 407 |
|
| 408 |
-
print(
|
| 409 |
-
print(
|
| 410 |
print(f" - ~{tokens_before:,} tokens total")
|
| 411 |
|
| 412 |
-
print(
|
| 413 |
-
print(
|
| 414 |
print(f" - Transforms: {plan.transforms}")
|
| 415 |
|
| 416 |
client.close()
|
|
@@ -433,6 +450,7 @@ Question: What are the key technical requirements?"""),
|
|
| 433 |
# SCENARIO 4: Real API Comparison (if API key available)
|
| 434 |
# ============================================================================
|
| 435 |
|
|
|
|
| 436 |
def scenario_live_api() -> ComparisonResult | None:
|
| 437 |
"""
|
| 438 |
Scenario: Live API comparison with actual timing.
|
|
@@ -497,7 +515,7 @@ def scenario_live_api() -> ComparisonResult | None:
|
|
| 497 |
print(f"\n[Latency] {latency_before:.0f}ms -> {latency_after:.0f}ms")
|
| 498 |
|
| 499 |
# Get metrics
|
| 500 |
-
|
| 501 |
|
| 502 |
headroom_client.close()
|
| 503 |
|
|
@@ -524,6 +542,7 @@ def scenario_live_api() -> ComparisonResult | None:
|
|
| 524 |
# MAIN: Run All Scenarios
|
| 525 |
# ============================================================================
|
| 526 |
|
|
|
|
| 527 |
def main():
|
| 528 |
"""Run all comparison scenarios."""
|
| 529 |
print("\n" + "=" * 70)
|
|
@@ -585,7 +604,7 @@ def main():
|
|
| 585 |
print(f" Total tokens saved: {total_saved:,}")
|
| 586 |
print(f" Average savings: {avg_savings:.1f}%")
|
| 587 |
print(f" Total cost saved: ${total_cost_saved:.4f}")
|
| 588 |
-
print(
|
| 589 |
print(f" Estimated monthly savings: ${total_cost_saved * 1_000_000 / len(results):,.2f}")
|
| 590 |
|
| 591 |
|
|
|
|
| 21 |
import tempfile
|
| 22 |
import time
|
| 23 |
from dataclasses import dataclass
|
|
|
|
| 24 |
|
| 25 |
# Check dependencies
|
| 26 |
try:
|
| 27 |
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
| 28 |
+
|
| 29 |
LANGCHAIN_AVAILABLE = True
|
| 30 |
except ImportError:
|
| 31 |
LANGCHAIN_AVAILABLE = False
|
| 32 |
print("LangChain not installed. Install with: pip install langchain-core")
|
| 33 |
|
| 34 |
try:
|
| 35 |
+
from langchain_openai import ChatOpenAI # noqa: F401
|
| 36 |
+
|
| 37 |
OPENAI_AVAILABLE = True
|
| 38 |
except ImportError:
|
| 39 |
OPENAI_AVAILABLE = False
|
|
|
|
| 41 |
|
| 42 |
# Import Headroom
|
| 43 |
try:
|
| 44 |
+
from headroom import ( # noqa: F401
|
| 45 |
HeadroomClient,
|
| 46 |
HeadroomConfig,
|
| 47 |
HeadroomMode,
|
| 48 |
OpenAIProvider,
|
| 49 |
)
|
| 50 |
+
|
| 51 |
HEADROOM_AVAILABLE = True
|
| 52 |
except ImportError:
|
| 53 |
HEADROOM_AVAILABLE = False
|
|
|
|
| 57 |
@dataclass
|
| 58 |
class ComparisonResult:
|
| 59 |
"""Result of before/after comparison."""
|
| 60 |
+
|
| 61 |
scenario: str
|
| 62 |
tokens_before: int
|
| 63 |
tokens_after: int
|
|
|
|
| 85 |
print(f"\n{'=' * 60}")
|
| 86 |
print(f"Scenario: {result.scenario}")
|
| 87 |
print(f"{'=' * 60}")
|
| 88 |
+
print("\n[Token Comparison]")
|
| 89 |
print(f" Before: {result.tokens_before:,} tokens")
|
| 90 |
print(f" After: {result.tokens_after:,} tokens")
|
| 91 |
print(f" Saved: {result.tokens_saved:,} tokens ({result.savings_percent:.1f}%)")
|
| 92 |
|
| 93 |
+
print("\n[Cost Impact] (GPT-4o pricing)")
|
| 94 |
print(f" Before: ${result.cost_before_usd:.4f}")
|
| 95 |
print(f" After: ${result.cost_after_usd:.4f}")
|
| 96 |
print(f" Saved: ${result.cost_saved_usd:.4f}")
|
| 97 |
|
| 98 |
if result.latency_before_ms and result.latency_after_ms:
|
| 99 |
+
print("\n[Latency]")
|
| 100 |
print(f" Before: {result.latency_before_ms:.0f}ms")
|
| 101 |
print(f" After: {result.latency_after_ms:.0f}ms")
|
| 102 |
|
|
|
|
| 125 |
]
|
| 126 |
openai_messages.append(msg_dict)
|
| 127 |
elif isinstance(msg, ToolMessage):
|
| 128 |
+
openai_messages.append(
|
| 129 |
+
{
|
| 130 |
+
"role": "tool",
|
| 131 |
+
"tool_call_id": msg.tool_call_id,
|
| 132 |
+
"content": msg.content,
|
| 133 |
+
}
|
| 134 |
+
)
|
| 135 |
return openai_messages
|
| 136 |
|
| 137 |
|
|
|
|
| 139 |
# SCENARIO 1: Agentic Workflow with Large Tool Outputs
|
| 140 |
# ============================================================================
|
| 141 |
|
| 142 |
+
|
| 143 |
def scenario_agentic_workflow() -> ComparisonResult:
|
| 144 |
"""
|
| 145 |
Scenario: AI agent that searches a database and processes results.
|
|
|
|
| 164 |
"metadata": {
|
| 165 |
"preferences": {"theme": "dark", "notifications": True},
|
| 166 |
"tags": ["premium", "verified"] if i % 5 == 0 else [],
|
| 167 |
+
},
|
| 168 |
}
|
| 169 |
for i in range(100)
|
| 170 |
]
|
| 171 |
|
| 172 |
# The conversation in LangChain format
|
| 173 |
lc_messages = [
|
| 174 |
+
SystemMessage(
|
| 175 |
+
content="""You are a helpful database assistant.
|
| 176 |
When searching for users, analyze the results and provide a summary.
|
| 177 |
+
Focus on active users in the Engineering department."""
|
| 178 |
+
),
|
| 179 |
HumanMessage(content="Find users in the Engineering department"),
|
| 180 |
AIMessage(
|
| 181 |
content="I'll search the database for Engineering users.",
|
| 182 |
+
tool_calls=[
|
| 183 |
+
{"id": "call_1", "name": "search_users", "args": {"department": "Engineering"}}
|
| 184 |
+
],
|
| 185 |
),
|
| 186 |
ToolMessage(
|
| 187 |
content=json.dumps(search_results), # 100 records!
|
|
|
|
| 217 |
tokens_saved = plan.tokens_saved
|
| 218 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 219 |
|
| 220 |
+
print("\n[Before Optimization]")
|
| 221 |
+
print(" - System prompt + conversation")
|
| 222 |
print(f" - Tool output: 100 user records ({len(json.dumps(search_results))} chars)")
|
| 223 |
|
| 224 |
+
print("\n[After Optimization]")
|
| 225 |
+
print(" - SmartCrusher kept: first 3, last 2, + relevance matches")
|
| 226 |
+
print(" - Estimated ~15 items preserved (Engineering dept matches)")
|
| 227 |
print(f" - Transforms: {plan.transforms}")
|
| 228 |
|
| 229 |
client.close()
|
|
|
|
| 246 |
# SCENARIO 2: Long Conversation with Context Window Pressure
|
| 247 |
# ============================================================================
|
| 248 |
|
| 249 |
+
|
| 250 |
def scenario_long_conversation() -> ComparisonResult:
|
| 251 |
"""
|
| 252 |
Scenario: Multi-turn conversation approaching context window limit.
|
|
|
|
| 260 |
|
| 261 |
# Simulate 50-turn conversation in LangChain format
|
| 262 |
lc_messages = [
|
| 263 |
+
SystemMessage(
|
| 264 |
+
content="""You are a customer support agent for TechCorp.
|
| 265 |
You have access to customer data and can help with:
|
| 266 |
- Account issues
|
| 267 |
- Billing questions
|
|
|
|
| 270 |
|
| 271 |
Current date: 2024-12-15
|
| 272 |
Agent ID: support-agent-42
|
| 273 |
+
"""
|
| 274 |
+
),
|
| 275 |
]
|
| 276 |
|
| 277 |
# Add 50 turns of conversation
|
|
|
|
| 286 |
for i in range(50):
|
| 287 |
topic = topics[i % len(topics)]
|
| 288 |
lc_messages.append(HumanMessage(content=f"Turn {i}: {topic}"))
|
| 289 |
+
lc_messages.append(
|
| 290 |
+
AIMessage(
|
| 291 |
+
content=f"Response to turn {i}: Thank you for reaching out about '{topic}'. "
|
| 292 |
+
f"I can help you with that. Here's what I found... " * 3
|
| 293 |
+
)
|
| 294 |
+
)
|
| 295 |
|
| 296 |
# Convert to OpenAI format
|
| 297 |
messages = langchain_to_openai_messages(lc_messages)
|
|
|
|
| 321 |
tokens_saved = plan.tokens_saved
|
| 322 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 323 |
|
| 324 |
+
print("\n[Before Optimization]")
|
| 325 |
+
print(" - 50-turn conversation")
|
| 326 |
print(f" - ~{tokens_before:,} tokens total")
|
| 327 |
|
| 328 |
+
print("\n[After Optimization]")
|
| 329 |
+
print(" - RollingWindow kept system + last N turns")
|
| 330 |
+
print(" - CacheAligner moved date to dynamic tail")
|
| 331 |
print(f" - Transforms: {plan.transforms}")
|
| 332 |
|
| 333 |
client.close()
|
|
|
|
| 350 |
# SCENARIO 3: RAG with Retrieved Documents
|
| 351 |
# ============================================================================
|
| 352 |
|
| 353 |
+
|
| 354 |
def scenario_rag_pipeline() -> ComparisonResult:
|
| 355 |
"""
|
| 356 |
Scenario: RAG pipeline that retrieves multiple documents.
|
|
|
|
| 374 |
"author": f"Author {i}",
|
| 375 |
"date": "2024-01-15",
|
| 376 |
"category": "Technical",
|
| 377 |
+
},
|
| 378 |
}
|
| 379 |
chunks.append(chunk)
|
| 380 |
|
| 381 |
+
context = "\n\n".join(
|
| 382 |
+
[f"[Source: {c['source']}, Page {c['page']}]\n{c['content']}" for c in chunks]
|
| 383 |
+
)
|
|
|
|
| 384 |
|
| 385 |
# LangChain format
|
| 386 |
lc_messages = [
|
| 387 |
SystemMessage(content="You are a helpful assistant. Answer based on the provided context."),
|
| 388 |
+
HumanMessage(
|
| 389 |
+
content=f"""Based on the following retrieved documents:
|
| 390 |
|
| 391 |
{context}
|
| 392 |
|
| 393 |
+
Question: What are the key technical requirements?"""
|
| 394 |
+
),
|
| 395 |
]
|
| 396 |
|
| 397 |
# Convert to OpenAI format
|
|
|
|
| 422 |
tokens_saved = plan.tokens_saved
|
| 423 |
savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
| 424 |
|
| 425 |
+
print("\n[Before Optimization]")
|
| 426 |
+
print(" - 10 retrieved document chunks")
|
| 427 |
print(f" - ~{tokens_before:,} tokens total")
|
| 428 |
|
| 429 |
+
print("\n[After Optimization]")
|
| 430 |
+
print(" - CacheAligner normalized whitespace")
|
| 431 |
print(f" - Transforms: {plan.transforms}")
|
| 432 |
|
| 433 |
client.close()
|
|
|
|
| 450 |
# SCENARIO 4: Real API Comparison (if API key available)
|
| 451 |
# ============================================================================
|
| 452 |
|
| 453 |
+
|
| 454 |
def scenario_live_api() -> ComparisonResult | None:
|
| 455 |
"""
|
| 456 |
Scenario: Live API comparison with actual timing.
|
|
|
|
| 515 |
print(f"\n[Latency] {latency_before:.0f}ms -> {latency_after:.0f}ms")
|
| 516 |
|
| 517 |
# Get metrics
|
| 518 |
+
headroom_client.get_summary()
|
| 519 |
|
| 520 |
headroom_client.close()
|
| 521 |
|
|
|
|
| 542 |
# MAIN: Run All Scenarios
|
| 543 |
# ============================================================================
|
| 544 |
|
| 545 |
+
|
| 546 |
def main():
|
| 547 |
"""Run all comparison scenarios."""
|
| 548 |
print("\n" + "=" * 70)
|
|
|
|
| 604 |
print(f" Total tokens saved: {total_saved:,}")
|
| 605 |
print(f" Average savings: {avg_savings:.1f}%")
|
| 606 |
print(f" Total cost saved: ${total_cost_saved:.4f}")
|
| 607 |
+
print("\n[Projection] At scale (1M requests/month):")
|
| 608 |
print(f" Estimated monthly savings: ${total_cost_saved * 1_000_000 / len(results):,.2f}")
|
| 609 |
|
| 610 |
|
examples/langchain_demo/mock_tools.py
CHANGED
|
@@ -10,7 +10,6 @@ These simulate real-world API responses that benefit from Headroom compression:
|
|
| 10 |
import json
|
| 11 |
import random
|
| 12 |
from datetime import datetime, timedelta
|
| 13 |
-
from typing import Any
|
| 14 |
|
| 15 |
|
| 16 |
def generate_user_database_results(query: str, count: int = 100) -> str:
|
|
@@ -39,9 +38,11 @@ def generate_user_database_results(query: str, count: int = 100) -> str:
|
|
| 39 |
"notifications": random.choice([True, False]),
|
| 40 |
"timezone": random.choice(["UTC", "PST", "EST", "CST"]),
|
| 41 |
},
|
| 42 |
-
"tags": random.sample(
|
|
|
|
|
|
|
| 43 |
"login_count": random.randint(1, 500),
|
| 44 |
-
}
|
| 45 |
}
|
| 46 |
users.append(user)
|
| 47 |
|
|
@@ -61,8 +62,8 @@ def generate_search_results(query: str, count: int = 50) -> str:
|
|
| 61 |
result = {
|
| 62 |
"id": f"doc_{random.randint(10000, 99999)}",
|
| 63 |
"title": f"Document {i}: {query.title()} Guide",
|
| 64 |
-
"snippet": f"This document covers {query}. " * random.randint(2, 5)
|
| 65 |
-
|
| 66 |
"url": f"https://docs.example.com/{query.replace(' ', '-')}/{i}",
|
| 67 |
"category": random.choice(categories),
|
| 68 |
"relevance_score": round(random.uniform(0.5, 1.0), 3),
|
|
@@ -88,23 +89,27 @@ def generate_log_entries(service: str, count: int = 200) -> str:
|
|
| 88 |
entries = []
|
| 89 |
levels = ["DEBUG", "INFO", "INFO", "INFO", "WARN", "ERROR"] # Most are INFO
|
| 90 |
|
| 91 |
-
for
|
| 92 |
timestamp = datetime.now() - timedelta(minutes=random.randint(1, 1440))
|
| 93 |
level = random.choice(levels)
|
| 94 |
|
| 95 |
if level == "ERROR":
|
| 96 |
-
message = random.choice(
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
elif level == "WARN":
|
| 103 |
-
message = random.choice(
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
else:
|
| 109 |
message = f"Processing request {random.randint(1000, 9999)} for {service}"
|
| 110 |
|
|
@@ -120,7 +125,7 @@ def generate_log_entries(service: str, count: int = 200) -> str:
|
|
| 120 |
"request_id": f"req_{random.randint(100000, 999999)}",
|
| 121 |
"user_agent": "Mozilla/5.0" if random.random() > 0.5 else "API-Client/1.0",
|
| 122 |
"duration_ms": random.randint(1, 5000),
|
| 123 |
-
}
|
| 124 |
}
|
| 125 |
entries.append(entry)
|
| 126 |
|
|
@@ -154,7 +159,9 @@ def generate_metrics_data(service: str, count: int = 100) -> str:
|
|
| 154 |
"error_rate": random.uniform(5, 15) if is_anomaly else random.uniform(0, 1),
|
| 155 |
"latency_p50_ms": random.randint(200, 500) if is_anomaly else random.randint(10, 50),
|
| 156 |
"latency_p99_ms": random.randint(1000, 3000) if is_anomaly else random.randint(50, 200),
|
| 157 |
-
"active_connections": random.randint(500, 1000)
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
metrics.append(metric)
|
| 160 |
|
|
@@ -184,24 +191,29 @@ def generate_api_response(endpoint: str, count: int = 75) -> str:
|
|
| 184 |
"name": f"Owner {random.randint(1, 100)}",
|
| 185 |
"email": f"owner{random.randint(1, 100)}@example.com",
|
| 186 |
},
|
| 187 |
-
"tags": random.sample(
|
|
|
|
|
|
|
| 188 |
"metadata": {
|
| 189 |
"source": random.choice(["web", "api", "mobile", "import"]),
|
| 190 |
"version": f"v{random.randint(1, 5)}.{random.randint(0, 9)}",
|
| 191 |
-
}
|
| 192 |
}
|
| 193 |
items.append(item)
|
| 194 |
|
| 195 |
-
return json.dumps(
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
"
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
| 202 |
},
|
| 203 |
-
|
| 204 |
-
|
| 205 |
|
| 206 |
|
| 207 |
# Tool definitions for LangChain
|
|
@@ -217,6 +229,7 @@ TOOL_FUNCTIONS = {
|
|
| 217 |
if __name__ == "__main__":
|
| 218 |
# Test output sizes
|
| 219 |
import tiktoken
|
|
|
|
| 220 |
enc = tiktoken.get_encoding("cl100k_base")
|
| 221 |
|
| 222 |
print("Tool Output Token Counts:")
|
|
|
|
| 10 |
import json
|
| 11 |
import random
|
| 12 |
from datetime import datetime, timedelta
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
def generate_user_database_results(query: str, count: int = 100) -> str:
|
|
|
|
| 38 |
"notifications": random.choice([True, False]),
|
| 39 |
"timezone": random.choice(["UTC", "PST", "EST", "CST"]),
|
| 40 |
},
|
| 41 |
+
"tags": random.sample(
|
| 42 |
+
["premium", "verified", "beta", "enterprise"], k=random.randint(0, 3)
|
| 43 |
+
),
|
| 44 |
"login_count": random.randint(1, 500),
|
| 45 |
+
},
|
| 46 |
}
|
| 47 |
users.append(user)
|
| 48 |
|
|
|
|
| 62 |
result = {
|
| 63 |
"id": f"doc_{random.randint(10000, 99999)}",
|
| 64 |
"title": f"Document {i}: {query.title()} Guide",
|
| 65 |
+
"snippet": f"This document covers {query}. " * random.randint(2, 5)
|
| 66 |
+
+ f"Learn more about implementing {query} in your application...",
|
| 67 |
"url": f"https://docs.example.com/{query.replace(' ', '-')}/{i}",
|
| 68 |
"category": random.choice(categories),
|
| 69 |
"relevance_score": round(random.uniform(0.5, 1.0), 3),
|
|
|
|
| 89 |
entries = []
|
| 90 |
levels = ["DEBUG", "INFO", "INFO", "INFO", "WARN", "ERROR"] # Most are INFO
|
| 91 |
|
| 92 |
+
for _i in range(count):
|
| 93 |
timestamp = datetime.now() - timedelta(minutes=random.randint(1, 1440))
|
| 94 |
level = random.choice(levels)
|
| 95 |
|
| 96 |
if level == "ERROR":
|
| 97 |
+
message = random.choice(
|
| 98 |
+
[
|
| 99 |
+
f"Connection refused to {service}-db: timeout after 30s",
|
| 100 |
+
"Failed to process request: NullPointerException at line 42",
|
| 101 |
+
"Authentication failed for user: invalid token",
|
| 102 |
+
"Rate limit exceeded: 429 Too Many Requests",
|
| 103 |
+
]
|
| 104 |
+
)
|
| 105 |
elif level == "WARN":
|
| 106 |
+
message = random.choice(
|
| 107 |
+
[
|
| 108 |
+
"Slow query detected: took 2.5s",
|
| 109 |
+
"Memory usage high: 85% of heap",
|
| 110 |
+
"Retrying request after transient failure",
|
| 111 |
+
]
|
| 112 |
+
)
|
| 113 |
else:
|
| 114 |
message = f"Processing request {random.randint(1000, 9999)} for {service}"
|
| 115 |
|
|
|
|
| 125 |
"request_id": f"req_{random.randint(100000, 999999)}",
|
| 126 |
"user_agent": "Mozilla/5.0" if random.random() > 0.5 else "API-Client/1.0",
|
| 127 |
"duration_ms": random.randint(1, 5000),
|
| 128 |
+
},
|
| 129 |
}
|
| 130 |
entries.append(entry)
|
| 131 |
|
|
|
|
| 159 |
"error_rate": random.uniform(5, 15) if is_anomaly else random.uniform(0, 1),
|
| 160 |
"latency_p50_ms": random.randint(200, 500) if is_anomaly else random.randint(10, 50),
|
| 161 |
"latency_p99_ms": random.randint(1000, 3000) if is_anomaly else random.randint(50, 200),
|
| 162 |
+
"active_connections": random.randint(500, 1000)
|
| 163 |
+
if is_anomaly
|
| 164 |
+
else random.randint(50, 150),
|
| 165 |
}
|
| 166 |
metrics.append(metric)
|
| 167 |
|
|
|
|
| 191 |
"name": f"Owner {random.randint(1, 100)}",
|
| 192 |
"email": f"owner{random.randint(1, 100)}@example.com",
|
| 193 |
},
|
| 194 |
+
"tags": random.sample(
|
| 195 |
+
["urgent", "review", "approved", "blocked", "in-progress"], k=random.randint(1, 3)
|
| 196 |
+
),
|
| 197 |
"metadata": {
|
| 198 |
"source": random.choice(["web", "api", "mobile", "import"]),
|
| 199 |
"version": f"v{random.randint(1, 5)}.{random.randint(0, 9)}",
|
| 200 |
+
},
|
| 201 |
}
|
| 202 |
items.append(item)
|
| 203 |
|
| 204 |
+
return json.dumps(
|
| 205 |
+
{
|
| 206 |
+
"data": items,
|
| 207 |
+
"pagination": {
|
| 208 |
+
"page": 1,
|
| 209 |
+
"per_page": count,
|
| 210 |
+
"total": count * 10, # Simulate more pages available
|
| 211 |
+
"total_pages": 10,
|
| 212 |
+
},
|
| 213 |
+
"endpoint": endpoint,
|
| 214 |
},
|
| 215 |
+
indent=2,
|
| 216 |
+
)
|
| 217 |
|
| 218 |
|
| 219 |
# Tool definitions for LangChain
|
|
|
|
| 229 |
if __name__ == "__main__":
|
| 230 |
# Test output sizes
|
| 231 |
import tiktoken
|
| 232 |
+
|
| 233 |
enc = tiktoken.get_encoding("cl100k_base")
|
| 234 |
|
| 235 |
print("Tool Output Token Counts:")
|
examples/langchain_demo/run_comparison.py
CHANGED
|
@@ -20,7 +20,6 @@ import os
|
|
| 20 |
import sys
|
| 21 |
import time
|
| 22 |
from dataclasses import dataclass
|
| 23 |
-
from typing import Any
|
| 24 |
|
| 25 |
# Check for required dependencies
|
| 26 |
try:
|
|
@@ -30,9 +29,14 @@ except ImportError:
|
|
| 30 |
sys.exit(1)
|
| 31 |
|
| 32 |
try:
|
| 33 |
-
from langchain_core.messages import
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
except ImportError:
|
| 37 |
print("ERROR: LangChain required. Run: pip install langchain langchain-openai langchain-core")
|
| 38 |
sys.exit(1)
|
|
@@ -40,7 +44,6 @@ except ImportError:
|
|
| 40 |
# Import our mock tools
|
| 41 |
from .mock_tools import TOOL_FUNCTIONS
|
| 42 |
|
| 43 |
-
|
| 44 |
# Token counter
|
| 45 |
ENCODER = tiktoken.get_encoding("cl100k_base")
|
| 46 |
|
|
@@ -71,6 +74,7 @@ def count_message_tokens(messages: list[dict]) -> int:
|
|
| 71 |
@dataclass
|
| 72 |
class AgentRun:
|
| 73 |
"""Results from a single agent run."""
|
|
|
|
| 74 |
scenario: str
|
| 75 |
mode: str # "baseline" or "headroom"
|
| 76 |
total_input_tokens: int
|
|
@@ -185,7 +189,7 @@ def run_agent_baseline(scenario: dict, api_key: str) -> AgentRun:
|
|
| 185 |
# Count output tokens
|
| 186 |
output_tokens = count_tokens(response.content) if response.content else 0
|
| 187 |
if response.tool_calls:
|
| 188 |
-
output_tokens += count_tokens(json.dumps(
|
| 189 |
total_output_tokens += output_tokens
|
| 190 |
|
| 191 |
# Check if done
|
|
@@ -212,10 +216,12 @@ def run_agent_baseline(scenario: dict, api_key: str) -> AgentRun:
|
|
| 212 |
tool_output_tokens += tool_tokens
|
| 213 |
|
| 214 |
# Add tool result
|
| 215 |
-
messages.append(
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
| 219 |
|
| 220 |
duration_ms = (time.time() - start_time) * 1000
|
| 221 |
|
|
@@ -251,7 +257,7 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun:
|
|
| 251 |
# Wrap with Headroom
|
| 252 |
config = HeadroomConfig(
|
| 253 |
smart_crusher_threshold=500, # Compress tool outputs > 500 tokens
|
| 254 |
-
smart_crusher_max_items=20,
|
| 255 |
cache_alignment=True,
|
| 256 |
rolling_window=True,
|
| 257 |
)
|
|
@@ -287,7 +293,7 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun:
|
|
| 287 |
# Count output tokens
|
| 288 |
output_tokens = count_tokens(response.content) if response.content else 0
|
| 289 |
if response.tool_calls:
|
| 290 |
-
output_tokens += count_tokens(json.dumps(
|
| 291 |
total_output_tokens += output_tokens
|
| 292 |
|
| 293 |
# Check if done
|
|
@@ -311,10 +317,12 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun:
|
|
| 311 |
tool_tokens = count_tokens(result)
|
| 312 |
tool_output_tokens += tool_tokens
|
| 313 |
|
| 314 |
-
messages.append(
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
| 318 |
|
| 319 |
duration_ms = (time.time() - start_time) * 1000
|
| 320 |
|
|
@@ -337,41 +345,59 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun:
|
|
| 337 |
def print_comparison(baseline: AgentRun, headroom: AgentRun):
|
| 338 |
"""Print comparison between baseline and headroom runs."""
|
| 339 |
|
| 340 |
-
print(f"\n{'='*70}")
|
| 341 |
print(f"SCENARIO: {baseline.scenario}")
|
| 342 |
-
print(f"{'='*70}")
|
| 343 |
|
| 344 |
# Token comparison
|
| 345 |
input_saved = baseline.total_input_tokens - headroom.total_input_tokens
|
| 346 |
-
input_pct = (
|
|
|
|
|
|
|
| 347 |
|
| 348 |
print(f"\n{'METRIC':<30} {'BASELINE':>15} {'HEADROOM':>15} {'SAVINGS':>15}")
|
| 349 |
print("-" * 75)
|
| 350 |
-
print(
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
print(f"{'Tool Calls':<30} {baseline.tool_calls:>15} {headroom.tool_calls:>15} {'':>15}")
|
| 354 |
print(f"{'Messages':<30} {baseline.messages_count:>15} {headroom.messages_count:>15} {'':>15}")
|
| 355 |
-
print(
|
|
|
|
|
|
|
| 356 |
|
| 357 |
# Cost estimation (gpt-4o-mini pricing)
|
| 358 |
input_cost_per_1m = 0.15
|
| 359 |
output_cost_per_1m = 0.60
|
| 360 |
|
| 361 |
-
baseline_cost = (
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
cost_saved = baseline_cost - headroom_cost
|
| 364 |
cost_pct = (cost_saved / baseline_cost * 100) if baseline_cost > 0 else 0
|
| 365 |
|
| 366 |
-
print(
|
|
|
|
|
|
|
| 367 |
|
| 368 |
|
| 369 |
def main():
|
| 370 |
"""Run the before/after comparison."""
|
| 371 |
|
| 372 |
-
print("\n" + "="*70)
|
| 373 |
print("LANGCHAIN AGENT: BEFORE/AFTER HEADROOM COMPARISON")
|
| 374 |
-
print("="*70)
|
| 375 |
|
| 376 |
# Check for API key
|
| 377 |
api_key = os.environ.get("OPENAI_API_KEY")
|
|
@@ -431,15 +457,17 @@ def run_simulation():
|
|
| 431 |
print(f"\n Total tool output: {total_tool_tokens:,} tokens")
|
| 432 |
print(f" With 3 iterations, baseline input would be: ~{total_tool_tokens * 2:,} tokens")
|
| 433 |
print(f" With Headroom (20 items max), estimated: ~{total_tool_tokens // 5:,} tokens")
|
| 434 |
-
print(
|
|
|
|
|
|
|
| 435 |
|
| 436 |
|
| 437 |
def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]):
|
| 438 |
"""Print overall summary."""
|
| 439 |
|
| 440 |
-
print("\n" + "="*70)
|
| 441 |
print("OVERALL SUMMARY")
|
| 442 |
-
print("="*70)
|
| 443 |
|
| 444 |
total_baseline_input = sum(r.total_input_tokens for r in baseline_runs)
|
| 445 |
total_headroom_input = sum(r.total_input_tokens for r in headroom_runs)
|
|
@@ -448,7 +476,9 @@ def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]):
|
|
| 448 |
|
| 449 |
print(f"\n{'Metric':<30} {'Baseline':>15} {'Headroom':>15} {'Savings':>15}")
|
| 450 |
print("-" * 75)
|
| 451 |
-
print(
|
|
|
|
|
|
|
| 452 |
print(f"{'Percentage Saved':<30} {'':>15} {'':>15} {pct_saved:>14.1f}%")
|
| 453 |
|
| 454 |
# Cost
|
|
@@ -457,11 +487,13 @@ def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]):
|
|
| 457 |
headroom_cost = total_headroom_input * input_cost
|
| 458 |
cost_saved = baseline_cost - headroom_cost
|
| 459 |
|
| 460 |
-
print(
|
|
|
|
|
|
|
| 461 |
|
| 462 |
-
print("\n" + "="*70)
|
| 463 |
print("CONCLUSION")
|
| 464 |
-
print("="*70)
|
| 465 |
print(f"""
|
| 466 |
Headroom reduced input tokens by {pct_saved:.1f}% across all scenarios.
|
| 467 |
|
|
|
|
| 20 |
import sys
|
| 21 |
import time
|
| 22 |
from dataclasses import dataclass
|
|
|
|
| 23 |
|
| 24 |
# Check for required dependencies
|
| 25 |
try:
|
|
|
|
| 29 |
sys.exit(1)
|
| 30 |
|
| 31 |
try:
|
| 32 |
+
from langchain_core.messages import ( # noqa: F401
|
| 33 |
+
AIMessage,
|
| 34 |
+
HumanMessage,
|
| 35 |
+
SystemMessage,
|
| 36 |
+
ToolMessage,
|
| 37 |
+
)
|
| 38 |
+
from langchain_core.tools import tool # noqa: F401
|
| 39 |
+
from langchain_openai import ChatOpenAI # noqa: F401
|
| 40 |
except ImportError:
|
| 41 |
print("ERROR: LangChain required. Run: pip install langchain langchain-openai langchain-core")
|
| 42 |
sys.exit(1)
|
|
|
|
| 44 |
# Import our mock tools
|
| 45 |
from .mock_tools import TOOL_FUNCTIONS
|
| 46 |
|
|
|
|
| 47 |
# Token counter
|
| 48 |
ENCODER = tiktoken.get_encoding("cl100k_base")
|
| 49 |
|
|
|
|
| 74 |
@dataclass
|
| 75 |
class AgentRun:
|
| 76 |
"""Results from a single agent run."""
|
| 77 |
+
|
| 78 |
scenario: str
|
| 79 |
mode: str # "baseline" or "headroom"
|
| 80 |
total_input_tokens: int
|
|
|
|
| 189 |
# Count output tokens
|
| 190 |
output_tokens = count_tokens(response.content) if response.content else 0
|
| 191 |
if response.tool_calls:
|
| 192 |
+
output_tokens += count_tokens(json.dumps(list(response.tool_calls)))
|
| 193 |
total_output_tokens += output_tokens
|
| 194 |
|
| 195 |
# Check if done
|
|
|
|
| 216 |
tool_output_tokens += tool_tokens
|
| 217 |
|
| 218 |
# Add tool result
|
| 219 |
+
messages.append(
|
| 220 |
+
ToolMessage(
|
| 221 |
+
content=result,
|
| 222 |
+
tool_call_id=tool_call["id"],
|
| 223 |
+
)
|
| 224 |
+
)
|
| 225 |
|
| 226 |
duration_ms = (time.time() - start_time) * 1000
|
| 227 |
|
|
|
|
| 257 |
# Wrap with Headroom
|
| 258 |
config = HeadroomConfig(
|
| 259 |
smart_crusher_threshold=500, # Compress tool outputs > 500 tokens
|
| 260 |
+
smart_crusher_max_items=20, # Keep max 20 items
|
| 261 |
cache_alignment=True,
|
| 262 |
rolling_window=True,
|
| 263 |
)
|
|
|
|
| 293 |
# Count output tokens
|
| 294 |
output_tokens = count_tokens(response.content) if response.content else 0
|
| 295 |
if response.tool_calls:
|
| 296 |
+
output_tokens += count_tokens(json.dumps(list(response.tool_calls)))
|
| 297 |
total_output_tokens += output_tokens
|
| 298 |
|
| 299 |
# Check if done
|
|
|
|
| 317 |
tool_tokens = count_tokens(result)
|
| 318 |
tool_output_tokens += tool_tokens
|
| 319 |
|
| 320 |
+
messages.append(
|
| 321 |
+
ToolMessage(
|
| 322 |
+
content=result,
|
| 323 |
+
tool_call_id=tool_call["id"],
|
| 324 |
+
)
|
| 325 |
+
)
|
| 326 |
|
| 327 |
duration_ms = (time.time() - start_time) * 1000
|
| 328 |
|
|
|
|
| 345 |
def print_comparison(baseline: AgentRun, headroom: AgentRun):
|
| 346 |
"""Print comparison between baseline and headroom runs."""
|
| 347 |
|
| 348 |
+
print(f"\n{'=' * 70}")
|
| 349 |
print(f"SCENARIO: {baseline.scenario}")
|
| 350 |
+
print(f"{'=' * 70}")
|
| 351 |
|
| 352 |
# Token comparison
|
| 353 |
input_saved = baseline.total_input_tokens - headroom.total_input_tokens
|
| 354 |
+
input_pct = (
|
| 355 |
+
(input_saved / baseline.total_input_tokens * 100) if baseline.total_input_tokens > 0 else 0
|
| 356 |
+
)
|
| 357 |
|
| 358 |
print(f"\n{'METRIC':<30} {'BASELINE':>15} {'HEADROOM':>15} {'SAVINGS':>15}")
|
| 359 |
print("-" * 75)
|
| 360 |
+
print(
|
| 361 |
+
f"{'Input Tokens':<30} {baseline.total_input_tokens:>15,} {headroom.total_input_tokens:>15,} {input_saved:>14,} ({input_pct:.1f}%)"
|
| 362 |
+
)
|
| 363 |
+
print(
|
| 364 |
+
f"{'Output Tokens':<30} {baseline.total_output_tokens:>15,} {headroom.total_output_tokens:>15,} {'N/A':>15}"
|
| 365 |
+
)
|
| 366 |
+
print(
|
| 367 |
+
f"{'Tool Output Tokens':<30} {baseline.tool_output_tokens:>15,} {headroom.tool_output_tokens:>15,} {'(raw)':>15}"
|
| 368 |
+
)
|
| 369 |
print(f"{'Tool Calls':<30} {baseline.tool_calls:>15} {headroom.tool_calls:>15} {'':>15}")
|
| 370 |
print(f"{'Messages':<30} {baseline.messages_count:>15} {headroom.messages_count:>15} {'':>15}")
|
| 371 |
+
print(
|
| 372 |
+
f"{'Duration (ms)':<30} {baseline.duration_ms:>15.0f} {headroom.duration_ms:>15.0f} {'':>15}"
|
| 373 |
+
)
|
| 374 |
|
| 375 |
# Cost estimation (gpt-4o-mini pricing)
|
| 376 |
input_cost_per_1m = 0.15
|
| 377 |
output_cost_per_1m = 0.60
|
| 378 |
|
| 379 |
+
baseline_cost = (
|
| 380 |
+
baseline.total_input_tokens * input_cost_per_1m
|
| 381 |
+
+ baseline.total_output_tokens * output_cost_per_1m
|
| 382 |
+
) / 1_000_000
|
| 383 |
+
headroom_cost = (
|
| 384 |
+
headroom.total_input_tokens * input_cost_per_1m
|
| 385 |
+
+ headroom.total_output_tokens * output_cost_per_1m
|
| 386 |
+
) / 1_000_000
|
| 387 |
cost_saved = baseline_cost - headroom_cost
|
| 388 |
cost_pct = (cost_saved / baseline_cost * 100) if baseline_cost > 0 else 0
|
| 389 |
|
| 390 |
+
print(
|
| 391 |
+
f"\n{'Estimated Cost (USD)':<30} ${baseline_cost:>14.6f} ${headroom_cost:>14.6f} ${cost_saved:>13.6f} ({cost_pct:.1f}%)"
|
| 392 |
+
)
|
| 393 |
|
| 394 |
|
| 395 |
def main():
|
| 396 |
"""Run the before/after comparison."""
|
| 397 |
|
| 398 |
+
print("\n" + "=" * 70)
|
| 399 |
print("LANGCHAIN AGENT: BEFORE/AFTER HEADROOM COMPARISON")
|
| 400 |
+
print("=" * 70)
|
| 401 |
|
| 402 |
# Check for API key
|
| 403 |
api_key = os.environ.get("OPENAI_API_KEY")
|
|
|
|
| 457 |
print(f"\n Total tool output: {total_tool_tokens:,} tokens")
|
| 458 |
print(f" With 3 iterations, baseline input would be: ~{total_tool_tokens * 2:,} tokens")
|
| 459 |
print(f" With Headroom (20 items max), estimated: ~{total_tool_tokens // 5:,} tokens")
|
| 460 |
+
print(
|
| 461 |
+
f" Estimated savings: ~{total_tool_tokens * 2 - total_tool_tokens // 5:,} tokens (~80%)"
|
| 462 |
+
)
|
| 463 |
|
| 464 |
|
| 465 |
def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]):
|
| 466 |
"""Print overall summary."""
|
| 467 |
|
| 468 |
+
print("\n" + "=" * 70)
|
| 469 |
print("OVERALL SUMMARY")
|
| 470 |
+
print("=" * 70)
|
| 471 |
|
| 472 |
total_baseline_input = sum(r.total_input_tokens for r in baseline_runs)
|
| 473 |
total_headroom_input = sum(r.total_input_tokens for r in headroom_runs)
|
|
|
|
| 476 |
|
| 477 |
print(f"\n{'Metric':<30} {'Baseline':>15} {'Headroom':>15} {'Savings':>15}")
|
| 478 |
print("-" * 75)
|
| 479 |
+
print(
|
| 480 |
+
f"{'Total Input Tokens':<30} {total_baseline_input:>15,} {total_headroom_input:>15,} {total_saved:>14,}"
|
| 481 |
+
)
|
| 482 |
print(f"{'Percentage Saved':<30} {'':>15} {'':>15} {pct_saved:>14.1f}%")
|
| 483 |
|
| 484 |
# Cost
|
|
|
|
| 487 |
headroom_cost = total_headroom_input * input_cost
|
| 488 |
cost_saved = baseline_cost - headroom_cost
|
| 489 |
|
| 490 |
+
print(
|
| 491 |
+
f"\n{'Est. Input Cost (USD)':<30} ${baseline_cost:>14.4f} ${headroom_cost:>14.4f} ${cost_saved:>13.4f}"
|
| 492 |
+
)
|
| 493 |
|
| 494 |
+
print("\n" + "=" * 70)
|
| 495 |
print("CONCLUSION")
|
| 496 |
+
print("=" * 70)
|
| 497 |
print(f"""
|
| 498 |
Headroom reduced input tokens by {pct_saved:.1f}% across all scenarios.
|
| 499 |
|
examples/langchain_demo/show_compression.py
CHANGED
|
@@ -19,13 +19,11 @@ except ImportError:
|
|
| 19 |
print("ERROR: tiktoken required. Run: uv pip install tiktoken")
|
| 20 |
sys.exit(1)
|
| 21 |
|
| 22 |
-
from headroom import HeadroomConfig
|
| 23 |
-
from headroom.transforms import SmartCrusher
|
| 24 |
from headroom.providers import OpenAIProvider
|
|
|
|
| 25 |
|
| 26 |
from .mock_tools import TOOL_FUNCTIONS
|
| 27 |
|
| 28 |
-
|
| 29 |
ENCODER = tiktoken.get_encoding("cl100k_base")
|
| 30 |
|
| 31 |
|
|
@@ -37,10 +35,10 @@ def count_tokens(text: str) -> int:
|
|
| 37 |
def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
| 38 |
"""Show before/after compression for a tool output."""
|
| 39 |
|
| 40 |
-
print(f"\n{'='*70}")
|
| 41 |
print(f"TOOL: {tool_name}({tool_arg!r})")
|
| 42 |
print(f"CONTEXT: {context!r}")
|
| 43 |
-
print(f"{'='*70}")
|
| 44 |
|
| 45 |
# Generate tool output
|
| 46 |
raw_output = TOOL_FUNCTIONS[tool_name](tool_arg)
|
|
@@ -59,11 +57,11 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
|
| 59 |
else:
|
| 60 |
item_count = "?"
|
| 61 |
|
| 62 |
-
print(
|
| 63 |
print(f"Items: {item_count}")
|
| 64 |
print(f"Tokens: {raw_tokens:,}")
|
| 65 |
print(f"Chars: {len(raw_output):,}")
|
| 66 |
-
print(
|
| 67 |
print(raw_output[:500] + "...")
|
| 68 |
|
| 69 |
# Create SmartCrusher with context
|
|
@@ -84,7 +82,19 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
|
| 84 |
messages = [
|
| 85 |
{"role": "system", "content": "You are a helpful assistant."},
|
| 86 |
{"role": "user", "content": context},
|
| 87 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
{"role": "tool", "content": raw_output, "tool_call_id": "call_1"},
|
| 89 |
]
|
| 90 |
|
|
@@ -112,18 +122,18 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
|
| 112 |
except json.JSONDecodeError:
|
| 113 |
compressed_items = "N/A"
|
| 114 |
|
| 115 |
-
print(
|
| 116 |
print(f"Items: {compressed_items}")
|
| 117 |
print(f"Tokens: {compressed_tokens:,}")
|
| 118 |
print(f"Chars: {len(compressed_output):,}")
|
| 119 |
-
print(
|
| 120 |
print(compressed_output[:500] + "...")
|
| 121 |
|
| 122 |
# Calculate savings
|
| 123 |
tokens_saved = raw_tokens - compressed_tokens
|
| 124 |
pct_saved = (tokens_saved / raw_tokens * 100) if raw_tokens > 0 else 0
|
| 125 |
|
| 126 |
-
print(
|
| 127 |
print(f"Tokens saved: {tokens_saved:,} ({pct_saved:.1f}%)")
|
| 128 |
print(f"Items reduced: {item_count} -> {compressed_items}")
|
| 129 |
|
|
@@ -139,9 +149,9 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
|
| 139 |
def main():
|
| 140 |
"""Run compression demonstrations."""
|
| 141 |
|
| 142 |
-
print("\n" + "="*70)
|
| 143 |
print("HEADROOM SMARTCRUSHER: BEFORE/AFTER COMPRESSION")
|
| 144 |
-
print("="*70)
|
| 145 |
print("""
|
| 146 |
This demonstrates how Headroom's SmartCrusher compresses large tool outputs.
|
| 147 |
|
|
@@ -156,44 +166,54 @@ Key techniques:
|
|
| 156 |
results = []
|
| 157 |
|
| 158 |
# Demo 1: User database search
|
| 159 |
-
results.append(
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
|
| 165 |
# Demo 2: Log search with errors
|
| 166 |
-
results.append(
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
| 171 |
|
| 172 |
# Demo 3: Metrics with anomalies
|
| 173 |
-
results.append(
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
|
| 179 |
# Demo 4: Documentation search
|
| 180 |
-
results.append(
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
|
| 186 |
# Demo 5: API data
|
| 187 |
-
results.append(
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
|
| 193 |
# Summary
|
| 194 |
-
print("\n" + "="*70)
|
| 195 |
print("SUMMARY: TOKEN SAVINGS ACROSS ALL TOOLS")
|
| 196 |
-
print("="*70)
|
| 197 |
|
| 198 |
print(f"\n{'Tool':<20} {'Before':>12} {'After':>12} {'Saved':>12} {'%':>8}")
|
| 199 |
print("-" * 66)
|
|
@@ -202,7 +222,9 @@ Key techniques:
|
|
| 202 |
total_after = 0
|
| 203 |
|
| 204 |
for r in results:
|
| 205 |
-
print(
|
|
|
|
|
|
|
| 206 |
total_before += r["before_tokens"]
|
| 207 |
total_after += r["after_tokens"]
|
| 208 |
|
|
@@ -210,7 +232,9 @@ Key techniques:
|
|
| 210 |
total_pct = (total_saved / total_before * 100) if total_before > 0 else 0
|
| 211 |
|
| 212 |
print("-" * 66)
|
| 213 |
-
print(
|
|
|
|
|
|
|
| 214 |
|
| 215 |
# Cost savings
|
| 216 |
input_cost_per_1m = 2.50 # gpt-4o pricing
|
|
@@ -218,11 +242,13 @@ Key techniques:
|
|
| 218 |
cost_after = total_after * input_cost_per_1m / 1_000_000
|
| 219 |
cost_saved = cost_before - cost_after
|
| 220 |
|
| 221 |
-
print(
|
| 222 |
print(f"Before: ${cost_before:.4f}")
|
| 223 |
print(f"After: ${cost_after:.4f}")
|
| 224 |
print(f"Saved: ${cost_saved:.4f} per request")
|
| 225 |
-
print(
|
|
|
|
|
|
|
| 226 |
|
| 227 |
|
| 228 |
if __name__ == "__main__":
|
|
|
|
| 19 |
print("ERROR: tiktoken required. Run: uv pip install tiktoken")
|
| 20 |
sys.exit(1)
|
| 21 |
|
|
|
|
|
|
|
| 22 |
from headroom.providers import OpenAIProvider
|
| 23 |
+
from headroom.transforms import SmartCrusher
|
| 24 |
|
| 25 |
from .mock_tools import TOOL_FUNCTIONS
|
| 26 |
|
|
|
|
| 27 |
ENCODER = tiktoken.get_encoding("cl100k_base")
|
| 28 |
|
| 29 |
|
|
|
|
| 35 |
def demonstrate_compression(tool_name: str, tool_arg: str, context: str):
|
| 36 |
"""Show before/after compression for a tool output."""
|
| 37 |
|
| 38 |
+
print(f"\n{'=' * 70}")
|
| 39 |
print(f"TOOL: {tool_name}({tool_arg!r})")
|
| 40 |
print(f"CONTEXT: {context!r}")
|
| 41 |
+
print(f"{'=' * 70}")
|
| 42 |
|
| 43 |
# Generate tool output
|
| 44 |
raw_output = TOOL_FUNCTIONS[tool_name](tool_arg)
|
|
|
|
| 57 |
else:
|
| 58 |
item_count = "?"
|
| 59 |
|
| 60 |
+
print("\n--- BEFORE COMPRESSION ---")
|
| 61 |
print(f"Items: {item_count}")
|
| 62 |
print(f"Tokens: {raw_tokens:,}")
|
| 63 |
print(f"Chars: {len(raw_output):,}")
|
| 64 |
+
print("\nFirst 500 chars:")
|
| 65 |
print(raw_output[:500] + "...")
|
| 66 |
|
| 67 |
# Create SmartCrusher with context
|
|
|
|
| 82 |
messages = [
|
| 83 |
{"role": "system", "content": "You are a helpful assistant."},
|
| 84 |
{"role": "user", "content": context},
|
| 85 |
+
{
|
| 86 |
+
"role": "assistant",
|
| 87 |
+
"content": None,
|
| 88 |
+
"tool_calls": [
|
| 89 |
+
{
|
| 90 |
+
"id": "call_1",
|
| 91 |
+
"function": {
|
| 92 |
+
"name": tool_name,
|
| 93 |
+
"arguments": json.dumps({tool_name.split("_")[-1]: tool_arg}),
|
| 94 |
+
},
|
| 95 |
+
}
|
| 96 |
+
],
|
| 97 |
+
},
|
| 98 |
{"role": "tool", "content": raw_output, "tool_call_id": "call_1"},
|
| 99 |
]
|
| 100 |
|
|
|
|
| 122 |
except json.JSONDecodeError:
|
| 123 |
compressed_items = "N/A"
|
| 124 |
|
| 125 |
+
print("\n--- AFTER COMPRESSION ---")
|
| 126 |
print(f"Items: {compressed_items}")
|
| 127 |
print(f"Tokens: {compressed_tokens:,}")
|
| 128 |
print(f"Chars: {len(compressed_output):,}")
|
| 129 |
+
print("\nFirst 500 chars:")
|
| 130 |
print(compressed_output[:500] + "...")
|
| 131 |
|
| 132 |
# Calculate savings
|
| 133 |
tokens_saved = raw_tokens - compressed_tokens
|
| 134 |
pct_saved = (tokens_saved / raw_tokens * 100) if raw_tokens > 0 else 0
|
| 135 |
|
| 136 |
+
print("\n--- SAVINGS ---")
|
| 137 |
print(f"Tokens saved: {tokens_saved:,} ({pct_saved:.1f}%)")
|
| 138 |
print(f"Items reduced: {item_count} -> {compressed_items}")
|
| 139 |
|
|
|
|
| 149 |
def main():
|
| 150 |
"""Run compression demonstrations."""
|
| 151 |
|
| 152 |
+
print("\n" + "=" * 70)
|
| 153 |
print("HEADROOM SMARTCRUSHER: BEFORE/AFTER COMPRESSION")
|
| 154 |
+
print("=" * 70)
|
| 155 |
print("""
|
| 156 |
This demonstrates how Headroom's SmartCrusher compresses large tool outputs.
|
| 157 |
|
|
|
|
| 166 |
results = []
|
| 167 |
|
| 168 |
# Demo 1: User database search
|
| 169 |
+
results.append(
|
| 170 |
+
demonstrate_compression(
|
| 171 |
+
tool_name="search_users",
|
| 172 |
+
tool_arg="Engineering users",
|
| 173 |
+
context="Find all users in the Engineering department who are currently active",
|
| 174 |
+
)
|
| 175 |
+
)
|
| 176 |
|
| 177 |
# Demo 2: Log search with errors
|
| 178 |
+
results.append(
|
| 179 |
+
demonstrate_compression(
|
| 180 |
+
tool_name="search_logs",
|
| 181 |
+
tool_arg="payment-service",
|
| 182 |
+
context="Check the payment-service logs for any ERROR entries",
|
| 183 |
+
)
|
| 184 |
+
)
|
| 185 |
|
| 186 |
# Demo 3: Metrics with anomalies
|
| 187 |
+
results.append(
|
| 188 |
+
demonstrate_compression(
|
| 189 |
+
tool_name="get_metrics",
|
| 190 |
+
tool_arg="api-gateway",
|
| 191 |
+
context="Look for any CPU spikes or high error rates in the api-gateway metrics",
|
| 192 |
+
)
|
| 193 |
+
)
|
| 194 |
|
| 195 |
# Demo 4: Documentation search
|
| 196 |
+
results.append(
|
| 197 |
+
demonstrate_compression(
|
| 198 |
+
tool_name="search_docs",
|
| 199 |
+
tool_arg="authentication",
|
| 200 |
+
context="Find documentation about authentication troubleshooting",
|
| 201 |
+
)
|
| 202 |
+
)
|
| 203 |
|
| 204 |
# Demo 5: API data
|
| 205 |
+
results.append(
|
| 206 |
+
demonstrate_compression(
|
| 207 |
+
tool_name="fetch_api_data",
|
| 208 |
+
tool_arg="orders",
|
| 209 |
+
context="Get recent orders with status 'pending'",
|
| 210 |
+
)
|
| 211 |
+
)
|
| 212 |
|
| 213 |
# Summary
|
| 214 |
+
print("\n" + "=" * 70)
|
| 215 |
print("SUMMARY: TOKEN SAVINGS ACROSS ALL TOOLS")
|
| 216 |
+
print("=" * 70)
|
| 217 |
|
| 218 |
print(f"\n{'Tool':<20} {'Before':>12} {'After':>12} {'Saved':>12} {'%':>8}")
|
| 219 |
print("-" * 66)
|
|
|
|
| 222 |
total_after = 0
|
| 223 |
|
| 224 |
for r in results:
|
| 225 |
+
print(
|
| 226 |
+
f"{r['tool']:<20} {r['before_tokens']:>12,} {r['after_tokens']:>12,} {r['saved_tokens']:>12,} {r['saved_pct']:>7.1f}%"
|
| 227 |
+
)
|
| 228 |
total_before += r["before_tokens"]
|
| 229 |
total_after += r["after_tokens"]
|
| 230 |
|
|
|
|
| 232 |
total_pct = (total_saved / total_before * 100) if total_before > 0 else 0
|
| 233 |
|
| 234 |
print("-" * 66)
|
| 235 |
+
print(
|
| 236 |
+
f"{'TOTAL':<20} {total_before:>12,} {total_after:>12,} {total_saved:>12,} {total_pct:>7.1f}%"
|
| 237 |
+
)
|
| 238 |
|
| 239 |
# Cost savings
|
| 240 |
input_cost_per_1m = 2.50 # gpt-4o pricing
|
|
|
|
| 242 |
cost_after = total_after * input_cost_per_1m / 1_000_000
|
| 243 |
cost_saved = cost_before - cost_after
|
| 244 |
|
| 245 |
+
print("\n--- COST IMPACT (at gpt-4o $2.50/1M input tokens) ---")
|
| 246 |
print(f"Before: ${cost_before:.4f}")
|
| 247 |
print(f"After: ${cost_after:.4f}")
|
| 248 |
print(f"Saved: ${cost_saved:.4f} per request")
|
| 249 |
+
print(
|
| 250 |
+
f"\nAt 1000 requests/day: ${cost_saved * 1000:.2f}/day = ${cost_saved * 1000 * 30:.2f}/month"
|
| 251 |
+
)
|
| 252 |
|
| 253 |
|
| 254 |
if __name__ == "__main__":
|
examples/langchain_demo/verify_errors_kept.py
CHANGED
|
@@ -6,16 +6,16 @@ This is critical - errors should NEVER be dropped during compression.
|
|
| 6 |
import json
|
| 7 |
|
| 8 |
from headroom.config import SmartCrusherConfig
|
| 9 |
-
from headroom.transforms import SmartCrusher
|
| 10 |
from headroom.providers import OpenAIProvider
|
|
|
|
| 11 |
|
| 12 |
from .mock_tools import generate_log_entries
|
| 13 |
|
| 14 |
|
| 15 |
def main():
|
| 16 |
-
print("\n" + "="*70)
|
| 17 |
print("VERIFYING ERROR PRESERVATION IN SMARTCRUSHER")
|
| 18 |
-
print("="*70)
|
| 19 |
|
| 20 |
# Generate logs with some ERROR entries
|
| 21 |
raw_output = generate_log_entries("test-service", count=200)
|
|
@@ -43,7 +43,13 @@ def main():
|
|
| 43 |
messages = [
|
| 44 |
{"role": "system", "content": "You are a helpful assistant."},
|
| 45 |
{"role": "user", "content": "Find ERROR entries in the logs"},
|
| 46 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
{"role": "tool", "content": raw_output, "tool_call_id": "call_1"},
|
| 48 |
]
|
| 49 |
|
|
@@ -57,11 +63,12 @@ def main():
|
|
| 57 |
except json.JSONDecodeError:
|
| 58 |
# Try to extract just the JSON object
|
| 59 |
import re
|
| 60 |
-
|
|
|
|
| 61 |
if json_match:
|
| 62 |
compressed_data = json.loads(json_match.group(1))
|
| 63 |
else:
|
| 64 |
-
print(
|
| 65 |
print(compressed_output[:500])
|
| 66 |
return
|
| 67 |
|
|
@@ -74,20 +81,22 @@ def main():
|
|
| 74 |
print(f" - {err['message'][:60]}...")
|
| 75 |
|
| 76 |
# Verification
|
| 77 |
-
print("\n" + "="*70)
|
| 78 |
if len(compressed_errors) >= len(original_errors):
|
| 79 |
print("SUCCESS: All ERROR entries were preserved!")
|
| 80 |
elif len(compressed_errors) > 0:
|
| 81 |
print(f"PARTIAL: {len(compressed_errors)}/{len(original_errors)} ERROR entries preserved")
|
| 82 |
else:
|
| 83 |
print("FAILURE: ERROR entries were dropped!")
|
| 84 |
-
print("="*70)
|
| 85 |
|
| 86 |
# Show compression ratio
|
| 87 |
original_count = len(data["entries"])
|
| 88 |
compressed_count = len(compressed_data["entries"])
|
| 89 |
reduction = (original_count - compressed_count) / original_count * 100
|
| 90 |
-
print(
|
|
|
|
|
|
|
| 91 |
print(f"But kept: {len(compressed_errors)} of {len(original_errors)} ERROR entries")
|
| 92 |
|
| 93 |
|
|
|
|
| 6 |
import json
|
| 7 |
|
| 8 |
from headroom.config import SmartCrusherConfig
|
|
|
|
| 9 |
from headroom.providers import OpenAIProvider
|
| 10 |
+
from headroom.transforms import SmartCrusher
|
| 11 |
|
| 12 |
from .mock_tools import generate_log_entries
|
| 13 |
|
| 14 |
|
| 15 |
def main():
|
| 16 |
+
print("\n" + "=" * 70)
|
| 17 |
print("VERIFYING ERROR PRESERVATION IN SMARTCRUSHER")
|
| 18 |
+
print("=" * 70)
|
| 19 |
|
| 20 |
# Generate logs with some ERROR entries
|
| 21 |
raw_output = generate_log_entries("test-service", count=200)
|
|
|
|
| 43 |
messages = [
|
| 44 |
{"role": "system", "content": "You are a helpful assistant."},
|
| 45 |
{"role": "user", "content": "Find ERROR entries in the logs"},
|
| 46 |
+
{
|
| 47 |
+
"role": "assistant",
|
| 48 |
+
"content": None,
|
| 49 |
+
"tool_calls": [
|
| 50 |
+
{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}
|
| 51 |
+
],
|
| 52 |
+
},
|
| 53 |
{"role": "tool", "content": raw_output, "tool_call_id": "call_1"},
|
| 54 |
]
|
| 55 |
|
|
|
|
| 63 |
except json.JSONDecodeError:
|
| 64 |
# Try to extract just the JSON object
|
| 65 |
import re
|
| 66 |
+
|
| 67 |
+
json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL)
|
| 68 |
if json_match:
|
| 69 |
compressed_data = json.loads(json_match.group(1))
|
| 70 |
else:
|
| 71 |
+
print("Could not parse compressed output:")
|
| 72 |
print(compressed_output[:500])
|
| 73 |
return
|
| 74 |
|
|
|
|
| 81 |
print(f" - {err['message'][:60]}...")
|
| 82 |
|
| 83 |
# Verification
|
| 84 |
+
print("\n" + "=" * 70)
|
| 85 |
if len(compressed_errors) >= len(original_errors):
|
| 86 |
print("SUCCESS: All ERROR entries were preserved!")
|
| 87 |
elif len(compressed_errors) > 0:
|
| 88 |
print(f"PARTIAL: {len(compressed_errors)}/{len(original_errors)} ERROR entries preserved")
|
| 89 |
else:
|
| 90 |
print("FAILURE: ERROR entries were dropped!")
|
| 91 |
+
print("=" * 70)
|
| 92 |
|
| 93 |
# Show compression ratio
|
| 94 |
original_count = len(data["entries"])
|
| 95 |
compressed_count = len(compressed_data["entries"])
|
| 96 |
reduction = (original_count - compressed_count) / original_count * 100
|
| 97 |
+
print(
|
| 98 |
+
f"\nCompression: {original_count} → {compressed_count} entries ({reduction:.1f}% reduction)"
|
| 99 |
+
)
|
| 100 |
print(f"But kept: {len(compressed_errors)} of {len(original_errors)} ERROR entries")
|
| 101 |
|
| 102 |
|
examples/mcp_demo/mock_mcp_servers.py
CHANGED
|
@@ -22,41 +22,50 @@ def generate_slack_search_results(query: str, count: int = 150) -> str:
|
|
| 22 |
# 15% chance of error-related message
|
| 23 |
is_error = random.random() < 0.15
|
| 24 |
if is_error:
|
| 25 |
-
text = random.choice(
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
else:
|
| 33 |
-
text = random.choice(
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def generate_database_query_results(query: str, count: int = 200) -> str:
|
|
@@ -72,7 +81,9 @@ def generate_database_query_results(query: str, count: int = 200) -> str:
|
|
| 72 |
"user_id": f"usr_{random.randint(10000, 99999)}",
|
| 73 |
"email": f"user{i}@example.com",
|
| 74 |
"full_name": f"User {i}",
|
| 75 |
-
"status": "ERROR: validation_failed"
|
|
|
|
|
|
|
| 76 |
"created_at": (datetime.now() - timedelta(days=random.randint(1, 365))).isoformat(),
|
| 77 |
"last_login": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
|
| 78 |
"balance": None if has_null else round(random.uniform(0, 10000), 2),
|
|
@@ -81,17 +92,19 @@ def generate_database_query_results(query: str, count: int = 200) -> str:
|
|
| 81 |
}
|
| 82 |
rows.append(row)
|
| 83 |
|
| 84 |
-
return json.dumps(
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def generate_log_search_results(service: str, count: int = 300) -> str:
|
| 93 |
"""Simulate log analysis MCP server results."""
|
| 94 |
-
levels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
|
| 95 |
services = [service, f"{service}-worker", f"{service}-scheduler", "auth-service"]
|
| 96 |
|
| 97 |
entries = []
|
|
@@ -99,34 +112,40 @@ def generate_log_search_results(service: str, count: int = 300) -> str:
|
|
| 99 |
# 20% error rate (ERROR or FATAL)
|
| 100 |
if random.random() < 0.20:
|
| 101 |
level = random.choice(["ERROR", "FATAL"])
|
| 102 |
-
message = random.choice(
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
else:
|
| 111 |
level = random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"])
|
| 112 |
-
message = random.choice(
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
return json.dumps({"entries": entries, "service": service}, indent=2)
|
| 132 |
|
|
@@ -140,25 +159,36 @@ def generate_github_issues_results(repo: str, count: int = 100) -> str:
|
|
| 140 |
for i in range(count):
|
| 141 |
# 25% bug rate
|
| 142 |
is_bug = random.random() < 0.25
|
| 143 |
-
labels =
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# 15% chance of error-related message
|
| 23 |
is_error = random.random() < 0.15
|
| 24 |
if is_error:
|
| 25 |
+
text = random.choice(
|
| 26 |
+
[
|
| 27 |
+
"ERROR: Database connection pool exhausted at 3:45am",
|
| 28 |
+
"CRITICAL: Memory usage at 95% on prod-api-01",
|
| 29 |
+
"Exception in PaymentService.processTransaction()",
|
| 30 |
+
"FAILED: Deploy pipeline broke - rolling back",
|
| 31 |
+
"ALERT: Latency spike detected on /api/users endpoint",
|
| 32 |
+
]
|
| 33 |
+
)
|
| 34 |
else:
|
| 35 |
+
text = random.choice(
|
| 36 |
+
[
|
| 37 |
+
f"Reviewed the PR for {query}, looks good to merge",
|
| 38 |
+
f"Updated the docs with new {query} endpoints",
|
| 39 |
+
"Meeting notes from standup attached",
|
| 40 |
+
"Can someone review my changes to the auth module?",
|
| 41 |
+
"Deployed v2.3.1 to staging environment",
|
| 42 |
+
"Thanks for the feedback on the design doc!",
|
| 43 |
+
"Working on the feature request from yesterday",
|
| 44 |
+
]
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
messages.append(
|
| 48 |
+
{
|
| 49 |
+
"id": f"msg_{i}",
|
| 50 |
+
"channel": random.choice(channels),
|
| 51 |
+
"user": random.choice(users),
|
| 52 |
+
"text": text,
|
| 53 |
+
"timestamp": (datetime.now() - timedelta(hours=i)).isoformat(),
|
| 54 |
+
"reactions": random.randint(0, 15),
|
| 55 |
+
"thread_replies": random.randint(0, 10),
|
| 56 |
+
"permalink": f"https://slack.com/archives/C123/p{i}",
|
| 57 |
+
}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
return json.dumps(
|
| 61 |
+
{
|
| 62 |
+
"query": query,
|
| 63 |
+
"messages": messages,
|
| 64 |
+
"total": count,
|
| 65 |
+
"has_more": count > 100,
|
| 66 |
+
},
|
| 67 |
+
indent=2,
|
| 68 |
+
)
|
| 69 |
|
| 70 |
|
| 71 |
def generate_database_query_results(query: str, count: int = 200) -> str:
|
|
|
|
| 81 |
"user_id": f"usr_{random.randint(10000, 99999)}",
|
| 82 |
"email": f"user{i}@example.com",
|
| 83 |
"full_name": f"User {i}",
|
| 84 |
+
"status": "ERROR: validation_failed"
|
| 85 |
+
if has_error
|
| 86 |
+
else random.choice(["active", "inactive", "pending"]),
|
| 87 |
"created_at": (datetime.now() - timedelta(days=random.randint(1, 365))).isoformat(),
|
| 88 |
"last_login": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
|
| 89 |
"balance": None if has_null else round(random.uniform(0, 10000), 2),
|
|
|
|
| 92 |
}
|
| 93 |
rows.append(row)
|
| 94 |
|
| 95 |
+
return json.dumps(
|
| 96 |
+
{
|
| 97 |
+
"query": query,
|
| 98 |
+
"rows": rows,
|
| 99 |
+
"count": count,
|
| 100 |
+
"execution_time_ms": random.randint(50, 500),
|
| 101 |
+
},
|
| 102 |
+
indent=2,
|
| 103 |
+
)
|
| 104 |
|
| 105 |
|
| 106 |
def generate_log_search_results(service: str, count: int = 300) -> str:
|
| 107 |
"""Simulate log analysis MCP server results."""
|
|
|
|
| 108 |
services = [service, f"{service}-worker", f"{service}-scheduler", "auth-service"]
|
| 109 |
|
| 110 |
entries = []
|
|
|
|
| 112 |
# 20% error rate (ERROR or FATAL)
|
| 113 |
if random.random() < 0.20:
|
| 114 |
level = random.choice(["ERROR", "FATAL"])
|
| 115 |
+
message = random.choice(
|
| 116 |
+
[
|
| 117 |
+
"Connection timeout to primary database",
|
| 118 |
+
"Failed to process message from queue",
|
| 119 |
+
"Authentication failed: invalid token",
|
| 120 |
+
"Out of memory error in request handler",
|
| 121 |
+
"Unhandled exception: NullPointerException",
|
| 122 |
+
"Circuit breaker open for external-api",
|
| 123 |
+
]
|
| 124 |
+
)
|
| 125 |
else:
|
| 126 |
level = random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"])
|
| 127 |
+
message = random.choice(
|
| 128 |
+
[
|
| 129 |
+
"Request processed successfully",
|
| 130 |
+
"Cache hit for user session",
|
| 131 |
+
"Starting scheduled job: cleanup",
|
| 132 |
+
"Connection pool stats: 10/20 active",
|
| 133 |
+
"Metrics exported to datadog",
|
| 134 |
+
"Health check passed",
|
| 135 |
+
]
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
entries.append(
|
| 139 |
+
{
|
| 140 |
+
"timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(),
|
| 141 |
+
"level": level,
|
| 142 |
+
"service": random.choice(services),
|
| 143 |
+
"message": message,
|
| 144 |
+
"trace_id": f"trace_{random.randint(100000, 999999)}",
|
| 145 |
+
"span_id": f"span_{random.randint(1000, 9999)}",
|
| 146 |
+
"host": f"prod-{random.choice(['api', 'worker', 'web'])}-{random.randint(1, 10):02d}",
|
| 147 |
+
}
|
| 148 |
+
)
|
| 149 |
|
| 150 |
return json.dumps({"entries": entries, "service": service}, indent=2)
|
| 151 |
|
|
|
|
| 159 |
for i in range(count):
|
| 160 |
# 25% bug rate
|
| 161 |
is_bug = random.random() < 0.25
|
| 162 |
+
labels = (
|
| 163 |
+
random.sample(bug_labels, k=random.randint(1, 2))
|
| 164 |
+
if is_bug
|
| 165 |
+
else random.sample(labels_pool, k=random.randint(0, 2))
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
issues.append(
|
| 169 |
+
{
|
| 170 |
+
"number": i + 1,
|
| 171 |
+
"title": f"{'[BUG] ' if is_bug else ''}{random.choice(['Fix auth flow', 'Add dark mode', 'Update docs', 'Improve perf'])}",
|
| 172 |
+
"state": random.choice(["open", "open", "closed"]),
|
| 173 |
+
"labels": labels,
|
| 174 |
+
"author": f"contributor{random.randint(1, 50)}",
|
| 175 |
+
"assignee": f"maintainer{random.randint(1, 5)}" if random.random() > 0.3 else None,
|
| 176 |
+
"created_at": (datetime.now() - timedelta(days=random.randint(1, 90))).isoformat(),
|
| 177 |
+
"updated_at": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
|
| 178 |
+
"comments": random.randint(0, 30),
|
| 179 |
+
"body": "Lorem ipsum dolor sit amet..." if random.random() > 0.5 else "",
|
| 180 |
+
"milestone": f"v{random.randint(1, 3)}.{random.randint(0, 9)}"
|
| 181 |
+
if random.random() > 0.7
|
| 182 |
+
else None,
|
| 183 |
+
}
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return json.dumps(
|
| 187 |
+
{
|
| 188 |
+
"repository": repo,
|
| 189 |
+
"issues": issues,
|
| 190 |
+
"total_count": count,
|
| 191 |
+
"open_count": sum(1 for i in issues if i["state"] == "open"),
|
| 192 |
+
},
|
| 193 |
+
indent=2,
|
| 194 |
+
)
|
examples/mcp_demo/run_agent_eval.py
CHANGED
|
@@ -24,21 +24,34 @@ from headroom.providers import OpenAIProvider
|
|
| 24 |
# Test Data Generators (Deterministic for eval reproducibility)
|
| 25 |
# ============================================================================
|
| 26 |
|
|
|
|
| 27 |
def generate_slack_with_specific_errors(seed: int = 42) -> tuple[str, list[dict]]:
|
| 28 |
"""Generate Slack messages with SPECIFIC errors we'll query for."""
|
| 29 |
random.seed(seed)
|
| 30 |
|
| 31 |
# These are the "needle" errors we'll ask the agent to find
|
| 32 |
critical_errors = [
|
| 33 |
-
{
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
]
|
| 43 |
|
| 44 |
# Generate noise messages
|
|
@@ -62,13 +75,15 @@ def generate_slack_with_specific_errors(seed: int = 42) -> tuple[str, list[dict]
|
|
| 62 |
messages.append(critical_errors[error_idx])
|
| 63 |
error_idx += 1
|
| 64 |
else:
|
| 65 |
-
messages.append(
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
return json.dumps({"messages": messages, "total": 150}), critical_errors
|
| 74 |
|
|
@@ -79,17 +94,43 @@ def generate_logs_with_specific_errors(seed: int = 43) -> tuple[str, list[dict]]
|
|
| 79 |
|
| 80 |
# These are the "needle" errors
|
| 81 |
critical_logs = [
|
| 82 |
-
{
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
]
|
| 91 |
|
| 92 |
-
services = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
info_messages = [
|
| 94 |
"Request processed successfully",
|
| 95 |
"Cache hit for user session",
|
|
@@ -105,13 +146,15 @@ def generate_logs_with_specific_errors(seed: int = 43) -> tuple[str, list[dict]]
|
|
| 105 |
entries.append(critical_logs[error_idx])
|
| 106 |
error_idx += 1
|
| 107 |
else:
|
| 108 |
-
entries.append(
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
|
| 116 |
return json.dumps({"entries": entries}), critical_logs
|
| 117 |
|
|
@@ -122,10 +165,24 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]:
|
|
| 122 |
|
| 123 |
# Anomalous records we'll ask about
|
| 124 |
anomalies = [
|
| 125 |
-
{
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
]
|
| 130 |
|
| 131 |
rows = []
|
|
@@ -135,15 +192,19 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]:
|
|
| 135 |
rows.append(anomalies[anomaly_idx])
|
| 136 |
anomaly_idx += 1
|
| 137 |
else:
|
| 138 |
-
rows.append(
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
return json.dumps({"rows": rows, "count": 200}), anomalies
|
| 149 |
|
|
@@ -152,9 +213,11 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]:
|
|
| 152 |
# Eval Test Cases
|
| 153 |
# ============================================================================
|
| 154 |
|
|
|
|
| 155 |
@dataclass
|
| 156 |
class EvalCase:
|
| 157 |
"""A single evaluation case."""
|
|
|
|
| 158 |
name: str
|
| 159 |
tool_name: str
|
| 160 |
tool_output: str
|
|
@@ -192,7 +255,13 @@ def create_eval_cases() -> list[EvalCase]:
|
|
| 192 |
tool_name="mcp__logs__search",
|
| 193 |
tool_output=logs_output,
|
| 194 |
user_query="List all ERROR and FATAL log entries with their services and messages.",
|
| 195 |
-
expected_findings=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
critical_data=log_errors,
|
| 197 |
),
|
| 198 |
EvalCase(
|
|
@@ -218,6 +287,7 @@ def create_eval_cases() -> list[EvalCase]:
|
|
| 218 |
# Agent Simulation
|
| 219 |
# ============================================================================
|
| 220 |
|
|
|
|
| 221 |
def run_agent_with_tool_output(
|
| 222 |
client: OpenAI,
|
| 223 |
user_query: str,
|
|
@@ -230,11 +300,22 @@ def run_agent_with_tool_output(
|
|
| 230 |
Returns: (answer, tokens_used)
|
| 231 |
"""
|
| 232 |
messages = [
|
| 233 |
-
{
|
|
|
|
|
|
|
|
|
|
| 234 |
{"role": "user", "content": user_query},
|
| 235 |
-
{
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
{"role": "tool", "content": tool_output, "tool_call_id": "call_1"},
|
| 239 |
]
|
| 240 |
|
|
@@ -269,6 +350,7 @@ def evaluate_answer(answer: str, expected_findings: list[str]) -> tuple[int, int
|
|
| 269 |
# Main Eval Runner
|
| 270 |
# ============================================================================
|
| 271 |
|
|
|
|
| 272 |
def main():
|
| 273 |
# Check for API key
|
| 274 |
if not os.environ.get("OPENAI_API_KEY"):
|
|
@@ -299,7 +381,7 @@ def main():
|
|
| 299 |
for case in eval_cases:
|
| 300 |
print(f"\n{'─' * 70}")
|
| 301 |
print(f"EVAL: {case.name}")
|
| 302 |
-
print(f
|
| 303 |
print(f"{'─' * 70}")
|
| 304 |
|
| 305 |
# Measure original tokens
|
|
@@ -312,26 +394,32 @@ def main():
|
|
| 312 |
user_query=case.user_query,
|
| 313 |
)
|
| 314 |
|
| 315 |
-
print(
|
| 316 |
print(f" Original: {original_tokens:,} tokens")
|
| 317 |
print(f" Compressed: {compression.compressed_tokens:,} tokens")
|
| 318 |
print(f" Saved: {compression.tokens_saved:,} ({compression.compression_ratio:.1%})")
|
| 319 |
|
| 320 |
# Run agent BEFORE (with original output)
|
| 321 |
-
print(
|
| 322 |
try:
|
| 323 |
answer_before, tokens_before = run_agent_with_tool_output(
|
| 324 |
client, case.user_query, case.tool_name, case.tool_output
|
| 325 |
)
|
| 326 |
-
found_before, total, missing_before = evaluate_answer(
|
|
|
|
|
|
|
| 327 |
except Exception as e:
|
| 328 |
print(f" ERROR: {e}")
|
| 329 |
answer_before = ""
|
| 330 |
-
found_before, total, missing_before =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
tokens_before = 0
|
| 332 |
|
| 333 |
# Run agent AFTER (with compressed output)
|
| 334 |
-
print(
|
| 335 |
try:
|
| 336 |
answer_after, tokens_after = run_agent_with_tool_output(
|
| 337 |
client, case.user_query, case.tool_name, compression.compressed_content
|
|
@@ -344,7 +432,7 @@ def main():
|
|
| 344 |
tokens_after = 0
|
| 345 |
|
| 346 |
# Results
|
| 347 |
-
print(
|
| 348 |
print(f" BEFORE: Found {found_before}/{total} expected findings")
|
| 349 |
if missing_before:
|
| 350 |
print(f" Missing: {missing_before}")
|
|
@@ -353,30 +441,34 @@ def main():
|
|
| 353 |
print(f" Missing: {missing_after}")
|
| 354 |
|
| 355 |
# Token usage comparison
|
| 356 |
-
print(
|
| 357 |
print(f" BEFORE: {tokens_before:,} tokens")
|
| 358 |
print(f" AFTER: {tokens_after:,} tokens")
|
| 359 |
if tokens_before > 0:
|
| 360 |
-
print(
|
|
|
|
|
|
|
| 361 |
|
| 362 |
# Pass/Fail
|
| 363 |
passed = found_after >= found_before
|
| 364 |
status = "PASS" if passed else "FAIL"
|
| 365 |
print(f"\n Status: {status}")
|
| 366 |
if not passed:
|
| 367 |
-
print(
|
| 368 |
print(f" Lost findings: {set(missing_after) - set(missing_before)}")
|
| 369 |
|
| 370 |
-
results.append(
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
| 380 |
|
| 381 |
# Summary
|
| 382 |
print("\n" + "=" * 70)
|
|
@@ -387,27 +479,31 @@ def main():
|
|
| 387 |
total_cases = len(results)
|
| 388 |
|
| 389 |
print(f"\n Tests Passed: {passed}/{total_cases}")
|
| 390 |
-
print(
|
| 391 |
print(f" {'Test Name':<35} {'Before':<10} {'After':<10} {'Compress':<10} {'Status':<8}")
|
| 392 |
-
print(f" {'-'*35} {'-'*10} {'-'*10} {'-'*10} {'-'*8}")
|
| 393 |
|
| 394 |
for r in results:
|
| 395 |
status = "PASS" if r["passed"] else "FAIL"
|
| 396 |
-
print(
|
|
|
|
|
|
|
| 397 |
|
| 398 |
# Token savings
|
| 399 |
total_tokens_before = sum(r["tokens_before"] for r in results)
|
| 400 |
total_tokens_after = sum(r["tokens_after"] for r in results)
|
| 401 |
|
| 402 |
-
print(
|
| 403 |
print(f" Before: {total_tokens_before:,}")
|
| 404 |
print(f" After: {total_tokens_after:,}")
|
| 405 |
-
print(
|
|
|
|
|
|
|
| 406 |
|
| 407 |
# Cost estimate
|
| 408 |
cost_before = total_tokens_before * 0.15 / 1_000_000 # gpt-4o-mini input
|
| 409 |
cost_after = total_tokens_after * 0.15 / 1_000_000
|
| 410 |
-
print(
|
| 411 |
print(f" Before: ${cost_before:.4f}")
|
| 412 |
print(f" After: ${cost_after:.4f}")
|
| 413 |
print(f" Saved: ${cost_before - cost_after:.4f}")
|
|
|
|
| 24 |
# Test Data Generators (Deterministic for eval reproducibility)
|
| 25 |
# ============================================================================
|
| 26 |
|
| 27 |
+
|
| 28 |
def generate_slack_with_specific_errors(seed: int = 42) -> tuple[str, list[dict]]:
|
| 29 |
"""Generate Slack messages with SPECIFIC errors we'll query for."""
|
| 30 |
random.seed(seed)
|
| 31 |
|
| 32 |
# These are the "needle" errors we'll ask the agent to find
|
| 33 |
critical_errors = [
|
| 34 |
+
{
|
| 35 |
+
"id": "msg_17",
|
| 36 |
+
"channel": "#incidents",
|
| 37 |
+
"user": "alice",
|
| 38 |
+
"text": "CRITICAL: Payment service is DOWN - customers cannot checkout. Error: ConnectionRefused to payment-db-01",
|
| 39 |
+
"timestamp": "2025-01-06T03:45:00Z",
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"id": "msg_42",
|
| 43 |
+
"channel": "#alerts",
|
| 44 |
+
"user": "bob",
|
| 45 |
+
"text": "ERROR: Auth service returning 500s. Stack trace shows NullPointerException in TokenValidator.java:127",
|
| 46 |
+
"timestamp": "2025-01-06T02:30:00Z",
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": "msg_89",
|
| 50 |
+
"channel": "#engineering",
|
| 51 |
+
"user": "charlie",
|
| 52 |
+
"text": "FAILED: Deploy to prod-us-east failed. Reason: Health check timeout after 300s on api-gateway-03",
|
| 53 |
+
"timestamp": "2025-01-05T23:15:00Z",
|
| 54 |
+
},
|
| 55 |
]
|
| 56 |
|
| 57 |
# Generate noise messages
|
|
|
|
| 75 |
messages.append(critical_errors[error_idx])
|
| 76 |
error_idx += 1
|
| 77 |
else:
|
| 78 |
+
messages.append(
|
| 79 |
+
{
|
| 80 |
+
"id": f"msg_{i}",
|
| 81 |
+
"channel": random.choice(channels),
|
| 82 |
+
"user": random.choice(users),
|
| 83 |
+
"text": random.choice(noise_messages),
|
| 84 |
+
"timestamp": (datetime.now() - timedelta(hours=i)).isoformat(),
|
| 85 |
+
}
|
| 86 |
+
)
|
| 87 |
|
| 88 |
return json.dumps({"messages": messages, "total": 150}), critical_errors
|
| 89 |
|
|
|
|
| 94 |
|
| 95 |
# These are the "needle" errors
|
| 96 |
critical_logs = [
|
| 97 |
+
{
|
| 98 |
+
"timestamp": "2025-01-06T03:44:58Z",
|
| 99 |
+
"level": "FATAL",
|
| 100 |
+
"service": "payment-service",
|
| 101 |
+
"message": "Cannot connect to payment-db-01: Connection refused",
|
| 102 |
+
"trace_id": "trace_payment_001",
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"timestamp": "2025-01-06T02:29:55Z",
|
| 106 |
+
"level": "ERROR",
|
| 107 |
+
"service": "auth-service",
|
| 108 |
+
"message": "NullPointerException in TokenValidator.validate() at line 127",
|
| 109 |
+
"trace_id": "trace_auth_001",
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"timestamp": "2025-01-05T23:14:30Z",
|
| 113 |
+
"level": "ERROR",
|
| 114 |
+
"service": "api-gateway",
|
| 115 |
+
"message": "Health check failed: timeout after 300000ms",
|
| 116 |
+
"trace_id": "trace_gateway_001",
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"timestamp": "2025-01-06T01:00:00Z",
|
| 120 |
+
"level": "ERROR",
|
| 121 |
+
"service": "user-service",
|
| 122 |
+
"message": "Database query timeout: SELECT * FROM users WHERE last_login > ?",
|
| 123 |
+
"trace_id": "trace_user_001",
|
| 124 |
+
},
|
| 125 |
]
|
| 126 |
|
| 127 |
+
services = [
|
| 128 |
+
"api-gateway",
|
| 129 |
+
"auth-service",
|
| 130 |
+
"payment-service",
|
| 131 |
+
"user-service",
|
| 132 |
+
"notification-service",
|
| 133 |
+
]
|
| 134 |
info_messages = [
|
| 135 |
"Request processed successfully",
|
| 136 |
"Cache hit for user session",
|
|
|
|
| 146 |
entries.append(critical_logs[error_idx])
|
| 147 |
error_idx += 1
|
| 148 |
else:
|
| 149 |
+
entries.append(
|
| 150 |
+
{
|
| 151 |
+
"timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(),
|
| 152 |
+
"level": random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"]),
|
| 153 |
+
"service": random.choice(services),
|
| 154 |
+
"message": random.choice(info_messages),
|
| 155 |
+
"trace_id": f"trace_{random.randint(100000, 999999)}",
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
|
| 159 |
return json.dumps({"entries": entries}), critical_logs
|
| 160 |
|
|
|
|
| 165 |
|
| 166 |
# Anomalous records we'll ask about
|
| 167 |
anomalies = [
|
| 168 |
+
{
|
| 169 |
+
"id": 23,
|
| 170 |
+
"user_id": "usr_99999",
|
| 171 |
+
"email": "admin@internal.com",
|
| 172 |
+
"status": "ERROR: account_locked",
|
| 173 |
+
"balance": 999999.99,
|
| 174 |
+
"login_attempts": 47,
|
| 175 |
+
"last_login": "2025-01-06T04:00:00Z",
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"id": 156,
|
| 179 |
+
"user_id": "usr_00001",
|
| 180 |
+
"email": "test@test.com",
|
| 181 |
+
"status": "ERROR: validation_failed",
|
| 182 |
+
"balance": -500.00,
|
| 183 |
+
"login_attempts": 0,
|
| 184 |
+
"last_login": None,
|
| 185 |
+
},
|
| 186 |
]
|
| 187 |
|
| 188 |
rows = []
|
|
|
|
| 192 |
rows.append(anomalies[anomaly_idx])
|
| 193 |
anomaly_idx += 1
|
| 194 |
else:
|
| 195 |
+
rows.append(
|
| 196 |
+
{
|
| 197 |
+
"id": i,
|
| 198 |
+
"user_id": f"usr_{random.randint(10000, 99999)}",
|
| 199 |
+
"email": f"user{i}@example.com",
|
| 200 |
+
"status": random.choice(["active", "active", "active", "inactive", "pending"]),
|
| 201 |
+
"balance": round(random.uniform(0, 5000), 2),
|
| 202 |
+
"login_attempts": random.randint(0, 5),
|
| 203 |
+
"last_login": (
|
| 204 |
+
datetime.now() - timedelta(days=random.randint(0, 30))
|
| 205 |
+
).isoformat(),
|
| 206 |
+
}
|
| 207 |
+
)
|
| 208 |
|
| 209 |
return json.dumps({"rows": rows, "count": 200}), anomalies
|
| 210 |
|
|
|
|
| 213 |
# Eval Test Cases
|
| 214 |
# ============================================================================
|
| 215 |
|
| 216 |
+
|
| 217 |
@dataclass
|
| 218 |
class EvalCase:
|
| 219 |
"""A single evaluation case."""
|
| 220 |
+
|
| 221 |
name: str
|
| 222 |
tool_name: str
|
| 223 |
tool_output: str
|
|
|
|
| 255 |
tool_name="mcp__logs__search",
|
| 256 |
tool_output=logs_output,
|
| 257 |
user_query="List all ERROR and FATAL log entries with their services and messages.",
|
| 258 |
+
expected_findings=[
|
| 259 |
+
"payment-service",
|
| 260 |
+
"auth-service",
|
| 261 |
+
"api-gateway",
|
| 262 |
+
"Connection refused",
|
| 263 |
+
"NullPointerException",
|
| 264 |
+
],
|
| 265 |
critical_data=log_errors,
|
| 266 |
),
|
| 267 |
EvalCase(
|
|
|
|
| 287 |
# Agent Simulation
|
| 288 |
# ============================================================================
|
| 289 |
|
| 290 |
+
|
| 291 |
def run_agent_with_tool_output(
|
| 292 |
client: OpenAI,
|
| 293 |
user_query: str,
|
|
|
|
| 300 |
Returns: (answer, tokens_used)
|
| 301 |
"""
|
| 302 |
messages = [
|
| 303 |
+
{
|
| 304 |
+
"role": "system",
|
| 305 |
+
"content": "You are a helpful assistant analyzing tool outputs. Be specific and cite exact details from the data.",
|
| 306 |
+
},
|
| 307 |
{"role": "user", "content": user_query},
|
| 308 |
+
{
|
| 309 |
+
"role": "assistant",
|
| 310 |
+
"content": None,
|
| 311 |
+
"tool_calls": [
|
| 312 |
+
{
|
| 313 |
+
"id": "call_1",
|
| 314 |
+
"type": "function",
|
| 315 |
+
"function": {"name": tool_name, "arguments": "{}"},
|
| 316 |
+
}
|
| 317 |
+
],
|
| 318 |
+
},
|
| 319 |
{"role": "tool", "content": tool_output, "tool_call_id": "call_1"},
|
| 320 |
]
|
| 321 |
|
|
|
|
| 350 |
# Main Eval Runner
|
| 351 |
# ============================================================================
|
| 352 |
|
| 353 |
+
|
| 354 |
def main():
|
| 355 |
# Check for API key
|
| 356 |
if not os.environ.get("OPENAI_API_KEY"):
|
|
|
|
| 381 |
for case in eval_cases:
|
| 382 |
print(f"\n{'─' * 70}")
|
| 383 |
print(f"EVAL: {case.name}")
|
| 384 |
+
print(f'Query: "{case.user_query}"')
|
| 385 |
print(f"{'─' * 70}")
|
| 386 |
|
| 387 |
# Measure original tokens
|
|
|
|
| 394 |
user_query=case.user_query,
|
| 395 |
)
|
| 396 |
|
| 397 |
+
print("\n Tool Output:")
|
| 398 |
print(f" Original: {original_tokens:,} tokens")
|
| 399 |
print(f" Compressed: {compression.compressed_tokens:,} tokens")
|
| 400 |
print(f" Saved: {compression.tokens_saved:,} ({compression.compression_ratio:.1%})")
|
| 401 |
|
| 402 |
# Run agent BEFORE (with original output)
|
| 403 |
+
print("\n Running agent with ORIGINAL output...")
|
| 404 |
try:
|
| 405 |
answer_before, tokens_before = run_agent_with_tool_output(
|
| 406 |
client, case.user_query, case.tool_name, case.tool_output
|
| 407 |
)
|
| 408 |
+
found_before, total, missing_before = evaluate_answer(
|
| 409 |
+
answer_before, case.expected_findings
|
| 410 |
+
)
|
| 411 |
except Exception as e:
|
| 412 |
print(f" ERROR: {e}")
|
| 413 |
answer_before = ""
|
| 414 |
+
found_before, total, missing_before = (
|
| 415 |
+
0,
|
| 416 |
+
len(case.expected_findings),
|
| 417 |
+
case.expected_findings,
|
| 418 |
+
)
|
| 419 |
tokens_before = 0
|
| 420 |
|
| 421 |
# Run agent AFTER (with compressed output)
|
| 422 |
+
print(" Running agent with COMPRESSED output...")
|
| 423 |
try:
|
| 424 |
answer_after, tokens_after = run_agent_with_tool_output(
|
| 425 |
client, case.user_query, case.tool_name, compression.compressed_content
|
|
|
|
| 432 |
tokens_after = 0
|
| 433 |
|
| 434 |
# Results
|
| 435 |
+
print("\n Results:")
|
| 436 |
print(f" BEFORE: Found {found_before}/{total} expected findings")
|
| 437 |
if missing_before:
|
| 438 |
print(f" Missing: {missing_before}")
|
|
|
|
| 441 |
print(f" Missing: {missing_after}")
|
| 442 |
|
| 443 |
# Token usage comparison
|
| 444 |
+
print("\n API Token Usage:")
|
| 445 |
print(f" BEFORE: {tokens_before:,} tokens")
|
| 446 |
print(f" AFTER: {tokens_after:,} tokens")
|
| 447 |
if tokens_before > 0:
|
| 448 |
+
print(
|
| 449 |
+
f" Saved: {tokens_before - tokens_after:,} ({(tokens_before - tokens_after) / tokens_before:.1%})"
|
| 450 |
+
)
|
| 451 |
|
| 452 |
# Pass/Fail
|
| 453 |
passed = found_after >= found_before
|
| 454 |
status = "PASS" if passed else "FAIL"
|
| 455 |
print(f"\n Status: {status}")
|
| 456 |
if not passed:
|
| 457 |
+
print(" Reason: Compressed output lost information")
|
| 458 |
print(f" Lost findings: {set(missing_after) - set(missing_before)}")
|
| 459 |
|
| 460 |
+
results.append(
|
| 461 |
+
{
|
| 462 |
+
"name": case.name,
|
| 463 |
+
"passed": passed,
|
| 464 |
+
"found_before": found_before,
|
| 465 |
+
"found_after": found_after,
|
| 466 |
+
"total": total,
|
| 467 |
+
"tokens_before": tokens_before,
|
| 468 |
+
"tokens_after": tokens_after,
|
| 469 |
+
"compression_ratio": compression.compression_ratio,
|
| 470 |
+
}
|
| 471 |
+
)
|
| 472 |
|
| 473 |
# Summary
|
| 474 |
print("\n" + "=" * 70)
|
|
|
|
| 479 |
total_cases = len(results)
|
| 480 |
|
| 481 |
print(f"\n Tests Passed: {passed}/{total_cases}")
|
| 482 |
+
print("\n Detailed Results:")
|
| 483 |
print(f" {'Test Name':<35} {'Before':<10} {'After':<10} {'Compress':<10} {'Status':<8}")
|
| 484 |
+
print(f" {'-' * 35} {'-' * 10} {'-' * 10} {'-' * 10} {'-' * 8}")
|
| 485 |
|
| 486 |
for r in results:
|
| 487 |
status = "PASS" if r["passed"] else "FAIL"
|
| 488 |
+
print(
|
| 489 |
+
f" {r['name']:<35} {r['found_before']}/{r['total']:<8} {r['found_after']}/{r['total']:<8} {r['compression_ratio']:.0%}{'':>6} {status:<8}"
|
| 490 |
+
)
|
| 491 |
|
| 492 |
# Token savings
|
| 493 |
total_tokens_before = sum(r["tokens_before"] for r in results)
|
| 494 |
total_tokens_after = sum(r["tokens_after"] for r in results)
|
| 495 |
|
| 496 |
+
print("\n Total API Tokens:")
|
| 497 |
print(f" Before: {total_tokens_before:,}")
|
| 498 |
print(f" After: {total_tokens_after:,}")
|
| 499 |
+
print(
|
| 500 |
+
f" Saved: {total_tokens_before - total_tokens_after:,} ({(total_tokens_before - total_tokens_after) / total_tokens_before:.1%})"
|
| 501 |
+
)
|
| 502 |
|
| 503 |
# Cost estimate
|
| 504 |
cost_before = total_tokens_before * 0.15 / 1_000_000 # gpt-4o-mini input
|
| 505 |
cost_after = total_tokens_after * 0.15 / 1_000_000
|
| 506 |
+
print("\n Cost (gpt-4o-mini):")
|
| 507 |
print(f" Before: ${cost_before:.4f}")
|
| 508 |
print(f" After: ${cost_after:.4f}")
|
| 509 |
print(f" Saved: ${cost_before - cost_after:.4f}")
|
examples/mcp_demo/show_before_after.py
CHANGED
|
@@ -22,16 +22,16 @@ def main():
|
|
| 22 |
|
| 23 |
print("\nBEFORE (in your MCP host application):")
|
| 24 |
print("-" * 40)
|
| 25 |
-
before_standalone =
|
| 26 |
# Your MCP host application
|
| 27 |
result = await mcp_client.call_tool("search_logs", {"service": "api"})
|
| 28 |
messages.append({"role": "tool", "content": result})
|
| 29 |
-
|
| 30 |
print(before_standalone)
|
| 31 |
|
| 32 |
print("\nAFTER (with Headroom compression):")
|
| 33 |
print("-" * 40)
|
| 34 |
-
after_standalone =
|
| 35 |
from headroom.integrations.mcp import compress_tool_result # ADD THIS
|
| 36 |
|
| 37 |
# Your MCP host application
|
|
@@ -42,7 +42,7 @@ compressed = compress_tool_result( # ADD THIS
|
|
| 42 |
user_query="find errors in api", # ADD THIS
|
| 43 |
) # ADD THIS
|
| 44 |
messages.append({"role": "tool", "content": compressed})
|
| 45 |
-
|
| 46 |
print(after_standalone)
|
| 47 |
|
| 48 |
# =========================================================================
|
|
@@ -54,7 +54,7 @@ messages.append({"role": "tool", "content": compressed})
|
|
| 54 |
|
| 55 |
print("\nBEFORE:")
|
| 56 |
print("-" * 40)
|
| 57 |
-
before_wrapper =
|
| 58 |
from mcp import Client
|
| 59 |
|
| 60 |
# Create MCP client
|
|
@@ -62,12 +62,12 @@ client = Client(transport)
|
|
| 62 |
|
| 63 |
# Use client normally
|
| 64 |
result = await client.call_tool("search_logs", {"service": "api"})
|
| 65 |
-
|
| 66 |
print(before_wrapper)
|
| 67 |
|
| 68 |
print("\nAFTER:")
|
| 69 |
print("-" * 40)
|
| 70 |
-
after_wrapper =
|
| 71 |
from mcp import Client
|
| 72 |
from headroom.integrations.mcp import HeadroomMCPClientWrapper # ADD THIS
|
| 73 |
|
|
@@ -77,7 +77,7 @@ client = HeadroomMCPClientWrapper(base_client) # WRAP IT (1 line)
|
|
| 77 |
|
| 78 |
# Use client normally - compression is automatic!
|
| 79 |
result = await client.call_tool("search_logs", {"service": "api"})
|
| 80 |
-
|
| 81 |
print(after_wrapper)
|
| 82 |
|
| 83 |
# =========================================================================
|
|
@@ -89,7 +89,7 @@ result = await client.call_tool("search_logs", {"service": "api"})
|
|
| 89 |
|
| 90 |
print("\nCode with metrics tracking:")
|
| 91 |
print("-" * 40)
|
| 92 |
-
with_metrics =
|
| 93 |
from headroom.integrations.mcp import compress_tool_result_with_metrics
|
| 94 |
|
| 95 |
result = await mcp_client.call_tool("search_logs", {"service": "api"})
|
|
@@ -104,7 +104,7 @@ print(f"Compression: {compression.compression_ratio:.1%}")
|
|
| 104 |
print(f"Errors preserved: {compression.errors_preserved}")
|
| 105 |
|
| 106 |
messages.append({"role": "tool", "content": compression.compressed_content})
|
| 107 |
-
|
| 108 |
print(with_metrics)
|
| 109 |
|
| 110 |
# =========================================================================
|
|
|
|
| 22 |
|
| 23 |
print("\nBEFORE (in your MCP host application):")
|
| 24 |
print("-" * 40)
|
| 25 |
+
before_standalone = """
|
| 26 |
# Your MCP host application
|
| 27 |
result = await mcp_client.call_tool("search_logs", {"service": "api"})
|
| 28 |
messages.append({"role": "tool", "content": result})
|
| 29 |
+
"""
|
| 30 |
print(before_standalone)
|
| 31 |
|
| 32 |
print("\nAFTER (with Headroom compression):")
|
| 33 |
print("-" * 40)
|
| 34 |
+
after_standalone = """
|
| 35 |
from headroom.integrations.mcp import compress_tool_result # ADD THIS
|
| 36 |
|
| 37 |
# Your MCP host application
|
|
|
|
| 42 |
user_query="find errors in api", # ADD THIS
|
| 43 |
) # ADD THIS
|
| 44 |
messages.append({"role": "tool", "content": compressed})
|
| 45 |
+
"""
|
| 46 |
print(after_standalone)
|
| 47 |
|
| 48 |
# =========================================================================
|
|
|
|
| 54 |
|
| 55 |
print("\nBEFORE:")
|
| 56 |
print("-" * 40)
|
| 57 |
+
before_wrapper = """
|
| 58 |
from mcp import Client
|
| 59 |
|
| 60 |
# Create MCP client
|
|
|
|
| 62 |
|
| 63 |
# Use client normally
|
| 64 |
result = await client.call_tool("search_logs", {"service": "api"})
|
| 65 |
+
"""
|
| 66 |
print(before_wrapper)
|
| 67 |
|
| 68 |
print("\nAFTER:")
|
| 69 |
print("-" * 40)
|
| 70 |
+
after_wrapper = """
|
| 71 |
from mcp import Client
|
| 72 |
from headroom.integrations.mcp import HeadroomMCPClientWrapper # ADD THIS
|
| 73 |
|
|
|
|
| 77 |
|
| 78 |
# Use client normally - compression is automatic!
|
| 79 |
result = await client.call_tool("search_logs", {"service": "api"})
|
| 80 |
+
"""
|
| 81 |
print(after_wrapper)
|
| 82 |
|
| 83 |
# =========================================================================
|
|
|
|
| 89 |
|
| 90 |
print("\nCode with metrics tracking:")
|
| 91 |
print("-" * 40)
|
| 92 |
+
with_metrics = """
|
| 93 |
from headroom.integrations.mcp import compress_tool_result_with_metrics
|
| 94 |
|
| 95 |
result = await mcp_client.call_tool("search_logs", {"service": "api"})
|
|
|
|
| 104 |
print(f"Errors preserved: {compression.errors_preserved}")
|
| 105 |
|
| 106 |
messages.append({"role": "tool", "content": compression.compressed_content})
|
| 107 |
+
"""
|
| 108 |
print(with_metrics)
|
| 109 |
|
| 110 |
# =========================================================================
|
examples/mcp_demo/show_compression.py
CHANGED
|
@@ -4,20 +4,18 @@ Run with:
|
|
| 4 |
PYTHONPATH=. python -m examples.mcp_demo.show_compression
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
import json
|
| 8 |
import random
|
| 9 |
|
| 10 |
from headroom.integrations.mcp import (
|
| 11 |
compress_tool_result_with_metrics,
|
| 12 |
-
HeadroomMCPCompressor,
|
| 13 |
)
|
| 14 |
from headroom.providers import OpenAIProvider
|
| 15 |
|
| 16 |
from .mock_mcp_servers import (
|
| 17 |
-
generate_slack_search_results,
|
| 18 |
generate_database_query_results,
|
| 19 |
-
generate_log_search_results,
|
| 20 |
generate_github_issues_results,
|
|
|
|
|
|
|
| 21 |
)
|
| 22 |
|
| 23 |
|
|
@@ -30,7 +28,7 @@ def main():
|
|
| 30 |
|
| 31 |
# Get token counter
|
| 32 |
provider = OpenAIProvider()
|
| 33 |
-
|
| 34 |
|
| 35 |
# Test scenarios
|
| 36 |
scenarios = [
|
|
@@ -71,7 +69,7 @@ def main():
|
|
| 71 |
print(f"\n{'─' * 70}")
|
| 72 |
print(f"Tool: {scenario['name']}")
|
| 73 |
print(f"MCP Server: {scenario['tool_name']}")
|
| 74 |
-
print(f
|
| 75 |
print(f"{'─' * 70}")
|
| 76 |
|
| 77 |
result = compress_tool_result_with_metrics(
|
|
|
|
| 4 |
PYTHONPATH=. python -m examples.mcp_demo.show_compression
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 7 |
import random
|
| 8 |
|
| 9 |
from headroom.integrations.mcp import (
|
| 10 |
compress_tool_result_with_metrics,
|
|
|
|
| 11 |
)
|
| 12 |
from headroom.providers import OpenAIProvider
|
| 13 |
|
| 14 |
from .mock_mcp_servers import (
|
|
|
|
| 15 |
generate_database_query_results,
|
|
|
|
| 16 |
generate_github_issues_results,
|
| 17 |
+
generate_log_search_results,
|
| 18 |
+
generate_slack_search_results,
|
| 19 |
)
|
| 20 |
|
| 21 |
|
|
|
|
| 28 |
|
| 29 |
# Get token counter
|
| 30 |
provider = OpenAIProvider()
|
| 31 |
+
provider.get_token_counter("gpt-4o")
|
| 32 |
|
| 33 |
# Test scenarios
|
| 34 |
scenarios = [
|
|
|
|
| 69 |
print(f"\n{'─' * 70}")
|
| 70 |
print(f"Tool: {scenario['name']}")
|
| 71 |
print(f"MCP Server: {scenario['tool_name']}")
|
| 72 |
+
print(f'User Query: "{scenario["user_query"]}"')
|
| 73 |
print(f"{'─' * 70}")
|
| 74 |
|
| 75 |
result = compress_tool_result_with_metrics(
|
examples/real_world_eval.py
CHANGED
|
@@ -37,10 +37,10 @@ provider = AnthropicProvider()
|
|
| 37 |
# AGGRESSIVE optimization config
|
| 38 |
aggressive_tool_crusher = ToolCrusherConfig(
|
| 39 |
enabled=True,
|
| 40 |
-
min_tokens_to_crush=100,
|
| 41 |
-
max_array_items=3,
|
| 42 |
-
max_string_length=200,
|
| 43 |
-
max_depth=3,
|
| 44 |
)
|
| 45 |
|
| 46 |
db_path = os.path.join(tempfile.gettempdir(), "headroom_eval.db")
|
|
@@ -54,7 +54,6 @@ headroom_client = HeadroomClient(
|
|
| 54 |
)
|
| 55 |
|
| 56 |
# Aggressive optimization client
|
| 57 |
-
from headroom.config import HeadroomConfig
|
| 58 |
aggressive_config = HeadroomConfig()
|
| 59 |
aggressive_config.tool_crusher = aggressive_tool_crusher
|
| 60 |
|
|
@@ -67,52 +66,56 @@ aggressive_client = HeadroomClient(
|
|
| 67 |
)
|
| 68 |
# Manually set aggressive config on pipeline
|
| 69 |
aggressive_client._config = aggressive_config
|
| 70 |
-
aggressive_client._pipeline = __import__(
|
| 71 |
-
|
| 72 |
-
)
|
| 73 |
|
| 74 |
|
| 75 |
# =============================================================================
|
| 76 |
# REALISTIC AGENTIC SCENARIO: Research Assistant
|
| 77 |
# =============================================================================
|
| 78 |
|
|
|
|
| 79 |
def generate_search_results(query: str, count: int = 25) -> str:
|
| 80 |
"""Generate realistic search results JSON."""
|
| 81 |
results = []
|
| 82 |
for i in range(count):
|
| 83 |
-
results.append(
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
"
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
return json.dumps({"results": results, "total_count": count, "query": query})
|
| 109 |
|
| 110 |
|
| 111 |
def generate_document_content(doc_id: str) -> str:
|
| 112 |
"""Generate realistic document content."""
|
| 113 |
-
return json.dumps(
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
Introduction:
|
| 117 |
This research investigates the complex interplay between artificial intelligence
|
| 118 |
and human decision-making processes. Our longitudinal study spanning 36 months
|
|
@@ -136,68 +139,99 @@ def generate_document_content(doc_id: str) -> str:
|
|
| 136 |
Conclusion:
|
| 137 |
The integration of AI in decision-making processes offers substantial benefits
|
| 138 |
but requires careful implementation to avoid potential negative outcomes.
|
| 139 |
-
"""
|
| 140 |
-
|
| 141 |
-
"
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
| 156 |
|
| 157 |
|
| 158 |
def generate_analytics_data() -> str:
|
| 159 |
"""Generate realistic analytics/metrics data."""
|
| 160 |
-
return json.dumps(
|
| 161 |
-
|
| 162 |
-
"
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
# =============================================================================
|
| 198 |
# BUILD COMPLEX AGENTIC CONVERSATION
|
| 199 |
# =============================================================================
|
| 200 |
|
|
|
|
| 201 |
def build_agentic_conversation() -> list[dict]:
|
| 202 |
"""Build a realistic multi-turn agentic conversation."""
|
| 203 |
|
|
@@ -210,26 +244,24 @@ def build_agentic_conversation() -> list[dict]:
|
|
| 210 |
{
|
| 211 |
"role": "user",
|
| 212 |
"content": "Current Date: 2024-12-15. I need you to research the impact of AI on workplace productivity. "
|
| 213 |
-
|
| 214 |
},
|
| 215 |
-
|
| 216 |
# Turn 2: Assistant decides to search
|
| 217 |
{
|
| 218 |
"role": "assistant",
|
| 219 |
"content": [
|
| 220 |
{
|
| 221 |
"type": "text",
|
| 222 |
-
"text": "I'll help you research AI's impact on workplace productivity. Let me search for recent academic papers on this topic."
|
| 223 |
},
|
| 224 |
{
|
| 225 |
"type": "tool_use",
|
| 226 |
"id": "search_1",
|
| 227 |
"name": "academic_search",
|
| 228 |
-
"input": {"query": "AI impact workplace productivity", "limit": 25}
|
| 229 |
-
}
|
| 230 |
-
]
|
| 231 |
},
|
| 232 |
-
|
| 233 |
# Turn 3: Tool result - large search results
|
| 234 |
{
|
| 235 |
"role": "user",
|
|
@@ -237,40 +269,38 @@ def build_agentic_conversation() -> list[dict]:
|
|
| 237 |
{
|
| 238 |
"type": "tool_result",
|
| 239 |
"tool_use_id": "search_1",
|
| 240 |
-
"content": generate_search_results("AI impact workplace productivity", 25)
|
| 241 |
}
|
| 242 |
-
]
|
| 243 |
},
|
| 244 |
-
|
| 245 |
# Turn 4: Assistant analyzes and fetches more
|
| 246 |
{
|
| 247 |
"role": "assistant",
|
| 248 |
"content": [
|
| 249 |
{
|
| 250 |
"type": "text",
|
| 251 |
-
"text": "I found 25 relevant papers. Let me fetch the full content of the top 3 most cited papers and get analytics on the overall trends."
|
| 252 |
},
|
| 253 |
{
|
| 254 |
"type": "tool_use",
|
| 255 |
"id": "fetch_1",
|
| 256 |
"name": "fetch_document",
|
| 257 |
-
"input": {"doc_id": "doc_0001"}
|
| 258 |
},
|
| 259 |
{
|
| 260 |
"type": "tool_use",
|
| 261 |
"id": "fetch_2",
|
| 262 |
"name": "fetch_document",
|
| 263 |
-
"input": {"doc_id": "doc_0002"}
|
| 264 |
},
|
| 265 |
{
|
| 266 |
"type": "tool_use",
|
| 267 |
"id": "analytics_1",
|
| 268 |
"name": "get_analytics",
|
| 269 |
-
"input": {"topic": "AI workplace productivity"}
|
| 270 |
-
}
|
| 271 |
-
]
|
| 272 |
},
|
| 273 |
-
|
| 274 |
# Turn 5: Multiple tool results
|
| 275 |
{
|
| 276 |
"role": "user",
|
|
@@ -278,56 +308,55 @@ def build_agentic_conversation() -> list[dict]:
|
|
| 278 |
{
|
| 279 |
"type": "tool_result",
|
| 280 |
"tool_use_id": "fetch_1",
|
| 281 |
-
"content": generate_document_content("doc_0001")
|
| 282 |
},
|
| 283 |
{
|
| 284 |
"type": "tool_result",
|
| 285 |
"tool_use_id": "fetch_2",
|
| 286 |
-
"content": generate_document_content("doc_0002")
|
| 287 |
},
|
| 288 |
{
|
| 289 |
"type": "tool_result",
|
| 290 |
"tool_use_id": "analytics_1",
|
| 291 |
-
"content": generate_analytics_data()
|
| 292 |
-
}
|
| 293 |
-
]
|
| 294 |
},
|
| 295 |
-
|
| 296 |
# Turn 6: Assistant provides initial summary
|
| 297 |
{
|
| 298 |
"role": "assistant",
|
| 299 |
"content": "Based on my analysis of 25 papers and detailed review of the top cited works, here's what the research shows:\n\n"
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
},
|
| 307 |
-
|
| 308 |
# Turn 7: User asks follow-up
|
| 309 |
{
|
| 310 |
"role": "user",
|
| 311 |
-
"content": "Yes, tell me more about the training and adoption factors. What makes AI adoption successful?"
|
| 312 |
},
|
| 313 |
-
|
| 314 |
# Turn 8: Assistant searches for more specific info
|
| 315 |
{
|
| 316 |
"role": "assistant",
|
| 317 |
"content": [
|
| 318 |
{
|
| 319 |
"type": "text",
|
| 320 |
-
"text": "Great question. Let me search for papers specifically about AI adoption success factors."
|
| 321 |
},
|
| 322 |
{
|
| 323 |
"type": "tool_use",
|
| 324 |
"id": "search_2",
|
| 325 |
"name": "academic_search",
|
| 326 |
-
"input": {
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
| 329 |
},
|
| 330 |
-
|
| 331 |
# Turn 9: More search results
|
| 332 |
{
|
| 333 |
"role": "user",
|
|
@@ -335,28 +364,28 @@ def build_agentic_conversation() -> list[dict]:
|
|
| 335 |
{
|
| 336 |
"type": "tool_result",
|
| 337 |
"tool_use_id": "search_2",
|
| 338 |
-
"content": generate_search_results(
|
|
|
|
|
|
|
| 339 |
}
|
| 340 |
-
]
|
| 341 |
},
|
| 342 |
-
|
| 343 |
# Turn 10: Assistant synthesizes
|
| 344 |
{
|
| 345 |
"role": "assistant",
|
| 346 |
"content": "Based on the additional research on AI adoption success factors:\n\n"
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
},
|
| 355 |
-
|
| 356 |
# Turn 11: User asks for final deliverable
|
| 357 |
{
|
| 358 |
"role": "user",
|
| 359 |
-
"content": "Perfect. Now synthesize everything into a brief executive summary I can share with my team."
|
| 360 |
},
|
| 361 |
]
|
| 362 |
|
|
@@ -367,9 +396,11 @@ def build_agentic_conversation() -> list[dict]:
|
|
| 367 |
# EVALUATION FRAMEWORK
|
| 368 |
# =============================================================================
|
| 369 |
|
|
|
|
| 370 |
@dataclass
|
| 371 |
class EvalResult:
|
| 372 |
"""Results from a single evaluation run."""
|
|
|
|
| 373 |
mode: str
|
| 374 |
tokens_input: int
|
| 375 |
tokens_output: int
|
|
@@ -443,15 +474,15 @@ Provide scores in this exact JSON format:
|
|
| 443 |
response = base_client.messages.create(
|
| 444 |
model="claude-3-5-haiku-latest",
|
| 445 |
max_tokens=500,
|
| 446 |
-
messages=[{"role": "user", "content": eval_prompt}]
|
| 447 |
)
|
| 448 |
|
| 449 |
try:
|
| 450 |
# Extract JSON from response
|
| 451 |
text = response.content[0].text
|
| 452 |
# Find JSON in response
|
| 453 |
-
start = text.find(
|
| 454 |
-
end = text.rfind(
|
| 455 |
if start >= 0 and end > start:
|
| 456 |
return json.loads(text[start:end])
|
| 457 |
except (json.JSONDecodeError, IndexError):
|
|
@@ -464,6 +495,7 @@ Provide scores in this exact JSON format:
|
|
| 464 |
# MAIN EVALUATION
|
| 465 |
# =============================================================================
|
| 466 |
|
|
|
|
| 467 |
def run_aggressive_evaluation(messages: list[dict], mode: str) -> EvalResult:
|
| 468 |
"""Run evaluation with aggressive client."""
|
| 469 |
tokenizer = provider.get_token_counter("claude-3-5-haiku-latest")
|
|
@@ -505,8 +537,8 @@ def main():
|
|
| 505 |
messages = build_agentic_conversation()
|
| 506 |
|
| 507 |
print(f"Scenario: Research Assistant with {len(messages)} turns")
|
| 508 |
-
print(
|
| 509 |
-
print(
|
| 510 |
print()
|
| 511 |
|
| 512 |
# =========================================================================
|
|
@@ -530,8 +562,12 @@ def main():
|
|
| 530 |
|
| 531 |
print(f"\n{'Mode':<20} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}")
|
| 532 |
print("-" * 60)
|
| 533 |
-
print(
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
print()
|
| 536 |
print(f"Conservative transforms: {sim_default.transforms}")
|
| 537 |
print(f"Aggressive transforms: {sim_aggressive.transforms}")
|
|
@@ -545,7 +581,9 @@ def main():
|
|
| 545 |
print("1. BASELINE (No Optimization)")
|
| 546 |
print("-" * 70)
|
| 547 |
baseline = run_evaluation(messages, "audit")
|
| 548 |
-
print(
|
|
|
|
|
|
|
| 549 |
print(f"Response: {baseline.response[:300]}...")
|
| 550 |
print()
|
| 551 |
|
|
@@ -553,7 +591,9 @@ def main():
|
|
| 553 |
print("2. CONSERVATIVE OPTIMIZATION (Default Settings)")
|
| 554 |
print("-" * 70)
|
| 555 |
conservative = run_evaluation(messages, "optimize")
|
| 556 |
-
print(
|
|
|
|
|
|
|
| 557 |
print(f"Response: {conservative.response[:300]}...")
|
| 558 |
print()
|
| 559 |
|
|
@@ -561,7 +601,9 @@ def main():
|
|
| 561 |
print("3. AGGRESSIVE OPTIMIZATION (max_array=3, max_string=200, max_depth=3)")
|
| 562 |
print("-" * 70)
|
| 563 |
aggressive = run_aggressive_evaluation(messages, "optimize")
|
| 564 |
-
print(
|
|
|
|
|
|
|
| 565 |
print(f"Response: {aggressive.response[:300]}...")
|
| 566 |
print()
|
| 567 |
|
|
@@ -574,10 +616,18 @@ def main():
|
|
| 574 |
|
| 575 |
print(f"\n{'Metric':<25} {'Baseline':>12} {'Conservative':>12} {'Aggressive':>12}")
|
| 576 |
print("-" * 65)
|
| 577 |
-
print(
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
|
| 582 |
# Savings vs baseline
|
| 583 |
cons_savings = baseline.tokens_input - conservative.tokens_input
|
|
@@ -586,12 +636,16 @@ def main():
|
|
| 586 |
aggr_pct = (aggr_savings / baseline.tokens_input) * 100 if baseline.tokens_input > 0 else 0
|
| 587 |
|
| 588 |
print()
|
| 589 |
-
print(
|
|
|
|
|
|
|
| 590 |
|
| 591 |
cons_cost_save = baseline.cost_estimate - conservative.cost_estimate
|
| 592 |
aggr_cost_save = baseline.cost_estimate - aggressive.cost_estimate
|
| 593 |
|
| 594 |
-
print(
|
|
|
|
|
|
|
| 595 |
print()
|
| 596 |
|
| 597 |
# =========================================================================
|
|
@@ -610,9 +664,9 @@ def main():
|
|
| 610 |
print(f"\n{'Criterion':<20} {'Baseline':>10} {'Conservative':>12} {'Aggressive':>12}")
|
| 611 |
print("-" * 55)
|
| 612 |
for criterion in ["completeness", "accuracy", "clarity", "actionability"]:
|
| 613 |
-
b_score = qual_cons[
|
| 614 |
-
c_score = qual_cons[
|
| 615 |
-
a_score = qual_aggr[
|
| 616 |
print(f"{criterion.title():<20} {b_score:>10} {c_score:>12} {a_score:>12}")
|
| 617 |
|
| 618 |
print()
|
|
|
|
| 37 |
# AGGRESSIVE optimization config
|
| 38 |
aggressive_tool_crusher = ToolCrusherConfig(
|
| 39 |
enabled=True,
|
| 40 |
+
min_tokens_to_crush=100, # Crush smaller outputs
|
| 41 |
+
max_array_items=3, # Only keep first 3 items (was 10)
|
| 42 |
+
max_string_length=200, # Truncate strings > 200 chars (was 1000)
|
| 43 |
+
max_depth=3, # Limit nesting to 3 levels (was 5)
|
| 44 |
)
|
| 45 |
|
| 46 |
db_path = os.path.join(tempfile.gettempdir(), "headroom_eval.db")
|
|
|
|
| 54 |
)
|
| 55 |
|
| 56 |
# Aggressive optimization client
|
|
|
|
| 57 |
aggressive_config = HeadroomConfig()
|
| 58 |
aggressive_config.tool_crusher = aggressive_tool_crusher
|
| 59 |
|
|
|
|
| 66 |
)
|
| 67 |
# Manually set aggressive config on pipeline
|
| 68 |
aggressive_client._config = aggressive_config
|
| 69 |
+
aggressive_client._pipeline = __import__(
|
| 70 |
+
"headroom.transforms", fromlist=["TransformPipeline"]
|
| 71 |
+
).TransformPipeline(aggressive_config, provider=provider)
|
| 72 |
|
| 73 |
|
| 74 |
# =============================================================================
|
| 75 |
# REALISTIC AGENTIC SCENARIO: Research Assistant
|
| 76 |
# =============================================================================
|
| 77 |
|
| 78 |
+
|
| 79 |
def generate_search_results(query: str, count: int = 25) -> str:
|
| 80 |
"""Generate realistic search results JSON."""
|
| 81 |
results = []
|
| 82 |
for i in range(count):
|
| 83 |
+
results.append(
|
| 84 |
+
{
|
| 85 |
+
"id": f"doc_{i:04d}",
|
| 86 |
+
"title": f"Research Paper: {query.title()} - Study {i + 1}",
|
| 87 |
+
"url": f"https://research.example.com/papers/{query.replace(' ', '-')}/{i}",
|
| 88 |
+
"snippet": f"This comprehensive study examines {query} through multiple methodologies. "
|
| 89 |
+
f"Key findings include significant correlations between variables A and B, "
|
| 90 |
+
f"with p-values < 0.05. The sample size of {1000 + i * 100} participants "
|
| 91 |
+
f"provides robust statistical power. Methods included: surveys, interviews, "
|
| 92 |
+
f"longitudinal tracking, and meta-analysis of {50 + i * 10} prior studies.",
|
| 93 |
+
"citations": 150 + i * 23,
|
| 94 |
+
"year": 2020 + (i % 5),
|
| 95 |
+
"authors": [
|
| 96 |
+
{"name": f"Dr. Smith{i}", "affiliation": "MIT"},
|
| 97 |
+
{"name": f"Prof. Jones{i}", "affiliation": "Stanford"},
|
| 98 |
+
{"name": f"Dr. Williams{i}", "affiliation": "Harvard"},
|
| 99 |
+
],
|
| 100 |
+
"keywords": ["machine learning", "data science", query, "research", "analysis"],
|
| 101 |
+
"abstract": f"Abstract for paper {i}: " + "Lorem ipsum dolor sit amet. " * 20,
|
| 102 |
+
"methodology": {
|
| 103 |
+
"type": "mixed-methods",
|
| 104 |
+
"sample_size": 1000 + i * 100,
|
| 105 |
+
"duration_months": 12 + i,
|
| 106 |
+
"instruments": ["survey", "interview", "observation"],
|
| 107 |
+
},
|
| 108 |
+
}
|
| 109 |
+
)
|
| 110 |
return json.dumps({"results": results, "total_count": count, "query": query})
|
| 111 |
|
| 112 |
|
| 113 |
def generate_document_content(doc_id: str) -> str:
|
| 114 |
"""Generate realistic document content."""
|
| 115 |
+
return json.dumps(
|
| 116 |
+
{
|
| 117 |
+
"id": doc_id,
|
| 118 |
+
"full_text": """
|
| 119 |
Introduction:
|
| 120 |
This research investigates the complex interplay between artificial intelligence
|
| 121 |
and human decision-making processes. Our longitudinal study spanning 36 months
|
|
|
|
| 139 |
Conclusion:
|
| 140 |
The integration of AI in decision-making processes offers substantial benefits
|
| 141 |
but requires careful implementation to avoid potential negative outcomes.
|
| 142 |
+
"""
|
| 143 |
+
* 3, # Make it longer
|
| 144 |
+
"metadata": {
|
| 145 |
+
"word_count": 15000,
|
| 146 |
+
"pages": 45,
|
| 147 |
+
"figures": 12,
|
| 148 |
+
"tables": 8,
|
| 149 |
+
"references": 150,
|
| 150 |
+
},
|
| 151 |
+
"sections": [
|
| 152 |
+
{"title": "Introduction", "page": 1, "word_count": 2000},
|
| 153 |
+
{"title": "Literature Review", "page": 5, "word_count": 4000},
|
| 154 |
+
{"title": "Methodology", "page": 15, "word_count": 3000},
|
| 155 |
+
{"title": "Results", "page": 22, "word_count": 3500},
|
| 156 |
+
{"title": "Discussion", "page": 32, "word_count": 2000},
|
| 157 |
+
{"title": "Conclusion", "page": 40, "word_count": 500},
|
| 158 |
+
],
|
| 159 |
+
}
|
| 160 |
+
)
|
| 161 |
|
| 162 |
|
| 163 |
def generate_analytics_data() -> str:
|
| 164 |
"""Generate realistic analytics/metrics data."""
|
| 165 |
+
return json.dumps(
|
| 166 |
+
{
|
| 167 |
+
"summary_statistics": {
|
| 168 |
+
"total_papers_analyzed": 500,
|
| 169 |
+
"date_range": {"start": "2020-01-01", "end": "2024-12-31"},
|
| 170 |
+
"avg_citations": 45.7,
|
| 171 |
+
"median_citations": 32,
|
| 172 |
+
"std_dev": 28.3,
|
| 173 |
+
},
|
| 174 |
+
"trend_analysis": [
|
| 175 |
+
{
|
| 176 |
+
"year": 2020,
|
| 177 |
+
"papers": 80,
|
| 178 |
+
"avg_citations": 52.3,
|
| 179 |
+
"top_keywords": ["covid", "remote", "digital"],
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"year": 2021,
|
| 183 |
+
"papers": 95,
|
| 184 |
+
"avg_citations": 48.1,
|
| 185 |
+
"top_keywords": ["hybrid", "adaptation", "resilience"],
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"year": 2022,
|
| 189 |
+
"papers": 110,
|
| 190 |
+
"avg_citations": 44.2,
|
| 191 |
+
"top_keywords": ["AI", "automation", "efficiency"],
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"year": 2023,
|
| 195 |
+
"papers": 120,
|
| 196 |
+
"avg_citations": 38.5,
|
| 197 |
+
"top_keywords": ["LLM", "generative", "ethics"],
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"year": 2024,
|
| 201 |
+
"papers": 95,
|
| 202 |
+
"avg_citations": 25.1,
|
| 203 |
+
"top_keywords": ["agents", "multimodal", "safety"],
|
| 204 |
+
},
|
| 205 |
+
],
|
| 206 |
+
"citation_distribution": {
|
| 207 |
+
"0-10": 150,
|
| 208 |
+
"11-25": 120,
|
| 209 |
+
"26-50": 100,
|
| 210 |
+
"51-100": 80,
|
| 211 |
+
"101-200": 35,
|
| 212 |
+
"200+": 15,
|
| 213 |
+
},
|
| 214 |
+
"top_authors": [
|
| 215 |
+
{"name": "Dr. Smith", "papers": 25, "total_citations": 1250, "h_index": 18},
|
| 216 |
+
{"name": "Prof. Jones", "papers": 22, "total_citations": 980, "h_index": 15},
|
| 217 |
+
{"name": "Dr. Williams", "papers": 20, "total_citations": 890, "h_index": 14},
|
| 218 |
+
]
|
| 219 |
+
* 5, # More authors
|
| 220 |
+
"collaboration_network": {
|
| 221 |
+
"nodes": 150,
|
| 222 |
+
"edges": 450,
|
| 223 |
+
"avg_degree": 6.0,
|
| 224 |
+
"clustering_coefficient": 0.45,
|
| 225 |
+
},
|
| 226 |
+
}
|
| 227 |
+
)
|
| 228 |
|
| 229 |
|
| 230 |
# =============================================================================
|
| 231 |
# BUILD COMPLEX AGENTIC CONVERSATION
|
| 232 |
# =============================================================================
|
| 233 |
|
| 234 |
+
|
| 235 |
def build_agentic_conversation() -> list[dict]:
|
| 236 |
"""Build a realistic multi-turn agentic conversation."""
|
| 237 |
|
|
|
|
| 244 |
{
|
| 245 |
"role": "user",
|
| 246 |
"content": "Current Date: 2024-12-15. I need you to research the impact of AI on workplace productivity. "
|
| 247 |
+
"Search for recent papers, analyze the top results, and give me a summary.",
|
| 248 |
},
|
|
|
|
| 249 |
# Turn 2: Assistant decides to search
|
| 250 |
{
|
| 251 |
"role": "assistant",
|
| 252 |
"content": [
|
| 253 |
{
|
| 254 |
"type": "text",
|
| 255 |
+
"text": "I'll help you research AI's impact on workplace productivity. Let me search for recent academic papers on this topic.",
|
| 256 |
},
|
| 257 |
{
|
| 258 |
"type": "tool_use",
|
| 259 |
"id": "search_1",
|
| 260 |
"name": "academic_search",
|
| 261 |
+
"input": {"query": "AI impact workplace productivity", "limit": 25},
|
| 262 |
+
},
|
| 263 |
+
],
|
| 264 |
},
|
|
|
|
| 265 |
# Turn 3: Tool result - large search results
|
| 266 |
{
|
| 267 |
"role": "user",
|
|
|
|
| 269 |
{
|
| 270 |
"type": "tool_result",
|
| 271 |
"tool_use_id": "search_1",
|
| 272 |
+
"content": generate_search_results("AI impact workplace productivity", 25),
|
| 273 |
}
|
| 274 |
+
],
|
| 275 |
},
|
|
|
|
| 276 |
# Turn 4: Assistant analyzes and fetches more
|
| 277 |
{
|
| 278 |
"role": "assistant",
|
| 279 |
"content": [
|
| 280 |
{
|
| 281 |
"type": "text",
|
| 282 |
+
"text": "I found 25 relevant papers. Let me fetch the full content of the top 3 most cited papers and get analytics on the overall trends.",
|
| 283 |
},
|
| 284 |
{
|
| 285 |
"type": "tool_use",
|
| 286 |
"id": "fetch_1",
|
| 287 |
"name": "fetch_document",
|
| 288 |
+
"input": {"doc_id": "doc_0001"},
|
| 289 |
},
|
| 290 |
{
|
| 291 |
"type": "tool_use",
|
| 292 |
"id": "fetch_2",
|
| 293 |
"name": "fetch_document",
|
| 294 |
+
"input": {"doc_id": "doc_0002"},
|
| 295 |
},
|
| 296 |
{
|
| 297 |
"type": "tool_use",
|
| 298 |
"id": "analytics_1",
|
| 299 |
"name": "get_analytics",
|
| 300 |
+
"input": {"topic": "AI workplace productivity"},
|
| 301 |
+
},
|
| 302 |
+
],
|
| 303 |
},
|
|
|
|
| 304 |
# Turn 5: Multiple tool results
|
| 305 |
{
|
| 306 |
"role": "user",
|
|
|
|
| 308 |
{
|
| 309 |
"type": "tool_result",
|
| 310 |
"tool_use_id": "fetch_1",
|
| 311 |
+
"content": generate_document_content("doc_0001"),
|
| 312 |
},
|
| 313 |
{
|
| 314 |
"type": "tool_result",
|
| 315 |
"tool_use_id": "fetch_2",
|
| 316 |
+
"content": generate_document_content("doc_0002"),
|
| 317 |
},
|
| 318 |
{
|
| 319 |
"type": "tool_result",
|
| 320 |
"tool_use_id": "analytics_1",
|
| 321 |
+
"content": generate_analytics_data(),
|
| 322 |
+
},
|
| 323 |
+
],
|
| 324 |
},
|
|
|
|
| 325 |
# Turn 6: Assistant provides initial summary
|
| 326 |
{
|
| 327 |
"role": "assistant",
|
| 328 |
"content": "Based on my analysis of 25 papers and detailed review of the top cited works, here's what the research shows:\n\n"
|
| 329 |
+
"**Key Findings:**\n"
|
| 330 |
+
"1. AI tools improve productivity by 25-40% on average\n"
|
| 331 |
+
"2. Biggest gains in data analysis and routine tasks\n"
|
| 332 |
+
"3. Mixed results for creative work\n"
|
| 333 |
+
"4. Training and adoption are critical success factors\n\n"
|
| 334 |
+
"Would you like me to dive deeper into any specific aspect?",
|
| 335 |
},
|
|
|
|
| 336 |
# Turn 7: User asks follow-up
|
| 337 |
{
|
| 338 |
"role": "user",
|
| 339 |
+
"content": "Yes, tell me more about the training and adoption factors. What makes AI adoption successful?",
|
| 340 |
},
|
|
|
|
| 341 |
# Turn 8: Assistant searches for more specific info
|
| 342 |
{
|
| 343 |
"role": "assistant",
|
| 344 |
"content": [
|
| 345 |
{
|
| 346 |
"type": "text",
|
| 347 |
+
"text": "Great question. Let me search for papers specifically about AI adoption success factors.",
|
| 348 |
},
|
| 349 |
{
|
| 350 |
"type": "tool_use",
|
| 351 |
"id": "search_2",
|
| 352 |
"name": "academic_search",
|
| 353 |
+
"input": {
|
| 354 |
+
"query": "AI adoption success factors workplace training",
|
| 355 |
+
"limit": 20,
|
| 356 |
+
},
|
| 357 |
+
},
|
| 358 |
+
],
|
| 359 |
},
|
|
|
|
| 360 |
# Turn 9: More search results
|
| 361 |
{
|
| 362 |
"role": "user",
|
|
|
|
| 364 |
{
|
| 365 |
"type": "tool_result",
|
| 366 |
"tool_use_id": "search_2",
|
| 367 |
+
"content": generate_search_results(
|
| 368 |
+
"AI adoption success factors workplace training", 20
|
| 369 |
+
),
|
| 370 |
}
|
| 371 |
+
],
|
| 372 |
},
|
|
|
|
| 373 |
# Turn 10: Assistant synthesizes
|
| 374 |
{
|
| 375 |
"role": "assistant",
|
| 376 |
"content": "Based on the additional research on AI adoption success factors:\n\n"
|
| 377 |
+
"**Critical Success Factors:**\n\n"
|
| 378 |
+
"1. **Leadership Support** - Executive buy-in correlates with 3x higher adoption rates\n"
|
| 379 |
+
"2. **Gradual Rollout** - Phased implementation outperforms big-bang approaches\n"
|
| 380 |
+
"3. **Training Investment** - Organizations spending >40 hours on training see 50% better outcomes\n"
|
| 381 |
+
"4. **Change Management** - Addressing employee concerns reduces resistance by 60%\n"
|
| 382 |
+
"5. **Clear Use Cases** - Specific, measurable goals improve success rates\n\n"
|
| 383 |
+
"The research consistently shows that technical capabilities matter less than organizational readiness.",
|
| 384 |
},
|
|
|
|
| 385 |
# Turn 11: User asks for final deliverable
|
| 386 |
{
|
| 387 |
"role": "user",
|
| 388 |
+
"content": "Perfect. Now synthesize everything into a brief executive summary I can share with my team.",
|
| 389 |
},
|
| 390 |
]
|
| 391 |
|
|
|
|
| 396 |
# EVALUATION FRAMEWORK
|
| 397 |
# =============================================================================
|
| 398 |
|
| 399 |
+
|
| 400 |
@dataclass
|
| 401 |
class EvalResult:
|
| 402 |
"""Results from a single evaluation run."""
|
| 403 |
+
|
| 404 |
mode: str
|
| 405 |
tokens_input: int
|
| 406 |
tokens_output: int
|
|
|
|
| 474 |
response = base_client.messages.create(
|
| 475 |
model="claude-3-5-haiku-latest",
|
| 476 |
max_tokens=500,
|
| 477 |
+
messages=[{"role": "user", "content": eval_prompt}],
|
| 478 |
)
|
| 479 |
|
| 480 |
try:
|
| 481 |
# Extract JSON from response
|
| 482 |
text = response.content[0].text
|
| 483 |
# Find JSON in response
|
| 484 |
+
start = text.find("{")
|
| 485 |
+
end = text.rfind("}") + 1
|
| 486 |
if start >= 0 and end > start:
|
| 487 |
return json.loads(text[start:end])
|
| 488 |
except (json.JSONDecodeError, IndexError):
|
|
|
|
| 495 |
# MAIN EVALUATION
|
| 496 |
# =============================================================================
|
| 497 |
|
| 498 |
+
|
| 499 |
def run_aggressive_evaluation(messages: list[dict], mode: str) -> EvalResult:
|
| 500 |
"""Run evaluation with aggressive client."""
|
| 501 |
tokenizer = provider.get_token_counter("claude-3-5-haiku-latest")
|
|
|
|
| 537 |
messages = build_agentic_conversation()
|
| 538 |
|
| 539 |
print(f"Scenario: Research Assistant with {len(messages)} turns")
|
| 540 |
+
print("Tool calls: 4 (search x2, fetch x2, analytics x1)")
|
| 541 |
+
print("Tool outputs: Large JSON payloads (~50KB total)")
|
| 542 |
print()
|
| 543 |
|
| 544 |
# =========================================================================
|
|
|
|
| 562 |
|
| 563 |
print(f"\n{'Mode':<20} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}")
|
| 564 |
print("-" * 60)
|
| 565 |
+
print(
|
| 566 |
+
f"{'Conservative':<20} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved / sim_default.tokens_before * 100:>7.1f}%"
|
| 567 |
+
)
|
| 568 |
+
print(
|
| 569 |
+
f"{'Aggressive':<20} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved / sim_aggressive.tokens_before * 100:>7.1f}%"
|
| 570 |
+
)
|
| 571 |
print()
|
| 572 |
print(f"Conservative transforms: {sim_default.transforms}")
|
| 573 |
print(f"Aggressive transforms: {sim_aggressive.transforms}")
|
|
|
|
| 581 |
print("1. BASELINE (No Optimization)")
|
| 582 |
print("-" * 70)
|
| 583 |
baseline = run_evaluation(messages, "audit")
|
| 584 |
+
print(
|
| 585 |
+
f"Input: {baseline.tokens_input:,} tokens | Cost: ${baseline.cost_estimate:.4f} | Latency: {baseline.latency_ms:.0f}ms"
|
| 586 |
+
)
|
| 587 |
print(f"Response: {baseline.response[:300]}...")
|
| 588 |
print()
|
| 589 |
|
|
|
|
| 591 |
print("2. CONSERVATIVE OPTIMIZATION (Default Settings)")
|
| 592 |
print("-" * 70)
|
| 593 |
conservative = run_evaluation(messages, "optimize")
|
| 594 |
+
print(
|
| 595 |
+
f"Input: {conservative.tokens_input:,} tokens | Cost: ${conservative.cost_estimate:.4f} | Latency: {conservative.latency_ms:.0f}ms"
|
| 596 |
+
)
|
| 597 |
print(f"Response: {conservative.response[:300]}...")
|
| 598 |
print()
|
| 599 |
|
|
|
|
| 601 |
print("3. AGGRESSIVE OPTIMIZATION (max_array=3, max_string=200, max_depth=3)")
|
| 602 |
print("-" * 70)
|
| 603 |
aggressive = run_aggressive_evaluation(messages, "optimize")
|
| 604 |
+
print(
|
| 605 |
+
f"Input: {aggressive.tokens_input:,} tokens | Cost: ${aggressive.cost_estimate:.4f} | Latency: {aggressive.latency_ms:.0f}ms"
|
| 606 |
+
)
|
| 607 |
print(f"Response: {aggressive.response[:300]}...")
|
| 608 |
print()
|
| 609 |
|
|
|
|
| 616 |
|
| 617 |
print(f"\n{'Metric':<25} {'Baseline':>12} {'Conservative':>12} {'Aggressive':>12}")
|
| 618 |
print("-" * 65)
|
| 619 |
+
print(
|
| 620 |
+
f"{'Input Tokens':<25} {baseline.tokens_input:>12,} {conservative.tokens_input:>12,} {aggressive.tokens_input:>12,}"
|
| 621 |
+
)
|
| 622 |
+
print(
|
| 623 |
+
f"{'Output Tokens':<25} {baseline.tokens_output:>12,} {conservative.tokens_output:>12,} {aggressive.tokens_output:>12,}"
|
| 624 |
+
)
|
| 625 |
+
print(
|
| 626 |
+
f"{'Cost':<25} ${baseline.cost_estimate:>11.4f} ${conservative.cost_estimate:>11.4f} ${aggressive.cost_estimate:>11.4f}"
|
| 627 |
+
)
|
| 628 |
+
print(
|
| 629 |
+
f"{'Latency (ms)':<25} {baseline.latency_ms:>12.0f} {conservative.latency_ms:>12.0f} {aggressive.latency_ms:>12.0f}"
|
| 630 |
+
)
|
| 631 |
|
| 632 |
# Savings vs baseline
|
| 633 |
cons_savings = baseline.tokens_input - conservative.tokens_input
|
|
|
|
| 636 |
aggr_pct = (aggr_savings / baseline.tokens_input) * 100 if baseline.tokens_input > 0 else 0
|
| 637 |
|
| 638 |
print()
|
| 639 |
+
print(
|
| 640 |
+
f"{'Token Savings vs Baseline':<25} {'-':>12} {cons_savings:>10,} ({cons_pct:.0f}%) {aggr_savings:>10,} ({aggr_pct:.0f}%)"
|
| 641 |
+
)
|
| 642 |
|
| 643 |
cons_cost_save = baseline.cost_estimate - conservative.cost_estimate
|
| 644 |
aggr_cost_save = baseline.cost_estimate - aggressive.cost_estimate
|
| 645 |
|
| 646 |
+
print(
|
| 647 |
+
f"{'Cost Savings vs Baseline':<25} {'-':>12} ${cons_cost_save:>10.4f} ${aggr_cost_save:>10.4f}"
|
| 648 |
+
)
|
| 649 |
print()
|
| 650 |
|
| 651 |
# =========================================================================
|
|
|
|
| 664 |
print(f"\n{'Criterion':<20} {'Baseline':>10} {'Conservative':>12} {'Aggressive':>12}")
|
| 665 |
print("-" * 55)
|
| 666 |
for criterion in ["completeness", "accuracy", "clarity", "actionability"]:
|
| 667 |
+
b_score = qual_cons["baseline"].get(criterion, "N/A")
|
| 668 |
+
c_score = qual_cons["optimized"].get(criterion, "N/A")
|
| 669 |
+
a_score = qual_aggr["optimized"].get(criterion, "N/A")
|
| 670 |
print(f"{criterion.title():<20} {b_score:>10} {c_score:>12} {a_score:>12}")
|
| 671 |
|
| 672 |
print()
|
examples/real_world_openai_eval.py
CHANGED
|
@@ -27,6 +27,7 @@ from openai import OpenAI
|
|
| 27 |
|
| 28 |
from headroom import HeadroomClient, OpenAIProvider, ToolCrusherConfig
|
| 29 |
from headroom.config import HeadroomConfig
|
|
|
|
| 30 |
|
| 31 |
load_dotenv(".env.local")
|
| 32 |
|
|
@@ -59,7 +60,6 @@ aggressive_client = HeadroomClient(
|
|
| 59 |
default_mode="audit",
|
| 60 |
)
|
| 61 |
aggressive_client._config = aggressive_config
|
| 62 |
-
from headroom.transforms import TransformPipeline
|
| 63 |
aggressive_client._pipeline = TransformPipeline(aggressive_config, provider=provider)
|
| 64 |
|
| 65 |
|
|
@@ -67,6 +67,7 @@ aggressive_client._pipeline = TransformPipeline(aggressive_config, provider=prov
|
|
| 67 |
# REALISTIC TOOL OUTPUTS - Based on actual production systems
|
| 68 |
# =============================================================================
|
| 69 |
|
|
|
|
| 70 |
def generate_metrics_response() -> str:
|
| 71 |
"""
|
| 72 |
Realistic Prometheus/Datadog metrics query response.
|
|
@@ -80,58 +81,71 @@ def generate_metrics_response() -> str:
|
|
| 80 |
ts = base_time + timedelta(minutes=i)
|
| 81 |
# Simulate spike around minute 45
|
| 82 |
value = 45 + (i * 0.5) if i < 45 else 85 + (i - 45) * 2
|
| 83 |
-
cpu_data.append(
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
|
| 89 |
# Memory metrics
|
| 90 |
memory_data = []
|
| 91 |
for i in range(60):
|
| 92 |
ts = base_time + timedelta(minutes=i)
|
| 93 |
value = 62 + (i * 0.3)
|
| 94 |
-
memory_data.append(
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
|
| 100 |
# Request latency (p99)
|
| 101 |
latency_data = []
|
| 102 |
for i in range(60):
|
| 103 |
ts = base_time + timedelta(minutes=i)
|
| 104 |
value = 120 if i < 45 else 450 + (i - 45) * 50
|
| 105 |
-
latency_data.append(
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# Error rate
|
| 112 |
error_data = []
|
| 113 |
for i in range(60):
|
| 114 |
ts = base_time + timedelta(minutes=i)
|
| 115 |
value = 0.1 if i < 45 else 2.5 + (i - 45) * 0.5
|
| 116 |
-
error_data.append(
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
"
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
def generate_logs_response() -> str:
|
|
@@ -144,7 +158,11 @@ def generate_logs_response() -> str:
|
|
| 144 |
logs = []
|
| 145 |
log_templates = [
|
| 146 |
("ERROR", "Connection pool exhausted, waiting for available connection", "api-server"),
|
| 147 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
("ERROR", "Database connection timeout after 30000ms", "api-server"),
|
| 149 |
("INFO", "Retry attempt 1/3 for database connection", "api-server"),
|
| 150 |
("ERROR", "Max retries exceeded for database operation", "api-server"),
|
|
@@ -164,103 +182,161 @@ def generate_logs_response() -> str:
|
|
| 164 |
ts = base_time + timedelta(seconds=i * 45)
|
| 165 |
level, msg, source = log_templates[i % len(log_templates)]
|
| 166 |
|
| 167 |
-
logs.append(
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
"
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
"
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
return json.dumps(
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
"
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
-
|
| 200 |
|
| 201 |
|
| 202 |
def generate_service_status() -> str:
|
| 203 |
"""
|
| 204 |
Realistic health check / service status response.
|
| 205 |
"""
|
| 206 |
-
return json.dumps(
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
|
| 266 |
def generate_deployments_response() -> str:
|
|
@@ -272,84 +348,86 @@ def generate_deployments_response() -> str:
|
|
| 272 |
deployments = []
|
| 273 |
for i in range(15):
|
| 274 |
ts = base_time - timedelta(hours=i * 4)
|
| 275 |
-
deployments.append(
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
"
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
"
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
| 298 |
|
| 299 |
-
return json.dumps(
|
| 300 |
-
"deployments": deployments,
|
| 301 |
-
|
| 302 |
-
"page": 1,
|
| 303 |
-
"per_page": 20
|
| 304 |
-
})
|
| 305 |
|
| 306 |
|
| 307 |
def generate_runbook_response() -> str:
|
| 308 |
"""
|
| 309 |
Realistic runbook/documentation lookup.
|
| 310 |
"""
|
| 311 |
-
return json.dumps(
|
| 312 |
-
|
| 313 |
-
"
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
"
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
"
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
"
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
"
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
}
|
| 346 |
-
|
| 347 |
|
| 348 |
|
| 349 |
# =============================================================================
|
| 350 |
# BUILD REALISTIC INCIDENT RESPONSE CONVERSATION
|
| 351 |
# =============================================================================
|
| 352 |
|
|
|
|
| 353 |
def build_incident_conversation() -> list[dict]:
|
| 354 |
"""
|
| 355 |
Build a realistic incident response agentic conversation.
|
|
@@ -373,15 +451,13 @@ You have access to the following tools:
|
|
| 373 |
- query_deployments: Get recent deployment history
|
| 374 |
- get_runbook: Lookup runbook documentation
|
| 375 |
|
| 376 |
-
Always be concise and focus on actionable insights."""
|
| 377 |
},
|
| 378 |
-
|
| 379 |
# User reports incident
|
| 380 |
{
|
| 381 |
"role": "user",
|
| 382 |
-
"content": "We're seeing elevated error rates on the API. Users reporting timeouts. Can you investigate?"
|
| 383 |
},
|
| 384 |
-
|
| 385 |
# Agent starts investigation - queries metrics
|
| 386 |
{
|
| 387 |
"role": "assistant",
|
|
@@ -392,22 +468,18 @@ Always be concise and focus on actionable insights."""
|
|
| 392 |
"type": "function",
|
| 393 |
"function": {
|
| 394 |
"name": "query_metrics",
|
| 395 |
-
"arguments": json.dumps(
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
| 400 |
}
|
| 401 |
-
]
|
| 402 |
},
|
| 403 |
-
|
| 404 |
# Metrics response
|
| 405 |
-
{
|
| 406 |
-
"role": "tool",
|
| 407 |
-
"tool_call_id": "call_metrics_1",
|
| 408 |
-
"content": generate_metrics_response()
|
| 409 |
-
},
|
| 410 |
-
|
| 411 |
# Agent analyzes and queries logs
|
| 412 |
{
|
| 413 |
"role": "assistant",
|
|
@@ -418,23 +490,19 @@ Always be concise and focus on actionable insights."""
|
|
| 418 |
"type": "function",
|
| 419 |
"function": {
|
| 420 |
"name": "search_logs",
|
| 421 |
-
"arguments": json.dumps(
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
| 427 |
}
|
| 428 |
-
]
|
| 429 |
},
|
| 430 |
-
|
| 431 |
# Logs response
|
| 432 |
-
{
|
| 433 |
-
"role": "tool",
|
| 434 |
-
"tool_call_id": "call_logs_1",
|
| 435 |
-
"content": generate_logs_response()
|
| 436 |
-
},
|
| 437 |
-
|
| 438 |
# Agent identifies root cause, checks service status
|
| 439 |
{
|
| 440 |
"role": "assistant",
|
|
@@ -443,21 +511,12 @@ Always be concise and focus on actionable insights."""
|
|
| 443 |
{
|
| 444 |
"id": "call_status_1",
|
| 445 |
"type": "function",
|
| 446 |
-
"function": {
|
| 447 |
-
"name": "get_service_status",
|
| 448 |
-
"arguments": json.dumps({})
|
| 449 |
-
}
|
| 450 |
}
|
| 451 |
-
]
|
| 452 |
},
|
| 453 |
-
|
| 454 |
# Service status response
|
| 455 |
-
{
|
| 456 |
-
"role": "tool",
|
| 457 |
-
"tool_call_id": "call_status_1",
|
| 458 |
-
"content": generate_service_status()
|
| 459 |
-
},
|
| 460 |
-
|
| 461 |
# Agent checks for recent deployments
|
| 462 |
{
|
| 463 |
"role": "assistant",
|
|
@@ -468,19 +527,17 @@ Always be concise and focus on actionable insights."""
|
|
| 468 |
"type": "function",
|
| 469 |
"function": {
|
| 470 |
"name": "query_deployments",
|
| 471 |
-
"arguments": json.dumps({"service": "api-server", "limit": 15})
|
| 472 |
-
}
|
| 473 |
}
|
| 474 |
-
]
|
| 475 |
},
|
| 476 |
-
|
| 477 |
# Deployments response
|
| 478 |
{
|
| 479 |
"role": "tool",
|
| 480 |
"tool_call_id": "call_deploys_1",
|
| 481 |
-
"content": generate_deployments_response()
|
| 482 |
},
|
| 483 |
-
|
| 484 |
# Agent gets runbook
|
| 485 |
{
|
| 486 |
"role": "assistant",
|
|
@@ -491,19 +548,13 @@ Always be concise and focus on actionable insights."""
|
|
| 491 |
"type": "function",
|
| 492 |
"function": {
|
| 493 |
"name": "get_runbook",
|
| 494 |
-
"arguments": json.dumps({"search": "database connection pool exhaustion"})
|
| 495 |
-
}
|
| 496 |
}
|
| 497 |
-
]
|
| 498 |
},
|
| 499 |
-
|
| 500 |
# Runbook response
|
| 501 |
-
{
|
| 502 |
-
"role": "tool",
|
| 503 |
-
"tool_call_id": "call_runbook_1",
|
| 504 |
-
"content": generate_runbook_response()
|
| 505 |
-
},
|
| 506 |
-
|
| 507 |
# Agent provides final analysis
|
| 508 |
{
|
| 509 |
"role": "assistant",
|
|
@@ -521,13 +572,12 @@ Always be concise and focus on actionable insights."""
|
|
| 521 |
2. Scale API servers to 5 replicas to distribute load
|
| 522 |
3. Consider rolling back deploy-999 if connection pool change is confirmed
|
| 523 |
|
| 524 |
-
Shall I proceed with the remediation steps?"""
|
| 525 |
},
|
| 526 |
-
|
| 527 |
# User asks for action
|
| 528 |
{
|
| 529 |
"role": "user",
|
| 530 |
-
"content": "Yes, give me the exact commands to run and summarize the incident for the post-mortem."
|
| 531 |
},
|
| 532 |
]
|
| 533 |
|
|
@@ -538,6 +588,7 @@ Shall I proceed with the remediation steps?"""
|
|
| 538 |
# EVALUATION
|
| 539 |
# =============================================================================
|
| 540 |
|
|
|
|
| 541 |
@dataclass
|
| 542 |
class EvalResult:
|
| 543 |
mode: str
|
|
@@ -548,7 +599,9 @@ class EvalResult:
|
|
| 548 |
cost_estimate: float
|
| 549 |
|
| 550 |
|
| 551 |
-
def evaluate_response_quality(
|
|
|
|
|
|
|
| 552 |
"""
|
| 553 |
Use GPT-4o as judge to evaluate if the optimized response maintains quality.
|
| 554 |
"""
|
|
@@ -595,10 +648,14 @@ PASS means overall_score >= 4.0, FAIL means < 4.0."""
|
|
| 595 |
)
|
| 596 |
|
| 597 |
import json as json_module
|
|
|
|
| 598 |
try:
|
| 599 |
return json_module.loads(response.choices[0].message.content)
|
| 600 |
-
except:
|
| 601 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 602 |
|
| 603 |
|
| 604 |
def run_eval(messages: list[dict], mode: str, use_aggressive: bool = False) -> EvalResult:
|
|
@@ -652,12 +709,18 @@ def main():
|
|
| 652 |
print("-" * 70)
|
| 653 |
|
| 654 |
sim_default = client.chat.completions.simulate(model="gpt-4o-mini", messages=messages)
|
| 655 |
-
sim_aggressive = aggressive_client.chat.completions.simulate(
|
|
|
|
|
|
|
| 656 |
|
| 657 |
print(f"\n{'Mode':<15} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}")
|
| 658 |
print("-" * 55)
|
| 659 |
-
print(
|
| 660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
print(f"\nTransforms: {sim_default.transforms}")
|
| 662 |
print()
|
| 663 |
|
|
@@ -679,7 +742,9 @@ def main():
|
|
| 679 |
print("\n3. AGGRESSIVE OPTIMIZATION...")
|
| 680 |
aggressive_opt = run_eval(messages, "optimize", use_aggressive=True)
|
| 681 |
print(f" Tokens: {aggressive_opt.tokens_input:,} in / {aggressive_opt.tokens_output:,} out")
|
| 682 |
-
print(
|
|
|
|
|
|
|
| 683 |
|
| 684 |
# Results table
|
| 685 |
print()
|
|
@@ -694,12 +759,22 @@ def main():
|
|
| 694 |
|
| 695 |
print(f"\n{'Metric':<20} {'Baseline':>12} {'Default Opt':>12} {'Aggressive':>12}")
|
| 696 |
print("-" * 60)
|
| 697 |
-
print(
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
print()
|
| 702 |
-
print(
|
|
|
|
|
|
|
| 703 |
|
| 704 |
# Show responses
|
| 705 |
print()
|
|
@@ -725,7 +800,9 @@ def main():
|
|
| 725 |
default_eval = evaluate_response_quality(baseline.response, default_opt.response, "default")
|
| 726 |
|
| 727 |
print("\nEvaluating AGGRESSIVE optimization vs Baseline...")
|
| 728 |
-
aggressive_eval = evaluate_response_quality(
|
|
|
|
|
|
|
| 729 |
|
| 730 |
print(f"\n{'Criterion':<20} {'Default':>12} {'Aggressive':>12}")
|
| 731 |
print("-" * 46)
|
|
@@ -796,8 +873,8 @@ Cost Impact @ 1K requests/day:
|
|
| 796 |
- Monthly savings: ${cost_save_monthly:.2f}
|
| 797 |
|
| 798 |
CONCLUSION:
|
| 799 |
-
{
|
| 800 |
-
{
|
| 801 |
""")
|
| 802 |
|
| 803 |
|
|
|
|
| 27 |
|
| 28 |
from headroom import HeadroomClient, OpenAIProvider, ToolCrusherConfig
|
| 29 |
from headroom.config import HeadroomConfig
|
| 30 |
+
from headroom.transforms import TransformPipeline
|
| 31 |
|
| 32 |
load_dotenv(".env.local")
|
| 33 |
|
|
|
|
| 60 |
default_mode="audit",
|
| 61 |
)
|
| 62 |
aggressive_client._config = aggressive_config
|
|
|
|
| 63 |
aggressive_client._pipeline = TransformPipeline(aggressive_config, provider=provider)
|
| 64 |
|
| 65 |
|
|
|
|
| 67 |
# REALISTIC TOOL OUTPUTS - Based on actual production systems
|
| 68 |
# =============================================================================
|
| 69 |
|
| 70 |
+
|
| 71 |
def generate_metrics_response() -> str:
|
| 72 |
"""
|
| 73 |
Realistic Prometheus/Datadog metrics query response.
|
|
|
|
| 81 |
ts = base_time + timedelta(minutes=i)
|
| 82 |
# Simulate spike around minute 45
|
| 83 |
value = 45 + (i * 0.5) if i < 45 else 85 + (i - 45) * 2
|
| 84 |
+
cpu_data.append(
|
| 85 |
+
{
|
| 86 |
+
"timestamp": ts.isoformat(),
|
| 87 |
+
"value": min(value, 98),
|
| 88 |
+
"labels": {"instance": "prod-api-1", "job": "api-server"},
|
| 89 |
+
}
|
| 90 |
+
)
|
| 91 |
|
| 92 |
# Memory metrics
|
| 93 |
memory_data = []
|
| 94 |
for i in range(60):
|
| 95 |
ts = base_time + timedelta(minutes=i)
|
| 96 |
value = 62 + (i * 0.3)
|
| 97 |
+
memory_data.append(
|
| 98 |
+
{
|
| 99 |
+
"timestamp": ts.isoformat(),
|
| 100 |
+
"value": min(value, 89),
|
| 101 |
+
"labels": {"instance": "prod-api-1", "job": "api-server"},
|
| 102 |
+
}
|
| 103 |
+
)
|
| 104 |
|
| 105 |
# Request latency (p99)
|
| 106 |
latency_data = []
|
| 107 |
for i in range(60):
|
| 108 |
ts = base_time + timedelta(minutes=i)
|
| 109 |
value = 120 if i < 45 else 450 + (i - 45) * 50
|
| 110 |
+
latency_data.append(
|
| 111 |
+
{
|
| 112 |
+
"timestamp": ts.isoformat(),
|
| 113 |
+
"value": min(value, 2500),
|
| 114 |
+
"labels": {"instance": "prod-api-1", "endpoint": "/api/v1/users"},
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
|
| 118 |
# Error rate
|
| 119 |
error_data = []
|
| 120 |
for i in range(60):
|
| 121 |
ts = base_time + timedelta(minutes=i)
|
| 122 |
value = 0.1 if i < 45 else 2.5 + (i - 45) * 0.5
|
| 123 |
+
error_data.append(
|
| 124 |
+
{
|
| 125 |
+
"timestamp": ts.isoformat(),
|
| 126 |
+
"value": min(value, 15),
|
| 127 |
+
"labels": {"instance": "prod-api-1", "status_code": "5xx"},
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
return json.dumps(
|
| 132 |
+
{
|
| 133 |
+
"status": "success",
|
| 134 |
+
"data": {
|
| 135 |
+
"resultType": "matrix",
|
| 136 |
+
"result": [
|
| 137 |
+
{"metric": {"__name__": "cpu_usage_percent"}, "values": cpu_data},
|
| 138 |
+
{"metric": {"__name__": "memory_usage_percent"}, "values": memory_data},
|
| 139 |
+
{
|
| 140 |
+
"metric": {"__name__": "http_request_duration_p99_ms"},
|
| 141 |
+
"values": latency_data,
|
| 142 |
+
},
|
| 143 |
+
{"metric": {"__name__": "http_errors_rate_percent"}, "values": error_data},
|
| 144 |
+
],
|
| 145 |
+
},
|
| 146 |
+
"query_time_ms": 127,
|
| 147 |
+
}
|
| 148 |
+
)
|
| 149 |
|
| 150 |
|
| 151 |
def generate_logs_response() -> str:
|
|
|
|
| 158 |
logs = []
|
| 159 |
log_templates = [
|
| 160 |
("ERROR", "Connection pool exhausted, waiting for available connection", "api-server"),
|
| 161 |
+
(
|
| 162 |
+
"WARN",
|
| 163 |
+
"Slow query detected: SELECT * FROM users WHERE status = 'active' took 2.3s",
|
| 164 |
+
"api-server",
|
| 165 |
+
),
|
| 166 |
("ERROR", "Database connection timeout after 30000ms", "api-server"),
|
| 167 |
("INFO", "Retry attempt 1/3 for database connection", "api-server"),
|
| 168 |
("ERROR", "Max retries exceeded for database operation", "api-server"),
|
|
|
|
| 182 |
ts = base_time + timedelta(seconds=i * 45)
|
| 183 |
level, msg, source = log_templates[i % len(log_templates)]
|
| 184 |
|
| 185 |
+
logs.append(
|
| 186 |
+
{
|
| 187 |
+
"@timestamp": ts.isoformat(),
|
| 188 |
+
"level": level,
|
| 189 |
+
"message": msg,
|
| 190 |
+
"service": source,
|
| 191 |
+
"trace_id": f"trace-{1000 + i:04d}-abcd-{i:04d}",
|
| 192 |
+
"span_id": f"span-{i:04d}",
|
| 193 |
+
"host": f"prod-{source}-{i % 3 + 1}",
|
| 194 |
+
"environment": "production",
|
| 195 |
+
"version": "2.4.1",
|
| 196 |
+
"kubernetes": {
|
| 197 |
+
"namespace": "production",
|
| 198 |
+
"pod": f"{source}-{i % 5 + 1}-abc123",
|
| 199 |
+
"container": source,
|
| 200 |
+
"node": f"node-{i % 3 + 1}.prod.internal",
|
| 201 |
+
},
|
| 202 |
+
"request": {
|
| 203 |
+
"method": "GET" if i % 2 == 0 else "POST",
|
| 204 |
+
"path": "/api/v1/users" if i % 3 == 0 else "/api/v1/orders",
|
| 205 |
+
"status_code": 500 if level == "ERROR" else 200,
|
| 206 |
+
"duration_ms": 150 + (i * 100) if level != "ERROR" else 30000,
|
| 207 |
+
},
|
| 208 |
}
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
return json.dumps(
|
| 212 |
+
{
|
| 213 |
+
"took": 234,
|
| 214 |
+
"timed_out": False,
|
| 215 |
+
"hits": {
|
| 216 |
+
"total": {"value": len(logs), "relation": "eq"},
|
| 217 |
+
"max_score": 1.0,
|
| 218 |
+
"hits": logs,
|
| 219 |
+
},
|
| 220 |
}
|
| 221 |
+
)
|
| 222 |
|
| 223 |
|
| 224 |
def generate_service_status() -> str:
|
| 225 |
"""
|
| 226 |
Realistic health check / service status response.
|
| 227 |
"""
|
| 228 |
+
return json.dumps(
|
| 229 |
+
{
|
| 230 |
+
"services": [
|
| 231 |
+
{
|
| 232 |
+
"name": "api-server",
|
| 233 |
+
"status": "degraded",
|
| 234 |
+
"instances": [
|
| 235 |
+
{
|
| 236 |
+
"id": "api-1",
|
| 237 |
+
"status": "unhealthy",
|
| 238 |
+
"cpu": 94,
|
| 239 |
+
"memory": 87,
|
| 240 |
+
"connections": 500,
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"id": "api-2",
|
| 244 |
+
"status": "healthy",
|
| 245 |
+
"cpu": 45,
|
| 246 |
+
"memory": 62,
|
| 247 |
+
"connections": 150,
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "api-3",
|
| 251 |
+
"status": "unhealthy",
|
| 252 |
+
"cpu": 91,
|
| 253 |
+
"memory": 85,
|
| 254 |
+
"connections": 480,
|
| 255 |
+
},
|
| 256 |
+
],
|
| 257 |
+
"last_check": datetime.now().isoformat(),
|
| 258 |
+
"error_rate": 12.5,
|
| 259 |
+
"p99_latency_ms": 2100,
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"name": "database-primary",
|
| 263 |
+
"status": "critical",
|
| 264 |
+
"instances": [
|
| 265 |
+
{
|
| 266 |
+
"id": "db-primary",
|
| 267 |
+
"status": "unhealthy",
|
| 268 |
+
"connections": 500,
|
| 269 |
+
"max_connections": 500,
|
| 270 |
+
"replication_lag_ms": 0,
|
| 271 |
+
"disk_usage_percent": 78,
|
| 272 |
+
}
|
| 273 |
+
],
|
| 274 |
+
"last_check": datetime.now().isoformat(),
|
| 275 |
+
"active_queries": 487,
|
| 276 |
+
"blocked_queries": 52,
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"name": "database-replica",
|
| 280 |
+
"status": "healthy",
|
| 281 |
+
"instances": [
|
| 282 |
+
{
|
| 283 |
+
"id": "db-replica-1",
|
| 284 |
+
"status": "healthy",
|
| 285 |
+
"connections": 120,
|
| 286 |
+
"max_connections": 500,
|
| 287 |
+
"replication_lag_ms": 150,
|
| 288 |
+
"disk_usage_percent": 76,
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"id": "db-replica-2",
|
| 292 |
+
"status": "healthy",
|
| 293 |
+
"connections": 115,
|
| 294 |
+
"max_connections": 500,
|
| 295 |
+
"replication_lag_ms": 180,
|
| 296 |
+
"disk_usage_percent": 77,
|
| 297 |
+
},
|
| 298 |
+
],
|
| 299 |
+
"last_check": datetime.now().isoformat(),
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"name": "redis-cache",
|
| 303 |
+
"status": "healthy",
|
| 304 |
+
"instances": [
|
| 305 |
+
{
|
| 306 |
+
"id": "redis-1",
|
| 307 |
+
"status": "healthy",
|
| 308 |
+
"memory_used_mb": 2048,
|
| 309 |
+
"memory_max_mb": 4096,
|
| 310 |
+
"connected_clients": 45,
|
| 311 |
+
"hit_rate": 0.94,
|
| 312 |
+
}
|
| 313 |
+
],
|
| 314 |
+
"last_check": datetime.now().isoformat(),
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"name": "nginx-ingress",
|
| 318 |
+
"status": "healthy",
|
| 319 |
+
"instances": [
|
| 320 |
+
{
|
| 321 |
+
"id": "nginx-1",
|
| 322 |
+
"status": "healthy",
|
| 323 |
+
"active_connections": 1250,
|
| 324 |
+
"requests_per_sec": 450,
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"id": "nginx-2",
|
| 328 |
+
"status": "healthy",
|
| 329 |
+
"active_connections": 1180,
|
| 330 |
+
"requests_per_sec": 420,
|
| 331 |
+
},
|
| 332 |
+
],
|
| 333 |
+
"last_check": datetime.now().isoformat(),
|
| 334 |
+
},
|
| 335 |
+
],
|
| 336 |
+
"overall_status": "critical",
|
| 337 |
+
"timestamp": datetime.now().isoformat(),
|
| 338 |
+
}
|
| 339 |
+
)
|
| 340 |
|
| 341 |
|
| 342 |
def generate_deployments_response() -> str:
|
|
|
|
| 348 |
deployments = []
|
| 349 |
for i in range(15):
|
| 350 |
ts = base_time - timedelta(hours=i * 4)
|
| 351 |
+
deployments.append(
|
| 352 |
+
{
|
| 353 |
+
"id": f"deploy-{1000 - i}",
|
| 354 |
+
"service": "api-server" if i % 3 != 2 else "database-migration",
|
| 355 |
+
"version": f"2.4.{15 - i}",
|
| 356 |
+
"status": "success" if i != 1 else "success", # Recent deploy
|
| 357 |
+
"timestamp": ts.isoformat(),
|
| 358 |
+
"commit": f"abc{i:04d}def",
|
| 359 |
+
"author": f"dev{i % 5 + 1}@company.com",
|
| 360 |
+
"message": [
|
| 361 |
+
"feat: Add new user endpoint",
|
| 362 |
+
"fix: Connection pool sizing",
|
| 363 |
+
"chore: Update dependencies",
|
| 364 |
+
"feat: Implement caching layer",
|
| 365 |
+
"fix: Memory leak in request handler",
|
| 366 |
+
][i % 5],
|
| 367 |
+
"changes": {
|
| 368 |
+
"files_changed": 5 + i,
|
| 369 |
+
"insertions": 100 + i * 20,
|
| 370 |
+
"deletions": 30 + i * 5,
|
| 371 |
+
},
|
| 372 |
+
"rollback_available": True,
|
| 373 |
+
"canary_status": "completed" if i > 0 else "in_progress",
|
| 374 |
+
}
|
| 375 |
+
)
|
| 376 |
|
| 377 |
+
return json.dumps(
|
| 378 |
+
{"deployments": deployments, "total_count": len(deployments), "page": 1, "per_page": 20}
|
| 379 |
+
)
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
|
| 382 |
def generate_runbook_response() -> str:
|
| 383 |
"""
|
| 384 |
Realistic runbook/documentation lookup.
|
| 385 |
"""
|
| 386 |
+
return json.dumps(
|
| 387 |
+
{
|
| 388 |
+
"runbook": {
|
| 389 |
+
"title": "Database Connection Pool Exhaustion",
|
| 390 |
+
"id": "RUN-DB-001",
|
| 391 |
+
"severity": "P1",
|
| 392 |
+
"last_updated": "2024-11-15",
|
| 393 |
+
"owner": "platform-team",
|
| 394 |
+
"symptoms": [
|
| 395 |
+
"High error rate on API endpoints",
|
| 396 |
+
"Connection timeout errors in logs",
|
| 397 |
+
"Database showing max connections reached",
|
| 398 |
+
"Increased latency across all services",
|
| 399 |
+
],
|
| 400 |
+
"diagnosis_steps": [
|
| 401 |
+
"1. Check current connection count: SELECT count(*) FROM pg_stat_activity",
|
| 402 |
+
"2. Identify connection holders: SELECT * FROM pg_stat_activity WHERE state != 'idle'",
|
| 403 |
+
"3. Check for long-running queries: SELECT * FROM pg_stat_activity WHERE state = 'active' AND query_start < now() - interval '1 minute'",
|
| 404 |
+
"4. Verify connection pool settings in application config",
|
| 405 |
+
"5. Check for connection leaks in recent deployments",
|
| 406 |
+
],
|
| 407 |
+
"remediation_steps": [
|
| 408 |
+
"1. IMMEDIATE: Kill idle connections older than 10 minutes",
|
| 409 |
+
"2. IMMEDIATE: Scale up API server replicas to distribute load",
|
| 410 |
+
"3. SHORT-TERM: Increase max_connections on database (requires restart)",
|
| 411 |
+
"4. SHORT-TERM: Review and optimize connection pool settings",
|
| 412 |
+
"5. LONG-TERM: Implement connection pooler (PgBouncer)",
|
| 413 |
+
],
|
| 414 |
+
"commands": {
|
| 415 |
+
"kill_idle_connections": "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND query_start < now() - interval '10 minutes'",
|
| 416 |
+
"check_pool_settings": "kubectl get configmap api-server-config -o yaml | grep -A5 'database'",
|
| 417 |
+
"scale_replicas": "kubectl scale deployment api-server --replicas=5",
|
| 418 |
+
},
|
| 419 |
+
"related_incidents": ["INC-2024-089", "INC-2024-056", "INC-2024-023"],
|
| 420 |
+
"escalation_path": ["on-call-sre", "database-team", "platform-lead"],
|
| 421 |
+
}
|
| 422 |
}
|
| 423 |
+
)
|
| 424 |
|
| 425 |
|
| 426 |
# =============================================================================
|
| 427 |
# BUILD REALISTIC INCIDENT RESPONSE CONVERSATION
|
| 428 |
# =============================================================================
|
| 429 |
|
| 430 |
+
|
| 431 |
def build_incident_conversation() -> list[dict]:
|
| 432 |
"""
|
| 433 |
Build a realistic incident response agentic conversation.
|
|
|
|
| 451 |
- query_deployments: Get recent deployment history
|
| 452 |
- get_runbook: Lookup runbook documentation
|
| 453 |
|
| 454 |
+
Always be concise and focus on actionable insights.""",
|
| 455 |
},
|
|
|
|
| 456 |
# User reports incident
|
| 457 |
{
|
| 458 |
"role": "user",
|
| 459 |
+
"content": "We're seeing elevated error rates on the API. Users reporting timeouts. Can you investigate?",
|
| 460 |
},
|
|
|
|
| 461 |
# Agent starts investigation - queries metrics
|
| 462 |
{
|
| 463 |
"role": "assistant",
|
|
|
|
| 468 |
"type": "function",
|
| 469 |
"function": {
|
| 470 |
"name": "query_metrics",
|
| 471 |
+
"arguments": json.dumps(
|
| 472 |
+
{
|
| 473 |
+
"query": "api_error_rate, cpu_usage, memory_usage, latency_p99",
|
| 474 |
+
"time_range": "1h",
|
| 475 |
+
}
|
| 476 |
+
),
|
| 477 |
+
},
|
| 478 |
}
|
| 479 |
+
],
|
| 480 |
},
|
|
|
|
| 481 |
# Metrics response
|
| 482 |
+
{"role": "tool", "tool_call_id": "call_metrics_1", "content": generate_metrics_response()},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
# Agent analyzes and queries logs
|
| 484 |
{
|
| 485 |
"role": "assistant",
|
|
|
|
| 490 |
"type": "function",
|
| 491 |
"function": {
|
| 492 |
"name": "search_logs",
|
| 493 |
+
"arguments": json.dumps(
|
| 494 |
+
{
|
| 495 |
+
"query": "level:ERROR OR level:WARN",
|
| 496 |
+
"time_range": "30m",
|
| 497 |
+
"services": ["api-server", "database"],
|
| 498 |
+
}
|
| 499 |
+
),
|
| 500 |
+
},
|
| 501 |
}
|
| 502 |
+
],
|
| 503 |
},
|
|
|
|
| 504 |
# Logs response
|
| 505 |
+
{"role": "tool", "tool_call_id": "call_logs_1", "content": generate_logs_response()},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
# Agent identifies root cause, checks service status
|
| 507 |
{
|
| 508 |
"role": "assistant",
|
|
|
|
| 511 |
{
|
| 512 |
"id": "call_status_1",
|
| 513 |
"type": "function",
|
| 514 |
+
"function": {"name": "get_service_status", "arguments": json.dumps({})},
|
|
|
|
|
|
|
|
|
|
| 515 |
}
|
| 516 |
+
],
|
| 517 |
},
|
|
|
|
| 518 |
# Service status response
|
| 519 |
+
{"role": "tool", "tool_call_id": "call_status_1", "content": generate_service_status()},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
# Agent checks for recent deployments
|
| 521 |
{
|
| 522 |
"role": "assistant",
|
|
|
|
| 527 |
"type": "function",
|
| 528 |
"function": {
|
| 529 |
"name": "query_deployments",
|
| 530 |
+
"arguments": json.dumps({"service": "api-server", "limit": 15}),
|
| 531 |
+
},
|
| 532 |
}
|
| 533 |
+
],
|
| 534 |
},
|
|
|
|
| 535 |
# Deployments response
|
| 536 |
{
|
| 537 |
"role": "tool",
|
| 538 |
"tool_call_id": "call_deploys_1",
|
| 539 |
+
"content": generate_deployments_response(),
|
| 540 |
},
|
|
|
|
| 541 |
# Agent gets runbook
|
| 542 |
{
|
| 543 |
"role": "assistant",
|
|
|
|
| 548 |
"type": "function",
|
| 549 |
"function": {
|
| 550 |
"name": "get_runbook",
|
| 551 |
+
"arguments": json.dumps({"search": "database connection pool exhaustion"}),
|
| 552 |
+
},
|
| 553 |
}
|
| 554 |
+
],
|
| 555 |
},
|
|
|
|
| 556 |
# Runbook response
|
| 557 |
+
{"role": "tool", "tool_call_id": "call_runbook_1", "content": generate_runbook_response()},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
# Agent provides final analysis
|
| 559 |
{
|
| 560 |
"role": "assistant",
|
|
|
|
| 572 |
2. Scale API servers to 5 replicas to distribute load
|
| 573 |
3. Consider rolling back deploy-999 if connection pool change is confirmed
|
| 574 |
|
| 575 |
+
Shall I proceed with the remediation steps?""",
|
| 576 |
},
|
|
|
|
| 577 |
# User asks for action
|
| 578 |
{
|
| 579 |
"role": "user",
|
| 580 |
+
"content": "Yes, give me the exact commands to run and summarize the incident for the post-mortem.",
|
| 581 |
},
|
| 582 |
]
|
| 583 |
|
|
|
|
| 588 |
# EVALUATION
|
| 589 |
# =============================================================================
|
| 590 |
|
| 591 |
+
|
| 592 |
@dataclass
|
| 593 |
class EvalResult:
|
| 594 |
mode: str
|
|
|
|
| 599 |
cost_estimate: float
|
| 600 |
|
| 601 |
|
| 602 |
+
def evaluate_response_quality(
|
| 603 |
+
baseline_response: str, optimized_response: str, optimization_level: str
|
| 604 |
+
) -> dict:
|
| 605 |
"""
|
| 606 |
Use GPT-4o as judge to evaluate if the optimized response maintains quality.
|
| 607 |
"""
|
|
|
|
| 648 |
)
|
| 649 |
|
| 650 |
import json as json_module
|
| 651 |
+
|
| 652 |
try:
|
| 653 |
return json_module.loads(response.choices[0].message.content)
|
| 654 |
+
except Exception:
|
| 655 |
+
return {
|
| 656 |
+
"error": "Failed to parse judge response",
|
| 657 |
+
"raw": response.choices[0].message.content,
|
| 658 |
+
}
|
| 659 |
|
| 660 |
|
| 661 |
def run_eval(messages: list[dict], mode: str, use_aggressive: bool = False) -> EvalResult:
|
|
|
|
| 709 |
print("-" * 70)
|
| 710 |
|
| 711 |
sim_default = client.chat.completions.simulate(model="gpt-4o-mini", messages=messages)
|
| 712 |
+
sim_aggressive = aggressive_client.chat.completions.simulate(
|
| 713 |
+
model="gpt-4o-mini", messages=messages
|
| 714 |
+
)
|
| 715 |
|
| 716 |
print(f"\n{'Mode':<15} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}")
|
| 717 |
print("-" * 55)
|
| 718 |
+
print(
|
| 719 |
+
f"{'Default':<15} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved / sim_default.tokens_before * 100:>7.1f}%"
|
| 720 |
+
)
|
| 721 |
+
print(
|
| 722 |
+
f"{'Aggressive':<15} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved / sim_aggressive.tokens_before * 100:>7.1f}%"
|
| 723 |
+
)
|
| 724 |
print(f"\nTransforms: {sim_default.transforms}")
|
| 725 |
print()
|
| 726 |
|
|
|
|
| 742 |
print("\n3. AGGRESSIVE OPTIMIZATION...")
|
| 743 |
aggressive_opt = run_eval(messages, "optimize", use_aggressive=True)
|
| 744 |
print(f" Tokens: {aggressive_opt.tokens_input:,} in / {aggressive_opt.tokens_output:,} out")
|
| 745 |
+
print(
|
| 746 |
+
f" Cost: ${aggressive_opt.cost_estimate:.6f} | Latency: {aggressive_opt.latency_ms:.0f}ms"
|
| 747 |
+
)
|
| 748 |
|
| 749 |
# Results table
|
| 750 |
print()
|
|
|
|
| 759 |
|
| 760 |
print(f"\n{'Metric':<20} {'Baseline':>12} {'Default Opt':>12} {'Aggressive':>12}")
|
| 761 |
print("-" * 60)
|
| 762 |
+
print(
|
| 763 |
+
f"{'Input Tokens':<20} {baseline.tokens_input:>12,} {default_opt.tokens_input:>12,} {aggressive_opt.tokens_input:>12,}"
|
| 764 |
+
)
|
| 765 |
+
print(
|
| 766 |
+
f"{'Output Tokens':<20} {baseline.tokens_output:>12,} {default_opt.tokens_output:>12,} {aggressive_opt.tokens_output:>12,}"
|
| 767 |
+
)
|
| 768 |
+
print(
|
| 769 |
+
f"{'Cost':<20} ${baseline.cost_estimate:>11.6f} ${default_opt.cost_estimate:>11.6f} ${aggressive_opt.cost_estimate:>11.6f}"
|
| 770 |
+
)
|
| 771 |
+
print(
|
| 772 |
+
f"{'Latency (ms)':<20} {baseline.latency_ms:>12.0f} {default_opt.latency_ms:>12.0f} {aggressive_opt.latency_ms:>12.0f}"
|
| 773 |
+
)
|
| 774 |
print()
|
| 775 |
+
print(
|
| 776 |
+
f"{'Token Savings':<20} {'-':>12} {def_savings:>10,} ({def_pct:.0f}%) {agg_savings:>10,} ({agg_pct:.0f}%)"
|
| 777 |
+
)
|
| 778 |
|
| 779 |
# Show responses
|
| 780 |
print()
|
|
|
|
| 800 |
default_eval = evaluate_response_quality(baseline.response, default_opt.response, "default")
|
| 801 |
|
| 802 |
print("\nEvaluating AGGRESSIVE optimization vs Baseline...")
|
| 803 |
+
aggressive_eval = evaluate_response_quality(
|
| 804 |
+
baseline.response, aggressive_opt.response, "aggressive"
|
| 805 |
+
)
|
| 806 |
|
| 807 |
print(f"\n{'Criterion':<20} {'Default':>12} {'Aggressive':>12}")
|
| 808 |
print("-" * 46)
|
|
|
|
| 873 |
- Monthly savings: ${cost_save_monthly:.2f}
|
| 874 |
|
| 875 |
CONCLUSION:
|
| 876 |
+
{"✓ Headroom achieves " + f"{agg_pct:.0f}% token reduction with PASSING quality scores." if a_verdict == "PASS" else "⚠ Aggressive optimization may degrade response quality - use conservative settings."}
|
| 877 |
+
{" The compressed context maintains semantic equivalence for model reasoning." if a_verdict == "PASS" else ""}
|
| 878 |
""")
|
| 879 |
|
| 880 |
|
examples/smart_vs_naive_eval.py
CHANGED
|
@@ -20,7 +20,7 @@ from datetime import datetime, timedelta
|
|
| 20 |
from dotenv import load_dotenv
|
| 21 |
from openai import OpenAI
|
| 22 |
|
| 23 |
-
from headroom import HeadroomClient, OpenAIProvider,
|
| 24 |
from headroom.config import HeadroomConfig
|
| 25 |
from headroom.transforms import TransformPipeline
|
| 26 |
|
|
@@ -91,6 +91,7 @@ baseline_client = HeadroomClient(
|
|
| 91 |
# GENERATE TEST DATA WITH CLEAR PATTERNS
|
| 92 |
# =============================================================================
|
| 93 |
|
|
|
|
| 94 |
def generate_metrics_with_spike() -> str:
|
| 95 |
"""
|
| 96 |
Generate metrics data with a CLEAR spike pattern.
|
|
@@ -111,22 +112,20 @@ def generate_metrics_with_spike() -> str:
|
|
| 111 |
cpu = 85 + (i - 45) * 2 # Spike: 85 -> 115
|
| 112 |
error_rate = 5 + (i - 45) # Error spike too
|
| 113 |
|
| 114 |
-
data_points.append(
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
return json.dumps({
|
| 126 |
-
"status": "success",
|
| 127 |
-
"metrics": data_points,
|
| 128 |
-
"query_time_ms": 127
|
| 129 |
-
})
|
| 130 |
|
| 131 |
|
| 132 |
def generate_clusterable_logs() -> str:
|
|
@@ -161,21 +160,20 @@ def generate_clusterable_logs() -> str:
|
|
| 161 |
ts = base_time + timedelta(seconds=i * 36)
|
| 162 |
level, msg = message_templates[i % len(message_templates)]
|
| 163 |
|
| 164 |
-
logs.append(
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
return json.dumps({
|
| 176 |
-
"took": 234,
|
| 177 |
-
"hits": {"total": len(logs), "hits": logs}
|
| 178 |
-
})
|
| 179 |
|
| 180 |
|
| 181 |
def generate_search_results() -> str:
|
|
@@ -185,14 +183,16 @@ def generate_search_results() -> str:
|
|
| 185 |
"""
|
| 186 |
results = []
|
| 187 |
for i in range(30):
|
| 188 |
-
results.append(
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
| 196 |
|
| 197 |
return json.dumps({"results": results, "total": 30})
|
| 198 |
|
|
@@ -201,6 +201,7 @@ def generate_search_results() -> str:
|
|
| 201 |
# BUILD TEST CONVERSATION
|
| 202 |
# =============================================================================
|
| 203 |
|
|
|
|
| 204 |
def build_test_conversation() -> list[dict]:
|
| 205 |
"""Build a conversation that exercises all SmartCrusher strategies."""
|
| 206 |
|
|
@@ -208,53 +209,47 @@ def build_test_conversation() -> list[dict]:
|
|
| 208 |
{
|
| 209 |
"role": "system",
|
| 210 |
"content": """You are an SRE assistant. Analyze the data and provide insights.
|
| 211 |
-
Current Date: 2024-12-15T14:30:00Z"""
|
| 212 |
},
|
| 213 |
{"role": "user", "content": "Check the metrics for the last hour."},
|
| 214 |
{
|
| 215 |
"role": "assistant",
|
| 216 |
"content": None,
|
| 217 |
-
"tool_calls": [
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
"role": "tool",
|
| 225 |
-
"tool_call_id": "call_1",
|
| 226 |
-
"content": generate_metrics_with_spike()
|
| 227 |
},
|
|
|
|
| 228 |
{"role": "assistant", "content": "I see CPU metrics. Let me check the logs."},
|
| 229 |
{
|
| 230 |
"role": "assistant",
|
| 231 |
"content": None,
|
| 232 |
-
"tool_calls": [
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
"role": "tool",
|
| 240 |
-
"tool_call_id": "call_2",
|
| 241 |
-
"content": generate_clusterable_logs()
|
| 242 |
},
|
|
|
|
| 243 |
{"role": "assistant", "content": "Found error patterns. Let me search docs."},
|
| 244 |
{
|
| 245 |
"role": "assistant",
|
| 246 |
"content": None,
|
| 247 |
-
"tool_calls": [
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
"role": "tool",
|
| 255 |
-
"tool_call_id": "call_3",
|
| 256 |
-
"content": generate_search_results()
|
| 257 |
},
|
|
|
|
| 258 |
{"role": "user", "content": "What's the root cause and what should we do?"},
|
| 259 |
]
|
| 260 |
|
|
@@ -265,6 +260,7 @@ Current Date: 2024-12-15T14:30:00Z"""
|
|
| 265 |
# EVALUATION
|
| 266 |
# =============================================================================
|
| 267 |
|
|
|
|
| 268 |
@dataclass
|
| 269 |
class EvalResult:
|
| 270 |
name: str
|
|
@@ -299,7 +295,9 @@ def evaluate(client, messages: list[dict], name: str, mode: str) -> EvalResult:
|
|
| 299 |
tokens_before=sim.tokens_before,
|
| 300 |
tokens_after=tokens_in,
|
| 301 |
tokens_saved=sim.tokens_before - tokens_in,
|
| 302 |
-
reduction_pct=(sim.tokens_before - tokens_in) / sim.tokens_before * 100
|
|
|
|
|
|
|
| 303 |
transforms=sim.transforms,
|
| 304 |
response=response.choices[0].message.content or "",
|
| 305 |
latency_ms=latency,
|
|
@@ -341,7 +339,7 @@ PASS = overall >= 4.0"""
|
|
| 341 |
|
| 342 |
try:
|
| 343 |
return json.loads(response.choices[0].message.content)
|
| 344 |
-
except:
|
| 345 |
return {"error": "Parse failed"}
|
| 346 |
|
| 347 |
|
|
@@ -380,12 +378,16 @@ def main():
|
|
| 380 |
|
| 381 |
print("\n2. NAIVE CRUSHER (fixed rules: keep first 10)...")
|
| 382 |
naive = evaluate(naive_client, messages, "Naive", "optimize")
|
| 383 |
-
print(
|
|
|
|
|
|
|
| 384 |
print(f" Transforms: {naive.transforms}")
|
| 385 |
|
| 386 |
print("\n3. SMART CRUSHER (statistical analysis)...")
|
| 387 |
smart = evaluate(smart_client, messages, "Smart", "optimize")
|
| 388 |
-
print(
|
|
|
|
|
|
|
| 389 |
print(f" Transforms: {smart.transforms}")
|
| 390 |
|
| 391 |
# Results comparison
|
|
@@ -397,17 +399,25 @@ def main():
|
|
| 397 |
print(f"\n{'Method':<20} {'Tokens':>10} {'Saved':>10} {'Reduction':>10}")
|
| 398 |
print("-" * 52)
|
| 399 |
print(f"{'Baseline':<20} {baseline.tokens_after:>10,} {'-':>10} {'-':>10}")
|
| 400 |
-
print(
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
# Show the difference
|
| 404 |
diff = naive.tokens_after - smart.tokens_after
|
| 405 |
if diff > 0:
|
| 406 |
-
print(
|
|
|
|
|
|
|
| 407 |
elif diff < 0:
|
| 408 |
-
print(
|
|
|
|
|
|
|
| 409 |
else:
|
| 410 |
-
print(
|
| 411 |
|
| 412 |
# Quality evaluation
|
| 413 |
print()
|
|
@@ -429,8 +439,12 @@ def main():
|
|
| 429 |
s_score = smart_quality.get(criterion, {}).get("score", "?")
|
| 430 |
print(f"{criterion.replace('_', ' ').title():<20} {n_score:>10}/5 {s_score:>10}/5")
|
| 431 |
print("-" * 42)
|
| 432 |
-
print(
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
print("\n[Quality Analysis]")
|
| 436 |
print(f" Naive: {naive_quality.get('data_awareness', {}).get('reason', 'N/A')}")
|
|
@@ -453,12 +467,12 @@ SmartCrusher vs NaiveCrusher on SRE incident data:
|
|
| 453 |
Token Efficiency:
|
| 454 |
- Naive: {naive.reduction_pct:.1f}% reduction
|
| 455 |
- Smart: {smart.reduction_pct:.1f}% reduction
|
| 456 |
-
- Winner: {
|
| 457 |
|
| 458 |
Response Quality:
|
| 459 |
- Naive: {n_overall}/5 ({n_verdict})
|
| 460 |
- Smart: {s_overall}/5 ({s_verdict})
|
| 461 |
-
- Winner: {
|
| 462 |
|
| 463 |
Key Insight:
|
| 464 |
SmartCrusher uses statistical analysis to preserve important data:
|
|
|
|
| 20 |
from dotenv import load_dotenv
|
| 21 |
from openai import OpenAI
|
| 22 |
|
| 23 |
+
from headroom import HeadroomClient, OpenAIProvider, SmartCrusherConfig, ToolCrusherConfig
|
| 24 |
from headroom.config import HeadroomConfig
|
| 25 |
from headroom.transforms import TransformPipeline
|
| 26 |
|
|
|
|
| 91 |
# GENERATE TEST DATA WITH CLEAR PATTERNS
|
| 92 |
# =============================================================================
|
| 93 |
|
| 94 |
+
|
| 95 |
def generate_metrics_with_spike() -> str:
|
| 96 |
"""
|
| 97 |
Generate metrics data with a CLEAR spike pattern.
|
|
|
|
| 112 |
cpu = 85 + (i - 45) * 2 # Spike: 85 -> 115
|
| 113 |
error_rate = 5 + (i - 45) # Error spike too
|
| 114 |
|
| 115 |
+
data_points.append(
|
| 116 |
+
{
|
| 117 |
+
"timestamp": ts.isoformat(),
|
| 118 |
+
"host": "prod-api-1", # CONSTANT - should be factored out
|
| 119 |
+
"region": "us-east-1", # CONSTANT - should be factored out
|
| 120 |
+
"datacenter": "dc-01", # CONSTANT - should be factored out
|
| 121 |
+
"cpu_percent": min(cpu, 99),
|
| 122 |
+
"memory_percent": 62, # CONSTANT
|
| 123 |
+
"error_rate": round(error_rate, 2),
|
| 124 |
+
"request_count": 1500 + (i * 10),
|
| 125 |
+
}
|
| 126 |
+
)
|
| 127 |
|
| 128 |
+
return json.dumps({"status": "success", "metrics": data_points, "query_time_ms": 127})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
def generate_clusterable_logs() -> str:
|
|
|
|
| 160 |
ts = base_time + timedelta(seconds=i * 36)
|
| 161 |
level, msg = message_templates[i % len(message_templates)]
|
| 162 |
|
| 163 |
+
logs.append(
|
| 164 |
+
{
|
| 165 |
+
"@timestamp": ts.isoformat(),
|
| 166 |
+
"level": level,
|
| 167 |
+
"message": msg,
|
| 168 |
+
"service": "api-server", # CONSTANT
|
| 169 |
+
"environment": "production", # CONSTANT
|
| 170 |
+
"version": "2.4.1", # CONSTANT
|
| 171 |
+
"host": f"prod-api-{i % 3 + 1}",
|
| 172 |
+
"trace_id": f"trace-{1000 + i:04d}",
|
| 173 |
+
}
|
| 174 |
+
)
|
| 175 |
|
| 176 |
+
return json.dumps({"took": 234, "hits": {"total": len(logs), "hits": logs}})
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
|
| 179 |
def generate_search_results() -> str:
|
|
|
|
| 183 |
"""
|
| 184 |
results = []
|
| 185 |
for i in range(30):
|
| 186 |
+
results.append(
|
| 187 |
+
{
|
| 188 |
+
"id": f"doc-{i + 1}",
|
| 189 |
+
"title": f"Result document {i + 1}",
|
| 190 |
+
"snippet": f"This is the snippet for document {i + 1} with relevant content...",
|
| 191 |
+
"score": 0.95 - (i * 0.02), # Decreasing relevance
|
| 192 |
+
"source": "knowledge_base", # CONSTANT
|
| 193 |
+
"category": "technical", # CONSTANT
|
| 194 |
+
}
|
| 195 |
+
)
|
| 196 |
|
| 197 |
return json.dumps({"results": results, "total": 30})
|
| 198 |
|
|
|
|
| 201 |
# BUILD TEST CONVERSATION
|
| 202 |
# =============================================================================
|
| 203 |
|
| 204 |
+
|
| 205 |
def build_test_conversation() -> list[dict]:
|
| 206 |
"""Build a conversation that exercises all SmartCrusher strategies."""
|
| 207 |
|
|
|
|
| 209 |
{
|
| 210 |
"role": "system",
|
| 211 |
"content": """You are an SRE assistant. Analyze the data and provide insights.
|
| 212 |
+
Current Date: 2024-12-15T14:30:00Z""",
|
| 213 |
},
|
| 214 |
{"role": "user", "content": "Check the metrics for the last hour."},
|
| 215 |
{
|
| 216 |
"role": "assistant",
|
| 217 |
"content": None,
|
| 218 |
+
"tool_calls": [
|
| 219 |
+
{
|
| 220 |
+
"id": "call_1",
|
| 221 |
+
"type": "function",
|
| 222 |
+
"function": {"name": "query_metrics", "arguments": "{}"},
|
| 223 |
+
}
|
| 224 |
+
],
|
|
|
|
|
|
|
|
|
|
| 225 |
},
|
| 226 |
+
{"role": "tool", "tool_call_id": "call_1", "content": generate_metrics_with_spike()},
|
| 227 |
{"role": "assistant", "content": "I see CPU metrics. Let me check the logs."},
|
| 228 |
{
|
| 229 |
"role": "assistant",
|
| 230 |
"content": None,
|
| 231 |
+
"tool_calls": [
|
| 232 |
+
{
|
| 233 |
+
"id": "call_2",
|
| 234 |
+
"type": "function",
|
| 235 |
+
"function": {"name": "search_logs", "arguments": "{}"},
|
| 236 |
+
}
|
| 237 |
+
],
|
|
|
|
|
|
|
|
|
|
| 238 |
},
|
| 239 |
+
{"role": "tool", "tool_call_id": "call_2", "content": generate_clusterable_logs()},
|
| 240 |
{"role": "assistant", "content": "Found error patterns. Let me search docs."},
|
| 241 |
{
|
| 242 |
"role": "assistant",
|
| 243 |
"content": None,
|
| 244 |
+
"tool_calls": [
|
| 245 |
+
{
|
| 246 |
+
"id": "call_3",
|
| 247 |
+
"type": "function",
|
| 248 |
+
"function": {"name": "search_docs", "arguments": "{}"},
|
| 249 |
+
}
|
| 250 |
+
],
|
|
|
|
|
|
|
|
|
|
| 251 |
},
|
| 252 |
+
{"role": "tool", "tool_call_id": "call_3", "content": generate_search_results()},
|
| 253 |
{"role": "user", "content": "What's the root cause and what should we do?"},
|
| 254 |
]
|
| 255 |
|
|
|
|
| 260 |
# EVALUATION
|
| 261 |
# =============================================================================
|
| 262 |
|
| 263 |
+
|
| 264 |
@dataclass
|
| 265 |
class EvalResult:
|
| 266 |
name: str
|
|
|
|
| 295 |
tokens_before=sim.tokens_before,
|
| 296 |
tokens_after=tokens_in,
|
| 297 |
tokens_saved=sim.tokens_before - tokens_in,
|
| 298 |
+
reduction_pct=(sim.tokens_before - tokens_in) / sim.tokens_before * 100
|
| 299 |
+
if sim.tokens_before
|
| 300 |
+
else 0,
|
| 301 |
transforms=sim.transforms,
|
| 302 |
response=response.choices[0].message.content or "",
|
| 303 |
latency_ms=latency,
|
|
|
|
| 339 |
|
| 340 |
try:
|
| 341 |
return json.loads(response.choices[0].message.content)
|
| 342 |
+
except Exception:
|
| 343 |
return {"error": "Parse failed"}
|
| 344 |
|
| 345 |
|
|
|
|
| 378 |
|
| 379 |
print("\n2. NAIVE CRUSHER (fixed rules: keep first 10)...")
|
| 380 |
naive = evaluate(naive_client, messages, "Naive", "optimize")
|
| 381 |
+
print(
|
| 382 |
+
f" Tokens: {naive.tokens_after:,} (saved {naive.tokens_saved:,}, {naive.reduction_pct:.1f}%)"
|
| 383 |
+
)
|
| 384 |
print(f" Transforms: {naive.transforms}")
|
| 385 |
|
| 386 |
print("\n3. SMART CRUSHER (statistical analysis)...")
|
| 387 |
smart = evaluate(smart_client, messages, "Smart", "optimize")
|
| 388 |
+
print(
|
| 389 |
+
f" Tokens: {smart.tokens_after:,} (saved {smart.tokens_saved:,}, {smart.reduction_pct:.1f}%)"
|
| 390 |
+
)
|
| 391 |
print(f" Transforms: {smart.transforms}")
|
| 392 |
|
| 393 |
# Results comparison
|
|
|
|
| 399 |
print(f"\n{'Method':<20} {'Tokens':>10} {'Saved':>10} {'Reduction':>10}")
|
| 400 |
print("-" * 52)
|
| 401 |
print(f"{'Baseline':<20} {baseline.tokens_after:>10,} {'-':>10} {'-':>10}")
|
| 402 |
+
print(
|
| 403 |
+
f"{'Naive Crusher':<20} {naive.tokens_after:>10,} {naive.tokens_saved:>10,} {naive.reduction_pct:>9.1f}%"
|
| 404 |
+
)
|
| 405 |
+
print(
|
| 406 |
+
f"{'Smart Crusher':<20} {smart.tokens_after:>10,} {smart.tokens_saved:>10,} {smart.reduction_pct:>9.1f}%"
|
| 407 |
+
)
|
| 408 |
|
| 409 |
# Show the difference
|
| 410 |
diff = naive.tokens_after - smart.tokens_after
|
| 411 |
if diff > 0:
|
| 412 |
+
print(
|
| 413 |
+
f"\n→ Smart Crusher saves {diff:,} MORE tokens than Naive ({diff / naive.tokens_after * 100:.1f}% better)"
|
| 414 |
+
)
|
| 415 |
elif diff < 0:
|
| 416 |
+
print(
|
| 417 |
+
f"\n→ Naive Crusher saves {-diff:,} MORE tokens than Smart ({-diff / smart.tokens_after * 100:.1f}% better)"
|
| 418 |
+
)
|
| 419 |
else:
|
| 420 |
+
print("\n→ Both methods produce same token count")
|
| 421 |
|
| 422 |
# Quality evaluation
|
| 423 |
print()
|
|
|
|
| 439 |
s_score = smart_quality.get(criterion, {}).get("score", "?")
|
| 440 |
print(f"{criterion.replace('_', ' ').title():<20} {n_score:>10}/5 {s_score:>10}/5")
|
| 441 |
print("-" * 42)
|
| 442 |
+
print(
|
| 443 |
+
f"{'OVERALL':<20} {naive_quality.get('overall', '?'):>10}/5 {smart_quality.get('overall', '?'):>10}/5"
|
| 444 |
+
)
|
| 445 |
+
print(
|
| 446 |
+
f"{'VERDICT':<20} {naive_quality.get('verdict', '?'):>10} {smart_quality.get('verdict', '?'):>10}"
|
| 447 |
+
)
|
| 448 |
|
| 449 |
print("\n[Quality Analysis]")
|
| 450 |
print(f" Naive: {naive_quality.get('data_awareness', {}).get('reason', 'N/A')}")
|
|
|
|
| 467 |
Token Efficiency:
|
| 468 |
- Naive: {naive.reduction_pct:.1f}% reduction
|
| 469 |
- Smart: {smart.reduction_pct:.1f}% reduction
|
| 470 |
+
- Winner: {"SMART" if smart.reduction_pct > naive.reduction_pct else "NAIVE" if naive.reduction_pct > smart.reduction_pct else "TIE"} (+{abs(smart.reduction_pct - naive.reduction_pct):.1f}% {"more" if smart.reduction_pct > naive.reduction_pct else "less"} reduction)
|
| 471 |
|
| 472 |
Response Quality:
|
| 473 |
- Naive: {n_overall}/5 ({n_verdict})
|
| 474 |
- Smart: {s_overall}/5 ({s_verdict})
|
| 475 |
+
- Winner: {"SMART" if s_overall > n_overall else "NAIVE" if n_overall > s_overall else "TIE"}
|
| 476 |
|
| 477 |
Key Insight:
|
| 478 |
SmartCrusher uses statistical analysis to preserve important data:
|
headroom/cache/__init__.py
CHANGED
|
@@ -25,6 +25,7 @@ Usage:
|
|
| 25 |
CacheOptimizerRegistry.register("my-provider", MyOptimizer)
|
| 26 |
"""
|
| 27 |
|
|
|
|
| 28 |
from .base import (
|
| 29 |
BaseCacheOptimizer,
|
| 30 |
CacheBreakpoint,
|
|
@@ -42,11 +43,10 @@ from .dynamic_detector import (
|
|
| 42 |
DynamicSpan,
|
| 43 |
detect_dynamic_content,
|
| 44 |
)
|
| 45 |
-
from .registry import CacheOptimizerRegistry
|
| 46 |
-
from .anthropic import AnthropicCacheOptimizer
|
| 47 |
-
from .openai import OpenAICacheOptimizer
|
| 48 |
from .google import GoogleCacheOptimizer
|
| 49 |
-
from .
|
|
|
|
|
|
|
| 50 |
|
| 51 |
__all__ = [
|
| 52 |
# Base types
|
|
|
|
| 25 |
CacheOptimizerRegistry.register("my-provider", MyOptimizer)
|
| 26 |
"""
|
| 27 |
|
| 28 |
+
from .anthropic import AnthropicCacheOptimizer
|
| 29 |
from .base import (
|
| 30 |
BaseCacheOptimizer,
|
| 31 |
CacheBreakpoint,
|
|
|
|
| 43 |
DynamicSpan,
|
| 44 |
detect_dynamic_content,
|
| 45 |
)
|
|
|
|
|
|
|
|
|
|
| 46 |
from .google import GoogleCacheOptimizer
|
| 47 |
+
from .openai import OpenAICacheOptimizer
|
| 48 |
+
from .registry import CacheOptimizerRegistry
|
| 49 |
+
from .semantic import SemanticCache, SemanticCacheLayer
|
| 50 |
|
| 51 |
__all__ = [
|
| 52 |
# Base types
|
headroom/cache/anthropic.py
CHANGED
|
@@ -39,7 +39,6 @@ from .base import (
|
|
| 39 |
OptimizationContext,
|
| 40 |
)
|
| 41 |
|
| 42 |
-
|
| 43 |
# Anthropic-specific constants
|
| 44 |
ANTHROPIC_MIN_CACHEABLE_TOKENS = 1024
|
| 45 |
ANTHROPIC_MAX_BREAKPOINTS = 4
|
|
@@ -147,13 +146,9 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 147 |
warnings.extend(plan.warnings)
|
| 148 |
|
| 149 |
# Step 4: Insert cache_control blocks
|
| 150 |
-
optimized_messages = self._insert_breakpoints(
|
| 151 |
-
optimized_messages, plan.breakpoints
|
| 152 |
-
)
|
| 153 |
if plan.breakpoints:
|
| 154 |
-
transforms_applied.append(
|
| 155 |
-
f"inserted_{len(plan.breakpoints)}_cache_breakpoints"
|
| 156 |
-
)
|
| 157 |
|
| 158 |
# Step 5: Compute metrics
|
| 159 |
prefix_content = self._extract_cacheable_content(optimized_messages)
|
|
@@ -194,9 +189,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 194 |
warnings=warnings,
|
| 195 |
)
|
| 196 |
|
| 197 |
-
def _analyze_sections(
|
| 198 |
-
self, messages: list[dict[str, Any]]
|
| 199 |
-
) -> list[ContentSection]:
|
| 200 |
"""Analyze messages to identify distinct content sections."""
|
| 201 |
sections: list[ContentSection] = []
|
| 202 |
|
|
@@ -207,9 +200,13 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 207 |
if role == "system":
|
| 208 |
section_type = "system"
|
| 209 |
elif role == "user":
|
| 210 |
-
section_type =
|
|
|
|
|
|
|
| 211 |
elif role == "assistant":
|
| 212 |
-
section_type =
|
|
|
|
|
|
|
| 213 |
else:
|
| 214 |
section_type = role
|
| 215 |
|
|
@@ -227,17 +224,17 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 227 |
|
| 228 |
if isinstance(content, str):
|
| 229 |
token_count = self._count_tokens_estimate(content)
|
| 230 |
-
is_cacheable, reason = self._assess_cacheability(
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
)
|
| 233 |
-
sections.append(ContentSection(
|
| 234 |
-
content=content,
|
| 235 |
-
section_type=section_type,
|
| 236 |
-
message_index=idx,
|
| 237 |
-
token_count=token_count,
|
| 238 |
-
is_cacheable=is_cacheable,
|
| 239 |
-
reason=reason,
|
| 240 |
-
))
|
| 241 |
|
| 242 |
elif isinstance(content, list):
|
| 243 |
for block_idx, block in enumerate(content):
|
|
@@ -247,15 +244,17 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 247 |
is_cacheable, reason = self._assess_cacheability(
|
| 248 |
section_type, token_count, text
|
| 249 |
)
|
| 250 |
-
sections.append(
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
| 259 |
|
| 260 |
return sections
|
| 261 |
|
|
@@ -264,7 +263,10 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 264 |
) -> tuple[bool, str]:
|
| 265 |
"""Assess whether a section is cacheable."""
|
| 266 |
if token_count < self.config.min_cacheable_tokens:
|
| 267 |
-
return
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
if section_type == "system":
|
| 270 |
return True, "System prompts are highly cacheable"
|
|
@@ -318,6 +320,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 318 |
def _estimate_tools_tokens(self, tools: Any) -> int:
|
| 319 |
"""Estimate token count for tool definitions."""
|
| 320 |
import json
|
|
|
|
| 321 |
try:
|
| 322 |
return self._count_tokens_estimate(json.dumps(tools))
|
| 323 |
except (TypeError, ValueError):
|
|
@@ -353,9 +356,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 353 |
|
| 354 |
return messages, transforms
|
| 355 |
|
| 356 |
-
def _stabilize_text(
|
| 357 |
-
self, text: str, config: CacheConfig
|
| 358 |
-
) -> tuple[str, list[str]]:
|
| 359 |
"""Stabilize a text string."""
|
| 360 |
transforms: list[str] = []
|
| 361 |
result = text
|
|
@@ -408,9 +409,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer):
|
|
| 408 |
|
| 409 |
for section in cacheable:
|
| 410 |
if len(selected) >= config.max_breakpoints:
|
| 411 |
-
plan.warnings.append(
|
| 412 |
-
f"Reached maximum breakpoints ({config.max_breakpoints})"
|
| 413 |
-
)
|
| 414 |
break
|
| 415 |
|
| 416 |
selected.append(section)
|
|
|
|
| 39 |
OptimizationContext,
|
| 40 |
)
|
| 41 |
|
|
|
|
| 42 |
# Anthropic-specific constants
|
| 43 |
ANTHROPIC_MIN_CACHEABLE_TOKENS = 1024
|
| 44 |
ANTHROPIC_MAX_BREAKPOINTS = 4
|
|
|
|
| 146 |
warnings.extend(plan.warnings)
|
| 147 |
|
| 148 |
# Step 4: Insert cache_control blocks
|
| 149 |
+
optimized_messages = self._insert_breakpoints(optimized_messages, plan.breakpoints)
|
|
|
|
|
|
|
| 150 |
if plan.breakpoints:
|
| 151 |
+
transforms_applied.append(f"inserted_{len(plan.breakpoints)}_cache_breakpoints")
|
|
|
|
|
|
|
| 152 |
|
| 153 |
# Step 5: Compute metrics
|
| 154 |
prefix_content = self._extract_cacheable_content(optimized_messages)
|
|
|
|
| 189 |
warnings=warnings,
|
| 190 |
)
|
| 191 |
|
| 192 |
+
def _analyze_sections(self, messages: list[dict[str, Any]]) -> list[ContentSection]:
|
|
|
|
|
|
|
| 193 |
"""Analyze messages to identify distinct content sections."""
|
| 194 |
sections: list[ContentSection] = []
|
| 195 |
|
|
|
|
| 200 |
if role == "system":
|
| 201 |
section_type = "system"
|
| 202 |
elif role == "user":
|
| 203 |
+
section_type = (
|
| 204 |
+
"examples" if self._looks_like_example(message, messages, idx) else "user"
|
| 205 |
+
)
|
| 206 |
elif role == "assistant":
|
| 207 |
+
section_type = (
|
| 208 |
+
"examples" if self._looks_like_example(message, messages, idx) else "assistant"
|
| 209 |
+
)
|
| 210 |
else:
|
| 211 |
section_type = role
|
| 212 |
|
|
|
|
| 224 |
|
| 225 |
if isinstance(content, str):
|
| 226 |
token_count = self._count_tokens_estimate(content)
|
| 227 |
+
is_cacheable, reason = self._assess_cacheability(section_type, token_count, content)
|
| 228 |
+
sections.append(
|
| 229 |
+
ContentSection(
|
| 230 |
+
content=content,
|
| 231 |
+
section_type=section_type,
|
| 232 |
+
message_index=idx,
|
| 233 |
+
token_count=token_count,
|
| 234 |
+
is_cacheable=is_cacheable,
|
| 235 |
+
reason=reason,
|
| 236 |
+
)
|
| 237 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
elif isinstance(content, list):
|
| 240 |
for block_idx, block in enumerate(content):
|
|
|
|
| 244 |
is_cacheable, reason = self._assess_cacheability(
|
| 245 |
section_type, token_count, text
|
| 246 |
)
|
| 247 |
+
sections.append(
|
| 248 |
+
ContentSection(
|
| 249 |
+
content=block,
|
| 250 |
+
section_type=section_type,
|
| 251 |
+
message_index=idx,
|
| 252 |
+
content_index=block_idx,
|
| 253 |
+
token_count=token_count,
|
| 254 |
+
is_cacheable=is_cacheable,
|
| 255 |
+
reason=reason,
|
| 256 |
+
)
|
| 257 |
+
)
|
| 258 |
|
| 259 |
return sections
|
| 260 |
|
|
|
|
| 263 |
) -> tuple[bool, str]:
|
| 264 |
"""Assess whether a section is cacheable."""
|
| 265 |
if token_count < self.config.min_cacheable_tokens:
|
| 266 |
+
return (
|
| 267 |
+
False,
|
| 268 |
+
f"Below minimum tokens ({token_count} < {self.config.min_cacheable_tokens})",
|
| 269 |
+
)
|
| 270 |
|
| 271 |
if section_type == "system":
|
| 272 |
return True, "System prompts are highly cacheable"
|
|
|
|
| 320 |
def _estimate_tools_tokens(self, tools: Any) -> int:
|
| 321 |
"""Estimate token count for tool definitions."""
|
| 322 |
import json
|
| 323 |
+
|
| 324 |
try:
|
| 325 |
return self._count_tokens_estimate(json.dumps(tools))
|
| 326 |
except (TypeError, ValueError):
|
|
|
|
| 356 |
|
| 357 |
return messages, transforms
|
| 358 |
|
| 359 |
+
def _stabilize_text(self, text: str, config: CacheConfig) -> tuple[str, list[str]]:
|
|
|
|
|
|
|
| 360 |
"""Stabilize a text string."""
|
| 361 |
transforms: list[str] = []
|
| 362 |
result = text
|
|
|
|
| 409 |
|
| 410 |
for section in cacheable:
|
| 411 |
if len(selected) >= config.max_breakpoints:
|
| 412 |
+
plan.warnings.append(f"Reached maximum breakpoints ({config.max_breakpoints})")
|
|
|
|
|
|
|
| 413 |
break
|
| 414 |
|
| 415 |
selected.append(section)
|
headroom/cache/base.py
CHANGED
|
@@ -82,11 +82,13 @@ class CacheConfig:
|
|
| 82 |
max_breakpoints: int = 4
|
| 83 |
|
| 84 |
# Patterns to extract and move to dynamic section
|
| 85 |
-
date_patterns: list[str] = field(
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
|
| 91 |
# Whether to normalize whitespace
|
| 92 |
normalize_whitespace: bool = True
|
|
@@ -317,6 +319,7 @@ class BaseCacheOptimizer(ABC):
|
|
| 317 |
def _compute_prefix_hash(self, content: str) -> str:
|
| 318 |
"""Compute a short hash of content."""
|
| 319 |
import hashlib
|
|
|
|
| 320 |
return hashlib.sha256(content.encode()).hexdigest()[:12]
|
| 321 |
|
| 322 |
def _extract_system_content(self, messages: list[dict[str, Any]]) -> str:
|
|
|
|
| 82 |
max_breakpoints: int = 4
|
| 83 |
|
| 84 |
# Patterns to extract and move to dynamic section
|
| 85 |
+
date_patterns: list[str] = field(
|
| 86 |
+
default_factory=lambda: [
|
| 87 |
+
r"Today is \w+ \d{1,2},? \d{4}\.?",
|
| 88 |
+
r"Current date: \d{4}-\d{2}-\d{2}",
|
| 89 |
+
r"The current time is .+\.",
|
| 90 |
+
]
|
| 91 |
+
)
|
| 92 |
|
| 93 |
# Whether to normalize whitespace
|
| 94 |
normalize_whitespace: bool = True
|
|
|
|
| 319 |
def _compute_prefix_hash(self, content: str) -> str:
|
| 320 |
"""Compute a short hash of content."""
|
| 321 |
import hashlib
|
| 322 |
+
|
| 323 |
return hashlib.sha256(content.encode()).hexdigest()[:12]
|
| 324 |
|
| 325 |
def _extract_system_content(self, messages: list[dict[str, Any]]) -> str:
|
headroom/cache/compression_feedback.py
CHANGED
|
@@ -30,7 +30,6 @@ from __future__ import annotations
|
|
| 30 |
import re
|
| 31 |
import threading
|
| 32 |
import time
|
| 33 |
-
from collections import defaultdict
|
| 34 |
from dataclasses import dataclass, field
|
| 35 |
from typing import TYPE_CHECKING, Any
|
| 36 |
|
|
@@ -185,7 +184,9 @@ class CompressionFeedback:
|
|
| 185 |
# Time-based tracking
|
| 186 |
self._last_analysis: float = 0.0
|
| 187 |
self._analysis_interval: float = analysis_interval
|
| 188 |
-
self._last_event_timestamp: float =
|
|
|
|
|
|
|
| 189 |
|
| 190 |
# Global statistics
|
| 191 |
self._total_compressions: int = 0
|
|
@@ -196,6 +197,7 @@ class CompressionFeedback:
|
|
| 196 |
"""Get the compression store (lazy load global if not set)."""
|
| 197 |
if self._store is None:
|
| 198 |
from .compression_store import get_compression_store
|
|
|
|
| 199 |
self._store = get_compression_store()
|
| 200 |
return self._store
|
| 201 |
|
|
@@ -301,9 +303,7 @@ class CompressionFeedback:
|
|
| 301 |
# Track query patterns
|
| 302 |
if event.query:
|
| 303 |
query_lower = event.query.lower()
|
| 304 |
-
pattern.common_queries[query_lower] = (
|
| 305 |
-
pattern.common_queries.get(query_lower, 0) + 1
|
| 306 |
-
)
|
| 307 |
|
| 308 |
# HIGH: Limit common_queries dict to prevent unbounded growth
|
| 309 |
if len(pattern.common_queries) > 100:
|
|
@@ -325,32 +325,32 @@ class CompressionFeedback:
|
|
| 325 |
from both dicts, then truncate both to the same key set.
|
| 326 |
"""
|
| 327 |
# Get top 40 strategies from each dict (using 40 to allow union to stay under 50)
|
| 328 |
-
top_compressions =
|
| 329 |
-
k
|
|
|
|
| 330 |
pattern.strategy_compressions.items(),
|
| 331 |
key=lambda x: x[1],
|
| 332 |
reverse=True,
|
| 333 |
)[:40]
|
| 334 |
-
|
| 335 |
-
top_retrievals =
|
| 336 |
-
k
|
|
|
|
| 337 |
pattern.strategy_retrievals.items(),
|
| 338 |
key=lambda x: x[1],
|
| 339 |
reverse=True,
|
| 340 |
)[:40]
|
| 341 |
-
|
| 342 |
|
| 343 |
# Keep union of top strategies from both
|
| 344 |
keys_to_keep = top_compressions | top_retrievals
|
| 345 |
|
| 346 |
# Truncate both dicts to same keys
|
| 347 |
pattern.strategy_compressions = {
|
| 348 |
-
k: v for k, v in pattern.strategy_compressions.items()
|
| 349 |
-
if k in keys_to_keep
|
| 350 |
}
|
| 351 |
pattern.strategy_retrievals = {
|
| 352 |
-
k: v for k, v in pattern.strategy_retrievals.items()
|
| 353 |
-
if k in keys_to_keep
|
| 354 |
}
|
| 355 |
|
| 356 |
def _extract_field_hints(self, pattern: LocalToolPattern, query: str) -> None:
|
|
@@ -361,22 +361,30 @@ class CompressionFeedback:
|
|
| 361 |
- JSON field names like "status", "error", "id"
|
| 362 |
"""
|
| 363 |
# Look for field:value patterns
|
| 364 |
-
field_patterns = re.findall(r
|
| 365 |
-
for
|
| 366 |
-
pattern.queried_fields[
|
| 367 |
-
pattern.queried_fields.get(field, 0) + 1
|
| 368 |
-
)
|
| 369 |
|
| 370 |
# Look for common JSON field names
|
| 371 |
common_fields = [
|
| 372 |
-
"id",
|
| 373 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
]
|
| 375 |
query_lower = query.lower()
|
| 376 |
-
for
|
| 377 |
-
if
|
| 378 |
-
pattern.queried_fields[
|
| 379 |
-
pattern.queried_fields.get(
|
| 380 |
)
|
| 381 |
|
| 382 |
# HIGH: Limit queried_fields dict to prevent unbounded growth
|
|
@@ -459,8 +467,7 @@ class CompressionFeedback:
|
|
| 459 |
hints.suggested_items = 10
|
| 460 |
hints.aggressiveness = 0.7
|
| 461 |
hints.reason = (
|
| 462 |
-
f"Low retrieval rate ({retrieval_rate:.0%}), "
|
| 463 |
-
f"current compression is effective"
|
| 464 |
)
|
| 465 |
|
| 466 |
# Add field preservation hints based on common queries
|
|
@@ -488,6 +495,7 @@ class CompressionFeedback:
|
|
| 488 |
HIGH FIX: Returns deep copies to prevent external mutation of internal state.
|
| 489 |
"""
|
| 490 |
import copy as copy_module
|
|
|
|
| 491 |
with self._lock:
|
| 492 |
# Deep copy to prevent external code from modifying internal state
|
| 493 |
return copy_module.deepcopy(self._tool_patterns)
|
|
@@ -504,7 +512,8 @@ class CompressionFeedback:
|
|
| 504 |
"total_retrievals": self._total_retrievals,
|
| 505 |
"global_retrieval_rate": (
|
| 506 |
self._total_retrievals / self._total_compressions
|
| 507 |
-
if self._total_compressions > 0
|
|
|
|
| 508 |
),
|
| 509 |
"tools_tracked": len(self._tool_patterns),
|
| 510 |
"tool_patterns": {
|
|
|
|
| 30 |
import re
|
| 31 |
import threading
|
| 32 |
import time
|
|
|
|
| 33 |
from dataclasses import dataclass, field
|
| 34 |
from typing import TYPE_CHECKING, Any
|
| 35 |
|
|
|
|
| 184 |
# Time-based tracking
|
| 185 |
self._last_analysis: float = 0.0
|
| 186 |
self._analysis_interval: float = analysis_interval
|
| 187 |
+
self._last_event_timestamp: float = (
|
| 188 |
+
0.0 # Track last processed event to avoid double-counting
|
| 189 |
+
)
|
| 190 |
|
| 191 |
# Global statistics
|
| 192 |
self._total_compressions: int = 0
|
|
|
|
| 197 |
"""Get the compression store (lazy load global if not set)."""
|
| 198 |
if self._store is None:
|
| 199 |
from .compression_store import get_compression_store
|
| 200 |
+
|
| 201 |
self._store = get_compression_store()
|
| 202 |
return self._store
|
| 203 |
|
|
|
|
| 303 |
# Track query patterns
|
| 304 |
if event.query:
|
| 305 |
query_lower = event.query.lower()
|
| 306 |
+
pattern.common_queries[query_lower] = pattern.common_queries.get(query_lower, 0) + 1
|
|
|
|
|
|
|
| 307 |
|
| 308 |
# HIGH: Limit common_queries dict to prevent unbounded growth
|
| 309 |
if len(pattern.common_queries) > 100:
|
|
|
|
| 325 |
from both dicts, then truncate both to the same key set.
|
| 326 |
"""
|
| 327 |
# Get top 40 strategies from each dict (using 40 to allow union to stay under 50)
|
| 328 |
+
top_compressions = {
|
| 329 |
+
k
|
| 330 |
+
for k, _ in sorted(
|
| 331 |
pattern.strategy_compressions.items(),
|
| 332 |
key=lambda x: x[1],
|
| 333 |
reverse=True,
|
| 334 |
)[:40]
|
| 335 |
+
}
|
| 336 |
+
top_retrievals = {
|
| 337 |
+
k
|
| 338 |
+
for k, _ in sorted(
|
| 339 |
pattern.strategy_retrievals.items(),
|
| 340 |
key=lambda x: x[1],
|
| 341 |
reverse=True,
|
| 342 |
)[:40]
|
| 343 |
+
}
|
| 344 |
|
| 345 |
# Keep union of top strategies from both
|
| 346 |
keys_to_keep = top_compressions | top_retrievals
|
| 347 |
|
| 348 |
# Truncate both dicts to same keys
|
| 349 |
pattern.strategy_compressions = {
|
| 350 |
+
k: v for k, v in pattern.strategy_compressions.items() if k in keys_to_keep
|
|
|
|
| 351 |
}
|
| 352 |
pattern.strategy_retrievals = {
|
| 353 |
+
k: v for k, v in pattern.strategy_retrievals.items() if k in keys_to_keep
|
|
|
|
| 354 |
}
|
| 355 |
|
| 356 |
def _extract_field_hints(self, pattern: LocalToolPattern, query: str) -> None:
|
|
|
|
| 361 |
- JSON field names like "status", "error", "id"
|
| 362 |
"""
|
| 363 |
# Look for field:value patterns
|
| 364 |
+
field_patterns = re.findall(r"(\w+)[=:]", query)
|
| 365 |
+
for field_name in field_patterns:
|
| 366 |
+
pattern.queried_fields[field_name] = pattern.queried_fields.get(field_name, 0) + 1
|
|
|
|
|
|
|
| 367 |
|
| 368 |
# Look for common JSON field names
|
| 369 |
common_fields = [
|
| 370 |
+
"id",
|
| 371 |
+
"name",
|
| 372 |
+
"status",
|
| 373 |
+
"error",
|
| 374 |
+
"message",
|
| 375 |
+
"type",
|
| 376 |
+
"code",
|
| 377 |
+
"result",
|
| 378 |
+
"value",
|
| 379 |
+
"data",
|
| 380 |
+
"items",
|
| 381 |
+
"count",
|
| 382 |
]
|
| 383 |
query_lower = query.lower()
|
| 384 |
+
for common_field in common_fields:
|
| 385 |
+
if common_field in query_lower:
|
| 386 |
+
pattern.queried_fields[common_field] = (
|
| 387 |
+
pattern.queried_fields.get(common_field, 0) + 1
|
| 388 |
)
|
| 389 |
|
| 390 |
# HIGH: Limit queried_fields dict to prevent unbounded growth
|
|
|
|
| 467 |
hints.suggested_items = 10
|
| 468 |
hints.aggressiveness = 0.7
|
| 469 |
hints.reason = (
|
| 470 |
+
f"Low retrieval rate ({retrieval_rate:.0%}), current compression is effective"
|
|
|
|
| 471 |
)
|
| 472 |
|
| 473 |
# Add field preservation hints based on common queries
|
|
|
|
| 495 |
HIGH FIX: Returns deep copies to prevent external mutation of internal state.
|
| 496 |
"""
|
| 497 |
import copy as copy_module
|
| 498 |
+
|
| 499 |
with self._lock:
|
| 500 |
# Deep copy to prevent external code from modifying internal state
|
| 501 |
return copy_module.deepcopy(self._tool_patterns)
|
|
|
|
| 512 |
"total_retrievals": self._total_retrievals,
|
| 513 |
"global_retrieval_rate": (
|
| 514 |
self._total_retrievals / self._total_compressions
|
| 515 |
+
if self._total_compressions > 0
|
| 516 |
+
else 0.0
|
| 517 |
),
|
| 518 |
"tools_tracked": len(self._tool_patterns),
|
| 519 |
"tool_patterns": {
|
headroom/cache/compression_store.py
CHANGED
|
@@ -33,7 +33,6 @@ Usage:
|
|
| 33 |
|
| 34 |
from __future__ import annotations
|
| 35 |
|
| 36 |
-
import copy
|
| 37 |
import hashlib
|
| 38 |
import heapq
|
| 39 |
import json
|
|
@@ -231,8 +230,7 @@ class CompressionStore:
|
|
| 231 |
# True hash collision - different content, same hash
|
| 232 |
# This is extremely rare with SHA256[:24] but should be logged
|
| 233 |
logger.warning(
|
| 234 |
-
"Hash collision detected: hash=%s tool=%s "
|
| 235 |
-
"(existing_len=%d, new_len=%d)",
|
| 236 |
hash_key,
|
| 237 |
tool_name,
|
| 238 |
len(existing.original_content),
|
|
@@ -437,15 +435,9 @@ class CompressionStore:
|
|
| 437 |
# Clean expired entries
|
| 438 |
self._clean_expired()
|
| 439 |
|
| 440 |
-
total_original_tokens = sum(
|
| 441 |
-
|
| 442 |
-
)
|
| 443 |
-
total_compressed_tokens = sum(
|
| 444 |
-
e.compressed_tokens for e in self._store.values()
|
| 445 |
-
)
|
| 446 |
-
total_retrievals = sum(
|
| 447 |
-
e.retrieval_count for e in self._store.values()
|
| 448 |
-
)
|
| 449 |
|
| 450 |
return {
|
| 451 |
"entry_count": len(self._store),
|
|
@@ -534,10 +526,7 @@ class CompressionStore:
|
|
| 534 |
|
| 535 |
CRITICAL FIX: Track stale heap entries when deleting to prevent memory leak.
|
| 536 |
"""
|
| 537 |
-
expired_keys = [
|
| 538 |
-
key for key, entry in self._store.items()
|
| 539 |
-
if entry.is_expired()
|
| 540 |
-
]
|
| 541 |
for key in expired_keys:
|
| 542 |
del self._store[key]
|
| 543 |
# CRITICAL FIX: Increment stale counter - the heap still has an entry
|
|
@@ -552,8 +541,7 @@ class CompressionStore:
|
|
| 552 |
"""
|
| 553 |
# Build new heap from current store entries only
|
| 554 |
self._eviction_heap = [
|
| 555 |
-
(entry.created_at, hash_key)
|
| 556 |
-
for hash_key, entry in self._store.items()
|
| 557 |
]
|
| 558 |
heapq.heapify(self._eviction_heap)
|
| 559 |
# Reset stale counter - heap is now clean
|
|
@@ -630,7 +618,7 @@ class CompressionStore:
|
|
| 630 |
|
| 631 |
# Keep only recent events
|
| 632 |
if len(self._retrieval_events) > self._max_events:
|
| 633 |
-
self._retrieval_events = self._retrieval_events[-self._max_events:]
|
| 634 |
|
| 635 |
# Queue event for feedback processing (will be processed after lock release)
|
| 636 |
# This is safe because process_pending_feedback() uses the lock to atomically
|
|
@@ -648,9 +636,9 @@ class CompressionStore:
|
|
| 648 |
This is called automatically on each retrieval to ensure the
|
| 649 |
feedback loop operates in real-time.
|
| 650 |
"""
|
| 651 |
-
from .compression_feedback import get_compression_feedback
|
| 652 |
from ..telemetry import get_telemetry_collector
|
| 653 |
from ..telemetry.toin import get_toin
|
|
|
|
| 654 |
|
| 655 |
# Get pending events and related entry data atomically
|
| 656 |
with self._lock:
|
|
@@ -664,12 +652,14 @@ class CompressionStore:
|
|
| 664 |
if entry:
|
| 665 |
# Use the ACTUAL tool_signature_hash stored during compression
|
| 666 |
# This MUST match the hash used by SmartCrusher
|
| 667 |
-
event_data.append(
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
|
|
|
|
|
|
| 673 |
else:
|
| 674 |
event_data.append((event, None, None, None))
|
| 675 |
|
|
@@ -679,7 +669,7 @@ class CompressionStore:
|
|
| 679 |
telemetry = get_telemetry_collector()
|
| 680 |
toin = get_toin()
|
| 681 |
|
| 682 |
-
for event,
|
| 683 |
# Notify feedback system (pass strategy for success rate tracking)
|
| 684 |
feedback.record_retrieval(event, strategy=strategy)
|
| 685 |
|
|
@@ -687,7 +677,7 @@ class CompressionStore:
|
|
| 687 |
query_fields = None
|
| 688 |
if event.query:
|
| 689 |
# Extract field:value patterns
|
| 690 |
-
query_fields = re.findall(r
|
| 691 |
|
| 692 |
# Notify telemetry for data flywheel
|
| 693 |
try:
|
|
|
|
| 33 |
|
| 34 |
from __future__ import annotations
|
| 35 |
|
|
|
|
| 36 |
import hashlib
|
| 37 |
import heapq
|
| 38 |
import json
|
|
|
|
| 230 |
# True hash collision - different content, same hash
|
| 231 |
# This is extremely rare with SHA256[:24] but should be logged
|
| 232 |
logger.warning(
|
| 233 |
+
"Hash collision detected: hash=%s tool=%s (existing_len=%d, new_len=%d)",
|
|
|
|
| 234 |
hash_key,
|
| 235 |
tool_name,
|
| 236 |
len(existing.original_content),
|
|
|
|
| 435 |
# Clean expired entries
|
| 436 |
self._clean_expired()
|
| 437 |
|
| 438 |
+
total_original_tokens = sum(e.original_tokens for e in self._store.values())
|
| 439 |
+
total_compressed_tokens = sum(e.compressed_tokens for e in self._store.values())
|
| 440 |
+
total_retrievals = sum(e.retrieval_count for e in self._store.values())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
|
| 442 |
return {
|
| 443 |
"entry_count": len(self._store),
|
|
|
|
| 526 |
|
| 527 |
CRITICAL FIX: Track stale heap entries when deleting to prevent memory leak.
|
| 528 |
"""
|
| 529 |
+
expired_keys = [key for key, entry in self._store.items() if entry.is_expired()]
|
|
|
|
|
|
|
|
|
|
| 530 |
for key in expired_keys:
|
| 531 |
del self._store[key]
|
| 532 |
# CRITICAL FIX: Increment stale counter - the heap still has an entry
|
|
|
|
| 541 |
"""
|
| 542 |
# Build new heap from current store entries only
|
| 543 |
self._eviction_heap = [
|
| 544 |
+
(entry.created_at, hash_key) for hash_key, entry in self._store.items()
|
|
|
|
| 545 |
]
|
| 546 |
heapq.heapify(self._eviction_heap)
|
| 547 |
# Reset stale counter - heap is now clean
|
|
|
|
| 618 |
|
| 619 |
# Keep only recent events
|
| 620 |
if len(self._retrieval_events) > self._max_events:
|
| 621 |
+
self._retrieval_events = self._retrieval_events[-self._max_events :]
|
| 622 |
|
| 623 |
# Queue event for feedback processing (will be processed after lock release)
|
| 624 |
# This is safe because process_pending_feedback() uses the lock to atomically
|
|
|
|
| 636 |
This is called automatically on each retrieval to ensure the
|
| 637 |
feedback loop operates in real-time.
|
| 638 |
"""
|
|
|
|
| 639 |
from ..telemetry import get_telemetry_collector
|
| 640 |
from ..telemetry.toin import get_toin
|
| 641 |
+
from .compression_feedback import get_compression_feedback
|
| 642 |
|
| 643 |
# Get pending events and related entry data atomically
|
| 644 |
with self._lock:
|
|
|
|
| 652 |
if entry:
|
| 653 |
# Use the ACTUAL tool_signature_hash stored during compression
|
| 654 |
# This MUST match the hash used by SmartCrusher
|
| 655 |
+
event_data.append(
|
| 656 |
+
(
|
| 657 |
+
event,
|
| 658 |
+
entry.tool_name,
|
| 659 |
+
entry.tool_signature_hash, # The correct hash!
|
| 660 |
+
entry.compression_strategy,
|
| 661 |
+
)
|
| 662 |
+
)
|
| 663 |
else:
|
| 664 |
event_data.append((event, None, None, None))
|
| 665 |
|
|
|
|
| 669 |
telemetry = get_telemetry_collector()
|
| 670 |
toin = get_toin()
|
| 671 |
|
| 672 |
+
for event, _tool_name, sig_hash, strategy in event_data:
|
| 673 |
# Notify feedback system (pass strategy for success rate tracking)
|
| 674 |
feedback.record_retrieval(event, strategy=strategy)
|
| 675 |
|
|
|
|
| 677 |
query_fields = None
|
| 678 |
if event.query:
|
| 679 |
# Extract field:value patterns
|
| 680 |
+
query_fields = re.findall(r"(\w+)[=:]", event.query)
|
| 681 |
|
| 682 |
# Notify telemetry for data flywheel
|
| 683 |
try:
|
headroom/cache/dynamic_detector.py
CHANGED
|
@@ -44,13 +44,15 @@ _SENTENCE_TRANSFORMERS_AVAILABLE = False
|
|
| 44 |
|
| 45 |
try:
|
| 46 |
import spacy
|
|
|
|
| 47 |
_SPACY_AVAILABLE = True
|
| 48 |
except ImportError:
|
| 49 |
spacy = None # type: ignore
|
| 50 |
|
| 51 |
try:
|
| 52 |
-
from sentence_transformers import SentenceTransformer
|
| 53 |
import numpy as np
|
|
|
|
|
|
|
| 54 |
_SENTENCE_TRANSFORMERS_AVAILABLE = True
|
| 55 |
except ImportError:
|
| 56 |
SentenceTransformer = None # type: ignore
|
|
@@ -138,35 +140,77 @@ class DetectorConfig:
|
|
| 138 |
"""Configuration for the dynamic content detector."""
|
| 139 |
|
| 140 |
# Which tiers to enable (order matters - later tiers can use earlier results)
|
| 141 |
-
tiers: list[Literal["regex", "ner", "semantic"]] = field(
|
| 142 |
-
default_factory=lambda: ["regex"]
|
| 143 |
-
)
|
| 144 |
|
| 145 |
# Tier 1: Structural labels that indicate dynamic content
|
| 146 |
# These are the KEY names that hint the VALUE is dynamic
|
| 147 |
# Users can add domain-specific labels
|
| 148 |
-
dynamic_labels: list[str] = field(
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
# Tier 1: Custom regex patterns (user-provided)
|
| 167 |
-
custom_patterns: list[tuple[str, DynamicCategory]] = field(
|
| 168 |
-
default_factory=list
|
| 169 |
-
)
|
| 170 |
|
| 171 |
# Entropy threshold for detecting random strings (0-1 scale normalized)
|
| 172 |
# Higher = more selective (only very random strings)
|
|
@@ -237,48 +281,47 @@ class RegexDetector:
|
|
| 237 |
# Universal patterns (these formats are language-agnostic)
|
| 238 |
UNIVERSAL_PATTERNS = [
|
| 239 |
# UUID - truly universal format
|
| 240 |
-
(
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
| 243 |
# ISO 8601 datetime (most universal date format)
|
| 244 |
-
(
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
| 247 |
# ISO 8601 date only
|
| 248 |
-
(r"\d{4}-\d{2}-\d{2}(?!\d)",
|
| 249 |
-
DynamicCategory.DATE, "iso_date"),
|
| 250 |
-
|
| 251 |
# Unix timestamps (10-13 digits, but NOT within longer numbers)
|
| 252 |
-
(r"(?<![0-9])\d{10,13}(?![0-9])",
|
| 253 |
-
DynamicCategory.TIMESTAMP, "unix_timestamp"),
|
| 254 |
-
|
| 255 |
# 24-hour time HH:MM:SS or HH:MM
|
| 256 |
-
(
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
| 259 |
# Version numbers with v prefix (unambiguous)
|
| 260 |
-
(r"\bv\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?",
|
| 261 |
-
DynamicCategory.VERSION, "version"),
|
| 262 |
-
|
| 263 |
# API key/token patterns (prefix + random string)
|
| 264 |
-
(
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
| 267 |
# Common prefixed IDs (req_, sess_, txn_, etc.)
|
| 268 |
-
(r"\b[a-z]{2,6}_[a-zA-Z0-9]{8,}",
|
| 269 |
-
DynamicCategory.REQUEST_ID, "prefixed_id"),
|
| 270 |
-
|
| 271 |
# Hex strings of common ID lengths (32 = MD5, 40 = SHA1, 64 = SHA256)
|
| 272 |
-
(r"\b[a-fA-F0-9]{32}\b",
|
| 273 |
-
|
| 274 |
-
(r"\b[a-fA-F0-9]{
|
| 275 |
-
DynamicCategory.IDENTIFIER, "hex_40"),
|
| 276 |
-
(r"\b[a-fA-F0-9]{64}\b",
|
| 277 |
-
DynamicCategory.IDENTIFIER, "hex_64"),
|
| 278 |
-
|
| 279 |
# JWT tokens (three base64 sections separated by dots)
|
| 280 |
-
(
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
| 282 |
]
|
| 283 |
|
| 284 |
def __init__(self, config: DetectorConfig):
|
|
@@ -296,7 +339,7 @@ class RegexDetector:
|
|
| 296 |
labels_pattern = "|".join(re.escape(label) for label in config.dynamic_labels)
|
| 297 |
self._structural_pattern = re.compile(
|
| 298 |
rf"(?P<label>(?:{labels_pattern}))(?P<sep>\s*[:=]\s*|\s+)(?P<value>[^\n,;]+)",
|
| 299 |
-
re.IGNORECASE
|
| 300 |
)
|
| 301 |
|
| 302 |
# Compile custom patterns
|
|
@@ -319,15 +362,17 @@ class RegexDetector:
|
|
| 319 |
if end - start < self.config.min_span_length:
|
| 320 |
continue
|
| 321 |
|
| 322 |
-
spans.append(
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
| 331 |
seen_ranges.add((start, end))
|
| 332 |
|
| 333 |
# 2. Structural detection: "Label: value" patterns
|
|
@@ -355,15 +400,17 @@ class RegexDetector:
|
|
| 355 |
if not value.strip():
|
| 356 |
continue
|
| 357 |
|
| 358 |
-
spans.append(
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
| 367 |
seen_ranges.add((value_start, value_end))
|
| 368 |
|
| 369 |
# 3. Entropy-based detection for remaining potential IDs
|
|
@@ -378,15 +425,17 @@ class RegexDetector:
|
|
| 378 |
if end - start < self.config.min_span_length:
|
| 379 |
continue
|
| 380 |
|
| 381 |
-
spans.append(
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
| 390 |
seen_ranges.add((start, end))
|
| 391 |
|
| 392 |
return sorted(spans, key=lambda s: s.start)
|
|
@@ -432,15 +481,17 @@ class RegexDetector:
|
|
| 432 |
entropy = calculate_entropy(text)
|
| 433 |
|
| 434 |
if entropy >= self.config.entropy_threshold:
|
| 435 |
-
spans.append(
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
| 444 |
seen_ranges.add((start, end))
|
| 445 |
|
| 446 |
return spans
|
|
@@ -452,10 +503,7 @@ class RegexDetector:
|
|
| 452 |
seen_ranges: set[tuple[int, int]],
|
| 453 |
) -> bool:
|
| 454 |
"""Check if range overlaps with any existing range."""
|
| 455 |
-
return any(
|
| 456 |
-
not (end <= s or start >= e)
|
| 457 |
-
for s, e in seen_ranges
|
| 458 |
-
)
|
| 459 |
|
| 460 |
def _categorize_label(self, label: str) -> DynamicCategory:
|
| 461 |
"""Categorize based on the label name."""
|
|
@@ -478,15 +526,35 @@ class RegexDetector:
|
|
| 478 |
return DynamicCategory.REQUEST_ID
|
| 479 |
|
| 480 |
# User-related
|
| 481 |
-
if label in {
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
return DynamicCategory.USER_DATA
|
| 484 |
|
| 485 |
# System state
|
| 486 |
if label in {"version", "build", "commit", "branch", "revision"}:
|
| 487 |
return DynamicCategory.VERSION
|
| 488 |
-
if label in {
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
return DynamicCategory.VOLATILE
|
| 491 |
|
| 492 |
# Order/ticket
|
|
@@ -570,8 +638,7 @@ class NERDetector:
|
|
| 570 |
|
| 571 |
# Check for overlap with existing spans
|
| 572 |
overlaps = any(
|
| 573 |
-
not (ent.end_char <= s or ent.start_char >= e)
|
| 574 |
-
for s, e in existing_ranges
|
| 575 |
)
|
| 576 |
if overlaps:
|
| 577 |
continue
|
|
@@ -583,15 +650,17 @@ class NERDetector:
|
|
| 583 |
if category == DynamicCategory.UNKNOWN:
|
| 584 |
continue
|
| 585 |
|
| 586 |
-
spans.append(
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
|
|
|
|
|
|
| 595 |
existing_ranges.add((ent.start_char, ent.end_char))
|
| 596 |
|
| 597 |
return sorted(spans, key=lambda s: s.start), None
|
|
@@ -611,18 +680,15 @@ class SemanticDetector:
|
|
| 611 |
"Real-time data",
|
| 612 |
"Live prices",
|
| 613 |
"Current stock price",
|
| 614 |
-
|
| 615 |
# Session-specific
|
| 616 |
"Your session ID",
|
| 617 |
"Your account balance",
|
| 618 |
"Your recent orders",
|
| 619 |
"Your conversation history",
|
| 620 |
-
|
| 621 |
# User-specific
|
| 622 |
"Hello [user]",
|
| 623 |
"Dear customer",
|
| 624 |
"Your name is",
|
| 625 |
-
|
| 626 |
# System state
|
| 627 |
"Server status",
|
| 628 |
"System load",
|
|
@@ -707,10 +773,7 @@ class SemanticDetector:
|
|
| 707 |
continue
|
| 708 |
|
| 709 |
# Check overlap with existing spans
|
| 710 |
-
overlaps = any(
|
| 711 |
-
not (end <= s or start >= e)
|
| 712 |
-
for s, e in existing_ranges
|
| 713 |
-
)
|
| 714 |
if overlaps:
|
| 715 |
continue
|
| 716 |
|
|
@@ -721,18 +784,20 @@ class SemanticDetector:
|
|
| 721 |
# Determine category based on exemplar
|
| 722 |
category = self._categorize_exemplar(best_exemplar)
|
| 723 |
|
| 724 |
-
spans.append(
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
|
|
|
|
|
|
| 736 |
existing_ranges.add((start, end))
|
| 737 |
|
| 738 |
return sorted(spans, key=lambda s: s.start), None
|
|
@@ -740,7 +805,7 @@ class SemanticDetector:
|
|
| 740 |
def _split_sentences(self, content: str) -> list[tuple[str, int, int]]:
|
| 741 |
"""Split content into sentences with positions."""
|
| 742 |
sentences: list[tuple[str, int, int]] = []
|
| 743 |
-
pattern = r
|
| 744 |
for match in re.finditer(pattern, content):
|
| 745 |
text = match.group().strip()
|
| 746 |
if len(text) > 10:
|
|
@@ -820,6 +885,7 @@ class DynamicContentDetector:
|
|
| 820 |
DetectionResult with spans, static/dynamic content split, etc.
|
| 821 |
"""
|
| 822 |
import time
|
|
|
|
| 823 |
start_time = time.perf_counter()
|
| 824 |
|
| 825 |
all_spans: list[DynamicSpan] = []
|
|
@@ -881,7 +947,7 @@ class DynamicContentDetector:
|
|
| 881 |
|
| 882 |
for span in reversed(spans):
|
| 883 |
dynamic_parts.append(span.text)
|
| 884 |
-
static = static[:span.start] + static[span.end:]
|
| 885 |
|
| 886 |
static = self._clean_static_content(static)
|
| 887 |
dynamic_parts.reverse()
|
|
|
|
| 44 |
|
| 45 |
try:
|
| 46 |
import spacy
|
| 47 |
+
|
| 48 |
_SPACY_AVAILABLE = True
|
| 49 |
except ImportError:
|
| 50 |
spacy = None # type: ignore
|
| 51 |
|
| 52 |
try:
|
|
|
|
| 53 |
import numpy as np
|
| 54 |
+
from sentence_transformers import SentenceTransformer
|
| 55 |
+
|
| 56 |
_SENTENCE_TRANSFORMERS_AVAILABLE = True
|
| 57 |
except ImportError:
|
| 58 |
SentenceTransformer = None # type: ignore
|
|
|
|
| 140 |
"""Configuration for the dynamic content detector."""
|
| 141 |
|
| 142 |
# Which tiers to enable (order matters - later tiers can use earlier results)
|
| 143 |
+
tiers: list[Literal["regex", "ner", "semantic"]] = field(default_factory=lambda: ["regex"])
|
|
|
|
|
|
|
| 144 |
|
| 145 |
# Tier 1: Structural labels that indicate dynamic content
|
| 146 |
# These are the KEY names that hint the VALUE is dynamic
|
| 147 |
# Users can add domain-specific labels
|
| 148 |
+
dynamic_labels: list[str] = field(
|
| 149 |
+
default_factory=lambda: [
|
| 150 |
+
# Time-related
|
| 151 |
+
"date",
|
| 152 |
+
"time",
|
| 153 |
+
"timestamp",
|
| 154 |
+
"datetime",
|
| 155 |
+
"created",
|
| 156 |
+
"updated",
|
| 157 |
+
"modified",
|
| 158 |
+
"expires",
|
| 159 |
+
"last",
|
| 160 |
+
"current",
|
| 161 |
+
"today",
|
| 162 |
+
"now",
|
| 163 |
+
# Identifiers
|
| 164 |
+
"id",
|
| 165 |
+
"uuid",
|
| 166 |
+
"guid",
|
| 167 |
+
"session",
|
| 168 |
+
"request",
|
| 169 |
+
"trace",
|
| 170 |
+
"span",
|
| 171 |
+
"transaction",
|
| 172 |
+
"correlation",
|
| 173 |
+
"token",
|
| 174 |
+
"key",
|
| 175 |
+
"secret",
|
| 176 |
+
# User-related
|
| 177 |
+
"user",
|
| 178 |
+
"username",
|
| 179 |
+
"email",
|
| 180 |
+
"name",
|
| 181 |
+
"phone",
|
| 182 |
+
"address",
|
| 183 |
+
"customer",
|
| 184 |
+
"client",
|
| 185 |
+
"employee",
|
| 186 |
+
"member",
|
| 187 |
+
# System state
|
| 188 |
+
"version",
|
| 189 |
+
"build",
|
| 190 |
+
"commit",
|
| 191 |
+
"branch",
|
| 192 |
+
"revision",
|
| 193 |
+
"status",
|
| 194 |
+
"state",
|
| 195 |
+
"count",
|
| 196 |
+
"total",
|
| 197 |
+
"balance",
|
| 198 |
+
"remaining",
|
| 199 |
+
"load",
|
| 200 |
+
"queue",
|
| 201 |
+
"active",
|
| 202 |
+
"pending",
|
| 203 |
+
# Order/ticket related
|
| 204 |
+
"order",
|
| 205 |
+
"ticket",
|
| 206 |
+
"case",
|
| 207 |
+
"invoice",
|
| 208 |
+
"reference",
|
| 209 |
+
]
|
| 210 |
+
)
|
| 211 |
|
| 212 |
# Tier 1: Custom regex patterns (user-provided)
|
| 213 |
+
custom_patterns: list[tuple[str, DynamicCategory]] = field(default_factory=list)
|
|
|
|
|
|
|
| 214 |
|
| 215 |
# Entropy threshold for detecting random strings (0-1 scale normalized)
|
| 216 |
# Higher = more selective (only very random strings)
|
|
|
|
| 281 |
# Universal patterns (these formats are language-agnostic)
|
| 282 |
UNIVERSAL_PATTERNS = [
|
| 283 |
# UUID - truly universal format
|
| 284 |
+
(
|
| 285 |
+
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
| 286 |
+
DynamicCategory.UUID,
|
| 287 |
+
"uuid",
|
| 288 |
+
),
|
| 289 |
# ISO 8601 datetime (most universal date format)
|
| 290 |
+
(
|
| 291 |
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?",
|
| 292 |
+
DynamicCategory.DATETIME,
|
| 293 |
+
"iso_datetime",
|
| 294 |
+
),
|
| 295 |
# ISO 8601 date only
|
| 296 |
+
(r"\d{4}-\d{2}-\d{2}(?!\d)", DynamicCategory.DATE, "iso_date"),
|
|
|
|
|
|
|
| 297 |
# Unix timestamps (10-13 digits, but NOT within longer numbers)
|
| 298 |
+
(r"(?<![0-9])\d{10,13}(?![0-9])", DynamicCategory.TIMESTAMP, "unix_timestamp"),
|
|
|
|
|
|
|
| 299 |
# 24-hour time HH:MM:SS or HH:MM
|
| 300 |
+
(
|
| 301 |
+
r"(?<![0-9])\d{1,2}:\d{2}(?::\d{2})?(?:\s*(?:AM|PM|am|pm))?(?![0-9])",
|
| 302 |
+
DynamicCategory.TIME,
|
| 303 |
+
"time",
|
| 304 |
+
),
|
| 305 |
# Version numbers with v prefix (unambiguous)
|
| 306 |
+
(r"\bv\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?", DynamicCategory.VERSION, "version"),
|
|
|
|
|
|
|
| 307 |
# API key/token patterns (prefix + random string)
|
| 308 |
+
(
|
| 309 |
+
r"\b(?:sk|pk|api|key|token|bearer|auth)[-_][a-zA-Z0-9]{16,}",
|
| 310 |
+
DynamicCategory.REQUEST_ID,
|
| 311 |
+
"api_key",
|
| 312 |
+
),
|
| 313 |
# Common prefixed IDs (req_, sess_, txn_, etc.)
|
| 314 |
+
(r"\b[a-z]{2,6}_[a-zA-Z0-9]{8,}", DynamicCategory.REQUEST_ID, "prefixed_id"),
|
|
|
|
|
|
|
| 315 |
# Hex strings of common ID lengths (32 = MD5, 40 = SHA1, 64 = SHA256)
|
| 316 |
+
(r"\b[a-fA-F0-9]{32}\b", DynamicCategory.IDENTIFIER, "hex_32"),
|
| 317 |
+
(r"\b[a-fA-F0-9]{40}\b", DynamicCategory.IDENTIFIER, "hex_40"),
|
| 318 |
+
(r"\b[a-fA-F0-9]{64}\b", DynamicCategory.IDENTIFIER, "hex_64"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
# JWT tokens (three base64 sections separated by dots)
|
| 320 |
+
(
|
| 321 |
+
r"eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+",
|
| 322 |
+
DynamicCategory.REQUEST_ID,
|
| 323 |
+
"jwt",
|
| 324 |
+
),
|
| 325 |
]
|
| 326 |
|
| 327 |
def __init__(self, config: DetectorConfig):
|
|
|
|
| 339 |
labels_pattern = "|".join(re.escape(label) for label in config.dynamic_labels)
|
| 340 |
self._structural_pattern = re.compile(
|
| 341 |
rf"(?P<label>(?:{labels_pattern}))(?P<sep>\s*[:=]\s*|\s+)(?P<value>[^\n,;]+)",
|
| 342 |
+
re.IGNORECASE,
|
| 343 |
)
|
| 344 |
|
| 345 |
# Compile custom patterns
|
|
|
|
| 362 |
if end - start < self.config.min_span_length:
|
| 363 |
continue
|
| 364 |
|
| 365 |
+
spans.append(
|
| 366 |
+
DynamicSpan(
|
| 367 |
+
text=match.group(),
|
| 368 |
+
start=start,
|
| 369 |
+
end=end,
|
| 370 |
+
category=category,
|
| 371 |
+
tier="regex",
|
| 372 |
+
confidence=1.0,
|
| 373 |
+
metadata={"pattern": pattern_name, "method": "universal"},
|
| 374 |
+
)
|
| 375 |
+
)
|
| 376 |
seen_ranges.add((start, end))
|
| 377 |
|
| 378 |
# 2. Structural detection: "Label: value" patterns
|
|
|
|
| 400 |
if not value.strip():
|
| 401 |
continue
|
| 402 |
|
| 403 |
+
spans.append(
|
| 404 |
+
DynamicSpan(
|
| 405 |
+
text=value,
|
| 406 |
+
start=value_start,
|
| 407 |
+
end=value_end,
|
| 408 |
+
category=category,
|
| 409 |
+
tier="regex",
|
| 410 |
+
confidence=0.9,
|
| 411 |
+
metadata={"pattern": "structural", "method": "structural", "label": label},
|
| 412 |
+
)
|
| 413 |
+
)
|
| 414 |
seen_ranges.add((value_start, value_end))
|
| 415 |
|
| 416 |
# 3. Entropy-based detection for remaining potential IDs
|
|
|
|
| 425 |
if end - start < self.config.min_span_length:
|
| 426 |
continue
|
| 427 |
|
| 428 |
+
spans.append(
|
| 429 |
+
DynamicSpan(
|
| 430 |
+
text=match.group(),
|
| 431 |
+
start=start,
|
| 432 |
+
end=end,
|
| 433 |
+
category=category,
|
| 434 |
+
tier="regex",
|
| 435 |
+
confidence=0.8,
|
| 436 |
+
metadata={"pattern": "custom", "method": "custom"},
|
| 437 |
+
)
|
| 438 |
+
)
|
| 439 |
seen_ranges.add((start, end))
|
| 440 |
|
| 441 |
return sorted(spans, key=lambda s: s.start)
|
|
|
|
| 481 |
entropy = calculate_entropy(text)
|
| 482 |
|
| 483 |
if entropy >= self.config.entropy_threshold:
|
| 484 |
+
spans.append(
|
| 485 |
+
DynamicSpan(
|
| 486 |
+
text=text,
|
| 487 |
+
start=start,
|
| 488 |
+
end=end,
|
| 489 |
+
category=DynamicCategory.IDENTIFIER,
|
| 490 |
+
tier="regex",
|
| 491 |
+
confidence=entropy, # Use entropy as confidence
|
| 492 |
+
metadata={"pattern": "entropy", "method": "entropy", "entropy": entropy},
|
| 493 |
+
)
|
| 494 |
+
)
|
| 495 |
seen_ranges.add((start, end))
|
| 496 |
|
| 497 |
return spans
|
|
|
|
| 503 |
seen_ranges: set[tuple[int, int]],
|
| 504 |
) -> bool:
|
| 505 |
"""Check if range overlaps with any existing range."""
|
| 506 |
+
return any(not (end <= s or start >= e) for s, e in seen_ranges)
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
def _categorize_label(self, label: str) -> DynamicCategory:
|
| 509 |
"""Categorize based on the label name."""
|
|
|
|
| 526 |
return DynamicCategory.REQUEST_ID
|
| 527 |
|
| 528 |
# User-related
|
| 529 |
+
if label in {
|
| 530 |
+
"user",
|
| 531 |
+
"username",
|
| 532 |
+
"email",
|
| 533 |
+
"name",
|
| 534 |
+
"phone",
|
| 535 |
+
"address",
|
| 536 |
+
"customer",
|
| 537 |
+
"client",
|
| 538 |
+
"employee",
|
| 539 |
+
"member",
|
| 540 |
+
}:
|
| 541 |
return DynamicCategory.USER_DATA
|
| 542 |
|
| 543 |
# System state
|
| 544 |
if label in {"version", "build", "commit", "branch", "revision"}:
|
| 545 |
return DynamicCategory.VERSION
|
| 546 |
+
if label in {
|
| 547 |
+
"status",
|
| 548 |
+
"state",
|
| 549 |
+
"count",
|
| 550 |
+
"total",
|
| 551 |
+
"balance",
|
| 552 |
+
"remaining",
|
| 553 |
+
"load",
|
| 554 |
+
"queue",
|
| 555 |
+
"active",
|
| 556 |
+
"pending",
|
| 557 |
+
}:
|
| 558 |
return DynamicCategory.VOLATILE
|
| 559 |
|
| 560 |
# Order/ticket
|
|
|
|
| 638 |
|
| 639 |
# Check for overlap with existing spans
|
| 640 |
overlaps = any(
|
| 641 |
+
not (ent.end_char <= s or ent.start_char >= e) for s, e in existing_ranges
|
|
|
|
| 642 |
)
|
| 643 |
if overlaps:
|
| 644 |
continue
|
|
|
|
| 650 |
if category == DynamicCategory.UNKNOWN:
|
| 651 |
continue
|
| 652 |
|
| 653 |
+
spans.append(
|
| 654 |
+
DynamicSpan(
|
| 655 |
+
text=ent.text,
|
| 656 |
+
start=ent.start_char,
|
| 657 |
+
end=ent.end_char,
|
| 658 |
+
category=category,
|
| 659 |
+
tier="ner",
|
| 660 |
+
confidence=0.9,
|
| 661 |
+
metadata={"entity_type": ent.label_},
|
| 662 |
+
)
|
| 663 |
+
)
|
| 664 |
existing_ranges.add((ent.start_char, ent.end_char))
|
| 665 |
|
| 666 |
return sorted(spans, key=lambda s: s.start), None
|
|
|
|
| 680 |
"Real-time data",
|
| 681 |
"Live prices",
|
| 682 |
"Current stock price",
|
|
|
|
| 683 |
# Session-specific
|
| 684 |
"Your session ID",
|
| 685 |
"Your account balance",
|
| 686 |
"Your recent orders",
|
| 687 |
"Your conversation history",
|
|
|
|
| 688 |
# User-specific
|
| 689 |
"Hello [user]",
|
| 690 |
"Dear customer",
|
| 691 |
"Your name is",
|
|
|
|
| 692 |
# System state
|
| 693 |
"Server status",
|
| 694 |
"System load",
|
|
|
|
| 773 |
continue
|
| 774 |
|
| 775 |
# Check overlap with existing spans
|
| 776 |
+
overlaps = any(not (end <= s or start >= e) for s, e in existing_ranges)
|
|
|
|
|
|
|
|
|
|
| 777 |
if overlaps:
|
| 778 |
continue
|
| 779 |
|
|
|
|
| 784 |
# Determine category based on exemplar
|
| 785 |
category = self._categorize_exemplar(best_exemplar)
|
| 786 |
|
| 787 |
+
spans.append(
|
| 788 |
+
DynamicSpan(
|
| 789 |
+
text=text,
|
| 790 |
+
start=start,
|
| 791 |
+
end=end,
|
| 792 |
+
category=category,
|
| 793 |
+
tier="semantic",
|
| 794 |
+
confidence=max_sim,
|
| 795 |
+
metadata={
|
| 796 |
+
"matched_exemplar": best_exemplar,
|
| 797 |
+
"similarity": max_sim,
|
| 798 |
+
},
|
| 799 |
+
)
|
| 800 |
+
)
|
| 801 |
existing_ranges.add((start, end))
|
| 802 |
|
| 803 |
return sorted(spans, key=lambda s: s.start), None
|
|
|
|
| 805 |
def _split_sentences(self, content: str) -> list[tuple[str, int, int]]:
|
| 806 |
"""Split content into sentences with positions."""
|
| 807 |
sentences: list[tuple[str, int, int]] = []
|
| 808 |
+
pattern = r"[^.!?\n]+[.!?\n]?"
|
| 809 |
for match in re.finditer(pattern, content):
|
| 810 |
text = match.group().strip()
|
| 811 |
if len(text) > 10:
|
|
|
|
| 885 |
DetectionResult with spans, static/dynamic content split, etc.
|
| 886 |
"""
|
| 887 |
import time
|
| 888 |
+
|
| 889 |
start_time = time.perf_counter()
|
| 890 |
|
| 891 |
all_spans: list[DynamicSpan] = []
|
|
|
|
| 947 |
|
| 948 |
for span in reversed(spans):
|
| 949 |
dynamic_parts.append(span.text)
|
| 950 |
+
static = static[: span.start] + static[span.end :]
|
| 951 |
|
| 952 |
static = self._clean_static_content(static)
|
| 953 |
dynamic_parts.reverse()
|
headroom/cache/google.py
CHANGED
|
@@ -274,16 +274,13 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 274 |
Returns:
|
| 275 |
CacheResult with analysis and cache information
|
| 276 |
"""
|
| 277 |
-
effective_config = config or self.config
|
| 278 |
|
| 279 |
# Extract cacheable content (system messages + static context)
|
| 280 |
cacheable_content = self._extract_cacheable_content(messages)
|
| 281 |
content_hash = self._compute_prefix_hash(cacheable_content)
|
| 282 |
|
| 283 |
# Estimate tokens
|
| 284 |
-
total_tokens = self._count_tokens_estimate(
|
| 285 |
-
self._messages_to_text(messages)
|
| 286 |
-
)
|
| 287 |
cacheable_tokens = self._count_tokens_estimate(cacheable_content)
|
| 288 |
|
| 289 |
# Check for existing cache
|
|
@@ -371,9 +368,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 371 |
cacheable_content = self._extract_cacheable_content(messages)
|
| 372 |
content_hash = self._compute_prefix_hash(cacheable_content)
|
| 373 |
|
| 374 |
-
total_tokens = self._count_tokens_estimate(
|
| 375 |
-
self._messages_to_text(messages)
|
| 376 |
-
)
|
| 377 |
cacheable_tokens = self._count_tokens_estimate(cacheable_content)
|
| 378 |
|
| 379 |
is_cacheable = cacheable_tokens >= GOOGLE_MIN_CACHE_TOKENS
|
|
@@ -384,33 +379,30 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 384 |
|
| 385 |
if not is_cacheable:
|
| 386 |
recommendations.append(
|
| 387 |
-
f"Add {tokens_below_minimum:,} more tokens to static content "
|
| 388 |
-
f"to enable caching"
|
| 389 |
)
|
| 390 |
recommendations.append(
|
| 391 |
"Consider adding detailed examples or documentation to system prompt"
|
| 392 |
)
|
| 393 |
else:
|
| 394 |
recommendations.append(
|
| 395 |
-
|
| 396 |
)
|
| 397 |
|
| 398 |
# Storage cost estimation (rough - actual pricing varies)
|
| 399 |
# Assuming ~$0.001 per 1000 tokens per hour (simplified)
|
| 400 |
hourly_cost = (cacheable_tokens / 1000) * 0.001
|
| 401 |
-
recommendations.append(
|
| 402 |
-
f"Estimated storage cost: ~${hourly_cost:.4f}/hour"
|
| 403 |
-
)
|
| 404 |
|
| 405 |
# Break-even analysis
|
| 406 |
if hourly_cost > 0:
|
| 407 |
# Assuming $0.01 per 1000 input tokens base price
|
| 408 |
base_cost_per_request = (cacheable_tokens / 1000) * 0.01
|
| 409 |
savings_per_request = base_cost_per_request * GOOGLE_CACHE_DISCOUNT
|
| 410 |
-
break_even_requests =
|
| 411 |
-
|
| 412 |
-
f"Break-even: ~{int(break_even_requests)} requests/hour"
|
| 413 |
)
|
|
|
|
| 414 |
|
| 415 |
return CacheabilityAnalysis(
|
| 416 |
is_cacheable=is_cacheable,
|
|
@@ -580,9 +572,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 580 |
old_expires = cache_info.expires_at
|
| 581 |
cache_info.expires_at = new_expires_at
|
| 582 |
|
| 583 |
-
logger.info(
|
| 584 |
-
f"Extended cache {cache_id} TTL from {old_expires} to {new_expires_at}"
|
| 585 |
-
)
|
| 586 |
|
| 587 |
return cache_info
|
| 588 |
|
|
@@ -721,8 +711,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 721 |
|
| 722 |
if not analysis.is_cacheable:
|
| 723 |
logger.debug(
|
| 724 |
-
f"Content not cacheable: {analysis.tokens_below_minimum} "
|
| 725 |
-
f"tokens below minimum"
|
| 726 |
)
|
| 727 |
return None
|
| 728 |
|
|
@@ -764,8 +753,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer):
|
|
| 764 |
"cached_content": cache_id,
|
| 765 |
"contents": dynamic_messages,
|
| 766 |
"_headroom_note": (
|
| 767 |
-
"Use cached_content parameter with GenerativeModel "
|
| 768 |
-
"to leverage the cache"
|
| 769 |
),
|
| 770 |
}
|
| 771 |
|
|
|
|
| 274 |
Returns:
|
| 275 |
CacheResult with analysis and cache information
|
| 276 |
"""
|
|
|
|
| 277 |
|
| 278 |
# Extract cacheable content (system messages + static context)
|
| 279 |
cacheable_content = self._extract_cacheable_content(messages)
|
| 280 |
content_hash = self._compute_prefix_hash(cacheable_content)
|
| 281 |
|
| 282 |
# Estimate tokens
|
| 283 |
+
total_tokens = self._count_tokens_estimate(self._messages_to_text(messages))
|
|
|
|
|
|
|
| 284 |
cacheable_tokens = self._count_tokens_estimate(cacheable_content)
|
| 285 |
|
| 286 |
# Check for existing cache
|
|
|
|
| 368 |
cacheable_content = self._extract_cacheable_content(messages)
|
| 369 |
content_hash = self._compute_prefix_hash(cacheable_content)
|
| 370 |
|
| 371 |
+
total_tokens = self._count_tokens_estimate(self._messages_to_text(messages))
|
|
|
|
|
|
|
| 372 |
cacheable_tokens = self._count_tokens_estimate(cacheable_content)
|
| 373 |
|
| 374 |
is_cacheable = cacheable_tokens >= GOOGLE_MIN_CACHE_TOKENS
|
|
|
|
| 379 |
|
| 380 |
if not is_cacheable:
|
| 381 |
recommendations.append(
|
| 382 |
+
f"Add {tokens_below_minimum:,} more tokens to static content to enable caching"
|
|
|
|
| 383 |
)
|
| 384 |
recommendations.append(
|
| 385 |
"Consider adding detailed examples or documentation to system prompt"
|
| 386 |
)
|
| 387 |
else:
|
| 388 |
recommendations.append(
|
| 389 |
+
"Content is cacheable. Create cache with google-generativeai SDK"
|
| 390 |
)
|
| 391 |
|
| 392 |
# Storage cost estimation (rough - actual pricing varies)
|
| 393 |
# Assuming ~$0.001 per 1000 tokens per hour (simplified)
|
| 394 |
hourly_cost = (cacheable_tokens / 1000) * 0.001
|
| 395 |
+
recommendations.append(f"Estimated storage cost: ~${hourly_cost:.4f}/hour")
|
|
|
|
|
|
|
| 396 |
|
| 397 |
# Break-even analysis
|
| 398 |
if hourly_cost > 0:
|
| 399 |
# Assuming $0.01 per 1000 input tokens base price
|
| 400 |
base_cost_per_request = (cacheable_tokens / 1000) * 0.01
|
| 401 |
savings_per_request = base_cost_per_request * GOOGLE_CACHE_DISCOUNT
|
| 402 |
+
break_even_requests = (
|
| 403 |
+
hourly_cost / savings_per_request if savings_per_request > 0 else float("inf")
|
|
|
|
| 404 |
)
|
| 405 |
+
recommendations.append(f"Break-even: ~{int(break_even_requests)} requests/hour")
|
| 406 |
|
| 407 |
return CacheabilityAnalysis(
|
| 408 |
is_cacheable=is_cacheable,
|
|
|
|
| 572 |
old_expires = cache_info.expires_at
|
| 573 |
cache_info.expires_at = new_expires_at
|
| 574 |
|
| 575 |
+
logger.info(f"Extended cache {cache_id} TTL from {old_expires} to {new_expires_at}")
|
|
|
|
|
|
|
| 576 |
|
| 577 |
return cache_info
|
| 578 |
|
|
|
|
| 711 |
|
| 712 |
if not analysis.is_cacheable:
|
| 713 |
logger.debug(
|
| 714 |
+
f"Content not cacheable: {analysis.tokens_below_minimum} tokens below minimum"
|
|
|
|
| 715 |
)
|
| 716 |
return None
|
| 717 |
|
|
|
|
| 753 |
"cached_content": cache_id,
|
| 754 |
"contents": dynamic_messages,
|
| 755 |
"_headroom_note": (
|
| 756 |
+
"Use cached_content parameter with GenerativeModel to leverage the cache"
|
|
|
|
| 757 |
),
|
| 758 |
}
|
| 759 |
|
headroom/cache/openai.py
CHANGED
|
@@ -42,10 +42,9 @@ Usage:
|
|
| 42 |
|
| 43 |
from __future__ import annotations
|
| 44 |
|
| 45 |
-
import re
|
| 46 |
from copy import deepcopy
|
| 47 |
from dataclasses import dataclass, field
|
| 48 |
-
from typing import Any
|
| 49 |
|
| 50 |
from .base import (
|
| 51 |
BaseCacheOptimizer,
|
|
@@ -233,12 +232,8 @@ class OpenAICacheOptimizer(BaseCacheOptimizer):
|
|
| 233 |
warnings.extend(result.warnings)
|
| 234 |
|
| 235 |
if result.spans:
|
| 236 |
-
transforms_applied.append(
|
| 237 |
-
|
| 238 |
-
)
|
| 239 |
-
transforms_applied.extend(
|
| 240 |
-
f"tier_{tier}" for tier in result.tiers_used
|
| 241 |
-
)
|
| 242 |
|
| 243 |
# Get static content with dynamic parts removed
|
| 244 |
stabilized = result.static_content
|
|
@@ -382,8 +377,7 @@ class OpenAICacheOptimizer(BaseCacheOptimizer):
|
|
| 382 |
# Check if prefix is stable
|
| 383 |
current_hash = self._compute_prefix_hash(system_content)
|
| 384 |
likely_hit = (
|
| 385 |
-
self._previous_prefix_hash is not None
|
| 386 |
-
and current_hash == self._previous_prefix_hash
|
| 387 |
)
|
| 388 |
|
| 389 |
if likely_hit:
|
|
@@ -427,7 +421,9 @@ class OpenAICacheOptimizer(BaseCacheOptimizer):
|
|
| 427 |
leading = len(line) - len(line.lstrip())
|
| 428 |
# Collapse multiple spaces in content (not indentation)
|
| 429 |
content_part = " ".join(stripped.split())
|
| 430 |
-
normalized_lines.append(
|
|
|
|
|
|
|
| 431 |
else:
|
| 432 |
normalized_lines.append("")
|
| 433 |
|
|
@@ -581,11 +577,8 @@ class OpenAICacheOptimizer(BaseCacheOptimizer):
|
|
| 581 |
for block in content:
|
| 582 |
if isinstance(block, dict):
|
| 583 |
if block.get("type") == "text":
|
| 584 |
-
total += self._count_tokens_estimate(
|
| 585 |
-
block.get("text", "")
|
| 586 |
-
)
|
| 587 |
elif block.get("type") == "image_url":
|
| 588 |
# Rough estimate for images
|
| 589 |
total += 85 # Base cost
|
| 590 |
return total
|
| 591 |
-
|
|
|
|
| 42 |
|
| 43 |
from __future__ import annotations
|
| 44 |
|
|
|
|
| 45 |
from copy import deepcopy
|
| 46 |
from dataclasses import dataclass, field
|
| 47 |
+
from typing import Any
|
| 48 |
|
| 49 |
from .base import (
|
| 50 |
BaseCacheOptimizer,
|
|
|
|
| 232 |
warnings.extend(result.warnings)
|
| 233 |
|
| 234 |
if result.spans:
|
| 235 |
+
transforms_applied.append(f"extracted_{len(result.spans)}_dynamic_elements")
|
| 236 |
+
transforms_applied.extend(f"tier_{tier}" for tier in result.tiers_used)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
# Get static content with dynamic parts removed
|
| 239 |
stabilized = result.static_content
|
|
|
|
| 377 |
# Check if prefix is stable
|
| 378 |
current_hash = self._compute_prefix_hash(system_content)
|
| 379 |
likely_hit = (
|
| 380 |
+
self._previous_prefix_hash is not None and current_hash == self._previous_prefix_hash
|
|
|
|
| 381 |
)
|
| 382 |
|
| 383 |
if likely_hit:
|
|
|
|
| 421 |
leading = len(line) - len(line.lstrip())
|
| 422 |
# Collapse multiple spaces in content (not indentation)
|
| 423 |
content_part = " ".join(stripped.split())
|
| 424 |
+
normalized_lines.append(
|
| 425 |
+
" " * leading + content_part[leading:] if leading else content_part
|
| 426 |
+
)
|
| 427 |
else:
|
| 428 |
normalized_lines.append("")
|
| 429 |
|
|
|
|
| 577 |
for block in content:
|
| 578 |
if isinstance(block, dict):
|
| 579 |
if block.get("type") == "text":
|
| 580 |
+
total += self._count_tokens_estimate(block.get("text", ""))
|
|
|
|
|
|
|
| 581 |
elif block.get("type") == "image_url":
|
| 582 |
# Rough estimate for images
|
| 583 |
total += 85 # Base cost
|
| 584 |
return total
|
|
|
headroom/cache/registry.py
CHANGED
|
@@ -7,9 +7,7 @@ This allows users to swap implementations and register custom optimizers.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
-
from
|
| 11 |
-
|
| 12 |
-
from .base import CacheOptimizer, BaseCacheOptimizer, CacheConfig
|
| 13 |
|
| 14 |
|
| 15 |
class CacheOptimizerRegistry:
|
|
@@ -32,14 +30,14 @@ class CacheOptimizerRegistry:
|
|
| 32 |
CacheOptimizerRegistry.register("my-provider", MyOptimizer)
|
| 33 |
"""
|
| 34 |
|
| 35 |
-
_optimizers: dict[str,
|
| 36 |
_instances: dict[str, BaseCacheOptimizer] = {}
|
| 37 |
|
| 38 |
@classmethod
|
| 39 |
def register(
|
| 40 |
cls,
|
| 41 |
name: str,
|
| 42 |
-
optimizer_class:
|
| 43 |
*,
|
| 44 |
override: bool = False,
|
| 45 |
) -> None:
|
|
@@ -56,8 +54,7 @@ class CacheOptimizerRegistry:
|
|
| 56 |
"""
|
| 57 |
if name in cls._optimizers and not override:
|
| 58 |
raise ValueError(
|
| 59 |
-
f"Optimizer '{name}' already registered. "
|
| 60 |
-
f"Use override=True to replace."
|
| 61 |
)
|
| 62 |
cls._optimizers[name] = optimizer_class
|
| 63 |
# Clear cached instance if exists
|
|
@@ -109,10 +106,7 @@ class CacheOptimizerRegistry:
|
|
| 109 |
|
| 110 |
if key not in cls._optimizers:
|
| 111 |
available = list(cls._optimizers.keys())
|
| 112 |
-
raise KeyError(
|
| 113 |
-
f"No optimizer registered for '{key}'. "
|
| 114 |
-
f"Available: {available}"
|
| 115 |
-
)
|
| 116 |
|
| 117 |
# Return cached instance if requested
|
| 118 |
cache_key = f"{key}:{id(config)}" if config else key
|
|
@@ -165,8 +159,8 @@ def _register_defaults() -> None:
|
|
| 165 |
"""Register default optimizers."""
|
| 166 |
# Import here to avoid circular imports
|
| 167 |
from .anthropic import AnthropicCacheOptimizer
|
| 168 |
-
from .openai import OpenAICacheOptimizer
|
| 169 |
from .google import GoogleCacheOptimizer
|
|
|
|
| 170 |
|
| 171 |
CacheOptimizerRegistry.register("anthropic", AnthropicCacheOptimizer)
|
| 172 |
CacheOptimizerRegistry.register("openai", OpenAICacheOptimizer)
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
from .base import BaseCacheOptimizer, CacheConfig
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
class CacheOptimizerRegistry:
|
|
|
|
| 30 |
CacheOptimizerRegistry.register("my-provider", MyOptimizer)
|
| 31 |
"""
|
| 32 |
|
| 33 |
+
_optimizers: dict[str, type[BaseCacheOptimizer]] = {}
|
| 34 |
_instances: dict[str, BaseCacheOptimizer] = {}
|
| 35 |
|
| 36 |
@classmethod
|
| 37 |
def register(
|
| 38 |
cls,
|
| 39 |
name: str,
|
| 40 |
+
optimizer_class: type[BaseCacheOptimizer],
|
| 41 |
*,
|
| 42 |
override: bool = False,
|
| 43 |
) -> None:
|
|
|
|
| 54 |
"""
|
| 55 |
if name in cls._optimizers and not override:
|
| 56 |
raise ValueError(
|
| 57 |
+
f"Optimizer '{name}' already registered. Use override=True to replace."
|
|
|
|
| 58 |
)
|
| 59 |
cls._optimizers[name] = optimizer_class
|
| 60 |
# Clear cached instance if exists
|
|
|
|
| 106 |
|
| 107 |
if key not in cls._optimizers:
|
| 108 |
available = list(cls._optimizers.keys())
|
| 109 |
+
raise KeyError(f"No optimizer registered for '{key}'. Available: {available}")
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# Return cached instance if requested
|
| 112 |
cache_key = f"{key}:{id(config)}" if config else key
|
|
|
|
| 159 |
"""Register default optimizers."""
|
| 160 |
# Import here to avoid circular imports
|
| 161 |
from .anthropic import AnthropicCacheOptimizer
|
|
|
|
| 162 |
from .google import GoogleCacheOptimizer
|
| 163 |
+
from .openai import OpenAICacheOptimizer
|
| 164 |
|
| 165 |
CacheOptimizerRegistry.register("anthropic", AnthropicCacheOptimizer)
|
| 166 |
CacheOptimizerRegistry.register("openai", OpenAICacheOptimizer)
|
headroom/cache/semantic.py
CHANGED
|
@@ -38,8 +38,9 @@ from __future__ import annotations
|
|
| 38 |
import hashlib
|
| 39 |
import time
|
| 40 |
from collections import OrderedDict
|
| 41 |
-
from
|
| 42 |
-
from
|
|
|
|
| 43 |
|
| 44 |
from .base import (
|
| 45 |
BaseCacheOptimizer,
|
|
@@ -303,7 +304,8 @@ class SemanticCache:
|
|
| 303 |
|
| 304 |
now = time.time()
|
| 305 |
expired = [
|
| 306 |
-
key
|
|
|
|
| 307 |
if now - entry.created_at > self.config.ttl_seconds
|
| 308 |
]
|
| 309 |
|
|
@@ -440,6 +442,7 @@ class SemanticCacheLayer:
|
|
| 440 |
def _compute_messages_hash(self, messages: list[dict[str, Any]]) -> str:
|
| 441 |
"""Compute a hash of all messages."""
|
| 442 |
import json
|
|
|
|
| 443 |
try:
|
| 444 |
content = json.dumps(messages, sort_keys=True)
|
| 445 |
return hashlib.sha256(content.encode()).hexdigest()[:24]
|
|
|
|
| 38 |
import hashlib
|
| 39 |
import time
|
| 40 |
from collections import OrderedDict
|
| 41 |
+
from collections.abc import Callable
|
| 42 |
+
from dataclasses import dataclass
|
| 43 |
+
from typing import Any
|
| 44 |
|
| 45 |
from .base import (
|
| 46 |
BaseCacheOptimizer,
|
|
|
|
| 304 |
|
| 305 |
now = time.time()
|
| 306 |
expired = [
|
| 307 |
+
key
|
| 308 |
+
for key, entry in self._cache.items()
|
| 309 |
if now - entry.created_at > self.config.ttl_seconds
|
| 310 |
]
|
| 311 |
|
|
|
|
| 442 |
def _compute_messages_hash(self, messages: list[dict[str, Any]]) -> str:
|
| 443 |
"""Compute a hash of all messages."""
|
| 444 |
import json
|
| 445 |
+
|
| 446 |
try:
|
| 447 |
content = json.dumps(messages, sort_keys=True)
|
| 448 |
return hashlib.sha256(content.encode()).hexdigest()[:24]
|
headroom/ccr/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from .tool_injection import (
|
|
| 21 |
# MCP server is optional (requires mcp package)
|
| 22 |
try:
|
| 23 |
from .mcp_server import CCRMCPServer, create_ccr_mcp_server
|
|
|
|
| 24 |
MCP_SERVER_AVAILABLE = True
|
| 25 |
except ImportError:
|
| 26 |
CCRMCPServer = None # type: ignore
|
|
|
|
| 21 |
# MCP server is optional (requires mcp package)
|
| 22 |
try:
|
| 23 |
from .mcp_server import CCRMCPServer, create_ccr_mcp_server
|
| 24 |
+
|
| 25 |
MCP_SERVER_AVAILABLE = True
|
| 26 |
except ImportError:
|
| 27 |
CCRMCPServer = None # type: ignore
|
headroom/ccr/mcp_server.py
CHANGED
|
@@ -32,7 +32,6 @@ import asyncio
|
|
| 32 |
import json
|
| 33 |
import logging
|
| 34 |
import os
|
| 35 |
-
import sys
|
| 36 |
from typing import Any
|
| 37 |
|
| 38 |
# Try to import MCP SDK
|
|
@@ -40,6 +39,7 @@ try:
|
|
| 40 |
from mcp.server import Server
|
| 41 |
from mcp.server.stdio import stdio_server
|
| 42 |
from mcp.types import TextContent, Tool
|
|
|
|
| 43 |
MCP_AVAILABLE = True
|
| 44 |
except ImportError:
|
| 45 |
MCP_AVAILABLE = False
|
|
@@ -49,6 +49,7 @@ except ImportError:
|
|
| 49 |
# Try to import httpx for proxy communication
|
| 50 |
try:
|
| 51 |
import httpx
|
|
|
|
| 52 |
HTTPX_AVAILABLE = True
|
| 53 |
except ImportError:
|
| 54 |
HTTPX_AVAILABLE = False
|
|
@@ -88,14 +89,11 @@ class CCRMCPServer:
|
|
| 88 |
self._http_client: httpx.AsyncClient | None = None
|
| 89 |
|
| 90 |
if not MCP_AVAILABLE:
|
| 91 |
-
raise ImportError(
|
| 92 |
-
"MCP SDK not installed. Install with: pip install mcp"
|
| 93 |
-
)
|
| 94 |
|
| 95 |
if not direct_mode and not HTTPX_AVAILABLE:
|
| 96 |
raise ImportError(
|
| 97 |
-
"httpx not installed (required for HTTP mode). "
|
| 98 |
-
"Install with: pip install httpx"
|
| 99 |
)
|
| 100 |
|
| 101 |
self.server = Server("headroom-ccr")
|
|
@@ -140,19 +138,23 @@ class CCRMCPServer:
|
|
| 140 |
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
| 141 |
"""Handle tool calls."""
|
| 142 |
if name != CCR_TOOL_NAME:
|
| 143 |
-
return [
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
| 147 |
|
| 148 |
hash_key = arguments.get("hash")
|
| 149 |
query = arguments.get("query")
|
| 150 |
|
| 151 |
if not hash_key:
|
| 152 |
-
return [
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
| 156 |
|
| 157 |
# Retrieve content
|
| 158 |
try:
|
|
@@ -161,16 +163,20 @@ class CCRMCPServer:
|
|
| 161 |
else:
|
| 162 |
result = await self._retrieve_via_proxy(hash_key, query)
|
| 163 |
|
| 164 |
-
return [
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
| 168 |
except Exception as e:
|
| 169 |
logger.error(f"Retrieval failed: {e}")
|
| 170 |
-
return [
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
|
| 175 |
async def _retrieve_via_proxy(
|
| 176 |
self,
|
|
|
|
| 32 |
import json
|
| 33 |
import logging
|
| 34 |
import os
|
|
|
|
| 35 |
from typing import Any
|
| 36 |
|
| 37 |
# Try to import MCP SDK
|
|
|
|
| 39 |
from mcp.server import Server
|
| 40 |
from mcp.server.stdio import stdio_server
|
| 41 |
from mcp.types import TextContent, Tool
|
| 42 |
+
|
| 43 |
MCP_AVAILABLE = True
|
| 44 |
except ImportError:
|
| 45 |
MCP_AVAILABLE = False
|
|
|
|
| 49 |
# Try to import httpx for proxy communication
|
| 50 |
try:
|
| 51 |
import httpx
|
| 52 |
+
|
| 53 |
HTTPX_AVAILABLE = True
|
| 54 |
except ImportError:
|
| 55 |
HTTPX_AVAILABLE = False
|
|
|
|
| 89 |
self._http_client: httpx.AsyncClient | None = None
|
| 90 |
|
| 91 |
if not MCP_AVAILABLE:
|
| 92 |
+
raise ImportError("MCP SDK not installed. Install with: pip install mcp")
|
|
|
|
|
|
|
| 93 |
|
| 94 |
if not direct_mode and not HTTPX_AVAILABLE:
|
| 95 |
raise ImportError(
|
| 96 |
+
"httpx not installed (required for HTTP mode). Install with: pip install httpx"
|
|
|
|
| 97 |
)
|
| 98 |
|
| 99 |
self.server = Server("headroom-ccr")
|
|
|
|
| 138 |
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
| 139 |
"""Handle tool calls."""
|
| 140 |
if name != CCR_TOOL_NAME:
|
| 141 |
+
return [
|
| 142 |
+
TextContent(
|
| 143 |
+
type="text",
|
| 144 |
+
text=json.dumps({"error": f"Unknown tool: {name}"}),
|
| 145 |
+
)
|
| 146 |
+
]
|
| 147 |
|
| 148 |
hash_key = arguments.get("hash")
|
| 149 |
query = arguments.get("query")
|
| 150 |
|
| 151 |
if not hash_key:
|
| 152 |
+
return [
|
| 153 |
+
TextContent(
|
| 154 |
+
type="text",
|
| 155 |
+
text=json.dumps({"error": "hash parameter is required"}),
|
| 156 |
+
)
|
| 157 |
+
]
|
| 158 |
|
| 159 |
# Retrieve content
|
| 160 |
try:
|
|
|
|
| 163 |
else:
|
| 164 |
result = await self._retrieve_via_proxy(hash_key, query)
|
| 165 |
|
| 166 |
+
return [
|
| 167 |
+
TextContent(
|
| 168 |
+
type="text",
|
| 169 |
+
text=json.dumps(result, indent=2),
|
| 170 |
+
)
|
| 171 |
+
]
|
| 172 |
except Exception as e:
|
| 173 |
logger.error(f"Retrieval failed: {e}")
|
| 174 |
+
return [
|
| 175 |
+
TextContent(
|
| 176 |
+
type="text",
|
| 177 |
+
text=json.dumps({"error": str(e)}),
|
| 178 |
+
)
|
| 179 |
+
]
|
| 180 |
|
| 181 |
async def _retrieve_via_proxy(
|
| 182 |
self,
|
headroom/ccr/tool_injection.py
CHANGED
|
@@ -321,10 +321,12 @@ class CCRToolInjector:
|
|
| 321 |
else:
|
| 322 |
# Append instructions
|
| 323 |
if isinstance(content, str):
|
| 324 |
-
updated_messages.append(
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
| 328 |
else:
|
| 329 |
# Handle structured content
|
| 330 |
updated_messages.append(message)
|
|
@@ -333,10 +335,13 @@ class CCRToolInjector:
|
|
| 333 |
|
| 334 |
# If no system message, prepend one
|
| 335 |
if not system_found:
|
| 336 |
-
updated_messages.insert(
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
return updated_messages
|
| 342 |
|
|
|
|
| 321 |
else:
|
| 322 |
# Append instructions
|
| 323 |
if isinstance(content, str):
|
| 324 |
+
updated_messages.append(
|
| 325 |
+
{
|
| 326 |
+
**message,
|
| 327 |
+
"content": content + instructions,
|
| 328 |
+
}
|
| 329 |
+
)
|
| 330 |
else:
|
| 331 |
# Handle structured content
|
| 332 |
updated_messages.append(message)
|
|
|
|
| 335 |
|
| 336 |
# If no system message, prepend one
|
| 337 |
if not system_found:
|
| 338 |
+
updated_messages.insert(
|
| 339 |
+
0,
|
| 340 |
+
{
|
| 341 |
+
"role": "system",
|
| 342 |
+
"content": instructions.strip(),
|
| 343 |
+
},
|
| 344 |
+
)
|
| 345 |
|
| 346 |
return updated_messages
|
| 347 |
|
headroom/cli.py
CHANGED
|
@@ -30,6 +30,7 @@ def get_version() -> str:
|
|
| 30 |
"""Get the current version."""
|
| 31 |
try:
|
| 32 |
from headroom import __version__
|
|
|
|
| 33 |
return __version__
|
| 34 |
except ImportError:
|
| 35 |
return "unknown"
|
|
@@ -63,9 +64,9 @@ def cmd_proxy(args: argparse.Namespace) -> int:
|
|
| 63 |
Starting proxy server...
|
| 64 |
|
| 65 |
URL: http://{config.host}:{config.port}
|
| 66 |
-
Optimization: {
|
| 67 |
-
Caching: {
|
| 68 |
-
Rate Limit: {
|
| 69 |
|
| 70 |
Usage with Claude Code:
|
| 71 |
ANTHROPIC_BASE_URL=http://{config.host}:{config.port} claude
|
|
@@ -119,7 +120,8 @@ Documentation: https://github.com/headroom-sdk/headroom
|
|
| 119 |
)
|
| 120 |
|
| 121 |
parser.add_argument(
|
| 122 |
-
"--version",
|
|
|
|
| 123 |
action="store_true",
|
| 124 |
help="Show version and exit",
|
| 125 |
)
|
|
@@ -138,7 +140,8 @@ Documentation: https://github.com/headroom-sdk/headroom
|
|
| 138 |
help="Host to bind to (default: 127.0.0.1)",
|
| 139 |
)
|
| 140 |
proxy_parser.add_argument(
|
| 141 |
-
"--port",
|
|
|
|
| 142 |
type=int,
|
| 143 |
default=8787,
|
| 144 |
help="Port to bind to (default: 8787)",
|
|
|
|
| 30 |
"""Get the current version."""
|
| 31 |
try:
|
| 32 |
from headroom import __version__
|
| 33 |
+
|
| 34 |
return __version__
|
| 35 |
except ImportError:
|
| 36 |
return "unknown"
|
|
|
|
| 64 |
Starting proxy server...
|
| 65 |
|
| 66 |
URL: http://{config.host}:{config.port}
|
| 67 |
+
Optimization: {"ENABLED" if config.optimize else "DISABLED"}
|
| 68 |
+
Caching: {"ENABLED" if config.cache_enabled else "DISABLED"}
|
| 69 |
+
Rate Limit: {"ENABLED" if config.rate_limit_enabled else "DISABLED"}
|
| 70 |
|
| 71 |
Usage with Claude Code:
|
| 72 |
ANTHROPIC_BASE_URL=http://{config.host}:{config.port} claude
|
|
|
|
| 120 |
)
|
| 121 |
|
| 122 |
parser.add_argument(
|
| 123 |
+
"--version",
|
| 124 |
+
"-V",
|
| 125 |
action="store_true",
|
| 126 |
help="Show version and exit",
|
| 127 |
)
|
|
|
|
| 140 |
help="Host to bind to (default: 127.0.0.1)",
|
| 141 |
)
|
| 142 |
proxy_parser.add_argument(
|
| 143 |
+
"--port",
|
| 144 |
+
"-p",
|
| 145 |
type=int,
|
| 146 |
default=8787,
|
| 147 |
help="Port to bind to (default: 8787)",
|
headroom/client.py
CHANGED
|
@@ -8,8 +8,8 @@ from typing import Any
|
|
| 8 |
|
| 9 |
from .cache import (
|
| 10 |
BaseCacheOptimizer,
|
| 11 |
-
CacheOptimizerRegistry,
|
| 12 |
CacheConfig,
|
|
|
|
| 13 |
OptimizationContext,
|
| 14 |
SemanticCacheLayer,
|
| 15 |
)
|
|
@@ -19,12 +19,6 @@ from .config import (
|
|
| 19 |
RequestMetrics,
|
| 20 |
SimulationResult,
|
| 21 |
)
|
| 22 |
-
from .exceptions import (
|
| 23 |
-
ConfigurationError,
|
| 24 |
-
ProviderError,
|
| 25 |
-
StorageError,
|
| 26 |
-
ValidationError,
|
| 27 |
-
)
|
| 28 |
from .parser import parse_messages
|
| 29 |
from .providers.base import Provider
|
| 30 |
from .storage import create_storage
|
|
@@ -386,9 +380,7 @@ class HeadroomClient:
|
|
| 386 |
tokenizer = self._get_tokenizer(model)
|
| 387 |
|
| 388 |
# Analyze original messages
|
| 389 |
-
blocks, block_breakdown, waste_signals = parse_messages(
|
| 390 |
-
messages, tokenizer
|
| 391 |
-
)
|
| 392 |
tokens_before = tokenizer.count_messages(messages)
|
| 393 |
|
| 394 |
# Compute cache alignment score
|
|
@@ -410,7 +402,9 @@ class HeadroomClient:
|
|
| 410 |
|
| 411 |
# Apply transforms if in optimize mode
|
| 412 |
if mode == HeadroomMode.OPTIMIZE:
|
| 413 |
-
output_buffer =
|
|
|
|
|
|
|
| 414 |
model_limit = self._get_context_limit(model)
|
| 415 |
|
| 416 |
result = self._pipeline.apply(
|
|
@@ -443,7 +437,9 @@ class HeadroomClient:
|
|
| 443 |
cached_response = cache_result.cached_response
|
| 444 |
|
| 445 |
# Update metrics from cache result
|
| 446 |
-
cache_optimizer_used =
|
|
|
|
|
|
|
| 447 |
cache_optimizer_strategy = cache_result.metrics.strategy
|
| 448 |
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
| 449 |
breakpoints_inserted = cache_result.metrics.breakpoints_inserted
|
|
@@ -456,9 +452,7 @@ class HeadroomClient:
|
|
| 456 |
|
| 457 |
elif self._cache_optimizer is not None:
|
| 458 |
# Direct cache optimizer (no semantic layer)
|
| 459 |
-
cache_result = self._cache_optimizer.optimize(
|
| 460 |
-
optimized_messages, cache_context
|
| 461 |
-
)
|
| 462 |
cache_optimizer_used = self._cache_optimizer.name
|
| 463 |
cache_optimizer_strategy = self._cache_optimizer.strategy.value
|
| 464 |
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
|
@@ -625,8 +619,7 @@ class HeadroomClient:
|
|
| 625 |
) -> Iterator[Any]:
|
| 626 |
"""Wrap stream to pass through chunks and save metrics at end."""
|
| 627 |
try:
|
| 628 |
-
|
| 629 |
-
yield chunk
|
| 630 |
finally:
|
| 631 |
# Save metrics when stream completes
|
| 632 |
# Note: output tokens unknown for streams
|
|
@@ -666,9 +659,7 @@ class HeadroomClient:
|
|
| 666 |
# Extract response content for caching
|
| 667 |
response_data = self._extract_response_content(response)
|
| 668 |
if response_data:
|
| 669 |
-
self._semantic_cache_layer.store_response(
|
| 670 |
-
messages, response_data, cache_context
|
| 671 |
-
)
|
| 672 |
|
| 673 |
def _extract_response_content(self, response: Any) -> dict[str, Any] | None:
|
| 674 |
"""Extract cacheable content from API response."""
|
|
@@ -704,18 +695,18 @@ class HeadroomClient:
|
|
| 704 |
tokenizer = self._get_tokenizer(model)
|
| 705 |
|
| 706 |
# Analyze original
|
| 707 |
-
blocks, block_breakdown, waste_signals = parse_messages(
|
| 708 |
-
messages, tokenizer
|
| 709 |
-
)
|
| 710 |
tokens_before = tokenizer.count_messages(messages)
|
| 711 |
|
| 712 |
# Compute original cache alignment
|
| 713 |
aligner = CacheAligner(self._config.cache_aligner)
|
| 714 |
cache_alignment_score = aligner.get_alignment_score(messages)
|
| 715 |
-
|
| 716 |
|
| 717 |
# Apply transforms
|
| 718 |
-
output_buffer =
|
|
|
|
|
|
|
| 719 |
model_limit = self._get_context_limit(model)
|
| 720 |
|
| 721 |
result = self._pipeline.simulate(
|
|
@@ -946,9 +937,7 @@ class HeadroomClient:
|
|
| 946 |
"config": {
|
| 947 |
"mode": self._default_mode.value,
|
| 948 |
"provider": self._provider.name,
|
| 949 |
-
"cache_optimizer": (
|
| 950 |
-
self._cache_optimizer.name if self._cache_optimizer else None
|
| 951 |
-
),
|
| 952 |
"semantic_cache": self._semantic_cache_layer is not None,
|
| 953 |
},
|
| 954 |
"transforms": {
|
|
|
|
| 8 |
|
| 9 |
from .cache import (
|
| 10 |
BaseCacheOptimizer,
|
|
|
|
| 11 |
CacheConfig,
|
| 12 |
+
CacheOptimizerRegistry,
|
| 13 |
OptimizationContext,
|
| 14 |
SemanticCacheLayer,
|
| 15 |
)
|
|
|
|
| 19 |
RequestMetrics,
|
| 20 |
SimulationResult,
|
| 21 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from .parser import parse_messages
|
| 23 |
from .providers.base import Provider
|
| 24 |
from .storage import create_storage
|
|
|
|
| 380 |
tokenizer = self._get_tokenizer(model)
|
| 381 |
|
| 382 |
# Analyze original messages
|
| 383 |
+
blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer)
|
|
|
|
|
|
|
| 384 |
tokens_before = tokenizer.count_messages(messages)
|
| 385 |
|
| 386 |
# Compute cache alignment score
|
|
|
|
| 402 |
|
| 403 |
# Apply transforms if in optimize mode
|
| 404 |
if mode == HeadroomMode.OPTIMIZE:
|
| 405 |
+
output_buffer = (
|
| 406 |
+
headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens
|
| 407 |
+
)
|
| 408 |
model_limit = self._get_context_limit(model)
|
| 409 |
|
| 410 |
result = self._pipeline.apply(
|
|
|
|
| 437 |
cached_response = cache_result.cached_response
|
| 438 |
|
| 439 |
# Update metrics from cache result
|
| 440 |
+
cache_optimizer_used = (
|
| 441 |
+
cache_result.metrics.optimizer_name or self._cache_optimizer.name
|
| 442 |
+
)
|
| 443 |
cache_optimizer_strategy = cache_result.metrics.strategy
|
| 444 |
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
| 445 |
breakpoints_inserted = cache_result.metrics.breakpoints_inserted
|
|
|
|
| 452 |
|
| 453 |
elif self._cache_optimizer is not None:
|
| 454 |
# Direct cache optimizer (no semantic layer)
|
| 455 |
+
cache_result = self._cache_optimizer.optimize(optimized_messages, cache_context)
|
|
|
|
|
|
|
| 456 |
cache_optimizer_used = self._cache_optimizer.name
|
| 457 |
cache_optimizer_strategy = self._cache_optimizer.strategy.value
|
| 458 |
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
|
|
|
| 619 |
) -> Iterator[Any]:
|
| 620 |
"""Wrap stream to pass through chunks and save metrics at end."""
|
| 621 |
try:
|
| 622 |
+
yield from stream
|
|
|
|
| 623 |
finally:
|
| 624 |
# Save metrics when stream completes
|
| 625 |
# Note: output tokens unknown for streams
|
|
|
|
| 659 |
# Extract response content for caching
|
| 660 |
response_data = self._extract_response_content(response)
|
| 661 |
if response_data:
|
| 662 |
+
self._semantic_cache_layer.store_response(messages, response_data, cache_context)
|
|
|
|
|
|
|
| 663 |
|
| 664 |
def _extract_response_content(self, response: Any) -> dict[str, Any] | None:
|
| 665 |
"""Extract cacheable content from API response."""
|
|
|
|
| 695 |
tokenizer = self._get_tokenizer(model)
|
| 696 |
|
| 697 |
# Analyze original
|
| 698 |
+
blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer)
|
|
|
|
|
|
|
| 699 |
tokens_before = tokenizer.count_messages(messages)
|
| 700 |
|
| 701 |
# Compute original cache alignment
|
| 702 |
aligner = CacheAligner(self._config.cache_aligner)
|
| 703 |
cache_alignment_score = aligner.get_alignment_score(messages)
|
| 704 |
+
compute_prefix_hash(messages)
|
| 705 |
|
| 706 |
# Apply transforms
|
| 707 |
+
output_buffer = (
|
| 708 |
+
headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens
|
| 709 |
+
)
|
| 710 |
model_limit = self._get_context_limit(model)
|
| 711 |
|
| 712 |
result = self._pipeline.simulate(
|
|
|
|
| 937 |
"config": {
|
| 938 |
"mode": self._default_mode.value,
|
| 939 |
"provider": self._provider.name,
|
| 940 |
+
"cache_optimizer": (self._cache_optimizer.name if self._cache_optimizer else None),
|
|
|
|
|
|
|
| 941 |
"semantic_cache": self._semantic_cache_layer is not None,
|
| 942 |
},
|
| 943 |
"transforms": {
|
headroom/config.py
CHANGED
|
@@ -266,7 +266,9 @@ class CCRConfig:
|
|
| 266 |
|
| 267 |
# Retrieval marker format
|
| 268 |
# Inserted at end of compressed content to tell LLM how to get more
|
| 269 |
-
marker_template: str =
|
|
|
|
|
|
|
| 270 |
|
| 271 |
|
| 272 |
@dataclass
|
|
|
|
| 266 |
|
| 267 |
# Retrieval marker format
|
| 268 |
# Inserted at end of compressed content to tell LLM how to get more
|
| 269 |
+
marker_template: str = (
|
| 270 |
+
"\n[{original_count} items compressed to {compressed_count}. Retrieve more: hash={hash}]"
|
| 271 |
+
)
|
| 272 |
|
| 273 |
|
| 274 |
@dataclass
|
headroom/exceptions.py
CHANGED
|
@@ -60,6 +60,7 @@ class ConfigurationError(HeadroomError):
|
|
| 60 |
details={"valid_modes": ["audit", "optimize"]}
|
| 61 |
)
|
| 62 |
"""
|
|
|
|
| 63 |
pass
|
| 64 |
|
| 65 |
|
|
@@ -77,6 +78,7 @@ class ProviderError(HeadroomError):
|
|
| 77 |
details={"provider": "foo", "known_providers": ["openai", "anthropic"]}
|
| 78 |
)
|
| 79 |
"""
|
|
|
|
| 80 |
pass
|
| 81 |
|
| 82 |
|
|
@@ -94,6 +96,7 @@ class StorageError(HeadroomError):
|
|
| 94 |
details={"url": "sqlite:///foo.db", "error": "Permission denied"}
|
| 95 |
)
|
| 96 |
"""
|
|
|
|
| 97 |
pass
|
| 98 |
|
| 99 |
|
|
@@ -111,6 +114,7 @@ class CompressionError(HeadroomError):
|
|
| 111 |
details={"tool_name": "search_api", "content_preview": "..."}
|
| 112 |
)
|
| 113 |
"""
|
|
|
|
| 114 |
pass
|
| 115 |
|
| 116 |
|
|
@@ -128,6 +132,7 @@ class TokenizationError(HeadroomError):
|
|
| 128 |
details={"model": "gpt-99", "fallback_used": True}
|
| 129 |
)
|
| 130 |
"""
|
|
|
|
| 131 |
pass
|
| 132 |
|
| 133 |
|
|
@@ -145,6 +150,7 @@ class CacheError(HeadroomError):
|
|
| 145 |
details={"hash": "abc123", "ttl": 300}
|
| 146 |
)
|
| 147 |
"""
|
|
|
|
| 148 |
pass
|
| 149 |
|
| 150 |
|
|
@@ -164,6 +170,7 @@ class ValidationError(HeadroomError):
|
|
| 164 |
}
|
| 165 |
)
|
| 166 |
"""
|
|
|
|
| 167 |
pass
|
| 168 |
|
| 169 |
|
|
@@ -181,4 +188,5 @@ class TransformError(HeadroomError):
|
|
| 181 |
details={"transform": "smart_crusher", "reason": "..."}
|
| 182 |
)
|
| 183 |
"""
|
|
|
|
| 184 |
pass
|
|
|
|
| 60 |
details={"valid_modes": ["audit", "optimize"]}
|
| 61 |
)
|
| 62 |
"""
|
| 63 |
+
|
| 64 |
pass
|
| 65 |
|
| 66 |
|
|
|
|
| 78 |
details={"provider": "foo", "known_providers": ["openai", "anthropic"]}
|
| 79 |
)
|
| 80 |
"""
|
| 81 |
+
|
| 82 |
pass
|
| 83 |
|
| 84 |
|
|
|
|
| 96 |
details={"url": "sqlite:///foo.db", "error": "Permission denied"}
|
| 97 |
)
|
| 98 |
"""
|
| 99 |
+
|
| 100 |
pass
|
| 101 |
|
| 102 |
|
|
|
|
| 114 |
details={"tool_name": "search_api", "content_preview": "..."}
|
| 115 |
)
|
| 116 |
"""
|
| 117 |
+
|
| 118 |
pass
|
| 119 |
|
| 120 |
|
|
|
|
| 132 |
details={"model": "gpt-99", "fallback_used": True}
|
| 133 |
)
|
| 134 |
"""
|
| 135 |
+
|
| 136 |
pass
|
| 137 |
|
| 138 |
|
|
|
|
| 150 |
details={"hash": "abc123", "ttl": 300}
|
| 151 |
)
|
| 152 |
"""
|
| 153 |
+
|
| 154 |
pass
|
| 155 |
|
| 156 |
|
|
|
|
| 170 |
}
|
| 171 |
)
|
| 172 |
"""
|
| 173 |
+
|
| 174 |
pass
|
| 175 |
|
| 176 |
|
|
|
|
| 188 |
details={"transform": "smart_crusher", "reason": "..."}
|
| 189 |
)
|
| 190 |
"""
|
| 191 |
+
|
| 192 |
pass
|
headroom/integrations/langchain.py
CHANGED
|
@@ -142,6 +142,7 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 142 |
|
| 143 |
class Config:
|
| 144 |
"""Pydantic config for LangChain compatibility."""
|
|
|
|
| 145 |
arbitrary_types_allowed = True
|
| 146 |
|
| 147 |
def __init__(
|
|
@@ -206,9 +207,7 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 206 |
"""History of optimization metrics."""
|
| 207 |
return self._metrics_history.copy()
|
| 208 |
|
| 209 |
-
def _convert_messages_to_openai(
|
| 210 |
-
self, messages: list[BaseMessage]
|
| 211 |
-
) -> list[dict[str, Any]]:
|
| 212 |
"""Convert LangChain messages to OpenAI format for Headroom."""
|
| 213 |
result = []
|
| 214 |
for msg in messages:
|
|
@@ -232,22 +231,24 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 232 |
]
|
| 233 |
result.append(entry)
|
| 234 |
elif isinstance(msg, ToolMessage):
|
| 235 |
-
result.append(
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
| 240 |
else:
|
| 241 |
# Generic fallback
|
| 242 |
-
result.append(
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
| 246 |
return result
|
| 247 |
|
| 248 |
-
def _convert_messages_from_openai(
|
| 249 |
-
self, messages: list[dict[str, Any]]
|
| 250 |
-
) -> list[BaseMessage]:
|
| 251 |
"""Convert OpenAI format messages back to LangChain format."""
|
| 252 |
result = []
|
| 253 |
for msg in messages:
|
|
@@ -262,17 +263,21 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 262 |
tool_calls = []
|
| 263 |
if "tool_calls" in msg:
|
| 264 |
for tc in msg["tool_calls"]:
|
| 265 |
-
tool_calls.append(
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
| 270 |
result.append(AIMessage(content=content, tool_calls=tool_calls))
|
| 271 |
elif role == "tool":
|
| 272 |
-
result.append(
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
| 276 |
return result
|
| 277 |
|
| 278 |
def _optimize_messages(
|
|
@@ -308,7 +313,8 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 308 |
tokens_saved=result.tokens_before - result.tokens_after,
|
| 309 |
savings_percent=(
|
| 310 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 311 |
-
if result.tokens_before > 0
|
|
|
|
| 312 |
),
|
| 313 |
transforms_applied=result.transforms_applied,
|
| 314 |
model=model,
|
|
@@ -400,9 +406,8 @@ class HeadroomChatModel(BaseChatModel):
|
|
| 400 |
return {
|
| 401 |
"total_requests": len(self._metrics_history),
|
| 402 |
"total_tokens_saved": self._total_tokens_saved,
|
| 403 |
-
"average_savings_percent": sum(
|
| 404 |
-
|
| 405 |
-
) / len(self._metrics_history),
|
| 406 |
"total_tokens_before": sum(m.tokens_before for m in self._metrics_history),
|
| 407 |
"total_tokens_after": sum(m.tokens_after for m in self._metrics_history),
|
| 408 |
}
|
|
@@ -530,7 +535,7 @@ class HeadroomCallbackHandler(BaseCallbackHandler):
|
|
| 530 |
if self.log_level in ("DEBUG", "INFO"):
|
| 531 |
logger.log(
|
| 532 |
logging.DEBUG if self.log_level == "DEBUG" else logging.INFO,
|
| 533 |
-
f"Chat model request: ~{estimated_tokens} input tokens"
|
| 534 |
)
|
| 535 |
|
| 536 |
def on_llm_end(self, response: Any, **kwargs) -> None:
|
|
@@ -565,7 +570,7 @@ class HeadroomCallbackHandler(BaseCallbackHandler):
|
|
| 565 |
duration = f"{self._current_request['duration_ms']:.0f}ms"
|
| 566 |
logger.log(
|
| 567 |
logging.DEBUG if self.log_level == "DEBUG" else logging.INFO,
|
| 568 |
-
f"LLM request completed: {tokens_info} in {duration}"
|
| 569 |
)
|
| 570 |
|
| 571 |
self._current_request = None
|
|
@@ -602,7 +607,8 @@ class HeadroomCallbackHandler(BaseCallbackHandler):
|
|
| 602 |
"average_tokens": total_tokens / len(successful) if successful else 0,
|
| 603 |
"average_duration_ms": (
|
| 604 |
sum(r.get("duration_ms", 0) for r in successful) / len(successful)
|
| 605 |
-
if successful
|
|
|
|
| 606 |
),
|
| 607 |
"errors": len(self._requests) - len(successful),
|
| 608 |
"alerts": len(self._alerts),
|
|
@@ -670,11 +676,13 @@ class HeadroomRunnable:
|
|
| 670 |
def __or__(self, other):
|
| 671 |
"""Support pipe operator for LCEL composition."""
|
| 672 |
from langchain_core.runnables import RunnableSequence
|
|
|
|
| 673 |
return RunnableSequence(first=self.as_runnable(), last=other)
|
| 674 |
|
| 675 |
def __ror__(self, other):
|
| 676 |
"""Support reverse pipe operator."""
|
| 677 |
from langchain_core.runnables import RunnableSequence
|
|
|
|
| 678 |
return RunnableSequence(first=other, last=self.as_runnable())
|
| 679 |
|
| 680 |
def as_runnable(self):
|
|
@@ -704,16 +712,20 @@ class HeadroomRunnable:
|
|
| 704 |
elif isinstance(msg, AIMessage):
|
| 705 |
openai_messages.append({"role": "assistant", "content": msg.content})
|
| 706 |
elif isinstance(msg, ToolMessage):
|
| 707 |
-
openai_messages.append(
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
|
|
|
|
|
|
| 712 |
elif hasattr(msg, "type") and hasattr(msg, "content"):
|
| 713 |
-
openai_messages.append(
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
|
|
|
|
|
|
| 717 |
|
| 718 |
# Get model context limit
|
| 719 |
model = "gpt-4o" # Default model for estimation
|
|
@@ -735,7 +747,8 @@ class HeadroomRunnable:
|
|
| 735 |
tokens_saved=result.tokens_before - result.tokens_after,
|
| 736 |
savings_percent=(
|
| 737 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 738 |
-
if result.tokens_before > 0
|
|
|
|
| 739 |
),
|
| 740 |
transforms_applied=result.transforms_applied,
|
| 741 |
model="gpt-4o",
|
|
@@ -755,10 +768,12 @@ class HeadroomRunnable:
|
|
| 755 |
elif role == "assistant":
|
| 756 |
output_messages.append(AIMessage(content=content))
|
| 757 |
elif role == "tool":
|
| 758 |
-
output_messages.append(
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
|
|
|
|
|
|
| 762 |
|
| 763 |
return output_messages
|
| 764 |
|
|
@@ -823,11 +838,13 @@ def optimize_messages(
|
|
| 823 |
]
|
| 824 |
openai_messages.append(entry)
|
| 825 |
elif isinstance(msg, ToolMessage):
|
| 826 |
-
openai_messages.append(
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
|
|
|
|
|
|
| 831 |
|
| 832 |
# Get model context limit
|
| 833 |
model_limit = provider.get_context_limit(model)
|
|
@@ -853,17 +870,21 @@ def optimize_messages(
|
|
| 853 |
tool_calls = []
|
| 854 |
if "tool_calls" in msg:
|
| 855 |
for tc in msg["tool_calls"]:
|
| 856 |
-
tool_calls.append(
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
|
|
|
|
|
|
| 861 |
output_messages.append(AIMessage(content=content, tool_calls=tool_calls))
|
| 862 |
elif role == "tool":
|
| 863 |
-
output_messages.append(
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
|
|
|
|
|
|
| 867 |
|
| 868 |
metrics = {
|
| 869 |
"tokens_before": result.tokens_before,
|
|
@@ -871,7 +892,8 @@ def optimize_messages(
|
|
| 871 |
"tokens_saved": result.tokens_before - result.tokens_after,
|
| 872 |
"savings_percent": (
|
| 873 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 874 |
-
if result.tokens_before > 0
|
|
|
|
| 875 |
),
|
| 876 |
"transforms_applied": result.transforms_applied,
|
| 877 |
}
|
|
|
|
| 142 |
|
| 143 |
class Config:
|
| 144 |
"""Pydantic config for LangChain compatibility."""
|
| 145 |
+
|
| 146 |
arbitrary_types_allowed = True
|
| 147 |
|
| 148 |
def __init__(
|
|
|
|
| 207 |
"""History of optimization metrics."""
|
| 208 |
return self._metrics_history.copy()
|
| 209 |
|
| 210 |
+
def _convert_messages_to_openai(self, messages: list[BaseMessage]) -> list[dict[str, Any]]:
|
|
|
|
|
|
|
| 211 |
"""Convert LangChain messages to OpenAI format for Headroom."""
|
| 212 |
result = []
|
| 213 |
for msg in messages:
|
|
|
|
| 231 |
]
|
| 232 |
result.append(entry)
|
| 233 |
elif isinstance(msg, ToolMessage):
|
| 234 |
+
result.append(
|
| 235 |
+
{
|
| 236 |
+
"role": "tool",
|
| 237 |
+
"tool_call_id": msg.tool_call_id,
|
| 238 |
+
"content": msg.content,
|
| 239 |
+
}
|
| 240 |
+
)
|
| 241 |
else:
|
| 242 |
# Generic fallback
|
| 243 |
+
result.append(
|
| 244 |
+
{
|
| 245 |
+
"role": getattr(msg, "type", "user"),
|
| 246 |
+
"content": msg.content,
|
| 247 |
+
}
|
| 248 |
+
)
|
| 249 |
return result
|
| 250 |
|
| 251 |
+
def _convert_messages_from_openai(self, messages: list[dict[str, Any]]) -> list[BaseMessage]:
|
|
|
|
|
|
|
| 252 |
"""Convert OpenAI format messages back to LangChain format."""
|
| 253 |
result = []
|
| 254 |
for msg in messages:
|
|
|
|
| 263 |
tool_calls = []
|
| 264 |
if "tool_calls" in msg:
|
| 265 |
for tc in msg["tool_calls"]:
|
| 266 |
+
tool_calls.append(
|
| 267 |
+
{
|
| 268 |
+
"id": tc["id"],
|
| 269 |
+
"name": tc["function"]["name"],
|
| 270 |
+
"args": json.loads(tc["function"]["arguments"]),
|
| 271 |
+
}
|
| 272 |
+
)
|
| 273 |
result.append(AIMessage(content=content, tool_calls=tool_calls))
|
| 274 |
elif role == "tool":
|
| 275 |
+
result.append(
|
| 276 |
+
ToolMessage(
|
| 277 |
+
content=content,
|
| 278 |
+
tool_call_id=msg.get("tool_call_id", ""),
|
| 279 |
+
)
|
| 280 |
+
)
|
| 281 |
return result
|
| 282 |
|
| 283 |
def _optimize_messages(
|
|
|
|
| 313 |
tokens_saved=result.tokens_before - result.tokens_after,
|
| 314 |
savings_percent=(
|
| 315 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 316 |
+
if result.tokens_before > 0
|
| 317 |
+
else 0
|
| 318 |
),
|
| 319 |
transforms_applied=result.transforms_applied,
|
| 320 |
model=model,
|
|
|
|
| 406 |
return {
|
| 407 |
"total_requests": len(self._metrics_history),
|
| 408 |
"total_tokens_saved": self._total_tokens_saved,
|
| 409 |
+
"average_savings_percent": sum(m.savings_percent for m in self._metrics_history)
|
| 410 |
+
/ len(self._metrics_history),
|
|
|
|
| 411 |
"total_tokens_before": sum(m.tokens_before for m in self._metrics_history),
|
| 412 |
"total_tokens_after": sum(m.tokens_after for m in self._metrics_history),
|
| 413 |
}
|
|
|
|
| 535 |
if self.log_level in ("DEBUG", "INFO"):
|
| 536 |
logger.log(
|
| 537 |
logging.DEBUG if self.log_level == "DEBUG" else logging.INFO,
|
| 538 |
+
f"Chat model request: ~{estimated_tokens} input tokens",
|
| 539 |
)
|
| 540 |
|
| 541 |
def on_llm_end(self, response: Any, **kwargs) -> None:
|
|
|
|
| 570 |
duration = f"{self._current_request['duration_ms']:.0f}ms"
|
| 571 |
logger.log(
|
| 572 |
logging.DEBUG if self.log_level == "DEBUG" else logging.INFO,
|
| 573 |
+
f"LLM request completed: {tokens_info} in {duration}",
|
| 574 |
)
|
| 575 |
|
| 576 |
self._current_request = None
|
|
|
|
| 607 |
"average_tokens": total_tokens / len(successful) if successful else 0,
|
| 608 |
"average_duration_ms": (
|
| 609 |
sum(r.get("duration_ms", 0) for r in successful) / len(successful)
|
| 610 |
+
if successful
|
| 611 |
+
else 0
|
| 612 |
),
|
| 613 |
"errors": len(self._requests) - len(successful),
|
| 614 |
"alerts": len(self._alerts),
|
|
|
|
| 676 |
def __or__(self, other):
|
| 677 |
"""Support pipe operator for LCEL composition."""
|
| 678 |
from langchain_core.runnables import RunnableSequence
|
| 679 |
+
|
| 680 |
return RunnableSequence(first=self.as_runnable(), last=other)
|
| 681 |
|
| 682 |
def __ror__(self, other):
|
| 683 |
"""Support reverse pipe operator."""
|
| 684 |
from langchain_core.runnables import RunnableSequence
|
| 685 |
+
|
| 686 |
return RunnableSequence(first=other, last=self.as_runnable())
|
| 687 |
|
| 688 |
def as_runnable(self):
|
|
|
|
| 712 |
elif isinstance(msg, AIMessage):
|
| 713 |
openai_messages.append({"role": "assistant", "content": msg.content})
|
| 714 |
elif isinstance(msg, ToolMessage):
|
| 715 |
+
openai_messages.append(
|
| 716 |
+
{
|
| 717 |
+
"role": "tool",
|
| 718 |
+
"tool_call_id": msg.tool_call_id,
|
| 719 |
+
"content": msg.content,
|
| 720 |
+
}
|
| 721 |
+
)
|
| 722 |
elif hasattr(msg, "type") and hasattr(msg, "content"):
|
| 723 |
+
openai_messages.append(
|
| 724 |
+
{
|
| 725 |
+
"role": msg.type,
|
| 726 |
+
"content": msg.content,
|
| 727 |
+
}
|
| 728 |
+
)
|
| 729 |
|
| 730 |
# Get model context limit
|
| 731 |
model = "gpt-4o" # Default model for estimation
|
|
|
|
| 747 |
tokens_saved=result.tokens_before - result.tokens_after,
|
| 748 |
savings_percent=(
|
| 749 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 750 |
+
if result.tokens_before > 0
|
| 751 |
+
else 0
|
| 752 |
),
|
| 753 |
transforms_applied=result.transforms_applied,
|
| 754 |
model="gpt-4o",
|
|
|
|
| 768 |
elif role == "assistant":
|
| 769 |
output_messages.append(AIMessage(content=content))
|
| 770 |
elif role == "tool":
|
| 771 |
+
output_messages.append(
|
| 772 |
+
ToolMessage(
|
| 773 |
+
content=content,
|
| 774 |
+
tool_call_id=msg.get("tool_call_id", ""),
|
| 775 |
+
)
|
| 776 |
+
)
|
| 777 |
|
| 778 |
return output_messages
|
| 779 |
|
|
|
|
| 838 |
]
|
| 839 |
openai_messages.append(entry)
|
| 840 |
elif isinstance(msg, ToolMessage):
|
| 841 |
+
openai_messages.append(
|
| 842 |
+
{
|
| 843 |
+
"role": "tool",
|
| 844 |
+
"tool_call_id": msg.tool_call_id,
|
| 845 |
+
"content": msg.content,
|
| 846 |
+
}
|
| 847 |
+
)
|
| 848 |
|
| 849 |
# Get model context limit
|
| 850 |
model_limit = provider.get_context_limit(model)
|
|
|
|
| 870 |
tool_calls = []
|
| 871 |
if "tool_calls" in msg:
|
| 872 |
for tc in msg["tool_calls"]:
|
| 873 |
+
tool_calls.append(
|
| 874 |
+
{
|
| 875 |
+
"id": tc["id"],
|
| 876 |
+
"name": tc["function"]["name"],
|
| 877 |
+
"args": json.loads(tc["function"]["arguments"]),
|
| 878 |
+
}
|
| 879 |
+
)
|
| 880 |
output_messages.append(AIMessage(content=content, tool_calls=tool_calls))
|
| 881 |
elif role == "tool":
|
| 882 |
+
output_messages.append(
|
| 883 |
+
ToolMessage(
|
| 884 |
+
content=content,
|
| 885 |
+
tool_call_id=msg.get("tool_call_id", ""),
|
| 886 |
+
)
|
| 887 |
+
)
|
| 888 |
|
| 889 |
metrics = {
|
| 890 |
"tokens_before": result.tokens_before,
|
|
|
|
| 892 |
"tokens_saved": result.tokens_before - result.tokens_after,
|
| 893 |
"savings_percent": (
|
| 894 |
(result.tokens_before - result.tokens_after) / result.tokens_before * 100
|
| 895 |
+
if result.tokens_before > 0
|
| 896 |
+
else 0
|
| 897 |
),
|
| 898 |
"transforms_applied": result.transforms_applied,
|
| 899 |
}
|
headroom/integrations/mcp.py
CHANGED
|
@@ -222,7 +222,7 @@ class HeadroomMCPCompressor:
|
|
| 222 |
|
| 223 |
# Try to parse as JSON
|
| 224 |
try:
|
| 225 |
-
|
| 226 |
except json.JSONDecodeError:
|
| 227 |
# Not JSON, return as-is
|
| 228 |
return MCPCompressionResult(
|
|
@@ -260,7 +260,12 @@ class HeadroomMCPCompressor:
|
|
| 260 |
{
|
| 261 |
"role": "assistant",
|
| 262 |
"content": None,
|
| 263 |
-
"tool_calls": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
},
|
| 265 |
{"role": "tool", "content": content, "tool_call_id": "call_1"},
|
| 266 |
]
|
|
@@ -287,7 +292,7 @@ class HeadroomMCPCompressor:
|
|
| 287 |
compressed_content = result.messages[-1]["content"]
|
| 288 |
|
| 289 |
# Remove any Headroom markers for clean output
|
| 290 |
-
compressed_content = re.sub(r
|
| 291 |
|
| 292 |
# Count items and errors
|
| 293 |
try:
|
|
@@ -295,13 +300,13 @@ class HeadroomMCPCompressor:
|
|
| 295 |
compressed_data = json.loads(compressed_content)
|
| 296 |
|
| 297 |
# Find the array in original
|
| 298 |
-
for
|
| 299 |
if isinstance(value, list):
|
| 300 |
items_before = len(value)
|
| 301 |
break
|
| 302 |
|
| 303 |
# Find the array in compressed
|
| 304 |
-
for
|
| 305 |
if isinstance(value, list):
|
| 306 |
items_after = len(value)
|
| 307 |
# Count errors preserved
|
|
@@ -521,7 +526,7 @@ def create_headroom_mcp_proxy(
|
|
| 521 |
```
|
| 522 |
"""
|
| 523 |
return {
|
| 524 |
-
"upstream_servers":
|
| 525 |
"compressor": HeadroomMCPCompressor(config=config),
|
| 526 |
"config": config or HeadroomConfig(),
|
| 527 |
}
|
|
|
|
| 222 |
|
| 223 |
# Try to parse as JSON
|
| 224 |
try:
|
| 225 |
+
json.loads(content)
|
| 226 |
except json.JSONDecodeError:
|
| 227 |
# Not JSON, return as-is
|
| 228 |
return MCPCompressionResult(
|
|
|
|
| 260 |
{
|
| 261 |
"role": "assistant",
|
| 262 |
"content": None,
|
| 263 |
+
"tool_calls": [
|
| 264 |
+
{
|
| 265 |
+
"id": "call_1",
|
| 266 |
+
"function": {"name": tool_name, "arguments": json.dumps(tool_args or {})},
|
| 267 |
+
}
|
| 268 |
+
],
|
| 269 |
},
|
| 270 |
{"role": "tool", "content": content, "tool_call_id": "call_1"},
|
| 271 |
]
|
|
|
|
| 292 |
compressed_content = result.messages[-1]["content"]
|
| 293 |
|
| 294 |
# Remove any Headroom markers for clean output
|
| 295 |
+
compressed_content = re.sub(r"\n<headroom:[^>]+>", "", compressed_content)
|
| 296 |
|
| 297 |
# Count items and errors
|
| 298 |
try:
|
|
|
|
| 300 |
compressed_data = json.loads(compressed_content)
|
| 301 |
|
| 302 |
# Find the array in original
|
| 303 |
+
for _key, value in original_data.items():
|
| 304 |
if isinstance(value, list):
|
| 305 |
items_before = len(value)
|
| 306 |
break
|
| 307 |
|
| 308 |
# Find the array in compressed
|
| 309 |
+
for _key, value in compressed_data.items():
|
| 310 |
if isinstance(value, list):
|
| 311 |
items_after = len(value)
|
| 312 |
# Count errors preserved
|
|
|
|
| 526 |
```
|
| 527 |
"""
|
| 528 |
return {
|
| 529 |
+
"upstream_servers": dict(upstream_servers),
|
| 530 |
"compressor": HeadroomMCPCompressor(config=config),
|
| 531 |
"config": config or HeadroomConfig(),
|
| 532 |
}
|
headroom/models/registry.py
CHANGED
|
@@ -649,7 +649,7 @@ class ModelRegistry:
|
|
| 649 |
Returns:
|
| 650 |
List of provider names.
|
| 651 |
"""
|
| 652 |
-
return list(
|
| 653 |
|
| 654 |
@classmethod
|
| 655 |
def get_context_limit(cls, model: str, default: int = 128000) -> int:
|
|
|
|
| 649 |
Returns:
|
| 650 |
List of provider names.
|
| 651 |
"""
|
| 652 |
+
return list({info.provider for info in _MODELS.values()})
|
| 653 |
|
| 654 |
@classmethod
|
| 655 |
def get_context_limit(cls, model: str, default: int = 128000) -> int:
|
headroom/pricing/registry.py
CHANGED
|
@@ -10,6 +10,7 @@ class ModelPricing:
|
|
| 10 |
|
| 11 |
All prices are in USD per 1 million tokens.
|
| 12 |
"""
|
|
|
|
| 13 |
model: str
|
| 14 |
provider: str
|
| 15 |
input_per_1m: float
|
|
@@ -24,6 +25,7 @@ class ModelPricing:
|
|
| 24 |
@dataclass
|
| 25 |
class CostEstimate:
|
| 26 |
"""Result of a cost estimation calculation."""
|
|
|
|
| 27 |
cost_usd: float
|
| 28 |
breakdown: dict = field(default_factory=dict)
|
| 29 |
pricing_date: date | None = None
|
|
|
|
| 10 |
|
| 11 |
All prices are in USD per 1 million tokens.
|
| 12 |
"""
|
| 13 |
+
|
| 14 |
model: str
|
| 15 |
provider: str
|
| 16 |
input_per_1m: float
|
|
|
|
| 25 |
@dataclass
|
| 26 |
class CostEstimate:
|
| 27 |
"""Result of a cost estimation calculation."""
|
| 28 |
+
|
| 29 |
cost_usd: float
|
| 30 |
breakdown: dict = field(default_factory=dict)
|
| 31 |
pricing_date: date | None = None
|
headroom/providers/anthropic.py
CHANGED
|
@@ -87,13 +87,14 @@ class AnthropicTokenCounter(TokenCounter):
|
|
| 87 |
"For accurate counting, pass an Anthropic client: "
|
| 88 |
"AnthropicProvider(client=Anthropic())",
|
| 89 |
UserWarning,
|
| 90 |
-
stacklevel=4
|
| 91 |
)
|
| 92 |
_FALLBACK_WARNING_SHOWN = True
|
| 93 |
|
| 94 |
# Load tiktoken as fallback
|
| 95 |
try:
|
| 96 |
import tiktoken
|
|
|
|
| 97 |
self._encoding = tiktoken.get_encoding("cl100k_base")
|
| 98 |
except ImportError:
|
| 99 |
if not self._use_api:
|
|
@@ -101,7 +102,7 @@ class AnthropicTokenCounter(TokenCounter):
|
|
| 101 |
"tiktoken not installed - token counting will be very approximate. "
|
| 102 |
"Install tiktoken or provide an Anthropic client.",
|
| 103 |
UserWarning,
|
| 104 |
-
stacklevel=4
|
| 105 |
)
|
| 106 |
|
| 107 |
def count_text(self, text: str) -> int:
|
|
@@ -184,11 +185,13 @@ class AnthropicTokenCounter(TokenCounter):
|
|
| 184 |
# Tool results in OpenAI format
|
| 185 |
return {
|
| 186 |
"role": "user",
|
| 187 |
-
"content": [
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
return {"role": role, "content": message.get("content", "")}
|
|
@@ -232,9 +235,7 @@ class AnthropicTokenCounter(TokenCounter):
|
|
| 232 |
except Exception as e:
|
| 233 |
# Fall back to estimation on API error
|
| 234 |
warnings.warn(
|
| 235 |
-
f"Token Count API failed ({e}), using estimation",
|
| 236 |
-
UserWarning,
|
| 237 |
-
stacklevel=3
|
| 238 |
)
|
| 239 |
return self._count_messages_estimated(messages)
|
| 240 |
|
|
@@ -318,8 +319,7 @@ class AnthropicProvider(Provider):
|
|
| 318 |
return True
|
| 319 |
# Check prefix matches
|
| 320 |
return any(
|
| 321 |
-
model.startswith(prefix)
|
| 322 |
-
for prefix in ["claude-3", "claude-2", "claude-instant"]
|
| 323 |
)
|
| 324 |
|
| 325 |
def estimate_cost(
|
|
|
|
| 87 |
"For accurate counting, pass an Anthropic client: "
|
| 88 |
"AnthropicProvider(client=Anthropic())",
|
| 89 |
UserWarning,
|
| 90 |
+
stacklevel=4,
|
| 91 |
)
|
| 92 |
_FALLBACK_WARNING_SHOWN = True
|
| 93 |
|
| 94 |
# Load tiktoken as fallback
|
| 95 |
try:
|
| 96 |
import tiktoken
|
| 97 |
+
|
| 98 |
self._encoding = tiktoken.get_encoding("cl100k_base")
|
| 99 |
except ImportError:
|
| 100 |
if not self._use_api:
|
|
|
|
| 102 |
"tiktoken not installed - token counting will be very approximate. "
|
| 103 |
"Install tiktoken or provide an Anthropic client.",
|
| 104 |
UserWarning,
|
| 105 |
+
stacklevel=4,
|
| 106 |
)
|
| 107 |
|
| 108 |
def count_text(self, text: str) -> int:
|
|
|
|
| 185 |
# Tool results in OpenAI format
|
| 186 |
return {
|
| 187 |
"role": "user",
|
| 188 |
+
"content": [
|
| 189 |
+
{
|
| 190 |
+
"type": "tool_result",
|
| 191 |
+
"tool_use_id": message.get("tool_call_id", ""),
|
| 192 |
+
"content": message.get("content", ""),
|
| 193 |
+
}
|
| 194 |
+
],
|
| 195 |
}
|
| 196 |
|
| 197 |
return {"role": role, "content": message.get("content", "")}
|
|
|
|
| 235 |
except Exception as e:
|
| 236 |
# Fall back to estimation on API error
|
| 237 |
warnings.warn(
|
| 238 |
+
f"Token Count API failed ({e}), using estimation", UserWarning, stacklevel=3
|
|
|
|
|
|
|
| 239 |
)
|
| 240 |
return self._count_messages_estimated(messages)
|
| 241 |
|
|
|
|
| 319 |
return True
|
| 320 |
# Check prefix matches
|
| 321 |
return any(
|
| 322 |
+
model.startswith(prefix) for prefix in ["claude-3", "claude-2", "claude-instant"]
|
|
|
|
| 323 |
)
|
| 324 |
|
| 325 |
def estimate_cost(
|