From 82be2ae1a5cbf0d2579c4fc346c2562464f85dd8 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sun, 1 Mar 2026 13:27:46 +0800 Subject: [PATCH 1/2] feat(tool): add web search proxy --- nanobot/agent/loop.py | 7 +++++-- nanobot/agent/subagent.py | 6 ++++-- nanobot/agent/tools/web.py | 30 +++++++++++++++++++++++++----- nanobot/cli/commands.py | 3 +++ nanobot/config/schema.py | 1 + 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 8da9fcb..488615d 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -58,6 +58,7 @@ class AgentLoop: memory_window: int = 100, reasoning_effort: str | None = None, brave_api_key: str | None = None, + web_proxy: str | None = None, exec_config: ExecToolConfig | None = None, cron_service: CronService | None = None, restrict_to_workspace: bool = False, @@ -77,6 +78,7 @@ class AgentLoop: self.memory_window = memory_window self.reasoning_effort = reasoning_effort self.brave_api_key = brave_api_key + self.web_proxy = web_proxy self.exec_config = exec_config or ExecToolConfig() self.cron_service = cron_service self.restrict_to_workspace = restrict_to_workspace @@ -93,6 +95,7 @@ class AgentLoop: max_tokens=self.max_tokens, reasoning_effort=reasoning_effort, brave_api_key=brave_api_key, + web_proxy=web_proxy, exec_config=self.exec_config, restrict_to_workspace=restrict_to_workspace, ) @@ -120,8 +123,8 @@ class AgentLoop: restrict_to_workspace=self.restrict_to_workspace, path_append=self.exec_config.path_append, )) - self.tools.register(WebSearchTool(api_key=self.brave_api_key)) - self.tools.register(WebFetchTool()) + self.tools.register(WebSearchTool(api_key=self.brave_api_key, proxy=self.web_proxy)) + self.tools.register(WebFetchTool(proxy=self.web_proxy)) self.tools.register(MessageTool(send_callback=self.bus.publish_outbound)) self.tools.register(SpawnTool(manager=self.subagents)) if self.cron_service: diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 9b543dc..f2d6ee5 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -31,6 +31,7 @@ class SubagentManager: max_tokens: int = 4096, reasoning_effort: str | None = None, brave_api_key: str | None = None, + web_proxy: str | None = None, exec_config: "ExecToolConfig | None" = None, restrict_to_workspace: bool = False, ): @@ -43,6 +44,7 @@ class SubagentManager: self.max_tokens = max_tokens self.reasoning_effort = reasoning_effort self.brave_api_key = brave_api_key + self.web_proxy = web_proxy self.exec_config = exec_config or ExecToolConfig() self.restrict_to_workspace = restrict_to_workspace self._running_tasks: dict[str, asyncio.Task[None]] = {} @@ -104,8 +106,8 @@ class SubagentManager: restrict_to_workspace=self.restrict_to_workspace, path_append=self.exec_config.path_append, )) - tools.register(WebSearchTool(api_key=self.brave_api_key)) - tools.register(WebFetchTool()) + tools.register(WebSearchTool(api_key=self.brave_api_key, proxy=self.web_proxy)) + tools.register(WebFetchTool(proxy=self.web_proxy)) system_prompt = self._build_subagent_prompt() messages: list[dict[str, Any]] = [ diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index e817a4c..0d2135d 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -8,6 +8,7 @@ from typing import Any from urllib.parse import urlparse import httpx +from loguru import logger from nanobot.agent.tools.base import Tool @@ -57,9 +58,10 @@ class WebSearchTool(Tool): "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.max_results = max_results + self.proxy = proxy @property def api_key(self) -> str: @@ -71,12 +73,16 @@ class WebSearchTool(Tool): return ( "Error: Brave Search API key not configured. " "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: 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( "https://api.search.brave.com/res/v1/web/search", params={"q": query, "count": n}, @@ -95,7 +101,11 @@ class WebSearchTool(Tool): if desc := item.get("description"): lines.append(f" {desc}") return "\n".join(lines) + except httpx.ProxyError as e: + logger.error("WebSearch proxy error: {}", e) + return f"Proxy error: {e}" except Exception as e: + logger.error("WebSearch error: {}", e) return f"Error: {e}" @@ -114,8 +124,9 @@ class WebFetchTool(Tool): "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.proxy = proxy async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str: 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) 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( follow_redirects=True, max_redirects=MAX_REDIRECTS, - timeout=30.0 + timeout=30.0, + proxy=self.proxy, ) as client: r = await client.get(url, headers={"User-Agent": USER_AGENT}) r.raise_for_status() @@ -156,7 +172,11 @@ class WebFetchTool(Tool): 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) + 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: + logger.error("WebFetch error for {}: {}", url, e) return json.dumps({"error": str(e), "url": url}, ensure_ascii=False) def _to_markdown(self, html: str) -> str: diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 4987c84..25fa8e1 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -284,6 +284,7 @@ def gateway( memory_window=config.agents.defaults.memory_window, reasoning_effort=config.agents.defaults.reasoning_effort, brave_api_key=config.tools.web.search.api_key or None, + web_proxy=config.tools.web.proxy or None, exec_config=config.tools.exec, cron_service=cron, restrict_to_workspace=config.tools.restrict_to_workspace, @@ -444,6 +445,7 @@ def agent( memory_window=config.agents.defaults.memory_window, reasoning_effort=config.agents.defaults.reasoning_effort, brave_api_key=config.tools.web.search.api_key or None, + web_proxy=config.tools.web.proxy or None, exec_config=config.tools.exec, cron_service=cron, restrict_to_workspace=config.tools.restrict_to_workspace, @@ -938,6 +940,7 @@ def cron_run( memory_window=config.agents.defaults.memory_window, reasoning_effort=config.agents.defaults.reasoning_effort, brave_api_key=config.tools.web.search.api_key or None, + web_proxy=config.tools.web.proxy or None, exec_config=config.tools.exec, restrict_to_workspace=config.tools.restrict_to_workspace, mcp_servers=config.tools.mcp_servers, diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 091a210..6b80c81 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -290,6 +290,7 @@ class WebSearchConfig(Base): class WebToolsConfig(Base): """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) From 15529c668e51c623ab860509b12346e3dfe956d6 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 1 Mar 2026 12:53:18 +0000 Subject: [PATCH 2/2] fix(web): sanitize proxy logs and polish search key hint --- nanobot/agent/tools/web.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index 0d2135d..0d8f4d1 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -71,17 +71,14 @@ class WebSearchTool(Tool): async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str: if not self.api_key: return ( - "Error: Brave Search API key not configured. " - "Set it in ~/.nanobot/config.json under tools.web.search.apiKey " - "(or export BRAIVE_API_KEY), then restart the gateway." + "Error: Brave Search API key not configured. Set it in " + "~/.nanobot/config.json under tools.web.search.apiKey " + "(or export BRAVE_API_KEY), then restart the gateway." ) try: n = min(max(count or self.max_results, 1), 10) - if self.proxy: - logger.info("WebSearch: using proxy {} for query: {}", self.proxy, query[:50]) - else: - logger.debug("WebSearch: direct connection for query: {}", query[:50]) + logger.debug("WebSearch: {}", "proxy enabled" if self.proxy else "direct connection") async with httpx.AsyncClient(proxy=self.proxy) as client: r = await client.get( "https://api.search.brave.com/res/v1/web/search", @@ -91,12 +88,12 @@ class WebSearchTool(Tool): ) r.raise_for_status() - results = r.json().get("web", {}).get("results", []) + results = r.json().get("web", {}).get("results", [])[:n] if not results: return f"No results for: {query}" lines = [f"Results for: {query}\n"] - for i, item in enumerate(results[:n], 1): + for i, item in enumerate(results, 1): lines.append(f"{i}. {item.get('title', '')}\n {item.get('url', '')}") if desc := item.get("description"): lines.append(f" {desc}") @@ -132,17 +129,12 @@ class WebFetchTool(Tool): from readability import Document max_chars = maxChars or self.max_chars - - # Validate URL before fetching is_valid, error_msg = _validate_url(url) if not is_valid: return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}, ensure_ascii=False) try: - if self.proxy: - logger.info("WebFetch: using proxy {} for {}", self.proxy, url) - else: - logger.debug("WebFetch: direct connection for {}", url) + logger.debug("WebFetch: {}", "proxy enabled" if self.proxy else "direct connection") async with httpx.AsyncClient( follow_redirects=True, max_redirects=MAX_REDIRECTS, @@ -154,10 +146,8 @@ class WebFetchTool(Tool): ctype = r.headers.get("content-type", "") - # JSON if "application/json" in ctype: text, extractor = json.dumps(r.json(), indent=2, ensure_ascii=False), "json" - # HTML elif "text/html" in ctype or r.text[:256].lower().startswith((" max_chars - if truncated: - text = text[:max_chars] + if truncated: text = text[:max_chars] 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)