Adds searxng support for tool calling

This commit is contained in:
2026-05-02 18:14:41 -07:00
parent 2125c5dfa4
commit d7967eaa75
7 changed files with 239 additions and 4 deletions

View File

@@ -1,7 +1,9 @@
import { convert as htmlToText } from "html-to-text";
import type OpenAI from "openai";
import { z } from "zod";
import { env } from "../env.js";
import { exaClient } from "../search/exa.js";
import { searchSearxng } from "../search/searxng.js";
import type { ChatMessage } from "./types.js";
const MAX_TOOL_ROUNDS = 4;
@@ -21,6 +23,8 @@ const WebSearchArgsSchema = z
})
.strict();
type WebSearchArgs = z.infer<typeof WebSearchArgsSchema>;
const FetchUrlArgsSchema = z
.object({
url: z.string().trim().url(),
@@ -267,8 +271,7 @@ function normalizeIncomingMessages(messages: ChatMessage[]) {
return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, ...normalized];
}
async function runWebSearchTool(input: unknown): Promise<ToolRunOutcome> {
const args = WebSearchArgsSchema.parse(input);
async function runExaWebSearchTool(args: WebSearchArgs): Promise<ToolRunOutcome> {
const exa = exaClient();
const response = await exa.search(args.query, {
type: args.type ?? "auto",
@@ -292,6 +295,7 @@ async function runWebSearchTool(input: unknown): Promise<ToolRunOutcome> {
const results = Array.isArray(response?.results) ? response.results : [];
return {
ok: true,
searchEngine: "exa",
query: args.query,
requestId: response?.requestId ?? null,
results: results.map((result: any, index: number) => ({
@@ -309,6 +313,40 @@ async function runWebSearchTool(input: unknown): Promise<ToolRunOutcome> {
};
}
async function runSearxngWebSearchTool(args: WebSearchArgs): Promise<ToolRunOutcome> {
const response = await searchSearxng(args.query, {
numResults: args.numResults ?? DEFAULT_WEB_RESULTS,
includeDomains: args.includeDomains,
excludeDomains: args.excludeDomains,
});
return {
ok: true,
searchEngine: "searxng",
query: args.query,
requestId: response.requestId,
results: response.results.map((result, index) => ({
rank: index + 1,
title: result.title,
url: result.url,
publishedDate: result.publishedDate,
author: null,
summary: result.summary,
text: result.text,
highlights: result.summary ? [clipText(result.summary, 280)] : [],
engines: result.engines,
})),
};
}
async function runWebSearchTool(input: unknown): Promise<ToolRunOutcome> {
const args = WebSearchArgsSchema.parse(input);
if (env.CHAT_WEB_SEARCH_ENGINE === "searxng") {
return runSearxngWebSearchTool(args);
}
return runExaWebSearchTool(args);
}
function assertSafeFetchUrl(urlRaw: string) {
const parsed = new URL(urlRaw);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {