requested changes complete

This commit is contained in:
umerkay 2026-04-28 12:56:39 +05:00 committed by chengyongru
parent 9cf9272920
commit bcfdd49fa4
7 changed files with 84 additions and 97 deletions

View File

@ -187,26 +187,6 @@ Configure these **two parts** in your config (other options have defaults). Add
nanobot agent nanobot agent
``` ```
**Optional: Web search via Olostep**
Default search provider stays `brave`, so existing setups are unchanged. To switch to Olostep:
```json
{
"tools": {
"web": {
"search": {
"provider": "olostep",
"olostepApiKey": "YOUR_OLOSTEP_API_KEY"
}
}
}
}
```
You can also set the key with the `OLOSTEP_API_KEY` environment variable.
Get an API key at https://www.olostep.com/dashboard.
- Want different LLM providers, web search, MCP, security settings, or more config options? See [Configuration](./docs/configuration.md) - Want different LLM providers, web search, MCP, security settings, or more config options? See [Configuration](./docs/configuration.md)
- Want to run nanobot in chat apps like Telegram, Discord, WeChat or Feishu? See [Chat Apps](./docs/chat-apps.md) - Want to run nanobot in chat apps like Telegram, Discord, WeChat or Feishu? See [Chat Apps](./docs/chat-apps.md)

View File

