fix(cli): show search engines (incl. Keenable) in onboard wizard
The onboard wizard dispatched field handlers by bare field name, so WebSearchConfig.provider was hijacked by the LLM-provider handler and showed LLM providers instead of search engines. Keenable was also never wired into the CLI wizard when it landed in the WebUI. - Add a single source of truth for selectable search providers (SEARCH_PROVIDER_OPTIONS in web.py); WebUI settings now import it. - Add a WebSearchConfig-aware search-provider picker to the wizard and resolve handlers by (model type, field name) so the LLM and search provider fields no longer collide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude Opus 4.8
parent
638af123ba
commit
9354b80a6e
@@ -36,6 +36,24 @@ _VOLCENGINE_TIME_RANGES = {"OneDay", "OneWeek", "OneMonth", "OneYear"}
|
|||||||
_VOLCENGINE_DATE_RANGE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$")
|
_VOLCENGINE_DATE_RANGE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$")
|
||||||
|
|
||||||
|
|
||||||
|
# Single source of truth for selectable search providers (CLI wizard + WebUI).
|
||||||
|
# "credential" describes what each provider needs: none / api_key / base_url /
|
||||||
|
# optional_api_key.
|
||||||
|
SEARCH_PROVIDER_OPTIONS: tuple[dict[str, str], ...] = (
|
||||||
|
{"name": "duckduckgo", "label": "DuckDuckGo", "credential": "none"},
|
||||||
|
{"name": "brave", "label": "Brave Search", "credential": "api_key"},
|
||||||
|
{"name": "tavily", "label": "Tavily", "credential": "api_key"},
|
||||||
|
{"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": "bocha", "label": "Bocha", "credential": "api_key"},
|
||||||
|
{"name": "volcengine", "label": "Volcengine Search", "credential": "api_key"},
|
||||||
|
{"name": "keenable", "label": "Keenable", "credential": "optional_api_key"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebSearchConfig(Base):
|
class WebSearchConfig(Base):
|
||||||
"""Web search configuration."""
|
"""Web search configuration."""
|
||||||
provider: str = "duckduckgo"
|
provider: str = "duckduckgo"
|
||||||
|
|||||||
+29
-1
@@ -836,6 +836,21 @@ def _handle_fallback_models_field(
|
|||||||
items.clear()
|
items.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_search_provider_field(
|
||||||
|
working_model: BaseModel, field_name: str, field_display: str, current_value: Any
|
||||||
|
) -> None:
|
||||||
|
"""Handle the web-search 'provider' field with the search-engine list."""
|
||||||
|
from nanobot.agent.tools.web import SEARCH_PROVIDER_OPTIONS
|
||||||
|
|
||||||
|
choices = [opt["name"] for opt in SEARCH_PROVIDER_OPTIONS]
|
||||||
|
default_choice = str(current_value) if current_value in choices else choices[0]
|
||||||
|
new_value = _select_with_back(field_display, choices, default=default_choice)
|
||||||
|
if new_value is _BACK_PRESSED:
|
||||||
|
return
|
||||||
|
if new_value is not None:
|
||||||
|
setattr(working_model, field_name, new_value)
|
||||||
|
|
||||||
|
|
||||||
_FIELD_HANDLERS: dict[str, Any] = {
|
_FIELD_HANDLERS: dict[str, Any] = {
|
||||||
"model": _handle_model_field,
|
"model": _handle_model_field,
|
||||||
"context_window_tokens": _handle_context_window_field,
|
"context_window_tokens": _handle_context_window_field,
|
||||||
@@ -844,6 +859,19 @@ _FIELD_HANDLERS: dict[str, Any] = {
|
|||||||
"fallback_models": _handle_fallback_models_field,
|
"fallback_models": _handle_fallback_models_field,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Handlers keyed by (model class name, field name); take precedence over the
|
||||||
|
# name-only handlers above. Needed because the bare "provider" field name is
|
||||||
|
# shared by LLM configs (LLM provider list) and WebSearchConfig (search engines).
|
||||||
|
_TYPED_FIELD_HANDLERS: dict[tuple[str, str], Any] = {
|
||||||
|
("WebSearchConfig", "provider"): _handle_search_provider_field,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_field_handler(model: BaseModel, field_name: str) -> Any:
|
||||||
|
"""Resolve a field handler, preferring model-type-specific handlers."""
|
||||||
|
typed = _TYPED_FIELD_HANDLERS.get((type(model).__name__, field_name))
|
||||||
|
return typed or _FIELD_HANDLERS.get(field_name)
|
||||||
|
|
||||||
|
|
||||||
def _is_str_or_none(annotation: Any) -> bool:
|
def _is_str_or_none(annotation: Any) -> bool:
|
||||||
"""Check whether a field annotation is ``str | None`` (or ``Optional[str]``)."""
|
"""Check whether a field annotation is ``str | None`` (or ``Optional[str]``)."""
|
||||||
@@ -934,7 +962,7 @@ def _configure_pydantic_model(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Registered special-field handlers
|
# Registered special-field handlers
|
||||||
handler = _FIELD_HANDLERS.get(field_name)
|
handler = _resolve_field_handler(working_model, field_name)
|
||||||
if handler:
|
if handler:
|
||||||
handler(working_model, field_name, field_display, current_value)
|
handler(working_model, field_name, field_display, current_value)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from zoneinfo import ZoneInfo
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from nanobot import __version__
|
from nanobot import __version__
|
||||||
|
from nanobot.agent.tools.web import SEARCH_PROVIDER_OPTIONS
|
||||||
from nanobot.audio.transcription import resolve_transcription_config
|
from nanobot.audio.transcription import resolve_transcription_config
|
||||||
from nanobot.audio.transcription_registry import (
|
from nanobot.audio.transcription_registry import (
|
||||||
resolve_transcription_provider,
|
resolve_transcription_provider,
|
||||||
@@ -79,19 +80,7 @@ _NATIVE_RESTART_BEHAVIOR_BY_SECTION = {
|
|||||||
"apps": "engineRestart",
|
"apps": "engineRestart",
|
||||||
}
|
}
|
||||||
|
|
||||||
_WEB_SEARCH_PROVIDER_OPTIONS: tuple[dict[str, str], ...] = (
|
_WEB_SEARCH_PROVIDER_OPTIONS = SEARCH_PROVIDER_OPTIONS
|
||||||
{"name": "duckduckgo", "label": "DuckDuckGo", "credential": "none"},
|
|
||||||
{"name": "brave", "label": "Brave Search", "credential": "api_key"},
|
|
||||||
{"name": "tavily", "label": "Tavily", "credential": "api_key"},
|
|
||||||
{"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": "bocha", "label": "Bocha", "credential": "api_key"},
|
|
||||||
{"name": "volcengine", "label": "Volcengine Search", "credential": "api_key"},
|
|
||||||
{"name": "keenable", "label": "Keenable", "credential": "optional_api_key"},
|
|
||||||
)
|
|
||||||
_WEB_SEARCH_PROVIDER_BY_NAME = {
|
_WEB_SEARCH_PROVIDER_BY_NAME = {
|
||||||
provider["name"]: provider for provider in _WEB_SEARCH_PROVIDER_OPTIONS
|
provider["name"]: provider for provider in _WEB_SEARCH_PROVIDER_OPTIONS
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1998,3 +1998,27 @@ class TestModelPresetWizard:
|
|||||||
defaults = AgentDefaults()
|
defaults = AgentDefaults()
|
||||||
_handle_provider_field(defaults, "provider", "Provider", "auto")
|
_handle_provider_field(defaults, "provider", "Provider", "auto")
|
||||||
assert defaults.provider == "anthropic"
|
assert defaults.provider == "anthropic"
|
||||||
|
|
||||||
|
def test_search_provider_field_handler(self, monkeypatch):
|
||||||
|
"""_handle_search_provider_field should set the search engine from choices."""
|
||||||
|
from nanobot.agent.tools.web import WebSearchConfig
|
||||||
|
from nanobot.cli.onboard import _handle_search_provider_field
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "keenable")
|
||||||
|
|
||||||
|
cfg = WebSearchConfig()
|
||||||
|
_handle_search_provider_field(cfg, "provider", "Provider", "duckduckgo")
|
||||||
|
assert cfg.provider == "keenable"
|
||||||
|
|
||||||
|
def test_provider_field_dispatch_is_model_type_aware(self):
|
||||||
|
"""WebSearchConfig.provider must not be hijacked by the LLM provider handler."""
|
||||||
|
from nanobot.agent.tools.web import WebSearchConfig
|
||||||
|
from nanobot.cli.onboard import (
|
||||||
|
_handle_provider_field,
|
||||||
|
_handle_search_provider_field,
|
||||||
|
_resolve_field_handler,
|
||||||
|
)
|
||||||
|
from nanobot.config.schema import AgentDefaults
|
||||||
|
|
||||||
|
assert _resolve_field_handler(WebSearchConfig(), "provider") is _handle_search_provider_field
|
||||||
|
assert _resolve_field_handler(AgentDefaults(), "provider") is _handle_provider_field
|
||||||
|
|||||||
Reference in New Issue
Block a user