feat: add new web search tool Brave Search (#6847)

* feat:Brave Search API adapted

* Modified hint message

* Modified according to AI reviews

* chore: revert compose.yml changes

---------

Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
daniel5u
2026-04-08 12:00:21 +08:00
committed by GitHub
co-authored by Soulter
parent ef15955096
commit c517cdb490
11 changed files with 194 additions and 8 deletions
+127
View File
@@ -24,6 +24,7 @@ class Main(star.Star):
"web_search_tavily",
"tavily_extract_web_page",
"web_search_bocha",
"web_search_brave",
]
def __init__(self, context: star.Context) -> None:
@@ -33,6 +34,8 @@ class Main(star.Star):
self.bocha_key_index = 0
self.bocha_key_lock = asyncio.Lock()
self.brave_key_index = 0
self.brave_key_lock = asyncio.Lock()
# 将 str 类型的 key 迁移至 list[str],并保存
cfg = self.context.get_config()
@@ -57,6 +60,14 @@ class Main(star.Star):
provider_settings["websearch_bocha_key"] = []
cfg.save_config()
brave_key = provider_settings.get("websearch_brave_key")
if isinstance(brave_key, str):
if brave_key:
provider_settings["websearch_brave_key"] = [brave_key]
else:
provider_settings["websearch_brave_key"] = []
cfg.save_config()
self.bing_search = Bing()
self.sogo_search = Sogo()
self.baidu_initialized = False
@@ -430,6 +441,50 @@ class Main(star.Star):
results.append(result)
return results
async def _get_brave_key(self, cfg: AstrBotConfig) -> str:
"""并发安全的从列表中获取并轮换 Brave API 密钥。"""
brave_keys = cfg.get("provider_settings", {}).get("websearch_brave_key", [])
async with self.brave_key_lock:
key = brave_keys[self.brave_key_index]
self.brave_key_index = (self.brave_key_index + 1) % len(brave_keys)
return key
async def _web_search_brave(
self,
cfg: AstrBotConfig,
payload: dict,
) -> list[SearchResult]:
"""使用 Brave 搜索引擎进行搜索"""
brave_key = await self._get_brave_key(cfg)
url = "https://api.search.brave.com/res/v1/web/search"
header = {
"Accept": "application/json",
"X-Subscription-Token": brave_key,
}
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url,
params=payload,
headers=header,
) as response:
if response.status != 200:
reason = await response.text()
raise Exception(
f"Brave web search failed: {reason}, status: {response.status}",
)
data = await response.json()
rows = data.get("web", {}).get("results", [])
results = []
for item in rows:
result = SearchResult(
title=item.get("title", ""),
url=item.get("url", ""),
snippet=item.get("description", ""),
)
results.append(result)
return results
@llm_tool("web_search_bocha")
async def search_from_bocha(
self,
@@ -537,6 +592,64 @@ class Main(star.Star):
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
return ret
@llm_tool("web_search_brave")
async def search_from_brave(
self,
event: AstrMessageEvent,
query: str,
count: int = 10,
country: str = "US",
search_lang: str = "zh-hans",
freshness: str = "",
) -> str:
"""
A web search tool based on Brave Search API.
Args:
query(string): Required. Search query.
count(number): Optional. Number of results to return. Range: 1–20. Default is 10.
country(string): Optional. Country code for region-specific results (e.g., "US", "CN").
search_lang(string): Optional. Brave language code (e.g., "zh-hans", "en", "en-gb").
freshness(string): Optional. "day", "week", "month", "year".
"""
logger.info(f"web_searcher - search_from_brave: {query}")
cfg = self.context.get_config(umo=event.unified_msg_origin)
if not cfg.get("provider_settings", {}).get("websearch_brave_key", []):
raise ValueError("Error: Brave API key is not configured in AstrBot.")
if count < 1:
count = 1
if count > 20:
count = 20
payload = {
"q": query,
"count": count,
"country": country,
"search_lang": search_lang,
}
if freshness in ["day", "week", "month", "year"]:
payload["freshness"] = freshness
results = await self._web_search_brave(cfg, payload)
if not results:
return "Error: Brave web searcher does not return any results."
ret_ls = []
ref_uuid = str(uuid.uuid4())[:4]
for idx, result in enumerate(results, 1):
index = f"{ref_uuid}.{idx}"
ret_ls.append(
{
"title": f"{result.title}",
"url": f"{result.url}",
"snippet": f"{result.snippet}",
"index": index,
}
)
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
return ret
@filter.on_llm_request(priority=-10000)
async def edit_web_search_tools(
self,
@@ -575,6 +688,7 @@ class Main(star.Star):
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
tool_set.remove_tool("web_search_brave")
elif provider == "tavily":
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
@@ -586,6 +700,7 @@ class Main(star.Star):
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
tool_set.remove_tool("web_search_brave")
elif provider == "baidu_ai_search":
try:
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
@@ -597,6 +712,7 @@ class Main(star.Star):
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("web_search_bocha")
tool_set.remove_tool("web_search_brave")
except Exception as e:
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
elif provider == "bocha":
@@ -608,3 +724,14 @@ class Main(star.Star):
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("web_search_brave")
elif provider == "brave":
web_search_brave = func_tool_mgr.get_func("web_search_brave")
if web_search_brave and web_search_brave.active:
tool_set.add_tool(web_search_brave)
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("web_search_bocha")
+2 -1
View File
@@ -59,7 +59,8 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
platform_name = run_context.context.event.get_platform_name()
if (
platform_name == "webchat"
and tool.name in ["web_search_tavily", "web_search_bocha"]
and tool.name
in ["web_search_tavily", "web_search_bocha", "web_search_brave"]
and len(run_context.messages) > 0
and tool_result
and len(tool_result.content)
+18 -1
View File
@@ -109,6 +109,7 @@ DEFAULT_CONFIG = {
"websearch_provider": "default",
"websearch_tavily_key": [],
"websearch_bocha_key": [],
"websearch_brave_key": [],
"websearch_baidu_app_builder_key": "",
"web_search_link": False,
"display_reasoning_text": False,
@@ -3173,7 +3174,13 @@ CONFIG_METADATA_3 = {
"provider_settings.websearch_provider": {
"description": "网页搜索提供商",
"type": "string",
"options": ["default", "tavily", "baidu_ai_search", "bocha"],
"options": [
"default",
"tavily",
"baidu_ai_search",
"bocha",
"brave",
],
"condition": {
"provider_settings.web_search": True,
},
@@ -3198,6 +3205,16 @@ CONFIG_METADATA_3 = {
"provider_settings.web_search": True,
},
},
"provider_settings.websearch_brave_key": {
"description": "Brave Search API Key",
"type": "list",
"items": {"type": "string"},
"hint": "可添加多个 Key 进行轮询。",
"condition": {
"provider_settings.websearch_provider": "brave",
"provider_settings.web_search": True,
},
},
"provider_settings.websearch_baidu_app_builder_key": {
"description": "百度千帆智能云 APP Builder API Key",
"type": "string",
+1 -1
View File
@@ -224,7 +224,7 @@ class ChatRoute(Route):
Returns:
包含 used 列表的字典,记录被引用的搜索结果
"""
supported = ["web_search_tavily", "web_search_bocha"]
supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"]
# 从 accumulated_parts 中找到所有 web_search_tavily 的工具调用结果
web_search_results = {}
tool_call_parts = [
+1 -1
View File
@@ -198,7 +198,7 @@ class LiveChatRoute(Route):
self, accumulated_text: str, accumulated_parts: list
) -> dict:
"""从消息中提取 web_search 引用。"""
supported = ["web_search_tavily", "web_search_bocha"]
supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"]
web_search_results = {}
tool_call_parts = [
p
@@ -302,8 +302,11 @@ export default {
}
part.tool_calls.forEach(toolCall => {
// 检查是否是 web_search_tavily 工具调用
if (toolCall.name !== 'web_search_tavily' || !toolCall.result) {
// 检查是否是支持引用解析的 web_search 工具调用
if (
!['web_search_tavily', 'web_search_bocha', 'web_search_brave'].includes(toolCall.name) ||
!toolCall.result
) {
return;
}
@@ -121,6 +121,10 @@
"description": "BoCha API Key",
"hint": "Multiple keys can be added for rotation."
},
"websearch_brave_key": {
"description": "Brave Search API Key",
"hint": "Multiple keys can be added for rotation."
},
"websearch_baidu_app_builder_key": {
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
"hint": "Reference: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"
@@ -121,6 +121,10 @@
"description": "API-ключ BoCha",
"hint": "Можно добавить несколько ключей для ротации."
},
"websearch_brave_key": {
"description": "API-ключ Brave Search",
"hint": "Можно добавить несколько ключей для ротации."
},
"websearch_baidu_app_builder_key": {
"description": "API-ключ Baidu Qianfan APP Builder",
"hint": "Ссылка: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"
@@ -123,6 +123,10 @@
"description": "BoCha API Key",
"hint": "可添加多个 Key 进行轮询。"
},
"websearch_brave_key": {
"description": "Brave Search API Key",
"hint": "可添加多个 Key 进行轮询。"
},
"websearch_baidu_app_builder_key": {
"description": "百度千帆智能云 APP Builder API Key",
"hint": "参考:[https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"
+14 -1
View File
@@ -60,6 +60,8 @@ The default AstrBot configuration is as follows:
"web_search": False,
"websearch_provider": "default",
"websearch_tavily_key": [],
"websearch_bocha_key": [],
"websearch_brave_key": [],
"web_search_link": False,
"display_reasoning_text": False,
"identifier": False,
@@ -286,16 +288,27 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`.
#### `provider_settings.websearch_provider`
Web search provider type. Default is `default`. Currently supports `default` and `tavily`.
Web search provider type. Default is `default`. Currently supports `default`, `tavily`, `bocha`, `baidu_ai_search`, and `brave`.
- `default`: Works best when Google is accessible. If Google fails, it tries Bing and Sogou in order.
- `tavily`: Uses the Tavily search engine.
- `bocha`: Uses the BoCha search engine.
- `baidu_ai_search`: Uses Baidu AI Search (MCP).
- `brave`: Uses Brave Search API.
#### `provider_settings.websearch_tavily_key`
API Key list for the Tavily search engine. Required when using `tavily` as the web search provider.
#### `provider_settings.websearch_bocha_key`
API Key list for the BoCha search engine. Required when using `bocha` as the web search provider.
#### `provider_settings.websearch_brave_key`
API Key list for the Brave search engine. Required when using `brave` as the web search provider.
#### `provider_settings.web_search_link`
Whether to prompt the model to include links to search results in the reply. Default is `false`.
+14 -1
View File
@@ -60,6 +60,8 @@ AstrBot 默认配置如下:
"web_search": False,
"websearch_provider": "default",
"websearch_tavily_key": [],
"websearch_bocha_key": [],
"websearch_brave_key": [],
"web_search_link": False,
"display_reasoning_text": False,
"identifier": False,
@@ -286,16 +288,27 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。
#### `provider_settings.websearch_provider`
网页搜索提供商类型。默认为 `default`。目前支持 `default` 和 `tavily`。
网页搜索提供商类型。默认为 `default`。目前支持 `default`、`tavily`、`bocha`、`baidu_ai_search`、`brave`。
- `default`:能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。
- `tavily`:使用 Tavily 搜索引擎。
- `bocha`:使用 BoCha 搜索引擎。
- `baidu_ai_search`:使用百度 AI Search(MCP)。
- `brave`:使用 Brave Search API。
#### `provider_settings.websearch_tavily_key`
Tavily 搜索引擎的 API Key 列表。使用 `tavily` 作为网页搜索提供商时需要填写。
#### `provider_settings.websearch_bocha_key`
BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供商时需要填写。
#### `provider_settings.websearch_brave_key`
Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。
#### `provider_settings.web_search_link`
是否在回复中提示模型附上搜索结果的链接。默认为 `false`。