headroom-ai 0.2.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. headroom/__init__.py +212 -0
  2. headroom/cache/__init__.py +76 -0
  3. headroom/cache/anthropic.py +517 -0
  4. headroom/cache/base.py +342 -0
  5. headroom/cache/compression_feedback.py +613 -0
  6. headroom/cache/compression_store.py +814 -0
  7. headroom/cache/dynamic_detector.py +1026 -0
  8. headroom/cache/google.py +884 -0
  9. headroom/cache/openai.py +584 -0
  10. headroom/cache/registry.py +175 -0
  11. headroom/cache/semantic.py +451 -0
  12. headroom/ccr/__init__.py +77 -0
  13. headroom/ccr/context_tracker.py +582 -0
  14. headroom/ccr/mcp_server.py +319 -0
  15. headroom/ccr/response_handler.py +772 -0
  16. headroom/ccr/tool_injection.py +415 -0
  17. headroom/cli.py +219 -0
  18. headroom/client.py +977 -0
  19. headroom/compression/__init__.py +42 -0
  20. headroom/compression/detector.py +424 -0
  21. headroom/compression/handlers/__init__.py +22 -0
  22. headroom/compression/handlers/base.py +219 -0
  23. headroom/compression/handlers/code_handler.py +506 -0
  24. headroom/compression/handlers/json_handler.py +418 -0
  25. headroom/compression/masks.py +345 -0
  26. headroom/compression/universal.py +465 -0
  27. headroom/config.py +474 -0
  28. headroom/exceptions.py +192 -0
  29. headroom/integrations/__init__.py +159 -0
  30. headroom/integrations/agno/__init__.py +53 -0
  31. headroom/integrations/agno/hooks.py +345 -0
  32. headroom/integrations/agno/model.py +625 -0
  33. headroom/integrations/agno/providers.py +154 -0
  34. headroom/integrations/langchain/__init__.py +106 -0
  35. headroom/integrations/langchain/agents.py +326 -0
  36. headroom/integrations/langchain/chat_model.py +1002 -0
  37. headroom/integrations/langchain/langsmith.py +324 -0
  38. headroom/integrations/langchain/memory.py +319 -0
  39. headroom/integrations/langchain/providers.py +200 -0
  40. headroom/integrations/langchain/retriever.py +371 -0
  41. headroom/integrations/langchain/streaming.py +341 -0
  42. headroom/integrations/mcp/__init__.py +37 -0
  43. headroom/integrations/mcp/server.py +533 -0
  44. headroom/memory/__init__.py +37 -0
  45. headroom/memory/extractor.py +390 -0
  46. headroom/memory/fast_store.py +621 -0
  47. headroom/memory/fast_wrapper.py +311 -0
  48. headroom/memory/inline_extractor.py +229 -0
  49. headroom/memory/store.py +434 -0
  50. headroom/memory/worker.py +260 -0
  51. headroom/memory/wrapper.py +321 -0
  52. headroom/models/__init__.py +39 -0
  53. headroom/models/registry.py +687 -0
  54. headroom/parser.py +293 -0
  55. headroom/pricing/__init__.py +51 -0
  56. headroom/pricing/anthropic_prices.py +81 -0
  57. headroom/pricing/litellm_pricing.py +113 -0
  58. headroom/pricing/openai_prices.py +91 -0
  59. headroom/pricing/registry.py +188 -0
  60. headroom/providers/__init__.py +61 -0
  61. headroom/providers/anthropic.py +621 -0
  62. headroom/providers/base.py +131 -0
  63. headroom/providers/cohere.py +362 -0
  64. headroom/providers/google.py +427 -0
  65. headroom/providers/litellm.py +297 -0
  66. headroom/providers/openai.py +566 -0
  67. headroom/providers/openai_compatible.py +521 -0
  68. headroom/proxy/__init__.py +19 -0
  69. headroom/proxy/server.py +2683 -0
  70. headroom/py.typed +0 -0
  71. headroom/relevance/__init__.py +124 -0
  72. headroom/relevance/base.py +106 -0
  73. headroom/relevance/bm25.py +255 -0
  74. headroom/relevance/embedding.py +255 -0
  75. headroom/relevance/hybrid.py +259 -0
  76. headroom/reporting/__init__.py +5 -0
  77. headroom/reporting/generator.py +549 -0
  78. headroom/storage/__init__.py +41 -0
  79. headroom/storage/base.py +125 -0
  80. headroom/storage/jsonl.py +220 -0
  81. headroom/storage/sqlite.py +289 -0
  82. headroom/telemetry/__init__.py +91 -0
  83. headroom/telemetry/collector.py +764 -0
  84. headroom/telemetry/models.py +880 -0
  85. headroom/telemetry/toin.py +1579 -0
  86. headroom/tokenizer.py +80 -0
  87. headroom/tokenizers/__init__.py +75 -0
  88. headroom/tokenizers/base.py +210 -0
  89. headroom/tokenizers/estimator.py +198 -0
  90. headroom/tokenizers/huggingface.py +317 -0
  91. headroom/tokenizers/mistral.py +245 -0
  92. headroom/tokenizers/registry.py +398 -0
  93. headroom/tokenizers/tiktoken_counter.py +248 -0
  94. headroom/transforms/__init__.py +106 -0
  95. headroom/transforms/base.py +57 -0
  96. headroom/transforms/cache_aligner.py +357 -0
  97. headroom/transforms/code_compressor.py +1313 -0
  98. headroom/transforms/content_detector.py +335 -0
  99. headroom/transforms/content_router.py +1158 -0
  100. headroom/transforms/llmlingua_compressor.py +638 -0
  101. headroom/transforms/log_compressor.py +529 -0
  102. headroom/transforms/pipeline.py +297 -0
  103. headroom/transforms/rolling_window.py +350 -0
  104. headroom/transforms/search_compressor.py +365 -0
  105. headroom/transforms/smart_crusher.py +2682 -0
  106. headroom/transforms/text_compressor.py +259 -0
  107. headroom/transforms/tool_crusher.py +338 -0
  108. headroom/utils.py +215 -0
  109. headroom_ai-0.2.13.dist-info/METADATA +315 -0
  110. headroom_ai-0.2.13.dist-info/RECORD +114 -0
  111. headroom_ai-0.2.13.dist-info/WHEEL +4 -0
  112. headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
  113. headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
  114. headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
