import * as cheerio from 'cheerio'; import {$Sync} from '@ztimson/node-utils'; import {ASet, consoleInterceptor, Http, fn as Fn, decodeHtml, objectMap} from '@ztimson/utils'; import * as os from 'node:os'; import {Ai} from './ai.ts'; import {LLMRequest} from './llm.ts'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; const getShell = () => { if(os.platform() == 'win32') return 'cmd'; return $Sync`echo $SHELL`?.split('/').pop() || 'bash'; } export type AiToolArg = {[key: string]: { /** Argument type */ type: 'array' | 'boolean' | 'number' | 'object' | 'string', /** Argument description */ description: string, /** Required argument */ required?: boolean; /** Default value */ default?: any, /** Options */ enum?: string[], /** Minimum value or length */ min?: number, /** Maximum value or length */ max?: number, /** Match pattern */ pattern?: string, /** Child arguments */ items?: {[key: string]: AiToolArg} }} export type AiTool = { /** Tool ID / Name - Must be snail_case */ name: string, /** Tool description / prompt */ description: string, /** Tool arguments */ args?: AiToolArg, /** Callback function */ fn: (args: any, stream: LLMRequest['stream'], ai: Ai, toolId?: string) => any | Promise, }; export function convertSchema(schema: any): any { if(!schema) return null; const convertProp = (prop: any): any => { const converted: any = { type: prop.type || 'string', }; if(prop.description) converted.description = prop.description; if(prop.default !== undefined) converted.default = prop.default; if(prop.enum) converted.enum = prop.enum; if(prop.pattern) converted.pattern = prop.pattern; // Handle array items if(prop.type === 'array' && prop.items) { converted.items = convertProp(prop.items); } // Handle object properties if(prop.type === 'object' && prop.items) { converted.properties = objectMap(prop.items, (key, value) => convertProp(value)); const required = Object.entries(prop.items).filter(([_, v]: any) => v.required).map(([k]) => k); if(required.length) converted.required = required; converted.additionalProperties = false; } // Handle min/max based on type if(prop.min !== undefined) { if(prop.type === 'string' || prop.type === 'array') converted.minLength = prop.min; else converted.minimum = prop.min; } if(prop.max !== undefined) { if(prop.type === 'string' || prop.type === 'array') converted.maxLength = prop.max; else converted.maximum = prop.max; } return converted; }; return { type: 'object', properties: objectMap(schema, (key, value) => convertProp(value)), required: Object.entries(schema).filter(([_, v]: any) => v.required).map(([k]) => k), additionalProperties: false }; } export const ExecCliTool: AiTool = { name: 'cli', description: 'Use the command line interface, returns any output', args: {command: {type: 'string', description: 'Command to run', required: true}}, fn: (args: {command: string}) => $Sync`${args.command}` } export const ExecJSTool: AiTool = { name: 'exec_javascript', description: 'Execute commonjs javascript', args: { code: {type: 'string', description: 'CommonJS javascript', required: true} }, fn: async (args: {code: string}) => { const c = consoleInterceptor(null); const resp = await Fn({console: c}, args.code, true).catch((err: any) => c.output.error.push(err)); return {...c.output, return: resp, stdout: undefined, stderr: undefined}; } } export const ExecPythonTool: AiTool = { name: 'exec_python', description: 'Execute commonjs javascript', args: { code: {type: 'string', description: 'CommonJS javascript', required: true} }, fn: async (args: {code: string}) => ({result: $Sync`python -c "${args.code}"`}) } export const ExecTool: AiTool = { name: 'exec', description: 'Run code/scripts', args: { language: {type: 'string', description: `Execution language (CLI: ${getShell()})`, enum: ['cli', 'node', 'python'], required: true}, code: {type: 'string', description: 'Code to execute', required: true} }, fn: async (args, stream, ai) => { try { switch(args.language) { case 'cli': return await ExecCliTool.fn({command: args.code}, stream, ai); case 'node': return await ExecJSTool.fn({code: args.code}, stream, ai); case 'python': return await ExecPythonTool.fn({code: args.code}, stream, ai); default: throw new Error(`Unsupported language: ${args.language}`); } } catch(err: any) { return {error: err?.message || err.toString()}; } } } export const FsDeleteTool = (whitelist: null | string[] = null): AiTool => { return { name: 'fs_delete', description: 'Delete a file or directory', args: { path: {type: 'string', description: 'Path to file or directory', required: true}, recursive: {type: 'boolean', description: 'Delete all children', required: false} }, fn: async ({path, recursive = false}) => { const {existsSync, rmSync} = await import('fs'); const normalizePath = p => p.replace(/\\/g, '/'); path = normalizePath(path); if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; if(!existsSync(path)) return {error: 'Path does not exist'}; rmSync(path, {recursive, force: true}); return {success: true, path}; } } } export const FsMoveTool = (whitelist: null | string[] = null): AiTool => { return { name: 'fs_move', description: 'Move or rename a file or directory', args: { source: {type: 'string', description: 'Path to source file or directory', required: true}, destination: {type: 'string', description: 'Path to destination file or directory', required: true} }, fn: async ({source, destination}) => { const {existsSync, renameSync} = await import('fs'); const normalizePath = p => p.replace(/\\/g, '/'); source = normalizePath(source); destination = normalizePath(destination); if(whitelist && !whitelist.some(p => source.startsWith(p) && destination.startsWith(p))) return {error: 'Permission denied'}; if(!existsSync(source)) return {error: 'Source path does not exist'}; if(existsSync(destination)) return {error: 'Destination path already exists'}; renameSync(source, destination); return {success: true, source, destination}; } } } export const FsReadTool = (whitelist: null | string[] = null): AiTool => { return { name: 'fs_read', description: 'Read the contents of a provided path. Works with files and directories', args: {path: {type: 'string', description: 'Path to file or directory', required: true}}, fn: async ({path}) => { const {existsSync, lstatSync, readdirSync, readFileSync} = await import('fs'); const {join} = await import('path'); const normalizePath = p => p.replace(/\\/g, '/'); path = normalizePath(path); if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; if(!existsSync(path)) return {error: 'Path does not exist'}; const stats = lstatSync(path); if(stats.isDirectory()) { const children = readdirSync(path).map(name => { const childPath = normalizePath(join(path, name)); const childStats = lstatSync(childPath); return {name, type: childStats.isDirectory() ? 'directory' : 'file', size: childStats.size}; }); return {type: 'directory', children}; } const content = readFileSync(path, 'utf-8'); return {type: 'file', content}; } } } export const FsSearchTool = (whitelist: null | string[] = null): AiTool => { return { name: 'fs_search', description: 'Scan a directory for matching glob patterns (e.g. "**/*.js", "src/**/*.test.ts")', args: { pattern: {type: 'string', description: 'Glob pattern to match against paths', required: true}, root: {type: 'string', description: 'Directory to search from', required: false, default: '.'} }, fn: async ({pattern, root = '.'}) => { const {existsSync, lstatSync, readdirSync} = await import('fs'); const {join, relative} = await import('path'); const normalizePath = p => p.replace(/\\/g, '/'); root = normalizePath(root); if(!existsSync(root)) return {error: 'Root path does not exist'}; if(!lstatSync(root).isDirectory()) return {error: 'Root path is not a directory'}; if(whitelist && !whitelist.some(p => root.startsWith(p))) return {error: 'Permission denied'}; const globToRegex = (glob) => { let re = ''; for(let i = 0; i < glob.length; i++) { const c = glob[i]; if(c === '*') { if(glob[i + 1] === '*') { const isSlash = glob[i + 2] === '/'; re += '.*'; i += isSlash ? 2 : 1; } else { re += '[^/]*'; } } else if(c === '?') { re += '[^/]'; } else if('.+^$(){}|[]\\'.includes(c)) { re += '\\' + c; } else { re += c; } } return new RegExp('^' + re + '$'); }; const regex = globToRegex(pattern); const results: any = []; const walk = (dir) => { for(const name of readdirSync(dir)) { const fullPath = normalizePath(join(dir, name)); const stats = lstatSync(fullPath); const relPath = normalizePath(relative(root, fullPath)); if(regex.test(relPath)) { results.push({path: relPath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size}); } if(stats.isDirectory()) walk(fullPath); } }; walk(root); return results; } } } export const FsWriteTool = (whitelist: null | string[] = null): AiTool => { return { name: 'fs_write', description: 'Create a directory, write content to a file or preform a find & replace', args: { path: {type: 'string', description: 'Path to file or directory', required: true}, content: {type: 'string', description: 'Content to write or replace (Omit to create a directory)'}, find: {type: 'string', description: 'Text or regex pattern to match (regex must match pattern: "/pattern/g")'} }, fn: async ({path, content, find}) => { const {existsSync, mkdirSync, readFileSync, writeFileSync} = await import('fs'); const {dirname} = await import('path'); const normalizePath = p => p.replace(/\\/g, '/'); path = normalizePath(path); if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; if(content === undefined) { mkdirSync(path, {recursive: true}); return {success: true, type: 'directory', path}; } const dir = normalizePath(dirname(path)); if(!existsSync(dir)) mkdirSync(dir, {recursive: true}); if(find && existsSync(path)) { const existing = readFileSync(path, 'utf-8'); const regexMatch = find.match(/^\/(.+)\/([gimuy]*)$/); const pattern = regexMatch ? new RegExp(regexMatch[1], regexMatch[2]) : find; if(!existing.match(pattern)) return {error: 'Find pattern not found in file'}; const updated = existing.replace(pattern, content); writeFileSync(path, updated, 'utf-8'); return {success: true, type: 'file', path, replaced: true, content: updated}; } writeFileSync(path, content, 'utf-8'); return {success: true, type: 'file', path, content}; } } } export const GetPathsTool: AiTool = { name: 'get_paths', description: 'Get the current working directory, and paths to the users home directory', fn: async () => { return { home: os.homedir(), cwd: process.cwd() }; } } export const GetDatetimeTool: AiTool = { name: 'get_datetime', description: 'Get local/UTC timestamp', args: { timezone: {type: 'string', description: 'Which timezone to return, defaults to local', enum: ['local', 'utc'], default: 'local'} }, fn: ({timezone}) => new Date()[timezone === 'local' ? 'toString' : 'toUTCString']() } export const GetDevice: AiTool = { name: 'get_device', description: 'Get comprehensive system information including hostname, specs, load, storage, and network status', args: {}, fn: async () => { const platform = os.platform(); const hostname = os.hostname(); // CPU Info const cpus = os.cpus(); const cpuModel = cpus[0].model; const cpuCores = cpus.length; // Memory Info const totalMem: any = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2); const freeMem: any = (os.freemem() / 1024 / 1024 / 1024).toFixed(2); const usedMem: any = (totalMem - freeMem).toFixed(2); const memUsage: any = ((usedMem / totalMem) * 100).toFixed(1); // Load Average (not available on Windows) const loadAvg = platform === 'win32' ? ['N/A', 'N/A', 'N/A'] : os.loadavg().map(l => l.toFixed(2)); // Storage Usage let storage = {}; if(platform === 'win32') { const ps = $Sync`powershell "Get-PSDrive C | Select-Object Used,Free | ConvertTo-Json"`.trim(); const drive = JSON.parse(ps); const used: any = (drive.Used / 1024 / 1024 / 1024).toFixed(2); const free: any = (drive.Free / 1024 / 1024 / 1024).toFixed(2); const total: any = (parseFloat(used) + parseFloat(free)).toFixed(2); const usage: any = ((used / total) * 100).toFixed(1); storage = { filesystem: 'C:', size: `${total} GB`, used: `${used} GB`, available: `${free} GB`, usage: `${usage}%` }; } else { const df = $Sync`df -h / | tail -1`.trim(); const s = df.split(/\s+/); storage = { filesystem: s[0], size: s[1], used: s[2], available: s[3], usage: s[4] }; } // Network Status const interfaces = os.networkInterfaces(); const activeIfaces = Object.entries(interfaces) .filter(([name]) => name !== 'lo' && !name.includes('Loopback')) .map(([name, addrs]) => { const ipv4 = addrs?.find(a => a.family === 'IPv4'); return ipv4 ? {name, ip: ipv4.address} : null; }) .filter(Boolean); // Internet connectivity check let internet = false; try { if(platform === 'win32') { $Sync`powershell "Test-Connection -ComputerName 8.8.8.8 -Count 1 -Quiet"`; } else { $Sync`ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1`; } internet = true; } catch {} // Uptime const uptime = os.uptime(); const days = Math.floor(uptime / 86400); const hours = Math.floor((uptime % 86400) / 3600); const minutes = Math.floor((uptime % 3600) / 60); return { hostname, cpu: { model: cpuModel, cores: cpuCores }, memory: { total: `${totalMem} GB`, used: `${usedMem} GB`, free: `${freeMem} GB`, usage: `${memUsage}%` }, load: { '1min': loadAvg[0], '5min': loadAvg[1], '15min': loadAvg[2] }, storage, network: { interfaces: activeIfaces, internet: internet ? 'connected' : 'disconnected' }, uptime: `${days}d ${hours}h ${minutes}m`, platform: `${os.type()} ${os.release()}` }; } } export const GetWikipediaTool: AiTool = { name: 'get_wikipedia', description: 'Search Wikipedia for matching articles', args: { query: {type: 'string', description: 'Search term or article title', required: true}, mode: {type: 'string', description: 'search - look for articles, summary - intro of first found article (default), full - complete first found article', enum: ['search', 'summary', 'full'], default: 'summary'}, ua: {type: 'string', description: 'User Agent'}, }, fn: async ({query, mode, ua}) => { class WikipediaClient { useragent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; constructor(useragent: string) { this.useragent = useragent; } async get(url) { const resp = await fetch(url, {headers: {'User-Agent': this.useragent}}); return resp.json(); } api(params) { const qs = new URLSearchParams({...params, format: 'json', utf8: '1'}).toString(); return this.get(`https://en.wikipedia.org/w/api.php?${qs}`); } clean(text) { const cutoffs = ['== See also ==', '== References ==', '== Bibliography ==', '== External links ==']; for (const marker of cutoffs) { const idx = text.indexOf(marker); if (idx !== -1) text = text.slice(0, idx); } return text .replace(/^={4}\s*(.+?)\s*={4}$/gm, '#### $1') .replace(/^={3}\s*(.+?)\s*={3}$/gm, '### $1') .replace(/^={2}\s*(.+?)\s*={2}$/gm, '## $1') .replace(/\n{3,}/g, '\n\n') .replace(/ {2,}/g, ' ') .replace(/\[\d+]/g, '') .trim(); } async searchTitles(query: string, limit = 6) { const data = await this.api({action: 'query', list: 'search', srsearch: query, srlimit: limit, srprop: 'snippet'}); return data.query?.search || []; } async fetchExtract(title: string, introOnly = false) { const params: any = {action: 'query', prop: 'extracts', titles: title, explaintext: 1, redirects: 1}; if(introOnly) params.exintro = 1; const data = await this.api(params); const page: any = Object.values(data.query?.pages || {})[0]; return this.clean(page?.extract || ''); } pageUrl(title: string) { return `https://en.wikipedia.org/wiki/${encodeURIComponent(title.replace(/ /g, '_'))}`; } stripHtml(text: string) { return text.replace(/<[^>]+>/g, ''); } async lookup(query: string, detail = 'summary') { const results = await this.searchTitles(query, 6); if(!results.length) return `āŒ No Wikipedia articles found for "${query}"`; const title = results[0].title; const url = this.pageUrl(title); const introOnly = detail !== 'full'; const content = await this.fetchExtract(title, introOnly); return `## ${title}\nšŸ”— ${url}\n\n${content}`; } async search(query: string) { const results = await this.searchTitles(query, 8); if(!results.length) return `āŒ No results for "${query}"`; const lines = [`### Search results for "${query}"\n`]; for(let i = 0; i < results.length; i++) { const r = results[i]; const snippet = this.stripHtml(r.snippet || '').trim(); lines.push(`**${i + 1}. ${r.title}**\n${snippet}\n${this.pageUrl(r.title)}`); } return lines.join('\n\n'); } } const wiki = new WikipediaClient(ua); if(mode === 'search') return wiki.search(query); return wiki.lookup(query, mode || 'summary'); } }; export const GeoCodeTool: AiTool = { name: 'geo_code', description: 'Converts coordinates to address OR vice versa', args: { query: {type: 'string', description: 'Search query - coordinates (lat,lon) or address string', required: true}, }, fn: async ({query}) => { const coordinates = /(-?\d+(?:\.\d+)?).*?,.*?(-?\d+(?:\.\d+)?)/.exec(query); if(coordinates) { // Geolocate const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${encodeURIComponent(coordinates[1])}&lon=${encodeURIComponent(coordinates[2])}`; const response = await fetch(url, {headers: {'User-Agent': 'OpenSight/1.0', 'Accept-Language': 'en'}}); const data = await response.json(); if(data.display_name) return {address: data.display_name, mode: 'geolocate'}; } else { // Geocode const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`; const response = await fetch(url, {headers: {'User-Agent': 'OpenSight/1.0'}}); const data = await response.json(); if(data[0]) return {latitude: parseFloat(data[0].lat), longitude: parseFloat(data[0].lon), mode: 'geocode'}; } return {error: 'Not found'}; }, } export const GeoWeatherTool: AiTool = { name: 'geo_weather', description: 'Gets weather and air quality info for a location and time', args: { query: {type: 'string', description: 'Location - address or place name', required: true}, day: {type: 'string', description: 'Date to retrieve (YYYY-MM-DD), defaults to today'}, }, fn: async ({query, day}) => { day = day || new Date().toISOString().slice(0, 10); const geoUrl = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`; const geoResponse = await fetch(geoUrl, {headers: {'User-Agent': 'OpenSight/1.0'}}); const geoData = await geoResponse.json(); if(!geoData[0]) return {error: 'Location not found'}; const lat = parseFloat(geoData[0].lat); const lon = parseFloat(geoData[0].lon); const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&start_date=${day}&end_date=${day}&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max,winddirection_10m_dominant,uv_index_max,sunrise,sunset&timezone=auto`; const airUrl = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}&start_date=${day}&end_date=${day}&hourly=us_aqi,european_aqi,pm10,pm2_5&timezone=auto`; const [weatherResponse, airResponse] = await Promise.all([fetch(weatherUrl), fetch(airUrl)]); const weatherData = await weatherResponse.json(); const airData = await airResponse.json(); const avg = arr => (arr && arr.length) ? arr.reduce((a, b) => a + b, 0) / arr.length : null; return { location: geoData[0].display_name, latitude: lat, longitude: lon, elevation: weatherData.elevation, date: day, weatherCode: weatherData.daily?.weathercode?.[0], tempMax: weatherData.daily?.temperature_2m_max?.[0], tempMin: weatherData.daily?.temperature_2m_min?.[0], feelsLikeMax: weatherData.daily?.apparent_temperature_max?.[0], feelsLikeMin: weatherData.daily?.apparent_temperature_min?.[0], precipitation: weatherData.daily?.precipitation_sum?.[0], precipitationChance: weatherData.daily?.precipitation_probability_max?.[0], windSpeedMax: weatherData.daily?.windspeed_10m_max?.[0], windDirection: weatherData.daily?.winddirection_10m_dominant?.[0], uvIndexMax: weatherData.daily?.uv_index_max?.[0], sunrise: weatherData.daily?.sunrise?.[0], sunset: weatherData.daily?.sunset?.[0], usAqi: avg(airData.hourly?.us_aqi), europeanAqi: avg(airData.hourly?.european_aqi), pm10: avg(airData.hourly?.pm10), pm2_5: avg(airData.hourly?.pm2_5), }; }, } export const WebFetchTool: AiTool = { name: 'web_fetch', description: 'Make HTTP request to URL', args: { url: {type: 'string', description: 'URL to fetch', required: true}, method: {type: 'string', description: 'HTTP method to use', enum: ['GET', 'POST', 'PUT', 'DELETE'], default: 'GET'}, headers: {type: 'object', description: 'HTTP headers to send', default: {}}, body: {type: 'object', description: 'HTTP body to send'}, }, fn: (args: { url: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; headers: {[key: string]: string}; body: any; }) => new Http({url: args.url, headers: args.headers}).request({method: args.method || 'GET', body: args.body}) } export const WebFlareSolverTool = (host: string) => { return { name: 'web_flaresolverr', description: 'Use a flaresolverr proxy to bypass cloudflare bot detection', args: { url: {type: 'string', description: 'URL to fetch', required: true}, cmd: {type: 'string', description: 'Flaresolverr cmd', enum: ['request.get', 'request.post'], default: 'request.get'}, maxTimeout: {type: 'number', description: 'Fetch time limit', default: 60_000}, postData: {type: 'object', description: 'Data to send during request.post requests'}, }, fn: async ({url, cmd, maxTimeout, postData}) => { function toFormUrlEncoded(obj, prefix = '') { const pairs: any = []; for (const key in obj) { if (!obj.hasOwnProperty(key)) continue; const value = obj[key]; const encodedKey = prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key); if (value === null || value === undefined) { pairs.push(`${encodedKey}=`); } else if (typeof value === 'object' && !Array.isArray(value)) { pairs.push(toFormUrlEncoded(value, encodedKey)); } else if (Array.isArray(value)) { value.forEach(item => { pairs.push(`${encodedKey}[]=${encodeURIComponent(item)}`); }); } else { pairs.push(`${encodedKey}=${encodeURIComponent(value)}`); } } return pairs.join('&'); } const res = await fetch(host + '/v1', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({cmd, url, maxTimeout, postData: postData ? toFormUrlEncoded(postData) : undefined}), }); if(!res.ok) throw new Error(`FlareSolverr HTTP error: ${res.status} ${res.statusText}`); const data = await res.json(); if(data.status !== 'ok') throw new Error(`FlareSolverr error: ${data.message ?? data.status}`); return data.solution.response; } } } export const WebReadTool: AiTool = { name: 'web_read', description: 'Extract clean content from webpages, or convert media/documents to accessible formats', args: { url: {type: 'string', description: 'URL to read', required: true}, mimeRegex: {type: 'string', description: 'Optional regex to filter MIME types (e.g., "^image/", "text/")'} }, fn: async (args: {url: string; mimeRegex?: string}) => { const ua = 'AiTools-Webpage/1.0'; const maxSize = 10 * 1024 * 1024; const response = await fetch(args.url, { headers: { 'User-Agent': ua, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5' }, redirect: 'follow' }).catch(err => {throw new Error(`Failed to fetch: ${err.message}`)}); const contentType = response.headers.get('content-type') || ''; const mimeType = contentType.split(';')[0].trim().toLowerCase(); if(args.mimeRegex && !new RegExp(args.mimeRegex, 'i').test(mimeType)) { return `āŒ MIME type rejected: ${mimeType} (filter: ${args.mimeRegex})`; } if(mimeType.match(/^(image|audio|video)\//)) { const buffer = await response.arrayBuffer(); if(buffer.byteLength > maxSize) { return `āŒ File too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB (max 10MB)\nType: ${mimeType}`; } const base64 = Buffer.from(buffer).toString('base64'); return `## Media File\n**Type:** ${mimeType}\n**Size:** ${(buffer.byteLength / 1024).toFixed(1)}KB\n**Data URL:** \`data:${mimeType};base64,${base64.slice(0, 100)}...\``; } if(mimeType.match(/^text\/(plain|csv|xml)/) || args.url.match(/\.(txt|csv|xml|md|yaml|yml)$/i)) { const text = await response.text(); const truncated = text.length > 50000 ? text.slice(0, 50000) : text; return `## Text File\n**Type:** ${mimeType}\n**URL:** ${args.url}\n\n${truncated}`; } if(mimeType.match(/application\/(json|xml|csv)/)) { const text = await response.text(); const truncated = text.length > 50000 ? text.slice(0, 50000) : text; return `## Structured Data\n**Type:** ${mimeType}\n**URL:** ${args.url}\n\n\`\`\`\n${truncated}\n\`\`\``; } if(mimeType === 'application/pdf' || (mimeType.startsWith('application/') && !mimeType.includes('html'))) { const buffer = await response.arrayBuffer(); if(buffer.byteLength > maxSize) { return `āŒ File too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB (max 10MB)\nType: ${mimeType}`; } const base64 = Buffer.from(buffer).toString('base64'); return `## Binary File\n**Type:** ${mimeType}\n**Size:** ${(buffer.byteLength / 1024).toFixed(1)}KB\n**Data URL:** \`data:${mimeType};base64,${base64.slice(0, 100)}...\``; } // HTML const html = await response.text(); const $ = cheerio.load(html); $('script, style, nav, footer, header, aside, iframe, noscript, svg').remove(); $('[role="navigation"], [role="banner"], [role="complementary"]').remove(); $('[aria-hidden="true"], [hidden], .visually-hidden, .sr-only, .screen-reader-text').remove(); $('.ad, .ads, .advertisement, .cookie, .popup, .modal, .sidebar, .related, .comments, .social-share').remove(); $('button, [class*="share"], [class*="follow"], [class*="social"]').remove(); const title = $('meta[property="og:title"]').attr('content') || $('title').text().trim() || ''; const description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || ''; const author = $('meta[name="author"]').attr('content') || ''; let content = ''; const selectors = ['article', 'main', '[role="main"]', '.content', '.post-content', '.entry-content', '.article-content']; for(const sel of selectors) { const el = $(sel).first(); if(el.length && el.text().trim().length > 200) { const paragraphs: string[] = []; el.find('p').each((_, p) => { const text = $(p).text().trim(); if(text.length > 80) paragraphs.push(text); }); if(paragraphs.length > 2) { content = paragraphs.join('\n\n'); break; } } } if(!content) { const paragraphs: string[] = []; $('body p').each((_, p) => { const text = $(p).text().trim(); if(text.length > 80) paragraphs.push(text); }); content = paragraphs.slice(0, 30).join('\n\n'); } // Decode escaped newlines and clean const parts = [`## ${title || 'Webpage'}`]; if(description) parts.push(`_${description}_`); if(author) parts.push(`šŸ‘¤ ${author}`); parts.push(`šŸ”— ${args.url}\n`); parts.push(content); return decodeHtml(parts.join('\n\n').replaceAll(/\n{3,}/g, '\n\n')); } }; export const WebSearchTool: AiTool = { name: 'web_search', description: 'Use duckduckgo (anonymous) to find find relevant online resources. Returns a list of URLs that works great with the `read_webpage` tool', args: { query: {type: 'string', description: 'Search string', required: true}, length: {type: 'string', description: 'Number of results to return', default: 5}, }, fn: async (args: { query: string; length: number; }) => { const html = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`, { headers: {"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"} }).then(resp => resp.text()); let match, regex = //g; const results = new ASet(); while((match = regex.exec(html)) !== null) { let url = /uddg=(.+)&?/.exec(decodeURIComponent(match[1]))?.[1]; if(url) url = decodeURIComponent(url); if(url) results.add(url); if(results.size >= (args.length || 5)) break; } return results; } }