@ -485,7 +485,7 @@ When a channel `send()` raises, nanobot retries at the channel-manager layer. By
nanobot supports multiple web search providers. Configure in `~/.nanobot/config.json` under `tools.web.search`. 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. By default, web tools are enabled and web search uses `brave` when a Brave key is configured; otherwise it falls back to `duckduckgo`, so search still 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. 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.
@ -505,6 +505,7 @@ If you need to allow trusted private ranges such as Tailscale / CGNAT addresses,
| `tavily` | `apiKey` | `TAVILY_API_KEY` | No | | `tavily` | `apiKey` | `TAVILY_API_KEY` | No |
| `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) | | `jina` | `apiKey` | `JINA_API_KEY` | Free tier (10M tokens) |
| `kagi` | `apiKey` | `KAGI_API_KEY` | No | | `kagi` | `apiKey` | `KAGI_API_KEY` | No |
| `olostep` | `apiKey` | `OLOSTEP_API_KEY` | No |
| `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) | | `searxng` | `baseUrl` | `SEARXNG_BASE_URL` | Yes (self-hosted) |
| `duckduckgo` (default) | — | — | Yes | | `duckduckgo` (default) | — | — | Yes |
@ -575,6 +576,22 @@ If you need to allow trusted private ranges such as Tailscale / CGNAT addresses,
} }
``` ```
**Olostep:**
```json
{
"tools": {
"web": {
"search": {
"provider": "olostep",
"apiKey": "YOUR_OLOSTEP_API_KEY"
}
}
}
}
```
You can also set `OLOSTEP_API_KEY` in the environment instead of storing it in config.
**SearXNG** (self-hosted, no API key needed): **SearXNG** (self-hosted, no API key needed):
```json ```json
{ {

View File

@ -320,12 +320,7 @@ class AgentLoop:
) )
if self.web_config.enable: if self.web_config.enable:
self.tools.register( self.tools.register(
WebSearchTool( WebSearchTool(config=self.web_config.search, proxy=self.web_config.proxy)
config=self.web_config.search,
proxy=self.web_config.proxy,
provider=self.web_config.search.provider,
olostep_api_key=self.web_config.search.olostep_api_key,
)
) )
self.tools.register(WebFetchTool(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(MessageTool(send_callback=self.bus.publish_outbound))

View File

@ -18,14 +18,11 @@ from nanobot.agent.tools.schema import IntegerSchema, StringSchema, tool_paramet
from nanobot.utils.helpers import build_image_content_blocks from nanobot.utils.helpers import build_image_content_blocks
try: try:
from olostep import AsyncOlostep, Olostep, Olostep_BaseError from olostep import AsyncOlostep, Olostep_BaseError
_OLOSTEP_SYNC_CLIENT = Olostep
_OLOSTEP_AVAILABLE = True _OLOSTEP_AVAILABLE = True
except ImportError: except ImportError:
Olostep = None
AsyncOlostep = None AsyncOlostep = None
_OLOSTEP_SYNC_CLIENT = None
class Olostep_BaseError(Exception): class Olostep_BaseError(Exception):
"""Fallback error type when olostep package is unavailable.""" """Fallback error type when olostep package is unavailable."""
@ -109,19 +106,12 @@ class WebSearchTool(Tool):
self, self,
config: WebSearchConfig | None = None, config: WebSearchConfig | None = None,
proxy: str | None = None, proxy: str | None = None,
provider: str | None = None,
olostep_api_key: str | None = None,
): ):
from nanobot.config.schema import WebSearchConfig from nanobot.config.schema import WebSearchConfig
self.config = config if config is not None else WebSearchConfig() self.config = config if config is not None else WebSearchConfig()
self.proxy = proxy self.proxy = proxy
self.provider = (provider or self.config.provider or "brave").strip().lower() self.provider = (self.config.provider or "brave").strip().lower()
self.olostep_api_key = (
olostep_api_key
or self.config.olostep_api_key
or os.environ.get("OLOSTEP_API_KEY", "")
)
def _effective_provider(self) -> str: def _effective_provider(self) -> str:
"""Resolve the backend that execute() will actually use.""" """Resolve the backend that execute() will actually use."""
@ -144,7 +134,8 @@ class WebSearchTool(Tool):
api_key = self.config.api_key or os.environ.get("KAGI_API_KEY", "") api_key = self.config.api_key or os.environ.get("KAGI_API_KEY", "")
return "kagi" if api_key else "duckduckgo" return "kagi" if api_key else "duckduckgo"
if provider == "olostep": if provider == "olostep":
return "olostep" api_key = self.config.api_key or os.environ.get("OLOSTEP_API_KEY", "")
return "olostep" if api_key else "duckduckgo"
return provider return provider
@property @property
@ -161,14 +152,6 @@ class WebSearchTool(Tool):
n = min(max(count or self.config.max_results, 1), 10) n = min(max(count or self.config.max_results, 1), 10)
if provider == "olostep": if provider == "olostep":
if not self.olostep_api_key:
return (
"Error: Olostep API key not configured. "
"Set it in ~/.nanobot/config.json under "
"tools.web.search.olostepApiKey and restart."
)
if not _OLOSTEP_AVAILABLE:
return "Error: olostep package not installed. Run: pip install olostep"
return await self._search_olostep(query, n) return await self._search_olostep(query, n)
if provider == "duckduckgo": if provider == "duckduckgo":
return await self._search_duckduckgo(query, n) return await self._search_duckduckgo(query, n)
@ -188,17 +171,31 @@ class WebSearchTool(Tool):
async def _search_olostep(self, query: str, n: int) -> str: async def _search_olostep(self, query: str, n: int) -> str:
if AsyncOlostep is None: if AsyncOlostep is None:
return "Error: olostep package not installed. Run: pip install olostep" return "Error: olostep package not installed. Run: pip install olostep"
api_key = self.config.api_key or os.environ.get("OLOSTEP_API_KEY", "")
if not api_key:
logger.warning("OLOSTEP_API_KEY not set, falling back to DuckDuckGo")
return await self._search_duckduckgo(query, n)
try: try:
async with AsyncOlostep(api_key=self.olostep_api_key) as client: async with AsyncOlostep(api_key=api_key) as client:
if self.proxy:
transport = getattr(client, "_transport", None)
http_client = getattr(transport, "_client", None)
if transport is not None and isinstance(http_client, httpx.AsyncClient):
await http_client.aclose()
transport._client = httpx.AsyncClient( # type: ignore[attr-defined]
proxy=self.proxy,
headers=dict(http_client.headers),
timeout=http_client.timeout,
limits=httpx.Limits(
max_keepalive_connections=100,
max_connections=200,
),
http2=True,
)
result = await client.answers.create(task=query) result = await client.answers.create(task=query)
answer_text = (getattr(result, "answer", "") or "").strip()
lines = [f"Answer: {answer_text}"] if answer_text else ["Answer:"]
sources = getattr(result, "sources", None) or [] sources = getattr(result, "sources", None) or []
if sources: source_lines = []
lines.append("")
lines.append("Sources:")
for i, source in enumerate(sources[:n], 1): for i, source in enumerate(sources[:n], 1):
if isinstance(source, dict): if isinstance(source, dict):
title = source.get("title", "") title = source.get("title", "")
@ -207,12 +204,15 @@ class WebSearchTool(Tool):
title = getattr(source, "title", "") title = getattr(source, "title", "")
url = getattr(source, "url", "") url = getattr(source, "url", "")
if title and url: if title and url:
lines.append(f"{i}. {title}{url}") source_lines.append(f"{i}. {title}{url}")
elif url: elif url:
lines.append(f"{i}. {url}") source_lines.append(f"{i}. {url}")
elif title: elif title:
lines.append(f"{i}. {title}") source_lines.append(f"{i}. {title}")
return "\n".join(lines)
answer_text = getattr(result, "answer", "") or ""
items = [{"title": answer_text or "Olostep answer", "url": "", "content": "\n".join(source_lines)}]
return _format_results(query, items, n)
except Olostep_BaseError as e: except Olostep_BaseError as e:
return f"Olostep search error: {type(e).__name__}: {e}" return f"Olostep search error: {type(e).__name__}: {e}"
except Exception as e: except Exception as e:

View File

