Add Exa web search provider

This commit is contained in:
erikmackinnon
2026-06-10 15:02:07 +08:00
committed by Xubin Ren
parent 5d91d59cf7
commit 31bfec58d0
4 changed files with 139 additions and 0 deletions
+1
View File
@@ -1699,6 +1699,7 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
assert body["web"]["fetch"]["use_jina_reader"] is True
search_providers = {provider["name"]: provider for provider in body["web_search"]["providers"]}
assert search_providers["duckduckgo"]["credential"] == "none"
assert search_providers["exa"]["credential"] == "api_key"
assert search_providers["volcengine"]["credential"] == "api_key"
assert search_providers["searxng"]["credential"] == "base_url"
assert body["image_generation"]["enabled"] is False
+82
View File
@@ -291,6 +291,71 @@ async def test_kagi_search(monkeypatch):
assert "ignored related search" not in result
@pytest.mark.asyncio
async def test_exa_search(monkeypatch):
async def mock_post(self, url, **kw):
assert url == "https://api.exa.ai/search"
assert kw["headers"]["x-api-key"] == "exa-key"
assert kw["headers"]["User-Agent"] == "nanobot-search-test"
assert kw["json"] == {
"query": "test",
"numResults": 2,
"contents": {"highlights": True},
}
return _response(json={
"results": [
{
"title": "Exa Result",
"url": "https://exa.ai",
"highlights": ["Relevant Exa highlight"],
}
]
})
monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)
tool = _tool(provider="exa", api_key="exa-key", user_agent="nanobot-search-test")
result = await tool.execute(query="test", count=2)
assert "Exa Result" in result
assert "https://exa.ai" in result
assert "Relevant Exa highlight" in result
@pytest.mark.asyncio
async def test_exa_search_uses_env_api_key(monkeypatch):
async def mock_post(self, url, **kw):
assert kw["headers"]["x-api-key"] == "env-exa-key"
return _response(json={
"results": [
{
"title": "Env Exa Result",
"url": "https://exa.ai/env",
"summary": "Summary fallback",
}
]
})
monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)
monkeypatch.setenv("EXA_API_KEY", "env-exa-key")
tool = _tool(provider="exa", api_key="")
result = await tool.execute(query="test", count=1)
assert "Env Exa Result" in result
assert "Summary fallback" in result
@pytest.mark.asyncio
async def test_exa_search_http_error(monkeypatch):
async def mock_post(self, url, **kw):
return _response(status=401, json={"error": "invalid key"})
monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)
tool = _tool(provider="exa", api_key="bad-exa-key")
result = await tool.execute(query="test")
assert "Error: Exa search failed (401)" in result
@pytest.mark.asyncio
async def test_unknown_provider():
tool = _tool(provider="unknown")
@@ -377,6 +442,23 @@ async def test_kagi_fallback_to_duckduckgo_when_no_key(monkeypatch):
assert "Fallback" in result
@pytest.mark.asyncio
async def test_exa_fallback_to_duckduckgo_when_no_key(monkeypatch):
class MockDDGS:
def __init__(self, **kw):
pass
def text(self, query, max_results=5):
return [{"title": "Fallback", "href": "https://ddg.example", "body": "DuckDuckGo fallback"}]
monkeypatch.setattr("ddgs.DDGS", MockDDGS)
monkeypatch.delenv("EXA_API_KEY", raising=False)
tool = _tool(provider="exa", api_key="")
result = await tool.execute(query="test")
assert "Fallback" in result
@pytest.mark.asyncio
async def test_jina_search_uses_path_encoded_query(monkeypatch):
calls = {}