fix(tools): improve web search provider fallback (#2629)
- centralize web search provider readiness and resolution logic - fall back when the configured provider is unavailable or invalid - allow native-search-capable models to use built-in search without the client tool - simplify the tools page and add direct access to web search settings - add backend, agent, and integration tests for the new selection behavior
This commit is contained in:
parent
451db2f5d8
commit
cac4f21746
16 changed files with 633 additions and 222 deletions
|
|
@ -100,33 +100,7 @@ func registerSharedTools(
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Tools.IsToolEnabled("web") {
|
if cfg.Tools.IsToolEnabled("web") {
|
||||||
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptionsFromConfig(cfg))
|
||||||
BraveAPIKeys: cfg.Tools.Web.Brave.APIKeys.Values(),
|
|
||||||
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
|
|
||||||
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
|
||||||
TavilyAPIKeys: cfg.Tools.Web.Tavily.APIKeys.Values(),
|
|
||||||
TavilyBaseURL: cfg.Tools.Web.Tavily.BaseURL,
|
|
||||||
TavilyMaxResults: cfg.Tools.Web.Tavily.MaxResults,
|
|
||||||
TavilyEnabled: cfg.Tools.Web.Tavily.Enabled,
|
|
||||||
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
|
|
||||||
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
|
|
||||||
PerplexityAPIKeys: cfg.Tools.Web.Perplexity.APIKeys.Values(),
|
|
||||||
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
|
|
||||||
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
|
|
||||||
SearXNGBaseURL: cfg.Tools.Web.SearXNG.BaseURL,
|
|
||||||
SearXNGMaxResults: cfg.Tools.Web.SearXNG.MaxResults,
|
|
||||||
SearXNGEnabled: cfg.Tools.Web.SearXNG.Enabled,
|
|
||||||
GLMSearchAPIKey: cfg.Tools.Web.GLMSearch.APIKey.String(),
|
|
||||||
GLMSearchBaseURL: cfg.Tools.Web.GLMSearch.BaseURL,
|
|
||||||
GLMSearchEngine: cfg.Tools.Web.GLMSearch.SearchEngine,
|
|
||||||
GLMSearchMaxResults: cfg.Tools.Web.GLMSearch.MaxResults,
|
|
||||||
GLMSearchEnabled: cfg.Tools.Web.GLMSearch.Enabled,
|
|
||||||
BaiduSearchAPIKey: cfg.Tools.Web.BaiduSearch.APIKey.String(),
|
|
||||||
BaiduSearchBaseURL: cfg.Tools.Web.BaiduSearch.BaseURL,
|
|
||||||
BaiduSearchMaxResults: cfg.Tools.Web.BaiduSearch.MaxResults,
|
|
||||||
BaiduSearchEnabled: cfg.Tools.Web.BaiduSearch.Enabled,
|
|
||||||
Proxy: cfg.Tools.Web.Proxy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("agent", "Failed to create web search tool", map[string]any{"error": err.Error()})
|
logger.ErrorCF("agent", "Failed to create web search tool", map[string]any{"error": err.Error()})
|
||||||
} else if searchTool != nil {
|
} else if searchTool != nil {
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,58 @@ func newTestAgentLoop(
|
||||||
return al, cfg, msgBus, provider, func() { os.RemoveAll(tmpDir) }
|
return al, cfg, msgBus, provider, func() { os.RemoveAll(tmpDir) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewAgentLoop_RegistersWebSearchTool(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Agents.Defaults.Workspace = t.TempDir()
|
||||||
|
|
||||||
|
al := NewAgentLoop(cfg, bus.NewMessageBus(), &mockProvider{})
|
||||||
|
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
if agent == nil {
|
||||||
|
t.Fatal("expected default agent")
|
||||||
|
}
|
||||||
|
if _, ok := agent.Tools.Get("web_search"); !ok {
|
||||||
|
t.Fatal("expected web_search tool to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAgentLoop_RegistersWebSearchTool_WhenExplicitProviderUnavailable(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Agents.Defaults.Workspace = t.TempDir()
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = true
|
||||||
|
|
||||||
|
al := NewAgentLoop(cfg, bus.NewMessageBus(), &mockProvider{})
|
||||||
|
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
if agent == nil {
|
||||||
|
t.Fatal("expected default agent")
|
||||||
|
}
|
||||||
|
if _, ok := agent.Tools.Get("web_search"); !ok {
|
||||||
|
t.Fatal("expected web_search tool to fall back to auto provider selection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAgentLoop_DoesNotRegisterWebSearchTool_WhenNoReadyProviders(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Agents.Defaults.Workspace = t.TempDir()
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = false
|
||||||
|
cfg.Tools.Web.DuckDuckGo.Enabled = false
|
||||||
|
|
||||||
|
al := NewAgentLoop(cfg, bus.NewMessageBus(), &mockProvider{})
|
||||||
|
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
if agent == nil {
|
||||||
|
t.Fatal("expected default agent")
|
||||||
|
}
|
||||||
|
if _, ok := agent.Tools.Get("web_search"); ok {
|
||||||
|
t.Fatal("expected web_search tool to be absent when no providers are ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessMessage_IncludesCurrentSenderInDynamicContext(t *testing.T) {
|
func TestProcessMessage_IncludesCurrentSenderInDynamicContext(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ func (p *Pipeline) CallLLM(
|
||||||
exec.providerToolDefs = ts.agent.Tools.ToProviderDefs()
|
exec.providerToolDefs = ts.agent.Tools.ToProviderDefs()
|
||||||
|
|
||||||
// Native web search support
|
// Native web search support
|
||||||
_, hasWebSearch := ts.agent.Tools.Get("web_search")
|
webSearchEnabled := al.cfg.Tools.IsToolEnabled("web")
|
||||||
exec.useNativeSearch = al.cfg.Tools.Web.PreferNative && hasWebSearch &&
|
exec.useNativeSearch = webSearchEnabled && al.cfg.Tools.Web.PreferNative &&
|
||||||
func() bool {
|
func() bool {
|
||||||
if ns, ok := ts.agent.Provider.(interface{ SupportsNativeSearch() bool }); ok {
|
if ns, ok := ts.agent.Provider.(providers.NativeSearchCapable); ok {
|
||||||
return ns.SupportsNativeSearch()
|
return ns.SupportsNativeSearch()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,35 @@ func (p *simpleConvProvider) GetDefaultModel() string {
|
||||||
return "simple-model"
|
return "simple-model"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nativeSearchCaptureProvider struct {
|
||||||
|
lastOpts map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nativeSearchCaptureProvider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []providers.Message,
|
||||||
|
tools []providers.ToolDefinition,
|
||||||
|
model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
|
p.lastOpts = make(map[string]any, len(opts))
|
||||||
|
for k, v := range opts {
|
||||||
|
p.lastOpts[k] = v
|
||||||
|
}
|
||||||
|
return &providers.LLMResponse{
|
||||||
|
Content: "Using native search",
|
||||||
|
FinishReason: "stop",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nativeSearchCaptureProvider) GetDefaultModel() string {
|
||||||
|
return "native-search-model"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nativeSearchCaptureProvider) SupportsNativeSearch() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// toolCallRespProvider returns a tool call response
|
// toolCallRespProvider returns a tool call response
|
||||||
type toolCallRespProvider struct {
|
type toolCallRespProvider struct {
|
||||||
toolName string
|
toolName string
|
||||||
|
|
@ -257,6 +286,41 @@ func TestPipeline_CallLLM_WithToolCall(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPipeline_CallLLM_UsesNativeSearchWithoutClientWebSearchTool(t *testing.T) {
|
||||||
|
provider := &nativeSearchCaptureProvider{}
|
||||||
|
al, agent, cleanup := newTurnCoordTestLoop(t, provider)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if _, ok := agent.Tools.Get("web_search"); ok {
|
||||||
|
t.Fatal("expected no client-side web_search tool to be registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
al.cfg.Tools.Web.Enabled = true
|
||||||
|
al.cfg.Tools.Web.PreferNative = true
|
||||||
|
|
||||||
|
pipeline := NewPipeline(al)
|
||||||
|
ts := newTurnState(agent, makeTestProcessOpts("test-session"), turnEventScope{
|
||||||
|
turnID: "turn-1",
|
||||||
|
context: newTurnContext(nil, nil, nil),
|
||||||
|
})
|
||||||
|
|
||||||
|
exec, err := pipeline.SetupTurn(context.Background(), ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetupTurn failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := pipeline.CallLLM(context.Background(), context.Background(), ts, exec, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallLLM failed: %v", err)
|
||||||
|
}
|
||||||
|
if ctrl != ControlBreak {
|
||||||
|
t.Fatalf("expected ControlBreak, got %v", ctrl)
|
||||||
|
}
|
||||||
|
if got, _ := provider.lastOpts["native_search"].(bool); !got {
|
||||||
|
t.Fatalf("expected native_search=true, got %#v", provider.lastOpts["native_search"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPipeline_CallLLM_TimeoutRetry(t *testing.T) {
|
func TestPipeline_CallLLM_TimeoutRetry(t *testing.T) {
|
||||||
errorPrv := &errorProvider{errType: "timeout"}
|
errorPrv := &errorProvider{errType: "timeout"}
|
||||||
al, agent, cleanup := newTurnCoordTestLoop(t, errorPrv)
|
al, agent, cleanup := newTurnCoordTestLoop(t, errorPrv)
|
||||||
|
|
|
||||||
|
|
@ -1113,12 +1113,147 @@ type WebSearchToolOptions struct {
|
||||||
Proxy string
|
Proxy string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WebSearchToolOptionsFromConfig(cfg *config.Config) WebSearchToolOptions {
|
||||||
|
return WebSearchToolOptions{
|
||||||
|
Provider: cfg.Tools.Web.Provider,
|
||||||
|
BraveAPIKeys: cfg.Tools.Web.Brave.APIKeys.Values(),
|
||||||
|
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
|
||||||
|
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
||||||
|
TavilyAPIKeys: cfg.Tools.Web.Tavily.APIKeys.Values(),
|
||||||
|
TavilyBaseURL: cfg.Tools.Web.Tavily.BaseURL,
|
||||||
|
TavilyMaxResults: cfg.Tools.Web.Tavily.MaxResults,
|
||||||
|
TavilyEnabled: cfg.Tools.Web.Tavily.Enabled,
|
||||||
|
SogouMaxResults: cfg.Tools.Web.Sogou.MaxResults,
|
||||||
|
SogouEnabled: cfg.Tools.Web.Sogou.Enabled,
|
||||||
|
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
|
||||||
|
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
|
||||||
|
PerplexityAPIKeys: cfg.Tools.Web.Perplexity.APIKeys.Values(),
|
||||||
|
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
|
||||||
|
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
|
||||||
|
SearXNGBaseURL: cfg.Tools.Web.SearXNG.BaseURL,
|
||||||
|
SearXNGMaxResults: cfg.Tools.Web.SearXNG.MaxResults,
|
||||||
|
SearXNGEnabled: cfg.Tools.Web.SearXNG.Enabled,
|
||||||
|
GLMSearchAPIKey: cfg.Tools.Web.GLMSearch.APIKey.String(),
|
||||||
|
GLMSearchBaseURL: cfg.Tools.Web.GLMSearch.BaseURL,
|
||||||
|
GLMSearchEngine: cfg.Tools.Web.GLMSearch.SearchEngine,
|
||||||
|
GLMSearchMaxResults: cfg.Tools.Web.GLMSearch.MaxResults,
|
||||||
|
GLMSearchEnabled: cfg.Tools.Web.GLMSearch.Enabled,
|
||||||
|
BaiduSearchAPIKey: cfg.Tools.Web.BaiduSearch.APIKey.String(),
|
||||||
|
BaiduSearchBaseURL: cfg.Tools.Web.BaiduSearch.BaseURL,
|
||||||
|
BaiduSearchMaxResults: cfg.Tools.Web.BaiduSearch.MaxResults,
|
||||||
|
BaiduSearchEnabled: cfg.Tools.Web.BaiduSearch.Enabled,
|
||||||
|
Proxy: cfg.Tools.Web.Proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebSearchProviderReady(opts WebSearchToolOptions, name string) bool {
|
||||||
|
return opts.providerReady(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveWebSearchProviderName(opts WebSearchToolOptions, query string) (string, error) {
|
||||||
|
return opts.resolveProviderName(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
knownWebSearchProviders = []string{
|
||||||
|
"sogou",
|
||||||
|
"duckduckgo",
|
||||||
|
"brave",
|
||||||
|
"tavily",
|
||||||
|
"perplexity",
|
||||||
|
"searxng",
|
||||||
|
"glm_search",
|
||||||
|
"baidu_search",
|
||||||
|
}
|
||||||
|
autoPrimaryWebSearchProviders = []string{"perplexity", "brave", "searxng", "tavily"}
|
||||||
|
autoFallbackWebSearchProviders = []string{"baidu_search", "glm_search"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func isKnownWebSearchProvider(name string) bool {
|
||||||
|
name = strings.ToLower(strings.TrimSpace(name))
|
||||||
|
for _, known := range knownWebSearchProviders {
|
||||||
|
if name == known {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts WebSearchToolOptions) providerReady(name string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||||
|
case "sogou":
|
||||||
|
return opts.SogouEnabled
|
||||||
|
case "duckduckgo":
|
||||||
|
return opts.DuckDuckGoEnabled
|
||||||
|
case "brave":
|
||||||
|
return opts.BraveEnabled && len(opts.BraveAPIKeys) > 0
|
||||||
|
case "tavily":
|
||||||
|
return opts.TavilyEnabled && len(opts.TavilyAPIKeys) > 0
|
||||||
|
case "perplexity":
|
||||||
|
return opts.PerplexityEnabled && len(opts.PerplexityAPIKeys) > 0
|
||||||
|
case "searxng":
|
||||||
|
return opts.SearXNGEnabled && strings.TrimSpace(opts.SearXNGBaseURL) != ""
|
||||||
|
case "glm_search":
|
||||||
|
return opts.GLMSearchEnabled && strings.TrimSpace(opts.GLMSearchAPIKey) != ""
|
||||||
|
case "baidu_search":
|
||||||
|
return opts.BaiduSearchEnabled && strings.TrimSpace(opts.BaiduSearchAPIKey) != ""
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts WebSearchToolOptions) normalizedProviderName() string {
|
||||||
|
providerName := strings.ToLower(strings.TrimSpace(opts.Provider))
|
||||||
|
if providerName != "" && providerName != "auto" && !isKnownWebSearchProvider(providerName) {
|
||||||
|
// Tolerate stale or manually edited config values at runtime by
|
||||||
|
// treating them as "auto" and falling back to the next ready provider.
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
return providerName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts WebSearchToolOptions) resolveProviderName(query string) (string, error) {
|
||||||
|
providerName := opts.normalizedProviderName()
|
||||||
|
if providerName != "" && providerName != "auto" && opts.providerReady(providerName) {
|
||||||
|
return providerName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range autoPrimaryWebSearchProviders {
|
||||||
|
if opts.providerReady(name) {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sogouReady := opts.providerReady("sogou")
|
||||||
|
duckReady := opts.providerReady("duckduckgo")
|
||||||
|
if sogouReady && duckReady {
|
||||||
|
if prefersDuckDuckGoQuery(query) {
|
||||||
|
return "duckduckgo", nil
|
||||||
|
}
|
||||||
|
return "sogou", nil
|
||||||
|
}
|
||||||
|
if sogouReady {
|
||||||
|
return "sogou", nil
|
||||||
|
}
|
||||||
|
if duckReady {
|
||||||
|
return "duckduckgo", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range autoFallbackWebSearchProviders {
|
||||||
|
if opts.providerReady(name) {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, int, error) {
|
func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, int, error) {
|
||||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||||
case "", "auto":
|
case "", "auto":
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
case "sogou":
|
case "sogou":
|
||||||
if !opts.SogouEnabled {
|
if !opts.providerReady("sogou") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1134,7 +1269,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "perplexity":
|
case "perplexity":
|
||||||
if !opts.PerplexityEnabled {
|
if !opts.providerReady("perplexity") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
|
||||||
|
|
@ -1151,7 +1286,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "brave":
|
case "brave":
|
||||||
if !opts.BraveEnabled {
|
if !opts.providerReady("brave") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1168,7 +1303,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "searxng":
|
case "searxng":
|
||||||
if !opts.SearXNGEnabled {
|
if !opts.providerReady("searxng") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1185,7 +1320,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "tavily":
|
case "tavily":
|
||||||
if !opts.TavilyEnabled {
|
if !opts.providerReady("tavily") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1203,7 +1338,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "duckduckgo":
|
case "duckduckgo":
|
||||||
if !opts.DuckDuckGoEnabled {
|
if !opts.providerReady("duckduckgo") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1219,7 +1354,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "baidu_search":
|
case "baidu_search":
|
||||||
if !opts.BaiduSearchEnabled {
|
if !opts.providerReady("baidu_search") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
|
||||||
|
|
@ -1237,7 +1372,7 @@ func (opts WebSearchToolOptions) providerByName(name string) (SearchProvider, in
|
||||||
client: client,
|
client: client,
|
||||||
}, maxResults, nil
|
}, maxResults, nil
|
||||||
case "glm_search":
|
case "glm_search":
|
||||||
if !opts.GLMSearchEnabled {
|
if !opts.providerReady("glm_search") {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
|
||||||
|
|
@ -1297,62 +1432,35 @@ func prefersDuckDuckGoQuery(text string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts WebSearchToolOptions) buildProviderResolver() (func(query string) (SearchProvider, int), error) {
|
func (opts WebSearchToolOptions) buildProviderResolver() (func(query string) (SearchProvider, int), error) {
|
||||||
providerName := strings.ToLower(strings.TrimSpace(opts.Provider))
|
providersByName := make(map[string]SearchProvider, len(knownWebSearchProviders))
|
||||||
if providerName != "" && providerName != "auto" {
|
maxResultsByName := make(map[string]int, len(knownWebSearchProviders))
|
||||||
provider, maxResults, err := opts.providerByName(providerName)
|
|
||||||
|
for _, name := range knownWebSearchProviders {
|
||||||
|
if !opts.providerReady(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider, maxResults, err := opts.providerByName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
return func(string) (SearchProvider, int) { return nil, 0 }, nil
|
continue
|
||||||
}
|
}
|
||||||
return func(string) (SearchProvider, int) { return provider, maxResults }, nil
|
providersByName[name] = provider
|
||||||
|
maxResultsByName[name] = maxResults
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{"perplexity", "brave", "searxng", "tavily"} {
|
|
||||||
provider, maxResults, err := opts.providerByName(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if provider != nil {
|
|
||||||
return func(string) (SearchProvider, int) { return provider, maxResults }, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sogouProvider, sogouMaxResults, err := opts.providerByName("sogou")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
duckProvider, duckMaxResults, err := opts.providerByName("duckduckgo")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if sogouProvider != nil && duckProvider != nil {
|
|
||||||
return func(query string) (SearchProvider, int) {
|
return func(query string) (SearchProvider, int) {
|
||||||
if prefersDuckDuckGoQuery(query) {
|
name, err := opts.resolveProviderName(query)
|
||||||
return duckProvider, duckMaxResults
|
|
||||||
}
|
|
||||||
return sogouProvider, sogouMaxResults
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if sogouProvider != nil {
|
|
||||||
return func(string) (SearchProvider, int) { return sogouProvider, sogouMaxResults }, nil
|
|
||||||
}
|
|
||||||
if duckProvider != nil {
|
|
||||||
return func(string) (SearchProvider, int) { return duckProvider, duckMaxResults }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range []string{"baidu_search", "glm_search"} {
|
|
||||||
provider, maxResults, err := opts.providerByName(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0
|
||||||
}
|
}
|
||||||
if provider != nil {
|
provider, ok := providersByName[name]
|
||||||
return func(string) (SearchProvider, int) { return provider, maxResults }, nil
|
if !ok {
|
||||||
|
return nil, 0
|
||||||
}
|
}
|
||||||
}
|
return provider, maxResultsByName[name]
|
||||||
|
}, nil
|
||||||
return func(string) (SearchProvider, int) { return nil, 0 }, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
|
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
|
||||||
|
|
|
||||||
|
|
@ -385,24 +385,14 @@ func TestWebFetchTool_PayloadTooLarge(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWebTool_WebSearch_NoApiKey verifies missing credentials are surfaced at execution time.
|
// TestWebTool_WebSearch_NoApiKey verifies providers without required credentials are not registered.
|
||||||
func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
|
func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
|
||||||
tool, err := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKeys: nil})
|
tool, err := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKeys: nil})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if tool == nil {
|
if tool != nil {
|
||||||
t.Fatalf("Expected tool when Brave is enabled, even without API keys")
|
t.Fatalf("Expected nil tool when only enabled provider is missing credentials")
|
||||||
}
|
|
||||||
|
|
||||||
result := tool.Execute(context.Background(), map[string]any{
|
|
||||||
"query": "test query",
|
|
||||||
})
|
|
||||||
if !result.IsError {
|
|
||||||
t.Fatalf("Expected missing Brave API key to return error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(result.ForLLM, "no API key provided") {
|
|
||||||
t.Fatalf("Unexpected error message: %s", result.ForLLM)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also nil when nothing is enabled
|
// Also nil when nothing is enabled
|
||||||
|
|
@ -1878,6 +1868,94 @@ func TestWebTool_AutoProviderPrefersConfiguredProvidersBeforeSogou(t *testing.T)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebTool_ExplicitProviderFallsBackWhenMissingCredentials(t *testing.T) {
|
||||||
|
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||||
|
Provider: "brave",
|
||||||
|
BraveEnabled: true,
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWebSearchTool() error: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tool.provider.(*SogouSearchProvider); !ok {
|
||||||
|
t.Fatalf("expected SogouSearchProvider after fallback, got %T", tool.provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebTool_ExplicitProviderFallsBackWhenMissingBaseURL(t *testing.T) {
|
||||||
|
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||||
|
Provider: "searxng",
|
||||||
|
SearXNGEnabled: true,
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWebSearchTool() error: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tool.provider.(*SogouSearchProvider); !ok {
|
||||||
|
t.Fatalf("expected SogouSearchProvider after fallback, got %T", tool.provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebTool_AutoProviderSkipsEnabledButUnreadyProviders(t *testing.T) {
|
||||||
|
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||||
|
Provider: "auto",
|
||||||
|
BraveEnabled: true,
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWebSearchTool() error: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tool.provider.(*SogouSearchProvider); !ok {
|
||||||
|
t.Fatalf("expected SogouSearchProvider when Brave has no API key, got %T", tool.provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveWebSearchProviderName_FallsBackFromExplicitUnavailableProvider(t *testing.T) {
|
||||||
|
got, err := ResolveWebSearchProviderName(WebSearchToolOptions{
|
||||||
|
Provider: "brave",
|
||||||
|
BraveEnabled: true,
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveWebSearchProviderName() error: %v", err)
|
||||||
|
}
|
||||||
|
if got != "sogou" {
|
||||||
|
t.Fatalf("ResolveWebSearchProviderName() = %q, want sogou", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebTool_UnknownExplicitProviderFallsBackToAuto(t *testing.T) {
|
||||||
|
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||||
|
Provider: "totally_unknown",
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWebSearchTool() error: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tool.provider.(*SogouSearchProvider); !ok {
|
||||||
|
t.Fatalf("expected SogouSearchProvider after fallback, got %T", tool.provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveWebSearchProviderName_FallsBackFromUnknownProvider(t *testing.T) {
|
||||||
|
got, err := ResolveWebSearchProviderName(WebSearchToolOptions{
|
||||||
|
Provider: "totally_unknown",
|
||||||
|
SogouEnabled: true,
|
||||||
|
SogouMaxResults: 5,
|
||||||
|
}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveWebSearchProviderName() error: %v", err)
|
||||||
|
}
|
||||||
|
if got != "sogou" {
|
||||||
|
t.Fatalf("ResolveWebSearchProviderName() = %q, want sogou", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type stubSearchProvider struct {
|
type stubSearchProvider struct {
|
||||||
result string
|
result string
|
||||||
calls []string
|
calls []string
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/audio/tts"
|
"github.com/sipeed/picoclaw/pkg/audio/tts"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/media"
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
integrationtools "github.com/sipeed/picoclaw/pkg/tools/integration"
|
integrationtools "github.com/sipeed/picoclaw/pkg/tools/integration"
|
||||||
|
|
@ -72,6 +73,18 @@ func GetPreferredWebSearchLanguage() string {
|
||||||
return integrationtools.GetPreferredWebSearchLanguage()
|
return integrationtools.GetPreferredWebSearchLanguage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WebSearchToolOptionsFromConfig(cfg *config.Config) WebSearchToolOptions {
|
||||||
|
return integrationtools.WebSearchToolOptionsFromConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebSearchProviderReady(opts WebSearchToolOptions, name string) bool {
|
||||||
|
return integrationtools.WebSearchProviderReady(opts, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveWebSearchProviderName(opts WebSearchToolOptions, query string) (string, error) {
|
||||||
|
return integrationtools.ResolveWebSearchProviderName(opts, query)
|
||||||
|
}
|
||||||
|
|
||||||
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
|
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
|
||||||
return integrationtools.NewWebSearchTool(opts)
|
return integrationtools.NewWebSearchTool(opts)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,8 @@ func buildToolSupport(cfg *config.Config) []toolSupportItem {
|
||||||
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseRegex)
|
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseRegex)
|
||||||
case "tool_search_tool_bm25":
|
case "tool_search_tool_bm25":
|
||||||
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseBM25)
|
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseBM25)
|
||||||
|
case "web_search":
|
||||||
|
status, reasonCode = resolveWebSearchToolSupport(cfg)
|
||||||
case "i2c", "spi":
|
case "i2c", "spi":
|
||||||
status, reasonCode = resolveHardwareToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey))
|
status, reasonCode = resolveHardwareToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey))
|
||||||
default:
|
default:
|
||||||
|
|
@ -304,6 +306,13 @@ func resolveDiscoveryToolSupport(cfg *config.Config, methodEnabled bool) (string
|
||||||
return "enabled", ""
|
return "enabled", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveWebSearchToolSupport(cfg *config.Config) (string, string) {
|
||||||
|
if !cfg.Tools.IsToolEnabled("web") {
|
||||||
|
return "disabled", ""
|
||||||
|
}
|
||||||
|
return "enabled", ""
|
||||||
|
}
|
||||||
|
|
||||||
func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
|
func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
|
||||||
switch toolName {
|
switch toolName {
|
||||||
case "read_file":
|
case "read_file":
|
||||||
|
|
@ -507,6 +516,7 @@ func normalizeWebSearchAPIKeys(apiKeys []string, apiKey string) ([]string, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
|
func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
|
||||||
|
opts := picotools.WebSearchToolOptionsFromConfig(cfg)
|
||||||
current := resolveCurrentWebSearchProvider(cfg)
|
current := resolveCurrentWebSearchProvider(cfg)
|
||||||
settings := map[string]webSearchProviderConfig{
|
settings := map[string]webSearchProviderConfig{
|
||||||
"sogou": {
|
"sogou": {
|
||||||
|
|
@ -563,59 +573,53 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
|
||||||
{
|
{
|
||||||
ID: "sogou",
|
ID: "sogou",
|
||||||
Label: "Sogou",
|
Label: "Sogou",
|
||||||
Configured: cfg.Tools.Web.Sogou.Enabled,
|
Configured: picotools.WebSearchProviderReady(opts, "sogou"),
|
||||||
Current: current == "sogou",
|
Current: current == "sogou",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "duckduckgo",
|
ID: "duckduckgo",
|
||||||
Label: "DuckDuckGo",
|
Label: "DuckDuckGo",
|
||||||
Configured: cfg.Tools.Web.DuckDuckGo.Enabled,
|
Configured: picotools.WebSearchProviderReady(opts, "duckduckgo"),
|
||||||
Current: current == "duckduckgo",
|
Current: current == "duckduckgo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "brave",
|
ID: "brave",
|
||||||
Label: "Brave Search",
|
Label: "Brave Search",
|
||||||
Configured: cfg.Tools.Web.Brave.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "brave"),
|
||||||
len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0,
|
|
||||||
Current: current == "brave",
|
Current: current == "brave",
|
||||||
RequiresAuth: true,
|
RequiresAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "tavily",
|
ID: "tavily",
|
||||||
Label: "Tavily",
|
Label: "Tavily",
|
||||||
Configured: cfg.Tools.Web.Tavily.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "tavily"),
|
||||||
len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0,
|
|
||||||
Current: current == "tavily",
|
Current: current == "tavily",
|
||||||
RequiresAuth: true,
|
RequiresAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "perplexity",
|
ID: "perplexity",
|
||||||
Label: "Perplexity",
|
Label: "Perplexity",
|
||||||
Configured: cfg.Tools.Web.Perplexity.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "perplexity"),
|
||||||
len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0,
|
|
||||||
Current: current == "perplexity",
|
Current: current == "perplexity",
|
||||||
RequiresAuth: true,
|
RequiresAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "searxng",
|
ID: "searxng",
|
||||||
Label: "SearXNG",
|
Label: "SearXNG",
|
||||||
Configured: cfg.Tools.Web.SearXNG.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "searxng"),
|
||||||
strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != "",
|
|
||||||
Current: current == "searxng",
|
Current: current == "searxng",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "glm_search",
|
ID: "glm_search",
|
||||||
Label: "GLM Search",
|
Label: "GLM Search",
|
||||||
Configured: cfg.Tools.Web.GLMSearch.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "glm_search"),
|
||||||
cfg.Tools.Web.GLMSearch.APIKey.String() != "",
|
|
||||||
Current: current == "glm_search",
|
Current: current == "glm_search",
|
||||||
RequiresAuth: true,
|
RequiresAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "baidu_search",
|
ID: "baidu_search",
|
||||||
Label: "Baidu Search",
|
Label: "Baidu Search",
|
||||||
Configured: cfg.Tools.Web.BaiduSearch.Enabled &&
|
Configured: picotools.WebSearchProviderReady(opts, "baidu_search"),
|
||||||
cfg.Tools.Web.BaiduSearch.APIKey.String() != "",
|
|
||||||
Current: current == "baidu_search",
|
Current: current == "baidu_search",
|
||||||
RequiresAuth: true,
|
RequiresAuth: true,
|
||||||
},
|
},
|
||||||
|
|
@ -637,57 +641,12 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveCurrentWebSearchProvider(cfg *config.Config) string {
|
func resolveCurrentWebSearchProvider(cfg *config.Config) string {
|
||||||
selected := normalizeWebSearchProvider(cfg.Tools.Web.Provider)
|
if cfg == nil || !cfg.Tools.IsToolEnabled("web") {
|
||||||
if selected != "" && selected != "auto" && webSearchProviderConfigured(cfg, selected) {
|
|
||||||
return selected
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range []string{"perplexity", "brave", "searxng", "tavily"} {
|
|
||||||
if webSearchProviderConfigured(cfg, name) {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if webSearchProviderConfigured(cfg, "sogou") && webSearchProviderConfigured(cfg, "duckduckgo") {
|
|
||||||
if picotools.GetPreferredWebSearchLanguage() == "en" {
|
|
||||||
return "duckduckgo"
|
|
||||||
}
|
|
||||||
return "sogou"
|
|
||||||
}
|
|
||||||
if webSearchProviderConfigured(cfg, "sogou") {
|
|
||||||
return "sogou"
|
|
||||||
}
|
|
||||||
if webSearchProviderConfigured(cfg, "duckduckgo") {
|
|
||||||
return "duckduckgo"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range []string{"baidu_search", "glm_search"} {
|
|
||||||
if webSearchProviderConfigured(cfg, name) {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
selected, err := picotools.ResolveWebSearchProviderName(picotools.WebSearchToolOptionsFromConfig(cfg), "")
|
||||||
func webSearchProviderConfigured(cfg *config.Config, name string) bool {
|
if err != nil {
|
||||||
switch name {
|
return ""
|
||||||
case "sogou":
|
|
||||||
return cfg.Tools.Web.Sogou.Enabled
|
|
||||||
case "duckduckgo":
|
|
||||||
return cfg.Tools.Web.DuckDuckGo.Enabled
|
|
||||||
case "brave":
|
|
||||||
return cfg.Tools.Web.Brave.Enabled && len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0
|
|
||||||
case "tavily":
|
|
||||||
return cfg.Tools.Web.Tavily.Enabled && len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0
|
|
||||||
case "perplexity":
|
|
||||||
return cfg.Tools.Web.Perplexity.Enabled && len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0
|
|
||||||
case "searxng":
|
|
||||||
return cfg.Tools.Web.SearXNG.Enabled && strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != ""
|
|
||||||
case "glm_search":
|
|
||||||
return cfg.Tools.Web.GLMSearch.Enabled && cfg.Tools.Web.GLMSearch.APIKey.String() != ""
|
|
||||||
case "baidu_search":
|
|
||||||
return cfg.Tools.Web.BaiduSearch.Enabled && cfg.Tools.Web.BaiduSearch.APIKey.String() != ""
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return selected
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,66 @@ func TestHandleUpdateToolState(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleListTools_ReportsWebSearchEnabledWhenToolIsOn(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
preferNative bool
|
||||||
|
}{
|
||||||
|
{name: "without prefer_native", preferNative: false},
|
||||||
|
{name: "with prefer_native", preferNative: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
cfg.Tools.Web.PreferNative = tt.preferNative
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = false
|
||||||
|
cfg.Tools.Web.DuckDuckGo.Enabled = false
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
cfg.Tools.Web.Brave.SetAPIKeys(nil)
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/tools", nil)
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp toolSupportResponse
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range resp.Tools {
|
||||||
|
if tool.Name != "web_search" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tool.Status != "enabled" || tool.ReasonCode != "" {
|
||||||
|
t.Fatalf("web_search = %#v, want enabled with no reason code", tool)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("expected web_search in response")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleGetWebSearchConfig(t *testing.T) {
|
func TestHandleGetWebSearchConfig(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
@ -206,6 +266,7 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LoadConfig() error = %v", err)
|
t.Fatalf("LoadConfig() error = %v", err)
|
||||||
}
|
}
|
||||||
|
cfg.Tools.Web.PreferNative = false
|
||||||
cfg.Tools.Web.Provider = "sogou"
|
cfg.Tools.Web.Provider = "sogou"
|
||||||
cfg.Tools.Web.Sogou.Enabled = true
|
cfg.Tools.Web.Sogou.Enabled = true
|
||||||
cfg.Tools.Web.Sogou.MaxResults = 6
|
cfg.Tools.Web.Sogou.MaxResults = 6
|
||||||
|
|
@ -242,6 +303,48 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleGetWebSearchConfig_DoesNotExposeNativeAsCurrentService(t *testing.T) {
|
||||||
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
cfg.Tools.Web.PreferNative = true
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = false
|
||||||
|
cfg.Tools.Web.DuckDuckGo.Enabled = false
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
cfg.Tools.Web.Brave.SetAPIKeys(nil)
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/tools/web-search-config", nil)
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp webSearchConfigResponse
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.PreferNative {
|
||||||
|
t.Fatal("prefer_native should remain true in response")
|
||||||
|
}
|
||||||
|
if resp.CurrentService != "" {
|
||||||
|
t.Fatalf("current_service = %q, want empty when no external provider is ready", resp.CurrentService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleUpdateWebSearchConfig(t *testing.T) {
|
func TestHandleUpdateWebSearchConfig(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
@ -393,6 +496,27 @@ func TestResolveCurrentWebSearchProvider_PrefersConfiguredProvidersBeforeSogou(t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveCurrentWebSearchProvider_FallsBackWhenExplicitProviderUnavailable(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = true
|
||||||
|
|
||||||
|
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
|
||||||
|
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveCurrentWebSearchProvider_FallsBackWhenProviderIsUnknown(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Web.Provider = "totally_unknown"
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = true
|
||||||
|
|
||||||
|
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
|
||||||
|
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) {
|
func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
cfg.Tools.Web.Provider = "auto"
|
cfg.Tools.Web.Provider = "auto"
|
||||||
|
|
@ -413,3 +537,22 @@ func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuc
|
||||||
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
|
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveCurrentWebSearchProvider_IgnoresPreferNativeInConfigView(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ModelList = []*config.ModelConfig{{
|
||||||
|
ModelName: "custom-default",
|
||||||
|
Model: "openai/gpt-4o",
|
||||||
|
APIKeys: config.SimpleSecureStrings("sk-default"),
|
||||||
|
}}
|
||||||
|
cfg.Agents.Defaults.ModelName = "custom-default"
|
||||||
|
cfg.Tools.Web.PreferNative = true
|
||||||
|
cfg.Tools.Web.Provider = "brave"
|
||||||
|
cfg.Tools.Web.Sogou.Enabled = false
|
||||||
|
cfg.Tools.Web.DuckDuckGo.Enabled = false
|
||||||
|
cfg.Tools.Web.Brave.Enabled = true
|
||||||
|
|
||||||
|
if got := resolveCurrentWebSearchProvider(cfg); got != "" {
|
||||||
|
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want empty when only native search would be available", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { IconSearch } from "@tabler/icons-react"
|
import { IconSearch, IconSettings } from "@tabler/icons-react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import type { ToolSupportItem } from "@/api/tools"
|
import type { ToolSupportItem } from "@/api/tools"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,6 +30,7 @@ interface ToolLibraryTabProps {
|
||||||
pendingToolName: string | null
|
pendingToolName: string | null
|
||||||
onSearchQueryChange: (value: string) => void
|
onSearchQueryChange: (value: string) => void
|
||||||
onStatusFilterChange: (value: ToolStatusFilter) => void
|
onStatusFilterChange: (value: ToolStatusFilter) => void
|
||||||
|
onOpenWebSearchSettings: () => void
|
||||||
onToggleTool: (name: string, enabled: boolean) => void
|
onToggleTool: (name: string, enabled: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ export function ToolLibraryTab({
|
||||||
pendingToolName,
|
pendingToolName,
|
||||||
onSearchQueryChange,
|
onSearchQueryChange,
|
||||||
onStatusFilterChange,
|
onStatusFilterChange,
|
||||||
|
onOpenWebSearchSettings,
|
||||||
onToggleTool,
|
onToggleTool,
|
||||||
}: ToolLibraryTabProps) {
|
}: ToolLibraryTabProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
@ -131,6 +134,7 @@ export function ToolLibraryTab({
|
||||||
key={tool.name}
|
key={tool.name}
|
||||||
tool={tool}
|
tool={tool}
|
||||||
isPending={pendingToolName === tool.name}
|
isPending={pendingToolName === tool.name}
|
||||||
|
onOpenWebSearchSettings={onOpenWebSearchSettings}
|
||||||
onToggleTool={onToggleTool}
|
onToggleTool={onToggleTool}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -146,10 +150,12 @@ export function ToolLibraryTab({
|
||||||
function ToolCard({
|
function ToolCard({
|
||||||
tool,
|
tool,
|
||||||
isPending,
|
isPending,
|
||||||
|
onOpenWebSearchSettings,
|
||||||
onToggleTool,
|
onToggleTool,
|
||||||
}: {
|
}: {
|
||||||
tool: ToolSupportItem
|
tool: ToolSupportItem
|
||||||
isPending: boolean
|
isPending: boolean
|
||||||
|
onOpenWebSearchSettings: () => void
|
||||||
onToggleTool: (name: string, enabled: boolean) => void
|
onToggleTool: (name: string, enabled: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
@ -157,8 +163,10 @@ function ToolCard({
|
||||||
? t(`pages.agent.tools.reasons.${tool.reason_code}`)
|
? t(`pages.agent.tools.reasons.${tool.reason_code}`)
|
||||||
: ""
|
: ""
|
||||||
const isEnabled = tool.status === "enabled"
|
const isEnabled = tool.status === "enabled"
|
||||||
|
const isToggledOn = tool.status !== "disabled"
|
||||||
const isDisabled = tool.status === "disabled"
|
const isDisabled = tool.status === "disabled"
|
||||||
const isBlocked = tool.status === "blocked"
|
const isBlocked = tool.status === "blocked"
|
||||||
|
const isWebSearchTool = tool.name === "web_search"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
@ -170,16 +178,32 @@ function ToolCard({
|
||||||
isDisabled && "opacity-[0.80] hover:opacity-100",
|
isDisabled && "opacity-[0.80] hover:opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardContent className="flex h-full flex-col p-6">
|
<CardContent className="flex h-full flex-col px-5 py-1">
|
||||||
<div className="mb-3 flex items-start justify-between gap-4">
|
<div className="mb-0.5 flex items-start justify-between gap-4">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
<h4 className="text-foreground/90 min-w-0 break-all font-mono text-sm font-semibold tracking-tight">
|
<h4 className="text-foreground/90 min-w-0 font-mono text-sm font-semibold tracking-tight break-all">
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</h4>
|
</h4>
|
||||||
<ToolStatusBadge status={tool.status} />
|
<ToolStatusBadge status={tool.status} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex h-8 shrink-0 items-center gap-2">
|
||||||
|
{isWebSearchTool && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onOpenWebSearchSettings}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-muted/60 size-8 rounded-lg"
|
||||||
|
aria-label={t(
|
||||||
|
"pages.agent.tools.web_search.open_settings",
|
||||||
|
"Open Settings",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconSettings className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
checked={isEnabled}
|
checked={isToggledOn}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
|
onCheckedChange={(checked) => onToggleTool(tool.name, checked)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -188,6 +212,7 @@ function ToolCard({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground/80 flex-1 text-[14px] leading-relaxed">
|
<p className="text-muted-foreground/80 flex-1 text-[14px] leading-relaxed">
|
||||||
{tool.description}
|
{tool.description}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useLayoutEffect, useRef } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { PageHeader } from "@/components/page-header"
|
import { PageHeader } from "@/components/page-header"
|
||||||
|
|
||||||
|
|
@ -8,9 +9,9 @@ import { WebSearchTab } from "./web-search-tab"
|
||||||
|
|
||||||
export function ToolsPage() {
|
export function ToolsPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const {
|
const {
|
||||||
activeTab,
|
activeTab,
|
||||||
currentProviderLabel,
|
|
||||||
expandedProvider,
|
expandedProvider,
|
||||||
groupedTools,
|
groupedTools,
|
||||||
pendingToolName,
|
pendingToolName,
|
||||||
|
|
@ -34,12 +35,19 @@ export function ToolsPage() {
|
||||||
updateWebSearchDraft,
|
updateWebSearchDraft,
|
||||||
} = useToolsPage()
|
} = useToolsPage()
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
scrollContainerRef.current?.scrollTo({ top: 0 })
|
||||||
|
}, [activeTab])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-full flex-col">
|
<div className="bg-background flex h-full flex-col">
|
||||||
<PageHeader title={t("navigation.tools", "Tools")} />
|
<PageHeader title={t("navigation.tools", "Tools")} />
|
||||||
<ToolsTabs activeTab={activeTab} onChange={setActiveTab} />
|
<ToolsTabs activeTab={activeTab} onChange={setActiveTab} />
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto px-6 py-6 pb-20">
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className="flex-1 overflow-auto px-6 py-6 pb-20"
|
||||||
|
>
|
||||||
<div className="mx-auto w-full max-w-6xl">
|
<div className="mx-auto w-full max-w-6xl">
|
||||||
{activeTab === "library" ? (
|
{activeTab === "library" ? (
|
||||||
<ToolLibraryTab
|
<ToolLibraryTab
|
||||||
|
|
@ -53,12 +61,12 @@ export function ToolsPage() {
|
||||||
pendingToolName={pendingToolName}
|
pendingToolName={pendingToolName}
|
||||||
onSearchQueryChange={setSearchQuery}
|
onSearchQueryChange={setSearchQuery}
|
||||||
onStatusFilterChange={setStatusFilter}
|
onStatusFilterChange={setStatusFilter}
|
||||||
|
onOpenWebSearchSettings={() => setActiveTab("web-search")}
|
||||||
onToggleTool={toggleTool}
|
onToggleTool={toggleTool}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<WebSearchTab
|
<WebSearchTab
|
||||||
draft={webSearchDraft}
|
draft={webSearchDraft}
|
||||||
currentProviderLabel={currentProviderLabel}
|
|
||||||
providerLabelMap={providerLabelMap}
|
providerLabelMap={providerLabelMap}
|
||||||
expandedProvider={expandedProvider}
|
expandedProvider={expandedProvider}
|
||||||
isLoading={isWebSearchLoading}
|
isLoading={isWebSearchLoading}
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,6 @@ export function useToolsPage() {
|
||||||
return new Map(providers.map((provider) => [provider.id, provider.label]))
|
return new Map(providers.map((provider) => [provider.id, provider.label]))
|
||||||
}, [webSearchDraft])
|
}, [webSearchDraft])
|
||||||
|
|
||||||
const currentProviderLabel = webSearchDraft?.current_service
|
|
||||||
? (providerLabelMap.get(webSearchDraft.current_service) ??
|
|
||||||
webSearchDraft.current_service)
|
|
||||||
: t("pages.agent.tools.web_search.none", "None")
|
|
||||||
|
|
||||||
const pendingToolName = toggleToolMutation.isPending
|
const pendingToolName = toggleToolMutation.isPending
|
||||||
? (toggleToolMutation.variables?.name ?? null)
|
? (toggleToolMutation.variables?.name ?? null)
|
||||||
: null
|
: null
|
||||||
|
|
@ -168,7 +163,6 @@ export function useToolsPage() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTab,
|
activeTab,
|
||||||
currentProviderLabel,
|
|
||||||
expandedProvider,
|
expandedProvider,
|
||||||
groupedTools: groupedTools.groupedTools,
|
groupedTools: groupedTools.groupedTools,
|
||||||
pendingToolName,
|
pendingToolName,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function WebSearchGeneralSettings({
|
||||||
label={t("pages.agent.tools.web_search.provider", "Primary Provider")}
|
label={t("pages.agent.tools.web_search.provider", "Primary Provider")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.agent.tools.web_search.provider_description",
|
"pages.agent.tools.web_search.provider_description",
|
||||||
"Select the default search engine that agents will fallback to.",
|
"Select the default provider to use when the web search tool handles a request.",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -95,7 +95,7 @@ export function WebSearchGeneralSettings({
|
||||||
)}
|
)}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.agent.tools.web_search.prefer_native_hint",
|
"pages.agent.tools.web_search.prefer_native_hint",
|
||||||
"Bypass external providers if the agent inherently supports web search tools.",
|
"When enabled, the model may use its built-in search capability instead of the configured provider list.",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { WebSearchProviderSettings } from "./web-search-provider-settings"
|
||||||
|
|
||||||
interface WebSearchTabProps {
|
interface WebSearchTabProps {
|
||||||
draft: WebSearchConfigResponse | null
|
draft: WebSearchConfigResponse | null
|
||||||
currentProviderLabel: string
|
|
||||||
providerLabelMap: Map<string, string>
|
providerLabelMap: Map<string, string>
|
||||||
expandedProvider: string | null
|
expandedProvider: string | null
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
|
@ -23,7 +22,6 @@ interface WebSearchTabProps {
|
||||||
|
|
||||||
export function WebSearchTab({
|
export function WebSearchTab({
|
||||||
draft,
|
draft,
|
||||||
currentProviderLabel,
|
|
||||||
providerLabelMap,
|
providerLabelMap,
|
||||||
expandedProvider,
|
expandedProvider,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
@ -52,21 +50,16 @@ export function WebSearchTab({
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div className="max-w-xl space-y-3">
|
<div className="max-w-xl space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
|
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight">
|
||||||
{t(
|
{t(
|
||||||
"pages.agent.tools.web_search.title",
|
"pages.agent.tools.web_search.title",
|
||||||
"Web Search Configuration",
|
"Web Search Configuration",
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-[11px] font-semibold tracking-wide text-emerald-600 uppercase dark:text-emerald-400">
|
|
||||||
{currentProviderLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground/80 text-[14px] leading-relaxed">
|
<p className="text-muted-foreground/80 text-[14px] leading-relaxed">
|
||||||
{t(
|
{t(
|
||||||
"pages.agent.tools.web_search.description",
|
"pages.agent.tools.web_search.description",
|
||||||
"Provide web search capability for agents to find the latest real-world info. Automatically routes to the optimal active provider.",
|
"Configure how the web search tool behaves by default, including whether the model may use its built-in search capability.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -555,16 +555,15 @@
|
||||||
"providers_config": "Integrations",
|
"providers_config": "Integrations",
|
||||||
"load_error": "Failed to load web search configuration.",
|
"load_error": "Failed to load web search configuration.",
|
||||||
"save": "Save Changes",
|
"save": "Save Changes",
|
||||||
|
"open_settings": "Open Settings",
|
||||||
"save_success": "Settings saved successfully.",
|
"save_success": "Settings saved successfully.",
|
||||||
"save_error": "Failed to save settings.",
|
"save_error": "Failed to save settings.",
|
||||||
"current_active": "Active: ",
|
|
||||||
"current_service": "Current Service",
|
|
||||||
"provider": "Primary Provider",
|
"provider": "Primary Provider",
|
||||||
"provider_description": "Select the default search engine that agents will fallback to.",
|
"provider_description": "Select the default provider to use when the web search tool handles a request.",
|
||||||
"proxy": "HTTPS Proxy",
|
"proxy": "HTTPS Proxy",
|
||||||
"proxy_description": "Optional global HTTP/S proxy for underlying web requests.",
|
"proxy_description": "Optional global HTTP/S proxy for underlying web requests.",
|
||||||
"prefer_native": "Prefer Native Search",
|
"prefer_native": "Prefer Native Search",
|
||||||
"prefer_native_hint": "Bypass external providers if the agent inherently supports web search tools.",
|
"prefer_native_hint": "When enabled, the model may use its built-in search capability instead of the configured provider list.",
|
||||||
"provider_hint": "Enable this provider and fill any required connection settings.",
|
"provider_hint": "Enable this provider and fill any required connection settings.",
|
||||||
"max_results": "Max Results",
|
"max_results": "Max Results",
|
||||||
"base_url": "Base URL",
|
"base_url": "Base URL",
|
||||||
|
|
@ -592,7 +591,8 @@
|
||||||
"requires_linux": "This tool only works on Linux hosts with the required device files exposed.",
|
"requires_linux": "This tool only works on Linux hosts with the required device files exposed.",
|
||||||
"requires_skills": "Enable `tools.skills` before this skill-registry tool can be used.",
|
"requires_skills": "Enable `tools.skills` before this skill-registry tool can be used.",
|
||||||
"requires_subagent": "Enable `tools.subagent` before the spawn tool can delegate work.",
|
"requires_subagent": "Enable `tools.subagent` before the spawn tool can delegate work.",
|
||||||
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available."
|
"requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available.",
|
||||||
|
"requires_web_search_provider": "Configure at least one ready external web-search provider."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -555,16 +555,15 @@
|
||||||
"providers_config": "集成",
|
"providers_config": "集成",
|
||||||
"load_error": "加载 Web Search 配置失败。",
|
"load_error": "加载 Web Search 配置失败。",
|
||||||
"save": "保存更改",
|
"save": "保存更改",
|
||||||
|
"open_settings": "打开设置",
|
||||||
"save_success": "设置保存成功。",
|
"save_success": "设置保存成功。",
|
||||||
"save_error": "保存设置失败。",
|
"save_error": "保存设置失败。",
|
||||||
"current_active": "活动: ",
|
|
||||||
"current_service": "当前服务",
|
|
||||||
"provider": "首选服务",
|
"provider": "首选服务",
|
||||||
"provider_description": "选择智能体在默认情况下进行网络搜索的回退引擎。",
|
"provider_description": "选择在由 Web Search 工具处理请求时默认使用的搜索引擎。",
|
||||||
"proxy": "HTTPS 代理",
|
"proxy": "HTTPS 代理",
|
||||||
"proxy_description": "用于底层网页请求的可选全局代理配置。",
|
"proxy_description": "用于底层网页请求的可选全局代理配置。",
|
||||||
"prefer_native": "优先使用模型搜索",
|
"prefer_native": "优先使用模型搜索",
|
||||||
"prefer_native_hint": "如果当前模型本身支持联网功能,则直接使用模型自带的搜索能力",
|
"prefer_native_hint": "启用后,模型在支持时可以直接使用自身搜索能力,而不必走已配置的搜索引擎列表。",
|
||||||
"provider_hint": "启用该服务后,可继续填写所需的连接参数。",
|
"provider_hint": "启用该服务后,可继续填写所需的连接参数。",
|
||||||
"max_results": "最大获取结果数",
|
"max_results": "最大获取结果数",
|
||||||
"base_url": "API 请求地址",
|
"base_url": "API 请求地址",
|
||||||
|
|
@ -592,7 +591,8 @@
|
||||||
"requires_linux": "该工具仅在 Linux 主机上可用,并且需要暴露对应的设备文件。",
|
"requires_linux": "该工具仅在 Linux 主机上可用,并且需要暴露对应的设备文件。",
|
||||||
"requires_skills": "需要先启用 `tools.skills`,该技能注册表工具才能使用。",
|
"requires_skills": "需要先启用 `tools.skills`,该技能注册表工具才能使用。",
|
||||||
"requires_subagent": "需要先启用 `tools.subagent`,`spawn` 才能委派任务。",
|
"requires_subagent": "需要先启用 `tools.subagent`,`spawn` 才能委派任务。",
|
||||||
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`,MCP 发现工具才会可用。"
|
"requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`,MCP 发现工具才会可用。",
|
||||||
|
"requires_web_search_provider": "请至少配置一个可用的外部网络搜索 provider。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue