Skip to content

Commit 4aae01c

Browse files
committed
feat: Enhance MetaSearchAgent with optional search functions and improve error handling in search handlers
1 parent fb86bdf commit 4aae01c

4 files changed

Lines changed: 96 additions & 61 deletions

File tree

apps/qwksearch-web/components/Settings/Sections/Models/TestModelsButton.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,29 +131,29 @@ export default function TestModelsButton({
131131
Total Tested
132132
</p>
133133
<p className="text-2xl font-semibold text-black dark:text-white">
134-
{results.totalModels}
134+
{results.totalModels ?? 0}
135135
</p>
136136
</div>
137137
<div className="p-4 rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-950/30">
138138
<p className="text-xs text-green-700 dark:text-green-400 mb-1">
139139
Available
140140
</p>
141141
<p className="text-2xl font-semibold text-green-700 dark:text-green-400">
142-
{results.availableModels.length}
142+
{results.availableModels?.length ?? 0}
143143
</p>
144144
</div>
145145
<div className="p-4 rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/30">
146146
<p className="text-xs text-red-700 dark:text-red-400 mb-1">
147147
Unavailable
148148
</p>
149149
<p className="text-2xl font-semibold text-red-700 dark:text-red-400">
150-
{results.unavailableModels.length}
150+
{results.unavailableModels?.length ?? 0}
151151
</p>
152152
</div>
153153
</div>
154154

155155
{/* Available Models */}
156-
{results.availableModels.length > 0 && (
156+
{results.availableModels && results.availableModels.length > 0 && (
157157
<div>
158158
<h3 className="text-sm font-medium mb-2 text-black dark:text-white flex items-center gap-2">
159159
<Check size={16} className="text-green-600 dark:text-green-400" />
@@ -192,7 +192,7 @@ export default function TestModelsButton({
192192
)}
193193

194194
{/* Unavailable Models */}
195-
{results.unavailableModels.length > 0 && (
195+
{results.unavailableModels && results.unavailableModels.length > 0 && (
196196
<div>
197197
<h3 className="text-sm font-medium mb-2 text-black dark:text-white flex items-center gap-2">
198198
<X size={16} className="text-red-600 dark:text-red-400" />

packages/agent-toolkit/src/tools/search/meta-search-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ export interface Config {
4444
queryGeneratorFewShots: FewShotExample[];
4545
responsePrompt: string;
4646
activeEngines: string[];
47+
/** Optional: Function to fetch documents from URLs */
48+
getDocumentsFromLinks?: (options: { links: string[] }) => Promise<any[]>;
49+
/** Optional: Function to search via SearXNG */
50+
searchSearxng?: (query: string, options: any) => Promise<{ results: any[]; suggestions: string[] }>;
51+
/** Optional: Function to search via Tavily */
52+
searchTavily?: (query: string, options: any) => Promise<{ results: any[]; suggestions: string[] }>;
53+
/** Optional: Function to check if Tavily is configured */
54+
isTavilyConfigured?: () => boolean;
55+
/** Optional: Function to scrape a URL */
56+
scrapeURL?: (url: string, options: any) => Promise<string>;
4757
}
4858

4959
export type BasicChainInput = {

packages/agent-toolkit/src/tools/search/metaSearchAgent.ts

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
*/
66
import { generateText, streamText, type LanguageModel } from "ai";
77
import { LineOutputParser, LineListOutputParser } from "../../utils/outputParser";
8-
import { getDocumentsFromLinks } from "extract-webpage/utils/documents";
98
import type { Document } from "./document";
10-
import { searchSearxng } from "extract-webpage/search/public-searxng";
11-
import { searchTavily, isTavilyConfigured } from "extract-webpage/search/tavily";
12-
import { scrapeURL } from "extract-webpage/search/url-to-html";
139

1410
/** Strip HTML tags and decode entities — works in Cloudflare edge runtime */
1511
function htmlToText(html: string): string {
@@ -26,7 +22,6 @@ function htmlToText(html: string): string {
2622
.replace(/\s+/g, ' ')
2723
.trim();
2824
}
29-
import { getSourceScrapeTimeout } from "../../config/serverRegistry";
3025
import { formatChatHistoryAsString } from "../../utils";
3126
import EventEmitter from "events";
3227
import type {
@@ -127,10 +122,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
127122
question = "latest information";
128123
}
129124

130-
if (links.length > 0) {
125+
if (links.length > 0 && this.config.getDocumentsFromLinks) {
131126
if (question.length === 0) question = "summarize";
132127

133-
const linkDocs = await getDocumentsFromLinks({ links });
128+
const linkDocs = await this.config.getDocumentsFromLinks({ links });
134129
const docs = await groupAndSummarizeDocs(llm, linkDocs, question);
135130

136131
return { query: question, docs };
@@ -174,53 +169,60 @@ class MetaSearchAgent implements MetaSearchAgentType {
174169

175170
let res: { results: any[]; suggestions: string[] };
176171

177-
const runSearxng = () => searchSearxng(question, {
178-
language: "en",
179-
engines: this.config.activeEngines,
180-
categories: [category],
181-
});
182-
183-
if (
184-
isTavilyConfigured() &&
185-
this.config.activeEngines.length === 0 &&
186-
category === "general"
187-
) {
188-
try {
189-
res = await searchTavily(question, { searchDepth: "basic", maxResults: 10 });
190-
} catch (error) {
191-
console.error("Tavily search failed, falling back to SearXNG:", error);
192-
res = await runSearxng();
193-
}
172+
// If no search functions provided, return empty results
173+
if (!this.config.searchSearxng && !this.config.searchTavily) {
174+
res = { results: [], suggestions: [] };
194175
} else {
195-
if (isTavilyConfigured()) {
176+
const runSearxng = () => this.config.searchSearxng!(question, {
177+
language: "en",
178+
engines: this.config.activeEngines,
179+
categories: [category],
180+
});
181+
182+
const isTavilyConfigured = this.config.isTavilyConfigured?.() ?? false;
183+
184+
if (
185+
isTavilyConfigured &&
186+
this.config.activeEngines.length === 0 &&
187+
category === "general"
188+
) {
196189
try {
197-
res = await Promise.race([
198-
runSearxng(),
199-
new Promise<{ results: any[]; suggestions: string[] }>((_, reject) =>
200-
setTimeout(() => reject(new Error("Timeout")), 10000)
201-
)
202-
]);
203-
} catch (err: any) {
204-
if (err.message === "Timeout") {
205-
console.warn("[MetaSearchAgent] SearXNG search did not respond in 10 seconds, falling back to Tavily.");
206-
try {
207-
res = await searchTavily(question, { searchDepth: "basic", maxResults: 10 });
208-
} catch (tavilyErr) {
209-
console.error("[MetaSearchAgent] Tavily fallback also failed, awaiting SearXNG directly:", tavilyErr);
210-
res = await runSearxng();
211-
}
212-
} else {
213-
console.error("[MetaSearchAgent] SearXNG search failed, falling back to Tavily:", err);
214-
try {
215-
res = await searchTavily(question, { searchDepth: "basic", maxResults: 10 });
216-
} catch (tavilyErr) {
217-
console.error("[MetaSearchAgent] Tavily fallback also failed:", tavilyErr);
218-
throw err;
190+
res = await this.config.searchTavily!(question, { searchDepth: "basic", maxResults: 10 });
191+
} catch (error) {
192+
console.error("Tavily search failed, falling back to SearXNG:", error);
193+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
194+
}
195+
} else {
196+
if (isTavilyConfigured && this.config.searchTavily) {
197+
try {
198+
res = await Promise.race([
199+
runSearxng(),
200+
new Promise<{ results: any[]; suggestions: string[] }>((_, reject) =>
201+
setTimeout(() => reject(new Error("Timeout")), 10000)
202+
)
203+
]);
204+
} catch (err: any) {
205+
if (err.message === "Timeout") {
206+
console.warn("[MetaSearchAgent] SearXNG search did not respond in 10 seconds, falling back to Tavily.");
207+
try {
208+
res = await this.config.searchTavily(question, { searchDepth: "basic", maxResults: 10 });
209+
} catch (tavilyErr) {
210+
console.error("[MetaSearchAgent] Tavily fallback also failed, awaiting SearXNG directly:", tavilyErr);
211+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
212+
}
213+
} else {
214+
console.error("[MetaSearchAgent] SearXNG search failed, falling back to Tavily:", err);
215+
try {
216+
res = await this.config.searchTavily(question, { searchDepth: "basic", maxResults: 10 });
217+
} catch (tavilyErr) {
218+
console.error("[MetaSearchAgent] Tavily fallback also failed:", tavilyErr);
219+
throw err;
220+
}
219221
}
220222
}
223+
} else {
224+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
221225
}
222-
} else {
223-
res = await runSearxng();
224226
}
225227
}
226228

@@ -253,7 +255,8 @@ class MetaSearchAgent implements MetaSearchAgentType {
253255
perSourceTimeout = Math.max(2, Math.floor(thinkingTimeLimit / scrapeCount));
254256
} else if (sourceExtractionEnabled) {
255257
scrapeCount = 3;
256-
perSourceTimeout = Math.max(1, getSourceScrapeTimeout());
258+
// Default to 5 seconds if no config function available
259+
perSourceTimeout = 5;
257260
} else {
258261
scrapeCount = 0;
259262
perSourceTimeout = 0;
@@ -263,12 +266,12 @@ class MetaSearchAgent implements MetaSearchAgentType {
263266
const docsToScrape = documents.slice(0, scrapeCount);
264267
emitSearching("running", `Extracting top ${docsToScrape.length} sources`, "extract");
265268

266-
const extractionTasks = docsToScrape.map(async (doc, idx) => {
269+
const extractionTasks = this.config.scrapeURL ? docsToScrape.map(async (doc, idx) => {
267270
const url = doc.metadata?.url;
268-
if (!url) return;
271+
if (!url || !this.config.scrapeURL) return;
269272
try {
270273
const result = await waitWithTimeout(
271-
scrapeURL(url, { timeout: perSourceTimeout }),
274+
this.config.scrapeURL(url, { timeout: perSourceTimeout }),
272275
perSourceTimeout * 1000 + 1500,
273276
);
274277
if (typeof result === "string" && result.length > 100) {
@@ -284,7 +287,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
284287
} catch {
285288
// Keep original snippet on scraping failure or timeout
286289
}
287-
});
290+
}) : [];
288291

289292
await Promise.allSettled(extractionTasks);
290293
emitSearching("done", `Extracting top ${docsToScrape.length} sources`, "extract");

packages/agent-toolkit/src/tools/search/search-handlers.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
/**
22
* @module research/search/index
33
* @description Research library module.
4+
*
5+
* Note: To use these search handlers, you need to provide search functions
6+
* (searchSearxng, searchTavily, etc.) from extract-webpage package.
7+
* See extract-webpage/src/search/index.ts for an example.
48
*/
59
import MetaSearchAgent from "./metaSearchAgent";
610
import prompts from "../../language-generation/prompts/search-prompts";
11+
import type { Config } from "./meta-search-types";
712

8-
export const searchHandlers: Record<string, MetaSearchAgent> = {
13+
/**
14+
* Creates search handler instances with provided search functions.
15+
* Pass in search functions from extract-webpage to enable web search.
16+
*/
17+
export const createSearchHandlers = (searchFunctions?: Partial<Config>) => ({
918
webSearch: new MetaSearchAgent({
1019
activeEngines: [],
1120
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
@@ -14,6 +23,7 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
1423
rerank: true,
1524
rerankThreshold: 0.3,
1625
searchWeb: true,
26+
...searchFunctions,
1727
}),
1828
academicSearch: new MetaSearchAgent({
1929
activeEngines: ["arxiv", "google scholar", "pubmed"],
@@ -23,6 +33,7 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
2333
rerank: true,
2434
rerankThreshold: 0,
2535
searchWeb: true,
36+
...searchFunctions,
2637
}),
2738
writingAssistant: new MetaSearchAgent({
2839
activeEngines: [],
@@ -32,6 +43,7 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
3243
rerank: true,
3344
rerankThreshold: 0,
3445
searchWeb: true,
46+
...searchFunctions,
3547
}),
3648
wolframAlphaSearch: new MetaSearchAgent({
3749
activeEngines: ["wolframalpha"],
@@ -41,6 +53,7 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
4153
rerank: false,
4254
rerankThreshold: 0,
4355
searchWeb: true,
56+
...searchFunctions,
4457
}),
4558
youtubeSearch: new MetaSearchAgent({
4659
activeEngines: ["youtube"],
@@ -50,6 +63,7 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
5063
rerank: true,
5164
rerankThreshold: 0.3,
5265
searchWeb: true,
66+
...searchFunctions,
5367
}),
5468
redditSearch: new MetaSearchAgent({
5569
activeEngines: ["reddit"],
@@ -59,5 +73,13 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
5973
rerank: true,
6074
rerankThreshold: 0.3,
6175
searchWeb: true,
76+
...searchFunctions,
6277
}),
63-
};
78+
});
79+
80+
/**
81+
* Default search handlers without search functions.
82+
* These will not perform actual web searches unless search functions are added to the Config.
83+
* @deprecated Use createSearchHandlers() with search functions instead
84+
*/
85+
export const searchHandlers = createSearchHandlers();

0 commit comments

Comments
 (0)