@ -178,7 +178,6 @@ class WebSearchConfig(Base):
provider: str = "brave" # brave, tavily, duckduckgo, searxng, jina, kagi, olostep provider: str = "brave" # brave, tavily, duckduckgo, searxng, jina, kagi, olostep
api_key: str = "" api_key: str = ""
olostep_api_key: str | None = None
base_url: str = "" # SearXNG base URL base_url: str = "" # SearXNG base URL
max_results: int = 5 max_results: int = 5
timeout: int = 30 # Wall-clock timeout (seconds) for search operations timeout: int = 30 # Wall-clock timeout (seconds) for search operations

View File

@ -30,7 +30,6 @@ dependencies = [
"websocket-client>=1.9.0,<2.0.0", "websocket-client>=1.9.0,<2.0.0",
"httpx>=0.28.0,<1.0.0", "httpx>=0.28.0,<1.0.0",
"ddgs>=9.5.5,<10.0.0", "ddgs>=9.5.5,<10.0.0",
"olostep>=0.1.0",
"oauth-cli-kit>=0.1.3,<1.0.0", "oauth-cli-kit>=0.1.3,<1.0.0",
"loguru>=0.7.3,<1.0.0", "loguru>=0.7.3,<1.0.0",
"readability-lxml>=0.8.4,<1.0.0", "readability-lxml>=0.8.4,<1.0.0",

View File

@ -1,16 +1,17 @@
"""Tests for Olostep web search provider.""" """Tests for Olostep web search provider."""
from types import SimpleNamespace from __future__ import annotations
import pytest import asyncio
from types import SimpleNamespace
from unittest.mock import patch
import nanobot.agent.tools.web as web_mod import nanobot.agent.tools.web as web_mod
from nanobot.agent.tools.web import WebSearchTool from nanobot.agent.tools.web import WebSearchTool
from nanobot.config.schema import WebSearchConfig from nanobot.config.schema import WebSearchConfig
@pytest.mark.asyncio def test_olostep_search_formats_answer_and_sources():
async def test_olostep_search_formats_answer_and_sources(monkeypatch):
calls: dict[str, str] = {} calls: dict[str, str] = {}
class MockAsyncOlostep: class MockAsyncOlostep:
@ -31,11 +32,9 @@ async def test_olostep_search_formats_answer_and_sources(monkeypatch):
sources=[SimpleNamespace(title="Example Source", url="https://example.com")], sources=[SimpleNamespace(title="Example Source", url="https://example.com")],
) )
monkeypatch.setattr(web_mod, "_OLOSTEP_AVAILABLE", True) with patch.object(web_mod, "AsyncOlostep", MockAsyncOlostep):
monkeypatch.setattr(web_mod, "AsyncOlostep", MockAsyncOlostep) tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key="olostep-key"))
result = asyncio.run(tool.execute(query="test query"))
tool = WebSearchTool(config=WebSearchConfig(provider="olostep", olostep_api_key="olostep-key"))
result = await tool.execute(query="test query")
assert calls["api_key"] == "olostep-key" assert calls["api_key"] == "olostep-key"
assert calls["task"] == "test query" assert calls["task"] == "test query"
@ -44,26 +43,24 @@ async def test_olostep_search_formats_answer_and_sources(monkeypatch):
assert "https://example.com" in result assert "https://example.com" in result
@pytest.mark.asyncio def test_olostep_missing_key_falls_back_to_duckduckgo():
async def test_olostep_missing_key_returns_config_error(monkeypatch): class MockDDGS:
monkeypatch.delenv("OLOSTEP_API_KEY", raising=False) def __init__(self, **kw):
pass
tool = WebSearchTool(config=WebSearchConfig(provider="olostep", olostep_api_key="")) def text(self, query, max_results=5):
result = await tool.execute(query="test query") return [{"title": "Fallback", "href": "https://ddg.example", "body": "fallback"}]
assert ( with patch.dict(web_mod.os.environ, {}, clear=False), patch("ddgs.DDGS", MockDDGS):
result tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key=""))
== "Error: Olostep API key not configured. " result = asyncio.run(tool.execute(query="test query"))
"Set it in ~/.nanobot/config.json under "
"tools.web.search.olostepApiKey and restart." assert "Fallback" in result
)
@pytest.mark.asyncio def test_olostep_package_missing_returns_install_hint():
async def test_olostep_package_missing_returns_install_hint(monkeypatch): with patch.object(web_mod, "AsyncOlostep", None):
monkeypatch.setattr(web_mod, "_OLOSTEP_AVAILABLE", False) tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key="olostep-key"))
result = asyncio.run(tool.execute(query="test query"))
tool = WebSearchTool(config=WebSearchConfig(provider="olostep", olostep_api_key="olostep-key"))
result = await tool.execute(query="test query")
assert result == "Error: olostep package not installed. Run: pip install olostep" assert result == "Error: olostep package not installed. Run: pip install olostep"