Commit 8ba44ba10170

Vincent Demeester <vincent@sbr.pm>
2026-02-16 15:05:55
Add SearXNG support for web search
Enhanced web search to support SearXNG instances: **SearXNG integration:** - Check for SEARXNG_URL environment variable - Try SearXNG first if configured - Fall back to DuckDuckGo if SearXNG fails or unavailable - Report which search engine was used in results **Configuration:** - SEARXNG_URL env var (optional) - Default: https://search.sbr.pm - Falls back to DuckDuckGo if not set or fails **Benefits:** - Better search results from SearXNG (aggregates multiple engines) - Privacy-focused (self-hosted instance) - Fallback ensures reliability **Test script fixes:** - Don't copy GEMINI_API_KEY to GOOGLE_API_KEY (Pi handles both) - Show status of all API keys (Gemini, Anthropic, OpenAI) - Clearer error messages Usage: export SEARXNG_URL=https://search.sbr.pm ./test-pi-bot.sh Note: Currently search.sbr.pm returns 502, so DuckDuckGo fallback is being used.
src/pi/tools/websearch.ts
@@ -1,7 +1,7 @@
 /**
- * Web Search Tool - Search the web using DuckDuckGo
+ * Web Search Tool - Search the web using SearXNG or DuckDuckGo
  * 
- * Uses DuckDuckGo's instant answer API for web search results
+ * Prefers SearXNG if SEARXNG_URL is set, falls back to DuckDuckGo
  */
 
 import { Type } from "@sinclair/typebox";
@@ -13,6 +13,55 @@ interface SearchResult {
   snippet: string;
 }
 
