2025-08-14 17:08:45 +02:00
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
2025-10-21 00:42:00 +02:00
"strings"
2025-08-31 00:25:03 +02:00
"time"
2025-08-14 17:08:45 +02:00
)
type ExaResult struct {
2025-08-31 00:25:03 +02:00
Title string ` json:"title" `
URL string ` json:"url" `
PublishedDate string ` json:"publishedDate,omitempty" `
SiteName string ` json:"siteName,omitempty" `
Summary string ` json:"summary,omitempty" `
Highlights [ ] string ` json:"highlights,omitempty" `
Text string ` json:"text,omitempty" `
2025-08-14 17:08:45 +02:00
}
2025-08-28 14:46:28 +02:00
type ExaCost struct {
Total float64 ` json:"total" `
}
2025-08-14 17:08:45 +02:00
type ExaResults struct {
2025-08-31 00:25:03 +02:00
RequestID string ` json:"requestId" `
SearchType string ` json:"resolvedSearchType" `
Results [ ] ExaResult ` json:"results" `
Cost ExaCost ` json:"costDollars" `
2025-08-14 17:08:45 +02:00
}
func ( e * ExaResults ) String ( ) string {
2025-08-31 23:46:22 +02:00
buf := GetFreeBuffer ( )
defer pool . Put ( buf )
2025-08-14 17:08:45 +02:00
2025-08-31 23:46:22 +02:00
json . NewEncoder ( buf ) . Encode ( map [ string ] any {
2025-08-31 00:25:03 +02:00
"results" : e . Results ,
} )
2025-08-14 17:08:45 +02:00
2025-08-31 23:46:22 +02:00
return buf . String ( )
2025-08-14 17:08:45 +02:00
}
func NewExaRequest ( ctx context . Context , path string , data any ) ( * http . Request , error ) {
buf , err := json . Marshal ( data )
if err != nil {
return nil , err
}
req , err := http . NewRequest ( "POST" , fmt . Sprintf ( "https://api.exa.ai%s" , path ) , bytes . NewReader ( buf ) )
if err != nil {
return nil , err
}
req = req . WithContext ( ctx )
req . Header . Set ( "Content-Type" , "application/json" )
2025-08-16 15:15:06 +02:00
req . Header . Set ( "X-Api-Key" , env . Tokens . Exa )
2025-08-14 17:08:45 +02:00
return req , nil
}
func RunExaRequest ( req * http . Request ) ( * ExaResults , error ) {
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
var result ExaResults
err = json . NewDecoder ( resp . Body ) . Decode ( & result )
if err != nil {
return nil , err
}
return & result , nil
}
func ExaRunSearch ( ctx context . Context , args SearchWebArguments ) ( * ExaResults , error ) {
2025-08-31 00:25:03 +02:00
if args . NumResults <= 0 {
args . NumResults = 6
} else if args . NumResults < 3 {
args . NumResults = 3
} else if args . NumResults >= 12 {
args . NumResults = 12
}
2025-10-21 00:42:00 +02:00
guidance := ExaGuidanceForIntent ( args )
2025-08-14 17:08:45 +02:00
data := map [ string ] any {
"query" : args . Query ,
"type" : "auto" ,
"numResults" : args . NumResults ,
2025-08-31 00:25:03 +02:00
}
if len ( args . Domains ) > 0 {
data [ "includeDomains" ] = args . Domains
}
contents := map [ string ] any {
2025-10-21 00:42:00 +02:00
"summary" : map [ string ] any {
"query" : guidance ,
2025-08-14 17:08:45 +02:00
} ,
2025-08-31 00:25:03 +02:00
"livecrawl" : "preferred" ,
}
2025-10-21 00:42:00 +02:00
highlights := map [ string ] any {
"numSentences" : 2 ,
"highlightsPerUrl" : 3 ,
"query" : guidance ,
}
2025-08-31 00:25:03 +02:00
switch args . Intent {
case "news" :
2025-10-21 00:42:00 +02:00
highlights [ "highlightsPerUrl" ] = 2
2025-08-31 00:25:03 +02:00
data [ "category" ] = "news"
data [ "numResults" ] = max ( 8 , args . NumResults )
data [ "startPublishedDate" ] = daysAgo ( 30 )
case "docs" :
2025-10-21 00:42:00 +02:00
highlights [ "numSentences" ] = 3
highlights [ "highlightsPerUrl" ] = 4
2025-08-31 00:25:03 +02:00
contents [ "subpages" ] = 1
contents [ "subpageTarget" ] = [ ] string { "documentation" , "changelog" , "release notes" }
case "papers" :
2025-10-21 00:42:00 +02:00
highlights [ "numSentences" ] = 4
highlights [ "highlightsPerUrl" ] = 4
2025-08-31 00:25:03 +02:00
data [ "category" ] = "research paper"
data [ "startPublishedDate" ] = daysAgo ( 365 * 2 )
case "code" :
2025-10-21 00:42:00 +02:00
highlights [ "highlightsPerUrl" ] = 4
2025-08-31 00:25:03 +02:00
contents [ "subpages" ] = 1
contents [ "subpageTarget" ] = [ ] string { "readme" , "changelog" , "code" }
contents [ "text" ] = map [ string ] any {
"maxCharacters" : 8000 ,
}
2025-10-21 00:42:00 +02:00
data [ "category" ] = "github"
case "deep_read" :
highlights [ "numSentences" ] = 3
highlights [ "highlightsPerUrl" ] = 5
contents [ "text" ] = map [ string ] any {
"maxCharacters" : 12000 ,
}
2025-08-31 00:25:03 +02:00
}
2025-10-21 00:42:00 +02:00
contents [ "highlights" ] = highlights
2025-08-31 00:25:03 +02:00
data [ "contents" ] = contents
switch args . Recency {
case "month" :
data [ "startPublishedDate" ] = daysAgo ( 30 )
case "year" :
2025-09-22 03:50:26 +02:00
data [ "startPublishedDate" ] = daysAgo ( 365 )
2025-08-14 17:08:45 +02:00
}
req , err := NewExaRequest ( ctx , "/search" , data )
if err != nil {
return nil , err
}
return RunExaRequest ( req )
}
func ExaRunContents ( ctx context . Context , args FetchContentsArguments ) ( * ExaResults , error ) {
data := map [ string ] any {
2025-08-31 00:25:03 +02:00
"urls" : args . URLs ,
"summary" : map [ string ] any { } ,
"highlights" : map [ string ] any {
"numSentences" : 2 ,
"highlightsPerUrl" : 3 ,
} ,
2025-08-14 17:08:45 +02:00
"text" : map [ string ] any {
"maxCharacters" : 8000 ,
} ,
2025-08-31 00:25:03 +02:00
"livecrawl" : "preferred" ,
2025-08-14 17:08:45 +02:00
}
req , err := NewExaRequest ( ctx , "/contents" , data )
if err != nil {
return nil , err
}
return RunExaRequest ( req )
}
2025-08-31 00:25:03 +02:00
func daysAgo ( days int ) string {
2025-09-22 03:50:26 +02:00
return time . Now ( ) . Add ( - time . Duration ( days ) * 24 * time . Hour ) . Format ( time . DateOnly )
2025-08-31 00:25:03 +02:00
}
2025-10-21 00:42:00 +02:00
func ExaGuidanceForIntent ( args SearchWebArguments ) string {
var recency string
switch args . Recency {
case "month" :
recency = " since " + daysAgo ( 30 )
case "year" :
recency = " since " + daysAgo ( 365 )
}
goal := strings . TrimSpace ( args . Query )
switch args . Intent {
case "news" :
return "Give who/what/when/where and key numbers" + recency +
". Include dates and named sources; 2-4 bullets. Note disagreements. Ignore speculation."
case "docs" :
return "Extract install command, minimal example, breaking changes" + recency + ", key config options with defaults, and deprecations. Prefer official docs and release notes."
case "papers" :
return "Summarize problem, method, dataset, metrics (with numbers), baselines, novelty, and limitations; include year/venue."
case "code" :
return "Summarize repo purpose, language, license, last release/commit" + recency + ", install steps and minimal example; note breaking changes. Prefer README/docs."
case "deep_read" :
return "Answer: " + goal + ". Extract exact numbers, dates, quotes (with speaker) plus 1-2 sentences of context."
}
return "Focus on answering: " + goal + ". Provide dates, versions, key numbers; 3-5 concise bullets. Ignore marketing fluff."
}