diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index 22dff91bd..1f64f6d46 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -19,15 +19,12 @@ from nanobot.utils.helpers import build_image_content_blocks try: from olostep import AsyncOlostep, Olostep_BaseError - - _OLOSTEP_AVAILABLE = True except ImportError: AsyncOlostep = None class Olostep_BaseError(Exception): """Fallback error type when olostep package is unavailable.""" - - _OLOSTEP_AVAILABLE = False + pass if TYPE_CHECKING: from nanobot.config.schema import WebSearchConfig @@ -111,11 +108,10 @@ class WebSearchTool(Tool): self.config = config if config is not None else WebSearchConfig() self.proxy = proxy - self.provider = (self.config.provider or "brave").strip().lower() def _effective_provider(self) -> str: """Resolve the backend that execute() will actually use.""" - provider = self.provider or "brave" + provider = (self.config.provider or "brave").strip().lower() if provider == "duckduckgo": return "duckduckgo" if provider == "brave": @@ -148,7 +144,7 @@ class WebSearchTool(Tool): return self._effective_provider() == "duckduckgo" async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str: - provider = self.provider or "brave" + provider = (self.config.provider or "brave").strip().lower() n = min(max(count or self.config.max_results, 1), 10) if provider == "olostep": diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index cca842f2b..6039245fb 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -176,7 +176,7 @@ class GatewayConfig(Base): class WebSearchConfig(Base): """Web search tool configuration.""" - provider: str = "brave" # brave, tavily, duckduckgo, searxng, jina, kagi, olostep + provider: str = "duckduckgo" # brave, tavily, duckduckgo, searxng, jina, kagi, olostep api_key: str = "" base_url: str = "" # SearXNG base URL max_results: int = 5 diff --git a/pyproject.toml b/pyproject.toml index 1e4ca97df..0ac2c775d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,9 @@ langsmith = [ pdf = [ "pymupdf>=1.25.0", ] +olostep = [ + "olostep>=0.1.0", +] dev = [ "pytest>=9.0.0,<10.0.0", "pytest-asyncio>=1.3.0,<2.0.0", diff --git a/tests/test_web_search_olostep.py b/tests/test_web_search_olostep.py deleted file mode 100644 index f68ca58ad..000000000 --- a/tests/test_web_search_olostep.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Tests for Olostep web search provider.""" - -from __future__ import annotations - -import asyncio -from types import SimpleNamespace -from unittest.mock import patch - -import nanobot.agent.tools.web as web_mod -from nanobot.agent.tools.web import WebSearchTool -from nanobot.config.schema import WebSearchConfig - - -def test_olostep_search_formats_answer_and_sources(): - calls: dict[str, str] = {} - - class MockAsyncOlostep: - def __init__(self, api_key: str): - calls["api_key"] = api_key - self.answers = self - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc, tb): - return None - - async def create(self, task: str): - calls["task"] = task - return SimpleNamespace( - answer="Mocked Olostep answer", - sources=[SimpleNamespace(title="Example Source", url="https://example.com")], - ) - - with patch.object(web_mod, "AsyncOlostep", MockAsyncOlostep): - tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key="olostep-key")) - result = asyncio.run(tool.execute(query="test query")) - - assert calls["api_key"] == "olostep-key" - assert calls["task"] == "test query" - assert "Mocked Olostep answer" in result - assert "Example Source" in result - assert "https://example.com" in result - - -def test_olostep_missing_key_falls_back_to_duckduckgo(): - class MockDDGS: - def __init__(self, **kw): - pass - - def text(self, query, max_results=5): - return [{"title": "Fallback", "href": "https://ddg.example", "body": "fallback"}] - - with patch.dict(web_mod.os.environ, {}, clear=False), patch("ddgs.DDGS", MockDDGS): - tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key="")) - result = asyncio.run(tool.execute(query="test query")) - - assert "Fallback" in result - - -def test_olostep_package_missing_returns_install_hint(): - with patch.object(web_mod, "AsyncOlostep", None): - tool = WebSearchTool(config=WebSearchConfig(provider="olostep", api_key="olostep-key")) - result = asyncio.run(tool.execute(query="test query")) - - assert result == "Error: olostep package not installed. Run: pip install olostep" diff --git a/tests/tools/test_web_search_tool.py b/tests/tools/test_web_search_tool.py index a42e51e1a..83cdf6665 100644 --- a/tests/tools/test_web_search_tool.py +++ b/tests/tools/test_web_search_tool.py @@ -281,3 +281,72 @@ async def test_duckduckgo_timeout_returns_error(monkeypatch): result = await tool.execute(query="test") gate.set() assert "Error" in result + + +@pytest.mark.asyncio +async def test_olostep_search_formats_answer_and_sources(monkeypatch): + from types import SimpleNamespace + from unittest.mock import patch + import nanobot.agent.tools.web as web_mod + + calls: dict[str, str] = {} + + class MockAsyncOlostep: + def __init__(self, api_key: str): + calls["api_key"] = api_key + self.answers = self + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + async def create(self, task: str): + calls["task"] = task + return SimpleNamespace( + answer="Mocked Olostep answer", + sources=[SimpleNamespace(title="Example Source", url="https://example.com")], + ) + + with patch.object(web_mod, "AsyncOlostep", MockAsyncOlostep): + tool = _tool(provider="olostep", api_key="olostep-key") + result = await tool.execute(query="test query") + + assert calls["api_key"] == "olostep-key" + assert calls["task"] == "test query" + assert "Mocked Olostep answer" in result + assert "Example Source" in result + assert "https://example.com" in result + + +@pytest.mark.asyncio +async def test_olostep_missing_key_falls_back_to_duckduckgo(monkeypatch): + from unittest.mock import patch + import nanobot.agent.tools.web as web_mod + + class MockDDGS: + def __init__(self, **kw): + pass + + def text(self, query, max_results=5): + return [{"title": "Fallback", "href": "https://ddg.example", "body": "fallback"}] + + monkeypatch.delenv("OLOSTEP_API_KEY", raising=False) + with patch("ddgs.DDGS", MockDDGS): + tool = _tool(provider="olostep", api_key="") + result = await tool.execute(query="test query") + + assert "Fallback" in result + + +@pytest.mark.asyncio +async def test_olostep_package_missing_returns_install_hint(monkeypatch): + from unittest.mock import patch + import nanobot.agent.tools.web as web_mod + + with patch.object(web_mod, "AsyncOlostep", None): + tool = _tool(provider="olostep", api_key="olostep-key") + result = await tool.execute(query="test query") + + assert result == "Error: olostep package not installed. Run: pip install olostep"