libs/core/langchain_core/runnables/retry.py PYTHON 380 lines View on github.com → Search inside
1"""`Runnable` that retries a `Runnable` if it fails."""23from typing import (4    TYPE_CHECKING,5    Any,6    TypeVar,7    cast,8)910from tenacity import (11    AsyncRetrying,12    RetryCallState,13    RetryError,14    Retrying,15    retry_if_exception_type,16    stop_after_attempt,17    wait_exponential_jitter,18)19from typing_extensions import TypedDict, override2021from langchain_core.runnables.base import RunnableBindingBase22from langchain_core.runnables.config import RunnableConfig, patch_config23from langchain_core.runnables.utils import Input, Output2425if TYPE_CHECKING:26    from langchain_core.callbacks.manager import (27        AsyncCallbackManagerForChainRun,28        CallbackManagerForChainRun,29    )3031    T = TypeVar("T", CallbackManagerForChainRun, AsyncCallbackManagerForChainRun)32U = TypeVar("U")333435class ExponentialJitterParams(TypedDict, total=False):36    """Parameters for `tenacity.wait_exponential_jitter`."""3738    initial: float39    """Initial wait."""40    max: float41    """Maximum wait."""42    exp_base: float43    """Base for exponential backoff."""44    jitter: float45    """Random additional wait sampled from random.uniform(0, jitter)."""464748class RunnableRetry(RunnableBindingBase[Input, Output]):  # type: ignore[no-redef]49    """Retry a Runnable if it fails.5051    RunnableRetry can be used to add retry logic to any object52    that subclasses the base Runnable.5354    Such retries are especially useful for network calls that may fail55    due to transient errors.5657    The RunnableRetry is implemented as a RunnableBinding. The easiest58    way to use it is through the `.with_retry()` method on all Runnables.5960    Example:61    Here's an example that uses a RunnableLambda to raise an exception6263        ```python64        import time656667        def foo(input) -> None:68            '''Fake function that raises an exception.'''69            raise ValueError(f"Invoking foo failed. At time {time.time()}")707172        runnable = RunnableLambda(foo)7374        runnable_with_retries = runnable.with_retry(75            retry_if_exception_type=(ValueError,),  # Retry only on ValueError76            wait_exponential_jitter=True,  # Add jitter to the exponential backoff77            stop_after_attempt=2,  # Try twice78            exponential_jitter_params={"initial": 2},  # if desired, customize backoff79        )8081        # The method invocation above is equivalent to the longer form below:8283        runnable_with_retries = RunnableRetry(84            bound=runnable,85            retry_exception_types=(ValueError,),86            max_attempt_number=2,87            wait_exponential_jitter=True,88            exponential_jitter_params={"initial": 2},89        )90        ```9192    This logic can be used to retry any Runnable, including a chain of Runnables,93    but in general it's best practice to keep the scope of the retry as small as94    possible. For example, if you have a chain of Runnables, you should only retry95    the Runnable that is likely to fail, not the entire chain.9697    Example:98        ```python99        from langchain_core.chat_models import ChatOpenAI100        from langchain_core.prompts import PromptTemplate101102        template = PromptTemplate.from_template("tell me a joke about {topic}.")103        model = ChatOpenAI(temperature=0.5)104105        # Good106        chain = template | model.with_retry()107108        # Bad109        chain = template | model110        retryable_chain = chain.with_retry()111        ```112    """113114    retry_exception_types: tuple[type[BaseException], ...] = (Exception,)115    """The exception types to retry on. By default all exceptions are retried.116117    In general you should only retry on exceptions that are likely to be118    transient, such as network errors.119120    Good exceptions to retry are all server errors (5xx) and selected client121    errors (4xx) such as 429 Too Many Requests.122    """123124    wait_exponential_jitter: bool = True125    """Whether to add jitter to the exponential backoff."""126127    exponential_jitter_params: ExponentialJitterParams | None = None128    """Parameters for `tenacity.wait_exponential_jitter`. Namely: `initial`,129    `max`, `exp_base`, and `jitter` (all `float` values).130    """131132    max_attempt_number: int = 3133    """The maximum number of attempts to retry the Runnable."""134135    @property136    def _kwargs_retrying(self) -> dict[str, Any]:137        kwargs: dict[str, Any] = {}138139        if self.max_attempt_number:140            kwargs["stop"] = stop_after_attempt(self.max_attempt_number)141142        if self.wait_exponential_jitter:143            kwargs["wait"] = wait_exponential_jitter(144                **(self.exponential_jitter_params or {})145            )146147        if self.retry_exception_types:148            kwargs["retry"] = retry_if_exception_type(self.retry_exception_types)149150        return kwargs151152    def _sync_retrying(self, **kwargs: Any) -> Retrying:153        return Retrying(**self._kwargs_retrying, **kwargs)154155    def _async_retrying(self, **kwargs: Any) -> AsyncRetrying:156        return AsyncRetrying(**self._kwargs_retrying, **kwargs)157158    @staticmethod159    def _patch_config(160        config: RunnableConfig,161        run_manager: "T",162        retry_state: RetryCallState,163    ) -> RunnableConfig:164        attempt = retry_state.attempt_number165        tag = f"retry:attempt:{attempt}" if attempt > 1 else None166        return patch_config(config, callbacks=run_manager.get_child(tag))167168    def _patch_config_list(169        self,170        config: list[RunnableConfig],171        run_manager: list["T"],172        retry_state: RetryCallState,173    ) -> list[RunnableConfig]:174        return [175            self._patch_config(c, rm, retry_state)176            for c, rm in zip(config, run_manager, strict=False)177        ]178179    def _invoke(180        self,181        input_: Input,182        run_manager: "CallbackManagerForChainRun",183        config: RunnableConfig,184        **kwargs: Any,185    ) -> Output:186        for attempt in self._sync_retrying(reraise=True):187            with attempt:188                result = super().invoke(189                    input_,190                    self._patch_config(config, run_manager, attempt.retry_state),191                    **kwargs,192                )193            if attempt.retry_state.outcome and not attempt.retry_state.outcome.failed:194                attempt.retry_state.set_result(result)195        return result196197    @override198    def invoke(199        self, input: Input, config: RunnableConfig | None = None, **kwargs: Any200    ) -> Output:201        return self._call_with_config(self._invoke, input, config, **kwargs)202203    async def _ainvoke(204        self,205        input_: Input,206        run_manager: "AsyncCallbackManagerForChainRun",207        config: RunnableConfig,208        **kwargs: Any,209    ) -> Output:210        async for attempt in self._async_retrying(reraise=True):211            with attempt:212                result = await super().ainvoke(213                    input_,214                    self._patch_config(config, run_manager, attempt.retry_state),215                    **kwargs,216                )217            if attempt.retry_state.outcome and not attempt.retry_state.outcome.failed:218                attempt.retry_state.set_result(result)219        return result220221    @override222    async def ainvoke(223        self, input: Input, config: RunnableConfig | None = None, **kwargs: Any224    ) -> Output:225        return await self._acall_with_config(self._ainvoke, input, config, **kwargs)226227    def _batch(228        self,229        inputs: list[Input],230        run_manager: list["CallbackManagerForChainRun"],231        config: list[RunnableConfig],232        **kwargs: Any,233    ) -> list[Output | Exception]:234        results_map: dict[int, Output] = {}235236        not_set: list[Output] = []237        result = not_set238        try:239            for attempt in self._sync_retrying():240                with attempt:241                    # Retry for inputs that have not yet succeeded242                    # Determine which original indices remain.243                    remaining_indices = [244                        i for i in range(len(inputs)) if i not in results_map245                    ]246                    if not remaining_indices:247                        break248                    pending_inputs = [inputs[i] for i in remaining_indices]249                    pending_configs = [config[i] for i in remaining_indices]250                    pending_run_managers = [run_manager[i] for i in remaining_indices]251                    # Invoke underlying batch only on remaining elements.252                    result = super().batch(253                        pending_inputs,254                        self._patch_config_list(255                            pending_configs, pending_run_managers, attempt.retry_state256                        ),257                        return_exceptions=True,258                        **kwargs,259                    )260                    # Register the results of the inputs that have succeeded, mapping261                    # back to their original indices.262                    first_exception = None263                    for offset, r in enumerate(result):264                        if isinstance(r, Exception):265                            if not first_exception:266                                first_exception = r267                            continue268                        orig_idx = remaining_indices[offset]269                        results_map[orig_idx] = r270                    # If any exception occurred, raise it, to retry the failed ones271                    if first_exception:272                        raise first_exception273                if (274                    attempt.retry_state.outcome275                    and not attempt.retry_state.outcome.failed276                ):277                    attempt.retry_state.set_result(result)278        except RetryError as e:279            if result is not_set:280                result = cast("list[Output]", [e] * len(inputs))281282        outputs: list[Output | Exception] = []283        for idx in range(len(inputs)):284            if idx in results_map:285                outputs.append(results_map[idx])286            else:287                outputs.append(result.pop(0))288        return outputs289290    @override291    def batch(292        self,293        inputs: list[Input],294        config: RunnableConfig | list[RunnableConfig] | None = None,295        *,296        return_exceptions: bool = False,297        **kwargs: Any,298    ) -> list[Output]:299        return self._batch_with_config(300            self._batch, inputs, config, return_exceptions=return_exceptions, **kwargs301        )302303    async def _abatch(304        self,305        inputs: list[Input],306        run_manager: list["AsyncCallbackManagerForChainRun"],307        config: list[RunnableConfig],308        **kwargs: Any,309    ) -> list[Output | Exception]:310        results_map: dict[int, Output] = {}311312        not_set: list[Output] = []313        result = not_set314        try:315            async for attempt in self._async_retrying():316                with attempt:317                    # Retry for inputs that have not yet succeeded318                    # Determine which original indices remain.319                    remaining_indices = [320                        i for i in range(len(inputs)) if i not in results_map321                    ]322                    if not remaining_indices:323                        break324                    pending_inputs = [inputs[i] for i in remaining_indices]325                    pending_configs = [config[i] for i in remaining_indices]326                    pending_run_managers = [run_manager[i] for i in remaining_indices]327                    result = await super().abatch(328                        pending_inputs,329                        self._patch_config_list(330                            pending_configs, pending_run_managers, attempt.retry_state331                        ),332                        return_exceptions=True,333                        **kwargs,334                    )335                    # Register the results of the inputs that have succeeded, mapping336                    # back to their original indices.337                    first_exception = None338                    for offset, r in enumerate(result):339                        if isinstance(r, Exception):340                            if not first_exception:341                                first_exception = r342                            continue343                        orig_idx = remaining_indices[offset]344                        results_map[orig_idx] = r345                    # If any exception occurred, raise it, to retry the failed ones346                    if first_exception:347                        raise first_exception348                if (349                    attempt.retry_state.outcome350                    and not attempt.retry_state.outcome.failed351                ):352                    attempt.retry_state.set_result(result)353        except RetryError as e:354            if result is not_set:355                result = cast("list[Output]", [e] * len(inputs))356357        outputs: list[Output | Exception] = []358        for idx in range(len(inputs)):359            if idx in results_map:360                outputs.append(results_map[idx])361            else:362                outputs.append(result.pop(0))363        return outputs364365    @override366    async def abatch(367        self,368        inputs: list[Input],369        config: RunnableConfig | list[RunnableConfig] | None = None,370        *,371        return_exceptions: bool = False,372        **kwargs: Any,373    ) -> list[Output]:374        return await self._abatch_with_config(375            self._abatch, inputs, config, return_exceptions=return_exceptions, **kwargs376        )377378    # stream() and transform() are not retried because retrying a stream379    # is not very intuitive.

Code quality findings 8

Ensure functions have docstrings for documentation
missing-docstring
def invoke(
Ensure functions have docstrings for documentation
missing-docstring
async def ainvoke(
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(r, Exception):
Ensure functions have docstrings for documentation
missing-docstring
def batch(
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(r, Exception):
Ensure functions have docstrings for documentation
missing-docstring
async def abatch(

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.