From ecf3f80fc7e11ed6723e0cec2b289290b269f1ec Mon Sep 17 00:00:00 2001 From: Adarsh Divakaran Date: Thu, 13 Aug 2026 16:12:22 +0530 Subject: [PATCH] feat: add per-tool result limits and refine compact responses --- README.md | 180 ++++------- cookbook/agno/main.py | 7 +- cookbook/autogen/main.py | 5 +- cookbook/claude-agent-sdk/main.py | 5 +- cookbook/google-adk/main.py | 5 +- cookbook/haystack/main.py | 5 +- cookbook/langchain/main.py | 5 +- cookbook/langgraph/main.py | 7 +- cookbook/llamaindex/main.py | 2 +- cookbook/microsoft-agent-framework/main.py | 5 +- cookbook/openai-agents/main.py | 5 +- cookbook/pydantic-ai/main.py | 5 +- cookbook/semantic-kernel/main.py | 5 +- cookbook/smolagents/main.py | 8 +- docs/sdk_examples/agno.qmd | 2 +- docs/sdk_examples/autogen.qmd | 3 +- docs/sdk_examples/claude_agent_sdk.qmd | 3 +- docs/sdk_examples/crewai.qmd | 2 +- docs/sdk_examples/google_adk.qmd | 2 +- docs/sdk_examples/langgraph.qmd | 2 +- .../microsoft_agent_framework.qmd | 3 +- docs/sdk_examples/openai_agents.qmd | 3 +- docs/sdk_examples/pydantic_ai.qmd | 3 +- docs/sdk_examples/semantic_kernel.qmd | 1 - docs/user_guide/01-introduction.qmd | 5 +- docs/user_guide/02-quickstart.qmd | 3 +- docs/user_guide/04-configuration.qmd | 60 +++- docs/user_guide/06-recipes.qmd | 16 +- docs/user_guide/10-web_search.qmd | 22 +- docs/user_guide/11-news_search.qmd | 15 +- docs/user_guide/12-maps_search.qmd | 8 +- docs/user_guide/13-images_search.qmd | 8 +- docs/user_guide/14-shopping_search.qmd | 19 +- docs/user_guide/15-videos_search.qmd | 8 +- docs/user_guide/16-hotels_search.qmd | 8 +- docs/user_guide/17-flights_search.qmd | 16 +- docs/user_guide/18-travel_explore_search.qmd | 15 +- docs/user_guide/19-managing_llm_context.qmd | 113 +++++++ docs/user_guide/20-debugging.qmd | 5 +- examples/README.md | 4 +- examples/agno_grok.py | 5 +- examples/autogen_openai.py | 3 +- examples/claude_agent_sdk_sonnet.py | 3 +- examples/crewai_grok.py | 2 +- examples/direct_cached_search.py | 3 +- examples/direct_marketplace_comparison.py | 2 +- examples/direct_multi_search.py | 6 +- examples/direct_regioned_search.py | 6 +- examples/direct_search.py | 2 +- examples/google_adk_gemini.py | 2 +- examples/haystack_openai.py | 3 +- examples/langchain_grok.py | 3 +- examples/langgraph_openai.py | 6 +- examples/llamaindex_openai.py | 3 +- examples/microsoft_agent_framework_openai.py | 5 +- examples/openai_agents_openai.py | 7 +- examples/pydantic_ai_openai.py | 3 +- examples/smolagents_openai.py | 3 +- src/serpapi_search_tools/_query_tools.py | 34 +++ src/serpapi_search_tools/_shared.py | 120 +++++++- src/serpapi_search_tools/_travel_tools.py | 17 ++ tests/live_contracts.py | 8 +- tests/test_live.py | 8 +- tests/test_live_adapters.py | 4 +- tests/test_result_limit.py | 289 ++++++++++++++++++ tests/test_shared.py | 225 +++++++++++++- 66 files changed, 1058 insertions(+), 312 deletions(-) create mode 100644 docs/user_guide/19-managing_llm_context.qmd create mode 100644 tests/test_result_limit.py diff --git a/README.md b/README.md index d7f2045..1b80650 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ Give Python AI agents live web, news, maps, image, shopping, video, hotel, and flight search with easy-to-use, customizable tools. -[Read the full documentation](https://serpapi.github.io/serpapi-search-tools-python/) -for guides, SDK examples, recipes, and the API reference. +[Read the full documentation](https://serpapi.github.io/serpapi-search-tools-python/) for guides, SDK examples, recipes, and the API reference. -`serpapi-search-tools` connects [SerpApi](https://serpapi.com) to popular Python agent SDKs. Enhance your AI agent with real-time search data across the capabilities it needs: +The package creates native [SerpApi](https://serpapi.com) tools for popular Python agent SDKs: ```python from serpapi_search_tools import maps_search, news_search, web_search @@ -24,25 +23,6 @@ tools = [ When one supported agent SDK is installed, the package detects it and creates tools ready for that SDK. -## Supported agent SDKs - -| SDK | Install extra | Returned tool | -|---------------------------|-----------------------------|------------------------------------------| -| OpenAI Agents SDK | `openai-agents` | OpenAI Agents `FunctionTool` | -| Pydantic AI | `pydantic-ai` | Pydantic AI `Tool` | -| LangChain | `langchain` | LangChain `StructuredTool` | -| LangGraph | `langgraph` | LangChain-compatible structured tool | -| CrewAI | `crewai` | CrewAI `BaseTool` | -| LlamaIndex | `llamaindex` | LlamaIndex `FunctionTool` | -| Claude Agent SDK | `claude-agent-sdk` | Claude SDK MCP tool | -| Microsoft Agent Framework | `microsoft-agent-framework` | Microsoft Agent Framework `FunctionTool` | -| AutoGen | `autogen` | AutoGen `FunctionTool` | -| Haystack | `haystack` | Haystack `Tool` | -| Semantic Kernel | `semantic-kernel` | Semantic Kernel function | -| Agno | `agno` | Agno `Function` | -| smolagents | `smolagents` | smolagents `Tool` | -| Google ADK | `google-adk` | Google ADK `FunctionTool` | - ## Install If your agent SDK is already installed, add only the base package: @@ -57,7 +37,7 @@ If you want this package to install a compatible agent SDK too, choose its extra pip install "serpapi-search-tools[openai-agents]" ``` -Extras are available for all supported SDKs listed above. +Extras are available for all supported SDKs listed below. Set a SerpApi key: @@ -67,18 +47,34 @@ export SERPAPI_API_KEY="your-key" `SERPAPI_KEY` is also supported. A directly supplied `api_key=` takes precedence over environment variables. +## Supported agent SDKs + +| SDK | Install extra | Returned tool | +|---------------------------|-----------------------------|------------------------------------------| +| OpenAI Agents SDK | `openai-agents` | OpenAI Agents `FunctionTool` | +| Pydantic AI | `pydantic-ai` | Pydantic AI `Tool` | +| LangChain | `langchain` | LangChain `StructuredTool` | +| LangGraph | `langgraph` | LangChain-compatible structured tool | +| CrewAI | `crewai` | CrewAI `BaseTool` | +| LlamaIndex | `llamaindex` | LlamaIndex `FunctionTool` | +| Claude Agent SDK | `claude-agent-sdk` | Claude SDK MCP tool | +| Microsoft Agent Framework | `microsoft-agent-framework` | Microsoft Agent Framework `FunctionTool` | +| AutoGen | `autogen` | AutoGen `FunctionTool` | +| Haystack | `haystack` | Haystack `Tool` | +| Semantic Kernel | `semantic-kernel` | Semantic Kernel function | +| Agno | `agno` | Agno `Function` | +| smolagents | `smolagents` | smolagents `Tool` | +| Google ADK | `google-adk` | Google ADK `FunctionTool` | + ## Quickstart: automatic SDK detection -This quickstart uses OpenAI Agents SDK to demonstrate automatic detection. It assumes the SDK is already installed in -your environment (install it with -`pip install openai-agents` if needed). Then add the base package: +This quickstart uses OpenAI Agents SDK to demonstrate automatic detection. It assumes the SDK is already installed in your environment (install it with `pip install openai-agents` if needed). Then add the base package: ```bash pip install serpapi-search-tools ``` -With one supported SDK installed, create the tool without any configuration. The package detects OpenAI Agents SDK and -returns its native `FunctionTool`. This example also expects the `OPENAI_API_KEY` used by your agent. +With one supported SDK installed, create the tool without any configuration. The package detects OpenAI Agents SDK and returns its native `FunctionTool`. This example also expects the `OPENAI_API_KEY` used by your agent. ```python from agents import Agent, Runner @@ -106,10 +102,7 @@ Install the LangChain extra and the model backend used by this example: pip install "serpapi-search-tools[langchain]" langchain-openai ``` -The `langchain` extra installs a compatible LangChain version. -`langchain-openai` provides this example's model integration; replace it with the backend your LangChain application -uses. With `langchain-openai`, set -`OPENAI_API_KEY` before running the agent. +The `langchain` extra installs a compatible LangChain version. `langchain-openai` provides this example's model integration; replace it with the backend your LangChain application uses. With `langchain-openai`, set `OPENAI_API_KEY` before running the agent. ```python from langchain.agents import create_agent @@ -142,15 +135,11 @@ result = agent.invoke( print(result["messages"][-1].content) ``` -The constructors use automatic SDK detection, just as in the first quickstart. For multi-SDK environments and explicit -selection, see [Agent SDKs](https://serpapi.github.io/serpapi-search-tools-python/user-guide/frameworks.html). +The constructors use automatic SDK detection, just as in the first quickstart. For multi-SDK environments and explicit selection, see [Agent SDKs](https://serpapi.github.io/serpapi-search-tools-python/user-guide/frameworks.html). -For a step-by-step explanation, keys, customization, and troubleshooting, read -the [detailed quickstart](https://serpapi.github.io/serpapi-search-tools-python/user-guide/quickstart.html). +For a step-by-step explanation, keys, customization, and troubleshooting, read the [detailed quickstart](https://serpapi.github.io/serpapi-search-tools-python/user-guide/quickstart.html). -Browse the [runnable examples](https://github.com/serpapi/serpapi-search-tools-python/tree/main/examples) -for focused integrations or the [agent cookbook](https://serpapi.github.io/serpapi-search-tools-python/docs/cookbook/) -for complete, task-oriented agents built with every supported SDK. +Browse the [runnable examples](https://github.com/serpapi/serpapi-search-tools-python/tree/main/examples) for focused integrations or the [agent cookbook](https://serpapi.github.io/serpapi-search-tools-python/docs/cookbook/) for complete, task-oriented agents built with every supported SDK. ## Choose the right tool @@ -177,9 +166,7 @@ tool = web_search( ) ``` -SerpApi supports multiple general web search engines, including Google Light, Google, Bing, Yahoo, and DuckDuckGo. -Google Light is the default because it provides fast, general-purpose web results. Use `allowed_engines` to choose which -engines are available and `default_engine` to select the initial one. +SerpApi supports multiple general web search engines, including Google Light, Google, Bing, Yahoo, and DuckDuckGo. Google Light is the default because it provides fast, general-purpose web results. Use `allowed_engines` to choose which engines are available and `default_engine` to select the initial one. ### News, maps, images, and videos @@ -194,12 +181,9 @@ tools = [ ] ``` -`news_search` supports keyword searches in Google News. `maps_search` searches -Google Maps and accepts optional `location`, `zoom` (`3` through `30`), and -`nearby` fields. Use `nearby=True` for “near me” intent with a separate -`location`; leave it false when the query already names a city or area. Place -details, reviews, and directions use different SerpApi APIs and are not part of -this search tool. +`news_search` supports keyword searches in Google News. `maps_search` searches Google Maps and accepts optional `location`, `zoom` (`3` through `30`), and `nearby` fields. Use `nearby=True` for “near me” intent with a separate `location`; leave it false when the query already names a city or area. Place details, reviews, and directions use different SerpApi APIs and are not part of this search tool. + +`images_search` returns images and their source pages. `videos_search` searches YouTube videos, Shorts, channels, playlists, movies, and categories. ### Shopping @@ -216,9 +200,7 @@ tool = shopping_search( ) ``` -The package routes one human `query` to each marketplace's native field: -Google Shopping uses `q`, Amazon uses `k`, Walmart uses `query`, and eBay uses -`_nkw`. +Use the same `query` input for every marketplace. The package translates it to the selected engine's request format. ### Travel @@ -232,41 +214,27 @@ travel_tools = [ ] ``` -These constructors create hotel, flight, and destination-discovery tools for the detected agent SDK. - -#### Hotels - -`hotels_search` lets the agent provide a destination, dates, and guest details. Hotel dates use `YYYY-MM-DD`. Checkout -must be after check-in. When `children` -is nonzero, provide exactly one age from 1 through 17 per child; use `1` for a -child under one year old. - -#### Flights - -`flights_search` lets the agent provide route, date, cabin, and passenger details. It requires an origin, destination, -and outbound date. Omitting -`return_date` creates a one-way request; including it creates a round trip. -Use specific airport IATA codes such as `LHR` and `CDG`, not metropolitan codes -such as `LON` and `PAR`. For a city-wide search, use a Google Knowledge Graph -location ID (KGMID) beginning with `/m/` or `/g/`, such as `/m/04jpl` for -London or `/m/05qtj` for Paris. Multi-city searches are not currently -supported. +These constructors create hotel, flight, and destination-discovery tools for the detected agent SDK. Read the [Hotels](https://serpapi.github.io/serpapi-search-tools-python/user-guide/hotels_search.html), [Flights](https://serpapi.github.io/serpapi-search-tools-python/user-guide/flights_search.html), and [Travel Explore](https://serpapi.github.io/serpapi-search-tools-python/user-guide/travel_explore_search.html) guides for their date, occupancy, location ID, and trip rules. -#### Explore destinations +## Configure tools -`travel_explore_search` lets the agent discover destinations and requires only a departure airport IATA code or city -KGMID. It -can also accept a specific arrival airport or city through `arrival_id`, or a -region or country KGMID through `arrival_area_id`. Fixed outbound/return dates, -cabin class, and passenger counts are optional. These travel tools send their -route and date fields directly to the matching SerpApi endpoint. +Every constructor accepts: -## Set advanced parameters in application code +| Option | Purpose | +|--------------------|-------------------------------------------------------------------------------------------------------| +| `provider` | Defaults to `"auto"`; select an SDK explicitly only when multiple supported SDKs share an environment | +| `include_examples` | Include or omit a short example in the model description | +| `api_key` | Explicit SerpApi key | +| `client` | Custom object with `search(params)` for caching, interception, or tests | +| `default_params` | Application-controlled SerpApi options | +| `timeout` | Timeout passed to the SerpApi SDK client | +| `name` | Tool name presented to the model | +| `mode` | Result detail level; compact mode is the default, while full mode keeps supporting sections and all fields on retained results | +| `result_limit` | Maximum items kept in each result list in either mode; defaults vary by tool; use `None` for all results | -Use `default_params` for documented SerpApi settings that should stay under your application's control, such as locale, -currency, safe search, or result count. The agent continues to supply only the inputs described by its search tool. +`web_search` and `shopping_search` also accept `allowed_engines` and `default_engine`. The tool offers only the engine values you configure. -Applications can supply documented advanced options at construction time: +Use `default_params` for documented SerpApi settings that should stay under your application's control, such as locale, currency, safe search, or pagination. Use `result_limit` to control how many results the tool returns. The agent continues to supply only the inputs described by its search tool. ```python tool = news_search( @@ -274,38 +242,18 @@ tool = news_search( ) ``` -Typed fields and the constructor-controlled engine override colliding entries in `default_params`. Known incompatible -combinations are rejected locally, such as Google News query plus topic tokens, Amazon keyword search plus `node`, or -flight airline include plus exclude filters. For a multi-engine tool, the same defaults are sent to every allowed -engine. Use only parameters shared by those engines, or create separate tool instances when each engine needs different -defaults. Reserved keys (`api_key`, `async`, `engine`, and `output`) are rejected in -`default_params`; use the constructor options documented below instead. +Typed tool inputs override matching values in `default_params`, and the constructor always controls `engine`. The package rejects known incompatible combinations, such as a Google News query with a topic token, an Amazon keyword search with `node`, or flight airline include and exclude filters. A multi-engine tool sends the same defaults to every allowed engine, so use parameters shared by those engines or create separate tool instances. Reserved keys (`api_key`, `async`, `engine`, and `output`) are rejected in `default_params`; use the constructor options above instead. + +Read [Manage LLM context](https://serpapi.github.io/serpapi-search-tools-python/user-guide/managing_llm_context.html) for compact and full response behavior, default result limits, and unlimited responses. ## Handle search failures -The search runtime raises `SerpApiSearchError` for SerpApi and transport failures. Invalid tool inputs raise -`ValueError`. Agent SDKs surface or handle tool errors differently, so use your SDK's normal tool-error mechanism. See -[Debugging](https://serpapi.github.io/serpapi-search-tools-python/user-guide/debugging.html) for detailed examples. +The search runtime raises `SerpApiSearchError` for SerpApi and transport failures. Invalid tool inputs raise `ValueError`. Agent SDKs surface or handle tool errors differently, so use your SDK's normal tool-error mechanism. See [Debugging](https://serpapi.github.io/serpapi-search-tools-python/user-guide/debugging.html) for detailed examples. -## Common factory options +## Engine API references -Every constructor accepts: - -| Option | Purpose | -|--------------------|-------------------------------------------------------------------------------------------------------| -| `provider` | Defaults to `"auto"`; select an SDK explicitly only when multiple supported SDKs share an environment | -| `include_examples` | Include or omit a short example in the model description | -| `api_key` | Explicit SerpApi key | -| `client` | Custom object with `search(params)` for caching, interception, or tests | -| `default_params` | Application-controlled SerpApi options | -| `timeout` | Timeout passed to the SerpApi SDK client | -| `name` | Tool name presented to the model | -| `mode` | Result detail level: `"compact"` (default) or `"full"` | - -`web_search` and `shopping_search` additionally accept `allowed_engines` and -`default_engine`. The tool offers only the engine values you configured. - -## Supported engine documentation +
+Show supported SerpApi engines - [Google Search](https://serpapi.com/search-api) - [Google Light](https://serpapi.com/google-light-api) @@ -324,18 +272,12 @@ Every constructor accepts: - [Google Flights](https://serpapi.com/google-flights-api) - [Google Travel Explore](https://serpapi.com/google-travel-explore-api) -For AI coding agents that need broader SerpApi API context, use -[SerpApi's agent-friendly documentation index (`llms.txt`)](https://serpapi.com/llms.txt). It links directly to Markdown -API references, including APIs beyond those wrapped by this package. +
+ +For AI coding agents that need broader SerpApi API context, use [SerpApi's agent-friendly documentation index (`llms.txt`)](https://serpapi.com/llms.txt). It links directly to Markdown API references, including APIs beyond those wrapped by this package. ## More guides -- [Introduction](https://serpapi.github.io/serpapi-search-tools-python/user-guide/introduction.html) -- [Quickstart](https://serpapi.github.io/serpapi-search-tools-python/user-guide/quickstart.html) -- [Choose a search tool](https://serpapi.github.io/serpapi-search-tools-python/user-guide/search_tools.html) -- [Usage and composition](https://serpapi.github.io/serpapi-search-tools-python/user-guide/usage.html) -- [Configuration](https://serpapi.github.io/serpapi-search-tools-python/user-guide/configuration.html) -- [Recipes](https://serpapi.github.io/serpapi-search-tools-python/user-guide/recipes.html) -- [Agent SDKs](https://serpapi.github.io/serpapi-search-tools-python/user-guide/frameworks.html) +- [Full documentation](https://serpapi.github.io/serpapi-search-tools-python/) +- [SDK examples](https://serpapi.github.io/serpapi-search-tools-python/docs/sdk-examples/) - [Agent cookbook](https://serpapi.github.io/serpapi-search-tools-python/docs/cookbook/) -- [Runnable examples](https://serpapi.github.io/serpapi-search-tools-python/docs/sdk-examples/) diff --git a/cookbook/agno/main.py b/cookbook/agno/main.py index 6b0c559..351f17e 100644 --- a/cookbook/agno/main.py +++ b/cookbook/agno/main.py @@ -65,17 +65,18 @@ def main() -> None: web_search( provider="agno", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), news_search( provider="agno", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), shopping_search( provider="agno", - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), ], diff --git a/cookbook/autogen/main.py b/cookbook/autogen/main.py index a5d8d50..4c499f9 100644 --- a/cookbook/autogen/main.py +++ b/cookbook/autogen/main.py @@ -70,11 +70,12 @@ async def main() -> None: web_search( provider="autogen", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), news_search( provider="autogen", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), ], reflect_on_tool_use=True, diff --git a/cookbook/claude-agent-sdk/main.py b/cookbook/claude-agent-sdk/main.py index cbf4f89..55fc8dd 100644 --- a/cookbook/claude-agent-sdk/main.py +++ b/cookbook/claude-agent-sdk/main.py @@ -54,11 +54,12 @@ async def main() -> None: web_search( provider="claude-agent-sdk", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), news_search( provider="claude-agent-sdk", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), ], ) diff --git a/cookbook/google-adk/main.py b/cookbook/google-adk/main.py index ccbf42c..3c36957 100644 --- a/cookbook/google-adk/main.py +++ b/cookbook/google-adk/main.py @@ -79,12 +79,13 @@ async def main() -> None: web_search( provider="google-adk", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), news_search( provider="google-adk", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), ], diff --git a/cookbook/haystack/main.py b/cookbook/haystack/main.py index 899f952..39bc2ad 100644 --- a/cookbook/haystack/main.py +++ b/cookbook/haystack/main.py @@ -73,12 +73,13 @@ def main() -> None: tools=[ news_search( provider="haystack", - default_params={"num": "7", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=7, ), web_search( provider="haystack", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), ], max_agent_steps=8, diff --git a/cookbook/langchain/main.py b/cookbook/langchain/main.py index e96cba8..c8d3e1f 100644 --- a/cookbook/langchain/main.py +++ b/cookbook/langchain/main.py @@ -59,12 +59,13 @@ def main() -> None: web_search( provider="langchain", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), news_search( provider="langchain", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), ], diff --git a/cookbook/langgraph/main.py b/cookbook/langgraph/main.py index ffdcbe7..cbed201 100644 --- a/cookbook/langgraph/main.py +++ b/cookbook/langgraph/main.py @@ -54,15 +54,16 @@ def main() -> None: web_search( provider="langgraph", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), news_search( provider="langgraph", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), shopping_search( provider="langgraph", - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), ] model = ChatOpenAI( diff --git a/cookbook/llamaindex/main.py b/cookbook/llamaindex/main.py index 7fda5aa..aeeffda 100644 --- a/cookbook/llamaindex/main.py +++ b/cookbook/llamaindex/main.py @@ -79,7 +79,7 @@ async def main() -> None: web_search( provider="llamaindex", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, api_key=api_key, ), maps_search( diff --git a/cookbook/microsoft-agent-framework/main.py b/cookbook/microsoft-agent-framework/main.py index fb5d78d..9560875 100644 --- a/cookbook/microsoft-agent-framework/main.py +++ b/cookbook/microsoft-agent-framework/main.py @@ -60,12 +60,13 @@ async def main() -> None: tools=[ news_search( provider="microsoft-agent-framework", - default_params={"num": "8", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=8, ), web_search( provider="microsoft-agent-framework", allowed_engines=["google_light", "bing"], - default_params={"num": "6", "hl": "en", "gl": "us"}, + result_limit=6, ), ], ) diff --git a/cookbook/openai-agents/main.py b/cookbook/openai-agents/main.py index 3b22286..1e7c78c 100644 --- a/cookbook/openai-agents/main.py +++ b/cookbook/openai-agents/main.py @@ -56,11 +56,12 @@ async def main() -> None: web_search( provider="openai-agents", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), news_search( provider="openai-agents", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), ], ) diff --git a/cookbook/pydantic-ai/main.py b/cookbook/pydantic-ai/main.py index 9d1f253..8c2b746 100644 --- a/cookbook/pydantic-ai/main.py +++ b/cookbook/pydantic-ai/main.py @@ -59,7 +59,8 @@ def main() -> None: tools=[ images_search( provider="pydantic-ai", - default_params={"num": "5", "hl": "en", "gl": "us", "safe": "active"}, + default_params={"hl": "en", "gl": "us", "safe": "active"}, + result_limit=5, ), maps_search( provider="pydantic-ai", @@ -68,7 +69,7 @@ def main() -> None: web_search( provider="pydantic-ai", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), ], ) diff --git a/cookbook/semantic-kernel/main.py b/cookbook/semantic-kernel/main.py index 2ee08be..cb8bbff 100644 --- a/cookbook/semantic-kernel/main.py +++ b/cookbook/semantic-kernel/main.py @@ -56,11 +56,12 @@ async def main() -> None: web_search( provider="semantic-kernel", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), news_search( provider="semantic-kernel", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), ], ) diff --git a/cookbook/smolagents/main.py b/cookbook/smolagents/main.py index 6577987..fec2d14 100644 --- a/cookbook/smolagents/main.py +++ b/cookbook/smolagents/main.py @@ -54,17 +54,19 @@ def main() -> None: tools=[ shopping_search( provider="smolagents", - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), images_search( provider="smolagents", - default_params={"num": "5", "hl": "en", "gl": "us", "safe": "active"}, + default_params={"hl": "en", "gl": "us", "safe": "active"}, + result_limit=5, include_examples=False, ), videos_search( provider="smolagents", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ), ], diff --git a/docs/sdk_examples/agno.qmd b/docs/sdk_examples/agno.qmd index 7fe3590..b2e8a29 100644 --- a/docs/sdk_examples/agno.qmd +++ b/docs/sdk_examples/agno.qmd @@ -52,7 +52,7 @@ main trade-offs.” products = shopping_search( allowed_engines=["google_shopping", "amazon"], default_engine="google_shopping", - default_params={"num": 3, "hl": "en", "gl": "us"}, + result_limit=3, ) ``` diff --git a/docs/sdk_examples/autogen.qmd b/docs/sdk_examples/autogen.qmd index 9554030..87cc695 100644 --- a/docs/sdk_examples/autogen.qmd +++ b/docs/sdk_examples/autogen.qmd @@ -69,7 +69,8 @@ trend.” ```python current_news = news_search( - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ) ``` diff --git a/docs/sdk_examples/claude_agent_sdk.qmd b/docs/sdk_examples/claude_agent_sdk.qmd index 376da6c..aec35fa 100644 --- a/docs/sdk_examples/claude_agent_sdk.qmd +++ b/docs/sdk_examples/claude_agent_sdk.qmd @@ -66,7 +66,8 @@ what changed. ```python current_news = news_search( - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, name="current_news", ) ``` diff --git a/docs/sdk_examples/crewai.qmd b/docs/sdk_examples/crewai.qmd index 77478b4..d45272e 100644 --- a/docs/sdk_examples/crewai.qmd +++ b/docs/sdk_examples/crewai.qmd @@ -60,7 +60,7 @@ practical impact of each. products = shopping_search( allowed_engines=["google_shopping", "amazon"], default_engine="google_shopping", - default_params={"num": 3, "hl": "en", "gl": "us"}, + result_limit=3, ) ``` diff --git a/docs/sdk_examples/google_adk.qmd b/docs/sdk_examples/google_adk.qmd index 9a89265..8674d18 100644 --- a/docs/sdk_examples/google_adk.qmd +++ b/docs/sdk_examples/google_adk.qmd @@ -77,7 +77,7 @@ useful features.” products = shopping_search( allowed_engines=["google_shopping", "walmart"], default_engine="google_shopping", - default_params={"num": 3, "hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ) ``` diff --git a/docs/sdk_examples/langgraph.qmd b/docs/sdk_examples/langgraph.qmd index 817f637..a3471e1 100644 --- a/docs/sdk_examples/langgraph.qmd +++ b/docs/sdk_examples/langgraph.qmd @@ -66,7 +66,7 @@ checklist.” multi_source_search = web_search( allowed_engines=["google", "bing", "duckduckgo"], default_engine="google", - default_params={"num": 5, "hl": "en"}, + result_limit=5, ) ``` diff --git a/docs/sdk_examples/microsoft_agent_framework.qmd b/docs/sdk_examples/microsoft_agent_framework.qmd index 177f9b9..d87a717 100644 --- a/docs/sdk_examples/microsoft_agent_framework.qmd +++ b/docs/sdk_examples/microsoft_agent_framework.qmd @@ -59,7 +59,8 @@ important company claims, and cite the original sources.” ```python current_news = news_search( provider="microsoft-agent-framework", - default_params={"num": 5, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ) ``` diff --git a/docs/sdk_examples/openai_agents.qmd b/docs/sdk_examples/openai_agents.qmd index efee8a3..0ab5de6 100644 --- a/docs/sdk_examples/openai_agents.qmd +++ b/docs/sdk_examples/openai_agents.qmd @@ -64,7 +64,8 @@ one matters.” ```python current_news = news_search( - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ) ``` diff --git a/docs/sdk_examples/pydantic_ai.qmd b/docs/sdk_examples/pydantic_ai.qmd index 355b4a9..dcf408f 100644 --- a/docs/sdk_examples/pydantic_ai.qmd +++ b/docs/sdk_examples/pydantic_ai.qmd @@ -58,7 +58,8 @@ themes.” ```python image_search = images_search( - default_params={"num": 5, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, include_examples=False, ) ``` diff --git a/docs/sdk_examples/semantic_kernel.qmd b/docs/sdk_examples/semantic_kernel.qmd index 5359e44..3351d53 100644 --- a/docs/sdk_examples/semantic_kernel.qmd +++ b/docs/sdk_examples/semantic_kernel.qmd @@ -66,7 +66,6 @@ first three Google results. privacy_search = web_search( allowed_engines=["duckduckgo", "bing"], default_engine="duckduckgo", - default_params={"hl": "en"}, include_examples=False, name="privacy_search", ) diff --git a/docs/user_guide/01-introduction.qmd b/docs/user_guide/01-introduction.qmd index 2dfc30c..1d8007f 100644 --- a/docs/user_guide/01-introduction.qmd +++ b/docs/user_guide/01-introduction.qmd @@ -70,10 +70,7 @@ response = json.loads(search(query="Python packaging")) print(response.get("organic_results", [])) ``` -Every tool returns compact JSON text by default: only its primary result -families, bounded to five entries, plus useful web answer sections. Application -code can opt into the untouched response with -`mode=SearchResultMode.FULL`. +Every tool returns compact JSON text by default, including its main result sections and useful web answer sections. `result_limit` controls how many items are kept in each result list in compact and full modes. Set `mode=SearchResultMode.FULL` and `result_limit=None` when application code needs every response section, field, and returned result. ## Next steps diff --git a/docs/user_guide/02-quickstart.qmd b/docs/user_guide/02-quickstart.qmd index 6c1e143..ad786d9 100644 --- a/docs/user_guide/02-quickstart.qmd +++ b/docs/user_guide/02-quickstart.qmd @@ -134,7 +134,8 @@ from serpapi_search_tools import web_search tool = web_search( allowed_engines=["google_light"], - default_params={"num": 5, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, name="current_web_research", ) ``` diff --git a/docs/user_guide/04-configuration.qmd b/docs/user_guide/04-configuration.qmd index 7156b67..0753986 100644 --- a/docs/user_guide/04-configuration.qmd +++ b/docs/user_guide/04-configuration.qmd @@ -29,7 +29,7 @@ It helps to keep three layers separate: 2. **Constructor options** control how the Python tool is created, such as `provider`, `name`, `timeout`, and web-engine choices. 3. **`default_params`** are SerpApi engine parameters fixed by your application, - such as language, country, currency, safe search, or result count. + such as language, country, currency, safe search, or pagination. This separation keeps the agent's search inputs short while your application retains control of service-level settings. @@ -43,9 +43,10 @@ All nine constructors accept these options: | `provider` | `"auto"` | Usually omit it. Select an SDK explicitly when multiple supported SDKs are installed, or use `"function"` for a callable. | | `api_key` | `None` | Pass a SerpApi key from a secret manager instead of using an environment variable. | | `client` | `None` | Replace the built-in client for tests, caching, logging, retries, or response reduction. | -| `default_params` | `None` | Set supported SerpApi options such as locale, currency, or result count in application code. | +| `default_params` | `None` | Set supported SerpApi options such as locale, currency, filters, or pagination in application code. | | `timeout` | `None` | Set a request timeout on the built-in SerpApi client. | -| `mode` | `SearchResultMode.COMPACT` | Return focused, bounded result families for agents, or opt into the untouched response with `SearchResultMode.FULL`. | +| `mode` | `SearchResultMode.COMPACT` | Return the main result data; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results. | +| `result_limit` | tool-specific | Set the maximum number of items kept in each result list in either mode. Use `None` to keep all returned items. | | `include_examples` | `True` | Keep or remove the short invocation hint in the tool description. | | `name` | constructor name | Give separately configured tools distinct names the model can understand. | @@ -53,7 +54,8 @@ All nine constructors accept these options: from serpapi_search_tools import news_search current_us_news = news_search( - default_params={"hl": "en", "gl": "us", "num": 5}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, timeout=20.0, name="current_us_news", ) @@ -95,7 +97,7 @@ The search inputs and useful SerpApi parameters depend on the capability: | Tool | Tool-specific configuration | | --- | --- | | [`web_search`](web_search.html) | `allowed_engines`, `default_engine`, plus per-engine web defaults | -| [`news_search`](news_search.html) | Google News query-mode locale and result settings | +| [`news_search`](news_search.html) | Google News query-mode locale and query filters | | [`maps_search`](maps_search.html) | Typed location/zoom/nearby inputs plus Maps defaults | | [`images_search`](images_search.html) | Safe search and image filters | | [`shopping_search`](shopping_search.html) | `allowed_engines`, `default_engine`, and marketplace-specific defaults | @@ -108,27 +110,46 @@ Read the dedicated page before adding engine-specific `default_params`. ## Choose compact or full results -All tools default to `SearchResultMode.COMPACT`. Compact mode normally removes -response metadata, parameters, pagination, filters, and other auxiliary -sections. It keeps errors and at most five entries from each primary result -family. If a successful response contains no recognized result family, it -returns `no_results: true` plus only safe status and search-information fields -instead of an ambiguous empty object. +All tools default to `SearchResultMode.COMPACT`. Compact mode keeps the main result sections and omits metadata, request parameters, pagination, filters, and other supporting sections. Non-list answer sections such as `answer_box` are kept whole. If a successful response has no supported result section, the tool returns `no_results: true` with available status and search information. + +| Tool | Default `result_limit` | +| --- | ---: | +| `web_search`, `maps_search`, `videos_search`, `flights_search` | `10` | +| `news_search` | `20` | +| `images_search` | `50` | +| `shopping_search` | `60` | +| `hotels_search` | `20` | +| `travel_explore_search` | `50` | | Tool | Compact result families | | --- | --- | -| `web_search` except Google Light | `answer_box`, `knowledge_graph`, `ai_overview`, `organic_results` | +| `web_search` with Google | `answer_box`, `knowledge_graph`, `ai_overview`, `organic_results` | | `web_search` with Google Light | `answer_box`, `knowledge_graph`, `organic_results`, `related_questions`, `related_searches`, `top_stories` | +| `web_search` with Bing | `answer_box`, `knowledge_graph`, `copilot_answer`, `organic_results` | +| `web_search` with Yahoo | `answer_box`, `knowledge_graph`, `organic_results` | +| `web_search` with DuckDuckGo | `knowledge_graph`, `organic_results` | | `news_search` | `news_results` | | `maps_search` | `local_results` | | `images_search` | `images_results` | | `shopping_search` | `shopping_results` for Google Shopping; `organic_results` for Amazon, Walmart, and eBay | -| `videos_search` | `video_results` | +| `videos_search` | `video_results`, `shorts_results`, `channel_results`, `playlist_results`, `movie_results`, `category_results` | | `hotels_search` | `properties` | | `flights_search` | `best_flights`, `other_flights` | | `travel_explore_search` | `destinations` | -Use full mode in application code that needs an auxiliary response section: +`result_limit` is application configuration rather than a model input: + +```python +from serpapi_search_tools import web_search + +broader_web_search = web_search( + result_limit=20, +) +``` + +`result_limit` changes the returned JSON, not the SerpApi request. It applies independently to each supported result list and does not shorten nested data such as flight legs, hotel amenities, or product offers. See the [`web_search`](web_search.html) and [`shopping_search`](shopping_search.html) pages for engine-specific retrieval and pagination options. + +Use full mode when application code needs supporting response sections or additional result fields. The tool's default result limit still applies: ```python from serpapi_search_tools import SearchResultMode, web_search @@ -136,12 +157,17 @@ from serpapi_search_tools import SearchResultMode, web_search debug_search = web_search( provider="function", mode=SearchResultMode.FULL, + result_limit=10, ) ``` +To keep the complete SerpApi response, including all returned results, set both `mode=SearchResultMode.FULL` and `result_limit=None`. + `mode` is a constructor option, not a model input. An agent cannot expand its own tool response and unexpectedly consume more context. +See [Manage LLM context](managing_llm_context.html) for examples using compact, full, limited, and unlimited responses. + ## Restrict web and shopping engines Only `web_search` and `shopping_search` expose an `engine` choice to the model. @@ -190,12 +216,14 @@ from serpapi_search_tools import web_search us_google = web_search( allowed_engines=["google_light"], - default_params={"hl": "en", "gl": "us", "num": 5}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, name="us_google", ) german_google = web_search( allowed_engines=["google_light"], - default_params={"hl": "de", "gl": "de", "num": 5}, + default_params={"hl": "de", "gl": "de"}, + result_limit=5, name="german_google", ) ``` diff --git a/docs/user_guide/06-recipes.qmd b/docs/user_guide/06-recipes.qmd index d823b50..2db1de1 100644 --- a/docs/user_guide/06-recipes.qmd +++ b/docs/user_guide/06-recipes.qmd @@ -7,7 +7,8 @@ guide-section: Start Here You rarely need every search capability. Give the agent the smallest useful set, name specialized configurations clearly, and keep policy choices such as -locale, currency, safe search, and result count in `default_params`. +locale, currency, and safe search in `default_params`. Use `result_limit` for +the number of results returned by the tool. The examples below use automatic SDK detection. They work as written when one supported agent SDK is installed. If multiple SDKs share the environment, add @@ -24,7 +25,7 @@ tools = [ web_search( allowed_engines=["google_light", "bing"], default_engine="google_light", - default_params={"num": 5}, + result_limit=5, ) ] ``` @@ -85,12 +86,13 @@ from serpapi_search_tools import shopping_search tools = [ shopping_search( allowed_engines=["google_shopping"], - default_params={"num": 5, "gl": "us", "hl": "en"}, + default_params={"gl": "us", "hl": "en"}, + result_limit=5, name="google_products", ), shopping_search( allowed_engines=["amazon"], - default_params={"num": 5}, + result_limit=5, name="amazon_products", ), ] @@ -225,11 +227,7 @@ preferences. ## Keep agent payloads manageable -Every tool returns a compact response by default, keeping only a bounded number -of primary results before the JSON reaches the model. Use a custom `client=` -for application-specific logging or caching, and opt into -`SearchResultMode.FULL` only outside model context when auxiliary sections are -required. +Every tool returns a compact response by default. Set `result_limit` on the tool constructor to control how many items are kept in each result list. Use a custom `client=` for application-specific logging or caching. Choose `SearchResultMode.FULL` when application code needs supporting response sections or additional result fields. Continue with [Runnable examples](examples.html), [Common configuration](configuration.html), or diff --git a/docs/user_guide/10-web_search.qmd b/docs/user_guide/10-web_search.qmd index a77596e..3a377cc 100644 --- a/docs/user_guide/10-web_search.qmd +++ b/docs/user_guide/10-web_search.qmd @@ -49,8 +49,9 @@ Google, Google Light, Bing, and DuckDuckGo use `q`; Yahoo uses `p`. | `provider` | `"auto"` | Detect the installed SDK; use `"function"` for a callable or an explicit SDK name in multi-SDK environments | | `allowed_engines` | all five engines | Restrict the enum visible to the model | | `default_engine` | `google_light` when allowed | Choose the engine used when the model omits `engine` | -| `default_params` | `None` | Fix engine parameters such as locale or result count in application code | -| `mode` | `compact` | Return focused web results; use `SearchResultMode.FULL` for the untouched response | +| `default_params` | `None` | Fix engine parameters such as locale, filters, or pagination in application code | +| `mode` | `compact` | Return the main web result sections; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `10` | Maximum items kept in each web result list in either mode | | `api_key` | `None` | Override the environment-based SerpApi key | | `timeout` | `None` | Set the built-in client's request timeout | | `client` | `None` | Supply a testing, caching, logging, or custom HTTP client | @@ -72,15 +73,16 @@ tool = web_search( ## Useful SerpApi parameters `default_params` is forwarded to the selected engine and is not editable by -the model. This Google Light-only tool fixes country, language, and result -count: +the model. This Google Light-only tool fixes country and language while +`result_limit` bounds the returned result list: ```python from serpapi_search_tools import web_search us_web = web_search( allowed_engines=["google_light"], - default_params={"gl": "us", "hl": "en", "num": 5}, + default_params={"gl": "us", "hl": "en"}, + result_limit=5, name="us_web_search", ) ``` @@ -90,15 +92,13 @@ and date filters. Parameter names and accepted values differ by engine. If you need engine-specific defaults, create separate tool instances instead of sending one `default_params` dictionary to several incompatible engines. +Use `result_limit` for a consistent limit across web engines. A DuckDuckGo-only tool can also use `default_params={"m": 20}` to control how many results DuckDuckGo retrieves. Pagination parameters differ by engine; see the linked API references for supported values. + ## What comes back -Compact mode returns up to five `organic_results` and, when present, the -`answer_box`, `knowledge_graph`, and `ai_overview`. It omits related searches, -pagination, metadata, parameters, and other auxiliary sections. +Both modes keep up to `result_limit` items in each web result list (10 by default). Compact mode returns `organic_results` and available answer sections. Google Light may also return related questions, related searches, and top stories. Metadata, parameters, pagination, and other supporting sections are omitted. -Treat sections as optional: a valid search may not contain every result family. -Use `mode=SearchResultMode.FULL` when application code needs the untouched -SerpApi response. +Result sections are optional. Use `mode=SearchResultMode.FULL` when application code needs supporting response sections or additional result fields. Add `result_limit=None` to keep every returned result. ## Common mistakes diff --git a/docs/user_guide/11-news_search.qmd b/docs/user_guide/11-news_search.qmd index 6513d07..e1d20f9 100644 --- a/docs/user_guide/11-news_search.qmd +++ b/docs/user_guide/11-news_search.qmd @@ -45,8 +45,9 @@ field or SerpApi's native `q` name. | Constructor option | Default | Use it for | | --- | --- | --- | | `provider` | `"auto"` | Auto-detect one installed SDK or select one explicitly | -| `default_params` | `None` | Fix locale, country, or bounded result settings | -| `mode` | `compact` | Return up to five news results; use `SearchResultMode.FULL` for the untouched response | +| `default_params` | `None` | Fix locale, country, or supported query filters | +| `mode` | `compact` | Return the main news result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `20` | Maximum `news_results` retained in either mode | | `api_key` | `None` | Override `SERPAPI_API_KEY` / `SERPAPI_KEY` | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Provide a custom search client | @@ -61,7 +62,8 @@ Keep locale and country decisions in the application: from serpapi_search_tools import news_search us_news = news_search( - default_params={"hl": "en", "gl": "us", "num": 5}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, name="us_news", ) ``` @@ -72,9 +74,10 @@ token fields such as `topic_token`, `publication_token`, `section_token`, and ## What comes back -Compact mode returns up to five `news_results`. Items commonly include a title, -source, date, snippet, thumbnail, and link. Metadata and parameters are omitted; -use `mode=SearchResultMode.FULL` when application code needs them. +Both modes return up to `result_limit` `news_results` (20 by default). Items +commonly include a title, source, date, snippet, thumbnail, and link. Compact +mode omits metadata and parameters; use `mode=SearchResultMode.FULL` when +application code needs them, and add `result_limit=None` to keep every result. ## Common mistakes diff --git a/docs/user_guide/12-maps_search.qmd b/docs/user_guide/12-maps_search.qmd index a0e6146..cd87411 100644 --- a/docs/user_guide/12-maps_search.qmd +++ b/docs/user_guide/12-maps_search.qmd @@ -51,7 +51,8 @@ field. | --- | --- | --- | | `provider` | `"auto"` | Detect one installed SDK or choose an SDK explicitly | | `default_params` | `None` | Fix Google Maps locale or supported search filters | -| `mode` | `compact` | Return up to five local results; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main local result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `10` | Maximum `local_results` retained in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add deterministic tests, caching, or request logging | @@ -78,10 +79,7 @@ this search-mode tool. ## What comes back -Compact mode returns up to five `local_results`. Place entries may include a -title, address, rating, review count, category, phone, website, hours, -coordinates, thumbnail, and identifiers. Availability varies by place and -query. +Both modes return up to `result_limit` `local_results` (10 by default). Place entries may include a title, address, rating, review count, category, phone, website, hours, coordinates, thumbnail, and identifiers. Compact mode omits supporting sections. Use `result_limit=None` to keep every returned place. Availability varies by place and query. ## Common mistakes diff --git a/docs/user_guide/13-images_search.qmd b/docs/user_guide/13-images_search.qmd index 30d2c47..0df33dc 100644 --- a/docs/user_guide/13-images_search.qmd +++ b/docs/user_guide/13-images_search.qmd @@ -42,7 +42,8 @@ image query. | --- | --- | --- | | `provider` | `"auto"` | Detect the installed agent SDK | | `default_params` | `None` | Fix safe search, locale, or other supported image filters | -| `mode` | `compact` | Return up to five image results; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main image result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `50` | Maximum `images_results` retained in either mode; use `None` for all returned images | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Provide a custom client for logging, caching, or tests | @@ -69,10 +70,7 @@ with explicit start/end dates. ## What comes back -Compact mode returns up to five `images_results`. Items can include title, -original image URL, thumbnail, source page, dimensions, source name, and -position. Your application should not assume every result contains every -optional field. +Both modes return up to `result_limit` `images_results` (50 by default). Compact mode keeps the image URLs, thumbnails, source pages, dimensions, titles, and other useful result data while omitting supporting response sections and redundant navigation fields. Use `result_limit=None` to keep all returned images. Result fields are optional, so normalization should handle missing values. ## Common mistakes diff --git a/docs/user_guide/14-shopping_search.qmd b/docs/user_guide/14-shopping_search.qmd index 51f9e8a..412b64d 100644 --- a/docs/user_guide/14-shopping_search.qmd +++ b/docs/user_guide/14-shopping_search.qmd @@ -48,8 +48,9 @@ Google Shopping, `k` for Amazon, `query` for Walmart, and `_nkw` for eBay. | `provider` | `"auto"` | Detect one installed SDK or select one explicitly | | `allowed_engines` | all four engines | Limit the marketplaces visible to the model | | `default_engine` | `google_shopping` when allowed | Select the marketplace used when `engine` is omitted | -| `default_params` | `None` | Fix result count or engine-specific marketplace filters | -| `mode` | `compact` | Return up to five primary product results; use `SearchResultMode.FULL` for the untouched response | +| `default_params` | `None` | Fix engine-specific marketplace filters or pagination | +| `mode` | `compact` | Return the main product result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `60` | Maximum primary product results retained in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add result normalization, caching, logging, or tests | @@ -63,22 +64,20 @@ from serpapi_search_tools import shopping_search google_products = shopping_search( allowed_engines=["google_shopping"], - default_params={"gl": "us", "hl": "en", "num": 5}, + default_params={"gl": "us", "hl": "en"}, + result_limit=5, name="google_products", ) amazon_products = shopping_search( allowed_engines=["amazon"], - default_params={"num": 5}, + result_limit=5, name="amazon_products", ) ``` ## Useful SerpApi parameters -`num` is useful for bounding a first result page. Locale, sorting, category, -condition, delivery, and price-filter parameters are marketplace-specific. -Follow the matching official reference rather than assuming a Google Shopping -parameter will work on Amazon, Walmart, or eBay. +Use `result_limit` for a consistent limit across marketplaces. An eBay-only tool can also use `_ipg` to set the retrieved page size. Locale, sorting, category, condition, delivery, price, and pagination parameters differ by marketplace. See the [Google Shopping](https://serpapi.com/google-shopping-api), [Amazon](https://serpapi.com/amazon-search-api), [Walmart](https://serpapi.com/walmart-search-api), and [eBay](https://serpapi.com/ebay-search-api) references for supported values. For a multi-engine tool, the same `default_params` dictionary is sent to every allowed engine. Restrict `allowed_engines` or use separate named tools before @@ -86,9 +85,7 @@ adding engine-specific values. ## What comes back -Compact mode returns up to five primary product results. Google Shopping uses -`shopping_results`; Amazon, Walmart, and eBay use `organic_results`. Titles, -prices, links, ratings, delivery details, and seller fields also vary. +Both modes return up to `result_limit` primary product results (60 by default). Google Shopping uses `shopping_results`; Amazon, Walmart, and eBay use `organic_results`. Compact mode keeps useful product details such as titles, prices, links, ratings, delivery information, and seller data while omitting supporting response sections and redundant fields. Use `result_limit=None` to keep every returned product. Normalize only fields your application needs and keep the original engine in the normalized record. diff --git a/docs/user_guide/15-videos_search.qmd b/docs/user_guide/15-videos_search.qmd index d127413..99feed3 100644 --- a/docs/user_guide/15-videos_search.qmd +++ b/docs/user_guide/15-videos_search.qmd @@ -42,7 +42,8 @@ YouTube's `search_query` parameter. | --- | --- | --- | | `provider` | `"auto"` | Detect one installed SDK or select one explicitly | | `default_params` | `None` | Fix locale or supported YouTube search filters | -| `mode` | `compact` | Return up to five video results; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main YouTube result sections; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `10` | Maximum entries retained from each supported YouTube result list in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add caching, logging, tests, or response reduction | @@ -68,10 +69,7 @@ valid token rather than inventing filter fields. ## What comes back -Compact mode returns up to five `video_results`. Entries commonly include -title, link, thumbnail, channel, duration, views, and publication information. -Use full mode when application code needs auxiliary channel or playlist -sections. +Both modes apply `result_limit` independently to videos, Shorts, channels, playlists, movies, and categories, keeping up to 10 items in each list by default. Entries commonly include a title, link, thumbnail, channel, duration, views, and publication information. Use full mode when application code needs supporting response sections or additional result fields. Add `result_limit=None` to keep every returned item. ## Common mistakes diff --git a/docs/user_guide/16-hotels_search.qmd b/docs/user_guide/16-hotels_search.qmd index f3b4d69..917079c 100644 --- a/docs/user_guide/16-hotels_search.qmd +++ b/docs/user_guide/16-hotels_search.qmd @@ -59,7 +59,8 @@ An agent invocation has a structured shape: | --- | --- | --- | | `provider` | `"auto"` | Detect one installed SDK or select one explicitly | | `default_params` | `None` | Fix currency, locale, country, or supported property filters | -| `mode` | `compact` | Return up to five properties; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main property result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `20` | Maximum `properties` retained in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add caching, request logging, or deterministic tests | @@ -86,10 +87,7 @@ not duplicate values in `default_params`. ## What comes back -Compact mode returns up to five `properties`. Entries may include property -names, links, GPS coordinates, ratings, amenities, images, extracted prices, -rate-per-night details, and nearby places. Use full mode for brands, filters, -metadata, or pagination. +Both modes return up to `result_limit` `properties` (20 by default). Compact entries include useful property details such as names, links, coordinates, ratings, amenities, prices, rates, and a preview image. Use full mode when application code needs supporting response sections or additional property fields. Add `result_limit=None` to keep every returned property. ## Common mistakes diff --git a/docs/user_guide/17-flights_search.qmd b/docs/user_guide/17-flights_search.qmd index 2827343..e2a949c 100644 --- a/docs/user_guide/17-flights_search.qmd +++ b/docs/user_guide/17-flights_search.qmd @@ -47,8 +47,8 @@ Example invocation: | Field | Type | Required | Default and constraints | | --- | --- | --- | --- | -| `departure_id` | string | yes | Specific airport IATA code such as `LHR`, or a city Google Knowledge Graph location ID (KGMID) beginning with `/m/` or `/g/`, such as `/m/04jpl` for London; do not use metropolitan codes such as `LON`; comma-separated values are supported | -| `arrival_id` | string | yes | Specific airport IATA code such as `CDG`, or a city KGMID beginning with `/m/` or `/g/`, such as `/m/05qtj` for Paris; do not use metropolitan codes such as `PAR`; comma-separated values are supported | +| `departure_id` | string | yes | Specific airport IATA code such as `LHR`, or an exact city Google Knowledge Graph location ID (KGMID) beginning with `/m/` or `/g/`, such as `/m/04jpl` for London; metropolitan codes such as `LON` are unsupported; see the [Google Flights API](https://serpapi.com/google-flights-api); comma-separated values are supported | +| `arrival_id` | string | yes | Specific airport IATA code such as `CDG`, or an exact city KGMID beginning with `/m/` or `/g/`, such as `/m/05qtj` for Paris; metropolitan codes such as `PAR` are unsupported; see the [Google Flights API](https://serpapi.com/google-flights-api); comma-separated values are supported | | `outbound_date` | date string | yes | Future date in `YYYY-MM-DD` format | | `return_date` | date string | no | Provide for a round trip; omit for one way; must not be before outbound | | `travel_class` | enum | no | `economy`; also `premium_economy`, `business`, or `first` | @@ -69,7 +69,8 @@ a round trip. | --- | --- | --- | | `provider` | `"auto"` | Detect one installed SDK or select one explicitly | | `default_params` | `None` | Fix currency, locale, stops, airline, bag, or time filters supported by SerpApi | -| `mode` | `compact` | Return up to five best and five other itineraries; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main flight result sections; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `10` | Maximum entries retained independently from `best_flights` and `other_flights` in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add caching, logging, testing, or response compaction | @@ -96,21 +97,18 @@ filters need a round trip, and bag counts cannot exceed eligible passengers. ## What comes back -Compact mode returns up to five `best_flights` and five `other_flights`. -Itinerary objects can contain one or more flight legs, layovers, total duration, -carbon emissions, price, airline, and booking tokens. Use full mode for price -insights, airports, or metadata. +Both modes apply `result_limit` independently to `best_flights` and `other_flights`, keeping up to 10 itineraries in each list by default. Flight legs and layovers within an itinerary are not shortened. Use full mode for supporting sections such as price insights, airports, and metadata. Add `result_limit=None` to keep every returned itinerary. ## Common mistakes -- Sending a text `q`; flight tools never use one. +- Flight searches use structured route and date fields; there is no `q` input. - Omitting the route or outbound date. - Using a metropolitan code such as `LON` or `PAR` instead of a specific airport code or city KGMID. - Supplying a return date before outbound. - Asking for multi-city itineraries through this one-route schema. - Combining both `include_airlines` and `exclude_airlines` defaults. -- Using stale dates copied from documentation. +- Travel dates must be in the future. ## Official SerpApi documentation diff --git a/docs/user_guide/18-travel_explore_search.qmd b/docs/user_guide/18-travel_explore_search.qmd index 4caf461..e05d1ae 100644 --- a/docs/user_guide/18-travel_explore_search.qmd +++ b/docs/user_guide/18-travel_explore_search.qmd @@ -39,7 +39,7 @@ Example broad invocation: | Field | Type | Required | Default and constraints | | --- | --- | --- | --- | -| `departure_id` | string | yes | Airport IATA code such as `JFK`, or a city Google Knowledge Graph location ID (KGMID) beginning with `/m/` or `/g/`, such as `/m/04jpl` for London; do not use metropolitan codes such as `LON`; comma-separated values are supported | +| `departure_id` | string | yes | Airport IATA code such as `JFK`, or an exact city Google Knowledge Graph location ID (KGMID) beginning with `/m/` or `/g/`, such as `/m/04jpl` for London; metropolitan codes such as `LON` are unsupported; see the [Google Travel Explore API](https://serpapi.com/google-travel-explore-api); comma-separated values are supported | | `arrival_id` | string | no | Specific arrival airport IATA code or city KGMID beginning with `/m/` or `/g/`, such as `/m/05qtj` for Paris; use `arrival_area_id` for a region or country; mutually exclusive with it | | `arrival_area_id` | string | no | Region or country KGMID beginning with `/m/` or `/g/`, such as `/m/02j9z` for Europe; use `arrival_id` for an airport or city; mutually exclusive with it | | `outbound_date` | date string | no | Future date in `YYYY-MM-DD` format; omit for flexible-date exploration | @@ -56,7 +56,8 @@ Example broad invocation: | --- | --- | --- | | `provider` | `"auto"` | Detect one installed SDK or select one explicitly | | `default_params` | `None` | Fix currency, locale, price, stops, duration, or supported interest filters | -| `mode` | `compact` | Return up to five destinations; use `SearchResultMode.FULL` for the untouched response | +| `mode` | `compact` | Return the main destination result section; use `SearchResultMode.FULL` to keep supporting sections and all fields on retained results | +| `result_limit` | `50` | Maximum `destinations` retained in either mode | | `api_key` | `None` | Override environment key lookup | | `timeout` | `None` | Set the built-in client timeout | | `client` | `None` | Add caching, logging, testing, or response reduction | @@ -83,18 +84,16 @@ those modes are incompatible. ## What comes back -Compact mode returns up to five `destinations`. Destination entries may include -city or place names, country, airport information, flight price, duration, -stops, dates, image, and location data. Use full mode for supporting sections. +Both modes return up to `result_limit` `destinations` (50 by default). Destination entries may include place names, countries, airports, flight prices, durations, stops, dates, images, and location data. Use full mode when application code needs supporting response sections or additional result fields. Add `result_limit=None` to keep every returned destination. ## Common mistakes -- Sending a text `query` or `q`; this tool starts from `departure_id`. +- Travel Explore starts from `departure_id` and has no text-query input. - Using a metropolitan code such as `LON` instead of an airport IATA code or city KGMID. - Supplying `return_date` without `outbound_date`. -- Treating `arrival_area_id` as ordinary text; it is a Google Knowledge Graph - identifier beginning with `/m/` or `/g/` for a region or country. +- `arrival_area_id` accepts a region or country Google Knowledge Graph location + ID beginning with `/m/` or `/g/`. - Combining `travel_mode` and `interest` in `default_params`. - Using Explore for a route that is already known; use `flights_search` for a tighter schema. diff --git a/docs/user_guide/19-managing_llm_context.qmd b/docs/user_guide/19-managing_llm_context.qmd new file mode 100644 index 0000000..e746203 --- /dev/null +++ b/docs/user_guide/19-managing_llm_context.qmd @@ -0,0 +1,113 @@ +--- +title: Manage LLM context +guide-section: Advanced Usage +--- + +# Manage search response size + +Search responses can contain many results and supporting sections. Use `mode` and `result_limit` to control what the tool returns to the model. + +Two constructor options work together: + +| Option | What it controls | Default | +| --- | --- | --- | +| `mode` | Which response sections and fields are kept | `SearchResultMode.COMPACT` | +| `result_limit` | Maximum items kept in each result list | Tool-specific | + +## Start with compact mode + +Compact mode is the default: + +```python +from serpapi_search_tools import web_search + +search = web_search() +``` + +It keeps the main result and answer sections while omitting metadata, request parameters, pagination, filters, and other supporting sections. It does not summarize, rewrite, or rank results. + +## Set the number of results separately + +`result_limit` applies in both compact and full mode: + +```python +from serpapi_search_tools import SearchResultMode, web_search + +compact_search = web_search(result_limit=5) +full_search = web_search( + mode=SearchResultMode.FULL, + result_limit=5, +) +``` + +Both tools keep at most five items in each supported result list. The full tool also keeps the response sections and fields omitted by compact mode. + +The option is application configuration. It is not visible to the model and does not change the number of results requested from SerpApi. + +## Understand per-list limits + +The limit applies independently when a response contains more than one result list: + +- Google Flights keeps up to the limit from both `best_flights` and + `other_flights`. +- YouTube keeps up to the limit from videos, shorts, channels, playlists, + movies, and categories. +- Google Light applies it to organic results, related questions, related + searches, and top stories. + +Nested data such as flight legs, layovers, hotel amenities, and product offers is not shortened. + +## Default result limits + +Each tool has a default suited to its result type: + +| Tool | Default `result_limit` | +| --- | ---: | +| `web_search` | `10` | +| `news_search` | `20` | +| `maps_search` | `10` | +| `images_search` | `50` | +| `shopping_search` | `60` | +| `videos_search` | `10` | +| `hotels_search` | `20` | +| `flights_search` | `10` per flight list | +| `travel_explore_search` | `50` | + +These are maximums. If SerpApi returns fewer results, the tool returns the available results. + +## Keep every returned result + +Set `result_limit=None` to keep all returned results: + +```python +from serpapi_search_tools import SearchResultMode, images_search + +all_compact_images = images_search(result_limit=None) + +full_images_response = images_search( + mode=SearchResultMode.FULL, + result_limit=None, +) +``` + +The first tool keeps every image result while still omitting the supporting sections removed by compact mode. The second keeps all response sections, fields, and returned results. + +## Choose a practical budget + +Start with the tool default. Lower it when results contain long snippets or when several search tools can run in one turn: + +```python +from serpapi_search_tools import images_search, news_search, web_search + +tools = [ + web_search(result_limit=5), + news_search(result_limit=5), + images_search(result_limit=20), +] +``` + +Raise a limit when the task benefits from more choices, such as image search, product comparison, or destination discovery. Create separately named tool instances when agent tasks need different limits. + +Continue with [Common configuration](configuration.html) for constructor +options or [Debug search responses](debugging.html) when application code needs +to inspect full provider data. diff --git a/docs/user_guide/20-debugging.qmd b/docs/user_guide/20-debugging.qmd index cb5206d..5ef5939 100644 --- a/docs/user_guide/20-debugging.qmd +++ b/docs/user_guide/20-debugging.qmd @@ -19,7 +19,8 @@ from serpapi_search_tools import SearchResultMode, web_search search = web_search( provider="function", allowed_engines=["google_light"], - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=None, mode=SearchResultMode.FULL, ) response = json.loads(search(query="Python packaging tutorials")) @@ -43,7 +44,7 @@ Compact mode returns these common result sections: | `maps_search` | `local_results` | | `images_search` | `images_results` | | `shopping_search` | `shopping_results` or engine-specific product results | -| `videos_search` | `video_results` | +| `videos_search` | `video_results`, `shorts_results`, `channel_results`, `playlist_results`, `movie_results`, or `category_results` | | `hotels_search` | `properties` | | `flights_search` | `best_flights`, `other_flights` | | `travel_explore_search` | `destinations` | diff --git a/examples/README.md b/examples/README.md index 0cb5123..f95c525 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,6 +61,4 @@ The examples default to `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-flash-lite-latest`, and `grok-4.5`. Override the corresponding model environment variable when needed. -The two multi-tool OpenAI Agents scenarios use `_logging_client.py` to log safe -request metadata and response shape. The package's default compact mode keeps -at most five primary results. +The two multi-tool OpenAI Agents scenarios use `_logging_client.py` to log safe request metadata and response structure. `result_limit` controls how many items are kept in each result list. Compact mode omits supporting response sections. diff --git a/examples/agno_grok.py b/examples/agno_grok.py index 90b2e4c..8fecb0a 100644 --- a/examples/agno_grok.py +++ b/examples/agno_grok.py @@ -46,12 +46,13 @@ def main() -> None: web_search( provider="agno", allowed_engines=["google_light", "google"], - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ), shopping_search( provider="agno", - default_params={"num": "3", "hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ), ], diff --git a/examples/autogen_openai.py b/examples/autogen_openai.py index e384a07..542be50 100644 --- a/examples/autogen_openai.py +++ b/examples/autogen_openai.py @@ -51,7 +51,8 @@ async def main() -> None: tools=[ news_search( provider="autogen", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ), web_search(provider="autogen", allowed_engines=["google_light"]), ], diff --git a/examples/claude_agent_sdk_sonnet.py b/examples/claude_agent_sdk_sonnet.py index a095628..4687b79 100644 --- a/examples/claude_agent_sdk_sonnet.py +++ b/examples/claude_agent_sdk_sonnet.py @@ -40,7 +40,8 @@ async def main() -> None: tools=[ news_search( provider="claude-agent-sdk", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, client=client, ), web_search( diff --git a/examples/crewai_grok.py b/examples/crewai_grok.py index a04b81c..520875f 100644 --- a/examples/crewai_grok.py +++ b/examples/crewai_grok.py @@ -46,7 +46,7 @@ def main() -> None: tools=[ shopping_search( provider="crewai", - default_params={"num": "3", "hl": "en", "gl": "us"}, + result_limit=3, ) ], max_iter=3, diff --git a/examples/direct_cached_search.py b/examples/direct_cached_search.py index f82a947..69feafc 100644 --- a/examples/direct_cached_search.py +++ b/examples/direct_cached_search.py @@ -48,7 +48,8 @@ def main() -> None: provider="function", client=client, allowed_engines=["google_light"], - default_params={"num": 3, "hl": "en"}, + default_params={"hl": "en"}, + result_limit=3, ) first = json.loads(search(query="Python packaging")) diff --git a/examples/direct_marketplace_comparison.py b/examples/direct_marketplace_comparison.py index d60c3b1..e8badf5 100644 --- a/examples/direct_marketplace_comparison.py +++ b/examples/direct_marketplace_comparison.py @@ -43,7 +43,7 @@ def main() -> None: search = shopping_search( provider="function", allowed_engines=[engine], - default_params={"num": 3}, + result_limit=3, ) result = json.loads(search(query="noise cancelling headphones")) products.append(_first_product(engine, result)) diff --git a/examples/direct_multi_search.py b/examples/direct_multi_search.py index a214bd5..98bebd2 100644 --- a/examples/direct_multi_search.py +++ b/examples/direct_multi_search.py @@ -22,7 +22,8 @@ def main() -> None: web = web_search( provider="function", allowed_engines=["google_light"], - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ) news = news_search( provider="function", @@ -35,7 +36,8 @@ def main() -> None: shopping = shopping_search( provider="function", allowed_engines=["google_shopping"], - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ) results = { diff --git a/examples/direct_regioned_search.py b/examples/direct_regioned_search.py index a3b0176..62ed0c2 100644 --- a/examples/direct_regioned_search.py +++ b/examples/direct_regioned_search.py @@ -21,14 +21,16 @@ def main() -> None: search_us = web_search( provider="function", allowed_engines=["google_light"], - default_params={"gl": "us", "hl": "en", "num": 3}, + default_params={"gl": "us", "hl": "en"}, + result_limit=3, include_examples=False, name="web_search_us", ) search_de = web_search( provider="function", allowed_engines=["google_light"], - default_params={"gl": "de", "hl": "de", "num": 3}, + default_params={"gl": "de", "hl": "de"}, + result_limit=3, include_examples=False, name="web_search_de", ) diff --git a/examples/direct_search.py b/examples/direct_search.py index cab2a71..57c567f 100644 --- a/examples/direct_search.py +++ b/examples/direct_search.py @@ -16,7 +16,7 @@ def main() -> None: web = web_search( provider="function", allowed_engines=["google_light", "bing"], - default_params={"num": 3, "hl": "en", "gl": "us"}, + result_limit=3, ) news = news_search( provider="function", diff --git a/examples/google_adk_gemini.py b/examples/google_adk_gemini.py index c5b6d57..667d38b 100644 --- a/examples/google_adk_gemini.py +++ b/examples/google_adk_gemini.py @@ -54,7 +54,7 @@ async def main() -> None: tools=[ shopping_search( provider="google-adk", - default_params={"num": "3", "hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ), web_search( diff --git a/examples/haystack_openai.py b/examples/haystack_openai.py index 32c09d3..e7687bf 100644 --- a/examples/haystack_openai.py +++ b/examples/haystack_openai.py @@ -43,7 +43,8 @@ def main() -> None: tools=[ maps_search( provider="haystack", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ), web_search(provider="haystack", allowed_engines=["google_light"]), ], diff --git a/examples/langchain_grok.py b/examples/langchain_grok.py index 2a48083..5181b83 100644 --- a/examples/langchain_grok.py +++ b/examples/langchain_grok.py @@ -44,7 +44,8 @@ def main() -> None: tools=[ maps_search( provider="langchain", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ), web_search( diff --git a/examples/langgraph_openai.py b/examples/langgraph_openai.py index 36cd19e..c164338 100644 --- a/examples/langgraph_openai.py +++ b/examples/langgraph_openai.py @@ -38,11 +38,13 @@ def main() -> None: web_search( provider="langgraph", allowed_engines=["google_light", "google"], - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ), news_search( provider="langgraph", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ), ] model = ChatOpenAI( diff --git a/examples/llamaindex_openai.py b/examples/llamaindex_openai.py index e3bec65..516575c 100644 --- a/examples/llamaindex_openai.py +++ b/examples/llamaindex_openai.py @@ -64,7 +64,8 @@ async def main() -> None: web_search( provider="llamaindex", allowed_engines=["google_light"], - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, api_key=_serpapi_api_key(), ), ], diff --git a/examples/microsoft_agent_framework_openai.py b/examples/microsoft_agent_framework_openai.py index 70afddd..d2e20ac 100644 --- a/examples/microsoft_agent_framework_openai.py +++ b/examples/microsoft_agent_framework_openai.py @@ -44,12 +44,13 @@ async def main() -> None: tools=[ news_search( provider="microsoft-agent-framework", - default_params={"num": "5", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=5, ), web_search( provider="microsoft-agent-framework", allowed_engines=["google_light", "bing"], - default_params={"num": "5", "hl": "en", "gl": "us"}, + result_limit=5, ), ], ) diff --git a/examples/openai_agents_openai.py b/examples/openai_agents_openai.py index 74878b0..38887d0 100644 --- a/examples/openai_agents_openai.py +++ b/examples/openai_agents_openai.py @@ -43,16 +43,17 @@ async def main() -> None: web_search( provider="openai-agents", allowed_engines=["google_light", "google"], - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, client=client, ), - news_search(provider="openai-agents", default_params={"num": "3"}, client=client), + news_search(provider="openai-agents", result_limit=3, client=client), maps_search( provider="openai-agents", default_params={"hl": "en", "gl": "us"}, client=client, ), - shopping_search(provider="openai-agents", default_params={"num": "3"}, client=client), + shopping_search(provider="openai-agents", result_limit=3, client=client), ], ) result = await Runner.run(agent, PROMPT) diff --git a/examples/pydantic_ai_openai.py b/examples/pydantic_ai_openai.py index b355862..b39c606 100644 --- a/examples/pydantic_ai_openai.py +++ b/examples/pydantic_ai_openai.py @@ -41,7 +41,8 @@ def main() -> None: tools=[ images_search( provider="pydantic-ai", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ), web_search(provider="pydantic-ai", allowed_engines=["google_light"]), ], diff --git a/examples/smolagents_openai.py b/examples/smolagents_openai.py index abbaf9a..eb45156 100644 --- a/examples/smolagents_openai.py +++ b/examples/smolagents_openai.py @@ -40,7 +40,8 @@ def main() -> None: tools=[ videos_search( provider="smolagents", - default_params={"num": "3", "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, include_examples=False, ) ], diff --git a/src/serpapi_search_tools/_query_tools.py b/src/serpapi_search_tools/_query_tools.py index a483566..503e3ef 100644 --- a/src/serpapi_search_tools/_query_tools.py +++ b/src/serpapi_search_tools/_query_tools.py @@ -77,6 +77,7 @@ def _runtime( default_params: Mapping[str, Any] | None, timeout: float | None, mode: SearchResultMode | str, + result_limit: int | None, ) -> SearchRuntime: return SearchRuntime( api_key=api_key, @@ -84,6 +85,7 @@ def _runtime( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) @@ -271,6 +273,7 @@ def web_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 10, name: str = "web_search", ) -> Any: """Create a general-web search tool. @@ -289,6 +292,9 @@ def web_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -319,6 +325,7 @@ def web_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ), include_examples=include_examples, ) @@ -339,6 +346,7 @@ def shopping_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 60, name: str = "shopping_search", ) -> Any: """Create a multi-marketplace product search tool. @@ -356,6 +364,9 @@ def shopping_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -387,6 +398,7 @@ def shopping_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ), include_examples=include_examples, ) @@ -405,6 +417,7 @@ def _fixed_query_factory( default_params: Mapping[str, Any] | None, timeout: float | None, mode: SearchResultMode | str, + result_limit: int | None, name: str, validator: Any = None, ) -> Any: @@ -419,6 +432,7 @@ def _fixed_query_factory( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ), include_examples=include_examples, validator=validator, @@ -484,6 +498,7 @@ def news_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 20, name: str = "news_search", ) -> Any: """Create a Google News query-mode tool for current articles and stories. @@ -497,6 +512,9 @@ def news_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -517,6 +535,7 @@ def news_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, name=name, validator=_validate_news_query_mode, ) @@ -531,6 +550,7 @@ def images_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 50, name: str = "images_search", ) -> Any: """Create a Google Images tool for image URLs and metadata. @@ -544,6 +564,9 @@ def images_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -564,6 +587,7 @@ def images_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, name=name, validator=_validate_images_query_mode, ) @@ -578,6 +602,7 @@ def videos_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 10, name: str = "videos_search", ) -> Any: """Create a YouTube tool for videos, channels, and playlists. @@ -591,6 +616,9 @@ def videos_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -611,6 +639,7 @@ def videos_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, name=name, ) @@ -624,6 +653,7 @@ def maps_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 10, name: str = "maps_search", ) -> Any: """Create a Google Maps search-mode tool for places and businesses. @@ -641,6 +671,9 @@ def maps_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -656,6 +689,7 @@ def maps_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) def maps_tool( diff --git a/src/serpapi_search_tools/_shared.py b/src/serpapi_search_tools/_shared.py index 735daef..d0a1752 100644 --- a/src/serpapi_search_tools/_shared.py +++ b/src/serpapi_search_tools/_shared.py @@ -27,8 +27,50 @@ ToolFunction: TypeAlias = Callable[..., str] _RESERVED_DEFAULT_PARAMS = frozenset({"api_key", "async", "engine", "output"}) -_COMPACT_RESULT_LIMIT = 5 -_WEB_RESULT_KEYS = ("answer_box", "knowledge_graph", "ai_overview", "organic_results") +_DEFAULT_RESULT_LIMIT = 10 +_COMPACT_DROPPED_RESULT_KEYS_BY_ENGINE: Mapping[str, frozenset[str]] = { + "google_images": frozenset( + { + "related_content_id", + "serpapi_related_content_link", + "source_logo", + } + ), + "google_shopping": frozenset( + { + "immersive_product_page_token", + "serpapi_immersive_product_api", + "serpapi_thumbnail", + "source_icon", + } + ), + "amazon": frozenset( + { + "link_clean", + "purchase_options", + "serpapi_link", + } + ), + "walmart": frozenset( + { + "muliple_options_available", + "seller_id", + "serpapi_product_page_url", + "variant_swatches", + } + ), + "ebay": frozenset({"buying_format_text", "serpapi_link", "watchers"}), + "google_hotels": frozenset( + { + "nearby_places", + "reviews_breakdown", + "serpapi_google_hotels_photos_link", + "serpapi_google_hotels_reviews_link", + "serpapi_property_details_link", + } + ), + "google_travel_explore": frozenset({"serpapi_link"}), +} _GOOGLE_LIGHT_RESULT_KEYS = ( "answer_box", "knowledge_graph", @@ -44,11 +86,11 @@ "organic_results_state", ) _COMPACT_RESULT_KEYS_BY_ENGINE: Mapping[str, tuple[str, ...]] = { - "google": _WEB_RESULT_KEYS, + "google": ("answer_box", "knowledge_graph", "ai_overview", "organic_results"), "google_light": _GOOGLE_LIGHT_RESULT_KEYS, - "bing": _WEB_RESULT_KEYS, - "yahoo": _WEB_RESULT_KEYS, - "duckduckgo": _WEB_RESULT_KEYS, + "bing": ("answer_box", "knowledge_graph", "copilot_answer", "organic_results"), + "yahoo": ("answer_box", "knowledge_graph", "organic_results"), + "duckduckgo": ("knowledge_graph", "organic_results"), "google_news": ("news_results",), "google_maps": ("local_results",), "google_images": ("images_results",), @@ -56,7 +98,14 @@ "amazon": ("organic_results",), "walmart": ("organic_results",), "ebay": ("organic_results",), - "youtube": ("video_results",), + "youtube": ( + "video_results", + "shorts_results", + "channel_results", + "playlist_results", + "movie_results", + "category_results", + ), "google_hotels": ("properties",), "google_flights": ("best_flights", "other_flights"), "google_travel_explore": ("destinations",), @@ -99,6 +148,7 @@ class SearchRuntime: default_params: Mapping[str, Any] | None = None timeout: float | None = None mode: SearchResultMode | str = SearchResultMode.COMPACT + result_limit: int | None = _DEFAULT_RESULT_LIMIT _builtin_clients: Any = field(init=False, default_factory=local, repr=False) _builtin_client_lock: Any = field(init=False, default_factory=RLock, repr=False) @@ -114,6 +164,11 @@ def __post_init__(self) -> None: except ValueError as exc: choices = ", ".join(member.value for member in SearchResultMode) raise ValueError(f"mode must be one of: {choices}.") from exc + if self.result_limit is not None: + if isinstance(self.result_limit, bool) or not isinstance(self.result_limit, int): + raise ValueError("result_limit must be a positive integer or None.") + if self.result_limit < 1: + raise ValueError("result_limit must be a positive integer or None.") def execute( self, @@ -142,6 +197,11 @@ def execute( raise SerpApiSearchError(f"SerpApi request failed: {message}") from None raise SerpApiSearchError("Custom search client request failed.") from None plain_result = dict(result) + plain_result = _limit_result_lists( + plain_result, + engine=engine, + result_limit=self.result_limit, + ) if self.mode is SearchResultMode.COMPACT: plain_result = _compact_result(plain_result, engine=engine) return json.dumps(plain_result, default=str, separators=(",", ":")) @@ -204,14 +264,21 @@ def _sanitized_provider_error(exc: Exception, *, api_key: str | None) -> str: return message -def _compact_result(result: Mapping[str, Any], *, engine: str) -> dict[str, Any]: +def _compact_result( + result: Mapping[str, Any], + *, + engine: str, +) -> dict[str, Any]: compact = {"error": result["error"]} if "error" in result else {} included_result = False for key in _COMPACT_RESULT_KEYS_BY_ENGINE.get(engine, ()): if key not in result: continue value = result[key] - compact[key] = value[:_COMPACT_RESULT_LIMIT] if isinstance(value, list) else value + if isinstance(value, list): + compact[key] = [_compact_result_item(item, engine=engine) for item in value] + else: + compact[key] = value included_result = True if "error" in compact or included_result: return compact @@ -232,6 +299,41 @@ def _compact_result(result: Mapping[str, Any], *, engine: str) -> dict[str, Any] return compact +def _compact_result_item(item: Any, *, engine: str) -> Any: + if not isinstance(item, Mapping): + return item + + dropped_keys = _COMPACT_DROPPED_RESULT_KEYS_BY_ENGINE.get(engine, frozenset()) + compact_item = {key: value for key, value in item.items() if key not in dropped_keys} + + if engine == "amazon": + clean_link = item.get("link_clean") + if isinstance(clean_link, str) and clean_link: + compact_item["link"] = clean_link + elif engine == "google_hotels": + images = compact_item.get("images") + if isinstance(images, list): + compact_item["images"] = images[:1] + + return compact_item + + +def _limit_result_lists( + result: Mapping[str, Any], + *, + engine: str, + result_limit: int | None, +) -> dict[str, Any]: + limited = dict(result) + if result_limit is None: + return limited + for key in _COMPACT_RESULT_KEYS_BY_ENGINE.get(engine, ()): + value = limited.get(key) + if isinstance(value, list): + limited[key] = value[:result_limit] + return limited + + def object_schema( properties: Mapping[str, Mapping[str, Any]], *, diff --git a/src/serpapi_search_tools/_travel_tools.py b/src/serpapi_search_tools/_travel_tools.py index 534d6fc..5b16851 100644 --- a/src/serpapi_search_tools/_travel_tools.py +++ b/src/serpapi_search_tools/_travel_tools.py @@ -43,6 +43,7 @@ def _runtime( default_params: Mapping[str, Any] | None, timeout: float | None, mode: SearchResultMode | str, + result_limit: int | None, ) -> SearchRuntime: return SearchRuntime( api_key=api_key, @@ -50,6 +51,7 @@ def _runtime( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) @@ -162,6 +164,7 @@ def hotels_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 20, name: str = "hotels_search", ) -> Any: """Create a Google Hotels tool with explicit stay dates. @@ -178,6 +181,9 @@ def hotels_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -193,6 +199,7 @@ def hotels_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) def hotels_tool( @@ -296,6 +303,7 @@ def flights_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 10, name: str = "flights_search", ) -> Any: """Create a Google Flights tool for one-way and round-trip routes. @@ -313,6 +321,9 @@ def flights_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -328,6 +339,7 @@ def flights_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) def flights_tool( @@ -450,6 +462,7 @@ def travel_explore_search( default_params: Mapping[str, Any] | None = None, timeout: float | None = None, mode: SearchResultMode | str = SearchResultMode.COMPACT, + result_limit: int | None = 50, name: str = "travel_explore_search", ) -> Any: """Create a Google Travel Explore destination-discovery tool. @@ -467,6 +480,9 @@ def travel_explore_search( api_key, client, default_params, timeout, mode Runtime authentication, custom client, application filters, timeout, and compact or full response selection. + result_limit + Maximum items kept in each result list in both response modes; use + ``None`` to keep all returned results. name Tool name presented to the model. @@ -482,6 +498,7 @@ def travel_explore_search( default_params=default_params, timeout=timeout, mode=mode, + result_limit=result_limit, ) def travel_explore_tool( diff --git a/tests/live_contracts.py b/tests/live_contracts.py index 9048462..197ee53 100644 --- a/tests/live_contracts.py +++ b/tests/live_contracts.py @@ -11,13 +11,15 @@ class LiveParameterCase: default_params: dict[str, Any] arguments: dict[str, Any] result_key: str + result_limit: int = 10 LIVE_PARAMETER_CASES = ( LiveParameterCase( id="readme-web-localized", factory="web_search", - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, arguments={"query": "Python packaging"}, result_key="organic_results", ), @@ -25,6 +27,7 @@ class LiveParameterCase: id="readme-news-localized", factory="news_search", default_params={"hl": "en", "gl": "us"}, + result_limit=20, arguments={"query": "Python releases"}, result_key="news_results", ), @@ -45,7 +48,8 @@ class LiveParameterCase: LiveParameterCase( id="examples-shopping-bounded", factory="shopping_search", - default_params={"num": 3}, + default_params={}, + result_limit=3, arguments={"query": "coffee grinder"}, result_key="shopping_results", ), diff --git a/tests/test_live.py b/tests/test_live.py index 1bd801c..9e977b1 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -230,7 +230,11 @@ def test_each_public_tool_returns_nonempty_compact_results_live( "flights_search": flights_search, "travel_explore_search": travel_explore_search, } - tool = factories[case.factory](provider="function", default_params=case.default_params) + tool = factories[case.factory]( + provider="function", + default_params=case.default_params, + result_limit=case.result_limit, + ) result = _decode_success(tool(**arguments), f"docs-{case.id}") if case.factory == "flights_search": @@ -238,7 +242,7 @@ def test_each_public_tool_returns_nonempty_compact_results_live( else: primary_results = _primary_results(result, case.result_key) assert primary_results, f"{case.id} returned no {case.result_key}" - assert len(primary_results) <= 5 + assert len(primary_results) <= case.result_limit def test_invalid_api_key_returns_an_actionable_error_contract() -> None: diff --git a/tests/test_live_adapters.py b/tests/test_live_adapters.py index cd4df4e..ad860f5 100644 --- a/tests/test_live_adapters.py +++ b/tests/test_live_adapters.py @@ -29,7 +29,8 @@ def test_native_sdk_adapter_executes_a_real_google_light_search() -> None: tool = web_search( provider=PROVIDER, allowed_engines=["google_light"], - default_params={"num": 3, "hl": "en", "gl": "us"}, + default_params={"hl": "en", "gl": "us"}, + result_limit=3, ) encoded = _invoke_native(PROVIDER, tool, {"query": "SerpApi Python"}) @@ -41,6 +42,7 @@ def test_native_sdk_adapter_executes_a_real_google_light_search() -> None: assert "search_metadata" not in result assert "search_parameters" not in result assert result["organic_results"] + assert len(result["organic_results"]) <= 3 first = result["organic_results"][0] assert first["title"] assert first["link"] diff --git a/tests/test_result_limit.py b/tests/test_result_limit.py new file mode 100644 index 0000000..d9b9f0a --- /dev/null +++ b/tests/test_result_limit.py @@ -0,0 +1,289 @@ +import inspect +import json + +import pytest + +from serpapi_search_tools import ( + SearchResultMode, + flights_search, + hotels_search, + images_search, + maps_search, + news_search, + shopping_search, + travel_explore_search, + videos_search, + web_search, +) + + +class ResultClient: + def search(self, params: dict[str, object]) -> dict[str, object]: + entries = [{"position": position} for position in range(70)] + return { + "answer_box": {"answer": "answer"}, + "knowledge_graph": {"title": "title"}, + "organic_results": entries, + "related_questions": entries, + "related_searches": entries, + "top_stories": entries, + "news_results": entries, + "local_results": entries, + "images_results": entries, + "shopping_results": entries, + "video_results": entries, + "shorts_results": entries, + "channel_results": entries, + "playlist_results": entries, + "movie_results": entries, + "category_results": entries, + "properties": entries, + "best_flights": entries, + "other_flights": entries, + "destinations": entries, + } + + +USE_FACTORY_DEFAULT = object() + + +@pytest.mark.parametrize( + ("factory", "call_kwargs", "default_limit"), + [ + (web_search, {"query": "coffee"}, 10), + (news_search, {"query": "coffee"}, 20), + (maps_search, {"query": "coffee"}, 10), + (images_search, {"query": "coffee"}, 50), + (shopping_search, {"query": "coffee"}, 60), + (videos_search, {"query": "coffee"}, 10), + ( + hotels_search, + { + "query": "hotels in Kyoto", + "check_in_date": "2030-08-01", + "check_out_date": "2030-08-04", + }, + 20, + ), + ( + flights_search, + { + "departure_id": "LAX", + "arrival_id": "AUS", + "outbound_date": "2030-08-01", + }, + 10, + ), + (travel_explore_search, {"departure_id": "JFK"}, 50), + ], +) +@pytest.mark.parametrize( + "result_limit_override", + [USE_FACTORY_DEFAULT, 3], + ids=["factory-default", "override"], +) +def test_every_factory_applies_the_result_limit_to_each_retained_list( + factory, + call_kwargs: dict[str, object], + default_limit: int, + result_limit_override: object, +) -> None: + factory_kwargs: dict[str, object] = { + "provider": "function", + "client": ResultClient(), + } + if result_limit_override is not USE_FACTORY_DEFAULT: + factory_kwargs["result_limit"] = result_limit_override + tool = factory(**factory_kwargs) + + result = json.loads(tool(**call_kwargs)) + retained_lists = [value for value in result.values() if isinstance(value, list)] + expected_length = ( + default_limit if result_limit_override is USE_FACTORY_DEFAULT else result_limit_override + ) + + assert retained_lists + assert all(len(value) == expected_length for value in retained_lists) + assert "result_limit" not in inspect.signature(tool).parameters + + +@pytest.mark.parametrize("mode", [SearchResultMode.COMPACT, SearchResultMode.FULL]) +def test_result_limit_does_not_truncate_nested_hotel_amenities( + mode: SearchResultMode, +) -> None: + result_item = { + "position": 1, + "title": "Hotel", + "amenities": ["Pool", "Parking", "Breakfast"], + "images": [{"thumbnail": "one"}, {"thumbnail": "two"}], + } + + class HotelsClient: + def search(self, params: dict[str, object]) -> dict[str, object]: + return { + "search_metadata": {"status": "Success"}, + "properties": [{**result_item, "position": position} for position in range(5)], + } + + tool = hotels_search( + provider="function", + client=HotelsClient(), + mode=mode, + result_limit=2, + ) + result = json.loads( + tool( + query="hotels in Kyoto", + check_in_date="2030-08-01", + check_out_date="2030-08-04", + ) + ) + + assert len(result["properties"]) == 2 + assert result["properties"][0]["amenities"] == result_item["amenities"] + if mode is SearchResultMode.FULL: + assert result["search_metadata"] == {"status": "Success"} + + +@pytest.mark.parametrize("mode", [SearchResultMode.COMPACT, SearchResultMode.FULL]) +def test_result_limit_none_keeps_every_returned_result(mode: SearchResultMode) -> None: + tool = images_search( + provider="function", + client=ResultClient(), + mode=mode, + result_limit=None, + ) + + result = json.loads(tool(query="coffee")) + + assert len(result["images_results"]) == 70 + + +@pytest.mark.parametrize( + ("engine", "result_key", "link_fields", "expected_link_field", "expected_link"), + [ + ( + "google_shopping", + "shopping_results", + {"product_link": "https://shopping.test/product"}, + "product_link", + "https://shopping.test/product", + ), + ( + "amazon", + "organic_results", + { + "link": "https://amazon.test/tracked", + "link_clean": "https://amazon.test/product", + "more_buying_choices_link": "https://amazon.test/offers", + }, + "link", + "https://amazon.test/product", + ), + ( + "walmart", + "organic_results", + {"product_page_url": "https://walmart.test/product"}, + "product_page_url", + "https://walmart.test/product", + ), + ( + "ebay", + "organic_results", + {"link": "https://ebay.test/product"}, + "link", + "https://ebay.test/product", + ), + ], +) +def test_compact_shopping_limit_preserves_engine_purchase_links( + engine: str, + result_key: str, + link_fields: dict[str, str], + expected_link_field: str, + expected_link: str, +) -> None: + entries = [ + { + "position": position, + "title": f"Result {position}", + **link_fields, + } + for position in range(7) + ] + + class MultiEngineClient: + def search(self, params: dict[str, object]) -> dict[str, object]: + return { + "search_metadata": {"status": "Success"}, + result_key: entries, + } + + tool = shopping_search( + provider="function", + client=MultiEngineClient(), + allowed_engines=[engine], + default_engine=engine, + result_limit=3, + ) + + result = json.loads(tool(query="coffee")) + + assert len(result[result_key]) == 3 + assert all(item[expected_link_field] == expected_link for item in result[result_key]) + if engine == "amazon": + assert all( + item["more_buying_choices_link"] == "https://amazon.test/offers" + for item in result[result_key] + ) + + +@pytest.mark.parametrize( + ("factory", "factory_kwargs", "expected_params"), + [ + ( + web_search, + { + "allowed_engines": ["duckduckgo"], + "default_engine": "duckduckgo", + "default_params": {"m": 20}, + }, + {"m": 20, "q": "coffee", "engine": "duckduckgo"}, + ), + ( + shopping_search, + { + "allowed_engines": ["ebay"], + "default_engine": "ebay", + "default_params": {"_ipg": 25}, + }, + {"_ipg": 25, "_nkw": "coffee", "engine": "ebay"}, + ), + ], +) +def test_result_limit_is_independent_from_valid_upstream_count_parameters( + factory, + factory_kwargs: dict[str, object], + expected_params: dict[str, object], +) -> None: + class RecordingClient: + def __init__(self) -> None: + self.calls: list[dict[str, object]] = [] + + def search(self, params: dict[str, object]) -> dict[str, object]: + self.calls.append(params) + return {"organic_results": [{"position": position} for position in range(10)]} + + client = RecordingClient() + tool = factory( + provider="function", + client=client, + result_limit=3, + **factory_kwargs, + ) + + result = json.loads(tool(query="coffee")) + + assert client.calls == [expected_params] + assert len(result["organic_results"]) == 3 + assert "result_limit" not in client.calls[0] diff --git a/tests/test_shared.py b/tests/test_shared.py index 5ddb99c..3852436 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -45,10 +45,12 @@ def test_runtime_merges_parameters_in_documented_precedence_order() -> None: encoded = runtime.execute( engine="google", - typed_params={"q": "coffee", "num": 3, "location": None}, + typed_params={"q": "coffee", "start": 10, "location": None}, ) - assert client.calls == [{"hl": "en", "gl": "gb", "q": "coffee", "num": 3, "engine": "google"}] + assert client.calls == [ + {"hl": "en", "gl": "gb", "q": "coffee", "start": 10, "engine": "google"} + ] assert json.loads(encoded)["params"] == client.calls[0] @@ -478,7 +480,7 @@ def search(self, params: dict[str, object]) -> dict[str, object]: "answer_box": {"answer": "Coffee is a brewed drink."}, "knowledge_graph": {"title": "Coffee"}, "ai_overview": {"text": "A compact overview."}, - "organic_results": [{"position": position} for position in range(1, 9)], + "organic_results": [{"position": position} for position in range(1, 13)], "related_searches": [{"query": "tea"}], "serpapi_pagination": {"next": "https://example.test/next"}, } @@ -493,7 +495,7 @@ def search(self, params: dict[str, object]) -> dict[str, object]: "answer_box": {"answer": "Coffee is a brewed drink."}, "knowledge_graph": {"title": "Coffee"}, "ai_overview": {"text": "A compact overview."}, - "organic_results": [{"position": position} for position in range(1, 6)], + "organic_results": [{"position": position} for position in range(1, 11)], } @@ -511,9 +513,9 @@ def search(self, params: dict[str, object]) -> dict[str, object]: "top_stories", }, ), - ("bing", {"answer_box", "knowledge_graph", "ai_overview", "organic_results"}), - ("yahoo", {"answer_box", "knowledge_graph", "ai_overview", "organic_results"}), - ("duckduckgo", {"answer_box", "knowledge_graph", "ai_overview", "organic_results"}), + ("bing", {"answer_box", "knowledge_graph", "copilot_answer", "organic_results"}), + ("yahoo", {"answer_box", "knowledge_graph", "organic_results"}), + ("duckduckgo", {"knowledge_graph", "organic_results"}), ("google_news", {"news_results"}), ("google_maps", {"local_results"}), ("google_images", {"images_results"}), @@ -521,7 +523,17 @@ def search(self, params: dict[str, object]) -> dict[str, object]: ("amazon", {"organic_results"}), ("walmart", {"organic_results"}), ("ebay", {"organic_results"}), - ("youtube", {"video_results"}), + ( + "youtube", + { + "video_results", + "shorts_results", + "channel_results", + "playlist_results", + "movie_results", + "category_results", + }, + ), ("google_hotels", {"properties"}), ("google_flights", {"best_flights", "other_flights"}), ("google_travel_explore", {"destinations"}), @@ -535,12 +547,18 @@ def test_compact_mode_selects_primary_result_families( "answer_box", "knowledge_graph", "ai_overview", + "copilot_answer", "organic_results", "news_results", "local_results", "images_results", "shopping_results", "video_results", + "shorts_results", + "channel_results", + "playlist_results", + "movie_results", + "category_results", "properties", "best_flights", "other_flights", @@ -565,6 +583,7 @@ def search(self, params: dict[str, object]) -> dict[str, object]: "answer_box", "knowledge_graph", "ai_overview", + "copilot_answer", } else [{"value": key}] ) @@ -585,9 +604,9 @@ class GoogleLightClient: def search(self, params: dict[str, object]) -> dict[str, object]: return { "ai_overview": {"text": "Not a Google Light response section."}, - "related_questions": [{"question": str(index)} for index in range(7)], - "related_searches": [{"query": str(index)} for index in range(7)], - "top_stories": [{"title": str(index)} for index in range(7)], + "related_questions": [{"question": str(index)} for index in range(12)], + "related_searches": [{"query": str(index)} for index in range(12)], + "top_stories": [{"title": str(index)} for index in range(12)], } result = json.loads( @@ -599,7 +618,162 @@ def search(self, params: dict[str, object]) -> dict[str, object]: assert "ai_overview" not in result assert set(result) == {"related_questions", "related_searches", "top_stories"} - assert all(len(result[key]) == 5 for key in result) + assert all(len(result[key]) == 10 for key in result) + + +def test_result_limit_applies_independently_to_each_result_family() -> None: + class FlightsClient: + def search(self, params: dict[str, object]) -> dict[str, object]: + return { + "best_flights": [{"position": position} for position in range(6)], + "other_flights": [{"position": position} for position in range(6)], + } + + result = json.loads( + SearchRuntime(client=FlightsClient(), result_limit=3).execute( + engine="google_flights", + typed_params={"departure_id": "LAX", "arrival_id": "AUS"}, + ) + ) + + assert result == { + "best_flights": [{"position": position} for position in range(3)], + "other_flights": [{"position": position} for position in range(3)], + } + + +@pytest.mark.parametrize( + ("engine", "result_key", "item", "expected"), + [ + ( + "google_images", + "images_results", + { + "title": "Latte art", + "original": "https://images.test/latte.jpg", + "source_logo": "data:image/png;base64,large", + "related_content_id": "token", + "serpapi_related_content_link": "https://serpapi.test/related", + }, + {"title": "Latte art", "original": "https://images.test/latte.jpg"}, + ), + ( + "google_shopping", + "shopping_results", + { + "title": "Grinder", + "price": "$25", + "product_link": "https://shopping.test/product", + "immersive_product_page_token": "large-token", + "serpapi_immersive_product_api": "https://serpapi.test/product", + "serpapi_thumbnail": "https://serpapi.test/thumbnail", + "source_icon": "data:image/png;base64,large", + }, + { + "title": "Grinder", + "price": "$25", + "product_link": "https://shopping.test/product", + }, + ), + ( + "amazon", + "organic_results", + { + "title": "Grinder", + "link": "https://amazon.test/tracked", + "link_clean": "https://amazon.test/product", + "serpapi_link": "https://serpapi.test/product", + "more_buying_choices_link": "https://amazon.test/offers", + "purchase_options": [{"price": "$25"}], + }, + { + "title": "Grinder", + "link": "https://amazon.test/product", + "more_buying_choices_link": "https://amazon.test/offers", + }, + ), + ( + "walmart", + "organic_results", + { + "title": "Grinder", + "product_page_url": "https://walmart.test/product", + "serpapi_product_page_url": "https://serpapi.test/product", + "seller_id": "seller", + "variant_swatches": [{"name": "Black"}], + "muliple_options_available": False, + }, + {"title": "Grinder", "product_page_url": "https://walmart.test/product"}, + ), + ( + "ebay", + "organic_results", + { + "title": "Grinder", + "link": "https://ebay.test/product?tracking=large", + "serpapi_link": "https://serpapi.test/product", + "watchers": "18 watchers", + "extracted_watchers": 18, + "buying_format": "buy_it_now", + "buying_format_text": "Buy It Now", + }, + { + "title": "Grinder", + "link": "https://ebay.test/product?tracking=large", + "extracted_watchers": 18, + "buying_format": "buy_it_now", + }, + ), + ( + "google_hotels", + "properties", + { + "name": "Hotel", + "rate_per_night": {"lowest": "$100"}, + "images": [{"thumbnail": "one"}, {"thumbnail": "two"}], + "reviews_breakdown": [{"name": "Rooms"}], + "nearby_places": [{"name": "Airport"}], + "serpapi_property_details_link": "https://serpapi.test/property", + "serpapi_google_hotels_reviews_link": "https://serpapi.test/reviews", + "serpapi_google_hotels_photos_link": "https://serpapi.test/photos", + }, + { + "name": "Hotel", + "rate_per_night": {"lowest": "$100"}, + "images": [{"thumbnail": "one"}], + }, + ), + ( + "google_travel_explore", + "destinations", + { + "name": "Paris", + "flight_price": 622, + "link": "https://google.test/travel", + "serpapi_link": "https://serpapi.test/travel", + }, + {"name": "Paris", "flight_price": 622, "link": "https://google.test/travel"}, + ), + ], +) +def test_compact_mode_projects_large_vertical_result_objects( + engine: str, + result_key: str, + item: dict[str, object], + expected: dict[str, object], +) -> None: + class ProjectionClient: + def search(self, params: dict[str, object]) -> dict[str, object]: + return {result_key: [item]} + + result = json.loads( + SearchRuntime(client=ProjectionClient()).execute( + engine=engine, + typed_params={"q": "coffee"}, + ) + ) + + assert result == {result_key: [expected]} def test_compact_mode_returns_bounded_status_when_no_result_family_is_present() -> None: @@ -652,11 +826,19 @@ def search(self, params: dict[str, object]) -> dict[str, object]: @pytest.mark.parametrize("mode", [SearchResultMode.FULL, "full"]) -def test_full_mode_preserves_every_response_section(mode: SearchResultMode | str) -> None: +def test_full_mode_limits_results_but_preserves_every_other_section( + mode: SearchResultMode | str, +) -> None: response = { "search_metadata": {"status": "Success"}, - "organic_results": [{"title": "Coffee"}], - "related_searches": [{"query": "tea"}], + "organic_results": [ + {"title": "Coffee"}, + {"title": "Tea"}, + ], + "related_searches": [ + {"query": "tea"}, + {"query": "coffee beans"}, + ], } class FullClient: @@ -666,11 +848,22 @@ def search(self, params: dict[str, object]) -> dict[str, object]: encoded = SearchRuntime( client=FullClient(), mode=mode, + result_limit=1, ).execute(engine="google_light", typed_params={"q": "coffee"}) - assert json.loads(encoded) == response + assert json.loads(encoded) == { + "search_metadata": {"status": "Success"}, + "organic_results": [{"title": "Coffee"}], + "related_searches": [{"query": "tea"}], + } def test_invalid_result_mode_fails_when_the_tool_is_created() -> None: with pytest.raises(ValueError, match=r"mode must be one of: compact, full"): SearchRuntime(client=FakeClient(), mode="summary") + + +@pytest.mark.parametrize("result_limit", [0, True, "5"]) +def test_invalid_result_limit_fails_when_the_tool_is_created(result_limit: object) -> None: + with pytest.raises(ValueError, match="result_limit must be a positive integer or None"): + SearchRuntime(client=FakeClient(), result_limit=result_limit) # type: ignore[arg-type]