mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-03 04:54:36 +00:00
Port Exa and Tavily provider clients from PR #825 into the tenant-aware overlay architecture (builtin_tool_tenant_configs.settings). - Add ExaConfig + TavilyConfig to WebToolsConfig with provider_order - Add Exa HTTP client (api.exa.ai/search, x-api-key auth) - Add Tavily HTTP client (api.tavily.com/search, Bearer auth) - Extend Brave + DDG constructors with per-provider maxResults - Add config_secrets plumbing for exa/tavily API keys (apply/collect/mask/strip) - Refactor gateway_setup.go to use WebSearchConfigFromConfig - Add NormalizeWebSearchProviderOrder (DDG always last, dedup, unknown skip) - Extract web_search_config.go (builder, normalizer, shared helpers) - 11 new unit tests for provider order, builder, clamp, normalize Credit: @kaitranntt (PR #825) for the original Exa + Tavily implementation.
85 lines
2.3 KiB
Go
85 lines
2.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type exaSearchProvider struct {
|
|
apiKey string
|
|
maxResults int
|
|
client *http.Client
|
|
}
|
|
|
|
func newExaSearchProvider(apiKey string, maxResults int) *exaSearchProvider {
|
|
return &exaSearchProvider{
|
|
apiKey: apiKey,
|
|
maxResults: normalizeProviderMaxResults(maxResults),
|
|
client: &http.Client{Timeout: time.Duration(searchTimeoutSeconds) * time.Second},
|
|
}
|
|
}
|
|
|
|
func (p *exaSearchProvider) Name() string { return searchProviderExa }
|
|
|
|
func (p *exaSearchProvider) Search(ctx context.Context, params searchParams) ([]searchResult, error) {
|
|
requestBody, err := json.Marshal(map[string]any{
|
|
"query": params.Query,
|
|
"type": "auto",
|
|
"numResults": clampProviderResultCount(params.Count, p.maxResults),
|
|
"text": true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exaSearchEndpoint, bytes.NewReader(requestBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", webSearchUserAgent)
|
|
req.Header.Set("x-api-key", p.apiKey)
|
|
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("exa API returned %d: %s", resp.StatusCode, truncateStr(string(body), 200))
|
|
}
|
|
|
|
var exaResp struct {
|
|
Results []struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Text string `json:"text"`
|
|
Summary string `json:"summary"`
|
|
} `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(body, &exaResp); err != nil {
|
|
return nil, fmt.Errorf("parse response: %w", err)
|
|
}
|
|
|
|
results := make([]searchResult, 0, len(exaResp.Results))
|
|
for _, r := range exaResp.Results {
|
|
results = append(results, searchResult{
|
|
Title: coalesceSearchText(r.Title, r.URL, "Untitled"),
|
|
URL: r.URL,
|
|
Description: truncateStr(coalesceSearchText(r.Text, r.Summary, ""), 240),
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|