src / toolsProvider.ts
import { rawFunctionTool, type ToolsProviderController } from "@lmstudio/sdk";
const MAX_FETCH_CHARS = 10000;
const MAX_SEARCH_RESULTS = 10;
const FETCH_TIMEOUT_MS = 15000;
const HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/json,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9,ru;q=0.8"
};
const SEARXNG_INSTANCES = [
"https://search.bus-hit.me",
"https://search.ononoki.org",
"https://searx.be",
"https://search.sapti.me",
"https://priv.au",
"https://searx.tiekoetter.com",
"https://searx.work",
"https://search.mdosch.de",
"https://searxng.site",
"https://etsi.me",
];
interface SearchResult {
title: string;
url: string;
snippet: string;
source: string;
}
function validateUrl(url: string): string {
const trimmed = url.trim();
if (!/^https?:\/\//i.test(trimmed)) {
return "https://" + trimmed;
}
return trimmed;
}
function extractTextFromHtml(html: string): string {
const withoutScripts = html.replace(/<script[\s\S]*?<\/script>/gi, "");
const withoutStyles = withoutScripts.replace(/<style[\s\S]*?<\/style>/gi, "");
const withoutComments = withoutStyles.replace(/<!--[\s\S]*?-->/g, "");
const withBreaks = withoutComments
.replace(/<(br|p|div|li|tr|h[1-6])[\s>]/gi, "\n")
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n")
.replace(/<\/td>/gi, " | ")
.replace(/<\/th>/gi, " | ");
const withoutTags = withBreaks.replace(/<[^>]*>/g, "");
const decoded = withoutTags
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ")
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
const normalized = decoded.replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n");
return normalized.trim();
}
async function fetchWithTimeout(url: string, options: RequestInit, timeout: number): Promise<Response> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(id);
}
}
async function searchWikipedia(query: string): Promise<SearchResult[]> {
const url = `https://en.wikipedia.org/w/api.php?action=opensearch&search=${encodeURIComponent(query)}&limit=${MAX_SEARCH_RESULTS}&format=json&origin=*`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const titles: string[] = data[1] || [];
const descriptions: string[] = data[2] || [];
const urls: string[] = data[3] || [];
return titles.map((title, i) => ({
title,
url: urls[i] || "",
snippet: descriptions[i] || "",
source: "Wikipedia"
}));
}
async function searchSearXNG(query: string, instance: string): Promise<SearchResult[]> {
const url = `${instance}/search?q=${encodeURIComponent(query)}&format=json&language=auto&categories=general`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const items = data.results || [];
return items.slice(0, MAX_SEARCH_RESULTS).map((item: any) => ({
title: item.title || "",
url: item.url || "",
snippet: item.content || "",
source: "SearXNG"
}));
}
async function searchDuckDuckGoHtml(query: string): Promise<SearchResult[]> {
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const res = await fetchWithTimeout(url, {
headers: {
...HEADERS,
"Cookie": "kl=wt-wt"
},
redirect: "follow"
}, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const html = await res.text();
if (html.length < 1000 || html.includes("One or more Invalid parameters")) return [];
const results: SearchResult[] = [];
const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = linkRegex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 2) continue;
results.push({
title,
url: m[1],
snippet: "",
source: "DuckDuckGo"
});
}
if (results.length === 0) {
const fallbackRegex = /<a[^>]*rel="nofollow"[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
while ((m = fallbackRegex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 3 || m[1].includes("duckduckgo")) continue;
results.push({ title, url: m[1], snippet: "", source: "DuckDuckGo" });
}
}
return results;
}
async function searchDuckDuckGoLite(query: string): Promise<SearchResult[]> {
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const html = await res.text();
if (html.length < 500) return [];
const results: SearchResult[] = [];
const regex = /<a[^>]*rel="nofollow"[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = regex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 3 || m[1].includes("duckduckgo")) continue;
results.push({ title, url: m[1], snippet: "", source: "DuckDuckGo Lite" });
}
return results;
}
async function searchDuckDuckGoSuggestions(query: string): Promise<SearchResult[]> {
const url = `https://duckduckgo.com/ac/?q=${encodeURIComponent(query)}&type=list`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const phrases: string[] = data[1] || [];
return phrases.map((phrase) => ({
title: phrase,
url: `https://duckduckgo.com/?q=${encodeURIComponent(phrase)}`,
snippet: "",
source: "DDG Suggestions"
}));
}
async function searchBing(query: string): Promise<SearchResult[]> {
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${MAX_SEARCH_RESULTS}`;
const res = await fetchWithTimeout(url, {
headers: { ...HEADERS, "Accept-Language": "en-US,en;q=0.9" },
redirect: "follow"
}, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const html = await res.text();
if (html.length < 2000) return [];
const results: SearchResult[] = [];
const regex = /<li class="b_algo"[^>]*>([\s\S]*?)<\/li>/gi;
let m;
while ((m = regex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const block = m[1];
const urlM = block.match(/href="(https?:\/\/[^"]+)"/);
const titleM = block.match(/<h2[^>]*><a[^>]*>([\s\S]*?)<\/a><\/h2>/i);
const snippetM = block.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
if (urlM && titleM) {
results.push({
title: extractTextFromHtml(titleM[1]).trim(),
url: urlM[1],
snippet: snippetM ? extractTextFromHtml(snippetM[1]).trim() : "",
source: "Bing"
});
}
}
if (results.length === 0) {
const fallback = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
while ((m = fallback.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 5 || m[1].includes("bing.com") || m[1].includes("microsoft.com")) continue;
results.push({ title, url: m[1], snippet: "", source: "Bing" });
}
}
return results;
}
async function searchYandex(query: string): Promise<SearchResult[]> {
const url = `https://yandex.ru/search/?text=${encodeURIComponent(query)}&lr=213&numdoc=${MAX_SEARCH_RESULTS}`;
const res = await fetchWithTimeout(url, {
headers: { ...HEADERS, "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8" },
redirect: "follow"
}, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const html = await res.text();
if (html.length < 2000) return [];
const results: SearchResult[] = [];
const regex = /<a[^>]*class="link[^"]*organic__link"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = regex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 2) continue;
results.push({ title, url: m[1], snippet: "", source: "Yandex" });
}
if (results.length === 0) {
const fallback = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*class="[^"]*link[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
while ((m = fallback.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 3 || m[1].includes("yandex.")) continue;
results.push({ title, url: m[1], snippet: "", source: "Yandex" });
}
}
if (results.length === 0) {
const anyLink = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([^<]{5,})<\/a>/gi;
const seen = new Set<string>();
while ((m = anyLink.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
if (m[1].includes("yandex.") || seen.has(m[1])) continue;
seen.add(m[1]);
results.push({ title: extractTextFromHtml(m[2]).trim(), url: m[1], snippet: "", source: "Yandex" });
}
}
return results;
}
async function searchGoogleSuggestions(query: string): Promise<SearchResult[]> {
const url = `https://suggestqueries.google.com/complete/search?client=firefox&q=${encodeURIComponent(query)}`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const phrases: string[] = data[1] || [];
return phrases.map((phrase) => ({
title: phrase,
url: `https://www.google.com/search?q=${encodeURIComponent(phrase)}`,
snippet: "",
source: "Google Suggestions"
}));
}
function deduplicateResults(allResults: SearchResult[]): SearchResult[] {
const seen = new Set<string>();
const unique: SearchResult[] = [];
for (const r of allResults) {
const key = r.url.toLowerCase().replace(/\/$/, "");
if (seen.has(key) || !r.title || r.title.length < 2) continue;
seen.add(key);
unique.push(r);
}
return unique.slice(0, MAX_SEARCH_RESULTS);
}
async function fetchPageContent(url: string): Promise<string> {
try {
const res = await fetchWithTimeout(url, { headers: HEADERS, redirect: "follow" }, FETCH_TIMEOUT_MS);
if (!res.ok) return "";
const html = await res.text();
const text = extractTextFromHtml(html);
return text.substring(0, 3000).trim();
} catch {
return "";
}
}
async function searchWttrIn(query: string): Promise<SearchResult[]> {
const url = `https://wttr.in/${encodeURIComponent(query)}?format=4&lang=ru`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const text = await res.text();
if (!text || text.length < 10) return [];
return [{
title: `Погода: ${query}`,
url: `https://wttr.in/${encodeURIComponent(query)}?lang=ru`,
snippet: text.trim(),
source: "wttr.in"
}];
}
async function searchOpenMeteo(query: string): Promise<SearchResult[]> {
const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=ru`;
const geoRes = await fetchWithTimeout(geoUrl, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!geoRes.ok) return [];
const geoData = await geoRes.json();
if (!geoData.results || geoData.results.length === 0) return [];
const loc = geoData.results[0];
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${loc.latitude}&longitude=${loc.longitude}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code,apparent_temperature&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max&timezone=auto&forecast_days=7`;
const weatherRes = await fetchWithTimeout(weatherUrl, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!weatherRes.ok) return [];
const data = await weatherRes.json();
const current = data.current;
const daily = data.daily;
const weatherCodes: Record<number, string> = {
0: "Ясно", 1: "Преимущественно ясно", 2: "Переменная облачность", 3: "Пасмурно",
45: "Туман", 48: "Изморозь", 51: "Лёгкая морось", 53: "Морось", 55: "Сильная морось",
61: "Небольшой дождь", 63: "Дождь", 65: "Сильный дождь",
71: "Небольшой снег", 73: "Снег", 75: "Сильный снег",
80: "Небольшой ливень", 81: "Ливень", 82: "Сильный ливень",
95: "Гроза", 96: "Гроза с градом", 99: "Сильная гроза с градом"
};
const weatherDesc = (code: number) => weatherCodes[code] || `Код ${code}`;
let text = `Погода в ${loc.name}, ${loc.admin1 || ""}, ${loc.country || ""}:\n`;
text += `Сейчас: ${weatherDesc(current.weather_code)}, ${current.temperature_2m}°C (ощущается как ${current.apparent_temperature}°C), влажность ${current.relative_humidity_2m}%, ветер ${current.wind_speed_10m} км/ч\n`;
text += "Прогноз на неделю:\n";
for (let i = 0; i < (daily.time?.length || 0); i++) {
const prob = daily.precipitation_probability_max?.[i] ?? "?";
text += ` ${daily.time[i]}: ${daily.temperature_2m_min[i]}°C / ${daily.temperature_2m_max[i]}°C — ${weatherDesc(daily.weather_code[i])}, осадки ${prob}%\n`;
}
return [{
title: `Погода в ${loc.name}`,
url: `https://open-meteo.com/`,
snippet: text.trim(),
source: "Open-Meteo"
}];
}
async function searchMetNo(query: string): Promise<SearchResult[]> {
const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=ru`;
const geoRes = await fetchWithTimeout(geoUrl, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!geoRes.ok) return [];
const geoData = await geoRes.json();
if (!geoData.results || geoData.results.length === 0) return [];
const loc = geoData.results[0];
const weatherUrl = `https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${loc.latitude}&lon=${loc.longitude}`;
const weatherRes = await fetchWithTimeout(weatherUrl, {
headers: { ...HEADERS, "User-Agent": "LMStudio-WebSearch/1.0" }
}, FETCH_TIMEOUT_MS);
if (!weatherRes.ok) return [];
const data = await weatherRes.json();
const timeseries = data.properties?.timeseries || [];
if (timeseries.length === 0) return [];
const current = timeseries[0];
const details = current.data?.instant?.details || {};
const next1h = timeseries[1]?.data?.instant?.details || {};
let text = `Погода в ${loc.name} (met.no):\n`;
text += `Сейчас: ${details.air_temperature || "?"}°C, ветер ${details.wind_speed || "?"} м/с, влажность ${details.relative_humidity || "?"}%, давление ${details.air_pressure_at_sea_level || "?"} гПа\n`;
if (next1h.air_temperature) {
text += `Через час: ${next1h.air_temperature}°C\n`;
}
const symbols = timeseries.slice(0, 7).map((t: any) => {
const sym = t.data?.next_1_hours?.summary?.symbol_code || "";
return `${t.time?.substring(5, 16) || ""} — ${sym}`;
}).filter(Boolean);
if (symbols.length > 0) {
text += "Ближайшие часы:\n" + symbols.join("\n") + "\n";
}
return [{
title: `Погода в ${loc.name} (met.no)`,
url: `https://www.yr.no/`,
snippet: text.trim(),
source: "met.no"
}];
}
async function search7Timer(query: string): Promise<SearchResult[]> {
const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=ru`;
const geoRes = await fetchWithTimeout(geoUrl, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!geoRes.ok) return [];
const geoData = await geoRes.json();
if (!geoData.results || geoData.results.length === 0) return [];
const loc = geoData.results[0];
const weatherUrl = `https://www.7timer.info/bin/api.pl?lon=${loc.longitude}&lat=${loc.latitude}&product=civillight&output=json`;
const weatherRes = await fetchWithTimeout(weatherUrl, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!weatherRes.ok) return [];
const data = await weatherRes.json();
const dataseries = data.dataseries || [];
if (dataseries.length === 0) return [];
let text = `Погода в ${loc.name} (7timer.info):\n`;
for (let i = 0; i < Math.min(7, dataseries.length); i++) {
const d = dataseries[i];
text += ` ${d.date?.substring(0, 4) || ""}-${d.date?.substring(4, 6) || ""}-${d.date?.substring(6, 8) || ""}: ${d.weather} — ${d.temp2m?.day || "?"}°C днём, ${d.temp2m?.night || "?"}°C ночью, ветер ${d.wind10m?.speed || "?"} м/с\n`;
}
return [{
title: `Погода в ${loc.name} (7timer)`,
url: `https://www.7timer.info/`,
snippet: text.trim(),
source: "7timer"
}];
}
async function searchQwant(query: string): Promise<SearchResult[]> {
const url = `https://api.qwant.com/v3/search/web?q=${encodeURIComponent(query)}&count=${MAX_SEARCH_RESULTS}&locale=en_US&device_type=desktop`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const items = data?.data?.result?.items || [];
return items.slice(0, MAX_SEARCH_RESULTS).map((item: any) => ({
title: item.title || "",
url: item.url || "",
snippet: item.desc || "",
source: "Qwant"
}));
}
async function searchMojeek(query: string): Promise<SearchResult[]> {
const url = `https://www.mojeek.com/search?q=${encodeURIComponent(query)}&t=${MAX_SEARCH_RESULTS}&fmt=json`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const items = data?.response?.results || [];
return items.slice(0, MAX_SEARCH_RESULTS).map((item: any) => ({
title: item.title || "",
url: item.url || "",
snippet: item.desc || "",
source: "Mojeek"
}));
}
async function searchMetaGer(query: string): Promise<SearchResult[]> {
const url = `https://metager.org/meta.json?q=${encodeURIComponent(query)}&lang=all`;
const res = await fetchWithTimeout(url, { headers: HEADERS }, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const data = await res.json();
const results = data?.results || [];
return results.slice(0, MAX_SEARCH_RESULTS).map((item: any) => ({
title: item.title || "",
url: item.link || item.url || "",
snippet: item.description || "",
source: "MetaGer"
}));
}
async function searchBrave(query: string): Promise<SearchResult[]> {
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}&tf=&source=web`;
const res = await fetchWithTimeout(url, {
headers: { ...HEADERS, "Accept": "application/json, text/html" },
redirect: "follow"
}, FETCH_TIMEOUT_MS);
if (!res.ok) return [];
const html = await res.text();
if (html.length < 2000) return [];
const results: SearchResult[] = [];
const regex = /<a[^>]*class="result-header"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = regex.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
const title = extractTextFromHtml(m[2]).trim();
if (title.length < 3) continue;
results.push({ title, url: m[1], snippet: "", source: "Brave" });
}
if (results.length === 0) {
const fallback = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([^<]{10,})<\/a>/gi;
const seen = new Set<string>();
while ((m = fallback.exec(html)) !== null && results.length < MAX_SEARCH_RESULTS) {
if (m[1].includes("brave.") || seen.has(m[1])) continue;
seen.add(m[1]);
results.push({ title: extractTextFromHtml(m[2]).trim(), url: m[1], snippet: "", source: "Brave" });
}
}
return results;
}
function isRealResult(r: SearchResult): boolean {
return !r.url.includes("google.com/search") &&
!r.url.includes("duckduckgo.com/?q=") &&
!r.url.includes("bing.com/search") &&
!r.url.includes("yandex.ru/search") &&
!r.url.includes("yandex.com/search");
}
function isWeatherQuery(query: string): boolean {
const weatherWords = ["погод", "weather", "температур", "дожд", "снег", "прогноз погоды", "осадк"];
const lower = query.toLowerCase();
return weatherWords.some(w => lower.includes(w));
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<any[]> {
const tools = [];
tools.push(rawFunctionTool({
name: "internet_search",
description: `
Search the internet for information.
IMPORTANT: When user asks about something you don't know, or asks to "find", "search", or provides a URL - USE THIS TOOL.
Examples:
- "what is python" -> use search
- "find info about kodacode" -> use search
- User gives URL -> use load_webpage tool
Available parameters:
- query: the search query (string, required)
`,
parametersJsonSchema: {
type: "object",
properties: {
query: { type: "string", description: "What to search for" }
},
required: ["query"]
},
implementation: async (params: Record<string, unknown>) => {
const query = String(params.query || "").trim();
if (!query) return "Please provide a search query";
const errors: string[] = [];
const allResults: SearchResult[] = [];
const tasks: Promise<void>[] = [];
tasks.push(searchWikipedia(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Wiki: ${e?.message}`); }));
tasks.push(searchDuckDuckGoHtml(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`DDG HTML: ${e?.message}`); }));
tasks.push(searchDuckDuckGoLite(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`DDG Lite: ${e?.message}`); }));
tasks.push(searchDuckDuckGoSuggestions(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`DDG AC: ${e?.message}`); }));
tasks.push(searchBing(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Bing: ${e?.message}`); }));
tasks.push(searchYandex(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Yandex: ${e?.message}`); }));
tasks.push(searchGoogleSuggestions(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Google AC: ${e?.message}`); }));
if (isWeatherQuery(query)) {
tasks.push(searchWttrIn(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`wttr: ${e?.message}`); }));
tasks.push(searchOpenMeteo(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`OpenMeteo: ${e?.message}`); }));
tasks.push(searchMetNo(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`met.no: ${e?.message}`); }));
tasks.push(search7Timer(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`7timer: ${e?.message}`); }));
}
tasks.push(searchQwant(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Qwant: ${e?.message}`); }));
tasks.push(searchMojeek(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Mojeek: ${e?.message}`); }));
tasks.push(searchMetaGer(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`MetaGer: ${e?.message}`); }));
tasks.push(searchBrave(query).then(r => { allResults.push(...r); void 0; }).catch(e => { errors.push(`Brave: ${e?.message}`); }));
for (const instance of SEARXNG_INSTANCES) {
tasks.push(searchSearXNG(query, instance).then(r => { if (r.length > 0) allResults.push(...r); void 0; }).catch(() => {}));
}
await Promise.allSettled(tasks);
const results = deduplicateResults(allResults);
const realResults = results.filter(isRealResult);
const suggestionResults = results.filter(r => !isRealResult(r));
if (realResults.length === 0 && suggestionResults.length > 0) {
const topSuggestions = suggestionResults.slice(0, 3);
const fetched: { url: string; content: string }[] = [];
for (const s of topSuggestions) {
const content = await fetchPageContent(s.url);
if (content && content.length > 100) {
fetched.push({ url: s.url, content });
}
}
if (fetched.length > 0) {
let out = `Search results for "${query}":\n\n`;
fetched.forEach((f, i) => {
out += `--- Result ${i + 1} (${f.url}) ---\n${f.content.substring(0, 2000)}\n\n`;
});
return out;
}
}
if (results.length === 0) {
let msg = `No results found for: "${query}"`;
if (errors.length > 0) msg += `\n\nErrors: ${errors.join("; ")}`;
return msg;
}
let out = `Found ${results.length} result(s) for "${query}":\n\n`;
results.forEach((r, i) => {
out += `${i + 1}. ${r.title}`;
if (r.source) out += ` [${r.source}]`;
out += `\n URL: ${r.url}\n`;
if (r.snippet) out += ` ${r.snippet}\n`;
out += "\n";
});
return out;
},
}));
tools.push(rawFunctionTool({
name: "load_webpage",
description: `
Load and read a webpage.
IMPORTANT: When user provides a URL and wants to see its content - USE THIS TOOL.
Examples:
- User writes "https://example.com" -> use this tool
- User says "open this link" with URL -> use this tool
Available parameters:
- url: the URL to load (string, required)
`,
parametersJsonSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL address to fetch" }
},
required: ["url"]
},
implementation: async (params: Record<string, unknown>) => {
const rawUrl = String(params.url || "").trim();
if (!rawUrl) return "Please provide a URL";
const url = validateUrl(rawUrl);
try {
new URL(url);
} catch {
return `Invalid URL: ${rawUrl}`;
}
try {
const res = await fetchWithTimeout(url, {
headers: HEADERS,
redirect: "follow"
}, FETCH_TIMEOUT_MS);
if (!res.ok) {
return `Failed to load page: HTTP ${res.status}`;
}
const contentType = res.headers.get("content-type") || "";
if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml")) {
return `Unsupported content type: ${contentType}`;
}
const html = await res.text();
const text = extractTextFromHtml(html);
if (text.length > MAX_FETCH_CHARS) {
return text.substring(0, MAX_FETCH_CHARS) + "\n\n[Content truncated, showing first " + MAX_FETCH_CHARS + " characters]";
}
return text || "Page appears to be empty";
} catch (e: any) {
if (e?.name === "AbortError") {
return "Page load timed out";
}
return "Error loading page: " + (e?.message || String(e));
}
},
}));
return tools;
}