/** * Web Search tool - search the internet using DuckDuckGo or SearXNG */ import { Tool } from 'ollama'; import { ToolHandler, ToolResult } from './types'; export const webSearchTool: Tool = { type: 'function', function: { name: 'web_search', description: 'Search the internet for current information. Returns a list of relevant search results with titles, URLs, and snippets.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'The search query', }, num_results: { type: 'number', description: 'Number of results to return (default: 5, max: 10)', }, }, required: ['query'], }, }, }; interface SearchResult { title: string; url: string; snippet: string; } // DuckDuckGo HTML search (no API key needed) async function searchDuckDuckGo(query: string, numResults: number): Promise { const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }, signal: AbortSignal.timeout(10000), }); if (!response.ok) { throw new Error(`Search failed: HTTP ${response.status}`); } const html = await response.text(); const results: SearchResult[] = []; // Parse results from DDG HTML // Results are in
elements const resultRegex = /]+class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?]+class="result__snippet"[^>]*>([^<]*(?:<[^>]+>[^<]*)*)<\/a>/gi; let match; while ((match = resultRegex.exec(html)) !== null && results.length < numResults) { const url = match[1]; const title = match[2].trim(); const snippet = match[3].replace(/<[^>]+>/g, '').trim(); if (url && title && !url.startsWith('/')) { results.push({ title, url, snippet }); } } // Fallback: try alternative parsing if no results if (results.length === 0) { const altRegex = /
]+href="([^"]+)"[^>]*>[\s\S]*?<\/a>[\s\S]*?class="result__title"[^>]*>([^<]+)/gi; while ((match = altRegex.exec(html)) !== null && results.length < numResults) { const url = match[1]; const title = match[2].trim(); if (url && title && !url.startsWith('/')) { results.push({ title, url, snippet: '' }); } } } return results; } export const webSearchHandler: ToolHandler = async (args): Promise => { const query = args.query as string; const numResults = Math.min((args.num_results as number) || 5, 10); if (!query) { return { success: false, error: 'No search query provided', }; } try { const results = await searchDuckDuckGo(query, numResults); if (results.length === 0) { return { success: true, result: `No search results found for: "${query}"`, }; } const formatted = results .map( (r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}${r.snippet ? `\n ${r.snippet}` : ''}` ) .join('\n\n'); return { success: true, result: `Search results for "${query}":\n\n${formatted}`, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Search failed', }; } };