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
328 lines
8.3 KiB
Markdown
328 lines
8.3 KiB
Markdown
---
|
|
title: "Retry Logic with Tenacity"
|
|
description: "Learn how to implement retry logic with Tenacity for LLM applications, including exponential backoff, conditional retries, and error handling."
|
|
---
|
|
|
|
# Retry Logic with Tenacity
|
|
|
|
Tenacity is a Python library for adding retry logic to your applications. Combined with Instructor, it helps handle API failures, rate limits, and validation errors.
|
|
|
|
## Basic Retry with Exponential Backoff
|
|
|
|
The most common pattern uses exponential backoff to delay retries:
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
email: str
|
|
|
|
|
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
|
def extract_user_info(text: str) -> UserInfo:
|
|
"""Extract user information with retry logic."""
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": f"Extract user info: {text}"}],
|
|
)
|
|
|
|
|
|
try:
|
|
user = extract_user_info("John is 30 years old with email john@example.com")
|
|
print(f"Success: {user.name}, {user.age}, {user.email}")
|
|
#> Success: John, 30, john@example.com
|
|
except Exception as e:
|
|
print(f"Failed after retries: {e}")
|
|
```
|
|
|
|
## Error-Specific Retries
|
|
|
|
Retry only on specific error types for better control:
|
|
|
|
```python
|
|
import instructor
|
|
from openai import APIError, RateLimitError
|
|
from pydantic import BaseModel, ValidationError
|
|
from tenacity import (
|
|
retry,
|
|
retry_if_exception_type,
|
|
stop_after_attempt,
|
|
wait_exponential,
|
|
)
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
email: str
|
|
|
|
|
|
# Retry on API errors with longer delays
|
|
@retry(
|
|
retry=retry_if_exception_type((RateLimitError, APIError)),
|
|
stop=stop_after_attempt(5),
|
|
wait=wait_exponential(multiplier=2, min=1, max=60),
|
|
)
|
|
def handle_api_errors(text: str) -> UserInfo:
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": text}],
|
|
)
|
|
|
|
|
|
# Retry on validation errors with shorter delays
|
|
@retry(
|
|
retry=retry_if_exception_type(ValidationError),
|
|
stop=stop_after_attempt(3),
|
|
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
)
|
|
def handle_validation_errors(text: str) -> UserInfo:
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": text}],
|
|
)
|
|
```
|
|
|
|
## Custom Retry Conditions
|
|
|
|
Retry based on the result content rather than exceptions:
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
from tenacity import retry, retry_if_result, stop_after_attempt
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
email: str
|
|
|
|
|
|
def should_retry(result: UserInfo) -> bool:
|
|
"""Retry if the result doesn't meet quality criteria."""
|
|
return result.age < 0 or result.age > 150 or not result.email
|
|
|
|
|
|
@retry(retry=retry_if_result(should_retry), stop=stop_after_attempt(3))
|
|
def extract_valid_user(text: str) -> UserInfo:
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": text}],
|
|
)
|
|
```
|
|
|
|
## Context-Based Validation with Retries
|
|
|
|
Use the `context` parameter to pass runtime data to validators:
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel, ValidationInfo, field_validator, ValidationError
|
|
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class Citation(BaseModel):
|
|
"""A claim with a supporting quote from source text."""
|
|
|
|
claim: str
|
|
quote: str
|
|
|
|
@field_validator('quote')
|
|
@classmethod
|
|
def verify_quote_exists(cls, v: str, info: ValidationInfo):
|
|
context = info.context
|
|
if context:
|
|
source_text = context.get('source_text', '')
|
|
if v not in source_text:
|
|
raise ValueError(f"Quote '{v}' not found in source text.")
|
|
return v
|
|
|
|
|
|
@retry(
|
|
retry=retry_if_exception_type(ValidationError),
|
|
stop=stop_after_attempt(3),
|
|
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
)
|
|
def extract_citation(claim: str, source_text: str) -> Citation:
|
|
return client.create(
|
|
response_model=Citation,
|
|
messages=[
|
|
{
|
|
"role": "system",
|
|
"content": "Extract the claim and find an exact quote from the source.",
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "Source: {{ source_text }}\n\nClaim: {{ claim }}",
|
|
},
|
|
],
|
|
context={"source_text": source_text, "claim": claim},
|
|
)
|
|
|
|
|
|
source = "The Eiffel Tower was completed in 1889 and stands 330 meters tall."
|
|
citation = extract_citation("The tower is over 300 meters", source)
|
|
print(f"Quote: {citation.quote}")
|
|
```
|
|
|
|
## Logging and Monitoring
|
|
|
|
Add logging to track retry attempts:
|
|
|
|
```python
|
|
import logging
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_exponential
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
email: str
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
@retry(
|
|
stop=stop_after_attempt(3),
|
|
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
before=before_log(logger, logging.INFO),
|
|
after=after_log(logger, logging.ERROR),
|
|
)
|
|
def logged_extraction(text: str) -> UserInfo:
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": text}],
|
|
)
|
|
```
|
|
|
|
## Instructor's Built-in Retries
|
|
|
|
Instructor has built-in retry support that works alongside Tenacity:
|
|
|
|
```python
|
|
import instructor
|
|
from instructor import Mode
|
|
from pydantic import BaseModel
|
|
from tenacity import retry, stop_after_attempt
|
|
|
|
client = instructor.from_provider(
|
|
"openai/gpt-4.1-mini",
|
|
mode=Mode.JSON,
|
|
max_retries=3,
|
|
retry_delay=1,
|
|
)
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
email: str
|
|
|
|
|
|
# Combine Instructor and Tenacity retries for additional resilience
|
|
@retry(stop=stop_after_attempt(2))
|
|
def double_retry_extraction(text: str) -> UserInfo:
|
|
return client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": text}],
|
|
)
|
|
```
|
|
|
|
## Failed Attempts Tracking
|
|
|
|
When retries fail, Instructor provides detailed failure history:
|
|
|
|
```python
|
|
import instructor
|
|
from instructor.core.exceptions import InstructorRetryException
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class UserInfo(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
@field_validator('age')
|
|
@classmethod
|
|
def validate_age(cls, v):
|
|
if v < 0 or v > 150:
|
|
raise ValueError(f"Age {v} is invalid")
|
|
return v
|
|
|
|
|
|
try:
|
|
result = client.create(
|
|
response_model=UserInfo,
|
|
messages=[{"role": "user", "content": "Extract: John is -5 years old"}],
|
|
max_retries=3,
|
|
)
|
|
except InstructorRetryException as e:
|
|
print(f"Failed after {e.n_attempts} attempts")
|
|
for attempt in e.failed_attempts:
|
|
print(f"Attempt {attempt.attempt_number}: {attempt.exception}")
|
|
```
|
|
|
|
Failed attempts are automatically propagated to reask handlers, enabling contextual error messages and progressive corrections.
|
|
|
|
## Best Practices
|
|
|
|
### Choose Appropriate Strategies
|
|
|
|
| Error Type | Attempts | Min Delay | Max Delay |
|
|
|------------|----------|-----------|-----------|
|
|
| Rate limits | 5 | 1s | 60-120s |
|
|
| Validation errors | 2-3 | 1s | 10s |
|
|
| Network errors | 4 | 2s | 30s |
|
|
|
|
### Always Set Stop Conditions
|
|
|
|
```python
|
|
from tenacity import retry, stop_after_attempt
|
|
|
|
# Good: bounded retries
|
|
@retry(stop=stop_after_attempt(3))
|
|
def bounded_retry():
|
|
pass
|
|
|
|
# Bad: could retry forever
|
|
@retry() # Don't do this!
|
|
def unbounded_retry():
|
|
pass
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
**Infinite retries**: Always set `stop_after_attempt()` or `stop_after_delay()`.
|
|
|
|
**Too many retries**: Use `retry_if_exception_type()` to retry only on specific errors.
|
|
|
|
**Still hitting rate limits**: Increase max delay and use `wait_exponential()` with higher multipliers.
|
|
|
|
## Related Resources
|
|
|
|
- [Tenacity Documentation](https://tenacity.readthedocs.io/)
|
|
- [Error Handling](./error_handling.md)
|
|
- [Validation](./validation.md)
|