mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Previously, HeadroomAgnoModel called wrapped_model.response() which ran the tool execution loop internally. This meant tool outputs (often 60k+ chars) were never optimized - only the initial messages were compressed. The fix delegates response() to the inherited Model.response(), which calls self.invoke() for each API call. Our invoke() override optimizes messages before delegating to wrapped_model.invoke(), ensuring tool outputs are compressed on every API request. Results from multi_tool_agent_test.py with Claude Sonnet: - Tokens before optimization: 25,713 - Tokens after optimization: 6,100 - Tokens saved: 19,613 (76.3%) - Both baseline and optimized found all critical information Also adds: - multi_tool_agent_test.py: Real function calling test with 4 tools - multi_tool_compression_test.py: Direct compression test - README update with multi-tool agent test results
337 lines
12 KiB
Python
337 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Multi-Tool Agent Test: Diverse Data Types with Claude API
|
|
|
|
This test creates an agent with multiple tools returning different data types:
|
|
- GitHub: Issues, PRs, repo metadata
|
|
- ArXiv: Paper abstracts and citations
|
|
- Code Search: Source code snippets
|
|
- Database: JSON records
|
|
|
|
We run it WITHOUT Headroom and WITH Headroom to compare token usage.
|
|
Uses Claude API for real function calling.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
from agno.agent import Agent
|
|
from agno.models.anthropic import Claude
|
|
from agno.tools import tool
|
|
|
|
# Check for API key
|
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
raise ValueError("ANTHROPIC_API_KEY environment variable required")
|
|
|
|
# =============================================================================
|
|
# MOCK TOOL DATA - Realistic responses from various sources
|
|
# =============================================================================
|
|
|
|
GITHUB_ISSUES = [
|
|
{
|
|
"number": i,
|
|
"title": f"Issue #{i}: {'Memory leak in worker pool' if i == 42 else 'Feature request: ' + ['dark mode', 'API pagination', 'webhook support', 'rate limiting'][i % 4]}",
|
|
"state": "open" if i % 3 != 0 else "closed",
|
|
"author": f"user{i % 20}",
|
|
"labels": ["bug", "priority:high"] if i == 42 else ["enhancement"],
|
|
"created_at": f"2024-12-{(i % 28) + 1:02d}T10:00:00Z",
|
|
"updated_at": f"2024-12-{(i % 28) + 1:02d}T15:00:00Z",
|
|
"comments": i % 10,
|
|
"body": "Worker threads are not being released after task completion, causing memory to grow unboundedly. Stack trace attached."
|
|
if i == 42
|
|
else f"Please add support for {['dark mode', 'API pagination', 'webhook support', 'rate limiting'][i % 4]}. This would greatly improve the user experience.",
|
|
"assignees": ["maintainer1"] if i == 42 else [],
|
|
"milestone": "v2.0" if i < 20 else None,
|
|
"reactions": {"thumbs_up": 47 if i == 42 else i % 5, "thumbs_down": 0},
|
|
}
|
|
for i in range(50)
|
|
]
|
|
|
|
ARXIV_PAPERS = [
|
|
{
|
|
"id": f"2401.{i:05d}",
|
|
"title": f"{'Attention Is All You Need: Revisited' if i == 15 else ['Deep Learning for Code Generation', 'Efficient Transformers', 'Neural Architecture Search', 'Language Model Scaling'][i % 4]}",
|
|
"authors": [f"Author{j}" for j in range(3 + i % 3)],
|
|
"abstract": "We revisit the transformer architecture and propose key optimizations that reduce memory usage by 40% while maintaining accuracy. Our method introduces sparse attention patterns..."
|
|
if i == 15
|
|
else f"This paper presents a novel approach to {['code generation', 'transformer efficiency', 'neural architecture', 'model scaling'][i % 4]}. We demonstrate state-of-the-art results on benchmark datasets.",
|
|
"categories": ["cs.LG", "cs.CL"] if i == 15 else ["cs.LG"],
|
|
"published": f"2024-01-{(i % 28) + 1:02d}",
|
|
"citations": 1247 if i == 15 else i * 3,
|
|
"pdf_url": f"https://arxiv.org/pdf/2401.{i:05d}.pdf",
|
|
"comment": "Accepted at NeurIPS 2024" if i == 15 else None,
|
|
}
|
|
for i in range(30)
|
|
]
|
|
|
|
CODE_SEARCH_RESULTS = [
|
|
{
|
|
"file": f"src/{'worker.py' if i == 23 else ['utils.py', 'api.py', 'models.py', 'handlers.py'][i % 4]}",
|
|
"line": 100 + i * 10,
|
|
"content": '''def cleanup_worker(self):
|
|
"""Release worker resources - MEMORY LEAK FIX"""
|
|
self.thread_pool.shutdown(wait=True)
|
|
self.connections.clear()
|
|
gc.collect() # Force garbage collection'''
|
|
if i == 23
|
|
else f'''def process_{["data", "request", "model", "event"][i % 4]}(self, input):
|
|
"""Process incoming {["data", "request", "model", "event"][i % 4]}"""
|
|
result = self.transform(input)
|
|
return self.validate(result)''',
|
|
"language": "python",
|
|
"repository": "main-app",
|
|
"relevance_score": 0.98 if i == 23 else 0.7 - (i * 0.01),
|
|
"context_before": [" # Worker management", " "],
|
|
"context_after": ["", " def start_worker(self):"],
|
|
}
|
|
for i in range(40)
|
|
]
|
|
|
|
DATABASE_RECORDS = [
|
|
{
|
|
"id": f"rec_{i:06d}",
|
|
"type": "error" if i == 17 else "info",
|
|
"timestamp": f"2024-12-15T{(i % 24):02d}:{(i % 60):02d}:00Z",
|
|
"service": "worker-pool" if i == 17 else ["api", "auth", "db", "cache"][i % 4],
|
|
"message": "OutOfMemoryError: heap space exhausted in WorkerPool.execute()"
|
|
if i == 17
|
|
else f"Operation completed: {['request processed', 'user authenticated', 'query executed', 'cache updated'][i % 4]}",
|
|
"metadata": {
|
|
"heap_used": "7.8GB" if i == 17 else f"{1 + i % 3}GB",
|
|
"heap_max": "8GB",
|
|
"thread_count": 847 if i == 17 else 50 + i % 50,
|
|
},
|
|
"stack_trace": "java.lang.OutOfMemoryError: Java heap space\n\tat WorkerPool.execute(WorkerPool.java:234)\n\tat TaskRunner.run(TaskRunner.java:89)"
|
|
if i == 17
|
|
else None,
|
|
}
|
|
for i in range(60)
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# TOOL DEFINITIONS
|
|
# =============================================================================
|
|
|
|
|
|
@tool(name="search_github_issues")
|
|
def search_github_issues(query: str, repo: str = "main-app") -> str:
|
|
"""Search GitHub issues in a repository.
|
|
|
|
Args:
|
|
query: Search query for issues
|
|
repo: Repository name
|
|
|
|
Returns:
|
|
JSON array of matching issues
|
|
"""
|
|
return json.dumps(GITHUB_ISSUES, indent=2)
|
|
|
|
|
|
@tool(name="search_arxiv_papers")
|
|
def search_arxiv_papers(query: str, max_results: int = 30) -> str:
|
|
"""Search ArXiv for academic papers.
|
|
|
|
Args:
|
|
query: Search query for papers
|
|
max_results: Maximum number of results
|
|
|
|
Returns:
|
|
JSON array of matching papers
|
|
"""
|
|
return json.dumps(ARXIV_PAPERS, indent=2)
|
|
|
|
|
|
@tool(name="search_code")
|
|
def search_code(query: str, language: str = "python") -> str:
|
|
"""Search codebase for matching code.
|
|
|
|
Args:
|
|
query: Code search query
|
|
language: Programming language filter
|
|
|
|
Returns:
|
|
JSON array of code search results
|
|
"""
|
|
return json.dumps(CODE_SEARCH_RESULTS, indent=2)
|
|
|
|
|
|
@tool(name="query_database")
|
|
def query_database(query: str, table: str = "logs") -> str:
|
|
"""Query the database for records.
|
|
|
|
Args:
|
|
query: SQL-like query
|
|
table: Table to query
|
|
|
|
Returns:
|
|
JSON array of database records
|
|
"""
|
|
return json.dumps(DATABASE_RECORDS, indent=2)
|
|
|
|
|
|
# =============================================================================
|
|
# TEST RUNNER
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class TestResult:
|
|
label: str
|
|
input_tokens: int
|
|
output_tokens: int
|
|
response: str
|
|
duration_ms: float
|
|
tool_calls: int
|
|
|
|
|
|
def count_tokens_approx(text: str) -> int:
|
|
"""Approximate token count (Ollama doesn't always report tokens)."""
|
|
return len(text) // 4
|
|
|
|
|
|
def run_agent_test(use_headroom: bool) -> TestResult:
|
|
"""Run the multi-tool agent test."""
|
|
|
|
label = "WITH Headroom" if use_headroom else "WITHOUT Headroom (Baseline)"
|
|
|
|
if use_headroom:
|
|
from headroom.integrations.agno import HeadroomAgnoModel
|
|
|
|
base_model = Claude(id="claude-sonnet-4-20250514")
|
|
model = HeadroomAgnoModel(wrapped_model=base_model)
|
|
else:
|
|
model = Claude(id="claude-sonnet-4-20250514")
|
|
|
|
agent = Agent(
|
|
model=model,
|
|
tools=[search_github_issues, search_arxiv_papers, search_code, query_database],
|
|
markdown=True,
|
|
)
|
|
|
|
# The question that requires searching multiple sources
|
|
question = """I'm investigating a memory leak in our application. Please:
|
|
1. Search GitHub issues for memory-related bugs
|
|
2. Search our codebase for memory leak fixes
|
|
3. Check the database logs for OutOfMemory errors
|
|
4. Find any relevant research papers about memory management in worker pools
|
|
|
|
Summarize what you find and recommend a fix."""
|
|
|
|
print(f"\n{'=' * 70}")
|
|
print(f"Running: {label}")
|
|
print(f"{'=' * 70}")
|
|
print(f"Question: {question[:100]}...")
|
|
|
|
start_time = time.time()
|
|
|
|
try:
|
|
response = agent.run(question)
|
|
response_text = response.content if hasattr(response, "content") else str(response)
|
|
except Exception as e:
|
|
response_text = f"Error: {e}"
|
|
|
|
duration_ms = (time.time() - start_time) * 1000
|
|
|
|
# Get token counts
|
|
if use_headroom and hasattr(model, "total_tokens_saved"):
|
|
summary = model.get_savings_summary()
|
|
input_tokens = summary.get("total_tokens_after", 0) # Actual tokens sent to API
|
|
tokens_before = summary.get("total_tokens_before", 0)
|
|
tokens_saved = model.total_tokens_saved
|
|
savings_pct = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0
|
|
print("\n📊 Headroom Optimization Stats:")
|
|
print(f" API requests made: {summary.get('total_requests', 0)}")
|
|
print(f" Tokens BEFORE optimization: {tokens_before:,}")
|
|
print(f" Tokens AFTER optimization: {input_tokens:,}")
|
|
print(f" Tokens SAVED: {tokens_saved:,} ({savings_pct:.1f}%)")
|
|
else:
|
|
# Estimate from data size
|
|
total_data = (
|
|
json.dumps(GITHUB_ISSUES)
|
|
+ json.dumps(ARXIV_PAPERS)
|
|
+ json.dumps(CODE_SEARCH_RESULTS)
|
|
+ json.dumps(DATABASE_RECORDS)
|
|
)
|
|
input_tokens = count_tokens_approx(total_data + question)
|
|
|
|
print(f"\nResponse preview: {response_text[:500]}...")
|
|
print(f"Duration: {duration_ms:.0f}ms")
|
|
|
|
return TestResult(
|
|
label=label,
|
|
input_tokens=input_tokens,
|
|
output_tokens=count_tokens_approx(response_text),
|
|
response=response_text,
|
|
duration_ms=duration_ms,
|
|
tool_calls=4, # We expect 4 tool calls
|
|
)
|
|
|
|
|
|
def main():
|
|
print("\n" + "=" * 70)
|
|
print("MULTI-TOOL AGENT TEST")
|
|
print("Testing diverse data types: GitHub, ArXiv, Code, Database")
|
|
print("Model: Claude Sonnet (claude-sonnet-4-20250514)")
|
|
print("=" * 70)
|
|
|
|
# Show data sizes
|
|
print("\nTool output sizes:")
|
|
print(
|
|
f" GitHub Issues: {len(json.dumps(GITHUB_ISSUES)):,} chars ({len(GITHUB_ISSUES)} items)"
|
|
)
|
|
print(f" ArXiv Papers: {len(json.dumps(ARXIV_PAPERS)):,} chars ({len(ARXIV_PAPERS)} items)")
|
|
print(
|
|
f" Code Search: {len(json.dumps(CODE_SEARCH_RESULTS)):,} chars ({len(CODE_SEARCH_RESULTS)} items)"
|
|
)
|
|
print(
|
|
f" Database Logs: {len(json.dumps(DATABASE_RECORDS)):,} chars ({len(DATABASE_RECORDS)} items)"
|
|
)
|
|
total_chars = sum(
|
|
len(json.dumps(d))
|
|
for d in [GITHUB_ISSUES, ARXIV_PAPERS, CODE_SEARCH_RESULTS, DATABASE_RECORDS]
|
|
)
|
|
print(f" TOTAL: {total_chars:,} chars (~{total_chars // 4:,} tokens)")
|
|
|
|
# Run baseline (no Headroom)
|
|
print("\n" + "-" * 70)
|
|
baseline = run_agent_test(use_headroom=False)
|
|
|
|
# Run with Headroom
|
|
print("\n" + "-" * 70)
|
|
optimized = run_agent_test(use_headroom=True)
|
|
|
|
# Final comparison
|
|
print("\n" + "=" * 70)
|
|
print("FINAL COMPARISON")
|
|
print("=" * 70)
|
|
|
|
print(f"""
|
|
Baseline Headroom
|
|
─────────────────────────────────────────────────────
|
|
Tokens Sent to API: {baseline.input_tokens:>6,} {optimized.input_tokens:>6,}
|
|
Duration: {baseline.duration_ms:>6,.0f}ms {optimized.duration_ms:>6,.0f}ms
|
|
Tool Calls: {baseline.tool_calls:>6} {optimized.tool_calls:>6}
|
|
""")
|
|
|
|
if baseline.input_tokens > optimized.input_tokens:
|
|
saved = baseline.input_tokens - optimized.input_tokens
|
|
percent = (saved / baseline.input_tokens) * 100
|
|
print(f" ✨ Tokens Saved: {saved:,} ({percent:.1f}% reduction)")
|
|
print(f" 💰 Estimated Cost Savings: {percent:.0f}% on input tokens")
|
|
|
|
print("\n" + "=" * 70)
|
|
print("BASELINE RESPONSE (excerpt):")
|
|
print("=" * 70)
|
|
print(baseline.response[:1500] if len(baseline.response) > 1500 else baseline.response)
|
|
|
|
print("\n" + "=" * 70)
|
|
print("HEADROOM RESPONSE (excerpt):")
|
|
print("=" * 70)
|
|
print(optimized.response[:1500] if len(optimized.response) > 1500 else optimized.response)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|