Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
ExaWebSearchTool,
FirecrawlExtractWebPageTool,
FirecrawlWebSearchTool,
SerplyWebSearchTool,
TavilyExtractWebPageTool,
TavilyWebSearchTool,
normalize_legacy_web_search_config,
Expand Down Expand Up @@ -145,6 +146,7 @@
"web_search_bocha",
"web_search_brave",
"web_search_exa",
"web_search_serply",
}
)
WEB_SEARCH_CITATION_PROMPT = (
Expand Down Expand Up @@ -1285,6 +1287,8 @@ async def _apply_web_search_tools(
elif provider == "exa":
req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExaWebSearchTool))
req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExaGetContentsTool))
elif provider == "serply":
req.func_tool.add_tool(tool_mgr.get_builtin_tool(SerplyWebSearchTool))


def _apply_web_search_citation_prompt(
Expand Down
13 changes: 13 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"websearch_baidu_app_builder_key": "",
"websearch_firecrawl_key": [],
"websearch_exa_key": [],
"websearch_serply_key": [],
"web_search_link": False,
"display_reasoning_text": False,
"identifier": False,
Expand Down Expand Up @@ -3567,6 +3568,7 @@
"brave",
"firecrawl",
"exa",
"serply",
],
"condition": {
"provider_settings.web_search": True,
Expand Down Expand Up @@ -3637,6 +3639,17 @@
"provider_settings.web_search": True,
},
},
"provider_settings.websearch_serply_key": {
"description": "Serply API Key",
"type": "list",
"items": {"type": "string"},
"hint": "可添加多个 Key 进行轮询。Get a key at https://serply.io",
"secret": True,
"condition": {
"provider_settings.websearch_provider": "serply",
"provider_settings.web_search": True,
},
},
"provider_settings.web_search_link": {
"description": "显示来源引用",
"type": "bool",
Expand Down
155 changes: 155 additions & 0 deletions astrbot/core/tools/web_search_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"firecrawl_extract_web_page",
"web_search_exa",
"exa_get_contents",
"web_search_serply",
]
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
"provider_settings.web_search": True,
Expand All @@ -48,6 +49,16 @@
"provider_settings.web_search": True,
"provider_settings.websearch_provider": "exa",
}
_SERPLY_WEB_SEARCH_TOOL_CONFIG = {
"provider_settings.web_search": True,
"provider_settings.websearch_provider": "serply",
}
# Serply search vertical -> (endpoint, key of the result list in the response).
_SERPLY_SEARCH_ENDPOINTS = {
"web": ("https://api.serply.io/v1/search/", "results"),
"news": ("https://api.serply.io/v1/news/", "entries"),
"scholar": ("https://api.serply.io/v1/scholar/", "articles"),
}


@std_dataclass
Expand Down Expand Up @@ -110,6 +121,7 @@ async def get(self, provider_settings: dict) -> str:
_BRAVE_KEY_ROTATOR = _KeyRotator("websearch_brave_key", "Brave")
_FIRECRAWL_KEY_ROTATOR = _KeyRotator("websearch_firecrawl_key", "Firecrawl")
_EXA_KEY_ROTATOR = _KeyRotator("websearch_exa_key", "Exa")
_SERPLY_KEY_ROTATOR = _KeyRotator("websearch_serply_key", "Serply")


def normalize_legacy_web_search_config(cfg) -> None:
Expand All @@ -134,6 +146,7 @@ def normalize_legacy_web_search_config(cfg) -> None:
"websearch_brave_key",
"websearch_firecrawl_key",
"websearch_exa_key",
"websearch_serply_key",
):
value = provider_settings.get(setting_name)
if isinstance(value, str):
Expand Down Expand Up @@ -1240,12 +1253,154 @@ async def call(self, context, **kwargs) -> ToolExecResult:
return ret or "Error: Exa get contents does not return any results."


async def _serply_search(
provider_settings: dict,
search_type: str,
params: dict,
) -> list[SearchResult]:
"""Call the Serply search API with API key failover.

Args:
provider_settings: Provider settings containing Serply API keys.
search_type: Search vertical, one of the keys of _SERPLY_SEARCH_ENDPOINTS.
params: Query parameters for the Serply endpoint.

Returns:
Normalized search results.

Raises:
ValueError: If Serply API keys are not configured.
Exception: If the request fails after all retryable keys are exhausted,
or if a non-retryable HTTP error is returned.
"""
keys = provider_settings.get("websearch_serply_key", [])
if not keys:
raise ValueError("Error: Serply API key is not configured in AstrBot.")

url, results_key = _SERPLY_SEARCH_ENDPOINTS[search_type]
last_error = None
for _ in range(len(keys)):
serply_key = await _SERPLY_KEY_ROTATOR.get(provider_settings)
header = {
"Accept": "application/json",
"X-Api-Key": serply_key,
}
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url,
params=params,
headers=header,
) as response:
if response.status == 200:
data = await response.json()
return [
SearchResult(
title=item.get("title", ""),
url=item.get("link", ""),
# News entries carry an HTML summary; use source and date instead.
snippet=item.get("description")
or (
f"{(item.get('source') or {}).get('title', '')} "
f"{item.get('published', '')}"
).strip(),
)
for item in data.get(results_key, [])
if item.get("link")
]
reason = await response.text()
if response.status in _RETRYABLE_HTTP_STATUSES:
last_error = Exception(
f"Serply web search failed: {reason}, status: {response.status}",
)
continue
raise Exception(
f"Serply web search failed: {reason}, status: {response.status}",
)

if last_error is not None:
raise last_error
raise Exception("Serply web search failed with all configured keys.")


@builtin_tool(config=_SERPLY_WEB_SEARCH_TOOL_CONFIG)
@pydantic_dataclass
class SerplyWebSearchTool(FunctionTool[AstrAgentContext]):
"""Web search tool powered by the Serply API (Google web, news and scholar)."""

name: str = "web_search_serply"
description: str = (
"A web search tool powered by Serply, which returns live Google search results. "
"Supports Google web search as well as the Google News and Google Scholar verticals."
)
parameters: dict = Field(
default_factory=lambda: {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Required. Search query."},
"num_results": {
"type": "integer",
"description": "Optional. Number of results to return. Range: 1-100. Default is 10.",
},
"search_type": {
"type": "string",
"description": (
'Optional. Search vertical. One of "web", "news", "scholar". '
'Default is "web". Use "news" for recent news coverage and '
'"scholar" for academic papers.'
),
},
"gl": {
"type": "string",
"description": 'Optional. Country code for region-specific results, for example "us" or "cn".',
},
"hl": {
"type": "string",
"description": 'Optional. Google interface language code, for example "en" or "zh-cn".',
},
},
"required": ["query"],
}
)

async def call(self, context, **kwargs) -> ToolExecResult:
_, provider_settings, _ = _get_runtime(context)
if not provider_settings.get("websearch_serply_key", []):
return "Error: Serply API key is not configured in AstrBot."

try:
num_results = int(kwargs.get("num_results", 10))
except (TypeError, ValueError):
num_results = 10
if num_results < 1:
num_results = 1
if num_results > 100:
num_results = 100

search_type = kwargs.get("search_type", "web")
if search_type not in _SERPLY_SEARCH_ENDPOINTS:
search_type = "web"

params: dict = {"q": kwargs["query"], "num": num_results}
if kwargs.get("gl"):
params["gl"] = kwargs["gl"]
if kwargs.get("hl"):
params["hl"] = kwargs["hl"]

# The news and scholar feeds do not honor `num`, so cap client-side.
results = await _serply_search(provider_settings, search_type, params)
results = results[:num_results]
if not results:
return "Error: Serply web search does not return any results."
return _search_result_payload(results)


