diff --git a/actions/serper/CHANGELOG.md b/actions/serper/CHANGELOG.md index 8a473c37..396bc019 100644 --- a/actions/serper/CHANGELOG.md +++ b/actions/serper/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). +## [1.1.3] - 2025-01-27 + +### Added + +- Exponential backoff retry logic for handling connection failures (RemoteDisconnected exceptions) + ## [1.1.2] - 2025-08-07 ### Changed diff --git a/actions/serper/README.md b/actions/serper/README.md index c93311e9..72945e0a 100644 --- a/actions/serper/README.md +++ b/actions/serper/README.md @@ -7,6 +7,7 @@ The Serper Action Package provides an interface to interact with the Serper API, - Perform Google searches using the Serper API. - Retrieve structured search results, including knowledge graph, organic results, places, people also ask, and related searches. - Securely manage API keys using Sema4.ai's Secret management. +- Built-in retry logic with exponential backoff for handling connection failures. ## Installation diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 28624e96..5b7f6489 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -1,5 +1,6 @@ import json import os +import time from pathlib import Path from typing import List, Optional @@ -11,6 +12,58 @@ load_dotenv(Path(__file__).absolute().parent / "devdata" / ".env") +def retry_with_exponential_backoff(func, max_retries=3, base_delay=1.0, max_delay=5.0): + """ + Retry a function with exponential backoff for connection-related exceptions. + + Args: + func: The function to retry + max_retries: Maximum number of retry attempts (default: 3) + base_delay: Base delay in seconds for exponential backoff (default: 1.0) + max_delay: Maximum delay in seconds (default: 5.0) + + Returns: + The result of the function call + + Raises: + ActionError: If all retries are exhausted + """ + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func() + except (HTTPError, Exception) as e: + last_exception = e + + # Check if it's a connection-related error that should be retried + error_str = str(e).lower() + is_connection_error = ( + "remote end closed connection" in error_str or + "connection aborted" in error_str or + "remotedisconnected" in error_str or + "connection reset" in error_str or + "broken pipe" in error_str + ) + + if not is_connection_error and not isinstance(e, HTTPError): + # Not a retryable error, raise immediately + raise ActionError(f"Non-retryable error occurred: {str(e)}") + + if attempt == max_retries: + # Last attempt failed, raise the exception + if is_connection_error: + raise ActionError(f"Remote connection failed after {max_retries} retries: {str(e)}") + else: + raise ActionError(f"HTTP error occurred: {str(e)}") + + # Calculate delay with exponential backoff + delay = min(base_delay * (2 ** attempt), max_delay) + time.sleep(delay) + + # This should never be reached, but just in case + raise ActionError(f"Unexpected error: {str(last_exception)}") + # Define Pydantic models for the response class KnowledgeGraph(BaseModel): @@ -81,7 +134,7 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: if not api_key: raise ActionError("API key is required but not provided") - try: + def make_request(): headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} payload = json.dumps({"q": q, "num": num}) @@ -92,10 +145,12 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: ) response.raise_for_status() + return response + try: + # Use retry logic with exponential backoff + response = retry_with_exponential_backoff(make_request) search_result = SearchResult(**response.json()) return Response(result=search_result) - except HTTPError as e: - raise ActionError(f"HTTP error occurred: {str(e)}") except Exception as e: raise ActionError(f"An unexpected error occurred: {str(e)}") diff --git a/actions/serper/package.yaml b/actions/serper/package.yaml index ecc1607d..db5ff6ce 100644 --- a/actions/serper/package.yaml +++ b/actions/serper/package.yaml @@ -5,7 +5,7 @@ name: Serper description: Interact with the Serper API to perform Google searches. # Required: The version of the action package. -version: 1.1.2 +version: 1.1.3 # The version of the `package.yaml` format. spec-version: v2 @@ -13,11 +13,12 @@ spec-version: v2 dependencies: conda-forge: - python=3.11.11 - - python-dotenv=1.1.0 + - python-dotenv=1.1.1 - uv=0.6.11 pypi: - - sema4ai-actions=1.4.1 - - pydantic=2.11.7 + - sema4ai-actions=1.4.2 + - pydantic=2.11.9 + - urllib3=2.5.0 external-endpoints: - name: "Google Serper API"