This commit is contained in:
Rakshith Ramprakash
2026-08-29 17:04:28 -07:00
committed by GitHub
6 changed files with 81 additions and 6 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ 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" | "firecrawl" (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.
+2
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from .providers import (
BraveProvider,
DuckDuckGoProvider,
FirecrawlProvider,
SearchResult,
TavilyProvider,
WebSearchProvider,
@@ -20,6 +21,7 @@ __all__ = [
"DuckDuckGoProvider",
"TavilyProvider",
"BraveProvider",
"FirecrawlProvider",
"build_provider",
"provider_names",
"make_web_search_tool",
+33 -3
View File
@@ -1,8 +1,8 @@
"""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
return a uniform `list[SearchResult]`; the heavy client libs are lazy-imported.
`duckduckgo` works with no API key (our "starting version of our own"). `tavily`, `brave`,
and `firecrawl` 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.
"""
from __future__ import annotations
@@ -108,10 +108,40 @@ class BraveProvider(WebSearchProvider):
]
class FirecrawlProvider(WebSearchProvider):
"""Firecrawl web search (https://docs.firecrawl.dev) via the v2 REST API."""
name = "firecrawl"
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.firecrawl.dev/v2/search",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"query": query, "limit": max_results},
timeout=_TIMEOUT,
)
data = resp.json()
return [
SearchResult(
title=r.get("title", ""),
url=r.get("url", ""),
snippet=r.get("description", ""),
)
for r in (data.get("data", {}) or {}).get("web", [])
]
_PROVIDERS = {
"duckduckgo": DuckDuckGoProvider,
"tavily": TavilyProvider,
"brave": BraveProvider,
"firecrawl": FirecrawlProvider,
}
+6
View File
@@ -8,6 +8,12 @@ model = "gpt-5.5" # default model id (override per session in the UI/
mode = "interactive" # plan | interactive | auto | custom
max_iterations = 12 # max model<->tool iterations per turn before stopping
# Web search provider for the `web_search` tool.
# duckduckgo (keyless default) | tavily | brave | firecrawl
# The keyed providers read their key from the SecretStore (set in the UI) or a
# <PROVIDER>_API_KEY env var, e.g. FIRECRAWL_API_KEY. See https://docs.firecrawl.dev
web_search_provider = "duckduckgo"
# Commands auto-allowed without an approval prompt (prefix match).
allowed_commands = [
"ls", "cat", "pwd", "grep", "find",
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies = [
"mcp>=1.28.1,<2", # MCP client (stdio + streamable-http); floor >=1.28.1 avoids PYSEC-2026-3481/3482/3483 (fixed in 1.27.2/1.28.1); <2 since 2.0.0 removed streamablehttp_client
"httpx>=0.27", # sync outbound senders for messaging connectors (send_message tool)
"websockets>=13", # managed Slack relay client transport (relay_client.py)
"ddgs>=9", # keyless default web-search provider (DuckDuckGo); Tavily/Brave use httpx
"ddgs>=9", # keyless default web-search provider (DuckDuckGo); Tavily/Brave/Firecrawl use httpx
"croniter>=2", # cron next-fire math for the automation scheduler
# PDF attachments for models without native PDF support (pdf_support.py):
# pypdf = pure-python text extraction; pypdfium2 = page rasterization (BSD pdfium,
+38 -1
View File
@@ -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/Firecrawl.
"""
from __future__ import annotations
@@ -18,6 +18,7 @@ from coworker.web import (
from coworker.web.providers import (
BraveProvider,
DuckDuckGoProvider,
FirecrawlProvider,
TavilyProvider,
WebSearchProvider,
)
@@ -79,6 +80,42 @@ 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("firecrawl") # no key
assert isinstance(build_provider("firecrawl", "fc-x"), FirecrawlProvider)
def test_firecrawl_maps_v2_search_response(monkeypatch):
import httpx
captured = {}
class _Resp:
def json(self):
return {
"success": True,
"data": {
"web": [
{"title": "t0", "url": "https://x/0", "description": "d0"},
{"title": "t1", "url": "https://x/1", "description": "d1"},
]
},
}
def fake_post(url, headers=None, json=None, timeout=None):
captured.update(url=url, headers=headers, json=json)
return _Resp()
monkeypatch.setattr(httpx, "post", fake_post)
results = FirecrawlProvider("fc-key").search("claude api", max_results=2)
assert captured["url"] == "https://api.firecrawl.dev/v2/search"
assert captured["headers"]["Authorization"] == "Bearer fc-key"
assert captured["json"] == {"query": "claude api", "limit": 2}
assert [(r.title, r.url, r.snippet) for r in results] == [
("t0", "https://x/0", "d0"),
("t1", "https://x/1", "d1"),
]
def test_tool_surfaces_missing_key_error(tmp_path):