feat(webui): add BYOK web search settings
Let WebUI users configure the single web search provider credential from BYOK while keeping saved secrets masked and hot-reloaded for new searches. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Cursor
parent
7c1aa5ae31
commit
56eee06736
@@ -524,6 +524,8 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
config = Config()
|
||||
config.agents.defaults.model = "openai/gpt-4o"
|
||||
config.providers.openai.api_key = "secret-key"
|
||||
config.tools.web.search.provider = "brave"
|
||||
config.tools.web.search.api_key = "brave-secret"
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
@@ -547,7 +549,13 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
assert providers["openai"]["api_key_hint"] == "secr••••-key"
|
||||
assert providers["openrouter"]["configured"] is False
|
||||
assert body["agent"]["has_api_key"] is True
|
||||
assert body["web_search"]["provider"] == "brave"
|
||||
assert body["web_search"]["api_key_hint"] == "brav••••cret"
|
||||
search_providers = {provider["name"]: provider for provider in body["web_search"]["providers"]}
|
||||
assert search_providers["duckduckgo"]["credential"] == "none"
|
||||
assert search_providers["searxng"]["credential"] == "base_url"
|
||||
assert "secret-key" not in settings.text
|
||||
assert "brave-secret" not in settings.text
|
||||
|
||||
provider_updated = await _http_get(
|
||||
"http://127.0.0.1:"
|
||||
@@ -571,11 +579,27 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
assert updated.status_code == 200
|
||||
assert updated.json()["requires_restart"] is False
|
||||
|
||||
search_updated = await _http_get(
|
||||
"http://127.0.0.1:"
|
||||
f"{port}/api/settings/web-search/update?provider=searxng"
|
||||
"&base_url=https%3A%2F%2Fsearch.example.com",
|
||||
headers={"Authorization": "Bearer tok"},
|
||||
)
|
||||
assert search_updated.status_code == 200
|
||||
search_body = search_updated.json()
|
||||
assert search_body["requires_restart"] is False
|
||||
assert search_body["web_search"]["provider"] == "searxng"
|
||||
assert search_body["web_search"]["api_key_hint"] is None
|
||||
assert search_body["web_search"]["base_url"] == "https://search.example.com"
|
||||
|
||||
saved = load_config(config_path)
|
||||
assert saved.agents.defaults.model == "openrouter/test"
|
||||
assert saved.agents.defaults.provider == "openrouter"
|
||||
assert saved.providers.openrouter.api_key == "sk-or-test"
|
||||
assert saved.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
||||
assert saved.tools.web.search.provider == "searxng"
|
||||
assert saved.tools.web.search.api_key == ""
|
||||
assert saved.tools.web.search.base_url == "https://search.example.com"
|
||||
finally:
|
||||
await channel.stop()
|
||||
await server_task
|
||||
|
||||
@@ -13,7 +13,24 @@ import pytest
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.agent.subagent import SubagentManager, SubagentStatus
|
||||
from nanobot.agent.tools.search import GlobTool, GrepTool
|
||||
from nanobot.agent.tools.web import WebSearchTool
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.config.schema import WebSearchConfig
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_search_tool_refreshes_dynamic_config_loader(monkeypatch) -> None:
|
||||
tool = WebSearchTool(
|
||||
config=WebSearchConfig(provider="brave"),
|
||||
config_loader=lambda: WebSearchConfig(provider="duckduckgo", max_results=3),
|
||||
)
|
||||
|
||||
async def fake_duckduckgo(self, query: str, n: int) -> str:
|
||||
return f"{self.config.provider}:{query}:{n}"
|
||||
|
||||
monkeypatch.setattr(WebSearchTool, "_search_duckduckgo", fake_duckduckgo)
|
||||
|
||||
assert await tool.execute("nanobot") == "duckduckgo:nanobot:3"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -185,7 +202,7 @@ async def test_grep_files_with_matches_supports_head_limit_and_offset(tmp_path:
|
||||
# 2. The pagination info is correct
|
||||
assert "pagination: limit=1, offset=1" in result
|
||||
# Count non-empty lines that start with src/ (file paths)
|
||||
file_lines = [l for l in result.splitlines() if l.startswith("src/")]
|
||||
file_lines = [line for line in result.splitlines() if line.startswith("src/")]
|
||||
assert len(file_lines) == 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user