Bumps the pip-minor-patch group with 1 update in the / directory: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.22 to 0.16.2 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p> <blockquote> <h2>0.16.2</h2> <h2>Release Notes</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> </ul> <h2>Install ruff 0.16.2</h2> <h3>Install prebuilt binaries via shell script</h3> <pre lang="sh"><code>curl --proto '=https' --tlsv1.2 -LsSf https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.sh | sh </code></pre> <h3>Install prebuilt binaries via powershell script</h3> <pre lang="sh"><code>powershell -ExecutionPolicy Bypass -c "irm https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.ps1 | iex" </code></pre> <h2>Download ruff 0.16.2</h2> <table> <thead> <tr> <th>File</th> <th>Platform</th> <th>Checksum</th> </tr> </thead> <tbody> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz">ruff-aarch64-apple-darwin.tar.gz</a></td> <td>Apple Silicon macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz">ruff-x86_64-apple-darwin.tar.gz</a></td> <td>Intel macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip">ruff-aarch64-pc-windows-msvc.zip</a></td> <td>ARM64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip">ruff-i686-pc-windows-msvc.zip</a></td> <td>x86 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip">ruff-x86_64-pc-windows-msvc.zip</a></td> <td>x64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz">ruff-aarch64-unknown-linux-gnu.tar.gz</a></td> <td>ARM64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz">ruff-i686-unknown-linux-gnu.tar.gz</a></td> <td>x86 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz">ruff-powerpc64-unknown-linux-gnu.tar.gz</a></td> <td>PPC64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz">ruff-powerpc64le-unknown-linux-gnu.tar.gz</a></td> <td>PPC64LE Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz">ruff-riscv64gc-unknown-linux-gnu.tar.gz</a></td> <td>RISCV Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz">ruff-s390x-unknown-linux-gnu.tar.gz</a></td> <td>S390x Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> </tbody> </table> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p> <blockquote> <h2>0.16.2</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> </ul> <h2>0.16.1</h2> <p>Released on 2026-07-30.</p> <h3>Preview features</h3> <ul> <li>Add an option to opt out of human-readable names (<a href="https://redirect.github.com/astral-sh/ruff/pull/27160">#27160</a>)</li> <li>[<code>flake8-pytest-style</code>] Make fixes safe by default and unsafe only when comments are present (<code>PT018</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27201">#27201</a>)</li> <li>[<code>pyupgrade</code>] Skip fix when a defaulted <code>TypeVar</code> precedes a non-defaulted one (<code>UP040</code>, <code>UP046</code>, <code>UP047</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27133">#27133</a>)</li> <li>[<code>ruff</code>] Fix false positive with unpacked arguments (<code>RUF065</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/26959">#26959</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Bump <code>gen-lsp-types</code> to gracefully handle unknown enumeration values in LSP messages (<a href="https://redirect.github.com/astral-sh/ruff/pull/27230">#27230</a>)</li> <li>[<code>flake8-bugbear</code>] Mark <code>range</code> as immutable (<code>B008</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27247">#27247</a>)</li> <li>[<code>flake8-comprehensions</code>] NFKC-normalize keyword names in <code>C408</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/26813">#26813</a>)</li> <li>[<code>flake8-return</code>] Fix false positive when variable is read in <code>finally</code> clause (<code>RET504</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25441">#25441</a>)</li> <li>[<code>pydocstyle</code>] Skip section detection inside RST directive bodies (<code>D214</code>, <code>D405</code>, <code>D413</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/23635">#23635</a>)</li> <li>[<code>refurb</code>] Parenthesize <code>yield</code> arguments in the <code>FURB192</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/27192">#27192</a>)</li> </ul> <h3>Rule changes</h3> <ul> <li>[<code>flake8-pytest-style</code>] Mark <code>PT022</code> fixes as unsafe (<a href="https://redirect.github.com/astral-sh/ruff/pull/26440">#26440</a>)</li> <li>[<code>refurb</code>] Mark fixes that remove unknown separators as unsafe (<code>FURB105</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27200">#27200</a>)</li> </ul> <h3>Server</h3> <ul> <li>Fix indexing of excluded nested Ruff workspaces (<a href="https://redirect.github.com/astral-sh/ruff/pull/27303">#27303</a>)</li> <li>Lint TOML files in the LSP (<a href="https://redirect.github.com/astral-sh/ruff/pull/26862">#26862</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="5b48a04097"><code>5b48a04</code></a> Bump 0.16.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27555">#27555</a>)</li> <li><a href="1b9e5fc483"><code>1b9e5fc</code></a> Update Swatinem/rust-cache action to v2.9.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27568">#27568</a>)</li> <li><a href="c4e86fc039"><code>c4e86fc</code></a> [ty] Add helper extension methods for half-range and equality constraints (<a href="https://redirect.github.com/astral-sh/ruff/issues/2">#2</a>...</li> <li><a href="17a00de2e2"><code>17a00de</code></a> [ty] Reuse primer commands in memory reports (<a href="https://redirect.github.com/astral-sh/ruff/issues/27553">#27553</a>)</li> <li><a href="6ea296b969"><code>6ea296b</code></a> [ty] Normalize type labels in structured docstrings (<a href="https://redirect.github.com/astral-sh/ruff/issues/26923">#26923</a>)</li> <li><a href="2fc445f005"><code>2fc445f</code></a> [ty] Diagnose invalid <strong>getattr</strong> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27502">#27502</a>)</li> <li><a href="22c7823c4e"><code>22c7823</code></a> [ty] Enable (but downrank) auto-import completion suggestions from stub-only ...</li> <li><a href="05160d507f"><code>05160d5</code></a> [ty] Diagnose invalid descriptor <code>__get__</code> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27400">#27400</a>)</li> <li><a href="baea3d0dce"><code>baea3d0</code></a> [ty] Expose strict analysis options in the playground (<a href="https://redirect.github.com/astral-sh/ruff/issues/27543">#27543</a>)</li> <li><a href="c88946ebeb"><code>c88946e</code></a> [ty] Bump ecosystem-analyzer for strict project settings (<a href="https://redirect.github.com/astral-sh/ruff/issues/27542">#27542</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.15.22...0.16.2">compare view</a></li> </ul> </details> <br /> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
18 KiB
LangChain Integration
Headroom provides seamless integration with LangChain, enabling automatic context optimization across all LangChain patterns: chat models, memory, retrievers, agents, and observability.
Installation
pip install "headroom-ai[langchain]"
This installs Headroom with LangChain dependencies (langchain-core).
Quick Start
Wrap Any Chat Model (1 Line)
from langchain_openai import ChatOpenAI
from headroom.integrations import HeadroomChatModel
# Wrap your model - that's it!
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
# Use exactly like before
response = llm.invoke("Hello!")
Headroom automatically:
- Detects the provider (OpenAI, Anthropic, Google)
- Compresses tool outputs in conversation history
- Optimizes for provider caching
- Tracks token savings
Check Your Savings
# After some usage
print(llm.get_metrics())
# {'tokens_saved': 12500, 'savings_percent': 45.2, 'requests': 50}
Integration Patterns
1. Chat Model Wrapper
The HeadroomChatModel wraps any LangChain BaseChatModel:
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from headroom.integrations import HeadroomChatModel
# OpenAI
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
# Anthropic (auto-detected)
llm = HeadroomChatModel(ChatAnthropic(model="claude-3-5-sonnet-20241022"))
# Custom configuration
from headroom import HeadroomConfig, HeadroomMode
config = HeadroomConfig(
default_mode=HeadroomMode.OPTIMIZE,
smart_crusher_target_ratio=0.3, # Target 70% compression
)
llm = HeadroomChatModel(
ChatOpenAI(model="gpt-4o"),
headroom_config=config,
)
Async Support
Full async support for ainvoke and astream:
# Async invoke
response = await llm.ainvoke("Hello!")
# Async streaming
async for chunk in llm.astream("Tell me a story"):
print(chunk.content, end="", flush=True)
Tool Calling
Works seamlessly with LangChain tool calling:
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the web."""
return {"results": [...]} # Large JSON response
llm_with_tools = llm.bind_tools([search])
response = llm_with_tools.invoke("Search for Python tutorials")
# Tool outputs are automatically compressed in subsequent turns
2. Memory Integration
HeadroomChatMessageHistory wraps any chat history with automatic compression:
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import ChatMessageHistory
from headroom.integrations import HeadroomChatMessageHistory
# Wrap any history
base_history = ChatMessageHistory()
compressed_history = HeadroomChatMessageHistory(
base_history,
compress_threshold_tokens=4000, # Compress when over 4K tokens
keep_recent_turns=5, # Always keep last 5 turns
)
# Use with any memory class
memory = ConversationBufferMemory(chat_memory=compressed_history)
# Zero changes to your chain!
chain = ConversationChain(llm=llm, memory=memory)
Why this matters: Long conversations can blow up to 50K+ tokens. HeadroomChatMessageHistory automatically compresses older turns while preserving recent context.
# Check compression stats
print(compressed_history.get_compression_stats())
# {'compression_count': 12, 'total_tokens_saved': 28000}
3. Retriever Integration
HeadroomDocumentCompressor filters retrieved documents by relevance:
from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.vectorstores import FAISS
from headroom.integrations import HeadroomDocumentCompressor
# Create vector store retriever (retrieve many for recall)
vectorstore = FAISS.from_documents(documents, embeddings)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 50})
# Wrap with Headroom compression (keep best for precision)
compressor = HeadroomDocumentCompressor(
max_documents=10, # Keep top 10
min_relevance=0.3, # Minimum relevance score
prefer_diverse=True, # MMR-style diversity
)
retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
# Retrieves 50 docs, returns best 10
docs = retriever.invoke("What is Python?")
Why this matters: Vector search often returns many marginally-relevant documents. HeadroomDocumentCompressor uses BM25-style scoring to keep only the most relevant ones, reducing context size while improving answer quality.
4. Agent Tool Wrapping
wrap_tools_with_headroom compresses tool outputs for agents:
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_core.tools import tool
from headroom.integrations import wrap_tools_with_headroom
@tool
def search_database(query: str) -> str:
"""Search the database."""
# Returns 1000 results as JSON
return json.dumps({"results": [...], "total": 1000})
@tool
def fetch_logs(service: str) -> str:
"""Fetch service logs."""
# Returns 500 log entries
return json.dumps({"logs": [...]})
# Wrap tools with compression
tools = [search_database, fetch_logs]
wrapped_tools = wrap_tools_with_headroom(
tools,
min_chars_to_compress=1000, # Only compress large outputs
)
# Create agent with wrapped tools
agent = create_openai_tools_agent(llm, wrapped_tools, prompt)
executor = AgentExecutor(agent=agent, tools=wrapped_tools)
# Tool outputs are automatically compressed
result = executor.invoke({"input": "Find users who logged in yesterday"})
Per-tool metrics:
from headroom.integrations import get_tool_metrics
metrics = get_tool_metrics()
print(metrics.get_summary())
# {
# 'total_invocations': 25,
# 'total_compressions': 18,
# 'total_chars_saved': 450000,
# 'by_tool': {
# 'search_database': {'invocations': 15, 'chars_saved': 320000},
# 'fetch_logs': {'invocations': 10, 'chars_saved': 130000},
# }
# }
5. Streaming Metrics
Track output tokens during streaming:
from headroom.integrations import StreamingMetricsTracker
tracker = StreamingMetricsTracker(model="gpt-4o")
for chunk in llm.stream("Write a poem about coding"):
tracker.add_chunk(chunk)
print(chunk.content, end="", flush=True)
metrics = tracker.finish()
print(f"\nOutput tokens: {metrics.output_tokens}")
print(f"Duration: {metrics.duration_ms:.0f}ms")
Context manager style:
from headroom.integrations import StreamingMetricsCallback
with StreamingMetricsCallback(model="gpt-4o") as tracker:
for chunk in llm.stream(messages):
tracker.add_chunk(chunk)
print(chunk.content, end="")
print(f"Metrics: {tracker.metrics}")
6. LangSmith Integration
Add Headroom metrics to LangSmith traces:
from headroom.integrations import HeadroomLangSmithCallbackHandler
# Create callback handler
langsmith_handler = HeadroomLangSmithCallbackHandler()
# Use with your LLM
llm = HeadroomChatModel(
ChatOpenAI(model="gpt-4o"),
callbacks=[langsmith_handler],
)
# After calls, metrics appear in LangSmith traces:
# - headroom.tokens_before
# - headroom.tokens_after
# - headroom.tokens_saved
# - headroom.compression_ratio
Real-World Examples
Example 1: LangGraph ReAct Agent
The ReAct pattern is the most common agent architecture. Here's how to optimize it:
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from headroom.integrations import HeadroomChatModel, wrap_tools_with_headroom
# Define tools that return large outputs
@tool
def search_web(query: str) -> str:
"""Search the web for information."""
# Simulating large search results
return json.dumps(
{
"results": [
{"title": f"Result {i}", "snippet": "..." * 100, "url": f"https://..."}
for i in range(100)
],
"total": 1000,
}
)
@tool
def query_database(sql: str) -> str:
"""Execute SQL query."""
return json.dumps(
{
"rows": [{"id": i, "data": "..." * 50} for i in range(500)],
"total": 500,
}
)
# Wrap model with Headroom
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
# Wrap tools with compression
tools = wrap_tools_with_headroom([search_web, query_database])
# Create ReAct agent
agent = create_react_agent(llm, tools)
# Run - tool outputs are automatically compressed between iterations
result = agent.invoke(
{"messages": [("user", "Find all users who signed up last week and their activity")]}
)
# Check savings
print(f"Tokens saved: {llm.get_metrics()['tokens_saved']}")
Without Headroom: Each tool call adds 10-50K tokens to context. With Headroom: Tool outputs compressed to 1-2K tokens, agent runs faster and cheaper.
Example 1b: LangGraph Custom Graph with compress_tool_messages Node
If you're building a custom LangGraph StateGraph (instead of using create_react_agent),
you can insert a compression node between tools and the agent. This compresses all
ToolMessage content in the graph state before the LLM sees it.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from headroom.integrations.langchain import create_compress_tool_messages_node
# Define your agent and tools nodes
def agent_node(state: MessagesState):
llm = ChatOpenAI(model="gpt-4o")
response = llm.invoke(state["messages"])
return {"messages": [response]}
def tools_node(state: MessagesState):
# Your tool execution logic here
...
# Build the graph with a compression step
graph = StateGraph(MessagesState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tools_node)
graph.add_node(
"compress",
create_compress_tool_messages_node(
min_tokens_to_compress=100, # Only compress outputs > ~100 tokens
),
)
# Wire: tools -> compress -> agent (instead of tools -> agent directly)
graph.add_edge(START, "agent")
graph.add_edge("tools", "compress")
graph.add_edge("compress", "agent")
# ... add conditional edges from agent to tools/END as needed
app = graph.compile()
result = app.invoke({"messages": [HumanMessage(content="Find sales data")]})
You can also use compress_tool_messages directly as a standalone function:
from headroom.integrations.langchain import compress_tool_messages
# Compress ToolMessages in any list of LangChain messages
result = compress_tool_messages(messages, min_tokens_to_compress=100)
compressed_messages = result.messages
print(f"Saved {result.total_tokens_saved} tokens across {result.messages_compressed} messages")
Example 2: RAG Pipeline with Document Filtering
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.retrievers import ContextualCompressionRetriever
from headroom.integrations import HeadroomChatModel, HeadroomDocumentCompressor
# Setup vector store
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents, embeddings)
# High-recall retriever (get many candidates)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 50})
# Headroom compressor for precision
compressor = HeadroomDocumentCompressor(
max_documents=5, # Keep only top 5
min_relevance=0.4, # Must be 40%+ relevant
prefer_diverse=True, # Avoid redundant docs
)
# Combine into compression retriever
retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
# Wrap LLM
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
# Create QA chain
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
return_source_documents=True,
)
# Query - retrieves 50 docs, uses best 5
result = qa_chain.invoke({"query": "How do I configure authentication?"})
print(f"Answer: {result['result']}")
print(f"Sources: {len(result['source_documents'])} docs")
Impact:
- Without filtering: 50 docs × ~500 tokens = 25K context tokens
- With Headroom: 5 docs × ~500 tokens = 2.5K context tokens (90% reduction)
Example 3: Conversational Agent with Memory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain.chains import ConversationChain
from headroom.integrations import HeadroomChatModel, HeadroomChatMessageHistory
# Wrap LLM
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
# Wrap memory with auto-compression
base_history = ChatMessageHistory()
compressed_history = HeadroomChatMessageHistory(
base_history,
compress_threshold_tokens=8000, # Compress when over 8K
keep_recent_turns=10, # Always keep last 10 turns
)
memory = ConversationBufferMemory(
chat_memory=compressed_history,
return_messages=True,
)
# Create conversation chain
chain = ConversationChain(llm=llm, memory=memory)
# Long conversation - memory auto-compresses
for i in range(100):
response = chain.invoke({"input": f"Tell me about topic {i}"})
print(f"Turn {i}: {len(response['response'])} chars")
# Check memory stats
print(compressed_history.get_compression_stats())
# {'compression_count': 8, 'total_tokens_saved': 45000}
Impact: Without compression, 100-turn conversation = 100K+ tokens. With HeadroomChatMessageHistory, it stays under 8K tokens while preserving recent context.
Example 4: Multi-Tool Research Agent
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from headroom.integrations import (
HeadroomChatModel,
wrap_tools_with_headroom,
get_tool_metrics,
reset_tool_metrics,
)
@tool
def search_arxiv(query: str) -> str:
"""Search arXiv for papers."""
return json.dumps(
{"papers": [{"title": f"Paper {i}", "abstract": "..." * 200} for i in range(50)]}
)
@tool
def search_github(query: str) -> str:
"""Search GitHub repositories."""
return json.dumps(
{
"repos": [
{"name": f"repo-{i}", "description": "..." * 100, "stars": i * 100}
for i in range(100)
]
}
)
@tool
def fetch_documentation(url: str) -> str:
"""Fetch documentation from URL."""
return "..." * 5000 # Large doc content
# Wrap everything
llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
tools = wrap_tools_with_headroom([search_arxiv, search_github, fetch_documentation])
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a research assistant. Use tools to gather information."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Reset metrics for this session
reset_tool_metrics()
# Run complex research task
result = executor.invoke(
{
"input": "Research the latest advances in LLM context compression and find relevant GitHub projects"
}
)
# Check per-tool metrics
metrics = get_tool_metrics().get_summary()
print(f"Total chars saved: {metrics['total_chars_saved']:,}")
print(f"Per-tool breakdown: {metrics['by_tool']}")
Configuration Options
HeadroomChatModel
HeadroomChatModel(
wrapped_model, # Any LangChain BaseChatModel
headroom_config=HeadroomConfig(), # Headroom configuration
auto_detect_provider=True, # Auto-detect from wrapped model
)
HeadroomChatMessageHistory
HeadroomChatMessageHistory(
base_history, # Any BaseChatMessageHistory
compress_threshold_tokens=4000, # Token threshold for compression
keep_recent_turns=5, # Minimum turns to preserve
model="gpt-4o", # Model for token counting
)
HeadroomDocumentCompressor
HeadroomDocumentCompressor(
max_documents=10, # Maximum docs to return
min_relevance=0.0, # Minimum relevance score (0-1)
prefer_diverse=False, # Use MMR for diversity
)
wrap_tools_with_headroom
wrap_tools_with_headroom(
tools, # List of LangChain tools
min_chars_to_compress=1000, # Minimum output size
smart_crusher_config=None, # SmartCrusher configuration
)
Import Reference
from headroom.integrations import (
# Chat Model
HeadroomChatModel,
# Memory
HeadroomChatMessageHistory,
# Retrievers
HeadroomDocumentCompressor,
# Agents
HeadroomToolWrapper,
wrap_tools_with_headroom,
get_tool_metrics,
reset_tool_metrics,
# Streaming
StreamingMetricsTracker,
StreamingMetricsCallback,
track_streaming_response,
# LangSmith
HeadroomLangSmithCallbackHandler,
# Provider Detection
detect_provider,
get_headroom_provider,
)
# Or import from subpackage directly
from headroom.integrations.langchain import HeadroomChatModel
from headroom.integrations.langchain.memory import HeadroomChatMessageHistory
Troubleshooting
LangChain not detected
from headroom.integrations import langchain_available
if not langchain_available():
print("Install with: pip install headroom-ai[langchain]")
Provider detection failing
# Force a specific provider
from headroom.providers import AnthropicProvider
llm = HeadroomChatModel(
ChatAnthropic(model="claude-3-5-sonnet-20241022"),
auto_detect_provider=False,
)
llm._provider = AnthropicProvider()
Memory not compressing
Check that your message count exceeds the threshold:
history = HeadroomChatMessageHistory(
base_history,
compress_threshold_tokens=1000, # Lower threshold
keep_recent_turns=2, # Fewer preserved turns
)
Performance Tips
- Use tool wrapping for agents - Agents with tools benefit most from compression
- Set appropriate thresholds - Don't compress small conversations
- Enable diversity for RAG -
prefer_diverse=Trueimproves answer quality - Monitor with LangSmith - Use the callback handler to track savings over time
- Batch similar requests - Provider caching works better with stable prefixes