Skip to content

Commit 91d68a3

Browse files
author
Zhao Xiaohong
committed
feat: add Metaso web search provider
Add MetasoWebSearchTool as a new web search backend using the Metaso Search API (https://metaso.cn/api/v1/search). Includes a built-in free-tier key (100 queries/day) and supports custom API key rotation via websearch_metaso_key config. - Add Metaso provider config and i18n metadata (en/ru/zh) - Register MetasoWebSearchTool in astr_main_agent - Map Metaso API error codes (401/403/429, code 3003/2005) to descriptive exceptions - Add 17 unit tests covering HTTP errors, API codes, key selection, size clamping, and empty results
1 parent 7a9fb33 commit 91d68a3

9 files changed

Lines changed: 564 additions & 2 deletions

File tree

astrbot/core/astr_main_agent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
BraveWebSearchTool,
8888
FirecrawlExtractWebPageTool,
8989
FirecrawlWebSearchTool,
90+
MetasoWebSearchTool,
9091
TavilyExtractWebPageTool,
9192
TavilyWebSearchTool,
9293
normalize_legacy_web_search_config,
@@ -1116,6 +1117,8 @@ async def _apply_web_search_tools(
11161117
req.func_tool.add_tool(tool_mgr.get_builtin_tool(FirecrawlExtractWebPageTool))
11171118
elif provider == "baidu_ai_search":
11181119
req.func_tool.add_tool(tool_mgr.get_builtin_tool(BaiduWebSearchTool))
1120+
elif provider == "metaso":
1121+
req.func_tool.add_tool(tool_mgr.get_builtin_tool(MetasoWebSearchTool))
11191122

11201123

11211124
def _get_compress_provider(

astrbot/core/computer/booters/shipyard_search_file_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _build_grep_command(
7474

7575

7676
def _quote_command(command: list[str]) -> str:
77-
return " ".join(shlex.quote(part) for part in command)
77+
return shlex.join(command)
7878

7979

8080
def build_search_command(

astrbot/core/config/default.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"websearch_brave_key": [],
113113
"websearch_baidu_app_builder_key": "",
114114
"websearch_firecrawl_key": [],
115+
"websearch_metaso_key": [],
115116
"web_search_link": False,
116117
"display_reasoning_text": False,
117118
"identifier": False,
@@ -3223,6 +3224,7 @@
32233224
"bocha",
32243225
"brave",
32253226
"firecrawl",
3227+
"metaso",
32263228
],
32273229
"condition": {
32283230
"provider_settings.web_search": True,
@@ -3268,6 +3270,16 @@
32683270
"provider_settings.web_search": True,
32693271
},
32703272
},
3273+
"provider_settings.websearch_metaso_key": {
3274+
"description": "Metaso API Key",
3275+
"type": "list",
3276+
"items": {"type": "string"},
3277+
"hint": "可添加多个 Key 进行轮询。内置 Key 每天有 100 次免费查询额度,配置自己的 Key 可获得更高配额。",
3278+
"condition": {
3279+
"provider_settings.websearch_provider": "metaso",
3280+
"provider_settings.web_search": True,
3281+
},
3282+
},
32713283
"provider_settings.websearch_baidu_app_builder_key": {
32723284
"description": "百度千帆智能云 APP Builder API Key",
32733285
"type": "string",

astrbot/core/tools/web_search_tools.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
from astrbot.core.astr_agent_context import AstrAgentContext
1414
from astrbot.core.tools.registry import builtin_tool
1515

16+
17+
class WebSearchError(RuntimeError):
18+
"""Raised when a web search provider request fails."""
19+
20+
1621
WEB_SEARCH_TOOL_NAMES = [
1722
"web_search_baidu",
1823
"web_search_tavily",
@@ -21,6 +26,7 @@
2126
"web_search_brave",
2227
"web_search_firecrawl",
2328
"firecrawl_extract_web_page",
29+
"web_search_metaso",
2430
]
2531
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
2632
"provider_settings.web_search": True,
@@ -42,6 +48,10 @@
4248
"provider_settings.web_search": True,
4349
"provider_settings.websearch_provider": "baidu_ai_search",
4450
}
51+
_METASO_WEB_SEARCH_TOOL_CONFIG = {
52+
"provider_settings.web_search": True,
53+
"provider_settings.websearch_provider": "metaso",
54+
}
4555

4656

4757
@std_dataclass
@@ -76,6 +86,11 @@ async def get(self, provider_settings: dict) -> str:
7686
_BOCHA_KEY_ROTATOR = _KeyRotator("websearch_bocha_key", "BoCha")
7787
_BRAVE_KEY_ROTATOR = _KeyRotator("websearch_brave_key", "Brave")
7888
_FIRECRAWL_KEY_ROTATOR = _KeyRotator("websearch_firecrawl_key", "Firecrawl")
89+
_METASO_KEY_ROTATOR = _KeyRotator("websearch_metaso_key", "Metaso")
90+
_METASO_DEFAULT_API_KEY = "mk-E384C1DD5E8501BB7EFE27C949AFDE5B"
91+
# The above default API key is intentionally public. It is the official Metaso
92+
# free-tier key provided by Metaso for evaluation and low-volume use (100 queries/day).
93+
# Configure your own key via websearch_metaso_key for higher quotas.
7994

8095

8196
def normalize_legacy_web_search_config(cfg) -> None:
@@ -99,6 +114,7 @@ def normalize_legacy_web_search_config(cfg) -> None:
99114
"websearch_bocha_key",
100115
"websearch_brave_key",
101116
"websearch_firecrawl_key",
117+
"websearch_metaso_key",
102118
):
103119
value = provider_settings.get(setting_name)
104120
if isinstance(value, str):
@@ -370,6 +386,65 @@ async def _baidu_search(
370386
]
371387

372388

389+
async def _metaso_search(
390+
provider_settings: dict,
391+
payload: dict,
392+
) -> list[SearchResult]:
393+
keys = provider_settings.get("websearch_metaso_key", [])
394+
metaso_key = (
395+
await _METASO_KEY_ROTATOR.get(provider_settings)
396+
if keys
397+
else _METASO_DEFAULT_API_KEY
398+
)
399+
headers = {
400+
"Authorization": f"Bearer {metaso_key}",
401+
"Content-Type": "application/json",
402+
}
403+
async with aiohttp.ClientSession(trust_env=True) as session:
404+
async with session.post(
405+
"https://metaso.cn/api/v1/search",
406+
json=payload,
407+
headers=headers,
408+
) as response:
409+
if response.status in (401, 403):
410+
raise WebSearchError(
411+
"Metaso search failed: unauthorized. Check your Metaso API key."
412+
)
413+
if response.status == 429:
414+
raise WebSearchError(
415+
"Metaso search failed: rate-limited. Try again later."
416+
)
417+
if response.status != 200:
418+
reason = await response.text()
419+
raise WebSearchError(
420+
f"Metaso search failed: {reason}, status: {response.status}",
421+
)
422+
data = await response.json()
423+
code = data.get("code", 0)
424+
if code == 3003:
425+
raise WebSearchError(
426+
"Metaso search failed: daily search limit reached. "
427+
"See: https://metaso.cn/search-api/playground"
428+
)
429+
if code == 2005:
430+
raise WebSearchError(
431+
"Metaso search failed: API key rejected. Check your Metaso API key."
432+
)
433+
if code != 0:
434+
raise WebSearchError(
435+
f"Metaso search failed: code={code}, message={data.get('message', '')}",
436+
)
437+
webpages = data.get("webpages", [])
438+
return [
439+
SearchResult(
440+
title=item.get("title", ""),
441+
url=item.get("link", ""),
442+
snippet=item.get("snippet") or item.get("summary") or "",
443+
)
444+
for item in webpages
445+
]
446+
447+
373448
@builtin_tool(config=_TAVILY_WEB_SEARCH_TOOL_CONFIG)
374449
@pydantic_dataclass
375450
class TavilyWebSearchTool(FunctionTool[AstrAgentContext]):
@@ -803,10 +878,56 @@ async def call(self, context, **kwargs) -> ToolExecResult:
803878
return _search_result_payload(results)
804879

805880

881+
@builtin_tool(config=_METASO_WEB_SEARCH_TOOL_CONFIG)
882+
@pydantic_dataclass
883+
class MetasoWebSearchTool(FunctionTool[AstrAgentContext]):
884+
name: str = "web_search_metaso"
885+
description: str = (
886+
"A web search tool based on Metaso Search API, used to retrieve web pages "
887+
"related to the user's query. Metaso provides 100 free queries per day by "
888+
"default. Configure your own API key (websearch_metaso_key) for higher quotas."
889+
)
890+
parameters: dict = Field(
891+
default_factory=lambda: {
892+
"type": "object",
893+
"properties": {
894+
"query": {"type": "string", "description": "Required. Search query."},
895+
"size": {
896+
"type": "integer",
897+
"description": "Optional. Number of search results to return. Range: 1-100. Default is 10.",
898+
},
899+
},
900+
"required": ["query"],
901+
}
902+
)
903+
904+
async def call(self, context, **kwargs) -> ToolExecResult:
905+
_, provider_settings, _ = _get_runtime(context)
906+
size = int(kwargs.get("size", 10))
907+
if size < 1:
908+
size = 1
909+
if size > 100:
910+
size = 100
911+
912+
payload = {
913+
"q": kwargs["query"],
914+
"scope": "webpage",
915+
"size": size,
916+
}
917+
918+
results = await _metaso_search(provider_settings, payload)
919+
if not results:
920+
return "Error: Metaso searcher did not return any results."
921+
return _search_result_payload(results)
922+
923+
806924
__all__ = [
807925
"BaiduWebSearchTool",
808926
"BochaWebSearchTool",
809927
"BraveWebSearchTool",
928+
"FirecrawlExtractWebPageTool",
929+
"FirecrawlWebSearchTool",
930+
"MetasoWebSearchTool",
810931
"TavilyExtractWebPageTool",
811932
"TavilyWebSearchTool",
812933
"WEB_SEARCH_TOOL_NAMES",

astrbot/utils/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@
129129
"description": "Firecrawl API Key",
130130
"hint": "Multiple keys can be added for rotation."
131131
},
132+
"websearch_metaso_key": {
133+
"description": "Metaso API Key",
134+
"hint": "Multiple keys can be added for rotation. Built-in key has 100 free queries/day; configure your own for higher quotas."
135+
},
132136
"websearch_baidu_app_builder_key": {
133137
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
134138
"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
@@ -129,6 +129,10 @@
129129
"description": "API-ключ Firecrawl",
130130
"hint": "Можно добавить несколько ключей для ротации."
131131
},
132+
"websearch_metaso_key": {
133+
"description": "API-ключ Metaso",
134+
"hint": "Можно добавить несколько ключей для ротации. Встроенный ключ даёт 100 бесплатных запросов/день; укажите свой для более высоких квот."
135+
},
132136
"websearch_baidu_app_builder_key": {
133137
"description": "API-ключ Baidu Qianfan APP Builder",
134138
"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
@@ -131,6 +131,10 @@
131131
"description": "Firecrawl API Key",
132132
"hint": "可添加多个 Key 进行轮询。"
133133
},
134+
"websearch_metaso_key": {
135+
"description": "Metaso API Key",
136+
"hint": "可添加多个 Key 进行轮询。内置 Key 每天 100 次免费查询,配置自己的 Key 可提升配额。"
137+
},
134138
"websearch_baidu_app_builder_key": {
135139
"description": "百度千帆智能云 APP Builder API Key",
136140
"hint": "参考:[https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"

0 commit comments

Comments
 (0)