@oevortex/ddg_search
Version:
A Model Context Protocol server for web search using DuckDuckGo and Felo AI
205 lines (178 loc) • 6.13 kB
JavaScript
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
// Rotating User Agents
const USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
];
// Cache results to avoid repeated requests
const resultsCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
/**
* Response class for Felo API responses
*/
class Response {
/**
* Create a new Response
* @param {string} text - The text content of the response
*/
constructor(text) {
this.text = text;
}
/**
* String representation of the response
* @returns {string} The text content
*/
toString() {
return this.text;
}
}
/**
* Get a random user agent from the list
* @returns {string} A random user agent string
*/
function getRandomUserAgent() {
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}
/**
* Generate a cache key for a search query
* @param {string} query - The search query
* @returns {string} The cache key
*/
function getCacheKey(query) {
return `felo-${query}`;
}
/**
* Clear old entries from the cache
*/
function clearOldCache() {
const now = Date.now();
for (const [key, value] of resultsCache.entries()) {
if (now - value.timestamp > CACHE_DURATION) {
resultsCache.delete(key);
}
}
}
/**
* Search using the Felo AI API
* @param {string} prompt - The search query or prompt
* @param {boolean} stream - If true, yields response chunks as they arrive
* @param {boolean} raw - If true, returns raw response dictionaries
* @returns {Promise<string|AsyncGenerator<string>>} The search results
*/
async function searchFelo(prompt, stream = false, raw = false) {
// Clear old cache entries
clearOldCache();
// Check cache first if not streaming
if (!stream) {
const cacheKey = getCacheKey(prompt);
const cachedResults = resultsCache.get(cacheKey);
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
return cachedResults.results;
}
}
// Create payload for Felo API
const payload = {
query: prompt,
search_uuid: uuidv4(),
lang: "",
agent_lang: "en",
search_options: {
langcode: "en-US"
},
search_video: true,
contexts_from: "google"
};
// Get a random user agent
const userAgent = getRandomUserAgent();
// Headers for the request
const headers = {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9',
'content-type': 'application/json',
'cookie': '_clck=1gifk45%7C2%7Cfoa%7C0%7C1686; _clsk=1g5lv07%7C1723558310439%7C1%7C1%7Cu.clarity.ms%2Fcollect; _ga=GA1.1.877307181.1723558313; _ga_8SZPRV97HV=GS1.1.1723558313.1.1.1723558341.0.0.0; _ga_Q9Q1E734CC=GS1.1.1723558313.1.1.1723558341.0.0.0',
'dnt': '1',
'origin': 'https://felo.ai',
'referer': 'https://felo.ai/',
'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent': userAgent
};
// Define the streaming function
async function* streamFunction() {
try {
const response = await axios.post('https://api.felo.ai/search/threads', payload, {
headers,
timeout: 30000, // 30 second timeout
responseType: 'stream'
});
let streamingText = '';
let buffer = '';
// Process the stream as it comes in
for await (const chunk of response.data) {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line in the buffer
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const data = JSON.parse(line.substring(5).trim());
if (data.type === 'answer' && 'text' in data.data) {
const newText = data.data.text;
if (newText.length > streamingText.length) {
const delta = newText.substring(streamingText.length);
streamingText = newText;
if (raw) {
yield { text: delta };
} else {
yield new Response(delta).toString();
}
}
}
} catch (error) {
// Ignore JSON parse errors and continue
}
}
}
}
// Cache the complete response
if (streamingText) {
resultsCache.set(getCacheKey(prompt), {
results: streamingText,
timestamp: Date.now()
});
}
} catch (error) {
console.error('Error searching Felo:', error.message);
throw new Error(`Failed to search Felo: ${error.message}`);
}
}
// If streaming is requested, return the generator
if (stream) {
return streamFunction();
}
// For non-streaming, collect all chunks and return as a single string
let fullResponse = '';
try {
for await (const chunk of streamFunction()) {
if (raw) {
fullResponse += chunk.text;
} else {
fullResponse += chunk;
}
}
return fullResponse;
} catch (error) {
console.error('Error in non-streaming Felo search:', error.message);
throw error;
}
}
export { searchFelo };