headroom-ai 0.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- headroom/__init__.py +212 -0
- headroom/cache/__init__.py +76 -0
- headroom/cache/anthropic.py +517 -0
- headroom/cache/base.py +342 -0
- headroom/cache/compression_feedback.py +613 -0
- headroom/cache/compression_store.py +814 -0
- headroom/cache/dynamic_detector.py +1026 -0
- headroom/cache/google.py +884 -0
- headroom/cache/openai.py +584 -0
- headroom/cache/registry.py +175 -0
- headroom/cache/semantic.py +451 -0
- headroom/ccr/__init__.py +77 -0
- headroom/ccr/context_tracker.py +582 -0
- headroom/ccr/mcp_server.py +319 -0
- headroom/ccr/response_handler.py +772 -0
- headroom/ccr/tool_injection.py +415 -0
- headroom/cli.py +219 -0
- headroom/client.py +977 -0
- headroom/compression/__init__.py +42 -0
- headroom/compression/detector.py +424 -0
- headroom/compression/handlers/__init__.py +22 -0
- headroom/compression/handlers/base.py +219 -0
- headroom/compression/handlers/code_handler.py +506 -0
- headroom/compression/handlers/json_handler.py +418 -0
- headroom/compression/masks.py +345 -0
- headroom/compression/universal.py +465 -0
- headroom/config.py +474 -0
- headroom/exceptions.py +192 -0
- headroom/integrations/__init__.py +159 -0
- headroom/integrations/agno/__init__.py +53 -0
- headroom/integrations/agno/hooks.py +345 -0
- headroom/integrations/agno/model.py +625 -0
- headroom/integrations/agno/providers.py +154 -0
- headroom/integrations/langchain/__init__.py +106 -0
- headroom/integrations/langchain/agents.py +326 -0
- headroom/integrations/langchain/chat_model.py +1002 -0
- headroom/integrations/langchain/langsmith.py +324 -0
- headroom/integrations/langchain/memory.py +319 -0
- headroom/integrations/langchain/providers.py +200 -0
- headroom/integrations/langchain/retriever.py +371 -0
- headroom/integrations/langchain/streaming.py +341 -0
- headroom/integrations/mcp/__init__.py +37 -0
- headroom/integrations/mcp/server.py +533 -0
- headroom/memory/__init__.py +37 -0
- headroom/memory/extractor.py +390 -0
- headroom/memory/fast_store.py +621 -0
- headroom/memory/fast_wrapper.py +311 -0
- headroom/memory/inline_extractor.py +229 -0
- headroom/memory/store.py +434 -0
- headroom/memory/worker.py +260 -0
- headroom/memory/wrapper.py +321 -0
- headroom/models/__init__.py +39 -0
- headroom/models/registry.py +687 -0
- headroom/parser.py +293 -0
- headroom/pricing/__init__.py +51 -0
- headroom/pricing/anthropic_prices.py +81 -0
- headroom/pricing/litellm_pricing.py +113 -0
- headroom/pricing/openai_prices.py +91 -0
- headroom/pricing/registry.py +188 -0
- headroom/providers/__init__.py +61 -0
- headroom/providers/anthropic.py +621 -0
- headroom/providers/base.py +131 -0
- headroom/providers/cohere.py +362 -0
- headroom/providers/google.py +427 -0
- headroom/providers/litellm.py +297 -0
- headroom/providers/openai.py +566 -0
- headroom/providers/openai_compatible.py +521 -0
- headroom/proxy/__init__.py +19 -0
- headroom/proxy/server.py +2683 -0
- headroom/py.typed +0 -0
- headroom/relevance/__init__.py +124 -0
- headroom/relevance/base.py +106 -0
- headroom/relevance/bm25.py +255 -0
- headroom/relevance/embedding.py +255 -0
- headroom/relevance/hybrid.py +259 -0
- headroom/reporting/__init__.py +5 -0
- headroom/reporting/generator.py +549 -0
- headroom/storage/__init__.py +41 -0
- headroom/storage/base.py +125 -0
- headroom/storage/jsonl.py +220 -0
- headroom/storage/sqlite.py +289 -0
- headroom/telemetry/__init__.py +91 -0
- headroom/telemetry/collector.py +764 -0
- headroom/telemetry/models.py +880 -0
- headroom/telemetry/toin.py +1579 -0
- headroom/tokenizer.py +80 -0
- headroom/tokenizers/__init__.py +75 -0
- headroom/tokenizers/base.py +210 -0
- headroom/tokenizers/estimator.py +198 -0
- headroom/tokenizers/huggingface.py +317 -0
- headroom/tokenizers/mistral.py +245 -0
- headroom/tokenizers/registry.py +398 -0
- headroom/tokenizers/tiktoken_counter.py +248 -0
- headroom/transforms/__init__.py +106 -0
- headroom/transforms/base.py +57 -0
- headroom/transforms/cache_aligner.py +357 -0
- headroom/transforms/code_compressor.py +1313 -0
- headroom/transforms/content_detector.py +335 -0
- headroom/transforms/content_router.py +1158 -0
- headroom/transforms/llmlingua_compressor.py +638 -0
- headroom/transforms/log_compressor.py +529 -0
- headroom/transforms/pipeline.py +297 -0
- headroom/transforms/rolling_window.py +350 -0
- headroom/transforms/search_compressor.py +365 -0
- headroom/transforms/smart_crusher.py +2682 -0
- headroom/transforms/text_compressor.py +259 -0
- headroom/transforms/tool_crusher.py +338 -0
- headroom/utils.py +215 -0
- headroom_ai-0.2.13.dist-info/METADATA +315 -0
- headroom_ai-0.2.13.dist-info/RECORD +114 -0
- headroom_ai-0.2.13.dist-info/WHEEL +4 -0
- headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
- headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
- headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
|
@@ -0,0 +1,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
|