Forked from zexigh/web-search
src / toolsProvider.ts
import { text, tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics, globalConfigSchematics } from "./configSchematics";
import { duckduckgoSearch, DuckDuckGoError } from "./duckduckgoClient";
import { searxngSearch, SearxngError } from "./searxngClient";
export async function toolsProvider(ctl: ToolsProviderController) {
const global = ctl.getGlobalPluginConfig(globalConfigSchematics);
const config = ctl.getPluginConfig(configSchematics);
const searchWeb = tool({
name: "search_web",
description: text`
Search the web and return ranked results (title, URL, snippet).
Use this for any question whose answer depends on up-to-date public information
(news, current events, recent releases, prices, sports scores, biographies of
living people, software documentation that may have changed). Quote or summarise
the snippets and cite the URLs; do not pretend to have read the full pages.
`,
parameters: {
query: z.string().min(1).describe("The search query, in natural language."),
max_results: z.number().int().min(1).max(20).optional().describe(
"Override the default maximum number of results returned.",
),
},
implementation: async ({ query, max_results }, ctx) => {
const backend = config.get("backend");
const limit = max_results ?? config.get("defaultMaxResults");
const timeoutMs = global.get("requestTimeoutMs");
const t0 = Date.now();
ctx.status(`Searching "${query}" via ${backend}…`);
if (backend === "duckduckgo") {
try {
ctx.status("Querying DuckDuckGo…");
const results = await duckduckgoSearch(query, limit, timeoutMs, ctx.signal);
ctx.status(`${results.length} result(s) in ${Date.now() - t0} ms.`);
return { query, backend, results };
} catch (e: unknown) {
if (ctx.signal.aborted) return { error: "Search cancelled." };
if (e instanceof DuckDuckGoError) return { error: e.message };
return { error: `Unexpected error: ${e instanceof Error ? e.message : String(e)}` };
}
}
const instanceUrl = global.get("searxngInstanceUrl");
const language = config.get("searxngLanguage");
const safesearch = config.get("searxngSafesearch") as "0" | "1" | "2";
ctx.status(`Querying ${instanceUrl || "SearXNG"}…`);
try {
const data = await searxngSearch(
instanceUrl,
{ query, language, safesearch },
timeoutMs,
ctx.signal,
);
if (data.unresponsive_engines?.length) {
ctx.warn(`Unresponsive engines: ${data.unresponsive_engines.join(", ")}`);
}
const results = data.results.slice(0, limit);
ctx.status(`${results.length} result(s) in ${Date.now() - t0} ms.`);
return {
query: data.query,
backend,
results,
answers: data.answers,
infoboxes: data.infoboxes,
suggestions: data.suggestions,
unresponsive_engines: data.unresponsive_engines,
};
} catch (e: unknown) {
if (ctx.signal.aborted) return { error: "Search cancelled." };
if (e instanceof SearxngError) return { error: e.message };
return { error: `Unexpected error: ${e instanceof Error ? e.message : String(e)}` };
}
},
});
return [searchWeb];
}