diff --git a/config/config.example.json b/config/config.example.json index 3c9158e9c..62ad2c5fe 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -14,7 +14,9 @@ "enabled": false, "token": "YOUR_TELEGRAM_BOT_TOKEN", "proxy": "", - "allow_from": ["YOUR_USER_ID"] + "allow_from": [ + "YOUR_USER_ID" + ] }, "discord": { "enabled": false, @@ -115,9 +117,15 @@ }, "tools": { "web": { - "search": { + "brave": { + "enabled": false, "api_key": "YOUR_BRAVE_API_KEY", "max_results": 5 + }, + "perplexity": { + "enabled": false, + "api_key": "pplx-xxx", + "max_results": 5 } } }, @@ -133,4 +141,4 @@ "host": "0.0.0.0", "port": 18790 } -} +} \ No newline at end of file diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cd4276155..d3afa298e 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -79,6 +79,9 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg BraveEnabled: cfg.Tools.Web.Brave.Enabled, DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults, DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled, + PerplexityAPIKey: cfg.Tools.Web.Perplexity.APIKey, + PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults, + PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled, }); searchTool != nil { registry.Register(searchTool) } diff --git a/pkg/config/config.go b/pkg/config/config.go index d189ff00b..558bf6a93 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -206,9 +206,16 @@ type DuckDuckGoConfig struct { MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` } +type PerplexityConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` +} + type WebToolsConfig struct { Brave BraveConfig `json:"brave"` DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"` + Perplexity PerplexityConfig `json:"perplexity"` } type ToolsConfig struct { @@ -321,6 +328,11 @@ func DefaultConfig() *Config { Enabled: true, MaxResults: 5, }, + Perplexity: PerplexityConfig{ + Enabled: false, + APIKey: "", + MaxResults: 5, + }, }, }, Heartbeat: HeartbeatConfig{ diff --git a/pkg/tools/web.go b/pkg/tools/web.go index ccd995842..6a6d40ecf 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -176,6 +176,71 @@ func stripTags(content string) string { return re.ReplaceAllString(content, "") } +type PerplexitySearchProvider struct { + apiKey string +} + +func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) { + searchURL := "https://api.perplexity.ai/chat/completions" + + payload := map[string]interface{}{ + "model": "sonar", + "messages": []map[string]string{ + {"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."}, + {"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)}, + }, + "max_tokens": 1000, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", searchURL, strings.NewReader(string(payloadBytes))) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+p.apiKey) + req.Header.Set("User-Agent", userAgent) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Perplexity API error: %s", string(body)) + } + + var searchResp struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + + if err := json.Unmarshal(body, &searchResp); err != nil { + return "", fmt.Errorf("failed to parse response: %w", err) + } + + if len(searchResp.Choices) == 0 { + return fmt.Sprintf("No results for: %s", query), nil + } + + return fmt.Sprintf("Results for: %s (via Perplexity)\n%s", query, searchResp.Choices[0].Message.Content), nil +} + type WebSearchTool struct { provider SearchProvider maxResults int @@ -187,14 +252,22 @@ type WebSearchToolOptions struct { BraveEnabled bool DuckDuckGoMaxResults int DuckDuckGoEnabled bool + PerplexityAPIKey string + PerplexityMaxResults int + PerplexityEnabled bool } func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { var provider SearchProvider maxResults := 5 - // Priority: Brave > DuckDuckGo - if opts.BraveEnabled && opts.BraveAPIKey != "" { + // Priority: Perplexity > Brave > DuckDuckGo + if opts.PerplexityEnabled && opts.PerplexityAPIKey != "" { + provider = &PerplexitySearchProvider{apiKey: opts.PerplexityAPIKey} + if opts.PerplexityMaxResults > 0 { + maxResults = opts.PerplexityMaxResults + } + } else if opts.BraveEnabled && opts.BraveAPIKey != "" { provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey} if opts.BraveMaxResults > 0 { maxResults = opts.BraveMaxResults