1313from astrbot .core .astr_agent_context import AstrAgentContext
1414from astrbot .core .tools .registry import builtin_tool
1515
16+
17+ class WebSearchError (RuntimeError ):
18+ """Raised when a web search provider request fails."""
19+
20+
1621WEB_SEARCH_TOOL_NAMES = [
1722 "web_search_baidu" ,
1823 "web_search_tavily" ,
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 ,
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
8196def 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
375450class 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" ,
0 commit comments