/** * Section 1: Backend Core - Search Integration * * Integrates DuckDuckGo search API for "Search Online" mode * Provides web search results to augment LLM responses */ /** * Search result interface */ export interface SearchResult { title: string; url: string; snippet: string; source?: string; } /** * Search the web using DuckDuckGo API * * @param query - Search query * @param maxResults - Maximum number of results to return * @returns Array of search results */ export async function searchOnline( query: string, maxResults: number = 5 ): Promise { try { if (!query || query.trim().length === 0) { return []; } console.log(`Searching for: "${query}"`); // Use DuckDuckGo search API (no authentication required for basic searches) // We'll use the instant answer API which is free and doesn't require authentication const searchUrl = new URL("https://api.duckduckgo.com/"); searchUrl.searchParams.set("q", query); searchUrl.searchParams.set("format", "json"); searchUrl.searchParams.set("no_html", "1"); searchUrl.searchParams.set("skip_disambig", "1"); const response = await fetch(searchUrl.toString(), { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }); if (!response.ok) { console.error(`DuckDuckGo API error: ${response.status}`); return []; } const data = (await response.json()) as any; // Parse DuckDuckGo response const results: SearchResult[] = []; // Add instant answer if available if (data.AbstractText) { results.push({ title: data.AbstractTitle || "Answer", url: data.AbstractURL || "", snippet: data.AbstractText, source: "DuckDuckGo Instant Answer", }); } // Add related topics if (data.RelatedTopics && Array.isArray(data.RelatedTopics)) { for (const topic of data.RelatedTopics.slice(0, maxResults - results.length)) { if (topic.FirstURL && topic.Text) { results.push({ title: topic.FirstURL.split("/")[2] || "Result", url: topic.FirstURL, snippet: topic.Text.substring(0, 200), source: "DuckDuckGo", }); } } } // If no results from instant answer, try fetching from search results if (results.length === 0) { return await searchDuckDuckGoWeb(query, maxResults); } return results.slice(0, maxResults); } catch (error) { console.error("Search error:", error); return []; } } /** * Fallback search using DuckDuckGo HTML scraping * (Limited by rate limiting and reliability) */ async function searchDuckDuckGoWeb( query: string, maxResults: number ): Promise { try { // This is a simplified fallback - in production, consider using a dedicated search API const searchUrl = `https://html.duckduckgo.com/?q=${encodeURIComponent(query)}&t=h_&ia=web`; const response = await fetch(searchUrl, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }); if (!response.ok) { return []; } // Basic HTML parsing (would need cheerio or similar in production) // For now, return empty to avoid complexity return []; } catch (error) { console.error("DuckDuckGo web search error:", error); return []; } } /** * Format search results into a context string for the LLM * * @param results - Array of search results * @returns Formatted string for LLM context */ export function formatSearchResults(results: SearchResult[]): string { if (results.length === 0) { return ""; } let formatted = "## Search Results\n\n"; results.forEach((result, index) => { formatted += `${index + 1}. **${result.title}**\n`; formatted += ` URL: ${result.url}\n`; formatted += ` ${result.snippet}\n\n`; }); formatted += "---\n"; return formatted; } /** * Validate and sanitize search query * * @param query - Raw search query * @returns Sanitized query */ export function sanitizeSearchQuery(query: string): string { // Remove potentially harmful characters return query .replace(/[<>]/g, "") .trim() .substring(0, 500); // Limit query length }