diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py
index 59183b8a613..1610308187e 100644
--- a/astrbot/core/astr_main_agent.py
+++ b/astrbot/core/astr_main_agent.py
@@ -107,6 +107,7 @@
FirecrawlWebSearchTool,
SerpApiFinanceSearchTool,
SerpApiImageSearchTool,
+ SerpApiNewsSearchTool,
SerpApiScholarSearchTool,
SerpApiWebSearchTool,
TavilyExtractWebPageTool,
@@ -170,6 +171,7 @@
"web_search_anysearch",
"web_search_deepseek",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
@@ -1312,6 +1314,7 @@ async def _apply_web_search_tools(
req.func_tool.add_tool(tool_mgr.get_builtin_tool(DeepSeekFetchWebPageTool))
elif provider == "serpapi":
req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerpApiWebSearchTool))
+ req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerpApiNewsSearchTool))
req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerpApiImageSearchTool))
req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerpApiFinanceSearchTool))
req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerpApiScholarSearchTool))
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 150804573a8..ee27298071a 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -3695,7 +3695,7 @@ def get_local_permission_defaults(system: str | None = None) -> dict:
"AnySearch",
],
"subtitles": [
- "Google 网页、图片、学术和财经搜索",
+ "Google 网页、新闻、图片、学术和财经搜索",
"DeepSeek 官方搜索接口",
"",
"",
@@ -3799,7 +3799,7 @@ def get_local_permission_defaults(system: str | None = None) -> dict:
"description": "SerpApi API Key",
"type": "list",
"items": {"type": "string"},
- "hint": "支持 Google 网页、图片、学术和财经搜索。可添加多个 Key 轮询。获取 Key:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)",
+ "hint": "支持 Google 网页、新闻、图片、学术和财经搜索。可添加多个 Key 轮询。获取 Key:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)",
"secret": True,
"condition": {
"provider_settings.websearch_provider": "serpapi",
diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py
index 35826648da2..fa7eea312f9 100644
--- a/astrbot/core/tools/web_search_tools.py
+++ b/astrbot/core/tools/web_search_tools.py
@@ -26,6 +26,7 @@
"exa_get_contents",
"web_search_anysearch",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
@@ -604,6 +605,108 @@ async def call(self, context, **kwargs) -> ToolExecResult:
return _search_result_payload(results[:max_results])
+@builtin_tool(config=_SERPAPI_WEB_SEARCH_TOOL_CONFIG)
+@pydantic_dataclass
+class SerpApiNewsSearchTool(FunctionTool[AstrAgentContext]):
+ name: str = "news_search_serpapi"
+ description: str = (
+ "Search current news using the SerpApi Google News API. Returns article "
+ "titles, source URLs, publishers, authors, snippets, and publication times."
+ )
+ parameters: dict = Field(
+ default_factory=lambda: {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Required. News search query. Google News operators such as site: and when: are supported.",
+ },
+ "max_results": {
+ "type": "integer",
+ "description": "Maximum articles to return, 1-10. Default is 5; fewer may be available.",
+ },
+ "country": {
+ "type": "string",
+ "description": "Optional. Google News country code, such as us, uk, or cn.",
+ },
+ "search_lang": {
+ "type": "string",
+ "description": "Optional. Google News language code, such as en or zh-cn.",
+ },
+ },
+ "required": ["query"],
+ }
+ )
+
+ async def call(self, context, **kwargs) -> ToolExecResult:
+ """Search Google News and preserve article metadata for citations.
+
+ Args:
+ context: Agent context containing the active profile settings.
+ **kwargs: News query, result limit, country, and language.
+
+ Returns:
+ News results as JSON, or a configuration or validation error.
+
+ Raises:
+ RuntimeError: If the search request fails.
+ """
+ _, provider_settings, _ = _get_runtime(context)
+ if not provider_settings.get("websearch_serpapi_key", []):
+ return "Error: SerpApi API key is not configured in AstrBot."
+
+ query = kwargs["query"].strip()
+ if not query:
+ return "Error: query must not be empty."
+ max_results = max(1, min(int(kwargs.get("max_results", 5)), 10))
+ payload = {"q": query}
+ for name, parameter in (("country", "gl"), ("search_lang", "hl")):
+ if kwargs.get(name):
+ payload[parameter] = kwargs[name]
+ data = await _serpapi_request(provider_settings, payload, engine="google_news")
+
+ ref_uuid = str(uuid.uuid4())[:4]
+ results = []
+ seen_urls = set()
+ for group in data.get("news_results", []):
+ articles = []
+ if group.get("link"):
+ articles.append(group)
+ if isinstance(group.get("highlight"), dict):
+ articles.append(group["highlight"])
+ articles.extend(
+ story for story in group.get("stories", []) if isinstance(story, dict)
+ )
+ for article in articles:
+ source_url = article.get("link")
+ if not source_url or source_url in seen_urls:
+ continue
+ seen_urls.add(source_url)
+ source = article.get("source") or {}
+ results.append(
+ {
+ "title": article.get("title") or "",
+ "url": source_url,
+ "snippet": article.get("snippet") or "",
+ "source": source.get("name") or source.get("title") or "",
+ "authors": source.get("authors") or [],
+ "published_at": article.get("iso_date")
+ or article.get("date")
+ or "",
+ "thumbnail_url": article.get("thumbnail")
+ or article.get("thumbnail_small")
+ or "",
+ "index": f"{ref_uuid}.{len(results) + 1}",
+ }
+ )
+ _cache_favicon(source_url, source.get("icon"))
+ if len(results) >= max_results:
+ break
+ if len(results) >= max_results:
+ break
+ return json.dumps({"results": results}, ensure_ascii=False)
+
+
@builtin_tool(config=_SERPAPI_WEB_SEARCH_TOOL_CONFIG)
@pydantic_dataclass
class SerpApiImageSearchTool(FunctionTool[AstrAgentContext]):
@@ -2128,6 +2231,7 @@ async def call(self, context, **kwargs) -> ToolExecResult:
"ExaWebSearchTool",
"SerpApiFinanceSearchTool",
"SerpApiImageSearchTool",
+ "SerpApiNewsSearchTool",
"SerpApiScholarSearchTool",
"SerpApiWebSearchTool",
"TavilyExtractWebPageTool",
diff --git a/astrbot/dashboard/services/chat_service.py b/astrbot/dashboard/services/chat_service.py
index fe3e24264cd..7c4a8a3f958 100644
--- a/astrbot/dashboard/services/chat_service.py
+++ b/astrbot/dashboard/services/chat_service.py
@@ -278,6 +278,7 @@ def extract_web_search_refs(accumulated_text: str, accumulated_parts: list) -> d
"web_search_brave",
"web_search_deepseek",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
diff --git a/astrbot/dashboard/services/live_chat_service.py b/astrbot/dashboard/services/live_chat_service.py
index 2b17c5e7c76..c34986923b8 100644
--- a/astrbot/dashboard/services/live_chat_service.py
+++ b/astrbot/dashboard/services/live_chat_service.py
@@ -289,6 +289,7 @@ def extract_web_search_refs(accumulated_text: str, accumulated_parts: list) -> d
"web_search_brave",
"web_search_deepseek",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md
index 2e6d55d8881..d7cb966a8ad 100644
--- a/changelogs/unreleased.md
+++ b/changelogs/unreleased.md
@@ -2,6 +2,8 @@
- Simplify Context Management Strategy settings: Handling for History Limits or Context Window Pressure → Compression Strategy; Model Provider ID for Context Compression → Compression Model. Move maximum turns, discarded turns, compression instruction, recent-context ratio and fallback context window size under the collapsed More Settings section. Preserve existing configuration keys, defaults and runtime behavior.
+- Add SerpApi Google News search with publisher, author, publication time, and WebUI citation support.
+
- Add compact data-cleanup checkboxes to the existing bot deletion dialog. Custom rules, preferences, configuration routes and scheduled tasks are selected by default; AI conversations, message history, local traces and usage statistics are retained unless selected. Preserve shared resources and match adapter IDs exactly.
- Resolve Lark group-message sender names through the existing user-info lookup, sharing the same per-user cache with private chats (30 minutes for successful names and one minute for failures). Application senders skip user-directory lookups.
diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
index c3ed8c06be6..9fd9a516ccd 100644
--- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
@@ -188,7 +188,7 @@
"AnySearch"
],
"subtitles": [
- "Google web, image, scholar, and finance search",
+ "Google web, news, image, scholar, and finance search",
"Official DeepSeek search API",
"",
"",
@@ -232,7 +232,7 @@
},
"websearch_serpapi_key": {
"description": "SerpApi API Key",
- "hint": "Supports Google web, image, scholar, and finance search. Multiple keys can be added for rotation. Get a key: [https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
+ "hint": "Supports Google web, news, image, scholar, and finance search. Multiple keys can be added for rotation. Get a key: [https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
},
"websearch_deepseek_key": {
"description": "DeepSeek API Key",
diff --git a/dashboard/src/i18n/locales/ja-JP/features/config-metadata.json b/dashboard/src/i18n/locales/ja-JP/features/config-metadata.json
index e798966582d..f13ca731991 100644
--- a/dashboard/src/i18n/locales/ja-JP/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/ja-JP/features/config-metadata.json
@@ -276,7 +276,7 @@
"AnySearch"
],
"subtitles": [
- "Google のウェブ・画像・学術・金融検索",
+ "Google のウェブ・ニュース・画像・学術・金融検索",
"DeepSeek 公式検索 API",
"",
"",
@@ -320,7 +320,7 @@
},
"websearch_serpapi_key": {
"description": "SerpApi API Key",
- "hint": "Google のウェブ・画像・学術・金融検索に対応。複数の Key を追加してローテーションできます。Key の取得:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
+ "hint": "Google のウェブ・ニュース・画像・学術・金融検索に対応。複数の Key を追加してローテーションできます。Key の取得:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
},
"websearch_deepseek_key": {
"description": "DeepSeek API Key",
diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
index 5b7ce75ed72..46bd0d7ce36 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
@@ -276,7 +276,7 @@
"AnySearch"
],
"subtitles": [
- "Веб-поиск, изображения, научные публикации и финансы Google",
+ "Веб-поиск, новости, изображения, научные публикации и финансы Google",
"Официальный поисковый API DeepSeek",
"",
"",
@@ -320,7 +320,7 @@
},
"websearch_serpapi_key": {
"description": "SerpApi API Key",
- "hint": "Поддерживает веб-поиск, поиск изображений, научных публикаций и финансовых данных Google. Можно добавить несколько ключей для ротации. Получить ключ: [https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
+ "hint": "Поддерживает веб-поиск, поиск новостей, изображений, научных публикаций и финансовых данных Google. Можно добавить несколько ключей для ротации. Получить ключ: [https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
},
"websearch_deepseek_key": {
"description": "API-ключ DeepSeek",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
index b50068469ed..e588d8c0c9d 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
@@ -204,7 +204,7 @@
"AnySearch"
],
"subtitles": [
- "Google 网页、图片、学术和财经搜索",
+ "Google 网页、新闻、图片、学术和财经搜索",
"DeepSeek 官方搜索接口",
"",
"",
@@ -248,7 +248,7 @@
},
"websearch_serpapi_key": {
"description": "SerpApi API Key",
- "hint": "支持 Google 网页、图片、学术和财经搜索。可添加多个 Key 轮询。获取 Key:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
+ "hint": "支持 Google 网页、新闻、图片、学术和财经搜索。可添加多个 Key 轮询。获取 Key:[https://serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)"
},
"websearch_deepseek_key": {
"description": "DeepSeek API Key",
diff --git a/docs/en/dev/astrbot-config.md b/docs/en/dev/astrbot-config.md
index 5c790c9e2ae..8395eb06ffb 100644
--- a/docs/en/dev/astrbot-config.md
+++ b/docs/en/dev/astrbot-config.md
@@ -296,7 +296,7 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`.
Web search provider type. Default is `tavily`. Currently supports `serpapi`, `deepseek`, `tavily`, `bocha`, `baidu_ai_search`, `brave`, `firecrawl`, `exa`, and `anysearch`.
-- `serpapi`: Uses SerpApi Google Search, Google Images Light, Google Finance, and Google Scholar.
+- `serpapi`: Uses SerpApi Google Search, Google News, Google Images Light, Google Finance, and Google Scholar.
- `deepseek`: Uses the DeepSeek Search API.
- `tavily`: Uses the Tavily search engine.
- `bocha`: Uses the BoCha search engine.
@@ -328,7 +328,7 @@ API Key list for the DeepSeek search engine. Required when using `deepseek` as t
#### `provider_settings.websearch_serpapi_key`
-API Key list shared by SerpApi Google Search, Google Images Light, Google Finance, and Google Scholar. Required when using `serpapi` as the web search provider. Obtain a key at [SerpApi](https://serpapi.com/manage-api-key). Multiple keys rotate, with failover on authentication and quota errors.
+API Key list shared by SerpApi Google Search, Google News, Google Images Light, Google Finance, and Google Scholar. Required when using `serpapi` as the web search provider. Obtain a key at [SerpApi](https://serpapi.com/manage-api-key). Multiple keys rotate, with failover on authentication and quota errors.
#### `provider_settings.web_search_link`
diff --git a/docs/en/use/websearch.md b/docs/en/use/websearch.md
index 6642246d048..262f464cd9c 100644
--- a/docs/en/use/websearch.md
+++ b/docs/en/use/websearch.md
@@ -31,7 +31,7 @@ AstrBot supports nine providers. Configure one to start; you do not need an acco
| Provider | Get a key | Capabilities available in AstrBot |
| --- | --- | --- |
-| [SerpApi](./websearch/serpapi.md) | [SerpApi API key](https://serpapi.com/manage-api-key) | Google web search, Images Light, Finance, and Scholar |
+| [SerpApi](./websearch/serpapi.md) | [SerpApi API key](https://serpapi.com/manage-api-key) | Google web search, News, Images Light, Finance, and Scholar |
| [DeepSeek Search API](./websearch/deepseek.md) | [DeepSeek Platform](https://platform.deepseek.com/api_keys) / [Search Docs](https://api-docs.deepseek.com/guides/search) | Web search and page content extraction from a URL |
| Tavily | [Tavily dashboard](https://app.tavily.com/home) | Web search and page content extraction from a URL |
| Bocha | [BoCha](https://bochaai.com/) | Web search |
diff --git a/docs/zh/dev/astrbot-config.md b/docs/zh/dev/astrbot-config.md
index ded6964b7ec..588284193e7 100644
--- a/docs/zh/dev/astrbot-config.md
+++ b/docs/zh/dev/astrbot-config.md
@@ -296,7 +296,7 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。
网页搜索提供商类型。默认为 `tavily`。目前支持 `serpapi`、`deepseek`、`tavily`、`bocha`、`baidu_ai_search`、`brave`、`firecrawl`、`exa`、`anysearch`。
-- `serpapi`:使用 SerpApi Google Search、Google Images Light、Google Finance 和 Google Scholar。
+- `serpapi`:使用 SerpApi Google Search、Google News、Google Images Light、Google Finance 和 Google Scholar。
- `deepseek`:使用 DeepSeek Search API。
- `tavily`:使用 Tavily 搜索引擎。
- `bocha`:使用 BoCha 搜索引擎。
@@ -328,7 +328,7 @@ DeepSeek 搜索引擎的 API Key 列表。使用 `deepseek` 作为网页搜索
#### `provider_settings.websearch_serpapi_key`
-SerpApi Google Search、Google Images Light、Google Finance 和 Google Scholar 共用的 API Key 列表。使用 `serpapi` 作为网页搜索提供商时需要填写。前往 [SerpApi](https://serpapi.com/manage-api-key) 获取 Key。多个 Key 轮询使用,鉴权或额度错误时会尝试下一个 Key。
+SerpApi Google Search、Google News、Google Images Light、Google Finance 和 Google Scholar 共用的 API Key 列表。使用 `serpapi` 作为网页搜索提供商时需要填写。前往 [SerpApi](https://serpapi.com/manage-api-key) 获取 Key。多个 Key 轮询使用,鉴权或额度错误时会尝试下一个 Key。
#### `provider_settings.web_search_link`
diff --git a/docs/zh/use/websearch.md b/docs/zh/use/websearch.md
index 5114e85faf3..4489df8ce29 100644
--- a/docs/zh/use/websearch.md
+++ b/docs/zh/use/websearch.md
@@ -31,7 +31,7 @@
| 提供商 | Key 申请入口 | AstrBot 提供的能力 |
| --- | --- | --- |
-| [SerpApi](./websearch/serpapi.md) | [SerpApi API Key](https://serpapi.com/manage-api-key) | Google 网页搜索、Images Light 图片搜索、Finance 金融查询、Scholar 学术搜索 |
+| [SerpApi](./websearch/serpapi.md) | [SerpApi API Key](https://serpapi.com/manage-api-key) | Google 网页搜索、News 新闻、Images Light 图片搜索、Finance 金融查询、Scholar 学术搜索 |
| [DeepSeek Search API](./websearch/deepseek.md) | [DeepSeek 开放平台](https://platform.deepseek.com/api_keys) / [搜索文档](https://api-docs.deepseek.com/zh-cn/guides/search) | 网页搜索、按 URL 提取网页正文 |
| Tavily | [Tavily 控制台](https://app.tavily.com/home) | 网页搜索、按 URL 提取网页正文 |
| Bocha | [博查](https://bochaai.com/) | 网页搜索 |
diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py
index 3089bbc7b34..a02d3054c54 100644
--- a/tests/unit/test_astr_main_agent.py
+++ b/tests/unit/test_astr_main_agent.py
@@ -799,6 +799,7 @@ async def test_serpapi_injects_all_search_tools(self, mock_event, mock_context):
assert req.func_tool.names() == [
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
@@ -898,6 +899,7 @@ async def test_apply_web_search_tools_adds_search_and_extract_tools(
"web_search_tavily",
"web_search_deepseek",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
@@ -1583,6 +1585,7 @@ async def test_ensure_tools_from_persona(self, mock_event, mock_context):
"serpapi",
[
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",
diff --git a/tests/unit/test_serpapi_search.py b/tests/unit/test_serpapi_search.py
index 60cfefea760..3ebe26752fb 100644
--- a/tests/unit/test_serpapi_search.py
+++ b/tests/unit/test_serpapi_search.py
@@ -542,10 +542,128 @@ async def test_serpapi_tool_limits_first_page_results(serpapi_http, limit, expec
}
+@pytest.mark.asyncio
+async def test_serpapi_news_maps_articles_and_metadata(
+ serpapi_http, serpapi_context, monkeypatch
+):
+ cache = {}
+ monkeypatch.setitem(tools.sp.temporary_cache, "_ws_favicon", cache)
+ serpapi_http.session.get.return_value = _response(
+ data={
+ "news_results": [
+ {
+ "title": "AstrBot adds a news search tool",
+ "link": "https://example.com/astrbot-news",
+ "snippet": "The project added Google News support.",
+ "source": {
+ "name": "Example News",
+ "icon": "https://example.com/favicon.ico",
+ "authors": ["A. Reporter"],
+ },
+ "iso_date": "2026-10-24T08:00:00Z",
+ "thumbnail": "https://example.com/news.jpg",
+ },
+ {"title": "Group without a direct article", "stories": []},
+ ]
+ }
+ )
+
+ result = json.loads(
+ await tools.SerpApiNewsSearchTool().call(
+ serpapi_context,
+ query="AstrBot",
+ country="us",
+ search_lang="en",
+ )
+ )["results"]
+
+ assert serpapi_http.session.get.call_args.kwargs["params"] == {
+ "engine": "google_news",
+ "api_key": "test-key",
+ "q": "AstrBot",
+ "gl": "us",
+ "hl": "en",
+ }
+ assert result == [
+ {
+ "title": "AstrBot adds a news search tool",
+ "url": "https://example.com/astrbot-news",
+ "snippet": "The project added Google News support.",
+ "source": "Example News",
+ "authors": ["A. Reporter"],
+ "published_at": "2026-10-24T08:00:00Z",
+ "thumbnail_url": "https://example.com/news.jpg",
+ "index": result[0]["index"],
+ }
+ ]
+ assert result[0]["index"].endswith(".1")
+ assert cache == {
+ "https://example.com/astrbot-news": "https://example.com/favicon.ico"
+ }
+
+
+@pytest.mark.asyncio
+async def test_serpapi_news_flattens_groups_deduplicates_and_limits(
+ serpapi_http, serpapi_context
+):
+ serpapi_http.session.get.return_value = _response(
+ data={
+ "news_results": [
+ {
+ "highlight": {
+ "title": "Highlighted article",
+ "link": "https://example.com/highlight",
+ },
+ "stories": [
+ {
+ "title": "Duplicate article",
+ "link": "https://example.com/highlight",
+ },
+ {
+ "title": "Related article",
+ "link": "https://example.com/related",
+ "date": "Oct 24, 2026",
+ },
+ ],
+ }
+ ]
+ }
+ )
+
+ result = json.loads(
+ await tools.SerpApiNewsSearchTool().call(
+ serpapi_context, query="AstrBot", max_results=2
+ )
+ )["results"]
+
+ assert [item["url"] for item in result] == [
+ "https://example.com/highlight",
+ "https://example.com/related",
+ ]
+ assert result[1]["published_at"] == "Oct 24, 2026"
+
+
+@pytest.mark.asyncio
+async def test_serpapi_news_validates_query_before_request(
+ serpapi_http, serpapi_context
+):
+ result = await tools.SerpApiNewsSearchTool().call(serpapi_context, query=" ")
+
+ assert result == "Error: query must not be empty."
+ serpapi_http.factory.assert_not_called()
+
+
@pytest.mark.asyncio
@pytest.mark.parametrize("status", [401, 403, 429])
@pytest.mark.parametrize(
- "engine", ["google", "google_images_light", "google_finance", "google_scholar"]
+ "engine",
+ [
+ "google",
+ "google_news",
+ "google_images_light",
+ "google_finance",
+ "google_scholar",
+ ],
)
async def test_serpapi_rotates_and_retries_key_errors(serpapi_http, status, engine):
serpapi_http.session.get.side_effect = [
@@ -652,6 +770,7 @@ async def test_serpapi_transport_errors_do_not_expose_credentials(
"tool_class",
[
tools.SerpApiWebSearchTool,
+ tools.SerpApiNewsSearchTool,
tools.SerpApiImageSearchTool,
tools.SerpApiFinanceSearchTool,
tools.SerpApiScholarSearchTool,
@@ -682,6 +801,7 @@ async def test_serpapi_requires_credentials_before_request(serpapi_http, tool_cl
"tool_class",
[
tools.SerpApiWebSearchTool,
+ tools.SerpApiNewsSearchTool,
tools.SerpApiImageSearchTool,
tools.SerpApiFinanceSearchTool,
tools.SerpApiScholarSearchTool,
diff --git a/tests/unit/test_web_search_refs.py b/tests/unit/test_web_search_refs.py
index d805e229888..3132f244ff1 100644
--- a/tests/unit/test_web_search_refs.py
+++ b/tests/unit/test_web_search_refs.py
@@ -19,6 +19,7 @@
[
"web_search_deepseek",
"web_search_serpapi",
+ "news_search_serpapi",
"image_search_serpapi",
"finance_search_serpapi",
"scholar_search_serpapi",