mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
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:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`。
|
||||
|
||||
Reference in New Issue
Block a user