web_search.go

 1package tools
 2
 3import (
 4	"context"
 5	_ "embed"
 6	"log/slog"
 7	"net/http"
 8	"time"
 9
10	"charm.land/fantasy"
11)
12
13//go:embed web_search.md
14var webSearchToolDescription []byte
15
16// NewWebSearchTool creates a web search tool for sub-agents (no permissions needed).
17func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
18	if client == nil {
19		transport := http.DefaultTransport.(*http.Transport).Clone()
20		transport.MaxIdleConns = 100
21		transport.MaxIdleConnsPerHost = 10
22		transport.IdleConnTimeout = 90 * time.Second
23
24		client = &http.Client{
25			Timeout:   30 * time.Second,
26			Transport: transport,
27		}
28	}
29
30	return fantasy.NewParallelAgentTool(
31		WebSearchToolName,
32		string(webSearchToolDescription),
33		func(ctx context.Context, params WebSearchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
34			if params.Query == "" {
35				return fantasy.NewTextErrorResponse("query is required"), nil
36			}
37
38			maxResults := params.MaxResults
39			if maxResults <= 0 {
40				maxResults = 10
41			}
42			if maxResults > 20 {
43				maxResults = 20
44			}
45
46			maybeDelaySearch()
47			results, err := searchDuckDuckGo(ctx, client, params.Query, maxResults)
48			slog.Debug("Web search completed", "query", params.Query, "results", len(results), "err", err)
49			if err != nil {
50				return fantasy.NewTextErrorResponse("Failed to search: " + err.Error()), nil
51			}
52
53			return fantasy.NewTextResponse(formatSearchResults(results)), nil
54		})
55}