104
play-life-llm/internal/tavily/client.go
Normal file
104
play-life-llm/internal/tavily/client.go
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user