/** * Web Search Tool * Uses SearXNG or Brave Search API. */ import { tool } from 'ai' import { z } from 'zod' import { toolRegistry } from './registry' async function searchSearXNG(query: string, searxngUrl: string): Promise { const url = `${searxngUrl.replace(/\/+$/, '')}/search?q=${encodeURIComponent(query)}&format=json` const response = await fetch(url, { headers: { 'Accept': 'application/json' } }) if (!response.ok) throw new Error(`SearXNG error: ${response.status}`) const data = await response.json() return (data.results || []).slice(0, 8).map((r: any) => ({ title: r.title, url: r.url, snippet: r.content || '', })) } async function searchBrave(query: string, apiKey: string): Promise { const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=8` const response = await fetch(url, { headers: { 'Accept': 'application/json', 'X-Subscription-Token': apiKey } }) if (!response.ok) throw new Error(`Brave error: ${response.status}`) const data = await response.json() return (data.web?.results || []).map((r: any) => ({ title: r.title, url: r.url, snippet: r.description || '', })) } toolRegistry.register({ name: 'web_search', description: 'Search the web for information. Returns a list of results with titles, URLs and snippets.', isInternal: false, buildTool: (ctx) => tool({ description: 'Search the web for information. Returns results with titles, URLs and snippets.', inputSchema: z.object({ query: z.string().describe('The search query'), }), execute: async ({ query }) => { try { const provider = ctx.config.WEB_SEARCH_PROVIDER || 'searxng' if (provider === 'brave' || provider === 'both') { const apiKey = ctx.config.BRAVE_SEARCH_API_KEY if (apiKey) { return await searchBrave(query, apiKey) } } // Default: SearXNG const searxngUrl = ctx.config.SEARXNG_URL || 'http://localhost:8080' return await searchSearXNG(query, searxngUrl) } catch (e: any) { return { error: `Web search failed: ${e.message}` } } }, }), })