diff --git a/README.md b/README.md index da7346b38..7ca22fd23 100644 --- a/README.md +++ b/README.md @@ -1217,17 +1217,30 @@ When a channel send operation raises an error, nanobot retries with exponential nanobot supports multiple web search providers. Configure in `~/.nanobot/config.json` under `tools.web.search`. +By default, web tools are enabled and web search uses `duckduckgo`, so search works out of the box without an API key. + +If you want to disable all built-in web tools entirely, set `tools.web.enable` to `false`. This removes both `web_search` and `web_fetch` from the tool list sent to the LLM. + | Provider | Config fields | Env var fallback | Free | |----------|--------------|------------------|------| -| `brave` (default) | `apiKey` | `BRAVE_API_KEY` | No | +| `brave` | `apiKey` | `BRAVE_API_KEY` | No | | `tavily` | `apiKey` | `TAVILY_API_KEY` | No | | `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) | | `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) | -| `duckduckgo` | — | — | Yes | +| `duckduckgo` (default) | — | — | Yes | -When credentials are missing, nanobot automatically falls back to DuckDuckGo. +**Disable all built-in web tools:** +```json +{ + "tools": { + "web": { + "enable": false + } + } +} +``` -**Brave** (default): +**Brave:** ```json { "tools": { @@ -1298,7 +1311,14 @@ When credentials are missing, nanobot automatically falls back to DuckDuckGo. | Option | Type | Default | Description | |--------|------|---------|-------------| -| `provider` | string | `"brave"` | Search backend: `brave`, `tavily`, `jina`, `searxng`, `duckduckgo` | +| `enable` | boolean | `true` | Enable or disable all built-in web tools (`web_search` + `web_fetch`) | +| `proxy` | string or null | `null` | Proxy for all web requests, for example `http://127.0.0.1:7890` | + +#### `tools.web.search` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `provider` | string | `"duckduckgo"` | Search backend: `brave`, `tavily`, `jina`, `searxng`, `duckduckgo` | | `apiKey` | string | `""` | API key for Brave or Tavily | | `baseUrl` | string | `""` | Base URL for SearXNG | | `maxResults` | integer | `5` | Results per search (1–10) | diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 4a68a19fc..48a68cdcd 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -36,7 +36,7 @@ from nanobot.utils.helpers import image_placeholder_text, truncate_text from nanobot.utils.runtime import EMPTY_FINAL_RESPONSE_MESSAGE if TYPE_CHECKING: - from nanobot.config.schema import ChannelsConfig, ExecToolConfig, WebSearchConfig + from nanobot.config.schema import ChannelsConfig, ExecToolConfig, WebToolsConfig from nanobot.cron.service import CronService @@ -171,8 +171,7 @@ class AgentLoop: context_block_limit: int | None = None, max_tool_result_chars: int | None = None, provider_retry_mode: str = "standard", - web_search_config: WebSearchConfig | None = None, - web_proxy: str | None = None, + web_config: WebToolsConfig | None = None, exec_config: ExecToolConfig | None = None, cron_service: CronService | None = None, restrict_to_workspace: bool = False, @@ -182,7 +181,7 @@ class AgentLoop: timezone: str | None = None, hooks: list[AgentHook] | None = None, ): - from nanobot.config.schema import ExecToolConfig, WebSearchConfig + from nanobot.config.schema import ExecToolConfig, WebToolsConfig defaults = AgentDefaults() self.bus = bus @@ -205,8 +204,7 @@ class AgentLoop: else defaults.max_tool_result_chars ) self.provider_retry_mode = provider_retry_mode - self.web_search_config = web_search_config or WebSearchConfig() - self.web_proxy = web_proxy + self.web_config = web_config or WebToolsConfig() self.exec_config = exec_config or ExecToolConfig() self.cron_service = cron_service self.restrict_to_workspace = restrict_to_workspace @@ -223,9 +221,8 @@ class AgentLoop: workspace=workspace, bus=bus, model=self.model, + web_config=self.web_config, max_tool_result_chars=self.max_tool_result_chars, - web_search_config=self.web_search_config, - web_proxy=web_proxy, exec_config=self.exec_config, restrict_to_workspace=restrict_to_workspace, ) @@ -271,8 +268,9 @@ class AgentLoop: restrict_to_workspace=self.restrict_to_workspace, path_append=self.exec_config.path_append, )) - self.tools.register(WebSearchTool(config=self.web_search_config, proxy=self.web_proxy)) - self.tools.register(WebFetchTool(proxy=self.web_proxy)) + if self.web_config.enable: + self.tools.register(WebSearchTool(config=self.web_config.search, proxy=self.web_config.proxy)) + self.tools.register(WebFetchTool(proxy=self.web_config.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 c7643a486..81e72c084 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -17,7 +17,7 @@ from nanobot.agent.tools.shell import ExecTool from nanobot.agent.tools.web import WebFetchTool, WebSearchTool from nanobot.bus.events import InboundMessage from nanobot.bus.queue import MessageBus -from nanobot.config.schema import ExecToolConfig +from nanobot.config.schema import ExecToolConfig, WebToolsConfig from nanobot.providers.base import LLMProvider @@ -46,20 +46,18 @@ class SubagentManager: bus: MessageBus, max_tool_result_chars: int, model: str | None = None, - web_search_config: "WebSearchConfig | None" = None, - web_proxy: str | None = None, + web_config: "WebToolsConfig | None" = None, exec_config: "ExecToolConfig | None" = None, restrict_to_workspace: bool = False, ): - from nanobot.config.schema import ExecToolConfig, WebSearchConfig + from nanobot.config.schema import ExecToolConfig self.provider = provider self.workspace = workspace self.bus = bus self.model = model or provider.get_default_model() + self.web_config = web_config or WebToolsConfig() self.max_tool_result_chars = max_tool_result_chars - self.web_search_config = web_search_config or WebSearchConfig() - self.web_proxy = web_proxy self.exec_config = exec_config or ExecToolConfig() self.restrict_to_workspace = restrict_to_workspace self.runner = AgentRunner(provider) @@ -124,9 +122,9 @@ class SubagentManager: restrict_to_workspace=self.restrict_to_workspace, path_append=self.exec_config.path_append, )) - tools.register(WebSearchTool(config=self.web_search_config, proxy=self.web_proxy)) - tools.register(WebFetchTool(proxy=self.web_proxy)) - + if self.web_config.enable: + tools.register(WebSearchTool(config=self.web_config.search, proxy=self.web_config.proxy)) + tools.register(WebFetchTool(proxy=self.web_config.proxy)) system_prompt = self._build_subagent_prompt() messages: list[dict[str, Any]] = [ {"role": "system", "content": system_prompt}, diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 4dcf3873f..dcbf66746 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -550,8 +550,7 @@ def serve( context_block_limit=runtime_config.agents.defaults.context_block_limit, max_tool_result_chars=runtime_config.agents.defaults.max_tool_result_chars, provider_retry_mode=runtime_config.agents.defaults.provider_retry_mode, - web_search_config=runtime_config.tools.web.search, - web_proxy=runtime_config.tools.web.proxy or None, + web_config=runtime_config.tools.web, exec_config=runtime_config.tools.exec, restrict_to_workspace=runtime_config.tools.restrict_to_workspace, session_manager=session_manager, @@ -637,11 +636,10 @@ def gateway( model=config.agents.defaults.model, max_iterations=config.agents.defaults.max_tool_iterations, context_window_tokens=config.agents.defaults.context_window_tokens, + web_config=config.tools.web, context_block_limit=config.agents.defaults.context_block_limit, max_tool_result_chars=config.agents.defaults.max_tool_result_chars, provider_retry_mode=config.agents.defaults.provider_retry_mode, - web_search_config=config.tools.web.search, - web_proxy=config.tools.web.proxy or None, exec_config=config.tools.exec, cron_service=cron, restrict_to_workspace=config.tools.restrict_to_workspace, @@ -846,11 +844,10 @@ def agent( model=config.agents.defaults.model, max_iterations=config.agents.defaults.max_tool_iterations, context_window_tokens=config.agents.defaults.context_window_tokens, + web_config=config.tools.web, context_block_limit=config.agents.defaults.context_block_limit, max_tool_result_chars=config.agents.defaults.max_tool_result_chars, provider_retry_mode=config.agents.defaults.provider_retry_mode, - web_search_config=config.tools.web.search, - web_proxy=config.tools.web.proxy or None, exec_config=config.tools.exec, cron_service=cron, restrict_to_workspace=config.tools.restrict_to_workspace, diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index e46663554..cc377cfe0 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -119,7 +119,7 @@ class GatewayConfig(Base): class WebSearchConfig(Base): """Web search tool configuration.""" - provider: str = "brave" # brave, tavily, duckduckgo, searxng, jina + provider: str = "duckduckgo" # brave, tavily, duckduckgo, searxng, jina api_key: str = "" base_url: str = "" # SearXNG base URL max_results: int = 5 @@ -128,6 +128,7 @@ class WebSearchConfig(Base): class WebToolsConfig(Base): """Web tools configuration.""" + enable: bool = True proxy: str | None = ( None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080" ) diff --git a/nanobot/nanobot.py b/nanobot/nanobot.py index 30282cb4f..4860fa312 100644 --- a/nanobot/nanobot.py +++ b/nanobot/nanobot.py @@ -76,8 +76,7 @@ class Nanobot: context_block_limit=defaults.context_block_limit, max_tool_result_chars=defaults.max_tool_result_chars, provider_retry_mode=defaults.provider_retry_mode, - web_search_config=config.tools.web.search, - web_proxy=config.tools.web.proxy or None, + web_config=config.tools.web, exec_config=config.tools.exec, restrict_to_workspace=config.tools.restrict_to_workspace, mcp_servers=config.tools.mcp_servers,