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 ,
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
8191def 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
375443class 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" ,
0 commit comments