chopratejas--headroom
0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
327 行
10 KiB
Python
327 行
10 KiB
Python
"""Agent tool integration for LangChain with output compression.
|
|
|
|
This module provides HeadroomToolWrapper and wrap_tools_with_headroom
|
|
for wrapping LangChain tools to automatically compress their outputs
|
|
and track per-tool compression metrics.
|
|
|
|
Example:
|
|
from langchain.agents import create_openai_tools_agent
|
|
from langchain.tools import Tool
|
|
from headroom.integrations import wrap_tools_with_headroom
|
|
|
|
# Define tools
|
|
tools = [
|
|
Tool(name="search", func=search_func, description="Search"),
|
|
Tool(name="database", func=db_func, description="Query DB"),
|
|
]
|
|
|
|
# Wrap with Headroom compression
|
|
wrapped_tools = wrap_tools_with_headroom(tools)
|
|
|
|
# Use in agent - outputs are automatically compressed
|
|
agent = create_openai_tools_agent(llm, wrapped_tools, prompt)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
# LangChain imports - these are optional dependencies
|
|
try:
|
|
from langchain_core.tools import BaseTool, StructuredTool, Tool
|
|
|
|
LANGCHAIN_AVAILABLE = True
|
|
except ImportError:
|
|
LANGCHAIN_AVAILABLE = False
|
|
BaseTool = object # type: ignore[misc,assignment]
|
|
StructuredTool = object # type: ignore[misc,assignment]
|
|
Tool = object # type: ignore[misc,assignment]
|
|
|
|
from headroom.integrations.mcp import compress_tool_result
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _check_langchain_available() -> None:
|
|
"""Raise ImportError if LangChain is not installed."""
|
|
if not LANGCHAIN_AVAILABLE:
|
|
raise ImportError(
|
|
"LangChain is required for this integration. "
|
|
"Install with: pip install headroom[langchain] "
|
|
"or: pip install langchain-core"
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ToolCompressionMetrics:
|
|
"""Metrics from a single tool compression."""
|
|
|
|
tool_name: str
|
|
timestamp: datetime
|
|
chars_before: int
|
|
chars_after: int
|
|
chars_saved: int
|
|
compression_ratio: float
|
|
was_compressed: bool
|
|
|
|
|
|
@dataclass
|
|
class ToolMetricsCollector:
|
|
"""Collects compression metrics across all tool invocations."""
|
|
|
|
metrics: list[ToolCompressionMetrics] = field(default_factory=list)
|
|
|
|
def add(self, metric: ToolCompressionMetrics) -> None:
|
|
"""Add a metric entry."""
|
|
self.metrics.append(metric)
|
|
# Keep only last 1000
|
|
if len(self.metrics) > 1000:
|
|
self.metrics = self.metrics[-1000:]
|
|
|
|
def get_summary(self) -> dict[str, Any]:
|
|
"""Get summary statistics."""
|
|
if not self.metrics:
|
|
return {
|
|
"total_invocations": 0,
|
|
"total_compressions": 0,
|
|
"total_chars_saved": 0,
|
|
}
|
|
|
|
compressed = [m for m in self.metrics if m.was_compressed]
|
|
return {
|
|
"total_invocations": len(self.metrics),
|
|
"total_compressions": len(compressed),
|
|
"total_chars_saved": sum(m.chars_saved for m in self.metrics),
|
|
"average_compression_ratio": (
|
|
sum(m.compression_ratio for m in compressed) / len(compressed) if compressed else 0
|
|
),
|
|
"by_tool": self._get_by_tool_stats(),
|
|
}
|
|
|
|
def _get_by_tool_stats(self) -> dict[str, dict[str, Any]]:
|
|
"""Get per-tool statistics."""
|
|
by_tool: dict[str, list[ToolCompressionMetrics]] = {}
|
|
for m in self.metrics:
|
|
if m.tool_name not in by_tool:
|
|
by_tool[m.tool_name] = []
|
|
by_tool[m.tool_name].append(m)
|
|
|
|
result = {}
|
|
for name, tool_metrics in by_tool.items():
|
|
compressed = [m for m in tool_metrics if m.was_compressed]
|
|
result[name] = {
|
|
"invocations": len(tool_metrics),
|
|
"compressions": len(compressed),
|
|
"chars_saved": sum(m.chars_saved for m in tool_metrics),
|
|
}
|
|
return result
|
|
|
|
|
|
# Global metrics collector
|
|
_global_metrics = ToolMetricsCollector()
|
|
|
|
|
|
def get_tool_metrics() -> ToolMetricsCollector:
|
|
"""Get the global tool metrics collector."""
|
|
return _global_metrics
|
|
|
|
|
|
def reset_tool_metrics() -> None:
|
|
"""Reset global tool metrics."""
|
|
global _global_metrics
|
|
_global_metrics = ToolMetricsCollector()
|
|
|
|
|
|
class HeadroomToolWrapper:
|
|
"""Wraps a LangChain tool to compress its output.
|
|
|
|
Applies SmartCrusher compression to tool outputs, particularly
|
|
useful for tools that return large JSON arrays (search results,
|
|
database queries, etc.).
|
|
|
|
Example:
|
|
from langchain.tools import Tool
|
|
from headroom.integrations import HeadroomToolWrapper
|
|
|
|
def search(query: str) -> str:
|
|
# Returns large JSON with 1000 results
|
|
return json.dumps({"results": [...1000 items...]})
|
|
|
|
search_tool = Tool(name="search", func=search, description="Search")
|
|
wrapped = HeadroomToolWrapper(search_tool)
|
|
|
|
# Use wrapped tool - output automatically compressed
|
|
result = wrapped("python tutorials")
|
|
|
|
Attributes:
|
|
tool: The wrapped LangChain tool
|
|
min_chars_to_compress: Minimum output size to trigger compression
|
|
metrics_collector: Collector for compression metrics
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
tool: BaseTool,
|
|
min_chars_to_compress: int = 1000,
|
|
metrics_collector: ToolMetricsCollector | None = None,
|
|
):
|
|
"""Initialize HeadroomToolWrapper.
|
|
|
|
Args:
|
|
tool: The LangChain BaseTool to wrap.
|
|
min_chars_to_compress: Minimum character count for output
|
|
before compression is applied. Default 1000.
|
|
metrics_collector: Collector for metrics. Uses global
|
|
collector if not specified.
|
|
"""
|
|
_check_langchain_available()
|
|
|
|
self.tool = tool
|
|
self.min_chars_to_compress = min_chars_to_compress
|
|
self._metrics = metrics_collector or _global_metrics
|
|
|
|
# Copy tool metadata
|
|
self.name = tool.name
|
|
self.description = tool.description
|
|
|
|
def __call__(self, *args: Any, **kwargs: Any) -> str:
|
|
"""Invoke the tool and compress output.
|
|
|
|
Args:
|
|
*args: Arguments to pass to the tool.
|
|
**kwargs: Keyword arguments to pass to the tool.
|
|
|
|
Returns:
|
|
Compressed tool output as string.
|
|
"""
|
|
# Invoke underlying tool
|
|
result = self.tool.invoke(*args, **kwargs)
|
|
|
|
# Convert to string if needed
|
|
if not isinstance(result, str):
|
|
result = str(result)
|
|
|
|
# Check if compression is needed
|
|
if len(result) < self.min_chars_to_compress:
|
|
self._record_metrics(result, result, was_compressed=False)
|
|
return str(result)
|
|
|
|
# Try to compress
|
|
compressed = self._compress_output(result)
|
|
self._record_metrics(result, compressed, was_compressed=True)
|
|
|
|
return compressed
|
|
|
|
def invoke(self, *args: Any, **kwargs: Any) -> str:
|
|
"""Invoke the tool (alias for __call__)."""
|
|
return self(*args, **kwargs)
|
|
|
|
def _compress_output(self, output: str) -> str:
|
|
"""Apply compression to tool output.
|
|
|
|
Args:
|
|
output: Tool output string.
|
|
|
|
Returns:
|
|
Compressed output.
|
|
"""
|
|
try:
|
|
return compress_tool_result(
|
|
content=output,
|
|
tool_name=self.name,
|
|
)
|
|
except Exception as e:
|
|
logger.debug(f"Tool compression failed: {e}")
|
|
return output
|
|
|
|
def _record_metrics(self, original: str, compressed: str, was_compressed: bool) -> None:
|
|
"""Record compression metrics.
|
|
|
|
Args:
|
|
original: Original output.
|
|
compressed: Compressed output.
|
|
was_compressed: Whether compression was applied.
|
|
"""
|
|
chars_before = len(original)
|
|
chars_after = len(compressed)
|
|
chars_saved = chars_before - chars_after
|
|
|
|
metric = ToolCompressionMetrics(
|
|
tool_name=self.name,
|
|
timestamp=datetime.now(),
|
|
chars_before=chars_before,
|
|
chars_after=chars_after,
|
|
chars_saved=max(0, chars_saved),
|
|
compression_ratio=chars_after / chars_before if chars_before > 0 else 1.0,
|
|
was_compressed=was_compressed and chars_saved > 0,
|
|
)
|
|
|
|
self._metrics.add(metric)
|
|
|
|
if was_compressed and chars_saved > 0:
|
|
logger.info(
|
|
f"HeadroomToolWrapper[{self.name}]: {chars_before} -> {chars_after} chars "
|
|
f"({chars_saved} saved, {metric.compression_ratio:.1%} of original)"
|
|
)
|
|
|
|
def as_langchain_tool(self) -> StructuredTool:
|
|
"""Convert wrapper back to a LangChain tool.
|
|
|
|
Useful when you need to pass the wrapped tool to APIs
|
|
that expect a LangChain tool type.
|
|
|
|
Returns:
|
|
StructuredTool that wraps this wrapper.
|
|
"""
|
|
return StructuredTool.from_function(
|
|
func=self.__call__,
|
|
name=self.name,
|
|
description=self.description,
|
|
)
|
|
|
|
|
|
def wrap_tools_with_headroom(
|
|
tools: list[BaseTool],
|
|
min_chars_to_compress: int = 1000,
|
|
metrics_collector: ToolMetricsCollector | None = None,
|
|
) -> list[StructuredTool]:
|
|
"""Wrap multiple LangChain tools with Headroom compression.
|
|
|
|
Convenience function to wrap all tools in a list at once.
|
|
|
|
Args:
|
|
tools: List of LangChain tools to wrap.
|
|
min_chars_to_compress: Minimum output size for compression.
|
|
metrics_collector: Shared metrics collector for all tools.
|
|
|
|
Returns:
|
|
List of wrapped tools as StructuredTools.
|
|
|
|
Example:
|
|
from langchain.tools import Tool
|
|
from headroom.integrations import wrap_tools_with_headroom
|
|
|
|
tools = [search_tool, database_tool, api_tool]
|
|
wrapped = wrap_tools_with_headroom(tools)
|
|
|
|
# Use wrapped tools in agent
|
|
agent = create_openai_tools_agent(llm, wrapped, prompt)
|
|
"""
|
|
_check_langchain_available()
|
|
|
|
collector = metrics_collector or _global_metrics
|
|
|
|
wrapped = []
|
|
for tool in tools:
|
|
wrapper = HeadroomToolWrapper(
|
|
tool=tool,
|
|
min_chars_to_compress=min_chars_to_compress,
|
|
metrics_collector=collector,
|
|
)
|
|
wrapped.append(wrapper.as_langchain_tool())
|
|
|
|
return wrapped
|