fix: the assistant is told which search engine it has, results say which one they came from (and why, when DuckDuckGo stood in), the stand-in is logged
This commit is contained in:
1 parent
060975a443
commit
74a22ab57f
4 files changed
+60
-23
No files matched your search
@@ -363,10 +363,6 @@ const aiAgent = `
|
|||||||
Режим агента: ты сам ведёшь задачу шаг за шагом. В каждом ответе - коротко, что делаешь, и ровно одно действие. Получив результат, решай следующий шаг.
|
Режим агента: ты сам ведёшь задачу шаг за шагом. В каждом ответе - коротко, что делаешь, и ровно одно действие. Получив результат, решай следующий шаг.
|
||||||
Когда задача решена (или решить нельзя) - ответь итогом без блоков действий.`
|
Когда задача решена (или решить нельзя) - ответь итогом без блоков действий.`
|
||||||
|
|
||||||
const aiSearch = `
|
|
||||||
Поиск в интернете включён: блок ` + "```search\nзапрос\n```" + ` - панель найдёт его в DuckDuckGo и пришлёт результаты (заголовок, адрес, отрывок) следующим сообщением.
|
|
||||||
Ищи активно: свежие данные, версии, документация, тексты ошибок, всё, в чём не уверен, - сначала поиск, потом ответ со ссылками на найденное. Одно действие за ответ. Ссылки не выдумывай.`
|
|
||||||
|
|
||||||
const aiThink = `
|
const aiThink = `
|
||||||
Режим размышления: начни ответ строкой ::думаю:: и подумай - разбери задачу, варианты и риски; затем строка ::ответ:: и сам ответ.`
|
Режим размышления: начни ответ строкой ::думаю:: и подумай - разбери задачу, варианты и риски; затем строка ::ответ:: и сам ответ.`
|
||||||
|
|
||||||
@@ -552,7 +548,7 @@ func (s *Server) handleAIChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
system += aiAgent
|
system += aiAgent
|
||||||
}
|
}
|
||||||
if in.Search {
|
if in.Search {
|
||||||
system += aiSearch
|
system += searchPrompt(s.searchConf())
|
||||||
}
|
}
|
||||||
if in.Think {
|
if in.Think {
|
||||||
system += aiThink
|
system += aiThink
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -99,8 +100,23 @@ var (
|
|||||||
ddgSearch = webSearch
|
ddgSearch = webSearch
|
||||||
)
|
)
|
||||||
|
|
||||||
// searchWith asks the engine c names; DuckDuckGo if that one fails.
|
// engineName is how the engine is called to people and to the model.
|
||||||
func searchWith(ctx context.Context, c searchConf, q string, n int) ([]searchResult, error) {
|
func engineName(e string) string {
|
||||||
|
switch e {
|
||||||
|
case "searxng":
|
||||||
|
return "SearXNG"
|
||||||
|
case "brave":
|
||||||
|
return "Brave Search"
|
||||||
|
case "tavily":
|
||||||
|
return "Tavily"
|
||||||
|
}
|
||||||
|
return "DuckDuckGo"
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchWith asks the engine c names; DuckDuckGo if that one fails. It
|
||||||
|
// tells which engine the results came from (and why, when it was not the
|
||||||
|
// chosen one).
|
||||||
|
func searchWith(ctx context.Context, c searchConf, q string, n int) ([]searchResult, string, error) {
|
||||||
var rs []searchResult
|
var rs []searchResult
|
||||||
var err error
|
var err error
|
||||||
switch c.Engine {
|
switch c.Engine {
|
||||||
@@ -111,15 +127,30 @@ func searchWith(ctx context.Context, c searchConf, q string, n int) ([]searchRes
|
|||||||
case "tavily":
|
case "tavily":
|
||||||
rs, err = searchTavily(ctx, c, q, n)
|
rs, err = searchTavily(ctx, c, q, n)
|
||||||
default:
|
default:
|
||||||
return ddgSearch(ctx, q, n)
|
rs, err = ddgSearch(ctx, q, n)
|
||||||
|
return rs, "DuckDuckGo", err
|
||||||
}
|
}
|
||||||
if err != nil || len(rs) == 0 {
|
if err != nil || len(rs) == 0 {
|
||||||
return ddgSearch(ctx, q, n)
|
why := "ничего не нашёл"
|
||||||
|
if err != nil {
|
||||||
|
why = err.Error()
|
||||||
|
}
|
||||||
|
log.Printf("search: %s failed (%s), DuckDuckGo instead", engineName(c.Engine), why)
|
||||||
|
rs, err = ddgSearch(ctx, q, n)
|
||||||
|
return rs, "DuckDuckGo (" + engineName(c.Engine) + " не ответил: " + why + ")", err
|
||||||
}
|
}
|
||||||
if len(rs) > n {
|
if len(rs) > n {
|
||||||
rs = rs[:n]
|
rs = rs[:n]
|
||||||
}
|
}
|
||||||
return rs, nil
|
return rs, engineName(c.Engine), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchPrompt tells the model it can search, and with what.
|
||||||
|
func searchPrompt(c searchConf) string {
|
||||||
|
return "\nПоиск в интернете включён (поисковик: " + engineName(c.Engine) + "): блок ```search\nзапрос\n``` - панель найдёт и пришлёт результаты " +
|
||||||
|
"(заголовок, адрес, отрывок) следующим сообщением, с пометкой, через какой поисковик они пришли.\n" +
|
||||||
|
"Ищи активно: свежие данные, версии, документация, тексты ошибок, всё, в чём не уверен, - сначала поиск, потом ответ со ссылками на найденное. " +
|
||||||
|
"Одно действие за ответ. Ссылки не выдумывай."
|
||||||
}
|
}
|
||||||
|
|
||||||
// getJSON fetches url (with the headers) into v.
|
// getJSON fetches url (with the headers) into v.
|
||||||
@@ -241,10 +272,10 @@ func (s *Server) handleAISearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, map[string]any{"ok": false, "error": "пустой запрос"})
|
writeJSON(w, map[string]any{"ok": false, "error": "пустой запрос"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rs, err := searchWith(r.Context(), s.searchConf(), strings.TrimSpace(in.Q), 6)
|
rs, used, err := searchWith(r.Context(), s.searchConf(), strings.TrimSpace(in.Q), 6)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
writeJSON(w, map[string]any{"ok": false, "error": err.Error(), "engine": used})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"ok": true, "data": rs})
|
writeJSON(w, map[string]any{"ok": true, "data": rs, "engine": used})
|
||||||
}
|
}
|
||||||
@@ -54,24 +54,24 @@ func TestSearchProviders(t *testing.T) {
|
|||||||
braveURL, tavilyURL = srv.URL+"/res/v1/web/search", srv.URL+"/search"
|
braveURL, tavilyURL = srv.URL+"/res/v1/web/search", srv.URL+"/search"
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
rs, err := searchWith(ctx, searchConf{Engine: "searxng", URL: srv.URL}, "q1", 6)
|
rs, used, err := searchWith(ctx, searchConf{Engine: "searxng", URL: srv.URL}, "q1", 6)
|
||||||
if err != nil || len(rs) != 1 || rs[0].Title != "SX" || gotQ != "q1" {
|
if err != nil || len(rs) != 1 || rs[0].Title != "SX" || gotQ != "q1" || used != "SearXNG" {
|
||||||
t.Fatalf("searxng: %+v %v", rs, err)
|
t.Fatalf("searxng: %+v %v", rs, err)
|
||||||
}
|
}
|
||||||
rs, err = searchWith(ctx, searchConf{Engine: "brave", Key: "bk"}, "q2", 6)
|
rs, _, err = searchWith(ctx, searchConf{Engine: "brave", Key: "bk"}, "q2", 6)
|
||||||
if err != nil || rs[0].Snippet != "from brave" || gotKey != "bk" || gotQ != "q2" {
|
if err != nil || rs[0].Snippet != "from brave" || gotKey != "bk" || gotQ != "q2" {
|
||||||
t.Fatalf("brave: %+v %v %q", rs, err, gotKey)
|
t.Fatalf("brave: %+v %v %q", rs, err, gotKey)
|
||||||
}
|
}
|
||||||
rs, err = searchWith(ctx, searchConf{Engine: "tavily", Key: "tk"}, "q3", 6)
|
rs, used, err = searchWith(ctx, searchConf{Engine: "tavily", Key: "tk"}, "q3", 6)
|
||||||
if err != nil || rs[0].Title != "TV" || gotKey != "Bearer tk" || gotQ != "q3" {
|
if err != nil || rs[0].Title != "TV" || gotKey != "Bearer tk" || gotQ != "q3" || used != "Tavily" {
|
||||||
t.Fatalf("tavily: %+v %v %q", rs, err, gotKey)
|
t.Fatalf("tavily: %+v %v %q", rs, err, gotKey)
|
||||||
}
|
}
|
||||||
// The chosen one fails: DuckDuckGo instead (stood in for here).
|
// The chosen one fails: DuckDuckGo instead (stood in for here).
|
||||||
ddgSearch = func(ctx context.Context, q string, n int) ([]searchResult, error) {
|
ddgSearch = func(ctx context.Context, q string, n int) ([]searchResult, error) {
|
||||||
return []searchResult{{Title: "DDG"}}, nil
|
return []searchResult{{Title: "DDG"}}, nil
|
||||||
}
|
}
|
||||||
rs, err = searchWith(ctx, searchConf{Engine: "searxng", URL: srv.URL + "/broken"}, "q4", 6)
|
rs, used, err = searchWith(ctx, searchConf{Engine: "searxng", URL: srv.URL + "/broken"}, "q4", 6)
|
||||||
if err != nil || rs[0].Title != "DDG" {
|
if err != nil || rs[0].Title != "DDG" || !strings.Contains(used, "DuckDuckGo") || !strings.Contains(used, "SearXNG не ответил") {
|
||||||
t.Fatalf("fallback: %+v %v", rs, err)
|
t.Fatalf("fallback: %+v %v", rs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,9 +89,19 @@ func TestSearchLimitFallsBack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
braveURL, tavilyURL = srv.URL, srv.URL
|
braveURL, tavilyURL = srv.URL, srv.URL
|
||||||
for _, e := range []string{"brave", "tavily", "searxng"} {
|
for _, e := range []string{"brave", "tavily", "searxng"} {
|
||||||
rs, err := searchWith(context.Background(), searchConf{Engine: e, Key: "k", URL: srv.URL}, "x", 6)
|
rs, _, err := searchWith(context.Background(), searchConf{Engine: e, Key: "k", URL: srv.URL}, "x", 6)
|
||||||
if err != nil || len(rs) != 1 || rs[0].Title != "DDG x" {
|
if err != nil || len(rs) != 1 || rs[0].Title != "DDG x" {
|
||||||
t.Fatalf("%s: %+v %v", e, rs, err)
|
t.Fatalf("%s: %+v %v", e, rs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The model is told which engine it searches with.
|
||||||
|
func TestSearchPromptNamesEngine(t *testing.T) {
|
||||||
|
if p := searchPrompt(searchConf{Engine: "tavily"}); !strings.Contains(p, "Tavily") || strings.Contains(p, "DuckDuckGo") {
|
||||||
|
t.Fatalf("tavily: %s", p)
|
||||||
|
}
|
||||||
|
if p := searchPrompt(searchConf{}); !strings.Contains(p, "DuckDuckGo") {
|
||||||
|
t.Fatalf("default: %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -321,8 +321,8 @@
|
|||||||
if (!searchOn()) return 'Ошибка: поиск выключен - включите плашку «Поиск»';
|
if (!searchOn()) return 'Ошибка: поиск выключен - включите плашку «Поиск»';
|
||||||
const r = await fetch('/api/panel/ai/search', {method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin',
|
const r = await fetch('/api/panel/ai/search', {method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin',
|
||||||
body: JSON.stringify({q: a.body})}).then(x => x.json()).catch(() => ({ok: false, error: 'нет связи'}));
|
body: JSON.stringify({q: a.body})}).then(x => x.json()).catch(() => ({ok: false, error: 'нет связи'}));
|
||||||
if (!r.ok) return 'Ошибка поиска «' + a.body + '»: ' + r.error;
|
if (!r.ok) return 'Ошибка поиска «' + a.body + '»' + (r.engine ? ' (' + r.engine + ')' : '') + ': ' + r.error;
|
||||||
return 'Результаты поиска «' + a.body + '»:\n' + r.data.map((x, i) => (i + 1) + '. ' + x.title + ' - ' + x.url + (x.snippet ? '\n ' + x.snippet : '')).join('\n');
|
return 'Результаты поиска «' + a.body + '» через ' + (r.engine || 'поисковик') + ':\n' + r.data.map((x, i) => (i + 1) + '. ' + x.title + ' - ' + x.url + (x.snippet ? '\n ' + x.snippet : '')).join('\n');
|
||||||
}
|
}
|
||||||
await loadMachines();
|
await loadMachines();
|
||||||
const m = machineOf(a.machine);
|
const m = machineOf(a.machine);
|
||||||
|
|||||||
Reference in new issue
Block a user