Files
tatlock/scripts/benchmark_steward.py
T
jpmschweitzerandClaude Sonnet 4.5 6eed5f4d13 feat: implement Phase 2 two-tier architecture with Steward
Add comprehensive two-tier architecture where Steward analyzes requests
and Tatlock executes with scoped tools. Includes full infrastructure for
request preprocessing, tool tracking, benchmarking, and streaming.

**Added:**
- Steward agent for request analysis and capability recommendation
- Household Registry for centralized capability management
- Request preprocessing pipeline (Steward → Tatlock flow)
- Tool usage tracking and benchmarking system
- Streaming transparency (Steward reasoning visible in streams)
- Structured logging with operation timing
- Redis benchmark storage with 30-day expiry
- Benchmark analysis CLI tools

**Infrastructure:**
- src/agents/steward/ - Steward agent implementation
- src/agents/tatlock_core/ - Tatlock capability domain
- src/core/preprocessing.py - Request preprocessing pipeline
- src/core/tool_tracking.py - Tool call tracking
- src/core/benchmarks.py - Benchmark recording system
- src/core/household_registry.py - Capability registry
- src/core/startup.py - Application startup coordination
- src/core/logging_config.py - Structured logging setup

**Integration:**
- Responses API uses Steward for Tatlock requests
- Chat Completions wraps Responses API for OpenAI compatibility
- Streaming coordinator supports Steward + Tatlock flow
- Tool scoping per request based on Steward recommendations

**Testing:**
- Integration tests for Steward-Tatlock flow
- Benchmark and registry unit tests
- Steward streaming tests

See PHASE2_PLAN.md and PHASE2_COMPLETE.md for detailed documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 15:39:20 +01:00

301 lines
9.3 KiB
Python
Executable File

