Menu
popagent
publicLatest change 6edc7e928c57dab23c0973ed3f7ee70b3ebb72d5 - Initial popagent baseline by AkurAI Build
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
const DEFAULT_SEARXNG_URL = "http://127.0.0.1:8889";
const searchResultSchema = z.object({
title: z.string(),
url: z.string(),
content: z.string(),
engine: z.string().optional(),
});
const webSearchOutputSchema = z.object({
query: z.string(),
results: z.array(searchResultSchema),
});
type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;
export async function searchWeb(
query: string,
limit = 8,
baseUrl = process.env.SEARXNG_URL ?? DEFAULT_SEARXNG_URL,
): Promise<WebSearchOutput> {
const url = new URL("/search", baseUrl);
url.searchParams.set("q", query);
url.searchParams.set("format", "json");
const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
if (!response.ok) throw new Error(`SearXNG search failed: ${response.status}`);
const body = (await response.json()) as {
results?: { title?: unknown; url?: unknown; content?: unknown; engine?: unknown }[];
};
const results = (body.results ?? []).flatMap((result) => {
if (typeof result.title !== "string" || typeof result.url !== "string") return [];
return [{
title: result.title,
url: result.url,
content: typeof result.content === "string" ? result.content : "",
engine: typeof result.engine === "string" ? result.engine : undefined,
}];
}).slice(0, limit);
return { query, results };
}
export const webSearch = createTool({
id: "web-search",
description: "Search the public web through the local SearXNG metasearch service. Returns titles, URLs, snippets, and source engines.",
inputSchema: z.object({
query: z.string().min(1).describe("Search query"),
limit: z.number().int().min(1).max(20).default(8).describe("Maximum results"),
}),
outputSchema: webSearchOutputSchema,
execute: async ({ query, limit }) => searchWeb(query, limit),
});