/** * Factory for a configured SearXNG metasearch tool. * * @module @nhtio/adk/batteries/tools/searxng * * @remarks * Unlike the other bundled tool categories — every one of which exports a ready-made, * stateless `Tool` constant — the SearXNG battery exports **factories**, * {@link createSearxngSearchTool} (async) and {@link createSearxngSearchToolSync}. A search tool * has to talk to a *specific* SearXNG instance, usually behind custom authentication, so it needs * per-deployment configuration (a base URL and headers) that cannot be baked in at module load. * * Because this module exports factories rather than `Tool` instances, they MUST NOT be * bulk-registered via `Object.values(batteries)`. Call a factory first, then register the * returned tool: `new ToolRegistry([await createSearxngSearchTool({ instanceUrl })])`. * * @see https://docs.searxng.org/dev/search_api.html */ import { Tool } from "../../../forge"; import { type ToolGateFn, type ArtifactResolver, type SyncArtifactResolver } from "../_shared/index"; import type { NextFn } from '@nhtio/middleware'; export { E_INVALID_SEARXNG_CONFIG } from "./exceptions"; /** A static set of request headers (used for custom authentication). */ export type SearxngHeaders = Record; /** * A resolver returning request headers, sync or async. Use this form when the auth token is * refreshable — the resolver runs on every search, so a fresh token can be minted per call. */ export type SearxngHeadersResolver = () => SearxngHeaders | Promise; /** The output shape the tool serialises. `either` lets the model pick per call. */ export type SearxngResultFormat = 'normalized' | 'raw' | 'either'; /** * A single normalised SearXNG result. SearXNG result items are deliberately untyped upstream, * so every field except a best-effort `title`/`url` is optional. */ export interface SearxngResult { /** Result title, when the source engine provided one. */ title?: string; /** Result URL, when the source engine provided one. */ url?: string; /** Snippet / summary text for the result. */ content?: string; /** The SearXNG engine that produced this result (e.g. `google`, `duckduckgo`). */ engine?: string; /** Relevance score as reported by SearXNG (higher is more relevant). */ score?: number; /** Publication date, when the source engine exposed one (ISO-ish string, engine-dependent). */ publishedDate?: string; } /** * Mutable context handed to each input-pipeline stage **before** the HTTP request is sent. * * @remarks * Stages mutate this in place (onion `(ctx, next)` style) to adjust the outgoing request — * inject or rotate auth headers, force a language, rewrite the query — or call * {@link SearxngRequestContext.shortCircuit} to skip the fetch entirely (e.g. a cache hit). */ export interface SearxngRequestContext { /** The tool's name (read-only). */ readonly toolName: string; /** The search query. Mutable. */ query: string; /** Extra SearXNG query parameters (`categories`, `engines`, `language`, …). Mutable. */ params: Record; /** Resolved request headers. Mutable — inject, redact, or rotate auth here. */ headers: SearxngHeaders; /** The target instance base URL (read-only). */ readonly instanceUrl: string; /** Cross-stage scratch space; also carried onto the response context. */ readonly stash: Map; /** Skip the fetch and return `result` verbatim as the tool's output. */ shortCircuit(result: string): void; } /** * Mutable context handed to each output-pipeline stage **after** the response JSON is parsed. * * @remarks * Stages reshape, redact, enrich, or re-rank {@link SearxngResponseContext.results}, mutate the * raw body, or set {@link SearxngResponseContext.output} to override the serialised string * verbatim (e.g. to render markdown that matches a markdown `artifact` resolver). */ export interface SearxngResponseContext { /** The tool's name (read-only). */ readonly toolName: string; /** The request context as it was sent (post-input-pipeline). */ readonly request: SearxngRequestContext; /** The parsed SearXNG JSON body. Mutable (used when `format` is `raw`). */ raw: unknown; /** The normalised result list. Mutable — filter, redact, or re-rank. */ results: SearxngResult[]; /** The effective payload shape for this call. */ format: 'normalized' | 'raw'; /** When set, used verbatim as the tool's output (overrides serialisation). */ output?: string; /** Cross-stage scratch space; carried over from the request context. */ readonly stash: Map; } /** An input-pipeline stage. Onion middleware over {@link SearxngRequestContext}. */ export type SearxngInputMiddlewareFn = (ctx: SearxngRequestContext, next: NextFn) => void | Promise; /** An output-pipeline stage. Onion middleware over {@link SearxngResponseContext}. */ export type SearxngOutputMiddlewareFn = (ctx: SearxngResponseContext, next: NextFn) => void | Promise; /** * Configuration for {@link createSearxngSearchTool} (async) and * {@link createSearxngSearchToolSync} (sync — `artifact` narrowed to the sync subset). * * @typeParam A - The {@link ArtifactResolver} variant accepted: the full resolver (async factory) * or the sync subset ({@link createSearxngSearchToolSync}). */ export interface SearxngToolConfig { /** Base URL of the SearXNG instance, e.g. `https://searx.example.org`. Required. */ instanceUrl: string; /** Custom request headers — a static object or a (sync/async) resolver for refreshable auth. */ headers?: SearxngHeaders | SearxngHeadersResolver; /** Request timeout in milliseconds. Default `10_000`. */ timeout?: number; /** * Output shape. `normalized`/`raw` pin the shape (the model cannot change it); `either` * (default) exposes a `format` argument so the model chooses per call. */ resultFormat?: SearxngResultFormat; /** Tool name. Default `searxng_search`. */ name?: string; /** Tool description override. */ description?: string; /** * Spool-artifact resolver for the tool's output. Default `() => SpooledJsonArtifact`. Accepts a * constructor, a sync resolver, or — via {@link createSearxngSearchTool} — an async / * dynamic-import resolver. Pass `() => SpooledMarkdownArtifact` (paired with an output stage that * renders markdown into `ctx.output`) or `() => SpooledArtifact` for plain text. */ artifact?: A; /** * Optional per-call gate run before the HTTP request — the seam for human-approval/RBAC * flows built on `ctx.waitFor` (the ADK gates primitive). Throwing aborts the call through * the standard tool-error path. Search queries reach the network on the agent's behalf, * which makes every call a candidate for gating. */ gate?: ToolGateFn; /** Stages run before the HTTP request. See {@link SearxngRequestContext}. */ inputPipeline?: SearxngInputMiddlewareFn[]; /** Stages run after the response is parsed. See {@link SearxngResponseContext}. */ outputPipeline?: SearxngOutputMiddlewareFn[]; } /** * Create a configured SearXNG search {@link Tool} (async — accepts a dynamic-import `artifact`). * * @remarks * Async because `artifact` may be an async / dynamic-import resolver, which must be resolved to the * sync `() => Ctor` that `Tool.artifactConstructor` requires before the tool is built (the * wrap-site invokes it synchronously). For the common case where you reference the artifact class * directly, use {@link createSearxngSearchToolSync} and skip the `await`. * * The handler always requests `format=json`. Note that SearXNG ships with JSON output * **disabled** by default (it is abused by bots); an instance that has not enabled * `search.formats: [json]` in its `settings.yml` answers with HTTP 403, which the tool returns * as a graceful `Error:` string naming the setting. * * @warning * Do not trust the `number_of_results` field for a result count — SearXNG frequently reports `0` * in JSON output even when `results` is non-empty. This is a long-standing upstream quirk, not a * tool defect (see {@link https://github.com/searxng/searxng/issues/2987 | searxng#2987} and * {@link https://github.com/searxng/searxng/issues/2457 | searxng#2457}). The tool passes the * field through verbatim; use `results.length` as the authoritative count. * * @param config - The instance URL, optional custom headers, output-format policy, `artifact` * resolver, and input/output middleware pipelines. See {@link SearxngToolConfig}. * @returns A promise of a `Tool` ready to register in a `ToolRegistry`. * @throws {@link E_INVALID_SEARXNG_CONFIG} when `instanceUrl` or `artifact` is invalid. */ export declare const createSearxngSearchTool: (config: SearxngToolConfig) => Promise; /** * Synchronous {@link createSearxngSearchTool} — the ergonomic common path. * * @remarks * `artifact` is narrowed to the sync subset (a constructor or a sync resolver). Passing an async * resolver is a compile-time type error and a runtime {@link E_INVALID_SEARXNG_CONFIG}; for * dynamic-import resolvers use the async {@link createSearxngSearchTool}. See its docs for the * `number_of_results` caveat and 403/JSON-disabled behaviour. * * @param config - Same as {@link SearxngToolConfig}, with `artifact` restricted to the sync subset. * @returns A `Tool` ready to register in a `ToolRegistry`. * @throws {@link E_INVALID_SEARXNG_CONFIG} when `instanceUrl` or `artifact` is invalid (incl. an async resolver). */ export declare const createSearxngSearchToolSync: (config: SearxngToolConfig) => Tool;