@@ -176,6 +176,71 @@ func stripTags(content string) string {
176176 return re .ReplaceAllString (content , "" )
177177}
178178
179+ type PerplexitySearchProvider struct {
180+ apiKey string
181+ }
182+
183+ func (p * PerplexitySearchProvider ) Search (ctx context.Context , query string , count int ) (string , error ) {
184+ searchURL := "https://api.perplexity.ai/chat/completions"
185+
186+ payload := map [string ]interface {}{
187+ "model" : "sonar" ,
188+ "messages" : []map [string ]string {
189+ {"role" : "system" , "content" : "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n 1. Title\n URL\n Description\n \n Do not add extra commentary." },
190+ {"role" : "user" , "content" : fmt .Sprintf ("Search for: %s. Provide up to %d relevant results." , query , count )},
191+ },
192+ "max_tokens" : 1000 ,
193+ }
194+
195+ payloadBytes , err := json .Marshal (payload )
196+ if err != nil {
197+ return "" , fmt .Errorf ("failed to marshal request: %w" , err )
198+ }
199+
200+ req , err := http .NewRequestWithContext (ctx , "POST" , searchURL , strings .NewReader (string (payloadBytes )))
201+ if err != nil {
202+ return "" , fmt .Errorf ("failed to create request: %w" , err )
203+ }
204+
205+ req .Header .Set ("Content-Type" , "application/json" )
206+ req .Header .Set ("Authorization" , "Bearer " + p .apiKey )
207+ req .Header .Set ("User-Agent" , userAgent )
208+
209+ client := & http.Client {Timeout : 30 * time .Second }
210+ resp , err := client .Do (req )
211+ if err != nil {
212+ return "" , fmt .Errorf ("request failed: %w" , err )
213+ }
214+ defer resp .Body .Close ()
215+
216+ body , err := io .ReadAll (resp .Body )
217+ if err != nil {
218+ return "" , fmt .Errorf ("failed to read response: %w" , err )
219+ }
220+
221+ if resp .StatusCode != http .StatusOK {
222+ return "" , fmt .Errorf ("Perplexity API error: %s" , string (body ))
223+ }
224+
225+ var searchResp struct {
226+ Choices []struct {
227+ Message struct {
228+ Content string `json:"content"`
229+ } `json:"message"`
230+ } `json:"choices"`
231+ }
232+
233+ if err := json .Unmarshal (body , & searchResp ); err != nil {
234+ return "" , fmt .Errorf ("failed to parse response: %w" , err )
235+ }
236+
237+ if len (searchResp .Choices ) == 0 {
238+ return fmt .Sprintf ("No results for: %s" , query ), nil
239+ }
240+
241+ return fmt .Sprintf ("Results for: %s (via Perplexity)\n %s" , query , searchResp .Choices [0 ].Message .Content ), nil
242+ }
243+
179244type WebSearchTool struct {
180245 provider SearchProvider
181246 maxResults int
@@ -187,14 +252,22 @@ type WebSearchToolOptions struct {
187252 BraveEnabled bool
188253 DuckDuckGoMaxResults int
189254 DuckDuckGoEnabled bool
255+ PerplexityAPIKey string
256+ PerplexityMaxResults int
257+ PerplexityEnabled bool
190258}
191259
192260func NewWebSearchTool (opts WebSearchToolOptions ) * WebSearchTool {
193261 var provider SearchProvider
194262 maxResults := 5
195263
196- // Priority: Brave > DuckDuckGo
197- if opts .BraveEnabled && opts .BraveAPIKey != "" {
264+ // Priority: Perplexity > Brave > DuckDuckGo
265+ if opts .PerplexityEnabled && opts .PerplexityAPIKey != "" {
266+ provider = & PerplexitySearchProvider {apiKey : opts .PerplexityAPIKey }
267+ if opts .PerplexityMaxResults > 0 {
268+ maxResults = opts .PerplexityMaxResults
269+ }
270+ } else if opts .BraveEnabled && opts .BraveAPIKey != "" {
198271 provider = & BraveSearchProvider {apiKey : opts .BraveAPIKey }
199272 if opts .BraveMaxResults > 0 {
200273 maxResults = opts .BraveMaxResults
0 commit comments