206 lines
4.6 KiB
Go
206 lines
4.6 KiB
Go
package websearch
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
libbrave "haixun-backend/internal/library/brave"
|
|
libexa "haixun-backend/internal/library/exa"
|
|
)
|
|
|
|
type Provider string
|
|
|
|
const (
|
|
ProviderBrave Provider = "brave"
|
|
ProviderExa Provider = "exa"
|
|
)
|
|
|
|
func ParseProvider(raw string) Provider {
|
|
switch Provider(strings.ToLower(strings.TrimSpace(raw))) {
|
|
case ProviderExa:
|
|
return ProviderExa
|
|
default:
|
|
return ProviderBrave
|
|
}
|
|
}
|
|
|
|
type Mode string
|
|
|
|
const (
|
|
ModeKnowledgeExpand Mode = "knowledge_expand"
|
|
ModeThreadsDiscover Mode = "threads_discover"
|
|
)
|
|
|
|
type SearchResult struct {
|
|
Title string
|
|
Snippet string
|
|
URL string
|
|
}
|
|
|
|
type SearchResponse struct {
|
|
Results []SearchResult
|
|
Query string
|
|
Status string
|
|
Provider Provider
|
|
}
|
|
|
|
type SearchOptions struct {
|
|
Query string
|
|
Limit int
|
|
Mode Mode
|
|
Country string
|
|
SearchLang string
|
|
UserLocation string
|
|
StartPublishedDate string
|
|
}
|
|
|
|
type Client interface {
|
|
Search(ctx context.Context, opts SearchOptions) (SearchResponse, error)
|
|
Enabled() bool
|
|
Provider() Provider
|
|
}
|
|
|
|
type Config struct {
|
|
Provider Provider
|
|
BraveKey string
|
|
ExaKey string
|
|
Country string
|
|
SearchLang string
|
|
UserLocation string
|
|
}
|
|
|
|
func New(cfg Config) Client {
|
|
switch ParseProvider(string(cfg.Provider)) {
|
|
case ProviderExa:
|
|
return &exaAdapter{
|
|
client: libexa.NewClient(cfg.ExaKey),
|
|
provider: ProviderExa,
|
|
userLocation: cfg.UserLocation,
|
|
}
|
|
default:
|
|
return &braveAdapter{
|
|
client: libbrave.NewClient(cfg.BraveKey),
|
|
provider: ProviderBrave,
|
|
country: cfg.Country,
|
|
searchLang: cfg.SearchLang,
|
|
}
|
|
}
|
|
}
|
|
|
|
func ConfigFromMember(braveKey, exaKey, provider, country, searchLang, userLocation string) Config {
|
|
return Config{
|
|
Provider: ParseProvider(provider),
|
|
BraveKey: braveKey,
|
|
ExaKey: exaKey,
|
|
Country: country,
|
|
SearchLang: searchLang,
|
|
UserLocation: userLocation,
|
|
}
|
|
}
|
|
|
|
type braveAdapter struct {
|
|
client *libbrave.Client
|
|
provider Provider
|
|
country string
|
|
searchLang string
|
|
}
|
|
|
|
func (a *braveAdapter) Provider() Provider { return a.provider }
|
|
|
|
func (a *braveAdapter) Enabled() bool {
|
|
return a != nil && a.client != nil && a.client.Enabled()
|
|
}
|
|
|
|
func (a *braveAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) {
|
|
mode := libbrave.ModeKnowledgeExpand
|
|
if opts.Mode == ModeThreadsDiscover {
|
|
mode = libbrave.ModeThreadsDiscover
|
|
}
|
|
res, err := a.client.Search(ctx, libbrave.SearchOptions{
|
|
Query: opts.Query,
|
|
Limit: opts.Limit,
|
|
Mode: mode,
|
|
Country: firstNonEmpty(opts.Country, a.country),
|
|
SearchLang: firstNonEmpty(opts.SearchLang, a.searchLang),
|
|
})
|
|
return toBraveResponse(res.Query, res.Status, a.provider, res.Results, err)
|
|
}
|
|
|
|
type exaAdapter struct {
|
|
client *libexa.Client
|
|
provider Provider
|
|
userLocation string
|
|
}
|
|
|
|
func (a *exaAdapter) Provider() Provider { return a.provider }
|
|
|
|
func (a *exaAdapter) Enabled() bool {
|
|
return a != nil && a.client != nil && a.client.Enabled()
|
|
}
|
|
|
|
func (a *exaAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) {
|
|
mode := libexa.ModeKnowledgeExpand
|
|
if opts.Mode == ModeThreadsDiscover {
|
|
mode = libexa.ModeThreadsDiscover
|
|
}
|
|
res, err := a.client.Search(ctx, libexa.SearchOptions{
|
|
Query: opts.Query,
|
|
Limit: opts.Limit,
|
|
Mode: mode,
|
|
UserLocation: firstNonEmpty(opts.UserLocation, a.userLocation),
|
|
StartPublishedDate: opts.StartPublishedDate,
|
|
})
|
|
out := SearchResponse{Query: res.Query, Status: res.Status, Provider: a.provider}
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
for _, item := range res.Results {
|
|
out.Results = append(out.Results, SearchResult{
|
|
Title: item.Title,
|
|
Snippet: item.Snippet,
|
|
URL: item.URL,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func toBraveResponse(query, status string, provider Provider, items []libbrave.SearchResult, err error) (SearchResponse, error) {
|
|
out := SearchResponse{Query: query, Status: status, Provider: provider}
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
for _, item := range items {
|
|
out.Results = append(out.Results, SearchResult{
|
|
Title: item.Title,
|
|
Snippet: item.Snippet,
|
|
URL: item.URL,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func ActiveAPIKey(cfg Config) string {
|
|
if ParseProvider(string(cfg.Provider)) == ProviderExa {
|
|
return strings.TrimSpace(cfg.ExaKey)
|
|
}
|
|
return strings.TrimSpace(cfg.BraveKey)
|
|
}
|
|
|
|
func ProviderLabel(provider Provider) string {
|
|
switch provider {
|
|
case ProviderExa:
|
|
return "Exa"
|
|
default:
|
|
return "Brave"
|
|
}
|
|
}
|