"""
Benchmark script for the Steward agent.
Tests Steward's request analysis performance with various scenarios
to ensure it meets latency targets:
- Target max: 5 seconds
- Target average: ~1.67 seconds
Usage:
python scripts/benchmark_steward.py [--iterations N] [--verbose]
"""
import argparse
import asyncio
import statistics
from datetime import datetime
from typing import List
from src.agents.steward import analyze_request
from src.core.startup import initialize_application
class BenchmarkResult:
"""Results from a single benchmark run."""
def __init__(self, scenario: str, duration: float, success: bool, error: str = None):
self.scenario = scenario
self.duration = duration
self.success = success
self.error = error
async def benchmark_scenario(
name: str,
request: str,
history: list[dict],
iterations: int = 10
) -> List[BenchmarkResult]:
"""
Benchmark a specific scenario.
Args:
name: Scenario name
request: User request to analyze
history: Conversation history
iterations: Number of times to run
Returns:
List of benchmark results
"""
results = []
print(f"\n📊 Benchmarking: {name}")
print(f" Request: {request[:50]}{'...' if len(request) > 50 else ''}")
print(f" History length: {len(history)} turns")
print(f" Iterations: {iterations}")
for i in range(iterations):
try:
start = datetime.now()
await analyze_request(request, history)
duration = (datetime.now() - start).total_seconds()
results.append(BenchmarkResult(name, duration, True))
# Progress indicator
print(".", end="", flush=True)
except Exception as e:
duration = (datetime.now() - start).total_seconds()
results.append(BenchmarkResult(name, duration, False, str(e)))
print("E", end="", flush=True)
print() # New line after progress
return results
def analyze_results(results: List[BenchmarkResult], scenario_name: str):
"""
Analyze and display benchmark results.
Args:
results: List of benchmark results
scenario_name: Name of the scenario
"""
successful = [r for r in results if r.success]
failed = [r for r in results if not r.success]
if not successful:
print(f"\n{scenario_name}: All runs failed!")
for r in failed[:3]: # Show first 3 errors
print(f" Error: {r.error}")
return
durations = [r.duration for r in successful]
min_duration = min(durations)
max_duration = max(durations)
avg_duration = statistics.mean(durations)
median_duration = statistics.median(durations)
# Calculate percentiles
sorted_durations = sorted(durations)
p95_idx = int(len(sorted_durations) * 0.95)
p99_idx = int(len(sorted_durations) * 0.99)
p95 = sorted_durations[p95_idx] if p95_idx < len(sorted_durations) else max_duration
p99 = sorted_durations[p99_idx] if p99_idx < len(sorted_durations) else max_duration
# Targets
target_max = 5.0
target_avg = 1.67
# Status emojis
max_status = "✅" if max_duration <= target_max else "⚠️"
avg_status = "✅" if avg_duration <= target_avg else "⚠️"
print(f"\n Results ({len(successful)}/{len(results)} successful):")
print(f" Min: {min_duration:6.3f}s")
print(f" Avg: {avg_duration:6.3f}s {avg_status} (target: ≤{target_avg}s)")
print(f" Median: {median_duration:6.3f}s")
print(f" P95: {p95:6.3f}s")
print(f" P99: {p99:6.3f}s")
print(f" Max: {max_duration:6.3f}s {max_status} (target: ≤{target_max}s)")
if failed:
print(f" Failed: {len(failed)} runs")
return {
"min": min_duration,
"avg": avg_duration,
"median": median_duration,
"p95": p95,
"p99": p99,
"max": max_duration,
"success_rate": len(successful) / len(results) * 100,
}
async def run_benchmarks(iterations: int = 10, verbose: bool = False):
"""
Run comprehensive Steward benchmarks.
Args:
iterations: Number of iterations per scenario
verbose: Enable verbose output
"""
print("=" * 60)
print("🔬 Steward Performance Benchmark")
print("=" * 60)
print(f"\nTargets:")
print(f" - Maximum response time: ≤5.0s")
print(f" - Average response time: ≤1.67s")
print(f"\nIterations per scenario: {iterations}")
# Initialize application
print("\n🚀 Initializing application...")
initialize_application()
all_stats = {}
# Scenario 1: Simple greeting (no capabilities needed)
results = await benchmark_scenario(
"Simple Greeting",
"Hello!",
[],
iterations
)
all_stats["simple_greeting"] = analyze_results(results, "Simple Greeting")
# Scenario 2: Single tool request (calculator)
results = await benchmark_scenario(
"Calculator Request",
"What's sqrt(144) + 25?",
[],
iterations
)
all_stats["calculator"] = analyze_results(results, "Calculator Request")
# Scenario 3: Web search request
results = await benchmark_scenario(
"Web Search Request",
"Search for the latest Python 3.12 features",
[],
iterations
)
all_stats["web_search"] = analyze_results(results, "Web Search Request")
# Scenario 4: Request with conversation history (short)
short_history = [
{"role": "user", "content": "What's 15 times 7?"},
{"role": "assistant", "content": "105"},
]
results = await benchmark_scenario(
"With Short History",
"And what's that divided by 3?",
short_history,
iterations
)
all_stats["short_history"] = analyze_results(results, "With Short History")
# Scenario 5: Request with longer conversation history
long_history = [
{"role": "user", "content": f"Question {i}"} if i % 2 == 0
else {"role": "assistant", "content": f"Answer {i}"}
for i in range(20)
]
results = await benchmark_scenario(
"With Long History",
"What was the first question I asked?",
long_history,
iterations
)
all_stats["long_history"] = analyze_results(results, "With Long History")
# Scenario 6: Complex request
results = await benchmark_scenario(
"Complex Request",
"Calculate the compound interest on $5000 at 4.5% over 10 years, "
"then search for current savings account rates to compare",
[],
iterations
)
all_stats["complex"] = analyze_results(results, "Complex Request")
# Scenario 7: Missing capabilities
results = await benchmark_scenario(
"Missing Capabilities",
"Generate an image of a sunset over mountains",
[],
iterations
)
all_stats["missing_caps"] = analyze_results(results, "Missing Capabilities")
# Summary
print("\n" + "=" * 60)
print("📈 SUMMARY")
print("=" * 60)
# Calculate overall stats
all_avgs = [stats["avg"] for stats in all_stats.values() if stats]
all_maxs = [stats["max"] for stats in all_stats.values() if stats]
if all_avgs:
overall_avg = statistics.mean(all_avgs)
overall_max = max(all_maxs)
avg_status = "✅" if overall_avg <= 1.67 else "⚠️"
max_status = "✅" if overall_max <= 5.0 else "⚠️"
print(f"\nOverall Performance:")
print(f" Average of averages: {overall_avg:.3f}s {avg_status}")
print(f" Maximum observed: {overall_max:.3f}s {max_status}")
# Performance verdict
print(f"\n{'=' * 60}")
if overall_avg <= 1.67 and overall_max <= 5.0:
print("✅ PERFORMANCE TARGETS MET!")
print(f" The Steward is operating within target parameters.")
elif overall_max <= 5.0:
print("⚠️ PARTIAL SUCCESS")
print(f" Max response time is good, but average is above target.")
print(f" Average: {overall_avg:.3f}s (target: ≤1.67s)")
print(f"\n Recommendations:")
print(f" - Consider using a faster model")
print(f" - Optimize system prompt length")
print(f" - Review tool call limits")
else:
print("❌ PERFORMANCE TARGETS NOT MET")
print(f" Max: {overall_max:.3f}s (target: ≤5.0s)")
print(f" Avg: {overall_avg:.3f}s (target: ≤1.67s)")
print(f"\n Recommendations:")
print(f" - Switch to a faster model (current: mistral-nemo)")
print(f" - Reduce system prompt complexity")
print(f" - Limit tool calls (currently limited to 3)")
print(f" - Consider caching household registry responses")
print("=" * 60)
async def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Benchmark Steward agent performance")
parser.add_argument(
"--iterations",
type=int,
default=10,
help="Number of iterations per scenario (default: 10)"
)
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose output"
)
args = parser.parse_args()
await run_benchmarks(iterations=args.iterations, verbose=args.verbose)
if __name__ == "__main__":
asyncio.run(main())