|
|
|
@ -35,13 +35,84 @@ U = TypeVar("U")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RunnableRetry(RunnableBinding[Input, Output]):
|
|
|
|
|
"""Retry a Runnable if it fails."""
|
|
|
|
|
"""Retry a Runnable if it fails.
|
|
|
|
|
|
|
|
|
|
A RunnableRetry helps can be used to add retry logic to any object
|
|
|
|
|
that subclasses the base Runnable.
|
|
|
|
|
|
|
|
|
|
Such retries are especially useful for network calls that may fail
|
|
|
|
|
due to transient errors.
|
|
|
|
|
|
|
|
|
|
The RunnableRetry is implemented as a RunnableBinding. The easiest
|
|
|
|
|
way to use it is through the `.with_retry()` method on all Runnables.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
Here's an example that uses a RunnableLambda to raise an exception
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
def foo(input) -> None:
|
|
|
|
|
'''Fake function that raises an exception.'''
|
|
|
|
|
raise ValueError("Invoking foo failed. At time {time.time()}")
|
|
|
|
|
|
|
|
|
|
runnable = RunnableLambda(foo)
|
|
|
|
|
|
|
|
|
|
runnable_with_retries = runnable.with_retry(
|
|
|
|
|
retry_exception_types=(ValueError,), # Retry only on ValueError
|
|
|
|
|
wait_exponential_jitter=True, # Add jitter to the exponential backoff
|
|
|
|
|
max_attempt_number=2, # Try twice
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# The method invocation above is equivalent to the longer form below:
|
|
|
|
|
|
|
|
|
|
runnable_with_retries = RunnableRetry(
|
|
|
|
|
bound=runnable,
|
|
|
|
|
retry_exception_types=(ValueError,),
|
|
|
|
|
max_attempt_number=2,
|
|
|
|
|
wait_exponential_jitter=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
This logic can be used to retry any Runnable, including a chain of Runnables,
|
|
|
|
|
but in general it's best practice to keep the scope of the retry as small as
|
|
|
|
|
possible. For example, if you have a chain of Runnables, you should only retry
|
|
|
|
|
the Runnable that is likely to fail, not the entire chain.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
from langchain.chat_models import ChatOpenAI
|
|
|
|
|
from langchain.prompts import PromptTemplate
|
|
|
|
|
|
|
|
|
|
template = PromptTemplate.from_template("tell me a joke about {topic}.")
|
|
|
|
|
model = ChatOpenAI(temperature=0.5)
|
|
|
|
|
|
|
|
|
|
# Good
|
|
|
|
|
chain = template | model.with_retry()
|
|
|
|
|
|
|
|
|
|
# Bad
|
|
|
|
|
chain = template | model
|
|
|
|
|
retryable_chain = chain.with_retry()
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
retry_exception_types: Tuple[Type[BaseException], ...] = (Exception,)
|
|
|
|
|
"""The exception types to retry on. By default all exceptions are retried.
|
|
|
|
|
|
|
|
|
|
In general you should only retry on exceptions that are likely to be
|
|
|
|
|
transient, such as network errors.
|
|
|
|
|
|
|
|
|
|
Good exceptions to retry are all server errors (5xx) and selected client
|
|
|
|
|
errors (4xx) such as 429 Too Many Requests.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
wait_exponential_jitter: bool = True
|
|
|
|
|
"""Whether to add jitter to the exponential backoff."""
|
|
|
|
|
|
|
|
|
|
max_attempt_number: int = 3
|
|
|
|
|
"""The maximum number of attempts to retry the runnable."""
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def _kwargs_retrying(self) -> Dict[str, Any]:
|
|
|
|
|