mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-22 12:36:26 +00:00
fix(tavily): use web_fetch credentials for extraction (#5496)
* fix(tavily): use fetch tool credentials for extraction * test: register scoped Tavily agent guidance --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
53798b44cd
commit
7f68fa2881
@ -1075,6 +1075,11 @@ Tools follow the same philosophy. DeerFlow comes with a core toolset — web sea
|
|||||||
|
|
||||||
When using Tavily for `web_fetch`, extracted pages without a title use their URL
|
When using Tavily for `web_fetch`, extracted pages without a title use their URL
|
||||||
as the heading; their content remains available to the agent.
|
as the heading; their content remains available to the agent.
|
||||||
|
Tavily search and fetch each read `api_key` from their own tool entry in
|
||||||
|
`config.yaml`, falling back to `TAVILY_API_KEY` when omitted. Fetch does not
|
||||||
|
reuse the search entry's key, so search can use a different provider. If you
|
||||||
|
previously configured a shared Tavily key only under `web_search`, also set it
|
||||||
|
under `web_fetch` or use `TAVILY_API_KEY` for both.
|
||||||
|
|
||||||
Advanced deployments can enable pluggable authorization with `authorization.enabled` in `config.yaml`. A configured `AuthorizationProvider` filters denied tools before they reach the model or deferred-tool catalog, then the same provider is checked again before every business-tool execution through the existing guardrail middleware. Gateway `threads:*` and `runs:*` route permissions are derived from the same provider, while existing owner checks and admin-only management gates remain in force. Every HTTP route that starts or enables a future Agent run requires `runs:create`: this includes the stateless `POST /api/runs/stream` and `POST /api/runs/wait` endpoints plus scheduled-task create, update, resume, and manual-trigger mutations. Scheduled-task mutations retain their existing `threads:write` requirement, and the stateless routes separately enforce ownership when the optional thread ID is supplied in the request body. A generated `tool_search` may bypass the second tool check only when it fronts the current build's already-filtered deferred catalog. Model access follows the same provider: the Gateway `models` list is filtered per principal, `model:use` is enforced on model detail requests and again when the runtime resolves the agent's model, and a denied default model falls back to the first remaining candidate that also passes `model:use`. The built-in RBAC provider supports per-role `tools`, `routes`, `models`, `skills`, and `sandbox` allow/deny policies and validates that `default_role` names a configured role; authorization is disabled by default. See `config.example.yaml` and the [authorization RFC](docs/plans/2026-07-10-pluggable-authorization-rfc.md).
|
Advanced deployments can enable pluggable authorization with `authorization.enabled` in `config.yaml`. A configured `AuthorizationProvider` filters denied tools before they reach the model or deferred-tool catalog, then the same provider is checked again before every business-tool execution through the existing guardrail middleware. Gateway `threads:*` and `runs:*` route permissions are derived from the same provider, while existing owner checks and admin-only management gates remain in force. Every HTTP route that starts or enables a future Agent run requires `runs:create`: this includes the stateless `POST /api/runs/stream` and `POST /api/runs/wait` endpoints plus scheduled-task create, update, resume, and manual-trigger mutations. Scheduled-task mutations retain their existing `threads:write` requirement, and the stateless routes separately enforce ownership when the optional thread ID is supplied in the request body. A generated `tool_search` may bypass the second tool check only when it fronts the current build's already-filtered deferred catalog. Model access follows the same provider: the Gateway `models` list is filtered per principal, `model:use` is enforced on model detail requests and again when the runtime resolves the agent's model, and a denied default model falls back to the first remaining candidate that also passes `model:use`. The built-in RBAC provider supports per-role `tools`, `routes`, `models`, `skills`, and `sandbox` allow/deny policies and validates that `default_role` names a configured role; authorization is disabled by default. See `config.example.yaml` and the [authorization RFC](docs/plans/2026-07-10-pluggable-authorization-rfc.md).
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
# Tavily tools
|
||||||
|
|
||||||
|
The client helper selects credentials by tool name: search defaults to
|
||||||
|
`web_search`, fetch passes `web_fetch`, and an omitted key uses the SDK's
|
||||||
|
`TAVILY_API_KEY` fallback. Keep credential regressions in `backend/tests/test_tavily_tools.py`
|
||||||
|
on the real helper and SDK constructor, mocking only search/extract calls.
|
||||||
@ -7,8 +7,8 @@ from deerflow.community.search_time_range import SearchTimeRange
|
|||||||
from deerflow.config import get_app_config
|
from deerflow.config import get_app_config
|
||||||
|
|
||||||
|
|
||||||
def _get_tavily_client() -> TavilyClient:
|
def _get_tavily_client(tool_name: str = "web_search") -> TavilyClient:
|
||||||
config = get_app_config().get_tool_config("web_search")
|
config = get_app_config().get_tool_config(tool_name)
|
||||||
api_key = None
|
api_key = None
|
||||||
if config is not None and "api_key" in config.model_extra:
|
if config is not None and "api_key" in config.model_extra:
|
||||||
api_key = config.model_extra.get("api_key")
|
api_key = config.model_extra.get("api_key")
|
||||||
@ -56,7 +56,7 @@ def web_fetch_tool(url: str) -> str:
|
|||||||
Args:
|
Args:
|
||||||
url: The URL to fetch the contents of.
|
url: The URL to fetch the contents of.
|
||||||
"""
|
"""
|
||||||
client = _get_tavily_client()
|
client = _get_tavily_client("web_fetch")
|
||||||
res = client.extract([url])
|
res = client.extract([url])
|
||||||
if "failed_results" in res and len(res["failed_results"]) > 0:
|
if "failed_results" in res and len(res["failed_results"]) > 0:
|
||||||
return f"Error: {res['failed_results'][0]['error']}"
|
return f"Error: {res['failed_results'][0]['error']}"
|
||||||
|
|||||||
@ -18,6 +18,7 @@ EXPECTED_GUIDANCE_PATHS = {
|
|||||||
"backend/packages/harness/deerflow/agents/AGENTS.md",
|
"backend/packages/harness/deerflow/agents/AGENTS.md",
|
||||||
"backend/packages/harness/deerflow/agents/middlewares/AGENTS.md",
|
"backend/packages/harness/deerflow/agents/middlewares/AGENTS.md",
|
||||||
"backend/packages/harness/deerflow/agents/memory/AGENTS.md",
|
"backend/packages/harness/deerflow/agents/memory/AGENTS.md",
|
||||||
|
"backend/packages/harness/deerflow/community/tavily/AGENTS.md",
|
||||||
"backend/packages/harness/deerflow/config/AGENTS.md",
|
"backend/packages/harness/deerflow/config/AGENTS.md",
|
||||||
"backend/packages/harness/deerflow/extensions/AGENTS.md",
|
"backend/packages/harness/deerflow/extensions/AGENTS.md",
|
||||||
"backend/packages/harness/deerflow/runtime/AGENTS.md",
|
"backend/packages/harness/deerflow/runtime/AGENTS.md",
|
||||||
|
|||||||
@ -4,8 +4,59 @@ import json
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tavily import TavilyClient
|
||||||
|
|
||||||
from deerflow.community.tavily.tools import web_fetch_tool, web_search_tool
|
from deerflow.community.tavily.tools import web_fetch_tool, web_search_tool
|
||||||
|
from deerflow.config.tool_config import ToolConfig
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("search_provider", "fetch_key", "expected_key"),
|
||||||
|
[
|
||||||
|
("serper", "fetch-key", "fetch-key"),
|
||||||
|
(None, "fetch-key", "fetch-key"),
|
||||||
|
("tavily", "fetch-key", "fetch-key"),
|
||||||
|
("serper", None, "env-key"),
|
||||||
|
("tavily", None, "env-key"),
|
||||||
|
(None, None, "env-key"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_web_fetch_uses_own_credentials(monkeypatch, search_provider, fetch_key, expected_key) -> None:
|
||||||
|
monkeypatch.setenv("TAVILY_API_KEY", "env-key")
|
||||||
|
fetch_config = ToolConfig(name="web_fetch", group="web", use="deerflow.community.tavily.tools:web_fetch_tool", **({"api_key": fetch_key} if fetch_key else {}))
|
||||||
|
configs = {"web_fetch": fetch_config}
|
||||||
|
if search_provider:
|
||||||
|
configs["web_search"] = ToolConfig(name="web_search", group="web", use=f"deerflow.community.{search_provider}.tools:web_search_tool", api_key="search-key")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("deerflow.community.tavily.tools.get_app_config") as mock_config,
|
||||||
|
patch.object(TavilyClient, "extract", autospec=True, return_value={"results": []}) as extract,
|
||||||
|
):
|
||||||
|
mock_config.return_value.get_tool_config.side_effect = configs.get
|
||||||
|
web_fetch_tool.invoke({"url": "https://example.com/report"})
|
||||||
|
|
||||||
|
client, urls = extract.call_args.args
|
||||||
|
assert client.api_key == expected_key
|
||||||
|
assert urls == ["https://example.com/report"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("search_key", ["search-key", None])
|
||||||
|
def test_web_search_preserves_own_credentials(monkeypatch, search_key) -> None:
|
||||||
|
monkeypatch.setenv("TAVILY_API_KEY", "env-key")
|
||||||
|
configs = {
|
||||||
|
"web_search": ToolConfig(name="web_search", group="web", use="deerflow.community.tavily.tools:web_search_tool", api_key=search_key),
|
||||||
|
"web_fetch": ToolConfig(name="web_fetch", group="web", use="deerflow.community.tavily.tools:web_fetch_tool", api_key="fetch-key"),
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
patch("deerflow.community.tavily.tools.get_app_config") as mock_config,
|
||||||
|
patch.object(TavilyClient, "search", autospec=True, return_value={"results": []}) as search,
|
||||||
|
):
|
||||||
|
mock_config.return_value.get_tool_config.side_effect = configs.get
|
||||||
|
web_search_tool.invoke({"query": "documentation"})
|
||||||
|
|
||||||
|
client, query = search.call_args.args
|
||||||
|
assert client.api_key == (search_key or "env-key")
|
||||||
|
assert query == "documentation"
|
||||||
|
|
||||||
|
|
||||||
def _tavily_response() -> dict:
|
def _tavily_response() -> dict:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user