From 8ad8d7332b42a585dc44db802e634dbedd4a8923 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Fri, 22 Aug 2025 18:24:42 -0400 Subject: [PATCH 01/10] Enhance search_google documentation to clarify query and result count parameters --- actions/serper/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 28624e96..c120b274 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -66,7 +66,7 @@ class SearchResult(BaseModel): @action def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: """ - Perform a search using the Serper API and return a structured summary. + Perform a search using the Serper API and return a structured summary. The number of results can be specified. Args: q: The search query. From 22a359b890a260c51b43dae566459ff87d2ee243 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:21:03 -0400 Subject: [PATCH 02/10] add test input data for new options --- actions/serper/devdata/input_search_maps.json | 31 +++++++++++++++ actions/serper/devdata/input_search_news.json | 38 +++++++++++++++++++ .../serper/devdata/input_search_patents.json | 32 ++++++++++++++++ .../serper/devdata/input_search_places.json | 38 +++++++++++++++++++ .../serper/devdata/input_search_reviews.json | 36 ++++++++++++++++++ .../serper/devdata/input_search_scholar.json | 34 +++++++++++++++++ .../serper/devdata/input_search_shopping.json | 36 ++++++++++++++++++ 7 files changed, 245 insertions(+) create mode 100644 actions/serper/devdata/input_search_maps.json create mode 100644 actions/serper/devdata/input_search_news.json create mode 100644 actions/serper/devdata/input_search_patents.json create mode 100644 actions/serper/devdata/input_search_places.json create mode 100644 actions/serper/devdata/input_search_reviews.json create mode 100644 actions/serper/devdata/input_search_scholar.json create mode 100644 actions/serper/devdata/input_search_shopping.json diff --git a/actions/serper/devdata/input_search_maps.json b/actions/serper/devdata/input_search_maps.json new file mode 100644 index 00000000..364f1dce --- /dev/null +++ b/actions/serper/devdata/input_search_maps.json @@ -0,0 +1,31 @@ +{ + "inputs": [ + { + "inputName": "Search for coffee shops in San Francisco", + "inputValue": { + "q": "coffee shops San Francisco", + "ll": "@37.7749,-122.4194,12", + "hl": "en", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_maps", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The location search query.", + "ll: string: GPS coordinates in format '@latitude,longitude,zoom'.", + "hl: string: Language code." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, ll: Optional[str] = None, placeid: Optional[str] = None, cid: Optional[str] = None, hl: Optional[str] = None, page: int = 1'" + } +} diff --git a/actions/serper/devdata/input_search_news.json b/actions/serper/devdata/input_search_news.json new file mode 100644 index 00000000..e2182171 --- /dev/null +++ b/actions/serper/devdata/input_search_news.json @@ -0,0 +1,38 @@ +{ + "inputs": [ + { + "inputName": "Search for AI news", + "inputValue": { + "q": "artificial intelligence news", + "num": 5, + "gl": "us", + "hl": "en", + "tbs": "qdr:d", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_news", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The news search query.", + "num: integer: Number of results (default: 10).", + "gl: string: Country code.", + "hl: string: Language code.", + "tbs: string: Time-based filter for news recency." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, gl: Optional[str] = None, location: Optional[str] = None, hl: Optional[str] = None, tbs: Optional[str] = None, autocorrect: Optional[bool] = None, num: int = 10, page: int = 1'" + } +} + + + diff --git a/actions/serper/devdata/input_search_patents.json b/actions/serper/devdata/input_search_patents.json new file mode 100644 index 00000000..be134dea --- /dev/null +++ b/actions/serper/devdata/input_search_patents.json @@ -0,0 +1,32 @@ +{ + "inputs": [ + { + "inputName": "Search for AI patents", + "inputValue": { + "q": "artificial intelligence patent", + "num": 5, + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_patents", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The patent search query.", + "num: integer: Number of results (default: 10)." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, num: int = 10, page: int = 1'" + } +} + + + diff --git a/actions/serper/devdata/input_search_places.json b/actions/serper/devdata/input_search_places.json new file mode 100644 index 00000000..862ee6dc --- /dev/null +++ b/actions/serper/devdata/input_search_places.json @@ -0,0 +1,38 @@ +{ + "inputs": [ + { + "inputName": "Search for restaurants in New York", + "inputValue": { + "q": "Italian restaurants", + "num": 5, + "gl": "us", + "location": "New York, NY", + "hl": "en", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_places", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The place search query.", + "num: integer: Number of results (default: 10).", + "gl: string: Country code.", + "location: string: Location filter.", + "hl: string: Language code." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, gl: Optional[str] = None, location: Optional[str] = None, hl: Optional[str] = None, tbs: Optional[str] = None, autocorrect: Optional[bool] = None, num: int = 10, page: int = 1'" + } +} + + + diff --git a/actions/serper/devdata/input_search_reviews.json b/actions/serper/devdata/input_search_reviews.json new file mode 100644 index 00000000..6918e557 --- /dev/null +++ b/actions/serper/devdata/input_search_reviews.json @@ -0,0 +1,36 @@ +{ + "inputs": [ + { + "inputName": "Search for restaurant reviews", + "inputValue": { + "placeid": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "sortBy": "Most relevant", + "gl": "us", + "hl": "en", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_reviews", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "placeid: string: Google Place ID.", + "sortBy: string: Sort method (e.g., 'Most relevant').", + "gl: string: Country code.", + "hl: string: Language code." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'api_key: Secret, fid: Optional[str] = None, cid: Optional[str] = None, placeid: Optional[str] = None, sortBy: Optional[str] = None, topicId: Optional[str] = None, nextPageToken: Optional[str] = None, gl: Optional[str] = None, hl: Optional[str] = None'" + } +} + + + diff --git a/actions/serper/devdata/input_search_scholar.json b/actions/serper/devdata/input_search_scholar.json new file mode 100644 index 00000000..ae9a771b --- /dev/null +++ b/actions/serper/devdata/input_search_scholar.json @@ -0,0 +1,34 @@ +{ + "inputs": [ + { + "inputName": "Search for machine learning papers", + "inputValue": { + "q": "machine learning algorithms", + "gl": "us", + "hl": "en", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_scholar", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The academic search query.", + "gl: string: Country code.", + "hl: string: Language code." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, gl: Optional[str] = None, location: Optional[str] = None, hl: Optional[str] = None, autocorrect: Optional[bool] = None, page: int = 1'" + } +} + + + diff --git a/actions/serper/devdata/input_search_shopping.json b/actions/serper/devdata/input_search_shopping.json new file mode 100644 index 00000000..d074b168 --- /dev/null +++ b/actions/serper/devdata/input_search_shopping.json @@ -0,0 +1,36 @@ +{ + "inputs": [ + { + "inputName": "Search for laptop deals", + "inputValue": { + "q": "gaming laptop", + "num": 5, + "gl": "us", + "hl": "en", + "api_key": "" + } + } + ], + "metadata": { + "actionName": "search_shopping", + "actionRelativePath": "actions.py", + "schemaDescription": [ + "q: string: The product search query.", + "num: integer: Number of results (default: 10).", + "gl: string: Country code.", + "hl: string: Language code." + ], + "managedParamsSchemaDescription": { + "api_key": { + "type": "Secret", + "description": "The API key for authentication." + } + }, + "inputFileVersion": "v3", + "kind": "action", + "actionSignature": "action/args: 'q: str, api_key: Secret, gl: Optional[str] = None, location: Optional[str] = None, hl: Optional[str] = None, autocorrect: Optional[bool] = None, num: int = 10, page: int = 1'" + } +} + + + From a77bee504c4eeae0efa966faa05ca41b38cf7897 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:22:16 -0400 Subject: [PATCH 03/10] add new search options for places, maps, locations, scholar, shopping, and patents --- actions/serper/actions.py | 496 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 476 insertions(+), 20 deletions(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index c120b274..06679bee 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -1,7 +1,8 @@ + import json import os from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Dict, Union import sema4ai_http from dotenv import load_dotenv @@ -12,6 +13,73 @@ load_dotenv(Path(__file__).absolute().parent / "devdata" / ".env") +# Input validation functions +def _validate_url(url: str) -> None: + """Validate URL format for lens search.""" + if not url or not isinstance(url, str): + raise ActionError("URL is required and must be a string") + if not url.startswith(('http://', 'https://')): + raise ActionError("URL must start with http:// or https://") + + +def _validate_query(q: str) -> None: + """Validate search query is not empty.""" + if not q or not isinstance(q, str) or not q.strip(): + raise ActionError("Search query is required and cannot be empty") + + +def _validate_coordinates(ll: str) -> None: + """Validate GPS coordinates format.""" + if not ll.startswith('@'): + raise ActionError("GPS coordinates must start with '@' (format: @latitude,longitude,zoom)") + parts = ll[1:].split(',') + if len(parts) < 2: + raise ActionError("GPS coordinates must include latitude and longitude (format: @latitude,longitude,zoom)") + + +def _validate_positive_integer(value: int, param_name: str) -> None: + """Validate that a parameter is a positive integer.""" + if not isinstance(value, int) or value <= 0: + raise ActionError(f"{param_name} must be a positive integer") + + +# Helper functions for common operations +def _get_api_key(api_key: Secret) -> str: + """Extract and validate API key.""" + key = api_key.value or os.getenv("SERPER_API_KEY") + if not key: + raise ActionError("API key is required but not provided") + return key + + +def _build_payload(base_params: dict, optional_params: dict = None) -> dict: + """Build API payload, only including non-None optional parameters.""" + payload = base_params.copy() + if optional_params: + for key, value in optional_params.items(): + if value is not None: + payload[key] = value + return payload + + +def _make_serper_request(endpoint: str, payload: dict, api_key_str: str, result_class): + """Make request to Serper API and return parsed result.""" + try: + headers = {"X-API-KEY": api_key_str, "Content-Type": "application/json"} + response = sema4ai_http.post( + f"https://google.serper.dev/{endpoint}", + body=json.dumps(payload), + headers=headers, + ) + response.raise_for_status() + search_result = result_class(**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)}") + + # Define Pydantic models for the response class KnowledgeGraph(BaseModel): title: str = Field(description="The title of the knowledge graph", default="") @@ -63,6 +131,135 @@ class SearchResult(BaseModel): credits: int = Field(description="The number of credits used", default=0) +# New models for additional search endpoints +class NewsResult(BaseModel): + title: str = Field(description="Article headline", default="") + link: str = Field(description="Article URL", default="") + snippet: str = Field(description="Article summary", default="") + date: str = Field(description="Publication date", default="") + source: str = Field(description="News source", default="") + imageUrl: Optional[str] = Field(description="Article image", default=None) + position: int = Field(description="Result position", default=0) + + +class NewsSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + news: List[NewsResult] = Field(description="A list of news results", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class ShoppingResult(BaseModel): + title: str = Field(description="Product name", default="") + source: str = Field(description="Vendor/retailer", default="") + link: str = Field(description="Product page URL", default="") + price: str = Field(description="Product price", default="") + imageUrl: str = Field(description="Product image", default="") + rating: float = Field(description="Product rating", default=0.0) + ratingCount: int = Field(description="Number of ratings", default=0) + productId: str = Field(description="Unique product identifier", default="") + position: int = Field(description="Result position", default=0) + + +class ShoppingSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + shopping: List[ShoppingResult] = Field(description="A list of shopping results", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class ScholarResult(BaseModel): + title: str = Field(description="Paper title", default="") + link: str = Field(description="Paper URL", default="") + publicationInfo: str = Field(description="Author and publication details", default="") + snippet: str = Field(description="Paper abstract/summary", default="") + year: int = Field(description="Publication year", default=0) + citedBy: int = Field(description="Citation count", default=0) + pdfUrl: Optional[str] = Field(description="Direct PDF link", default=None) + id: str = Field(description="Unique paper identifier", default="") + position: int = Field(description="Result position", default=0) + + +class ScholarSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + organic: List[ScholarResult] = Field(description="A list of scholarly results", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class PatentResult(BaseModel): + title: str = Field(description="Patent title", default="") + link: str = Field(description="Patent URL", default="") + snippet: str = Field(description="Patent description/abstract", default="") + position: int = Field(description="Result position", default=0) + priorityDate: Optional[str] = Field(description="Priority date", default=None) + filingDate: Optional[str] = Field(description="Filing date", default=None) + grantDate: Optional[str] = Field(description="Grant date", default=None) + publicationDate: Optional[str] = Field(description="Publication date", default=None) + inventor: Optional[str] = Field(description="Inventor name(s)", default=None) + assignee: Optional[str] = Field(description="Patent assignee/owner", default=None) + publicationNumber: Optional[str] = Field(description="Publication number", default=None) + language: Optional[str] = Field(description="Patent language", default=None) + pdfUrl: Optional[str] = Field(description="Direct PDF link", default=None) + figures: Optional[List[str]] = Field(description="Patent figures/images", default=None) + + +class PatentSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + organic: List[PatentResult] = Field(description="A list of patent results", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class EnhancedPlace(BaseModel): + position: int = Field(description="Result position", default=0) + title: str = Field(description="Place name", default="") + address: Optional[str] = Field(description="Full address", default=None) + latitude: float = Field(description="GPS latitude", default=0.0) + longitude: float = Field(description="GPS longitude", default=0.0) + rating: float = Field(description="Average rating", default=0.0) + ratingCount: int = Field(description="Number of reviews", default=0) + priceLevel: Optional[str] = Field(description="Price range indicator (e.g., '$$$')", default=None) + type: Optional[str] = Field(description="Primary business type", default=None) + types: Optional[List[str]] = Field(description="All business type categories", default=None) + website: Optional[str] = Field(description="Business website", default=None) + phoneNumber: Optional[str] = Field(description="Contact phone number", default=None) + description: Optional[str] = Field(description="Business description", default=None) + openingHours: Optional[Dict[str, str]] = Field(description="Operating hours by day", default=None) + thumbnailUrl: Optional[str] = Field(description="Place thumbnail image", default=None) + cid: Optional[str] = Field(description="Customer/place ID (numeric)", default=None) + fid: Optional[str] = Field(description="Feature ID (hex format)", default=None) + placeId: Optional[str] = Field(description="Google Place ID", default=None) + # Keep legacy field for backward compatibility + category: str = Field(description="Business category (legacy field)", default="") + placeid: Optional[str] = Field(description="Google Place ID (legacy field)", default=None) + + +class MapSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + ll: Optional[str] = Field(description="GPS coordinates if provided", default=None) + places: List[EnhancedPlace] = Field(description="A list of places", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class PlaceSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + places: List[EnhancedPlace] = Field(description="A list of places", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + +class ReviewResult(BaseModel): + title: Optional[str] = Field(description="Review title", default=None) + rating: Optional[float] = Field(description="Review rating", default=None) + text: Optional[str] = Field(description="Review text", default=None) + author: Optional[str] = Field(description="Review author", default=None) + date: Optional[str] = Field(description="Review date", default=None) + position: Optional[int] = Field(description="Result position", default=None) + + +class ReviewSearchResult(BaseModel): + searchParameters: dict = Field(description="The search parameters used", default={}) + reviews: List[ReviewResult] = Field(description="A list of review results", default=[]) + credits: int = Field(description="The number of credits used", default=0) + + + @action def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: """ @@ -76,26 +273,285 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: Returns: SearchResult: A structured summary of the search results. """ - # Check if API key is provided - api_key = api_key.value or os.getenv("SERPER_API_KEY") - if not api_key: - raise ActionError("API key is required but not provided") + # Validate inputs + _validate_query(q) + _validate_positive_integer(num, "num") + + # Build payload and make request + payload = _build_payload({"q": q, "num": num}) + api_key_str = _get_api_key(api_key) + return _make_serper_request("search", payload, api_key_str, SearchResult) - try: - headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} - payload = json.dumps({"q": q, "num": num}) - response = sema4ai_http.post( - "https://google.serper.dev/search", - body=payload, - headers=headers, - ) +@action +def search_news( + q: str, + api_key: Secret, + gl: Optional[str] = None, + location: Optional[str] = None, + hl: Optional[str] = None, + tbs: Optional[str] = None, + autocorrect: Optional[bool] = None, + num: int = 10, + page: int = 1 +) -> Response[NewsSearchResult]: + """ + Search for news articles using the Serper API with publication dates and source information. + + Args: + q: The news search query. + api_key: The API key for authentication. + gl: Country code. + location: Location filter. + hl: Language code. + tbs: Time-based filter for news recency. + autocorrect: Enable/disable autocorrect. + num: Number of results (default: 10). + page: Page number (default: 1). + + Returns: + NewsSearchResult: A structured summary of the news search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(num, "num") + _validate_positive_integer(page, "page") + + # Build payload and make request + base_params = {"q": q, "num": num, "page": page} + optional_params = {"gl": gl, "location": location, "hl": hl, "tbs": tbs, "autocorrect": autocorrect} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("news", payload, api_key_str, NewsSearchResult) + + +@action +def search_shopping( + q: str, + api_key: Secret, + gl: Optional[str] = None, + location: Optional[str] = None, + hl: Optional[str] = None, + autocorrect: Optional[bool] = None, + num: int = 10, + page: int = 1 +) -> Response[ShoppingSearchResult]: + """ + Search for products using the Serper API with pricing, ratings, and vendor information. + + Args: + q: The product search query. + api_key: The API key for authentication. + gl: Country code. + location: Location filter. + hl: Language code. + autocorrect: Enable/disable autocorrect. + num: Number of results (default: 10). + page: Page number (default: 1). + + Returns: + ShoppingSearchResult: A structured summary of the shopping search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(num, "num") + _validate_positive_integer(page, "page") + + # Build payload and make request + base_params = {"q": q, "num": num, "page": page} + optional_params = {"gl": gl, "location": location, "hl": hl, "autocorrect": autocorrect} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("shopping", payload, api_key_str, ShoppingSearchResult) + + +@action +def search_scholar( + q: str, + api_key: Secret, + gl: Optional[str] = None, + location: Optional[str] = None, + hl: Optional[str] = None, + autocorrect: Optional[bool] = None, + page: int = 1 +) -> Response[ScholarSearchResult]: + """ + Search for academic papers and scholarly articles using the Serper API with citation data. + + Args: + q: The academic search query. + api_key: The API key for authentication. + gl: Country code. + location: Location filter. + hl: Language code. + autocorrect: Enable/disable autocorrect. + page: Page number (default: 1). + + Returns: + ScholarSearchResult: A structured summary of the scholarly search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(page, "page") + + # Build payload and make request + base_params = {"q": q, "page": page} + optional_params = {"gl": gl, "location": location, "hl": hl, "autocorrect": autocorrect} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("scholar", payload, api_key_str, ScholarSearchResult) + + +@action +def search_patents( + q: str, + api_key: Secret, + num: int = 10, + page: int = 1 +) -> Response[PatentSearchResult]: + """ + Search for patents and patent documents using the Serper API. + + Args: + q: The patent search query. + api_key: The API key for authentication. + num: Number of results (default: 10). + page: Page number (default: 1). + + Returns: + PatentSearchResult: A structured summary of the patent search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(num, "num") + _validate_positive_integer(page, "page") + + # Build payload and make request + payload = _build_payload({"q": q, "num": num, "page": page}) + api_key_str = _get_api_key(api_key) + return _make_serper_request("patents", payload, api_key_str, PatentSearchResult) + + +@action +def search_maps( + q: str, + api_key: Secret, + ll: Optional[str] = None, + placeid: Optional[str] = None, + cid: Optional[str] = None, + hl: Optional[str] = None, + page: int = 1 +) -> Response[MapSearchResult]: + """ + Enhanced geographic search using the Serper API with detailed place information and opening hours. + + Args: + q: The location search query. + api_key: The API key for authentication. + ll: GPS coordinates in format '@latitude,longitude,zoom'. + placeid: Google Place ID. + cid: Customer/location ID. + hl: Language code. + page: Page number (default: 1). + + Returns: + MapSearchResult: A structured summary of the map search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(page, "page") + if ll: + _validate_coordinates(ll) + + # Build payload and make request + base_params = {"q": q, "page": page} + optional_params = {"ll": ll, "placeid": placeid, "cid": cid, "hl": hl} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("maps", payload, api_key_str, MapSearchResult) + + +@action +def search_places( + q: str, + api_key: Secret, + gl: Optional[str] = None, + location: Optional[str] = None, + hl: Optional[str] = None, + tbs: Optional[str] = None, + autocorrect: Optional[bool] = None, + num: int = 10, + page: int = 1 +) -> Response[PlaceSearchResult]: + """ + Search for local businesses and places using the Serper API with contact and rating information. + + Args: + q: The place search query. + api_key: The API key for authentication. + gl: Country code. + location: Location filter. + hl: Language code. + tbs: Time-based filter. + autocorrect: Enable/disable autocorrect. + num: Number of results (default: 10). + page: Page number (default: 1). + + Returns: + PlaceSearchResult: A structured summary of the place search results. + """ + # Validate inputs + _validate_query(q) + _validate_positive_integer(num, "num") + _validate_positive_integer(page, "page") + + # Build payload and make request + base_params = {"q": q, "num": num, "page": page} + optional_params = {"gl": gl, "location": location, "hl": hl, "tbs": tbs, "autocorrect": autocorrect} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("places", payload, api_key_str, PlaceSearchResult) + + +@action +def search_reviews( + api_key: Secret, + fid: Optional[str] = None, + cid: Optional[str] = None, + placeid: Optional[str] = None, + sortBy: Optional[str] = None, + topicId: Optional[str] = None, + nextPageToken: Optional[str] = None, + gl: Optional[str] = None, + hl: Optional[str] = None +) -> Response[ReviewSearchResult]: + """ + Search for reviews of specific businesses or places using the Serper API. + + Args: + api_key: The API key for authentication. + fid: Feature/business ID. + cid: Customer/client ID. + placeid: Google Place ID. + sortBy: Sort method (e.g., 'Most relevant'). + topicId: Topic identifier. + nextPageToken: Pagination token. + gl: Country code. + hl: Language code. + + Returns: + ReviewSearchResult: A structured summary of the review search results. + """ + # Validate inputs + if not any([fid, cid, placeid]): + raise ActionError("At least one of fid, cid, or placeid must be provided") + + # Build payload and make request + base_params = {} + optional_params = {"fid": fid, "cid": cid, "placeid": placeid, "sortBy": sortBy, + "topicId": topicId, "nextPageToken": nextPageToken, "gl": gl, "hl": hl} + payload = _build_payload(base_params, optional_params) + api_key_str = _get_api_key(api_key) + return _make_serper_request("reviews", payload, api_key_str, ReviewSearchResult) - response.raise_for_status() - 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)}") From d7314295c8f02739350a2a0eef67441ab1b0a103 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:27:08 -0400 Subject: [PATCH 04/10] Update docs to include params --- actions/serper/actions.py | 128 +++++++------------------------------- 1 file changed, 24 insertions(+), 104 deletions(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 06679bee..439b0800 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -263,15 +263,9 @@ class ReviewSearchResult(BaseModel): @action def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: """ - Perform a search using the Serper API and return a structured summary. The number of results can be specified. - - Args: - q: The search query. - num: The number of results to return. - api_key: The API key for authentication. - - Returns: - SearchResult: A structured summary of the search results. + Perform a comprehensive Google search using the Serper API. Returns organic results, knowledge graphs, related questions, and more. + + You can specify the number of results to return (num, default: 10, max: 100). """ # Validate inputs _validate_query(q) @@ -296,21 +290,9 @@ def search_news( page: int = 1 ) -> Response[NewsSearchResult]: """ - Search for news articles using the Serper API with publication dates and source information. - - Args: - q: The news search query. - api_key: The API key for authentication. - gl: Country code. - location: Location filter. - hl: Language code. - tbs: Time-based filter for news recency. - autocorrect: Enable/disable autocorrect. - num: Number of results (default: 10). - page: Page number (default: 1). - - Returns: - NewsSearchResult: A structured summary of the news search results. + Search for recent news articles with publication dates, sources, and images. Perfect for staying updated on current events. + + You can specify the number of articles (num, default: 10), page number (page, default: 1), country (gl), and time filter (tbs: "qdr:h" for hour, "qdr:d" for day, "qdr:w" for week, "qdr:m" for month). """ # Validate inputs _validate_query(q) @@ -337,20 +319,9 @@ def search_shopping( page: int = 1 ) -> Response[ShoppingSearchResult]: """ - Search for products using the Serper API with pricing, ratings, and vendor information. - - Args: - q: The product search query. - api_key: The API key for authentication. - gl: Country code. - location: Location filter. - hl: Language code. - autocorrect: Enable/disable autocorrect. - num: Number of results (default: 10). - page: Page number (default: 1). - - Returns: - ShoppingSearchResult: A structured summary of the shopping search results. + Search for products with prices, ratings, and vendor information. Perfect for price comparison and product research. + + You can specify the number of products (num, default: 10), page number (page, default: 1), and country for localized pricing (gl). """ # Validate inputs _validate_query(q) @@ -376,19 +347,9 @@ def search_scholar( page: int = 1 ) -> Response[ScholarSearchResult]: """ - Search for academic papers and scholarly articles using the Serper API with citation data. - - Args: - q: The academic search query. - api_key: The API key for authentication. - gl: Country code. - location: Location filter. - hl: Language code. - autocorrect: Enable/disable autocorrect. - page: Page number (default: 1). - - Returns: - ScholarSearchResult: A structured summary of the scholarly search results. + Search for academic papers and scholarly articles with citations, publication info, and PDF links. Perfect for research and literature reviews. + + You can specify the page number (page, default: 1) and country for regional academic sources (gl). """ # Validate inputs _validate_query(q) @@ -410,16 +371,9 @@ def search_patents( page: int = 1 ) -> Response[PatentSearchResult]: """ - Search for patents and patent documents using the Serper API. - - Args: - q: The patent search query. - api_key: The API key for authentication. - num: Number of results (default: 10). - page: Page number (default: 1). - - Returns: - PatentSearchResult: A structured summary of the patent search results. + Search for patents with detailed metadata including inventors, filing dates, and PDF documents. Perfect for IP research and prior art searches. + + You can specify the number of patents (num, default: 10) and page number (page, default: 1). """ # Validate inputs _validate_query(q) @@ -443,19 +397,9 @@ def search_maps( page: int = 1 ) -> Response[MapSearchResult]: """ - Enhanced geographic search using the Serper API with detailed place information and opening hours. - - Args: - q: The location search query. - api_key: The API key for authentication. - ll: GPS coordinates in format '@latitude,longitude,zoom'. - placeid: Google Place ID. - cid: Customer/location ID. - hl: Language code. - page: Page number (default: 1). - - Returns: - MapSearchResult: A structured summary of the map search results. + Enhanced geographic search with detailed place information, opening hours, and ratings. Perfect for finding local businesses and locations. + + You can specify GPS coordinates (ll: '@latitude,longitude,zoom'), Google Place ID (placeid), or business ID (cid). Page number defaults to 1. """ # Validate inputs _validate_query(q) @@ -484,21 +428,9 @@ def search_places( page: int = 1 ) -> Response[PlaceSearchResult]: """ - Search for local businesses and places using the Serper API with contact and rating information. - - Args: - q: The place search query. - api_key: The API key for authentication. - gl: Country code. - location: Location filter. - hl: Language code. - tbs: Time-based filter. - autocorrect: Enable/disable autocorrect. - num: Number of results (default: 10). - page: Page number (default: 1). - - Returns: - PlaceSearchResult: A structured summary of the place search results. + Search for local businesses and places with contact information, ratings, and categories. Perfect for finding services and establishments. + + You can specify the number of places (num, default: 10), page number (page, default: 1), country (gl), and location filter. """ # Validate inputs _validate_query(q) @@ -526,21 +458,9 @@ def search_reviews( hl: Optional[str] = None ) -> Response[ReviewSearchResult]: """ - Search for reviews of specific businesses or places using the Serper API. - - Args: - api_key: The API key for authentication. - fid: Feature/business ID. - cid: Customer/client ID. - placeid: Google Place ID. - sortBy: Sort method (e.g., 'Most relevant'). - topicId: Topic identifier. - nextPageToken: Pagination token. - gl: Country code. - hl: Language code. - - Returns: - ReviewSearchResult: A structured summary of the review search results. + Search for customer reviews of specific businesses or places with ratings, dates, and detailed feedback. Perfect for reputation analysis. + + Requires at least one ID: business ID (fid), customer ID (cid), or Google Place ID (placeid). You can sort by "Most relevant", "Newest", "Highest rating", or "Lowest rating". """ # Validate inputs if not any([fid, cid, placeid]): From c174c8d00d4102a308a79a6b775929d9c5d4f8f4 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:31:58 -0400 Subject: [PATCH 05/10] Bump version to 1.2.0 for new search endpoints --- actions/serper/package.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/serper/package.yaml b/actions/serper/package.yaml index ecc1607d..3b88b3ab 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.2.0 # The version of the `package.yaml` format. spec-version: v2 From 81ae67b08d763f00e6eb26f9470d25a22ca81161 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:33:42 -0400 Subject: [PATCH 06/10] Add required Args documentation to all action functions for linter --- actions/serper/actions.py | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 439b0800..4a91ab6d 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -266,6 +266,11 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: Perform a comprehensive Google search using the Serper API. Returns organic results, knowledge graphs, related questions, and more. You can specify the number of results to return (num, default: 10, max: 100). + + Args: + q: Search query string + num: Number of search results to return (1-100) + api_key: Serper API key for authentication """ # Validate inputs _validate_query(q) @@ -293,6 +298,17 @@ def search_news( Search for recent news articles with publication dates, sources, and images. Perfect for staying updated on current events. You can specify the number of articles (num, default: 10), page number (page, default: 1), country (gl), and time filter (tbs: "qdr:h" for hour, "qdr:d" for day, "qdr:w" for week, "qdr:m" for month). + + Args: + q: News search query + api_key: Serper API key for authentication + gl: Country code for localized results + location: Location filter for regional news + hl: Language code for results + tbs: Time filter for news recency + autocorrect: Enable automatic spelling correction + num: Number of articles to return + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -322,6 +338,16 @@ def search_shopping( Search for products with prices, ratings, and vendor information. Perfect for price comparison and product research. You can specify the number of products (num, default: 10), page number (page, default: 1), and country for localized pricing (gl). + + Args: + q: Product search query + api_key: Serper API key for authentication + gl: Country code for localized pricing + location: Location for regional availability + hl: Language code for product descriptions + autocorrect: Enable automatic spelling correction + num: Number of products to return + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -350,6 +376,15 @@ def search_scholar( Search for academic papers and scholarly articles with citations, publication info, and PDF links. Perfect for research and literature reviews. You can specify the page number (page, default: 1) and country for regional academic sources (gl). + + Args: + q: Academic search query + api_key: Serper API key for authentication + gl: Country code for regional academic sources + location: Location filter for institutional research + hl: Language code for papers + autocorrect: Enable automatic spelling correction + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -374,6 +409,12 @@ def search_patents( Search for patents with detailed metadata including inventors, filing dates, and PDF documents. Perfect for IP research and prior art searches. You can specify the number of patents (num, default: 10) and page number (page, default: 1). + + Args: + q: Patent search query + api_key: Serper API key for authentication + num: Number of patents to return + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -400,6 +441,15 @@ def search_maps( Enhanced geographic search with detailed place information, opening hours, and ratings. Perfect for finding local businesses and locations. You can specify GPS coordinates (ll: '@latitude,longitude,zoom'), Google Place ID (placeid), or business ID (cid). Page number defaults to 1. + + Args: + q: Location search query + api_key: Serper API key for authentication + ll: GPS coordinates in format '@latitude,longitude,zoom' + placeid: Google Place ID for specific location lookup + cid: Customer/location ID for business lookup + hl: Language code for results + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -431,6 +481,17 @@ def search_places( Search for local businesses and places with contact information, ratings, and categories. Perfect for finding services and establishments. You can specify the number of places (num, default: 10), page number (page, default: 1), country (gl), and location filter. + + Args: + q: Place search query + api_key: Serper API key for authentication + gl: Country code for localized results + location: Location filter for regional search + hl: Language code for results + tbs: Time-based filter for business hours or recency + autocorrect: Enable automatic spelling correction + num: Number of places to return + page: Page number for pagination """ # Validate inputs _validate_query(q) @@ -461,6 +522,17 @@ def search_reviews( Search for customer reviews of specific businesses or places with ratings, dates, and detailed feedback. Perfect for reputation analysis. Requires at least one ID: business ID (fid), customer ID (cid), or Google Place ID (placeid). You can sort by "Most relevant", "Newest", "Highest rating", or "Lowest rating". + + Args: + api_key: Serper API key for authentication + fid: Feature/business ID from Google Maps + cid: Customer/client ID for the business + placeid: Google Place ID for the location + sortBy: Sort method for reviews + topicId: Topic identifier for filtered reviews + nextPageToken: Pagination token for additional results + gl: Country code for localized reviews + hl: Language code for review text """ # Validate inputs if not any([fid, cid, placeid]): From 4922cbb1ea1aafc852d35ecae822941c270c2f9a Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:42:35 -0400 Subject: [PATCH 07/10] Fix PatentResult figures field to handle objects with imageUrl property --- actions/serper/actions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 4a91ab6d..6ee3e60d 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -184,6 +184,10 @@ class ScholarSearchResult(BaseModel): credits: int = Field(description="The number of credits used", default=0) +class PatentFigure(BaseModel): + imageUrl: str = Field(description="URL of the patent figure image") + + class PatentResult(BaseModel): title: str = Field(description="Patent title", default="") link: str = Field(description="Patent URL", default="") @@ -198,7 +202,7 @@ class PatentResult(BaseModel): publicationNumber: Optional[str] = Field(description="Publication number", default=None) language: Optional[str] = Field(description="Patent language", default=None) pdfUrl: Optional[str] = Field(description="Direct PDF link", default=None) - figures: Optional[List[str]] = Field(description="Patent figures/images", default=None) + figures: Optional[List[PatentFigure]] = Field(description="Patent figures/images", default=None) class PatentSearchResult(BaseModel): From 653f5e9aeded9533fe3e1f06211e608d810ac8d7 Mon Sep 17 00:00:00 2001 From: aaron yim Date: Sun, 7 Sep 2025 14:54:40 -0400 Subject: [PATCH 08/10] Fix ReviewResult model to match actual API response structure --- actions/serper/actions.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 6ee3e60d..f37bf7af 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -248,12 +248,24 @@ class PlaceSearchResult(BaseModel): credits: int = Field(description="The number of credits used", default=0) +class ReviewUser(BaseModel): + name: Optional[str] = Field(description="Reviewer name", default=None) + thumbnail: Optional[str] = Field(description="Reviewer profile image", default=None) + link: Optional[str] = Field(description="Reviewer profile link", default=None) + reviews: Optional[int] = Field(description="Number of reviews by user", default=None) + photos: Optional[int] = Field(description="Number of photos by user", default=None) + + class ReviewResult(BaseModel): title: Optional[str] = Field(description="Review title", default=None) rating: Optional[float] = Field(description="Review rating", default=None) - text: Optional[str] = Field(description="Review text", default=None) - author: Optional[str] = Field(description="Review author", default=None) + snippet: Optional[str] = Field(description="Review text content", default=None) + text: Optional[str] = Field(description="Review text (legacy field)", default=None) + user: Optional[ReviewUser] = Field(description="Review author information", default=None) + author: Optional[str] = Field(description="Review author (legacy field)", default=None) date: Optional[str] = Field(description="Review date", default=None) + isoDate: Optional[str] = Field(description="Review date in ISO format", default=None) + likes: Optional[int] = Field(description="Number of likes on review", default=None) position: Optional[int] = Field(description="Result position", default=None) @@ -445,7 +457,7 @@ def search_maps( Enhanced geographic search with detailed place information, opening hours, and ratings. Perfect for finding local businesses and locations. You can specify GPS coordinates (ll: '@latitude,longitude,zoom'), Google Place ID (placeid), or business ID (cid). Page number defaults to 1. - + e in Args: q: Location search query api_key: Serper API key for authentication From 501f79e8b7cbf60ff15d4299c6f74dbbc0318c2f Mon Sep 17 00:00:00 2001 From: Aaron Yim Date: Mon, 8 Sep 2025 14:40:38 -0400 Subject: [PATCH 09/10] Remove empty line at beginning of file Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/serper/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index f37bf7af..0573d4b8 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -1,4 +1,3 @@ - import json import os from pathlib import Path From da4b26ccc23c55137b83ebf23d91eeaead484ade Mon Sep 17 00:00:00 2001 From: Aaron Yim Date: Mon, 8 Sep 2025 14:41:05 -0400 Subject: [PATCH 10/10] Remove duplicate field definition Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/serper/actions.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 0573d4b8..949edc1c 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -228,11 +228,17 @@ class EnhancedPlace(BaseModel): thumbnailUrl: Optional[str] = Field(description="Place thumbnail image", default=None) cid: Optional[str] = Field(description="Customer/place ID (numeric)", default=None) fid: Optional[str] = Field(description="Feature ID (hex format)", default=None) - placeId: Optional[str] = Field(description="Google Place ID", default=None) - # Keep legacy field for backward compatibility + placeId: Optional[str] = Field( + description="Google Place ID (accepts both 'placeId' and legacy 'placeid' keys)", + default=None, + alias="placeid", + alias_priority=2 + ) + # 'placeid' is supported as a legacy alias for backward compatibility category: str = Field(description="Business category (legacy field)", default="") - placeid: Optional[str] = Field(description="Google Place ID (legacy field)", default=None) + class Config: + allow_population_by_field_name = True class MapSearchResult(BaseModel): searchParameters: dict = Field(description="The search parameters used", default={})