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
+55
View File
@@ -300,6 +300,9 @@ class WebSearchTool(Tool):
if provider == "kagi":
api_key = self.config.api_key or os.environ.get("KAGI_API_KEY", "")
return "kagi" if api_key else "duckduckgo"
if provider == "exa":
api_key = self.config.api_key or os.environ.get("EXA_API_KEY", "")
return "exa" if api_key else "duckduckgo"
if provider == "olostep":
api_key = self.config.api_key or os.environ.get("OLOSTEP_API_KEY", "")
return "olostep" if api_key else "duckduckgo"
@@ -356,6 +359,8 @@ class WebSearchTool(Tool):
return await self._search_brave(query, n)
elif provider == "kagi":
return await self._search_kagi(query, n)
elif provider == "exa":
return await self._search_exa(query, n)
else:
return f"Error: unknown search provider '{provider}'"
@@ -542,6 +547,56 @@ class WebSearchTool(Tool):
except Exception as e:
return f"Error: {e}"
async def _search_exa(self, query: str, n: int) -> str:
api_key = self.config.api_key or os.environ.get("EXA_API_KEY", "")
if not api_key:
logger.warning("EXA_API_KEY not set, falling back to DuckDuckGo")
return await self._search_duckduckgo(query, n)
try:
headers = {
"Content-Type": "application/json",
"x-api-key": api_key,
"User-Agent": self.user_agent,
}
body = {
"query": query,
"numResults": n,
"contents": {"highlights": True},
}
async with httpx.AsyncClient(proxy=self.proxy) as client:
r = await client.post(
"https://api.exa.ai/search",
headers=headers,
json=body,
timeout=float(self.config.timeout),
)
r.raise_for_status()
items = []
for result in r.json().get("results", []):
if not isinstance(result, dict):
continue
highlights = result.get("highlights") or []
if isinstance(highlights, list):
content = "\n".join(str(highlight) for highlight in highlights if highlight)
else:
content = str(highlights)
if not content:
content = str(result.get("summary") or result.get("text") or "")[:500]
items.append(
{
"title": result.get("title", ""),
"url": result.get("url", ""),
"content": content,
}
)
return _format_results(query, items, n)
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
return "Error: Exa search rate limited. Try again later or reduce search frequency."
return f"Error: Exa search failed ({e.response.status_code}): {e}"
except Exception as e:
return f"Error: Exa search failed: {e}"
async def _search_volcengine(
self,
query: str,
+1
View File
@@ -78,6 +78,7 @@ _WEB_SEARCH_PROVIDER_OPTIONS: tuple[dict[str, str], ...] = (
{"name": "searxng", "label": "SearXNG", "credential": "base_url"},
{"name": "jina", "label": "Jina", "credential": "api_key"},
{"name": "kagi", "label": "Kagi", "credential": "api_key"},
{"name": "exa", "label": "Exa", "credential": "api_key"},
{"name": "olostep", "label": "Olostep", "credential": "api_key"},
{"name": "volcengine", "label": "Volcengine Search", "credential": "api_key"},
)