feat: the assistant's search engine in Настройки - DuckDuckGo, a SearXNG of one's own, Brave Search or Tavily, with DuckDuckGo behind the chosen one
This commit is contained in:
1 parent
9bf30b281e
commit
9912255289
6 files changed
+200
-2
No files matched your search
+123
-1
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -74,6 +75,127 @@ func parseDDG(page string) []searchResult {
|
||||
|
||||
var searchClient = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
// The engine is the admins' choice (Настройки › ИИ-ассистент): DuckDuckGo,
|
||||
// a SearXNG of one's own, Brave Search or Tavily; when the chosen one fails,
|
||||
// DuckDuckGo answers instead.
|
||||
const (
|
||||
cfgSearchEngine = "ai_search"
|
||||
cfgSearchURL = "ai_search_url"
|
||||
cfgSearchKey = "ai_search_key"
|
||||
)
|
||||
|
||||
type searchConf struct{ Engine, URL, Key string }
|
||||
|
||||
func (s *Server) searchConf() searchConf {
|
||||
e, _ := s.db.GetConfig(cfgSearchEngine)
|
||||
u, _ := s.db.GetConfig(cfgSearchURL)
|
||||
k, _ := s.db.GetConfig(cfgSearchKey)
|
||||
return searchConf{Engine: e, URL: strings.TrimRight(u, "/"), Key: k}
|
||||
}
|
||||
|
||||
var (
|
||||
braveURL = "https://api.search.brave.com/res/v1/web/search"
|
||||
tavilyURL = "https://api.tavily.com/search"
|
||||
ddgSearch = webSearch
|
||||
)
|
||||
|
||||
// searchWith asks the engine c names; DuckDuckGo if that one fails.
|
||||
func searchWith(ctx context.Context, c searchConf, q string, n int) ([]searchResult, error) {
|
||||
var rs []searchResult
|
||||
var err error
|
||||
switch c.Engine {
|
||||
case "searxng":
|
||||
rs, err = searchSearx(ctx, c, q)
|
||||
case "brave":
|
||||
rs, err = searchBrave(ctx, c, q, n)
|
||||
case "tavily":
|
||||
rs, err = searchTavily(ctx, c, q, n)
|
||||
default:
|
||||
return ddgSearch(ctx, q, n)
|
||||
}
|
||||
if err != nil || len(rs) == 0 {
|
||||
return ddgSearch(ctx, q, n)
|
||||
}
|
||||
if len(rs) > n {
|
||||
rs = rs[:n]
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// getJSON fetches url (with the headers) into v.
|
||||
func getJSON(ctx context.Context, method, url string, body io.Reader, hdr map[string]string, v any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
for k, val := range hdr {
|
||||
req.Header.Set(k, val)
|
||||
}
|
||||
resp, err := searchClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return errors.New("поисковик ответил " + resp.Status)
|
||||
}
|
||||
return json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(v)
|
||||
}
|
||||
|
||||
// searchSearx: a SearXNG with JSON output on (search.formats: json).
|
||||
func searchSearx(ctx context.Context, c searchConf, q string) ([]searchResult, error) {
|
||||
if c.URL == "" {
|
||||
return nil, errors.New("не задан адрес SearXNG")
|
||||
}
|
||||
var out struct {
|
||||
Results []struct{ Title, URL, Content string } `json:"results"`
|
||||
}
|
||||
if err := getJSON(ctx, http.MethodGet, c.URL+"/search?format=json&q="+url.QueryEscape(q), nil, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rs []searchResult
|
||||
for _, r := range out.Results {
|
||||
rs = append(rs, searchResult{Title: r.Title, URL: r.URL, Snippet: text(r.Content)})
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func searchBrave(ctx context.Context, c searchConf, q string, n int) ([]searchResult, error) {
|
||||
var out struct {
|
||||
Web struct {
|
||||
Results []struct{ Title, URL, Description string } `json:"results"`
|
||||
} `json:"web"`
|
||||
}
|
||||
err := getJSON(ctx, http.MethodGet, braveURL+"?count="+strconv.Itoa(n)+"&q="+url.QueryEscape(q), nil,
|
||||
map[string]string{"X-Subscription-Token": c.Key}, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rs []searchResult
|
||||
for _, r := range out.Web.Results {
|
||||
rs = append(rs, searchResult{Title: text(r.Title), URL: r.URL, Snippet: text(r.Description)})
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func searchTavily(ctx context.Context, c searchConf, q string, n int) ([]searchResult, error) {
|
||||
b, _ := json.Marshal(map[string]any{"query": q, "max_results": n})
|
||||
var out struct {
|
||||
Results []struct{ Title, URL, Content string } `json:"results"`
|
||||
}
|
||||
err := getJSON(ctx, http.MethodPost, tavilyURL, strings.NewReader(string(b)),
|
||||
map[string]string{"Authorization": "Bearer " + c.Key, "Content-Type": "application/json"}, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rs []searchResult
|
||||
for _, r := range out.Results {
|
||||
rs = append(rs, searchResult{Title: r.Title, URL: r.URL, Snippet: r.Content})
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// webSearch asks DuckDuckGo for q: up to n results.
|
||||
func webSearch(ctx context.Context, q string, n int) ([]searchResult, error) {
|
||||
var last error
|
||||
@@ -119,7 +241,7 @@ func (s *Server) handleAISearch(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]any{"ok": false, "error": "пустой запрос"})
|
||||
return
|
||||
}
|
||||
rs, err := webSearch(r.Context(), strings.TrimSpace(in.Q), 6)
|
||||
rs, err := searchWith(r.Context(), s.searchConf(), strings.TrimSpace(in.Q), 6)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user