feat: Add Perplexity search provider integration (#138)

* feat: Add Perplexity search provider integration

- Add PerplexityConfig struct to config package
- Add PerplexitySearchProvider implementing SearchProvider interface
- Update WebSearchTool to support Perplexity with priority system (Perplexity > Brave > DuckDuckGo)
- Update agent loop to pass Perplexity config options
- Update config.example.json with Perplexity configuration template
- Uses Perplexity's 'sonar' model for web search capabilities

* Edit config example

* make fmt

---------

Co-authored-by: Hua <zhangmikoto@gmail.com>
This commit is contained in:
Hua Audio 2026-02-17 14:02:56 +01:00 committed by GitHub
parent da79c201c7
commit f929268ab2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 5 deletions

View file

@ -14,7 +14,9 @@
"enabled": false, "enabled": false,
"token": "YOUR_TELEGRAM_BOT_TOKEN", "token": "YOUR_TELEGRAM_BOT_TOKEN",
"proxy": "", "proxy": "",
"allow_from": ["YOUR_USER_ID"] "allow_from": [
"YOUR_USER_ID"
]
}, },
"discord": { "discord": {
"enabled": false, "enabled": false,
@ -115,9 +117,15 @@
}, },
"tools": { "tools": {
"web": { "web": {
"search": { "brave": {
"enabled": false,
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
},
"perplexity": {
"enabled": false,
"api_key": "pplx-xxx",
"max_results": 5
} }
} }
}, },

View file

@ -79,6 +79,9 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
BraveEnabled: cfg.Tools.Web.Brave.Enabled, BraveEnabled: cfg.Tools.Web.Brave.Enabled,
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults, DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled, 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 { }); searchTool != nil {
registry.Register(searchTool) registry.Register(searchTool)
} }

View file

@ -206,9 +206,16 @@ type DuckDuckGoConfig struct {
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` 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 { type WebToolsConfig struct {
Brave BraveConfig `json:"brave"` Brave BraveConfig `json:"brave"`
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"` DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
Perplexity PerplexityConfig `json:"perplexity"`
} }
type ToolsConfig struct { type ToolsConfig struct {
@ -321,6 +328,11 @@ func DefaultConfig() *Config {
Enabled: true, Enabled: true,
MaxResults: 5, MaxResults: 5,
}, },
Perplexity: PerplexityConfig{
Enabled: false,
APIKey: "",
MaxResults: 5,
},
}, },
}, },
Heartbeat: HeartbeatConfig{ Heartbeat: HeartbeatConfig{

View file

@ -176,6 +176,71 @@ func stripTags(content string) string {
return re.ReplaceAllString(content, "") 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 { type WebSearchTool struct {
provider SearchProvider provider SearchProvider
maxResults int maxResults int
@ -187,14 +252,22 @@ type WebSearchToolOptions struct {
BraveEnabled bool BraveEnabled bool
DuckDuckGoMaxResults int DuckDuckGoMaxResults int
DuckDuckGoEnabled bool DuckDuckGoEnabled bool
PerplexityAPIKey string
PerplexityMaxResults int
PerplexityEnabled bool
} }
func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
var provider SearchProvider var provider SearchProvider
maxResults := 5 maxResults := 5
// Priority: Brave > DuckDuckGo // Priority: Perplexity > Brave > DuckDuckGo
if opts.BraveEnabled && opts.BraveAPIKey != "" { 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} provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey}
if opts.BraveMaxResults > 0 { if opts.BraveMaxResults > 0 {
maxResults = opts.BraveMaxResults maxResults = opts.BraveMaxResults