Files
goose/examples/goose-error-control.py
David Katz 2da115e02d first pass
2025-10-08 13:19:48 -04:00

301 lines
9.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Goose Error Injection Controller
A simple CLI tool to control error injection in a running goose instance
that's using the error_proxy provider.
Usage:
# Enable rate limit errors every 3rd call
./goose-error-control.py enable rate_limit --pattern every_nth --nth 3
# Enable random context errors with 30% probability
./goose-error-control.py enable context_exceeded --pattern random --probability 0.3
# Enable burst of 5 server errors
./goose-error-control.py enable server_error --pattern burst --burst-count 5
# Disable error injection
./goose-error-control.py disable
# Show current status
./goose-error-control.py status
# Watch the control file for changes
./goose-error-control.py watch
"""
import argparse
import json
import os
import sys
import time
from pathlib import Path
from typing import Optional, Dict, Any
DEFAULT_CONTROL_FILE = "/tmp/goose-error-control.json"
class ErrorController:
def __init__(self, control_file: str = DEFAULT_CONTROL_FILE):
self.control_file = Path(control_file)
def read_config(self) -> Dict[str, Any]:
"""Read the current configuration."""
if not self.control_file.exists():
return {"enabled": False}
try:
with open(self.control_file, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {"enabled": False}
def write_config(self, config: Dict[str, Any]) -> None:
"""Write configuration to the control file."""
# Ensure directory exists
self.control_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.control_file, 'w') as f:
json.dump(config, f, indent=2)
print(f"✓ Configuration written to {self.control_file}")
def enable(self, error_type: str, pattern: str = "every_nth", **kwargs) -> None:
"""Enable error injection with specified parameters."""
config = {
"enabled": True,
"error_type": error_type,
"pattern": pattern,
}
# Add pattern-specific parameters
if pattern == "every_nth":
config["nth"] = kwargs.get("nth", 3)
elif pattern == "random":
config["probability"] = kwargs.get("probability", 0.5)
elif pattern == "burst":
config["burst_count"] = kwargs.get("burst_count", 3)
# Add optional parameters
if "retry_after" in kwargs:
config["retry_after_seconds"] = kwargs["retry_after"]
if "target_models" in kwargs:
config["target_models"] = kwargs["target_models"]
if "message" in kwargs:
config["custom_message"] = kwargs["message"]
self.write_config(config)
print(f"✓ Error injection enabled: {error_type} with {pattern} pattern")
def disable(self) -> None:
"""Disable error injection."""
config = {"enabled": False}
self.write_config(config)
print("✓ Error injection disabled")
def status(self) -> None:
"""Show current configuration status."""
config = self.read_config()
print("=== Goose Error Injection Status ===")
print(f"Control file: {self.control_file}")
print()
if not config.get("enabled", False):
print("Status: DISABLED")
else:
print("Status: ENABLED")
print(f"Error type: {config.get('error_type', 'unknown')}")
print(f"Pattern: {config.get('pattern', 'unknown')}")
# Show pattern-specific parameters
if config.get("pattern") == "every_nth":
print(f" Every: {config.get('nth', 3)} calls")
elif config.get("pattern") == "random":
print(f" Probability: {config.get('probability', 0.5):.1%}")
elif config.get("pattern") == "burst":
print(f" Burst count: {config.get('burst_count', 3)}")
# Show optional parameters
if "retry_after_seconds" in config:
print(f"Retry after: {config['retry_after_seconds']} seconds")
if "target_models" in config:
print(f"Target models: {', '.join(config['target_models'])}")
if "custom_message" in config:
print(f"Custom message: {config['custom_message']}")
print("\nRaw configuration:")
print(json.dumps(config, indent=2))
def watch(self, interval: float = 1.0) -> None:
"""Watch the control file for changes."""
print(f"Watching {self.control_file} for changes (Ctrl+C to stop)...")
last_config = None
try:
while True:
config = self.read_config()
if config != last_config:
print(f"\n[{time.strftime('%H:%M:%S')}] Configuration changed:")
print(json.dumps(config, indent=2))
last_config = config
time.sleep(interval)
except KeyboardInterrupt:
print("\nStopped watching")
def main():
parser = argparse.ArgumentParser(
description="Control error injection in goose error_proxy provider",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"--control-file", "-f",
default=os.environ.get("ERROR_PROXY_CONTROL_FILE", DEFAULT_CONTROL_FILE),
help=f"Path to control file (default: {DEFAULT_CONTROL_FILE})"
)
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Enable command
enable_parser = subparsers.add_parser("enable", help="Enable error injection")
enable_parser.add_argument(
"error_type",
choices=["rate_limit", "context_exceeded", "server_error", "auth_error", "timeout"],
help="Type of error to inject"
)
enable_parser.add_argument(
"--pattern", "-p",
choices=["every_nth", "random", "burst", "continuous", "once"],
default="every_nth",
help="Pattern for error injection (default: every_nth)"
)
enable_parser.add_argument(
"--nth", "-n",
type=int,
default=3,
help="For every_nth pattern: inject error every N calls (default: 3)"
)
enable_parser.add_argument(
"--probability", "-r",
type=float,
default=0.5,
help="For random pattern: probability of error 0.0-1.0 (default: 0.5)"
)
enable_parser.add_argument(
"--burst-count", "-b",
type=int,
default=3,
help="For burst pattern: number of consecutive errors (default: 3)"
)
enable_parser.add_argument(
"--retry-after",
type=int,
help="For rate_limit errors: retry after N seconds"
)
enable_parser.add_argument(
"--target-models",
nargs="+",
help="Only inject errors for specific models"
)
enable_parser.add_argument(
"--message", "-m",
help="Custom error message"
)
# Disable command
subparsers.add_parser("disable", help="Disable error injection")
# Status command
subparsers.add_parser("status", help="Show current status")
# Watch command
watch_parser = subparsers.add_parser("watch", help="Watch control file for changes")
watch_parser.add_argument(
"--interval", "-i",
type=float,
default=1.0,
help="Check interval in seconds (default: 1.0)"
)
# Quick presets
preset_parser = subparsers.add_parser("preset", help="Use a preset configuration")
preset_parser.add_argument(
"preset_name",
choices=["flaky", "overloaded", "broken", "slow"],
help="Preset configuration to use"
)
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
controller = ErrorController(args.control_file)
if args.command == "enable":
kwargs = {}
if args.pattern == "every_nth":
kwargs["nth"] = args.nth
elif args.pattern == "random":
kwargs["probability"] = args.probability
elif args.pattern == "burst":
kwargs["burst_count"] = args.burst_count
if args.retry_after:
kwargs["retry_after"] = args.retry_after
if args.target_models:
kwargs["target_models"] = args.target_models
if args.message:
kwargs["message"] = args.message
controller.enable(args.error_type, args.pattern, **kwargs)
elif args.command == "disable":
controller.disable()
elif args.command == "status":
controller.status()
elif args.command == "watch":
controller.watch(args.interval)
elif args.command == "preset":
presets = {
"flaky": {
"error_type": "server_error",
"pattern": "random",
"probability": 0.2,
"message": "Simulating flaky service"
},
"overloaded": {
"error_type": "rate_limit",
"pattern": "every_nth",
"nth": 5,
"retry_after": 30,
"message": "Simulating overloaded API"
},
"broken": {
"error_type": "server_error",
"pattern": "continuous",
"message": "Simulating broken service"
},
"slow": {
"error_type": "timeout",
"pattern": "random",
"probability": 0.3,
"message": "Simulating slow/timeout responses"
}
}
preset = presets[args.preset_name]
controller.enable(**preset)
print(f"Applied preset: {args.preset_name}")
return 0
if __name__ == "__main__":
sys.exit(main())