__all__ = [
"BaiduWebSearchTool",
"BochaWebSearchTool",
"BraveWebSearchTool",
"ExaGetContentsTool",
"ExaWebSearchTool",
"SerplyWebSearchTool",
"TavilyExtractWebPageTool",
"TavilyWebSearchTool",
"WEB_SEARCH_TOOL_NAMES",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
"websearch_exa_key": {
"description": "Exa API Key",
"hint": "Multiple keys can be added for rotation. Get a key at https://dashboard.exa.ai"
},
"websearch_serply_key": {
"description": "Serply API Key",
"hint": "Multiple keys can be added for rotation. Get a key at https://serply.io"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
"websearch_exa_key": {
"description": "API-ключ Exa",
"hint": "Можно добавить несколько ключей для ротации. Получить ключ: https://dashboard.exa.ai"
},
"websearch_serply_key": {
"description": "API-ключ Serply",
"hint": "Можно добавить несколько ключей для ротации. Получить ключ: https://serply.io"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@
"websearch_exa_key": {
"description": "Exa API Key",
"hint": "可添加多个 Key 进行轮询。获取 Key: https://dashboard.exa.ai"
},
"websearch_serply_key": {
"description": "Serply API Key",
"hint": "可添加多个 Key 进行轮询。获取 Key: https://serply.io"
}
}
},
Expand Down
8 changes: 6 additions & 2 deletions docs/en/use/websearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ When using a large language model that supports function calling with the web se

And other prompts with search intent to trigger the model to invoke the search tool.

AstrBot currently supports 6 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, and `Exa`.
AstrBot currently supports 7 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, `Exa`, and `Serply`.

![image](https://files.astrbot.app/docs/source/images/websearch/image.png)

Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, or `Exa`.
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, `Exa`, or `Serply`.

### Tavily

Expand All @@ -44,6 +44,10 @@ Go to [Firecrawl](https://firecrawl.dev) to get an API Key, then fill it in the

Go to [Exa](https://dashboard.exa.ai) to get an API Key, then fill it in the corresponding configuration item. Exa is an AI-native search engine that supports keyword and semantic search with category filters, domain restrictions, and date ranges.

### Serply

Go to [Serply](https://serply.io) to get an API Key, then fill it in the corresponding configuration item. Serply returns live Google search results and also exposes the Google News and Google Scholar verticals through the `search_type` parameter of the search tool. See the [Serply API docs](https://serply.io/docs) for details.

If you use Tavily as your web search source, you will get a better experience optimization on AstrBot ChatUI, including citation source display and more:

![](https://files.astrbot.app/docs/source/images/websearch/image1.png)
8 changes: 6 additions & 2 deletions docs/zh/use/websearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ AstrBot 内置的网页搜索功能依赖大模型提供 `函数调用` 能力

等等带有搜索意味的提示让大模型触发调用搜索工具。

AstrBot 当前支持 6 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl`、`Exa`。
AstrBot 当前支持 7 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl`、`Exa`、`Serply`。

![image](https://files.astrbot.app/docs/source/images/websearch/image.png)

进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl` 或 `Exa`。
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl`、`Exa` 或 `Serply`。

### Tavily

Expand All @@ -43,6 +43,10 @@ AstrBot 当前支持 6 种网页搜索源接入方式:`Tavily`、`BoCha`、`

前往 [Exa](https://dashboard.exa.ai) 获取 API Key,然后填写在相应的配置项。Exa 是一个 AI 原生搜索引擎,支持关键词和语义搜索,提供分类过滤、域名限制和日期范围等高级搜索功能。

### Serply

前往 [Serply](https://serply.io) 获取 API Key,然后填写在相应的配置项。Serply 返回实时的 Google 搜索结果,并可通过搜索工具的 `search_type` 参数使用 Google 新闻和 Google 学术两个垂直搜索。详情参见 [Serply API 文档](https://serply.io/docs)。

如果您使用 Tavily 作为网页搜索源,在 AstrBot ChatUI 上将会获得更好的体验优化,包括引用来源展示等:

![](https://files.astrbot.app/docs/source/images/websearch/image1.png)
Loading