feat(web_search): 新增 Metaso 搜索引擎支持
为联网搜索工具集成了新的搜索引擎 Metaso,为用户提供更多搜索选择。 - 在搜索引擎基类 `BaseSearchEngine` 中添加了可选的 `read_url` 方法,为未来支持直接读取网页内容奠定基础。 - 更新了插件加载、工具执行逻辑和配置文件模板,以完整支持 Metaso 引擎。 - 将默认回复生成器的任务超时时间从 15 秒延长至 45 秒,以适应联网搜索等耗时较长的操作,提高稳定性。
This commit is contained in:
@@ -1303,7 +1303,7 @@ class DefaultReplyer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 设置超时
|
# 设置超时
|
||||||
timeout = 15.0 # 秒
|
timeout = 45.0 # 秒
|
||||||
|
|
||||||
async def get_task_result(task_name, task):
|
async def get_task_result(task_name, task):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Base search engine interface
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
class BaseSearchEngine(ABC):
|
class BaseSearchEngine(ABC):
|
||||||
@@ -24,6 +24,12 @@ class BaseSearchEngine(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def read_url(self, url: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
读取URL内容,如果引擎不支持则返回None
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_available(self) -> bool:
|
def is_available(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
107
src/plugins/built_in/web_search_tool/engines/metaso_engine.py
Normal file
107
src/plugins/built_in/web_search_tool/engines/metaso_engine.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
Metaso Search Engine (Chat Completions Mode)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from src.common.logger import get_logger
|
||||||
|
from src.plugin_system.apis import config_api
|
||||||
|
|
||||||
|
from ..utils.api_key_manager import create_api_key_manager_from_config
|
||||||
|
from .base import BaseSearchEngine
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetasoClient:
|
||||||
|
"""A client to interact with the Metaso API."""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.base_url = "https://metaso.cn/api/v1"
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def search(self, query: str, **kwargs) -> List[dict[str, Any]]:
|
||||||
|
"""Perform a search using the Metaso Chat Completions API."""
|
||||||
|
payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]}
|
||||||
|
search_url = f"{self.base_url}/chat/completions"
|
||||||
|
full_response_content = ""
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=90.0) as client:
|
||||||
|
try:
|
||||||
|
async with client.stream("POST", search_url, headers=self.headers, json=payload) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
async for line in response.aiter_lines():
|
||||||
|
if line.startswith("data:"):
|
||||||
|
data_str = line[len("data:") :].strip()
|
||||||
|
if data_str == "[DONE]":
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
data = json.loads(data_str)
|
||||||
|
delta = data.get("choices", [{}])[0].get("delta", {})
|
||||||
|
content_chunk = delta.get("content")
|
||||||
|
if content_chunk:
|
||||||
|
full_response_content += content_chunk
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning(f"Metaso stream: could not decode JSON line: {data_str}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not full_response_content:
|
||||||
|
logger.warning("Metaso search returned an empty stream.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"title": query,
|
||||||
|
"url": "https://metaso.cn/",
|
||||||
|
"snippet": full_response_content,
|
||||||
|
"provider": "Metaso (Chat)",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
logger.error(f"HTTP error occurred while searching with Metaso Chat: {e.response.text}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred while searching with Metaso Chat: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class MetasoSearchEngine(BaseSearchEngine):
|
||||||
|
"""Metaso Search Engine implementation."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._initialize_clients()
|
||||||
|
|
||||||
|
def _initialize_clients(self):
|
||||||
|
"""Initialize Metaso clients."""
|
||||||
|
metaso_api_keys = config_api.get_global_config("web_search.metaso_api_keys", None)
|
||||||
|
self.api_manager = create_api_key_manager_from_config(
|
||||||
|
metaso_api_keys, lambda key: MetasoClient(api_key=key), "Metaso"
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if the Metaso search engine is available."""
|
||||||
|
return self.api_manager.is_available()
|
||||||
|
|
||||||
|
async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
"""Execute a Metaso search."""
|
||||||
|
if not self.is_available():
|
||||||
|
return []
|
||||||
|
|
||||||
|
query = args["query"]
|
||||||
|
try:
|
||||||
|
metaso_client = self.api_manager.get_next_client()
|
||||||
|
if not metaso_client:
|
||||||
|
logger.error("Could not get Metaso client.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return await metaso_client.search(query)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Metaso search failed: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
@@ -22,6 +22,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
|||||||
提供网络搜索和URL解析功能,支持多种搜索引擎:
|
提供网络搜索和URL解析功能,支持多种搜索引擎:
|
||||||
- Exa (需要API密钥)
|
- Exa (需要API密钥)
|
||||||
- Tavily (需要API密钥)
|
- Tavily (需要API密钥)
|
||||||
|
- Metaso (需要API密钥)
|
||||||
- DuckDuckGo (免费)
|
- DuckDuckGo (免费)
|
||||||
- Bing (免费)
|
- Bing (免费)
|
||||||
"""
|
"""
|
||||||
@@ -43,6 +44,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
|||||||
from .engines.exa_engine import ExaSearchEngine
|
from .engines.exa_engine import ExaSearchEngine
|
||||||
from .engines.searxng_engine import SearXNGSearchEngine
|
from .engines.searxng_engine import SearXNGSearchEngine
|
||||||
from .engines.tavily_engine import TavilySearchEngine
|
from .engines.tavily_engine import TavilySearchEngine
|
||||||
|
from .engines.metaso_engine import MetasoSearchEngine
|
||||||
|
|
||||||
# 实例化所有搜索引擎,这会触发API密钥管理器的初始化
|
# 实例化所有搜索引擎,这会触发API密钥管理器的初始化
|
||||||
exa_engine = ExaSearchEngine()
|
exa_engine = ExaSearchEngine()
|
||||||
@@ -50,14 +52,16 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
|||||||
ddg_engine = DDGSearchEngine()
|
ddg_engine = DDGSearchEngine()
|
||||||
bing_engine = BingSearchEngine()
|
bing_engine = BingSearchEngine()
|
||||||
searxng_engine = SearXNGSearchEngine()
|
searxng_engine = SearXNGSearchEngine()
|
||||||
|
metaso_engine = MetasoSearchEngine()
|
||||||
# 报告每个引擎的状态
|
|
||||||
|
# 报告每个引擎的状态
|
||||||
engines_status = {
|
engines_status = {
|
||||||
"Exa": exa_engine.is_available(),
|
"Exa": exa_engine.is_available(),
|
||||||
"Tavily": tavily_engine.is_available(),
|
"Tavily": tavily_engine.is_available(),
|
||||||
"DuckDuckGo": ddg_engine.is_available(),
|
"DuckDuckGo": ddg_engine.is_available(),
|
||||||
"Bing": bing_engine.is_available(),
|
"Bing": bing_engine.is_available(),
|
||||||
"SearXNG": searxng_engine.is_available(),
|
"SearXNG": searxng_engine.is_available(),
|
||||||
|
"Metaso": metaso_engine.is_available(),
|
||||||
}
|
}
|
||||||
|
|
||||||
available_engines = [name for name, available in engines_status.items() if available]
|
available_engines = [name for name, available in engines_status.items() if available]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from ..engines.ddg_engine import DDGSearchEngine
|
|||||||
from ..engines.exa_engine import ExaSearchEngine
|
from ..engines.exa_engine import ExaSearchEngine
|
||||||
from ..engines.searxng_engine import SearXNGSearchEngine
|
from ..engines.searxng_engine import SearXNGSearchEngine
|
||||||
from ..engines.tavily_engine import TavilySearchEngine
|
from ..engines.tavily_engine import TavilySearchEngine
|
||||||
|
from ..engines.metaso_engine import MetasoSearchEngine
|
||||||
from ..utils.formatters import deduplicate_results, format_search_results
|
from ..utils.formatters import deduplicate_results, format_search_results
|
||||||
|
|
||||||
logger = get_logger("web_search_tool")
|
logger = get_logger("web_search_tool")
|
||||||
@@ -51,6 +52,7 @@ class WebSurfingTool(BaseTool):
|
|||||||
"ddg": DDGSearchEngine(),
|
"ddg": DDGSearchEngine(),
|
||||||
"bing": BingSearchEngine(),
|
"bing": BingSearchEngine(),
|
||||||
"searxng": SearXNGSearchEngine(),
|
"searxng": SearXNGSearchEngine(),
|
||||||
|
"metaso": MetasoSearchEngine(),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "7.3.1"
|
version = "7.3.2"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增version的值
|
||||||
@@ -489,11 +489,12 @@ enable_web_search_tool = true # 是否启用联网搜索tool
|
|||||||
enable_url_tool = true # 是否启用URL解析tool
|
enable_url_tool = true # 是否启用URL解析tool
|
||||||
tavily_api_keys = ["None"]# Tavily API密钥列表,支持轮询机制
|
tavily_api_keys = ["None"]# Tavily API密钥列表,支持轮询机制
|
||||||
exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制
|
exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制
|
||||||
|
metaso_api_keys = ["None"]# Metaso API密钥列表,支持轮询机制
|
||||||
searxng_instances = [] # SearXNG 实例 URL 列表
|
searxng_instances = [] # SearXNG 实例 URL 列表
|
||||||
searxng_api_keys = []# SearXNG 实例 API 密钥列表
|
searxng_api_keys = []# SearXNG 实例 API 密钥列表
|
||||||
|
|
||||||
# 搜索引擎配置
|
# 搜索引擎配置
|
||||||
enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing"
|
enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso"
|
||||||
search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个)
|
search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个)
|
||||||
|
|
||||||
[sleep_system]
|
[sleep_system]
|
||||||
|
|||||||
Reference in New Issue
Block a user