Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,12 @@ waits for answer text with a source link instead of stopping at the empty Search
Assist heading, and supports expanded answer cards. Browser requests share one
slot across networks to limit memory use on small hosts.

Both the lightweight and browser lookups explicitly request Search Assist using
the `assiston=1` web route published in DuckDuckGo's [official bang catalog](https://duckduckgo.com/bang.js)
for `!assist`. The no-answer search link requests it too. This avoids relying on
the normal search page's automatic answer-display setting, but is not a public
Search Assist API, a challenge bypass, or a guarantee of an answer for every query.

The plugin is configured under `plugins.ask`:

~~~yaml
Expand Down
18 changes: 13 additions & 5 deletions plugins/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ func askDuckDuckGoSearchAssist(ctx context.Context, question string) (askSource,
if attempt < len(queries) {
query = queries[attempt]
}
if source, ok := askDuckDuckGoSearchAssistOnce(ctx, query, duckDuckGoSearchURL(question)); ok {
if source, ok := askDuckDuckGoSearchAssistOnce(ctx, query, duckDuckGoSearchAssistURL(question)); ok {
return source, true
}
if err := ctx.Err(); err != nil {
Expand Down Expand Up @@ -830,7 +830,7 @@ func askSearchAssistQueryVariants(question string) []string {
var askVersusPattern = regexp.MustCompile(`(?i)\bvs\b\.?`)

func askDuckDuckGoSearchAssistOnce(ctx context.Context, question, fallbackURL string) (askSource, bool) {
pageURL := duckDuckGoSearchURL(question)
pageURL := duckDuckGoSearchAssistURL(question)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL, nil)
if err != nil {
return askSource{}, false
Expand Down Expand Up @@ -946,7 +946,7 @@ func askDuckDuckGoRenderedSearchAssist(ctx context.Context, question, browserPat
if index < len(queries) {
query = queries[index]
}
if source, ok := askDuckDuckGoRenderedSearchAssistOnce(ctx, query, duckDuckGoSearchURL(question), executable, fetchResults); ok {
if source, ok := askDuckDuckGoRenderedSearchAssistOnce(ctx, query, duckDuckGoSearchAssistURL(question), executable, fetchResults); ok {
return source, true
}
if err := ctx.Err(); err != nil {
Expand Down Expand Up @@ -986,7 +986,7 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba
defer browserCancel()

var result askRenderedSearchAssistData
if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchURL(question))); err != nil {
if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil {
return askSource{}, false
}
// Navigation and browser startup have already consumed some of the budget.
Expand Down Expand Up @@ -1484,6 +1484,14 @@ func duckDuckGoSearchURL(query string) string {
return "https://duckduckgo.com/?q=" + url.QueryEscape(strings.TrimSpace(query))
}

// This is the destination used by DuckDuckGo's official !assist bang
// (https://duckduckgo.com/bang.js). It requests Search Assist explicitly,
// rather than relying on the default search setting to display an answer.
// It is a web route, not an API or a way to bypass provider challenges.
func duckDuckGoSearchAssistURL(query string) string {
return "https://duckduckgo.com/?assiston=1&q=" + url.QueryEscape(strings.TrimSpace(query))
}

func askHTTPSuccess(status int) bool {
return status >= http.StatusOK && status < http.StatusMultipleChoices
}
Expand Down Expand Up @@ -2056,7 +2064,7 @@ func formatAskResponse(nick, answer, sourceURL string, maxLength, maxResponseCha

func formatAskNoAnswer(question string, maxLength int) string {
maxLength = clampAskLength(maxLength, 120, 450, 360)
return truncateAskBytes("I couldn't find a reliable answer — search: "+duckDuckGoSearchURL(question), maxLength)
return truncateAskBytes("I couldn't find a reliable answer — search: "+duckDuckGoSearchAssistURL(question), maxLength)
}

func clampAskLength(value, min, max, fallback int) int {
Expand Down
16 changes: 16 additions & 0 deletions plugins/ask_resilience_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plugins
import (
"context"
"net/http"
"net/url"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -72,6 +73,9 @@ func TestAskComparisonRecoversOnNormalizedRetry(t *testing.T) {
if r.URL.Path == "/assist.js" {
return newPluginResponse(200, `DDG.deep.deepPayload={"instantAnswers":[{"data":{"answer":"Compare equivalent dirt bikes and go-karts, including maintenance costs.","sources":[{"article":{"link":"https://example.org/comparison"}}]}}]};callback();`), nil
}
if r.URL.Query().Get("assiston") != "1" {
t.Fatal("Search Assist was not explicitly requested")
}
queries = append(queries, r.URL.Query().Get("q"))
if len(queries) == 1 {
return newPluginResponse(200, `<html><body>No answer yet</body></html>`), nil
Expand All @@ -87,6 +91,18 @@ func TestAskComparisonRecoversOnNormalizedRetry(t *testing.T) {
}
}

func TestAskExplicitAssistURL(t *testing.T) {
for _, question := range []string{"how much do dirt bikes cost vs GoKarts?", "R&D vs C++? &assiston=0", "日本語の質問"} {
u, err := url.Parse(duckDuckGoSearchAssistURL(question))
if err != nil || u.Host != "duckduckgo.com" || u.Scheme != "https" || u.Query().Get("q") != question || !reflect.DeepEqual(u.Query()["assiston"], []string{"1"}) {
t.Fatalf("invalid explicit route: %v %v", u, err)
}
}
if !strings.Contains(formatAskNoAnswer("a question", 360), "?assiston=1&q=a+question") {
t.Fatal("fallback link does not request Search Assist")
}
}

func TestAskComparisonDoesNotReturnEntityDescription(t *testing.T) {
old := askHTTPClient
t.Cleanup(func() { askHTTPClient = old })
Expand Down
2 changes: 1 addition & 1 deletion plugins/ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ func TestAskResponseDropsInvalidSourceURL(t *testing.T) {

func TestAskNoAnswerProvidesSearchFallback(t *testing.T) {
got := formatAskNoAnswer("Is the Earth flat?", 180)
if !strings.Contains(got, "I couldn't find a reliable answer") || !strings.Contains(got, "https://duckduckgo.com/?q=Is+the+Earth+flat%3F") {
if !strings.Contains(got, "I couldn't find a reliable answer") || !strings.Contains(got, "https://duckduckgo.com/?assiston=1&q=Is+the+Earth+flat%3F") {
t.Fatalf("formatAskNoAnswer() = %q, want bounded search fallback", got)
}
if strings.ContainsAny(got, "\r\n") || len([]byte(got)) > 180 {
Expand Down
Loading