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>
This commit is contained in:
Executable
+300
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user