ForcePilot/backend/package/yuxi/agents/toolkits/buildin/search/chain.py
Kris 077de8e6d6 feat(agent-toolkit): add web search and crawl built-in toolkits
实现了完整的网页搜索与抓取内置工具集:
1. 新增搜索工具链:支持SearXNG与Tavily多Provider降级、滑动窗口限流、统一结果格式
2. 新增网页抓取工具:单页抓取、站点递归抓取、内容入库知识库功能
3. 完善安全防护:SSRF校验、robots.txt合规、请求限流
4. 自动注册工具到系统注册表,无需额外配置即可使用
2026-06-22 21:20:02 +08:00

81 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Provider 降级编排链,前者出错时自动降级到后者。"""
import httpx
from yuxi.config import config
from yuxi.utils.logging_config import logger
from .models import SearchResult
from .providers import SearchProvider, get_searxng_provider, get_tavily_provider
class ProviderChain:
"""Provider 优先级链,前者出错时自动降级到后者。
降级规则:
1. Provider 抛异常时降级(网络错误 / 429 / 5xx
2. 空结果不降级(空结果是合法的"无匹配"
3. 4xx非 429不降级视为配置错误如 403 format 未启用、401 API Key 无效)
"""
def __init__(self, providers: list[SearchProvider]):
self._providers = providers
@property
def is_empty(self) -> bool:
"""是否没有可用 Provider。"""
return len(self._providers) == 0
async def search(self, query: str, *, max_results: int = 5) -> tuple[list[SearchResult], str]:
"""返回 (结果列表, 实际命中的 Provider 名)。
所有 Provider 均失败时返回 ([], ""),由调用方返回友好提示。
"""
for provider in self._providers:
if not provider.is_available():
logger.debug(f"Provider {provider.name} 不可用,跳过")
continue
try:
results = await provider.search(query, max_results=max_results)
# 空结果不降级,直接返回
return results, provider.name
except Exception as exc:
if self._is_fallbackable(exc):
logger.warning(
f"provider_fallback from={provider.name} reason={type(exc).__name__}: {exc}"
)
continue
# 不可降级错误4xx 非 429记录错误后返回失败
logger.error(f"Provider {provider.name} 不可降级错误: {exc}")
return [], provider.name
return [], ""
@staticmethod
def _is_fallbackable(exc: Exception) -> bool:
"""判断异常是否可降级。"""
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code
return status == 429 or status >= 500
# TransportError 是 ConnectError/TimeoutException/ReadError 等的基类
return isinstance(exc, httpx.TransportError)
def build_provider_chain() -> ProviderChain:
"""根据 config.web_search_provider_chain 构建 ProviderChain。
每次调用时从 config 读取,支持配置热生效。
Provider 实例通过单例工厂获取,复用 HTTP 连接池。
"""
provider_factories = {
"searxng": get_searxng_provider,
"tavily": get_tavily_provider,
}
chain_names = config.web_search_provider_chain
providers = []
for name in chain_names:
factory = provider_factories.get(name)
if factory is None:
logger.warning(f"未知 Provider 名: {name},跳过")
continue
providers.append(factory())
return ProviderChain(providers)