feat(tool): add web search proxy

This commit is contained in:
chengyongru
2026-03-01 13:27:46 +08:00
parent aff8d8e9e1
commit 82be2ae1a5
5 changed files with 38 additions and 9 deletions

View File

@@ -58,6 +58,7 @@ class AgentLoop:
memory_window: int = 100, memory_window: int = 100,
reasoning_effort: str | None = None, reasoning_effort: str | None = None,
brave_api_key: str | None = None, brave_api_key: str | None = None,
web_proxy: str | None = None,
exec_config: ExecToolConfig | None = None, exec_config: ExecToolConfig | None = None,
cron_service: CronService | None = None, cron_service: CronService | None = None,
restrict_to_workspace: bool = False, restrict_to_workspace: bool = False,
@@ -77,6 +78,7 @@ class AgentLoop:
self.memory_window = memory_window self.memory_window = memory_window
self.reasoning_effort = reasoning_effort self.reasoning_effort = reasoning_effort
self.brave_api_key = brave_api_key self.brave_api_key = brave_api_key
self.web_proxy = web_proxy
self.exec_config = exec_config or ExecToolConfig() self.exec_config = exec_config or ExecToolConfig()
self.cron_service = cron_service self.cron_service = cron_service
self.restrict_to_workspace = restrict_to_workspace self.restrict_to_workspace = restrict_to_workspace
@@ -93,6 +95,7 @@ class AgentLoop:
max_tokens=self.max_tokens, max_tokens=self.max_tokens,
reasoning_effort=reasoning_effort, reasoning_effort=reasoning_effort,
brave_api_key=brave_api_key, brave_api_key=brave_api_key,
web_proxy=web_proxy,
exec_config=self.exec_config, exec_config=self.exec_config,
restrict_to_workspace=restrict_to_workspace, restrict_to_workspace=restrict_to_workspace,
) )
@@ -120,8 +123,8 @@ class AgentLoop:
restrict_to_workspace=self.restrict_to_workspace, restrict_to_workspace=self.restrict_to_workspace,
path_append=self.exec_config.path_append, path_append=self.exec_config.path_append,
)) ))
self.tools.register(WebSearchTool(api_key=self.brave_api_key)) self.tools.register(WebSearchTool(api_key=self.brave_api_key, proxy=self.web_proxy))
self.tools.register(WebFetchTool()) self.tools.register(WebFetchTool(proxy=self.web_proxy))
self.tools.register(MessageTool(send_callback=self.bus.publish_outbound)) self.tools.register(MessageTool(send_callback=self.bus.publish_outbound))
self.tools.register(SpawnTool(manager=self.subagents)) self.tools.register(SpawnTool(manager=self.subagents))
if self.cron_service: if self.cron_service:

View File

