Merge PR #1370: add web tools proxy support
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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]] = [
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -69,14 +71,15 @@ class WebSearchTool(Tool):
|
|||||||
async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str:
|
async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str:
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
return (
|
return (
|
||||||
"Error: Brave Search API key not configured. "
|
"Error: Brave Search API key not configured. Set it in "
|
||||||
"Set it in ~/.nanobot/config.json under tools.web.search.apiKey "
|
"~/.nanobot/config.json under tools.web.search.apiKey "
|
||||||
"(or export BRAVE_API_KEY), then restart the gateway."
|
"(or export BRAVE_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:
|
logger.debug("WebSearch: {}", "proxy enabled" if self.proxy else "direct connection")
|
||||||
|
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},
|
||||||
@@ -85,17 +88,21 @@ class WebSearchTool(Tool):
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
results = r.json().get("web", {}).get("results", [])
|
results = r.json().get("web", {}).get("results", [])[:n]
|
||||||
if not results:
|
if not results:
|
||||||
return f"No results for: {query}"
|
return f"No results for: {query}"
|
||||||
|
|
||||||
lines = [f"Results for: {query}\n"]
|
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', '')}")
|
lines.append(f"{i}. {item.get('title', '')}\n {item.get('url', '')}")
|
||||||
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,34 +121,33 @@ 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
|
||||||
|
|
||||||
max_chars = maxChars or self.max_chars
|
max_chars = maxChars or self.max_chars
|
||||||
|
|
||||||
# Validate URL before fetching
|
|
||||||
is_valid, error_msg = _validate_url(url)
|
is_valid, error_msg = _validate_url(url)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
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:
|
||||||
|
logger.debug("WebFetch: {}", "proxy enabled" if self.proxy else "direct connection")
|
||||||
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()
|
||||||
|
|
||||||
ctype = r.headers.get("content-type", "")
|
ctype = r.headers.get("content-type", "")
|
||||||
|
|
||||||
# JSON
|
|
||||||
if "application/json" in ctype:
|
if "application/json" in ctype:
|
||||||
text, extractor = json.dumps(r.json(), indent=2, ensure_ascii=False), "json"
|
text, extractor = json.dumps(r.json(), indent=2, ensure_ascii=False), "json"
|
||||||
# HTML
|
|
||||||
elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")):
|
elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")):
|
||||||
doc = Document(r.text)
|
doc = Document(r.text)
|
||||||
content = self._to_markdown(doc.summary()) if extractMode == "markdown" else _strip_tags(doc.summary())
|
content = self._to_markdown(doc.summary()) if extractMode == "markdown" else _strip_tags(doc.summary())
|
||||||
@@ -151,12 +157,15 @@ class WebFetchTool(Tool):
|
|||||||
text, extractor = r.text, "raw"
|
text, extractor = r.text, "raw"
|
||||||
|
|
||||||
truncated = len(text) > max_chars
|
truncated = len(text) > max_chars
|
||||||
if truncated:
|
if truncated: text = text[:max_chars]
|
||||||
text = text[:max_chars]
|
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -456,6 +457,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,
|
||||||
@@ -950,6 +952,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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user