Commit 8ba44ba10170
2026-02-16 15:05:55
1 parent
e16b76f
Changed files (3)
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 "==================================="