Skip to content

Commit c517cdb

Browse files
daniel5uSoulter
andauthored
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>
1 parent ef15955 commit c517cdb

File tree

11 files changed

+194
-8
lines changed

11 files changed

+194
-8
lines changed

astrbot/builtin_stars/web_searcher/main.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class Main(star.Star):
2424
"web_search_tavily",
2525
"tavily_extract_web_page",
2626
"web_search_bocha",
27+
"web_search_brave",
2728
]
2829

2930
def __init__(self, context: star.Context) -> None:
@@ -33,6 +34,8 @@ def __init__(self, context: star.Context) -> None:
3334

3435
self.bocha_key_index = 0
3536
self.bocha_key_lock = asyncio.Lock()
37+
self.brave_key_index = 0
38+
self.brave_key_lock = asyncio.Lock()
3639

3740
# 将 str 类型的 key 迁移至 list[str],并保存
3841
cfg = self.context.get_config()
@@ -57,6 +60,14 @@ def __init__(self, context: star.Context) -> None:
5760
provider_settings["websearch_bocha_key"] = []
5861
cfg.save_config()
5962

63+
brave_key = provider_settings.get("websearch_brave_key")
64+
if isinstance(brave_key, str):
65+
if brave_key:
66+
provider_settings["websearch_brave_key"] = [brave_key]
67+
else:
68+
provider_settings["websearch_brave_key"] = []
69+
cfg.save_config()
70+
6071
self.bing_search = Bing()
6172
self.sogo_search = Sogo()
6273
self.baidu_initialized = False
@@ -430,6 +441,50 @@ async def _web_search_bocha(
430441
results.append(result)
431442
return results
432443

444+
async def _get_brave_key(self, cfg: AstrBotConfig) -> str:
445+
"""并发安全的从列表中获取并轮换 Brave API 密钥。"""
446+
brave_keys = cfg.get("provider_settings", {}).get("websearch_brave_key", [])
447+
448+
async with self.brave_key_lock:
449+
key = brave_keys[self.brave_key_index]
450+
self.brave_key_index = (self.brave_key_index + 1) % len(brave_keys)
451+
return key
452+
453+
async def _web_search_brave(
454+
self,
455+
cfg: AstrBotConfig,
456+
payload: dict,
457+
) -> list[SearchResult]:
458+
"""使用 Brave 搜索引擎进行搜索"""
459+
brave_key = await self._get_brave_key(cfg)
460+
url = "https://api.search.brave.com/res/v1/web/search"
461+
header = {
462+
"Accept": "application/json",
463+
"X-Subscription-Token": brave_key,
464+
}
465+
async with aiohttp.ClientSession(trust_env=True) as session:
466+
async with session.get(
467+
url,
468+
params=payload,
469+
headers=header,
470+
) as response:
471+
if response.status != 200:
472+
reason = await response.text()
473+
raise Exception(
474+
f"Brave web search failed: {reason}, status: {response.status}",
475+
)
476+
data = await response.json()
477+
rows = data.get("web", {}).get("results", [])
478+
results = []
479+
for item in rows:
480+
result = SearchResult(
481+
title=item.get("title", ""),
482+
url=item.get("url", ""),
483+
snippet=item.get("description", ""),
484+
)
485+
results.append(result)
486+
return results
487+
433488
@llm_tool("web_search_bocha")
434489
async def search_from_bocha(
435490
self,
@@ -537,6 +592,64 @@ async def search_from_bocha(
537592
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
538593
return ret
539594

595+
@llm_tool("web_search_brave")
596+
async def search_from_brave(
597+
self,
598+
event: AstrMessageEvent,
599+
query: str,
600+
count: int = 10,
601+
country: str = "US",
602+
search_lang: str = "zh-hans",
603+
freshness: str = "",
604+
) -> str:
605+
"""
606+
A web search tool based on Brave Search API.
607+
608+
Args:
609+
query(string): Required. Search query.
610+
count(number): Optional. Number of results to return. Range: 1–20. Default is 10.
611+
country(string): Optional. Country code for region-specific results (e.g., "US", "CN").
612+
search_lang(string): Optional. Brave language code (e.g., "zh-hans", "en", "en-gb").
613+
freshness(string): Optional. "day", "week", "month", "year".
614+
"""
615+
logger.info(f"web_searcher - search_from_brave: {query}")
616+
cfg = self.context.get_config(umo=event.unified_msg_origin)
617+
if not cfg.get("provider_settings", {}).get("websearch_brave_key", []):
618+
raise ValueError("Error: Brave API key is not configured in AstrBot.")
619+
620+
if count < 1:
621+
count = 1
622+
if count > 20:
623+
count = 20
624+
625+
payload = {
626+
"q": query,
627+
"count": count,
628+
"country": country,
629+
"search_lang": search_lang,
630+
}
631+
if freshness in ["day", "week", "month", "year"]:
632+
payload["freshness"] = freshness
633+
634+
results = await self._web_search_brave(cfg, payload)
635+
if not results:
636+
return "Error: Brave web searcher does not return any results."
637+
638+
ret_ls = []
639+
ref_uuid = str(uuid.uuid4())[:4]
640+
for idx, result in enumerate(results, 1):
641+
index = f"{ref_uuid}.{idx}"
642+
ret_ls.append(
643+
{
644+
"title": f"{result.title}",
645+
"url": f"{result.url}",
646+
"snippet": f"{result.snippet}",
647+
"index": index,
648+
}
649+
)
650+
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
651+
return ret
652+
540653
@filter.on_llm_request(priority=-10000)
541654
async def edit_web_search_tools(
542655
self,
@@ -575,6 +688,7 @@ async def edit_web_search_tools(
575688
tool_set.remove_tool("tavily_extract_web_page")
576689
tool_set.remove_tool("AIsearch")
577690
tool_set.remove_tool("web_search_bocha")
691+
tool_set.remove_tool("web_search_brave")
578692
elif provider == "tavily":
579693
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
580694
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
@@ -586,6 +700,7 @@ async def edit_web_search_tools(
586700
tool_set.remove_tool("fetch_url")
587701
tool_set.remove_tool("AIsearch")
588702
tool_set.remove_tool("web_search_bocha")
703+
tool_set.remove_tool("web_search_brave")
589704
elif provider == "baidu_ai_search":
590705
try:
591706
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
@@ -597,6 +712,7 @@ async def edit_web_search_tools(
597712
tool_set.remove_tool("web_search_tavily")
598713
tool_set.remove_tool("tavily_extract_web_page")
599714
tool_set.remove_tool("web_search_bocha")
715+
tool_set.remove_tool("web_search_brave")
600716
except Exception as e:
601717
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
602718
elif provider == "bocha":
@@ -608,3 +724,14 @@ async def edit_web_search_tools(
608724
tool_set.remove_tool("AIsearch")
609725
tool_set.remove_tool("web_search_tavily")
610726
tool_set.remove_tool("tavily_extract_web_page")
727+
tool_set.remove_tool("web_search_brave")
728+
elif provider == "brave":
729+
web_search_brave = func_tool_mgr.get_func("web_search_brave")
730+
if web_search_brave and web_search_brave.active:
731+
tool_set.add_tool(web_search_brave)
732+
tool_set.remove_tool("web_search")
733+
tool_set.remove_tool("fetch_url")
734+
tool_set.remove_tool("AIsearch")
735+
tool_set.remove_tool("web_search_tavily")
736+
tool_set.remove_tool("tavily_extract_web_page")
737+
tool_set.remove_tool("web_search_bocha")

astrbot/core/astr_agent_hooks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ async def on_tool_end(
5959
platform_name = run_context.context.event.get_platform_name()
6060
if (
6161
platform_name == "webchat"
62-
and tool.name in ["web_search_tavily", "web_search_bocha"]
62+
and tool.name
63+
in ["web_search_tavily", "web_search_bocha", "web_search_brave"]
6364
and len(run_context.messages) > 0
6465
and tool_result
6566
and len(tool_result.content)

astrbot/core/config/default.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"websearch_provider": "default",
110110
"websearch_tavily_key": [],
111111
"websearch_bocha_key": [],
112+
"websearch_brave_key": [],
112113
"websearch_baidu_app_builder_key": "",
113114
"web_search_link": False,
114115
"display_reasoning_text": False,
@@ -3173,7 +3174,13 @@ class ChatProviderTemplate(TypedDict):
31733174
"provider_settings.websearch_provider": {
31743175
"description": "网页搜索提供商",
31753176
"type": "string",
3176-
"options": ["default", "tavily", "baidu_ai_search", "bocha"],
3177+
"options": [
3178+
"default",
3179+
"tavily",
3180+
"baidu_ai_search",
3181+
"bocha",
3182+
"brave",
3183+
],
31773184
"condition": {
31783185
"provider_settings.web_search": True,
31793186
},
@@ -3198,6 +3205,16 @@ class ChatProviderTemplate(TypedDict):
31983205
"provider_settings.web_search": True,
31993206
},
32003207
},
3208+
"provider_settings.websearch_brave_key": {
3209+
"description": "Brave Search API Key",
3210+
"type": "list",
3211+
"items": {"type": "string"},
3212+
"hint": "可添加多个 Key 进行轮询。",
3213+
"condition": {
3214+
"provider_settings.websearch_provider": "brave",
3215+
"provider_settings.web_search": True,
3216+
},
3217+
},
32013218
"provider_settings.websearch_baidu_app_builder_key": {
32023219
"description": "百度千帆智能云 APP Builder API Key",
32033220
"type": "string",

astrbot/dashboard/routes/chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def _extract_web_search_refs(
224224
Returns:
225225
包含 used 列表的字典,记录被引用的搜索结果
226226
"""
227-
supported = ["web_search_tavily", "web_search_bocha"]
227+
supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"]
228228
# 从 accumulated_parts 中找到所有 web_search_tavily 的工具调用结果
229229
web_search_results = {}
230230
tool_call_parts = [

astrbot/dashboard/routes/live_chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _extract_web_search_refs(
198198
self, accumulated_text: str, accumulated_parts: list
199199
) -> dict:
200200
"""从消息中提取 web_search 引用。"""
201-
supported = ["web_search_tavily", "web_search_bocha"]
201+
supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"]
202202
web_search_results = {}
203203
tool_call_parts = [
204204
p

dashboard/src/components/chat/MessageList.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,11 @@ export default {
302302
}
303303
304304
part.tool_calls.forEach(toolCall => {
305-
// 检查是否是 web_search_tavily 工具调用
306-
if (toolCall.name !== 'web_search_tavily' || !toolCall.result) {
305+
// 检查是否是支持引用解析的 web_search 工具调用
306+
if (
307+
!['web_search_tavily', 'web_search_bocha', 'web_search_brave'].includes(toolCall.name) ||
308+
!toolCall.result
309+
) {
307310
return;
308311
}
309312

dashboard/src/i18n/locales/en-US/features/config-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
"description": "BoCha API Key",
122122
"hint": "Multiple keys can be added for rotation."
123123
},
124+
"websearch_brave_key": {
125+
"description": "Brave Search API Key",
126+
"hint": "Multiple keys can be added for rotation."
127+
},
124128
"websearch_baidu_app_builder_key": {
125129
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
126130
"hint": "Reference: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"

dashboard/src/i18n/locales/ru-RU/features/config-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
"description": "API-ключ BoCha",
122122
"hint": "Можно добавить несколько ключей для ротации."
123123
},
124+
"websearch_brave_key": {
125+
"description": "API-ключ Brave Search",
126+
"hint": "Можно добавить несколько ключей для ротации."
127+
},
124128
"websearch_baidu_app_builder_key": {
125129
"description": "API-ключ Baidu Qianfan APP Builder",
126130
"hint": "Ссылка: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"

dashboard/src/i18n/locales/zh-CN/features/config-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@
123123
"description": "BoCha API Key",
124124
"hint": "可添加多个 Key 进行轮询。"
125125
},
126+
"websearch_brave_key": {
127+
"description": "Brave Search API Key",
128+
"hint": "可添加多个 Key 进行轮询。"
129+
},
126130
"websearch_baidu_app_builder_key": {
127131
"description": "百度千帆智能云 APP Builder API Key",
128132
"hint": "参考:[https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"

docs/en/dev/astrbot-config.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ The default AstrBot configuration is as follows:
6060
"web_search": False,
6161
"websearch_provider": "default",
6262
"websearch_tavily_key": [],
63+
"websearch_bocha_key": [],
64+
"websearch_brave_key": [],
6365
"web_search_link": False,
6466
"display_reasoning_text": False,
6567
"identifier": False,
@@ -286,16 +288,27 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`.
286288

287289
#### `provider_settings.websearch_provider`
288290

289-
Web search provider type. Default is `default`. Currently supports `default` and `tavily`.
291+
Web search provider type. Default is `default`. Currently supports `default`, `tavily`, `bocha`, `baidu_ai_search`, and `brave`.
290292

291293
- `default`: Works best when Google is accessible. If Google fails, it tries Bing and Sogou in order.
292294

293295
- `tavily`: Uses the Tavily search engine.
296+
- `bocha`: Uses the BoCha search engine.
297+
- `baidu_ai_search`: Uses Baidu AI Search (MCP).
298+
- `brave`: Uses Brave Search API.
294299

295300
#### `provider_settings.websearch_tavily_key`
296301

297302
API Key list for the Tavily search engine. Required when using `tavily` as the web search provider.
298303

304+
#### `provider_settings.websearch_bocha_key`
305+
306+
API Key list for the BoCha search engine. Required when using `bocha` as the web search provider.
307+
308+
#### `provider_settings.websearch_brave_key`
309+
310+
API Key list for the Brave search engine. Required when using `brave` as the web search provider.
311+
299312
#### `provider_settings.web_search_link`
300313

301314
Whether to prompt the model to include links to search results in the reply. Default is `false`.

0 commit comments

Comments
 (0)