Files
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

522 lines
17 KiB
Python

"""
Tenacity Retry Logic Benchmarks with Instructor
This script demonstrates and benchmarks different retry patterns for LLM processing:
- Basic retry with exponential backoff
- Conditional retries for specific errors
- Validation error retries
- Custom retry conditions
- Rate limit handling
- Network error recovery
- Logging and monitoring
- Circuit breaker patterns
Run this script to see retry behavior and verify all code examples work.
"""
import instructor
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
retry_if_result,
before_log,
after_log,
wait_random_exponential,
)
from pydantic import BaseModel, field_validator, ValidationError
from openai import OpenAI, RateLimitError, APIError
import time
import logging
import random
import os
from functools import lru_cache
import httpx
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Set up the client with Instructor
client = instructor.from_openai(OpenAI())
class UserInfo(BaseModel):
name: str
age: int
email: str
@field_validator("age")
@classmethod
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError(f"Age {v} is invalid")
return v
@field_validator("email")
@classmethod
def validate_email(cls, v):
if "@" not in v:
raise ValueError(f"Invalid email: {v}")
return v.lower()
# Sample data for testing
test_texts = [
"John is 30 years old with email john@example.com",
"Sarah is 25 with email sarah@test.com",
"Mike is 35 and his email is mike@demo.org",
"Alice is 28 with email alice@example.com",
"Bob is 32 with email bob@test.com",
]
# Error simulation for testing
class MockError:
def __init__(self):
self.call_count = 0
self.fail_until = 2 # Fail first 2 calls, succeed on 3rd
def maybe_fail(self):
self.call_count += 1
if self.call_count <= self.fail_until:
# Simulate different types of errors
error_type = random.choice(
[ValidationError, RateLimitError, APIError, Exception]
)
if error_type == ValidationError:
raise ValidationError.from_exception_data("UserInfo", [])
elif error_type == RateLimitError:
# Create a simple mock response for RateLimitError
mock_response = httpx.Response(
status_code=429, headers={}, content=b"Rate limit exceeded"
)
raise RateLimitError(
"Rate limit exceeded",
response=mock_response,
body="Rate limit exceeded",
)
elif error_type == APIError:
# Create a simple mock request for APIError
mock_request = httpx.Request(
"POST", "https://api.openai.com/v1/chat/completions"
)
raise APIError(
"API error occurred",
request=mock_request,
body="API error occurred",
)
else:
raise Exception("Generic error occurred")
mock_error = MockError()
def extract_user_info_with_mock_errors(text: str) -> UserInfo:
"""Extract user info with simulated errors for testing."""
if not os.getenv("OPENAI_API_KEY"):
# Simulate errors for testing when no API key
mock_error.maybe_fail()
# Return mock data if no errors
return UserInfo(name="Mock User", age=30, email="mock@example.com")
return client.chat.completions.create(
model="gpt-4o-mini",
response_model=UserInfo,
messages=[{"role": "user", "content": f"Extract user info: {text}"}],
)
# Method 1: Basic Retry with Exponential Backoff
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=5), # Shorter waits for demo
)
def extract_user_info(text: str) -> UserInfo:
"""Extract user information with basic retry logic."""
print(f" Attempting extraction for: {text[:30]}...")
if not os.getenv("OPENAI_API_KEY"):
mock_error.maybe_fail()
return UserInfo(name="Test User", age=25, email="test@example.com")
return client.chat.completions.create(
model="gpt-4o-mini",
response_model=UserInfo,
messages=[{"role": "user", "content": f"Extract user info: {text}"}],
)
# Method 2: Conditional Retries for Specific Errors
@retry(
retry=retry_if_exception_type((RateLimitError, APIError)),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=5),
)
def robust_extraction(text: str) -> UserInfo:
"""Retry only on specific API errors."""
print(f" Robust extraction for: {text[:30]}...")
return extract_user_info_with_mock_errors(text)
# Method 3: Validation Error Retries
@retry(
retry=retry_if_exception_type(ValidationError),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=3),
)
def extract_with_validation(text: str) -> UserInfo:
"""Retry when Pydantic validation fails."""
print(f" Validation retry for: {text[:30]}...")
return extract_user_info_with_mock_errors(text)
# Method 4: Custom Retry Conditions
def should_retry(result: UserInfo) -> bool:
"""Custom retry logic based on result content."""
# Retry if age is invalid or email is missing
return result.age < 18 or result.age > 100 or not result.email
@retry(
retry=retry_if_result(should_retry),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=3),
)
def extract_valid_user(text: str) -> UserInfo:
"""Retry based on result validation."""
print(f" Custom retry for: {text[:30]}...")
# Simulate returning invalid data first time
if not hasattr(extract_valid_user, "call_count"):
extract_valid_user.call_count = 0
extract_valid_user.call_count += 1
if extract_valid_user.call_count == 1:
# Return invalid data first time
return UserInfo(name="Invalid User", age=200, email="invalid")
else:
# Return valid data on retry
return UserInfo(name="Valid User", age=30, email="valid@example.com")
# Method 5: Rate Limit Specific Retry
@retry(
retry=retry_if_exception_type(RateLimitError),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=1, max=10),
before_sleep=lambda retry_state: print(
f" Rate limited, waiting... (attempt {retry_state.attempt_number})"
),
)
def rate_limit_safe_extraction(text: str) -> UserInfo:
"""Handle rate limits with longer delays."""
print(f" Rate limit safe for: {text[:30]}...")
return extract_user_info_with_mock_errors(text)
# Method 6: Network Error Retry
@retry(
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
stop=stop_after_attempt(4),
wait=wait_random_exponential(multiplier=1, min=1, max=5),
)
def network_resilient_extraction(text: str) -> UserInfo:
"""Handle network issues with random exponential backoff."""
print(f" Network resilient for: {text[:30]}...")
return extract_user_info_with_mock_errors(text)
# Method 7: Logging and Monitoring
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=5),
before=before_log(logger, logging.INFO),
after=after_log(logger, logging.ERROR),
)
def logged_extraction(text: str) -> UserInfo:
"""Extract with comprehensive logging."""
print(f" Logged extraction for: {text[:30]}...")
return extract_user_info_with_mock_errors(text)
# Method 8: Circuit Breaker Pattern
@lru_cache(maxsize=1)
def get_client():
"""Cache the client to avoid repeated initialization."""
return instructor.from_openai(OpenAI())
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=5))
def circuit_breaker_extraction(text: str) -> UserInfo:
"""Extract with circuit breaker pattern."""
print(f" Circuit breaker for: {text[:30]}...")
client = get_client()
return extract_user_info_with_mock_errors(text)
# Method 9: Performance Monitoring
@retry(stop=stop_after_attempt(3))
def monitored_extraction(text: str) -> UserInfo:
"""Extract with performance monitoring."""
start_time = time.time()
try:
print(f" Monitored extraction for: {text[:30]}...")
result = extract_user_info_with_mock_errors(text)
end_time = time.time()
print(f" Extraction took {end_time - start_time:.2f} seconds")
return result
except Exception as e:
end_time = time.time()
print(f" Extraction failed after {end_time - start_time:.2f} seconds: {e}")
raise
def benchmark_retry_methods():
"""Test all retry methods and measure their behavior."""
print("=== Python Tenacity Retry Logic with Instructor Benchmarks ===\n")
if not os.getenv("OPENAI_API_KEY"):
print("⚠️ OPENAI_API_KEY not set. Using mock responses for demonstration.\n")
# Test different retry strategies
strategies = [
("Basic Retry", extract_user_info),
("Conditional Retry", robust_extraction),
("Validation Retry", extract_with_validation),
("Custom Retry", extract_valid_user),
("Rate Limit Retry", rate_limit_safe_extraction),
("Network Retry", network_resilient_extraction),
("Logged Retry", logged_extraction),
("Circuit Breaker", circuit_breaker_extraction),
("Monitored Retry", monitored_extraction),
]
results = {}
test_text = test_texts[0] # Use first text for all tests
for name, strategy in strategies:
print(f"\n{'=' * 60}")
print(f"Testing: {name}")
print("=" * 60)
# Reset mock error for each test
global mock_error
mock_error = MockError()
# Reset call count for custom retry
if hasattr(extract_valid_user, "call_count"):
delattr(extract_valid_user, "call_count")
start_time = time.time()
try:
user = strategy(test_text)
end_time = time.time()
duration = end_time - start_time
results[name] = {
"success": True,
"duration": duration,
"user": user,
"attempts": getattr(mock_error, "call_count", 1),
}
print(f"✓ Success: {user.name} ({duration:.2f}s)")
print(f" Age: {user.age}, Email: {user.email}")
print(f" Attempts made: {results[name]['attempts']}")
except Exception as e:
end_time = time.time()
duration = end_time - start_time
results[name] = {
"success": False,
"duration": duration,
"error": str(e),
"attempts": getattr(mock_error, "call_count", 1),
}
print(f"✗ Failed: {e} ({duration:.2f}s)")
print(f" Attempts made: {results[name]['attempts']}")
# Print summary table
print(f"\n{'=' * 80}")
print("RETRY STRATEGY SUMMARY")
print("=" * 80)
print(
f"{'Strategy':<20} {'Status':<10} {'Time (s)':<10} {'Attempts':<10} {'Result'}"
)
print("-" * 80)
for name, result in results.items():
status = "✓ Success" if result["success"] else "✗ Failed"
attempts = result["attempts"]
if result["success"]:
result_text = f"{result['user'].name}"
else:
result_text = "Failed"
print(
f"{name:<20} {status:<10} {result['duration']:<10.2f} {attempts:<10} {result_text}"
)
# Show retry efficiency
print(f"\nRetry Efficiency Analysis:")
successful_strategies = {k: v for k, v in results.items() if v["success"]}
if successful_strategies:
avg_attempts = sum(r["attempts"] for r in successful_strategies.values()) / len(
successful_strategies
)
avg_duration = sum(r["duration"] for r in successful_strategies.values()) / len(
successful_strategies
)
print(f" Average attempts: {avg_attempts:.1f}")
print(f" Average duration: {avg_duration:.2f}s")
# Find most efficient strategy
most_efficient = min(
successful_strategies.items(),
key=lambda x: x[1]["attempts"] * x[1]["duration"],
)
print(
f" Most efficient: {most_efficient[0]} ({most_efficient[1]['attempts']} attempts, {most_efficient[1]['duration']:.2f}s)"
)
def test_batch_processing():
"""Test batch processing with retries."""
print(f"\n{'=' * 60}")
print("Batch Processing Test")
print("=" * 60)
@retry(stop=stop_after_attempt(2))
def process_batch(texts: list[str]) -> list[UserInfo]:
"""Process multiple texts with retry logic."""
results = []
for text in texts:
try:
# Reset mock error for each item
global mock_error
mock_error = MockError()
result = extract_user_info_with_mock_errors(text)
results.append(result)
print(f" ✓ Processed: {result.name}")
except Exception as e:
print(f" ✗ Failed to process: {text[:30]}... - {e}")
continue
return results
start_time = time.time()
try:
results = process_batch(test_texts[:3]) # Process first 3 texts
end_time = time.time()
duration = end_time - start_time
print(f"\nBatch processing completed:")
print(f" Successfully processed: {len(results)}/{len(test_texts[:3])} items")
print(f" Total time: {duration:.2f} seconds")
print(f" Average time per item: {duration / len(test_texts[:3]):.2f} seconds")
except Exception as e:
print(f"Batch processing failed: {e}")
def demonstrate_error_types():
"""Demonstrate handling different error types."""
print(f"\n{'=' * 60}")
print("Error Type Demonstration")
print("=" * 60)
# Simulate different error scenarios
error_scenarios = [
("Validation Error", ValidationError),
("Rate Limit Error", RateLimitError),
("API Error", APIError),
("Generic Error", Exception),
]
for error_name, error_type in error_scenarios:
print(f"\nTesting {error_name}:")
def create_error_handler(error_type):
@retry(
retry=retry_if_exception_type(error_type),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=0.5, max=2),
)
def handle_specific_error():
# Simulate the specific error type
if error_type == ValidationError:
raise ValidationError.from_exception_data("UserInfo", [])
elif error_type == RateLimitError:
# Create a simple mock response for RateLimitError
mock_response = httpx.Response(
status_code=429, headers={}, content=b"Rate limit exceeded"
)
raise RateLimitError(
"Rate limit exceeded",
response=mock_response,
body="Rate limit exceeded",
)
elif error_type == APIError:
# Create a simple mock request for APIError
mock_request = httpx.Request(
"POST", "https://api.openai.com/v1/chat/completions"
)
raise APIError(
"API error occurred",
request=mock_request,
body="API error occurred",
)
else:
raise Exception("Generic error occurred")
return handle_specific_error
error_handler = create_error_handler(error_type)
try:
error_handler()
except Exception as e:
print(f" Expected failure: {type(e).__name__}: {e}")
def main():
"""Main function to run all benchmarks and demonstrations."""
try:
benchmark_retry_methods()
test_batch_processing()
demonstrate_error_types()
print(f"\n{'=' * 80}")
print("🎉 All tenacity retry patterns demonstrated successfully!")
print("💡 Key takeaways:")
print(" - Different retry strategies serve different purposes")
print(" - Exponential backoff prevents overwhelming APIs")
print(" - Conditional retries optimize for specific error types")
print(" - Monitoring helps debug and optimize retry behavior")
print("=" * 80)
except KeyboardInterrupt:
print("\n⚠️ Interrupted by user")
except Exception as e:
print(f"❌ Error: {e}")
logger.exception("Unexpected error occurred")
if __name__ == "__main__":
print("🚀 Starting tenacity retry benchmarks with Instructor...")
print("💡 This script demonstrates retry patterns with simulated errors")
print("⏱️ Each test includes artificial delays and error scenarios\n")
main()