Skip to content

Commit 4d0f7b5

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 4d0f7b5

10 files changed

Lines changed: 558 additions & 3 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/pipeline/stage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def initialize(self, ctx: PipelineContext) -> None:
3333
async def process(
3434
self,
3535
event: AstrMessageEvent,
36-
) -> None | AsyncGenerator[None, None]:
36+
) -> None | AsyncGenerator[None]:
3737
"""处理事件
3838
3939
Args:

astrbot/core/tools/web_search_tools.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"web_search_brave",
2222
"web_search_firecrawl",
2323
"firecrawl_extract_web_page",
24+
"web_search_metaso",
2425
]
2526
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
2627
"provider_settings.web_search": True,
@@ -42,6 +43,10 @@
4243
"provider_settings.web_search": True,
4344
"provider_settings.websearch_provider": "baidu_ai_search",
4445
}
46+
_METASO_WEB_SEARCH_TOOL_CONFIG = {
47+
"provider_settings.web_search": True,
48+
"provider_settings.websearch_provider": "metaso",
49+
}
4550

4651

4752
@std_dataclass
@@ -76,6 +81,11 @@ async def get(self, provider_settings: dict) -> str:
7681
_BOCHA_KEY_ROTATOR = _KeyRotator("websearch_bocha_key", "BoCha")
7782
_BRAVE_KEY_ROTATOR = _KeyRotator("websearch_brave_key", "Brave")
7883
_FIRECRAWL_KEY_ROTATOR = _KeyRotator("websearch_firecrawl_key", "Firecrawl")
84+
_METASO_KEY_ROTATOR = _KeyRotator("websearch_metaso_key", "Metaso")
85+
_METASO_DEFAULT_API_KEY = "mk-E384C1DD5E8501BB7EFE27C949AFDE5B"
86+
# The above default API key is intentionally public. It is the official Metaso
87+
# free-tier key provided by Metaso for evaluation and low-volume use (100 queries/day).
88+
# Configure your own key via websearch_metaso_key for higher quotas.
7989

8090

8191
def normalize_legacy_web_search_config(cfg) -> None:
@@ -99,6 +109,7 @@ def normalize_legacy_web_search_config(cfg) -> None:
99109
"websearch_bocha_key",
100110
"websearch_brave_key",
101111
"websearch_firecrawl_key",
112+
"websearch_metaso_key",
102113
):
103114
value = provider_settings.get(setting_name)
104115
if isinstance(value, str):
@@ -370,6 +381,63 @@ async def _baidu_search(
370381
]
371382

372383

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

805873

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