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
127 changes: 127 additions & 0 deletions astrbot/builtin_stars/web_searcher/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -33,6 +34,8 @@ def __init__(self, context: star.Context) -> None:

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()
Expand All @@ -57,6 +60,14 @@ def __init__(self, context: star.Context) -> None:
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
Expand Down Expand Up @@ -430,6 +441,50 @@ async def _web_search_bocha(
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,
Expand Down Expand Up @@ -537,6 +592,64 @@ async def search_from_bocha(
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.")
Comment on lines +617 to +618
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid duplicated configuration checks and inconsistent error messages for missing Brave key.

This case is handled both here and in _get_brave_key, with different error messages. Please choose a single place to perform the validation and define the message—either centralize it in _get_brave_key and let search_from_brave rely on that, or have _get_brave_key assume a non-empty list and keep the check only here. A single validation path will keep behavior consistent and easier to maintain.

Suggested implementation:

        logger.info(f"web_searcher - search_from_brave: {query}")

        if count < 1:

These changes assume that _get_brave_key:

  1. Is the single source of truth for accessing the Brave API key.
  2. Performs its own validation and raises a clear, consistent error if the key is missing or invalid.

If _get_brave_key does not yet validate the key or has an undesired error message, you should:

  • Add the non-empty-key validation there.
  • Standardize the error message in _get_brave_key to whatever you want to expose to callers like search_from_brave.

Comment on lines +617 to +618
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This check for the Brave API key is redundant. The _get_brave_key method, which is called on line 636, already performs this check and raises a ValueError if no keys are configured. You can remove these lines to avoid duplicate logic.

Additionally, the error message here is in English, while the one in _get_brave_key is in Chinese, which is inconsistent. Centralizing the check will resolve this.


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,
Expand Down Expand Up @@ -575,6 +688,7 @@ async def edit_web_search_tools(
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")
Expand All @@ -586,6 +700,7 @@ async def edit_web_search_tools(
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)
Expand All @@ -597,6 +712,7 @@ async def edit_web_search_tools(
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":
Expand All @@ -608,3 +724,14 @@ async def edit_web_search_tools(
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")
3 changes: 2 additions & 1 deletion astrbot/core/astr_agent_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ async def on_tool_end(
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)
Expand Down
19 changes: 18 additions & 1 deletion astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"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,
Expand Down Expand Up @@ -3080,7 +3081,13 @@ class ChatProviderTemplate(TypedDict):
"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,
},
Expand All @@ -3105,6 +3112,16 @@ class ChatProviderTemplate(TypedDict):
"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",
Expand Down
2 changes: 1 addition & 1 deletion astrbot/dashboard/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def _extract_web_search_refs(
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 = [
Expand Down
2 changes: 1 addition & 1 deletion astrbot/dashboard/routes/live_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def _extract_web_search_refs(
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
Expand Down
10 changes: 4 additions & 6 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
---
# 当接入 QQ NapCat 时,请使用这个 compose 文件一键部署: https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml

services:
astrbot:
image: soulter/astrbot:latest
container_name: astrbot
restart: always
ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497
- "6185:6185" # 必选,AstrBot WebUI 端口
- "6199:6199" # 可选, QQ 个人号 WebSocket 端口
ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497
- 6185:6185 # 必选,AstrBot WebUI 端口
# - "6195:6195" # 可选, 企业微信 Webhook 端口
# - "6196:6196" # 可选, QQ 官方接口 Webhook 端口
environment:
- TZ=Asia/Shanghai
environment: [TZ=Asia/Shanghai]
volumes:
- ./data:/AstrBot/data
# - /etc/timezone:/etc/timezone:ro
Expand Down
7 changes: 5 additions & 2 deletions dashboard/src/components/chat/MessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
15 changes: 14 additions & 1 deletion docs/en/dev/astrbot-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`.
Expand Down
15 changes: 14 additions & 1 deletion docs/zh/dev/astrbot-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 搜索引擎。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): Possible typo: "Sogo" should likely be "Sogou" (or its Chinese name).

This likely refers to the search engine “Sogou” (搜狗). Please change “Sogo” to “Sogou” or its Chinese brand name for correctness.

Suggested change
- `default`:能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。
- `default`:能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogou 搜索引擎。


- `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`。
Expand Down
Loading