base_agent/tools/web_search.py

168 lines
5.7 KiB
Python
Raw Permalink Normal View History

2026-03-30 08:48:36 +00:00
"""
tools/web_search.py
网络搜索工具 支持 mock / SerpAPI / Brave Search
配置通过 settings.tools['web_search'] 读取
"""
2026-02-28 08:21:35 +00:00
2026-03-30 08:48:36 +00:00
import json
2026-02-28 08:21:35 +00:00
import time
2026-03-30 08:48:36 +00:00
from dataclasses import dataclass, field
2026-03-09 05:37:29 +00:00
from config.settings import settings
2026-04-15 08:20:22 +00:00
from tools.base_tool import BaseTool
2026-03-30 08:48:36 +00:00
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)
2026-02-28 08:21:35 +00:00
2026-03-30 08:48:36 +00:00
@dataclass
class SearchResult:
title: str
url: str
snippet: str
rank: int = 0
2026-02-28 08:21:35 +00:00
2026-03-30 08:48:36 +00:00
def __str__(self) -> str:
return f"[{self.rank}] {self.title}\n {self.url}\n {self.snippet}"
2026-04-15 08:20:22 +00:00
class Tool(BaseTool):
2026-02-28 08:21:35 +00:00
name = "web_search"
2026-03-30 08:48:36 +00:00
description = (
"在互联网上搜索信息,返回相关网页的标题、链接和摘要。"
"适用于需要实时信息、最新资讯或不确定的知识查询。"
)
2026-02-28 08:21:35 +00:00
parameters = {
2026-03-30 08:48:36 +00:00
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或问题,例如: 'Python 3.12 新特性'",
},
"max_results": {
"type": "integer",
"description": "返回结果数量(默认来自 config.yaml web_search.max_results",
},
},
"required": ["query"],
2026-02-28 08:21:35 +00:00
}
2026-03-30 08:48:36 +00:00
def execute(self, query: str = "", max_results: int | None = None, **_) -> str:
if not query or not query.strip():
return "❌ 参数错误: query 不能为空"
2026-03-09 05:37:29 +00:00
2026-03-30 08:48:36 +00:00
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')}]"
2026-02-28 08:21:35 +00:00
)
2026-03-30 08:48:36 +00:00
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)