Domify-Academy-Bot / search.ts
Domify's picture
Upload 35 files
93c19dc verified
/**
* 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<SearchResult[]> {
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<SearchResult[]> {
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
}