AkurAI Build
Menu

popagent

public

Latest 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),
});