+/**
+ * Search using SearXNG instance
+ */
+async function searchSearXNG(query: string, maxResults: number = 5, baseUrl?: string): Promise<SearchResult[]> {
+  if (!query.trim()) {
+    return [];
+  }
+
+  const searxngUrl = baseUrl || process.env.SEARXNG_URL || "https://search.sbr.pm";
+
+  try {
+    const url = new URL("/search", searxngUrl);
+    url.searchParams.set("q", query);
+    url.searchParams.set("format", "json");
+    url.searchParams.set("pageno", "1");
+
+    const response = await fetch(url.toString(), {
+      headers: {
+        'User-Agent': 'Daneel-Bot/1.0',
+      },
+    });
+
+    if (!response.ok) {
+      throw new Error(`SearXNG search failed: ${response.statusText}`);
+    }
+
+    const data = await response.json();
+    const results: SearchResult[] = [];
+
+    if (data.results && Array.isArray(data.results)) {
+      for (const result of data.results.slice(0, maxResults)) {
+        if (result.url && result.title) {
+          results.push({
+            title: result.title,
+            url: result.url,
+            snippet: result.content || result.title,
+          });
+        }
+      }
+    }
+
+    return results;
+  } catch (error) {
+    console.error("SearXNG search error:", error);
+    // Fall back to DuckDuckGo
+    return [];
+  }
+}
+
 /**
  * Search using DuckDuckGo API (no API key required)
  * Returns related topics which include URLs
@@ -82,7 +131,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 DuckDuckGo. Returns relevant web pages with titles, URLs, and snippets.",
+  description: "Search the web for current information using SearXNG (if available) or DuckDuckGo. Returns relevant web pages with titles, URLs, and snippets.",
   parameters: Type.Object({
     query: Type.String({ description: "The search query" }),
     maxResults: Type.Optional(Type.Number({ 
@@ -106,7 +155,21 @@ export const webSearchTool: AgentTool = {
       };
     }
 
-    const results = await searchDuckDuckGo(query, maxResults);
+    // Try SearXNG first if configured
+    let results: SearchResult[] = [];
+    let searchEngine = "DuckDuckGo";
+    
+    if (process.env.SEARXNG_URL) {
+      results = await searchSearXNG(query, maxResults);
+      if (results.length > 0) {
+        searchEngine = "SearXNG";
+      }
+    }
+    
+    // Fall back to DuckDuckGo if SearXNG failed or not configured
+    if (results.length === 0) {
+      results = await searchDuckDuckGo(query, maxResults);
+    }
 
     if (results.length === 0) {
       return {
@@ -116,12 +179,12 @@ export const webSearchTool: AgentTool = {
             text: `No results found for: "${query}"`,
           },
         ],
-        details: { query, resultCount: 0 },
+        details: { query, resultCount: 0, searchEngine },
       };
     }
 
     // Format results
-    let resultText = `Search results for "${query}":\n\n`;
+    let resultText = `Search results for "${query}" (via ${searchEngine}):\n\n`;
     
     results.forEach((result, index) => {
       resultText += `${index + 1}. **${result.title}**\n`;
@@ -139,6 +202,7 @@ export const webSearchTool: AgentTool = {
       details: {
         query,
         resultCount: results.length,
+        searchEngine,
         results: results.map(r => ({
           title: r.title,
           url: r.url,
.env.example
@@ -0,0 +1,39 @@
+# Daneel Environment Configuration Example
+# Copy this to .env and fill in the values
+
+# === Required ===
+DANEEL_XMPP_JID=researchbot@xmpp.sbr.pm
+DANEEL_XMPP_PASSWORD=<get-from-aomi>
+DANEEL_OWNER_JID=vincent@xmpp.sbr.pm
+
+# === Optional Paths ===
+DANEEL_DATA_DIR=./data
+DANEEL_INBOX_PATH=/home/vincent/desktop/org/inbox.org
+DANEEL_DEFAULT_MODEL=gemini
+
+# === Search Engine ===
+# SEARXNG_URL=https://search.sbr.pm  # Use SearXNG for web search (optional)
+
+# === Debug ===
+DANEEL_DEBUG=true
+
+# === API Keys (at least one required) ===
+# Google/Gemini (we have this!)
+GOOGLE_API_KEY=${GEMINI_API_KEY}
+
+# Anthropic Claude (optional)
+# ANTHROPIC_API_KEY=sk-ant-...
+
+# OpenAI (optional)
+# OPENAI_API_KEY=sk-...
+
+# GitHub Copilot (optional)
+# GITHUB_TOKEN=ghp_...
+
+# Ollama (optional, local)
+# OLLAMA_BASE_URL=http://localhost:11434
+
+# Other providers (optional)
+# GROQ_API_KEY=
+# MISTRAL_API_KEY=
+# OPENROUTER_API_KEY=
test-pi-bot.sh
@@ -33,19 +33,17 @@ if [ -z "${DANEEL_XMPP_PASSWORD:-}" ]; then
     exit 1
 fi
 
-# Check for API key
-if [ -z "${GOOGLE_API_KEY:-}" ] && [ -z "${GEMINI_API_KEY:-}" ]; then
-    echo "ERROR: No Google API key found"
+# Check for API key (Pi uses GEMINI_API_KEY for Google/Gemini models)
+if [ -z "${GEMINI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
+    echo "ERROR: No API key found"
     echo ""
-    echo "Set either:"
-    echo "  export GOOGLE_API_KEY='...'"
-    echo "  export GEMINI_API_KEY='...'"
+    echo "Set at least one of:"
+    echo "  export GEMINI_API_KEY='...'        (for Gemini)"
+    echo "  export ANTHROPIC_API_KEY='...'     (for Claude)"
+    echo "  export OPENAI_API_KEY='...'        (for GPT)"
     exit 1
 fi
 
-# Use GEMINI_API_KEY if GOOGLE_API_KEY not set
-: "${GOOGLE_API_KEY:=${GEMINI_API_KEY:-}}"
-
 # Optional settings
 : "${DANEEL_DATA_DIR:=./data}"
 : "${DANEEL_INBOX_PATH:=~/desktop/org/inbox.org}"
@@ -62,7 +60,9 @@ echo "Inbox Path:    $DANEEL_INBOX_PATH"
 echo "Debug:         $DANEEL_DEBUG"
 echo ""
 echo "API Keys:"
-echo "  Google/Gemini: $([ -n "$GOOGLE_API_KEY" ] && echo "✓" || echo "✗")"
+echo "  Gemini:    $([ -n "${GEMINI_API_KEY:-}" ] && echo "✓" || echo "✗")"
+echo "  Anthropic: $([ -n "${ANTHROPIC_API_KEY:-}" ] && echo "✓" || echo "✗")"
+echo "  OpenAI:    $([ -n "${OPENAI_API_KEY:-}" ] && echo "✓" || echo "✗")"
 echo ""
 echo "Starting Daneel (Pi Edition)..."
 echo "==================================="