Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
model_retry.py313 linesDownload Raw Back to middleware
1"""Model retry middleware for agents."""2 3from __future__ import annotations4 5import asyncio6import time7from typing import TYPE_CHECKING8 9from langchain_core.messages import AIMessage10 11from langchain.agents.middleware._retry import (12    OnFailure,13    RetryOn,14    calculate_delay,15    should_retry_exception,16    validate_retry_params,17)18from langchain.agents.middleware.types import (19    AgentMiddleware,20    AgentState,21    ContextT,22    ModelRequest,23    ModelResponse,24    ResponseT,25)26 27if TYPE_CHECKING:28    from collections.abc import Awaitable, Callable29 30 31class ModelRetryMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):32    """Middleware that automatically retries failed model calls with configurable backoff.33 34    Supports retrying on specific exceptions and exponential backoff.35 36    Examples:37        !!! example "Basic usage with default settings (2 retries, exponential backoff)"38 39            ```python40            from langchain.agents import create_agent41            from langchain.agents.middleware import ModelRetryMiddleware42 43            agent = create_agent(model, tools=[search_tool], middleware=[ModelRetryMiddleware()])44            ```45 46        !!! example "Retry specific exceptions only"47 48            ```python49            from anthropic import RateLimitError50            from openai import APITimeoutError51 52            retry = ModelRetryMiddleware(53                max_retries=4,54                retry_on=(APITimeoutError, RateLimitError),55                backoff_factor=1.5,56            )57            ```58 59        !!! example "Custom exception filtering"60 61            ```python62            from anthropic import APIStatusError63 64 65            def should_retry(exc: Exception) -> bool:66                # Only retry on 5xx errors67                if isinstance(exc, APIStatusError):68                    return 500 <= exc.status_code < 60069                return False70 71 72            retry = ModelRetryMiddleware(73                max_retries=3,74                retry_on=should_retry,75            )76            ```77 78        !!! example "Custom error handling"79 80            ```python81            def format_error(exc: Exception) -> str:82                return "Model temporarily unavailable. Please try again later."83 84 85            retry = ModelRetryMiddleware(86                max_retries=4,87                on_failure=format_error,88            )89            ```90 91        !!! example "Constant backoff (no exponential growth)"92 93            ```python94            retry = ModelRetryMiddleware(95                max_retries=5,96                backoff_factor=0.0,  # No exponential growth97                initial_delay=2.0,  # Always wait 2 seconds98            )99            ```100 101        !!! example "Raise exception on failure"102 103            ```python104            retry = ModelRetryMiddleware(105                max_retries=2,106                on_failure="error",  # Re-raise exception instead of returning message107            )108            ```109    """110 111    def __init__(112        self,113        *,114        max_retries: int = 2,115        retry_on: RetryOn = (Exception,),116        on_failure: OnFailure = "continue",117        backoff_factor: float = 2.0,118        initial_delay: float = 1.0,119        max_delay: float = 60.0,120        jitter: bool = True,121    ) -> None:122        """Initialize `ModelRetryMiddleware`.123 124        Args:125            max_retries: Maximum number of retry attempts after the initial call.126 127                Must be `>= 0`.128            retry_on: Either a tuple of exception types to retry on, or a callable129                that takes an exception and returns `True` if it should be retried.130 131                Default is to retry on all exceptions.132            on_failure: Behavior when all retries are exhausted.133 134                Options:135 136                - `'continue'`: Return an `AIMessage` with error details,137                    allowing the agent to continue with an error response.138                - `'error'`: Re-raise the exception, stopping agent execution.139                - **Custom callable:** Function that takes the exception and returns a140                    string for the `AIMessage` content, allowing custom error141                    formatting.142            backoff_factor: Multiplier for exponential backoff.143 144                Each retry waits `initial_delay * (backoff_factor ** retry_number)`145                seconds.146 147                Set to `0.0` for constant delay.148            initial_delay: Initial delay in seconds before first retry.149            max_delay: Maximum delay in seconds between retries.150 151                Caps exponential backoff growth.152            jitter: Whether to add random jitter (`±25%`) to delay to avoid thundering herd.153 154        Raises:155            ValueError: If `max_retries < 0` or delays are negative.156        """157        super().__init__()158 159        # Validate parameters160        validate_retry_params(max_retries, initial_delay, max_delay, backoff_factor)161 162        self.max_retries = max_retries163        self.tools = []  # No additional tools registered by this middleware164        self.retry_on = retry_on165        self.on_failure = on_failure166        self.backoff_factor = backoff_factor167        self.initial_delay = initial_delay168        self.max_delay = max_delay169        self.jitter = jitter170 171    @staticmethod172    def _format_failure_message(exc: Exception, attempts_made: int) -> AIMessage:173        """Format the failure message when retries are exhausted.174 175        Args:176            exc: The exception that caused the failure.177            attempts_made: Number of attempts actually made.178 179        Returns:180            `AIMessage` with formatted error message.181        """182        exc_type = type(exc).__name__183        exc_msg = str(exc)184        attempt_word = "attempt" if attempts_made == 1 else "attempts"185        content = (186            f"Model call failed after {attempts_made} {attempt_word} with {exc_type}: {exc_msg}"187        )188        return AIMessage(content=content)189 190    def _handle_failure(self, exc: Exception, attempts_made: int) -> ModelResponse[ResponseT]:191        """Handle failure when all retries are exhausted.192 193        Args:194            exc: The exception that caused the failure.195            attempts_made: Number of attempts actually made.196 197        Returns:198            `ModelResponse` with error details.199 200        Raises:201            Exception: If `on_failure` is `'error'`, re-raises the exception.202        """203        if self.on_failure == "error":204            raise exc205 206        if callable(self.on_failure):207            content = self.on_failure(exc)208            ai_msg = AIMessage(content=content)209        else:210            ai_msg = self._format_failure_message(exc, attempts_made)211 212        return ModelResponse(result=[ai_msg])213 214    def wrap_model_call(215        self,216        request: ModelRequest[ContextT],217        handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],218    ) -> ModelResponse[ResponseT] | AIMessage:219        """Intercept model execution and retry on failure.220 221        Args:222            request: Model request with model, messages, state, and runtime.223            handler: Callable to execute the model (can be called multiple times).224 225        Returns:226            `ModelResponse` or `AIMessage` (the final result).227 228        Raises:229            RuntimeError: If the retry loop completes without returning. (This should not happen.)230        """231        # Initial attempt + retries232        for attempt in range(self.max_retries + 1):233            try:234                return handler(request)235            except Exception as exc:236                attempts_made = attempt + 1  # attempt is 0-indexed237 238                # Check if we should retry this exception239                if not should_retry_exception(exc, self.retry_on):240                    # Exception is not retryable, handle failure immediately241                    return self._handle_failure(exc, attempts_made)242 243                # Check if we have more retries left244                if attempt < self.max_retries:245                    # Calculate and apply backoff delay246                    delay = calculate_delay(247                        attempt,248                        backoff_factor=self.backoff_factor,249                        initial_delay=self.initial_delay,250                        max_delay=self.max_delay,251                        jitter=self.jitter,252                    )253                    if delay > 0:254                        time.sleep(delay)255                    # Continue to next retry256                else:257                    # No more retries, handle failure258                    return self._handle_failure(exc, attempts_made)259 260        # Unreachable: loop always returns via handler success or _handle_failure261        msg = "Unexpected: retry loop completed without returning"262        raise RuntimeError(msg)263 264    async def awrap_model_call(265        self,266        request: ModelRequest[ContextT],267        handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],268    ) -> ModelResponse[ResponseT] | AIMessage:269        """Intercept and control async model execution with retry logic.270 271        Args:272            request: Model request with model, messages, state, and runtime.273            handler: Async callable to execute the model and returns `ModelResponse`.274 275        Returns:276            `ModelResponse` or `AIMessage` (the final result).277 278        Raises:279            RuntimeError: If the retry loop completes without returning. (This should not happen.)280        """281        # Initial attempt + retries282        for attempt in range(self.max_retries + 1):283            try:284                return await handler(request)285            except Exception as exc:286                attempts_made = attempt + 1  # attempt is 0-indexed287 288                # Check if we should retry this exception289                if not should_retry_exception(exc, self.retry_on):290                    # Exception is not retryable, handle failure immediately291                    return self._handle_failure(exc, attempts_made)292 293                # Check if we have more retries left294                if attempt < self.max_retries:295                    # Calculate and apply backoff delay296                    delay = calculate_delay(297                        attempt,298                        backoff_factor=self.backoff_factor,299                        initial_delay=self.initial_delay,300                        max_delay=self.max_delay,301                        jitter=self.jitter,302                    )303                    if delay > 0:304                        await asyncio.sleep(delay)305                    # Continue to next retry306                else:307                    # No more retries, handle failure308                    return self._handle_failure(exc, attempts_made)309 310        # Unreachable: loop always returns via handler success or _handle_failure311        msg = "Unexpected: retry loop completed without returning"312        raise RuntimeError(msg)313 
codekingpro/portable-devtools · Team Ai