import z from 'zod'; import type { ToolDef } from './types.js'; const SEARCH_SVC = process.env.SEARCH_SVC_URL?.trim() ?? ''; const SEARXNG = process.env.SEARXNG_URL?.trim() ?? 'https://searx.tiekoetter.com'; const schema = z.object({ queries: z.array(z.string()).max(3).describe('Search queries (keywords, SEO-friendly)'), }); async function doSearch(q: string): Promise<{ results: { title: string; url: string; content?: string }[] }> { if (SEARCH_SVC) { const url = `${SEARCH_SVC.replace(/\/$/, '')}/api/v1/search?q=${encodeURIComponent(q)}`; const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); if (!res.ok) throw new Error(`Search HTTP ${res.status}`); const data = (await res.json()) as { results?: { title?: string; url?: string; content?: string }[] }; return { results: (data.results ?? []).map((r) => ({ title: r.title ?? '', url: r.url ?? '', content: r.content ?? '', })), }; } const url = `${SEARXNG}/search?format=json&q=${encodeURIComponent(q)}`; const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); const data = (await res.json()) as { results?: { title?: string; url?: string; content?: string }[] }; return { results: (data.results ?? []).map((r) => ({ title: r.title ?? '', url: r.url ?? '', content: r.content ?? '', })), }; } export const webSearchTool: ToolDef = { name: 'web_search', description: 'Search the web for information. Use SEO-friendly keywords. Up to 3 queries per call.', schema, execute: async (params, _ctx) => { const { queries } = schema.parse(params); const results = await Promise.all(queries.map(doSearch)); const all = results.flatMap((r) => r.results).slice(0, 15); return JSON.stringify( all.map((r) => ({ title: r.title, url: r.url, snippet: (r.content ?? '').slice(0, 200) })), null, 2, ); }, };