package tavily import ( "bytes" "encoding/json" "fmt" "net/http" "time" ) const ( baseURL = "https://api.tavily.com" searchPath = "/search" timeout = 30 * time.Second ) // Client calls Tavily Search API. type Client struct { APIKey string HTTPClient *http.Client } // NewClient creates a Tavily client. apiKey is required for search. func NewClient(apiKey string) *Client { return &Client{ APIKey: apiKey, HTTPClient: &http.Client{ Timeout: timeout, }, } } // SearchRequest is the POST body for /search. type SearchRequest struct { Query string `json:"query"` SearchDepth string `json:"search_depth,omitempty"` // basic, advanced, etc. MaxResults int `json:"max_results,omitempty"` } // SearchResult is one result item. type SearchResult struct { Title string `json:"title"` URL string `json:"url"` Content string `json:"content"` } // SearchResponse is the Tavily search response. type SearchResponse struct { Query string `json:"query"` Answer string `json:"answer,omitempty"` Results []SearchResult `json:"results"` } // Search runs a web search and returns a single text suitable for passing to Ollama as tool result. func (c *Client) Search(query string) (string, error) { if c.APIKey == "" { return "", fmt.Errorf("tavily: API key not set") } body, err := json.Marshal(SearchRequest{ Query: query, MaxResults: 5, }) if err != nil { return "", fmt.Errorf("marshal request: %w", err) } url := baseURL + searchPath req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("new request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.APIKey) resp, err := c.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("do request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("tavily returned %d", resp.StatusCode) } var out SearchResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", fmt.Errorf("decode response: %w", err) } // Build a single text for the model: prefer answer if present, else concatenate results. if out.Answer != "" { return out.Answer, nil } var b bytes.Buffer for i, r := range out.Results { if i > 0 { b.WriteString("\n\n") } b.WriteString(r.Title) b.WriteString(": ") b.WriteString(r.Content) } return b.String(), nil }