base_agent/tools/web_search.py

168 lines
5.7 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.

"""
tools/web_search.py
网络搜索工具 —— 支持 mock / SerpAPI / Brave Search
配置通过 settings.tools['web_search'] 读取
"""
import json
import time
from dataclasses import dataclass, field
from config.settings import settings
from tools.base_tool import BaseTool
from utils.logger import get_logger
logger = get_logger("TOOL.WebSearch")
def _cfg(key: str, fallback=None):
return settings.tools['web_search'].get(key, fallback)
@dataclass
class SearchResult:
title: str
url: str
snippet: str
rank: int = 0
def __str__(self) -> str:
return f"[{self.rank}] {self.title}\n {self.url}\n {self.snippet}"
class Tool(BaseTool):
name = "web_search"
description = (
"在互联网上搜索信息,返回相关网页的标题、链接和摘要。"
"适用于需要实时信息、最新资讯或不确定的知识查询。"
)
parameters = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或问题,例如: 'Python 3.12 新特性'",
},
"max_results": {
"type": "integer",
"description": "返回结果数量(默认来自 config.yaml web_search.max_results",
},
},
"required": ["query"],
}
def execute(self, query: str = "", max_results: int | None = None, **_) -> str:
if not query or not query.strip():
return "❌ 参数错误: query 不能为空"
n = max_results or _cfg('max_results', 5)
engine = _cfg('engine', 'mock')
logger.info(
f"🔍 搜索: {query}\n"
f" 引擎={engine} max_results={n} "
f"[config engine={_cfg('engine')} max_results={_cfg('max_results')}]"
)
match engine:
case "serpapi":
results = self._search_serpapi(query, n)
case "brave":
results = self._search_brave(query, n)
case _:
results = self._search_mock(query, n)
if not results:
return f"🔍 搜索 '{query}' 未找到相关结果"
lines = [f"🔍 搜索结果: {query} (共 {len(results)} 条)", "" * 50]
for r in results:
lines.append(str(r))
return "\n".join(lines)
# ── 搜索引擎实现 ──────────────────────────────────────────
@staticmethod
def _search_mock(query: str, n: int) -> list[SearchResult]:
"""Mock 搜索(无需 API Key用于测试"""
return [
SearchResult(
title=f"搜索结果 {i + 1}: {query}",
url=f"https://example.com/result/{i + 1}",
snippet=(
f"这是关于 '{query}' 的第 {i + 1} 条模拟搜索结果。"
f"实际使用请在 config.yaml 中配置 engine: serpapi 或 brave。"
),
rank=i + 1,
)
for i in range(n)
]
def _search_serpapi(self, query: str, n: int) -> list[SearchResult]:
"""SerpAPI 搜索"""
api_key = _cfg('api_key', '')
if not api_key:
logger.warning("⚠️ SerpAPI api_key 未配置,回退到 mock 模式")
return self._search_mock(query, n)
try:
import httpx
timeout = _cfg('timeout', 10)
resp = httpx.get(
"https://serpapi.com/search",
params={
"q": query,
"num": n,
"api_key": api_key,
"engine": "google",
},
timeout=timeout,
)
resp.raise_for_status()
data = resp.json()
organic = data.get("organic_results", [])
return [
SearchResult(
title=r.get("title", ""),
url=r.get("link", ""),
snippet=r.get("snippet", ""),
rank=i + 1,
)
for i, r in enumerate(organic[:n])
]
except Exception as e:
logger.error(f"❌ SerpAPI 搜索失败: {e},回退到 mock")
return self._search_mock(query, n)
def _search_brave(self, query: str, n: int) -> list[SearchResult]:
"""Brave Search API"""
api_key = _cfg('api_key', '')
if not api_key:
logger.warning("⚠️ Brave Search api_key 未配置,回退到 mock 模式")
return self._search_mock(query, n)
try:
import httpx
timeout = _cfg('timeout', 10)
resp = httpx.get(
"https://api.search.brave.com/res/v1/web/search",
params={"q": query, "count": n},
headers={
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": api_key,
},
timeout=timeout,
)
resp.raise_for_status()
data = resp.json()
web = data.get("web", {}).get("results", [])
return [
SearchResult(
title=r.get("title", ""),
url=r.get("url", ""),
snippet=r.get("description", ""),
rank=i + 1,
)
for i, r in enumerate(web[:n])
]
except Exception as e:
logger.error(f"❌ Brave Search 失败: {e},回退到 mock")
return self._search_mock(query, n)