@@ -0,0 +1,154 @@
1
+ """Provider detection for Agno models.
2
+
3
+ Automatically detects the correct Headroom provider based on the Agno model type.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ from headroom.providers import (
12
+ AnthropicProvider,
13
+ CohereProvider,
14
+ GoogleProvider,
15
+ OpenAIProvider,
16
+ )
17
+ from headroom.providers.base import Provider
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Mapping from Agno model class names to Headroom providers
22
+ _AGNO_MODEL_PROVIDERS: dict[str, type[Provider]] = {
23
+ # OpenAI models
24
+ "OpenAIChat": OpenAIProvider,
25
+ "OpenAILike": OpenAIProvider,
26
+ # Anthropic models (direct and cloud variants)
27
+ "Claude": AnthropicProvider,
28
+ "Anthropic": AnthropicProvider,
29
+ "AwsBedrock": AnthropicProvider, # Bedrock Claude models
30
+ "BedrockClaude": AnthropicProvider,
31
+ # Google models
32
+ "Gemini": GoogleProvider,
33
+ "GoogleGenerativeAI": GoogleProvider,
34
+ "VertexAI": GoogleProvider,
35
+ # LiteLLM (uses OpenAI-compatible interface, provider detected from model ID)
36
+ "LiteLLM": OpenAIProvider,
37
+ "LiteLLMChat": OpenAIProvider,
38
+ # Others default to OpenAI-compatible tokenization
39
+ "Groq": OpenAIProvider,
40
+ "Mistral": OpenAIProvider,
41
+ "MistralChat": OpenAIProvider,
42
+ "Together": OpenAIProvider,
43
+ "TogetherChat": OpenAIProvider,
44
+ "Fireworks": OpenAIProvider,
45
+ "FireworksChat": OpenAIProvider,
46
+ "Ollama": OpenAIProvider,
47
+ "OllamaChat": OpenAIProvider,
48
+ "DeepSeek": OpenAIProvider,
49
+ "DeepSeekChat": OpenAIProvider,
50
+ "xAI": OpenAIProvider,
51
+ "XAI": OpenAIProvider,
52
+ "Grok": OpenAIProvider,
53
+ "Cohere": CohereProvider,
54
+ "CohereChat": CohereProvider,
55
+ "Perplexity": OpenAIProvider,
56
+ "Anyscale": OpenAIProvider,
57
+ "OpenRouter": OpenAIProvider,
58
+ "Replicate": OpenAIProvider,
59
+ "HuggingFace": OpenAIProvider,
60
+ "HuggingFaceChat": OpenAIProvider,
61
+ }
62
+
63
+
64
+ def get_headroom_provider(agno_model: Any) -> Provider:
65
+ """Get the appropriate Headroom provider for an Agno model.
66
+
67
+ Detection strategy:
68
+ 1. Check model class name against known Agno model types
69
+ 2. Check for provider hints in model attributes
70
+ 3. Fall back to OpenAI provider (most compatible)
71
+
72
+ Args:
73
+ agno_model: An Agno model instance (OpenAIChat, Claude, etc.)
74
+
75
+ Returns:
76
+ Appropriate Headroom Provider instance.
77
+
78
+ Example:
79
+ from agno.models.openai import OpenAIChat
80
+ from headroom.integrations.agno.providers import get_headroom_provider
81
+
82
+ model = OpenAIChat(id="gpt-4o")
83
+ provider = get_headroom_provider(model) # Returns OpenAIProvider
84
+ """
85
+ # Strategy 1: Class name matching
86
+ class_name = agno_model.__class__.__name__
87
+ if class_name in _AGNO_MODEL_PROVIDERS:
88
+ provider_class = _AGNO_MODEL_PROVIDERS[class_name]
89
+ logger.debug(f"Detected provider {provider_class.__name__} from class {class_name}")
90
+ return provider_class()
91
+
92
+ # Strategy 2: Check module path
93
+ module_path = agno_model.__class__.__module__
94
+ if "anthropic" in module_path.lower():
95
+ logger.debug(f"Detected AnthropicProvider from module {module_path}")
96
+ return AnthropicProvider()
97
+ elif "google" in module_path.lower() or "gemini" in module_path.lower():
98
+ logger.debug(f"Detected GoogleProvider from module {module_path}")
99
+ return GoogleProvider()
100
+ elif "cohere" in module_path.lower():
101
+ logger.debug(f"Detected CohereProvider from module {module_path}")
102
+ return CohereProvider()
103
+ elif "openai" in module_path.lower() or "litellm" in module_path.lower():
104
+ logger.debug(f"Detected OpenAIProvider from module {module_path}")
105
+ return OpenAIProvider()
106
+
107
+ # Strategy 3: Check model ID/name for hints
108
+ model_id = getattr(agno_model, "id", "") or getattr(agno_model, "model", "")
109
+ if isinstance(model_id, str) and model_id:
110
+ model_id_lower = model_id.lower()
111
+ if "claude" in model_id_lower:
112
+ logger.debug(f"Detected AnthropicProvider from model ID {model_id}")
113
+ return AnthropicProvider()
114
+ elif "gemini" in model_id_lower:
115
+ logger.debug(f"Detected GoogleProvider from model ID {model_id}")
116
+ return GoogleProvider()
117
+ elif "gpt" in model_id_lower or "o1" in model_id_lower or "o3" in model_id_lower:
118
+ logger.debug(f"Detected OpenAIProvider from model ID {model_id}")
119
+ return OpenAIProvider()
120
+ elif "command" in model_id_lower or "cohere" in model_id_lower:
121
+ logger.debug(f"Detected CohereProvider from model ID {model_id}")
122
+ return CohereProvider()
123
+
124
+ # Strategy 4: Default fallback
125
+ logger.warning(
126
+ f"Unknown Agno model class '{class_name}', defaulting to OpenAIProvider. "
127
+ "Token counting may be inaccurate."
128
+ )
129
+ return OpenAIProvider()
130
+
131
+
132
+ def get_model_name_from_agno(agno_model: Any) -> str:
133
+ """Extract the model name/ID from an Agno model.
134
+
135
+ Args:
136
+ agno_model: An Agno model instance
137
+
138
+ Returns:
139
+ Model name string (e.g., "gpt-4o", "claude-3-5-sonnet-20241022")
140
+ """
141
+ # Try common attribute names
142
+ for attr in ["id", "model", "model_name", "model_id"]:
143
+ value = getattr(agno_model, attr, None)
144
+ if value and isinstance(value, str):
145
+ return str(value)
146
+
147
+ # Fallback with warning
148
+ class_name = agno_model.__class__.__name__
149
+ logger.warning(
150
+ f"Could not extract model name from {class_name} (no 'id', 'model', "
151
+ f"'model_name', or 'model_id' attribute). Defaulting to 'gpt-4o'. "
152
+ "Token counting may be inaccurate."
153
+ )
154
+ return "gpt-4o"
@@ -0,0 +1,106 @@
1
+ """LangChain integration for Headroom.
2
+
3
+ This package provides seamless integration with LangChain, including:
4
+ - HeadroomChatModel: Drop-in wrapper for any LangChain chat model
5
+ - HeadroomChatMessageHistory: Automatic conversation compression
6
+ - HeadroomDocumentCompressor: Relevance-based document filtering
7
+ - HeadroomToolWrapper: Tool output compression for agents
8
+ - StreamingMetricsTracker: Token counting during streaming
9
+ - HeadroomLangSmithCallbackHandler: LangSmith trace enrichment
10
+
11
+ Example:
12
+ from langchain_openai import ChatOpenAI
13
+ from headroom.integrations.langchain import HeadroomChatModel
14
+
15
+ # Wrap any LangChain model
16
+ llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
17
+
18
+ # Use like normal - optimization happens automatically
19
+ response = llm.invoke("Hello!")
20
+
21
+ Install: pip install headroom[langchain]
22
+ """
23
+
24
+ # Core chat model wrapper
25
+ # Agent tool wrapping
26
+ from .agents import (
27
+ HeadroomToolWrapper,
28
+ ToolCompressionMetrics,
29
+ ToolMetricsCollector,
30
+ get_tool_metrics,
31
+ reset_tool_metrics,
32
+ wrap_tools_with_headroom,
33
+ )
34
+ from .chat_model import (
35
+ HeadroomCallbackHandler,
36
+ HeadroomChatModel,
37
+ HeadroomRunnable,
38
+ OptimizationMetrics,
39
+ langchain_available,
40
+ optimize_messages,
41
+ )
42
+
43
+ # LangSmith integration
44
+ from .langsmith import (
45
+ HeadroomLangSmithCallbackHandler,
46
+ is_langsmith_available,
47
+ is_langsmith_tracing_enabled,
48
+ )
49
+
50
+ # Memory integration
51
+ from .memory import HeadroomChatMessageHistory
52
+
53
+ # Provider auto-detection
54
+ from .providers import (
55
+ detect_provider,
56
+ get_headroom_provider,
57
+ get_model_name_from_langchain,
58
+ )
59
+
60
+ # Retriever integration
61
+ from .retriever import CompressionMetrics, HeadroomDocumentCompressor
62
+
63
+ # Streaming metrics
64
+ from .streaming import (
65
+ StreamingMetrics,
66
+ StreamingMetricsCallback,
67
+ StreamingMetricsTracker,
68
+ track_async_streaming_response,
69
+ track_streaming_response,
70
+ )
71
+
72
+ __all__ = [
73
+ # Core
74
+ "HeadroomChatModel",
75
+ "HeadroomCallbackHandler",
76
+ "HeadroomRunnable",
77
+ "OptimizationMetrics",
78
+ "optimize_messages",
79
+ "langchain_available",
80
+ # Provider Detection
81
+ "detect_provider",
82
+ "get_headroom_provider",
83
+ "get_model_name_from_langchain",
84
+ # Memory
85
+ "HeadroomChatMessageHistory",
86
+ # Retrievers
87
+ "HeadroomDocumentCompressor",
88
+ "CompressionMetrics",
89
+ # Agents
90
+ "HeadroomToolWrapper",
91
+ "ToolCompressionMetrics",
92
+ "ToolMetricsCollector",
93
+ "wrap_tools_with_headroom",
94
+ "get_tool_metrics",
95
+ "reset_tool_metrics",
96
+ # LangSmith
97
+ "HeadroomLangSmithCallbackHandler",
98
+ "is_langsmith_available",
99
+ "is_langsmith_tracing_enabled",
100
+ # Streaming
101
+ "StreamingMetricsTracker",
102
+ "StreamingMetricsCallback",
103
+ "StreamingMetrics",
104
+ "track_streaming_response",
105
+ "track_async_streaming_response",
106
+ ]
@@ -0,0 +1,326 @@
1
+ """Agent tool integration for LangChain with output compression.
2
+
3
+ This module provides HeadroomToolWrapper and wrap_tools_with_headroom
4
+ for wrapping LangChain tools to automatically compress their outputs
5
+ and track per-tool compression metrics.
6
+
7
+ Example:
8
+ from langchain.agents import create_openai_tools_agent
9
+ from langchain.tools import Tool
10
+ from headroom.integrations import wrap_tools_with_headroom
11
+
12
+ # Define tools
13
+ tools = [
14
+ Tool(name="search", func=search_func, description="Search"),
15
+ Tool(name="database", func=db_func, description="Query DB"),
16
+ ]
17
+
18
+ # Wrap with Headroom compression
19
+ wrapped_tools = wrap_tools_with_headroom(tools)
20
+
21
+ # Use in agent - outputs are automatically compressed
22
+ agent = create_openai_tools_agent(llm, wrapped_tools, prompt)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from dataclasses import dataclass, field
29
+ from datetime import datetime
30
+ from typing import Any
31
+
32
+ # LangChain imports - these are optional dependencies
33
+ try:
34
+ from langchain_core.tools import BaseTool, StructuredTool, Tool
35
+
36
+ LANGCHAIN_AVAILABLE = True
37
+ except ImportError:
38
+ LANGCHAIN_AVAILABLE = False
39
+ BaseTool = object # type: ignore[misc,assignment]
40
+ StructuredTool = object # type: ignore[misc,assignment]
41
+ Tool = object # type: ignore[misc,assignment]
42
+
43
+ from headroom.integrations.mcp import compress_tool_result
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ def _check_langchain_available() -> None:
49
+ """Raise ImportError if LangChain is not installed."""
50
+ if not LANGCHAIN_AVAILABLE:
51
+ raise ImportError(
52
+ "LangChain is required for this integration. "
53
+ "Install with: pip install headroom[langchain] "
54
+ "or: pip install langchain-core"
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class ToolCompressionMetrics:
60
+ """Metrics from a single tool compression."""
61
+
62
+ tool_name: str
63
+ timestamp: datetime
64
+ chars_before: int
65
+ chars_after: int
66
+ chars_saved: int
67
+ compression_ratio: float
68
+ was_compressed: bool
69
+
70
+
71
+ @dataclass
72
+ class ToolMetricsCollector:
73
+ """Collects compression metrics across all tool invocations."""
74
+
75
+ metrics: list[ToolCompressionMetrics] = field(default_factory=list)
76
+
77
+ def add(self, metric: ToolCompressionMetrics) -> None:
78
+ """Add a metric entry."""
79
+ self.metrics.append(metric)
80
+ # Keep only last 1000
81
+ if len(self.metrics) > 1000:
82
+ self.metrics = self.metrics[-1000:]
83
+
84
+ def get_summary(self) -> dict[str, Any]:
85
+ """Get summary statistics."""
86
+ if not self.metrics:
87
+ return {
88
+ "total_invocations": 0,
89
+ "total_compressions": 0,
90
+ "total_chars_saved": 0,
91
+ }
92
+
93
+ compressed = [m for m in self.metrics if m.was_compressed]
94
+ return {
95
+ "total_invocations": len(self.metrics),
96
+ "total_compressions": len(compressed),
97
+ "total_chars_saved": sum(m.chars_saved for m in self.metrics),
98
+ "average_compression_ratio": (
99
+ sum(m.compression_ratio for m in compressed) / len(compressed) if compressed else 0
100
+ ),
101
+ "by_tool": self._get_by_tool_stats(),
102
+ }
103
+
104
+ def _get_by_tool_stats(self) -> dict[str, dict[str, Any]]:
105
+ """Get per-tool statistics."""
106
+ by_tool: dict[str, list[ToolCompressionMetrics]] = {}
107
+ for m in self.metrics:
108
+ if m.tool_name not in by_tool:
109
+ by_tool[m.tool_name] = []
110
+ by_tool[m.tool_name].append(m)
111
+
112
+ result = {}
113
+ for name, tool_metrics in by_tool.items():
114
+ compressed = [m for m in tool_metrics if m.was_compressed]
115
+ result[name] = {
116
+ "invocations": len(tool_metrics),
117
+ "compressions": len(compressed),
118
+ "chars_saved": sum(m.chars_saved for m in tool_metrics),
119
+ }
120
+ return result
121
+
122
+
123
+ # Global metrics collector
124
+ _global_metrics = ToolMetricsCollector()
125
+
126
+
127
+ def get_tool_metrics() -> ToolMetricsCollector:
128
+ """Get the global tool metrics collector."""
129
+ return _global_metrics
130
+
131
+
132
+ def reset_tool_metrics() -> None:
133
+ """Reset global tool metrics."""
134
+ global _global_metrics
135
+ _global_metrics = ToolMetricsCollector()
136
+
137
+
138
+ class HeadroomToolWrapper:
139
+ """Wraps a LangChain tool to compress its output.
140
+
141
+ Applies SmartCrusher compression to tool outputs, particularly
142
+ useful for tools that return large JSON arrays (search results,
143
+ database queries, etc.).
144
+
145
+ Example:
146
+ from langchain.tools import Tool
147
+ from headroom.integrations import HeadroomToolWrapper
148
+
149
+ def search(query: str) -> str:
150
+ # Returns large JSON with 1000 results
151
+ return json.dumps({"results": [...1000 items...]})
152
+
153
+ search_tool = Tool(name="search", func=search, description="Search")
154
+ wrapped = HeadroomToolWrapper(search_tool)
155
+
156
+ # Use wrapped tool - output automatically compressed
157
+ result = wrapped("python tutorials")
158
+
159
+ Attributes:
160
+ tool: The wrapped LangChain tool
161
+ min_chars_to_compress: Minimum output size to trigger compression
162
+ metrics_collector: Collector for compression metrics
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ tool: BaseTool,
168
+ min_chars_to_compress: int = 1000,
169
+ metrics_collector: ToolMetricsCollector | None = None,
170
+ ):
171
+ """Initialize HeadroomToolWrapper.
172
+
173
+ Args:
174
+ tool: The LangChain BaseTool to wrap.
175
+ min_chars_to_compress: Minimum character count for output
176
+ before compression is applied. Default 1000.
177
+ metrics_collector: Collector for metrics. Uses global
178
+ collector if not specified.
179
+ """
180
+ _check_langchain_available()
181
+
182
+ self.tool = tool
183
+ self.min_chars_to_compress = min_chars_to_compress
184
+ self._metrics = metrics_collector or _global_metrics
185
+
186
+ # Copy tool metadata
187
+ self.name = tool.name
188
+ self.description = tool.description
189
+
190
+ def __call__(self, *args: Any, **kwargs: Any) -> str:
191
+ """Invoke the tool and compress output.
192
+
193
+ Args:
194
+ *args: Arguments to pass to the tool.
195
+ **kwargs: Keyword arguments to pass to the tool.
196
+
197
+ Returns:
198
+ Compressed tool output as string.
199
+ """
200
+ # Invoke underlying tool
201
+ result = self.tool.invoke(*args, **kwargs)
202
+
203
+ # Convert to string if needed
204
+ if not isinstance(result, str):
205
+ result = str(result)
206
+
207
+ # Check if compression is needed
208
+ if len(result) < self.min_chars_to_compress:
209
+ self._record_metrics(result, result, was_compressed=False)
210
+ return result
211
+
212
+ # Try to compress
213
+ compressed = self._compress_output(result)
214
+ self._record_metrics(result, compressed, was_compressed=True)
215
+
216
+ return compressed
217
+
218
+ def invoke(self, *args: Any, **kwargs: Any) -> str:
219
+ """Invoke the tool (alias for __call__)."""
220
+ return self(*args, **kwargs)
221
+
222
+ def _compress_output(self, output: str) -> str:
223
+ """Apply compression to tool output.
224
+
225
+ Args:
226
+ output: Tool output string.
227
+
228
+ Returns:
229
+ Compressed output.
230
+ """
231
+ try:
232
+ return compress_tool_result(
233
+ content=output,
234
+ tool_name=self.name,
235
+ )
236
+ except Exception as e:
237
+ logger.debug(f"Tool compression failed: {e}")
238
+ return output
239
+
240
+ def _record_metrics(self, original: str, compressed: str, was_compressed: bool) -> None:
241
+ """Record compression metrics.
242
+
243
+ Args:
244
+ original: Original output.
245
+ compressed: Compressed output.
246
+ was_compressed: Whether compression was applied.
247
+ """
248
+ chars_before = len(original)
249
+ chars_after = len(compressed)
250
+ chars_saved = chars_before - chars_after
251
+
252
+ metric = ToolCompressionMetrics(
253
+ tool_name=self.name,
254
+ timestamp=datetime.now(),
255
+ chars_before=chars_before,
256
+ chars_after=chars_after,
257
+ chars_saved=max(0, chars_saved),
258
+ compression_ratio=chars_after / chars_before if chars_before > 0 else 1.0,
259
+ was_compressed=was_compressed and chars_saved > 0,
260
+ )
261
+
262
+ self._metrics.add(metric)
263
+
264
+ if was_compressed and chars_saved > 0:
265
+ logger.info(
266
+ f"HeadroomToolWrapper[{self.name}]: {chars_before} -> {chars_after} chars "
267
+ f"({chars_saved} saved, {metric.compression_ratio:.1%} of original)"
268
+ )
269
+
270
+ def as_langchain_tool(self) -> StructuredTool:
271
+ """Convert wrapper back to a LangChain tool.
272
+
273
+ Useful when you need to pass the wrapped tool to APIs
274
+ that expect a LangChain tool type.
275
+
276
+ Returns:
277
+ StructuredTool that wraps this wrapper.
278
+ """
279
+ return StructuredTool.from_function(
280
+ func=self.__call__,
281
+ name=self.name,
282
+ description=self.description,
283
+ )
284
+
285
+
286
+ def wrap_tools_with_headroom(
287
+ tools: list[BaseTool],
288
+ min_chars_to_compress: int = 1000,
289
+ metrics_collector: ToolMetricsCollector | None = None,
290
+ ) -> list[StructuredTool]:
291
+ """Wrap multiple LangChain tools with Headroom compression.
292
+
293
+ Convenience function to wrap all tools in a list at once.
294
+
295
+ Args:
296
+ tools: List of LangChain tools to wrap.
297
+ min_chars_to_compress: Minimum output size for compression.
298
+ metrics_collector: Shared metrics collector for all tools.
299
+
300
+ Returns:
301
+ List of wrapped tools as StructuredTools.
302
+
303
+ Example:
304
+ from langchain.tools import Tool
305
+ from headroom.integrations import wrap_tools_with_headroom
306
+
307
+ tools = [search_tool, database_tool, api_tool]
308
+ wrapped = wrap_tools_with_headroom(tools)
309
+
310
+ # Use wrapped tools in agent
311
+ agent = create_openai_tools_agent(llm, wrapped, prompt)
312
+ """
313
+ _check_langchain_available()
314
+
315
+ collector = metrics_collector or _global_metrics
316
+
317
+ wrapped = []
318
+ for tool in tools:
319
+ wrapper = HeadroomToolWrapper(
320
+ tool=tool,
321
+ min_chars_to_compress=min_chars_to_compress,
322
+ metrics_collector=collector,
323
+ )
324
+ wrapped.append(wrapper.as_langchain_tool())
325
+
326
+ return wrapped