Commit ac6bd0e52616

Vincent Demeester <vincent@sbr.pm>
2026-02-16 15:09:31
Use ddgr for web search (much better results!)
Switched from DuckDuckGo Instant Answer API to ddgr CLI tool: **Why ddgr:** - Actually does web search (not just instant answers) - Returns comprehensive results for general queries - Works for 'typescript best practices' and similar queries - Same tool pi-coding-agent uses **Search engine priority:** 1. SearXNG (if SEARXNG_URL configured) 2. ddgr (DuckDuckGo CLI - NEW DEFAULT) 3. DuckDuckGo API (fallback, limited) **Implementation:** - Use child_process to execute ddgr with --json flag - Parse JSON output line-by-line - Timeout after 10 seconds - Falls back gracefully if ddgr not available **Results:** - Web search now works for general queries ✅ - Research tool will get better source material ✅ - Tests still pass (now actually searching) ✅ Example: 'typescript best practices' now returns 5 results instead of 0!
Changed files (1)
src
src/pi/tools/websearch.ts
@@ -1,11 +1,15 @@
 /**
- * Web Search Tool - Search the web using SearXNG or DuckDuckGo
+ * Web Search Tool - Search the web using SearXNG, ddgr, or DuckDuckGo API
  * 
- * Prefers SearXNG if SEARXNG_URL is set, falls back to DuckDuckGo
+ * Tries in order: SearXNG → ddgr → DuckDuckGo API
  */
 
 import { Type } from "@sinclair/typebox";
 import type { AgentTool } from "@mariozechner/pi-agent-core";
+import { exec } from "child_process";
+import { promisify } from "util";
+
+const execAsync = promisify(exec);
 
 interface SearchResult {
   title: string;
@@ -13,6 +17,52 @@ interface SearchResult {
   snippet: string;
 }
 
+/**
+ * Search using ddgr (DuckDuckGo command-line tool)
+ */
+async function searchDdgr(query: string, maxResults: number = 5): Promise<SearchResult[]> {
+  if (!query.trim()) {
+    return [];
+  }
+
+  try {
+    // Use ddgr with JSON output
+    const { stdout } = await execAsync(
+      `ddgr --json -n ${maxResults} ${JSON.stringify(query)}`,
+      { timeout: 10000 }
+    );
+
+    const results: SearchResult[] = [];
+    const lines = stdout.trim().split('\n');
+
+    for (const line of lines) {
+      if (!line.trim()) continue;
+      
+      try {
+        const data = JSON.parse(line);
+        if (data.url && data.title) {
+          results.push({
+            title: data.title,
+            url: data.url,
+            snippet: data.abstract || data.title,
+          });
+        }
+      } catch (e) {
+        // Skip invalid JSON lines
+      }
+    }
+
+    return results;
+  } catch (error) {
+    // ddgr not available or failed
+    if (process.env.DANEEL_DEBUG) {
+      const msg = error instanceof Error ? error.message : String(error);
+      console.warn(`[ddgr] Unavailable (${msg})`);
+    }
+    return [];
+  }
+}
+
 /**
  * Search using SearXNG instance
  */
@@ -134,7 +184,7 @@ async function searchDuckDuckGo(query: string, maxResults: number = 5): Promise<
 export const webSearchTool: AgentTool = {
   name: "web_search",
   label: "Web Search",
-  description: "Search the web for current information using SearXNG (if available) or DuckDuckGo. Returns relevant web pages with titles, URLs, and snippets.",
+  description: "Search the web for current information. Uses SearXNG, ddgr, or DuckDuckGo API (in order of preference). Returns relevant web pages with titles, URLs, and snippets.",
   parameters: Type.Object({
     query: Type.String({ description: "The search query" }),
     maxResults: Type.Optional(Type.Number({ 
@@ -158,10 +208,11 @@ export const webSearchTool: AgentTool = {
       };
     }
 
-    // Try SearXNG first if configured
+    // Try search engines in order of preference
     let results: SearchResult[] = [];
-    let searchEngine = "DuckDuckGo";
+    let searchEngine = "none";
     
+    // 1. Try SearXNG if configured
     if (process.env.SEARXNG_URL) {
       results = await searchSearXNG(query, maxResults);
       if (results.length > 0) {
@@ -169,18 +220,29 @@ export const webSearchTool: AgentTool = {
       }
     }
     
-    // Fall back to DuckDuckGo if SearXNG failed or not configured
+    // 2. Try ddgr (DuckDuckGo CLI)
+    if (results.length === 0) {
+      results = await searchDdgr(query, maxResults);
+      if (results.length > 0) {
+        searchEngine = "ddgr";
+      }
+    }
+    
+    // 3. Fall back to DuckDuckGo API (limited)
     if (results.length === 0) {
       results = await searchDuckDuckGo(query, maxResults);
+      if (results.length > 0) {
+        searchEngine = "DuckDuckGo API";
+      }
     }
 
     if (results.length === 0) {
       let message = `No results found for: "${query}"`;
       
-      // If SearXNG is configured but we're using DuckDuckGo, explain why
-      if (process.env.SEARXNG_URL && searchEngine === "DuckDuckGo") {
-        message += `\n\nNote: SearXNG (${process.env.SEARXNG_URL}) is currently unavailable. The DuckDuckGo fallback has limited coverage for general queries. Please try a more specific search or wait for SearXNG to be available.`;
-      } else if (searchEngine === "DuckDuckGo") {
+      // Provide helpful context based on what failed
+      if (process.env.SEARXNG_URL && searchEngine === "none") {
+        message += `\n\nNote: All search engines (SearXNG, ddgr, DuckDuckGo API) are currently unavailable or returned no results. Please try again later or with a different query.`;
+      } else if (searchEngine === "DuckDuckGo API") {
         message += `\n\nNote: The DuckDuckGo API has limited coverage for general queries. Try a more specific search term.`;
       }