@@ -31,6 +31,7 @@ class SubagentManager:
max_tokens: int = 4096, max_tokens: int = 4096,
reasoning_effort: str | None = None, reasoning_effort: str | None = None,
brave_api_key: str | None = None, brave_api_key: str | None = None,
web_proxy: str | None = None,
exec_config: "ExecToolConfig | None" = None, exec_config: "ExecToolConfig | None" = None,
restrict_to_workspace: bool = False, restrict_to_workspace: bool = False,
): ):
@@ -43,6 +44,7 @@ class SubagentManager:
self.max_tokens = max_tokens self.max_tokens = max_tokens
self.reasoning_effort = reasoning_effort self.reasoning_effort = reasoning_effort
self.brave_api_key = brave_api_key self.brave_api_key = brave_api_key
self.web_proxy = web_proxy
self.exec_config = exec_config or ExecToolConfig() self.exec_config = exec_config or ExecToolConfig()
self.restrict_to_workspace = restrict_to_workspace self.restrict_to_workspace = restrict_to_workspace
self._running_tasks: dict[str, asyncio.Task[None]] = {} self._running_tasks: dict[str, asyncio.Task[None]] = {}
@@ -104,8 +106,8 @@ class SubagentManager:
restrict_to_workspace=self.restrict_to_workspace, restrict_to_workspace=self.restrict_to_workspace,
path_append=self.exec_config.path_append, path_append=self.exec_config.path_append,
)) ))
tools.register(WebSearchTool(api_key=self.brave_api_key)) tools.register(WebSearchTool(api_key=self.brave_api_key, proxy=self.web_proxy))
tools.register(WebFetchTool()) tools.register(WebFetchTool(proxy=self.web_proxy))
system_prompt = self._build_subagent_prompt() system_prompt = self._build_subagent_prompt()
messages: list[dict[str, Any]] = [ messages: list[dict[str, Any]] = [

View File

@@ -8,6 +8,7 @@ from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
from loguru import logger
from nanobot.agent.tools.base import Tool from nanobot.agent.tools.base import Tool
@@ -57,9 +58,10 @@ class WebSearchTool(Tool):
"required": ["query"] "required": ["query"]
} }
def __init__(self, api_key: str | None = None, max_results: int = 5): def __init__(self, api_key: str | None = None, max_results: int = 5, proxy: str | None = None):
self._init_api_key = api_key self._init_api_key = api_key
self.max_results = max_results self.max_results = max_results
self.proxy = proxy
@property @property
def api_key(self) -> str: def api_key(self) -> str:
@@ -71,12 +73,16 @@ class WebSearchTool(Tool):
return ( return (
"Error: Brave Search API key not configured. " "Error: Brave Search API key not configured. "
"Set it in ~/.nanobot/config.json under tools.web.search.apiKey " "Set it in ~/.nanobot/config.json under tools.web.search.apiKey "
"(or export BRAVE_API_KEY), then restart the gateway." "(or export BRAIVE_API_KEY), then restart the gateway."
) )
try: try:
n = min(max(count or self.max_results, 1), 10) n = min(max(count or self.max_results, 1), 10)
async with httpx.AsyncClient() as client: if self.proxy:
logger.info("WebSearch: using proxy {} for query: {}", self.proxy, query[:50])
else:
logger.debug("WebSearch: direct connection for query: {}", query[:50])
async with httpx.AsyncClient(proxy=self.proxy) as client:
r = await client.get( r = await client.get(
"https://api.search.brave.com/res/v1/web/search", "https://api.search.brave.com/res/v1/web/search",
params={"q": query, "count": n}, params={"q": query, "count": n},
@@ -95,7 +101,11 @@ class WebSearchTool(Tool):
if desc := item.get("description"): if desc := item.get("description"):
lines.append(f" {desc}") lines.append(f" {desc}")
return "\n".join(lines) return "\n".join(lines)
except httpx.ProxyError as e:
logger.error("WebSearch proxy error: {}", e)
return f"Proxy error: {e}"
except Exception as e: except Exception as e:
logger.error("WebSearch error: {}", e)
return f"Error: {e}" return f"Error: {e}"
@@ -114,8 +124,9 @@ class WebFetchTool(Tool):
"required": ["url"] "required": ["url"]
} }
def __init__(self, max_chars: int = 50000): def __init__(self, max_chars: int = 50000, proxy: str | None = None):
self.max_chars = max_chars self.max_chars = max_chars
self.proxy = proxy
async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str: async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str:
from readability import Document from readability import Document
@@ -128,10 +139,15 @@ class WebFetchTool(Tool):
return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}, ensure_ascii=False) return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}, ensure_ascii=False)
try: try:
if self.proxy:
logger.info("WebFetch: using proxy {} for {}", self.proxy, url)
else:
logger.debug("WebFetch: direct connection for {}", url)
async with httpx.AsyncClient( async with httpx.AsyncClient(
follow_redirects=True, follow_redirects=True,
max_redirects=MAX_REDIRECTS, max_redirects=MAX_REDIRECTS,
timeout=30.0 timeout=30.0,
proxy=self.proxy,
) as client: ) as client:
r = await client.get(url, headers={"User-Agent": USER_AGENT}) r = await client.get(url, headers={"User-Agent": USER_AGENT})
r.raise_for_status() r.raise_for_status()
@@ -156,7 +172,11 @@ class WebFetchTool(Tool):
return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code, return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code,
"extractor": extractor, "truncated": truncated, "length": len(text), "text": text}, ensure_ascii=False) "extractor": extractor, "truncated": truncated, "length": len(text), "text": text}, ensure_ascii=False)
except httpx.ProxyError as e:
logger.error("WebFetch proxy error for {}: {}", url, e)
return json.dumps({"error": f"Proxy error: {e}", "url": url}, ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error("WebFetch error for {}: {}", url, e)
return json.dumps({"error": str(e), "url": url}, ensure_ascii=False) return json.dumps({"error": str(e), "url": url}, ensure_ascii=False)
def _to_markdown(self, html: str) -> str: def _to_markdown(self, html: str) -> str:

View File

@@ -284,6 +284,7 @@ def gateway(
memory_window=config.agents.defaults.memory_window, memory_window=config.agents.defaults.memory_window,
reasoning_effort=config.agents.defaults.reasoning_effort, reasoning_effort=config.agents.defaults.reasoning_effort,
brave_api_key=config.tools.web.search.api_key or None, brave_api_key=config.tools.web.search.api_key or None,
web_proxy=config.tools.web.proxy or None,
exec_config=config.tools.exec, exec_config=config.tools.exec,
cron_service=cron, cron_service=cron,
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
@@ -444,6 +445,7 @@ def agent(
memory_window=config.agents.defaults.memory_window, memory_window=config.agents.defaults.memory_window,
reasoning_effort=config.agents.defaults.reasoning_effort, reasoning_effort=config.agents.defaults.reasoning_effort,
brave_api_key=config.tools.web.search.api_key or None, brave_api_key=config.tools.web.search.api_key or None,
web_proxy=config.tools.web.proxy or None,
exec_config=config.tools.exec, exec_config=config.tools.exec,
cron_service=cron, cron_service=cron,
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
@@ -938,6 +940,7 @@ def cron_run(
memory_window=config.agents.defaults.memory_window, memory_window=config.agents.defaults.memory_window,
reasoning_effort=config.agents.defaults.reasoning_effort, reasoning_effort=config.agents.defaults.reasoning_effort,
brave_api_key=config.tools.web.search.api_key or None, brave_api_key=config.tools.web.search.api_key or None,
web_proxy=config.tools.web.proxy or None,
exec_config=config.tools.exec, exec_config=config.tools.exec,
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
mcp_servers=config.tools.mcp_servers, mcp_servers=config.tools.mcp_servers,

View File

@@ -290,6 +290,7 @@ class WebSearchConfig(Base):
class WebToolsConfig(Base): class WebToolsConfig(Base):
"""Web tools configuration.""" """Web tools configuration."""
proxy: str | None = None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080"
search: WebSearchConfig = Field(default_factory=WebSearchConfig) search: WebSearchConfig = Field(default_factory=WebSearchConfig)