import { formatServiceError, isAbortError } from "../shared/errors"; import { fetchJson } from "../shared/http"; import { BRAVE_URL, EXA_URL, JINA_URL, METASO_URL, type SearchResult } from "../types"; import { normalizeUrl } from "../shared/render"; export function dedupSearchResults(results: SearchResult[]): SearchResult[] { const seen = new Map(); for (const result of results) { const key = normalizeUrl(result.url); const existing = seen.get(key); if (!existing || result.snippet.length > existing.snippet.length) { seen.set(key, result); } } return Array.from(seen.values()); } export async function searchExa(query: string, apiKey: string, count: number, signal?: AbortSignal): Promise<{ results: SearchResult[]; error?: string }> { try { const data = await fetchJson(EXA_URL, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey, }, body: JSON.stringify({ query, numResults: count, contents: { highlights: { numSentences: 3, highlightsPerUrl: 1, }, }, }), signal, }); return { results: (data.results ?? []).map((result: any) => ({ title: result.title ?? "", url: result.url ?? "", snippet: result.highlights?.[0] ?? "", source: "exa" as const, })).slice(0, count), }; } catch (error) { if (isAbortError(error)) throw error; return { results: [], error: formatServiceError("Exa", error) }; } } export async function searchBrave(query: string, apiKey: string, count: number, signal?: AbortSignal): Promise<{ results: SearchResult[]; error?: string }> { try { const params = new URLSearchParams({ q: query, count: String(count) }); const data = await fetchJson(`${BRAVE_URL}?${params}`, { headers: { Accept: "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": apiKey, }, signal, }); return { results: (data.web?.results ?? []).map((result: any) => ({ title: result.title ?? "", url: result.url ?? "", snippet: result.description ?? "", source: "brave" as const, })).slice(0, count), }; } catch (error) { if (isAbortError(error)) throw error; return { results: [], error: formatServiceError("Brave", error) }; } } export async function searchJina(query: string, apiKey: string, count: number, signal?: AbortSignal): Promise<{ results: SearchResult[]; error?: string }> { try { const params = new URLSearchParams({ q: query, numResults: String(count) }); const data = await fetchJson(`${JINA_URL}?${params}`, { headers: { "Accept": "application/json", "Authorization": `Bearer ${apiKey}`, "X-Respond-With": "no-content", }, signal, }); return { results: (data.data ?? []).map((result: any) => ({ title: result.title ?? "", url: result.url ?? "", snippet: result.description ?? "", source: "jina" as const, })).slice(0, count), }; } catch (error) { if (isAbortError(error)) throw error; return { results: [], error: formatServiceError("Jina", error) }; } } export async function searchMetaso(query: string, apiKey: string, count: number, signal?: AbortSignal): Promise<{ results: SearchResult[]; error?: string }> { try { const data = await fetchJson(METASO_URL, { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Accept": "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ q: query, scope: "webpage", includeSummary: false, size: String(count), includeRawContent: false, conciseSnippet: false, }), signal, }); return { results: (data.webpages ?? []).map((result: any) => ({ title: result.title ?? "", url: result.link ?? "", snippet: result.snippet ?? "", source: "metaso" as const, })).slice(0, count), }; } catch (error) { if (isAbortError(error)) throw error; return { results: [], error: formatServiceError("Metaso", error) }; } }