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

590 lines
19 KiB
Python

"""
Comprehensive Caching Example for Instructor
===========================================
This example demonstrates various caching strategies for LLM applications:
1. functools.cache - Simple in-memory caching
2. diskcache - Persistent disk-based caching
3. Redis - Distributed caching
4. Performance benchmarks and cost analysis
5. Advanced patterns: hierarchical caching, monitoring, schema invalidation
Run this example to see real-world performance improvements and cost savings.
"""
import asyncio
import functools
import hashlib
import inspect
import json
import logging
import time
from collections import defaultdict
from typing import Any, Callable, Optional, TypeVar
import instructor
from openai import AsyncOpenAI, OpenAI
from pydantic import BaseModel, Field
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize clients
client = instructor.from_openai(OpenAI())
aclient = instructor.from_openai(AsyncOpenAI())
# Test data
TEST_QUERIES = [
"Extract: Jason is 25 years old and works as a software engineer",
"Extract: Sarah is 30 years old and is a data scientist",
"Extract: Mike is 28 years old and works in marketing",
"Extract: Lisa is 32 years old and is a product manager",
"Extract: Jason is 25 years old and works as a software engineer", # Duplicate for cache hit
]
F = TypeVar("F", bound=Callable[..., Any])
class UserDetail(BaseModel):
"""Enhanced user model with more fields for testing"""
name: str = Field(description="User's full name")
age: int = Field(description="User's age", ge=0, le=150)
occupation: Optional[str] = Field(None, description="User's job title")
class CacheMetrics:
"""Production-ready cache monitoring"""
def __init__(self):
self.hits = 0
self.misses = 0
self.total_time_saved = 0.0
self.error_count = 0
self.hit_rate_by_function: dict[str, dict[str, int]] = defaultdict(
lambda: {"hits": 0, "misses": 0}
)
def record_hit(self, func_name: str, time_saved: float):
self.hits += 1
self.total_time_saved += time_saved
self.hit_rate_by_function[func_name]["hits"] += 1
logger.debug(f"Cache HIT for {func_name}, saved {time_saved:.3f}s")
def record_miss(self, func_name: str):
self.misses += 1
self.hit_rate_by_function[func_name]["misses"] += 1
logger.debug(f"Cache MISS for {func_name}")
def record_error(self, func_name: str, error: str):
self.error_count += 1
logger.warning(f"Cache ERROR in {func_name}: {error}")
@property
def hit_rate(self) -> float:
total = self.hits + self.misses
return self.hits / total if total > 0 else 0.0
def get_stats(self) -> dict[str, Any]:
return {
"hit_rate": f"{self.hit_rate:.2%}",
"total_hits": self.hits,
"total_misses": self.misses,
"error_count": self.error_count,
"time_saved_seconds": f"{self.total_time_saved:.3f}",
"function_stats": dict(self.hit_rate_by_function),
}
def reset(self):
"""Reset all metrics for new test runs"""
self.hits = 0
self.misses = 0
self.total_time_saved = 0.0
self.error_count = 0
self.hit_rate_by_function.clear()
# Global metrics instance
metrics = CacheMetrics()
def smart_cache_key(
func_name: str, args: tuple, kwargs: dict, model_class: type
) -> str:
"""Generate cache key with schema versioning for automatic invalidation"""
# Include model schema in cache key for automatic invalidation
schema_hash = hashlib.md5(
json.dumps(model_class.model_json_schema(), sort_keys=True).encode()
).hexdigest()[:8]
args_hash = hashlib.md5(str((args, kwargs)).encode()).hexdigest()[:8]
return f"{func_name}:{schema_hash}:{args_hash}"
# 1. Simple functools.cache implementation
@functools.lru_cache(maxsize=1000)
def extract_functools(data: str) -> UserDetail:
"""Simple in-memory caching with functools.lru_cache"""
start_time = time.perf_counter()
result = client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# This won't be called on cache hits, so we track metrics differently
return result
def monitored_functools_cache(func: F) -> F:
"""functools.cache with monitoring"""
cached_func = functools.lru_cache(maxsize=1000)(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Check if we'll get a cache hit by calling cache_info
info_before = cached_func.cache_info()
start_time = time.perf_counter()
result = cached_func(*args, **kwargs)
execution_time = time.perf_counter() - start_time
info_after = cached_func.cache_info()
if info_after.hits > info_before.hits:
# We got a cache hit
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
else:
# Cache miss
metrics.record_miss(func.__name__)
return result
# Preserve cache_info method
wrapper.cache_info = cached_func.cache_info
wrapper.cache_clear = cached_func.cache_clear
return wrapper
@monitored_functools_cache
def extract_functools_monitored(data: str) -> UserDetail:
"""functools.cache with monitoring"""
return client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# 2. Enhanced diskcache implementation
def create_diskcache_decorator(
cache_dir: str = "./cache_directory", ttl: Optional[int] = None
):
"""Factory for diskcache decorator with enhanced features"""
try:
import diskcache
cache = diskcache.Cache(cache_dir)
except ImportError:
logger.warning("diskcache not available, skipping disk cache example")
return lambda func: func
def decorator(func: F) -> F:
return_type = inspect.signature(func).return_annotation
if not (inspect.isclass(return_type) and issubclass(return_type, BaseModel)):
raise ValueError("The return type must be a Pydantic model")
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Generate smart cache key with schema versioning
key = smart_cache_key(func.__name__, args, kwargs, return_type)
try:
# Check if the result is already cached
if (cached := cache.get(key)) is not None:
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
return return_type.model_validate_json(cached)
metrics.record_miss(func.__name__)
except Exception as e:
metrics.record_error(func.__name__, str(e))
logger.warning(f"Cache read error: {e}")
# Call the function and cache its result
result = func(*args, **kwargs)
try:
serialized_result = result.model_dump_json()
if ttl:
cache.set(key, serialized_result, expire=ttl)
else:
cache.set(key, serialized_result)
except Exception as e:
metrics.record_error(func.__name__, str(e))
logger.warning(f"Cache write error: {e}")
return result
return wrapper
return decorator
@create_diskcache_decorator(ttl=3600) # 1 hour TTL
def extract_diskcache(data: str) -> UserDetail:
"""Persistent disk-based caching with TTL"""
return client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# 3. Enhanced Redis implementation (with fallback)
def create_redis_decorator(
redis_url: str = "redis://localhost:6379",
ttl: int = 3600,
prefix: str = "instructor",
):
"""Factory for Redis decorator with production features"""
try:
import redis
cache = redis.from_url(redis_url, decode_responses=True)
# Test connection
cache.ping()
logger.info("Connected to Redis successfully")
except ImportError as e:
logger.warning(f"Redis not available (ImportError: {e}), using fallback")
return lambda func: func
except Exception as e: # Covers redis.RedisError and other connection issues
logger.warning(f"Redis not available ({e}), using fallback")
return lambda func: func
def decorator(func: F) -> F:
return_type = inspect.signature(func).return_annotation
if not (inspect.isclass(return_type) and issubclass(return_type, BaseModel)):
raise ValueError("The return type must be a Pydantic model")
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key with schema versioning
schema_hash = hashlib.md5(
json.dumps(return_type.model_json_schema(), sort_keys=True).encode()
).hexdigest()[:8]
key = f"{prefix}:{func.__name__}:{schema_hash}:{functools._make_key(args, kwargs, typed=False)}"
try:
# Check if the result is already cached
if (cached := cache.get(key)) is not None:
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
logger.debug(f"Cache hit for key: {key}")
return return_type.model_validate_json(cached)
metrics.record_miss(func.__name__)
logger.debug(f"Cache miss for key: {key}")
except redis.RedisError as e:
metrics.record_error(func.__name__, str(e))
logger.warning(f"Redis read error: {e}")
# Call the function and cache its result
result = func(*args, **kwargs)
try:
serialized_result = result.model_dump_json()
cache.setex(key, ttl, serialized_result)
logger.debug(f"Cached result for key: {key}")
except redis.RedisError as e:
metrics.record_error(func.__name__, str(e))
logger.warning(f"Redis write error: {e}")
return result
return wrapper
return decorator
@create_redis_decorator(ttl=3600)
def extract_redis(data: str) -> UserDetail:
"""Distributed Redis caching with error handling"""
return client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# 4. No cache baseline for comparison
def extract_no_cache(data: str) -> UserDetail:
"""Baseline function without caching"""
metrics.record_miss("extract_no_cache")
return client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# 5. Hierarchical caching example
@functools.lru_cache(maxsize=50) # L1: Fast in-memory
def extract_l1(data: str) -> UserDetail:
return extract_l2(data)
@create_diskcache_decorator() # L2: Persistent disk
def extract_l2(data: str) -> UserDetail:
return extract_l3(data)
@create_redis_decorator() # L3: Shared distributed
def extract_l3(data: str) -> UserDetail:
return client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
def benchmark_caching_strategy(
func: Callable, name: str, queries: list[str]
) -> dict[str, Any]:
"""Benchmark a specific caching strategy"""
logger.info(f"\n=== Benchmarking {name} ===")
# Reset metrics for this test
metrics.reset()
times = []
results = []
for i, query in enumerate(queries):
start_time = time.perf_counter()
try:
result = func(query)
execution_time = time.perf_counter() - start_time
times.append(execution_time)
results.append(result)
logger.info(
f"Query {i + 1}: {execution_time:.3f}s - {result.name}, {result.age}, {result.occupation}"
)
except Exception as e:
logger.error(f"Error in {name}: {e}")
times.append(float("inf"))
results.append(None)
# Calculate statistics
valid_times = [t for t in times if t != float("inf")]
if valid_times:
avg_time = sum(valid_times) / len(valid_times)
total_time = sum(valid_times)
fastest_time = min(valid_times)
slowest_time = max(valid_times)
else:
avg_time = total_time = fastest_time = slowest_time = 0
stats = {
"name": name,
"total_time": total_time,
"avg_time": avg_time,
"fastest_time": fastest_time,
"slowest_time": slowest_time,
"cache_metrics": metrics.get_stats(),
"success_rate": len(valid_times) / len(queries),
}
logger.info(f"Total time: {total_time:.3f}s")
logger.info(f"Average time: {avg_time:.3f}s")
logger.info(f"Cache hit rate: {metrics.hit_rate:.2%}")
return stats
def calculate_cost_savings(baseline_stats: dict, cached_stats: dict) -> dict[str, Any]:
"""Calculate cost savings from caching"""
baseline_time = baseline_stats["total_time"]
cached_time = cached_stats["total_time"]
# Assume $0.002 per API call (rough average)
cost_per_call = 0.002
num_queries = len(TEST_QUERIES)
# Without caching: every call costs money
cost_without_cache = num_queries * cost_per_call
# With caching: only cache misses cost money
cache_misses = cached_stats["cache_metrics"]["total_misses"]
cost_with_cache = cache_misses * cost_per_call
savings = cost_without_cache - cost_with_cache
savings_percent = (
(savings / cost_without_cache) * 100 if cost_without_cache > 0 else 0
)
time_saved = baseline_time - cached_time
time_savings_percent = (
(time_saved / baseline_time) * 100 if baseline_time > 0 else 0
)
return {
"cost_without_cache": cost_without_cache,
"cost_with_cache": cost_with_cache,
"cost_savings": savings,
"cost_savings_percent": savings_percent,
"time_saved": time_saved,
"time_savings_percent": time_savings_percent,
"speed_improvement": (
baseline_time / cached_time if cached_time > 0 else float("inf")
),
}
async def run_async_example():
"""Demonstrate async caching patterns"""
logger.info("\n=== Async Caching Example ===")
# Simple async function with metrics
async def extract_async(data: str) -> UserDetail:
metrics.record_miss("extract_async")
return await aclient.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": data},
],
)
# Run concurrent requests
start_time = time.perf_counter()
tasks = [
extract_async(query) for query in TEST_QUERIES[:3]
] # First 3 to save costs
results = await asyncio.gather(*tasks)
total_time = time.perf_counter() - start_time
logger.info(f"Async processing time: {total_time:.3f}s")
for i, result in enumerate(results):
logger.info(f"Result {i + 1}: {result.name}, {result.age}, {result.occupation}")
def demonstrate_schema_invalidation():
"""Show how cache keys change when model schema changes"""
logger.info("\n=== Schema-Based Cache Invalidation ===")
# Original model
class OriginalUser(BaseModel):
name: str
age: int
# Modified model (different schema)
class ModifiedUser(BaseModel):
name: str
age: int
email: Optional[str] = None # New field
# Generate cache keys for same function args but different models
args = ("test data",)
kwargs = {}
key1 = smart_cache_key("test_func", args, kwargs, OriginalUser)
key2 = smart_cache_key("test_func", args, kwargs, ModifiedUser)
logger.info(f"Original model cache key: {key1}")
logger.info(f"Modified model cache key: {key2}")
logger.info(f"Keys are different: {key1 != key2}")
logger.info("This ensures cache invalidation when model schemas change!")
def main():
"""Run comprehensive caching demonstration"""
logger.info("🚀 Starting Comprehensive Caching Demonstration")
logger.info("=" * 60)
# Run benchmarks for each strategy
strategies = [
(extract_no_cache, "No Cache (Baseline)"),
(extract_functools_monitored, "functools.lru_cache"),
(extract_diskcache, "diskcache"),
(extract_redis, "Redis"),
(extract_l1, "Hierarchical (L1→L2→L3)"),
]
all_stats = {}
for func, name in strategies:
try:
stats = benchmark_caching_strategy(func, name, TEST_QUERIES)
all_stats[name] = stats
except Exception as e:
logger.error(f"Failed to benchmark {name}: {e}")
continue
# Print summary comparison
logger.info("\n" + "=" * 60)
logger.info("📊 PERFORMANCE COMPARISON SUMMARY")
logger.info("=" * 60)
baseline_stats = all_stats.get("No Cache (Baseline)")
if baseline_stats:
for name, stats in all_stats.items():
if name == "No Cache (Baseline)":
continue
logger.info(f"\n{name}:")
logger.info(f" Total time: {stats['total_time']:.3f}s")
logger.info(f" Cache hit rate: {stats['cache_metrics']['hit_rate']}")
# Calculate savings
savings = calculate_cost_savings(baseline_stats, stats)
logger.info(f" Speed improvement: {savings['speed_improvement']:.1f}x")
logger.info(
f" Time saved: {savings['time_saved']:.3f}s ({savings['time_savings_percent']:.1f}%)"
)
logger.info(
f" Cost savings: ${savings['cost_savings']:.4f} ({savings['cost_savings_percent']:.1f}%)"
)
# Additional demonstrations
demonstrate_schema_invalidation()
# Run async example
asyncio.run(run_async_example())
# Print cache info for functools
logger.info(
f"\nfunctools.lru_cache info: {extract_functools_monitored.cache_info()}"
)
logger.info("\n" + "=" * 60)
logger.info("✅ Caching demonstration completed!")
logger.info("💡 Key takeaways:")
logger.info(" - Caching can provide 10x-1000x speed improvements")
logger.info(" - Choose the right strategy based on your needs:")
logger.info(" • functools.cache: Development, single process")
logger.info(" • diskcache: Persistence, moderate performance")
logger.info(" • Redis: Distributed systems, high performance")
logger.info(" • Hierarchical: Best of all worlds")
logger.info(" - Smart cache keys prevent stale data")
logger.info(" - Monitoring helps optimize cache performance")
if __name__ == "__main__":
main()