mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
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:
+1
-27
@@ -100,33 +100,7 @@ func registerSharedTools(
|
||||
}
|
||||
|
||||
if cfg.Tools.IsToolEnabled("web") {
|
||||
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||
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,
|
||||
})
|
||||
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptionsFromConfig(cfg))
|
||||
if err != nil {
|
||||
logger.ErrorCF("agent", "Failed to create web search tool", map[string]any{"error": err.Error()})
|
||||
} else if searchTool != nil {
|
||||
|
||||
@@ -161,6 +161,58 @@ func newTestAgentLoop(
|
||||
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) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
|
||||
@@ -39,10 +39,10 @@ func (p *Pipeline) CallLLM(
|
||||
exec.providerToolDefs = ts.agent.Tools.ToProviderDefs()
|
||||
|
||||
// Native web search support
|
||||
_, hasWebSearch := ts.agent.Tools.Get("web_search")
|
||||
exec.useNativeSearch = al.cfg.Tools.Web.PreferNative && hasWebSearch &&
|
||||
webSearchEnabled := al.cfg.Tools.IsToolEnabled("web")
|
||||
exec.useNativeSearch = webSearchEnabled && al.cfg.Tools.Web.PreferNative &&
|
||||
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 false
|
||||
|
||||
@@ -36,6 +36,35 @@ func (p *simpleConvProvider) GetDefaultModel() string {
|
||||
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
|
||||
type toolCallRespProvider struct {
|
||||
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) {
|
||||
errorPrv := &errorProvider{errType: "timeout"}
|
||||
al, agent, cleanup := newTurnCoordTestLoop(t, errorPrv)
|
||||
|
||||
Reference in New Issue
Block a user