mirror of
https://github.com/andrewyng/openworker.git
synced 2026-08-30 22:53:41 +00:00
Merge eab9ab47d49fb83e1a6b29276ef9fe132934bdcb into 9e145d9ceb6268b6957fef6aa1889cb99e9f35be
This commit is contained in:
commit
fca62643c5
@ -54,7 +54,8 @@ class Config:
|
||||
auto_approve_shadow: bool = False
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8765
|
||||
# Web search provider: "duckduckgo" (keyless default) | "tavily" | "brave" (need a key).
|
||||
# Web search provider: "duckduckgo" (keyless default) | "tavily" | "brave" | "exa"
|
||||
# (the last three need a key).
|
||||
web_search_provider: str = "duckduckgo"
|
||||
# OpenWorker Cloud (sign-in + managed connectors). Config, never constants:
|
||||
# dev/staging/BYO-VPC deployments point these at their own instances.
|
||||
|
||||
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from .providers import (
|
||||
BraveProvider,
|
||||
DuckDuckGoProvider,
|
||||
ExaProvider,
|
||||
SearchResult,
|
||||
TavilyProvider,
|
||||
WebSearchProvider,
|
||||
@ -20,6 +21,7 @@ __all__ = [
|
||||
"DuckDuckGoProvider",
|
||||
"TavilyProvider",
|
||||
"BraveProvider",
|
||||
"ExaProvider",
|
||||
"build_provider",
|
||||
"provider_names",
|
||||
"make_web_search_tool",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Web search providers — a keyless default + pluggable third-party services.
|
||||
|
||||
`duckduckgo` works with no API key (our "starting version of our own"). `tavily` and `brave`
|
||||
give better results but need a key (configured via the SecretStore / env). All providers
|
||||
`duckduckgo` works with no API key (our "starting version of our own"). `tavily`, `brave` and
|
||||
`exa` give better results but need a key (configured via the SecretStore / env). All providers
|
||||
return a uniform `list[SearchResult]`; the heavy client libs are lazy-imported.
|
||||
"""
|
||||
|
||||
@ -108,10 +108,47 @@ class BraveProvider(WebSearchProvider):
|
||||
]
|
||||
|
||||
|
||||
class ExaProvider(WebSearchProvider):
|
||||
name = "exa"
|
||||
requires_key = True
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self.api_key = api_key
|
||||
|
||||
def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
|
||||
import httpx
|
||||
|
||||
resp = httpx.post(
|
||||
"https://api.exa.ai/search",
|
||||
headers={
|
||||
"x-api-key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"x-exa-integration": "andrewyng/openworker-integration",
|
||||
},
|
||||
json={
|
||||
"query": query,
|
||||
"type": "auto",
|
||||
"numResults": max_results,
|
||||
"contents": {"highlights": True},
|
||||
},
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data = resp.json()
|
||||
return [
|
||||
SearchResult(
|
||||
title=r.get("title") or "",
|
||||
url=r.get("url") or "",
|
||||
snippet=" ".join(r.get("highlights") or []),
|
||||
)
|
||||
for r in data.get("results", [])
|
||||
]
|
||||
|
||||
|
||||
_PROVIDERS = {
|
||||
"duckduckgo": DuckDuckGoProvider,
|
||||
"tavily": TavilyProvider,
|
||||
"brave": BraveProvider,
|
||||
"exa": ExaProvider,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Tests for web search — provider abstraction, the tool, and config resolution.
|
||||
|
||||
No network: a FakeProvider is injected; third-party key handling and the REST config path
|
||||
are exercised without hitting DuckDuckGo/Tavily/Brave.
|
||||
are exercised without hitting DuckDuckGo/Tavily/Brave/Exa.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -18,6 +18,7 @@ from coworker.web import (
|
||||
from coworker.web.providers import (
|
||||
BraveProvider,
|
||||
DuckDuckGoProvider,
|
||||
ExaProvider,
|
||||
TavilyProvider,
|
||||
WebSearchProvider,
|
||||
)
|
||||
@ -79,6 +80,48 @@ def test_build_provider_third_party_requires_key():
|
||||
build_provider("tavily") # no key
|
||||
assert isinstance(build_provider("tavily", "tvly-x"), TavilyProvider)
|
||||
assert isinstance(build_provider("brave", "brv-x"), BraveProvider)
|
||||
with pytest.raises(ValueError):
|
||||
build_provider("exa") # no key
|
||||
assert isinstance(build_provider("exa", "exa-x"), ExaProvider)
|
||||
|
||||
|
||||
def test_exa_provider_parses_highlights(monkeypatch):
|
||||
import httpx
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, headers=None, json=None, timeout=None):
|
||||
captured.update(url=url, headers=headers, json=json)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"title": "Exa",
|
||||
"url": "https://exa.ai",
|
||||
"highlights": ["neural search", "for AI"],
|
||||
},
|
||||
{"title": None, "url": "https://exa.ai/docs"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(httpx, "post", fake_post)
|
||||
results = ExaProvider("exa-x").search("what is exa", max_results=2)
|
||||
|
||||
assert captured["url"] == "https://api.exa.ai/search"
|
||||
assert captured["headers"]["x-api-key"] == "exa-x"
|
||||
assert captured["headers"]["x-exa-integration"] == "andrewyng/openworker-integration"
|
||||
assert captured["json"]["numResults"] == 2
|
||||
assert captured["json"]["contents"] == {"highlights": True}
|
||||
assert [r.to_dict() for r in results] == [
|
||||
{
|
||||
"title": "Exa",
|
||||
"url": "https://exa.ai",
|
||||
"snippet": "neural search for AI",
|
||||
},
|
||||
{"title": "", "url": "https://exa.ai/docs", "snippet": ""},
|
||||
]
|
||||
|
||||
|
||||
def test_tool_surfaces_missing_key_error(tmp_path):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user