import axios from 'axios';
import WebSocket from 'ws';
import * as cheerio from 'cheerio';
import TurndownService from 'turndown';
import * as tough from 'tough-cookie';
import { wrapper } from 'axios-cookiejar-support';
import { getRandomUserAgent } from './user_agents.js';
const { CookieJar } = tough;
// Valid modes and detail levels
const VALID_MODES = ['question', 'academic', 'forums', 'wiki', 'thinking'];
const VALID_DETAIL_LEVELS = ['concise', 'detailed', 'comprehensive'];
// Cache results to avoid repeated requests
const resultsCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const DEFAULT_TIMEOUT = 30000;
const API_ENDPOINT = 'https://iask.ai/';
/**
* Generate a cache key for a search query
* @param {string} query - The search query
* @param {string} mode - The search mode
* @param {string|null} detailLevel - The detail level
* @returns {string} The cache key
*/
function getCacheKey(query, mode, detailLevel) {
return `iask-${mode}-${detailLevel || 'default'}-${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);
}
}
}
/**
* Recursively search for cached HTML content in diff object
* @param {any} diff - The diff object to search
* @returns {string|null} The found content or null
*/
function cacheFind(diff) {
const values = Array.isArray(diff) ? diff : Object.values(diff);
for (const value of values) {
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
const cache = cacheFind(value);
if (cache) return cache;
}
if (typeof value === 'string' && /<p>.+?<\/p>/.test(value)) {
const turndownService = new TurndownService();
return turndownService.turndown(value).trim();
}
}
return null;
}
/**
* Format HTML content into readable markdown text
* @param {string} htmlContent - The HTML content to format
* @returns {string} Formatted text
*/
function formatHtml(htmlContent) {
if (!htmlContent) return '';
const $ = cheerio.load(htmlContent);
const outputLines = [];
$('h1, h2, h3, p, ol, ul, div').each((_, element) => {
const tagName = element.tagName.toLowerCase();
const $el = $(element);
if (['h1', 'h2', 'h3'].includes(tagName)) {
outputLines.push(`\n**${$el.text().trim()}**\n`);
} else if (tagName === 'p') {
let text = $el.text().trim();
// Remove IAsk attribution
text = text.replace(/^According to Ask AI & Question AI www\.iAsk\.ai:\s*/i, '').trim();
// Remove footnote markers
text = text.replace(/\[\d+\]\(#fn:\d+ 'see footnote'\)/g, '');
if (text) outputLines.push(text + '\n');
} else if (['ol', 'ul'].includes(tagName)) {
$el.find('li').each((_, li) => {
outputLines.push('- ' + $(li).text().trim() + '\n');
});
} else if (tagName === 'div' && $el.hasClass('footnotes')) {
outputLines.push('\n**Authoritative Sources**\n');
$el.find('li').each((_, li) => {
const link = $(li).find('a');
if (link.length) {
outputLines.push(`- ${link.text().trim()} (${link.attr('href')})\n`);
}
});
}
});
return outputLines.join('');
}
/**
* Search using IAsk AI via WebSocket (Phoenix LiveView)
* @param {string} prompt - The search query or prompt
* @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
* @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
* @returns {Promise<string>} The search results
*/
async function searchIAsk(prompt, mode = 'thinking', detailLevel = null) {
// Input validation
if (!prompt || typeof prompt !== 'string') {
throw new Error('Invalid prompt: prompt must be a non-empty string');
}
// Validate mode
if (!VALID_MODES.includes(mode)) {
throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
}
// Validate detail level
if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
}
console.log(`IAsk search starting: "${prompt}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'})`);
// Clear old cache entries
clearOldCache();
const cacheKey = getCacheKey(prompt, mode, detailLevel);
const cachedResults = resultsCache.get(cacheKey);
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
console.log(`Cache hit for IAsk query: "${prompt}"`);
return cachedResults.results;
}
// Build URL parameters
const params = new URLSearchParams({ mode, q: prompt });
if (detailLevel) {
params.append('options[detail_level]', detailLevel);
}
// Create a cookie jar for session management
const jar = new CookieJar();
const client = wrapper(axios.create({ jar }));
try {
// Get initial page and extract tokens
console.log('Fetching IAsk AI initial page...');
const response = await client.get(API_ENDPOINT, {
params: Object.fromEntries(params),
timeout: DEFAULT_TIMEOUT,
headers: {
'User-Agent': getRandomUserAgent()
}
});
const $ = cheerio.load(response.data);
const phxNode = $('[id^="phx-"]').first();
const csrfToken = $('[name="csrf-token"]').attr('content');
const phxId = phxNode.attr('id');
const phxSession = phxNode.attr('data-phx-session');
if (!phxId || !csrfToken) {
throw new Error('Failed to extract required tokens from IAsk AI page');
}
// Get the actual response URL (after any redirects)
const responseUrl = response.request.res?.responseUrl || response.config.url;
// Get cookies from the jar for WebSocket connection
const cookies = await jar.getCookies(API_ENDPOINT);
const cookieString = cookies.map(c => `${c.key}=${c.value}`).join('; ');
// Build WebSocket URL
const wsParams = new URLSearchParams({
'_csrf_token': csrfToken,
'vsn': '2.0.0'
});
const wsUrl = `wss://iask.ai/live/websocket?${wsParams.toString()}`;
return new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl, {
headers: {
'Cookie': cookieString,
'User-Agent': getRandomUserAgent(),
'Origin': 'https://iask.ai'
}
});
let buffer = '';
let timeoutId;
let connectionTimeoutId;
// Set connection timeout
connectionTimeoutId = setTimeout(() => {
ws.close();
reject(new Error('IAsk connection timeout: unable to establish WebSocket connection'));
}, 15000);
ws.on('open', () => {
clearTimeout(connectionTimeoutId);
console.log('IAsk WebSocket connection established');
// Send phx_join message
ws.send(JSON.stringify([
null,
null,
`lv:${phxId}`,
'phx_join',
{
params: { _csrf_token: csrfToken },
url: responseUrl,
session: phxSession
}
]));
// Set message timeout
timeoutId = setTimeout(() => {
ws.close();
if (buffer) {
resolve(buffer || 'No results found.');
} else {
reject(new Error('IAsk response timeout: no response received'));
}
}, DEFAULT_TIMEOUT);
});
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (!msg) return;
const diff = msg[4];
if (!diff) return;
let chunk = null;
try {
// Try to get chunk from diff.e[0][1].data
if (diff.e) {
chunk = diff.e[0][1].data;
if (chunk) {
let formatted;
if (/<[^>]+>/.test(chunk)) {
formatted = formatHtml(chunk);
} else {
formatted = chunk.replace(/<br\/>/g, '\n');
}
buffer += formatted;
}
} else {
throw new Error('No diff.e');
}
} catch {
// Fallback to cacheFind
const cache = cacheFind(diff);
if (cache) {
let formatted;
if (/<[^>]+>/.test(cache)) {
formatted = formatHtml(cache);
} else {
formatted = cache;
}
buffer += formatted;
// Close after cache find
ws.close();
return;
}
}
} catch (err) {
console.error('Error parsing IAsk message:', err.message);
}
});
ws.on('close', () => {
clearTimeout(timeoutId);
clearTimeout(connectionTimeoutId);
console.log(`IAsk search completed: ${buffer.length} characters received`);
// Cache the result
if (buffer) {
resultsCache.set(cacheKey, {
results: buffer,
timestamp: Date.now()
});
}
resolve(buffer || 'No results found.');
});
ws.on('error', (err) => {
clearTimeout(timeoutId);
clearTimeout(connectionTimeoutId);
console.error('IAsk WebSocket error:', err.message);
if (err.message.includes('timeout')) {
reject(new Error('IAsk WebSocket timeout: connection took too long'));
} else if (err.message.includes('connection refused')) {
reject(new Error('IAsk connection refused: service may be unavailable'));
} else {
reject(new Error(`IAsk WebSocket error: ${err.message}`));
}
});
});
} catch (error) {
console.error('Error in IAsk search:', error.message);
// Enhanced error handling
if (error.code === 'ENOTFOUND') {
throw new Error('IAsk network error: unable to resolve host');
}
if (error.code === 'ECONNREFUSED') {
throw new Error('IAsk network error: connection refused');
}
if (error.message.includes('timeout')) {
throw new Error(`IAsk timeout: ${error.message}`);
}
throw new Error(`IAsk search failed for "${prompt}": ${error.message}`);
}
}
export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };