chat-agent-toolkit 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/package.json +101 -0
- package/src/config/config-manager.ts +319 -0
- package/src/config/config-types.ts +65 -0
- package/src/config/environment-variables.ts +8 -0
- package/src/config/index.ts +26 -0
- package/src/config/language-models-database.ts +1426 -0
- package/src/config/mcp-server-registry.ts +24 -0
- package/src/config/model-registry.ts +256 -0
- package/src/config/model-tester.ts +211 -0
- package/src/config/model-utils.ts +193 -0
- package/src/config/provider-ui-config.ts +196 -0
- package/src/connectors/composio.json +154 -0
- package/src/index.ts +22 -0
- package/src/memory/ARCHITECTURE.md +302 -0
- package/src/memory/MASTRA_INTEGRATION.md +106 -0
- package/src/memory/README.md +224 -0
- package/src/memory/agent-memory-manager.ts +416 -0
- package/src/memory/example.ts +343 -0
- package/src/memory/index.ts +47 -0
- package/src/memory/mastra-integration.ts +609 -0
- package/src/memory/storage/drizzle-storage.ts +205 -0
- package/src/memory/storage/in-memory-storage.ts +551 -0
- package/src/memory/storage/storage-interface.ts +68 -0
- package/src/memory/types.ts +125 -0
- package/src/models/providers.ts +5 -0
- package/src/models/registry.ts +4 -0
- package/src/models/types.ts +4 -0
- package/src/search.ts +5 -0
- package/src/tools/composio-mastra.ts +305 -0
- package/src/tools/composio-mcp.ts +205 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/qwksearch-api-tools.ts +327 -0
- package/src/tools/search/doc-utils.ts +75 -0
- package/src/tools/search/document.ts +47 -0
- package/src/tools/search/index.ts +13 -0
- package/src/tools/search/link-summarizer.ts +112 -0
- package/src/tools/search/meta-search-types.ts +62 -0
- package/src/tools/search/metaSearchAgent.ts +454 -0
- package/src/tools/search/search-handlers.ts +85 -0
- package/src/tools/search/suggestionGeneratorAgent.ts +54 -0
- package/src/types.d.ts +137 -0
- package/src/utils/README.md +98 -0
- package/src/utils/chat-helpers.ts +18 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown-to-html.ts +53 -0
- package/src/utils/outputParser.ts +73 -0
- package/src/utils/provider-image-cropper.ts +130 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrator for complex research queries.
|
|
3
|
+
* Manages query expansion, web search execution, and result synthesis using
|
|
4
|
+
* LLMs through the Vercel AI SDK (generateText/streamText).
|
|
5
|
+
*/
|
|
6
|
+
import { generateText, streamText, type LanguageModel } from "ai";
|
|
7
|
+
import { LineOutputParser, LineListOutputParser } from "../../utils/outputParser";
|
|
8
|
+
import type { Document } from "./document";
|
|
9
|
+
|
|
10
|
+
/** Strip HTML tags and decode entities — works in Cloudflare edge runtime */
|
|
11
|
+
function htmlToText(html: string): string {
|
|
12
|
+
return html
|
|
13
|
+
.replace(/<(script|style)[^>]*>[\s\S]*?<\/(script|style)>/gi, ' ')
|
|
14
|
+
.replace(/<[^>]+>/g, ' ')
|
|
15
|
+
.replace(/ /g, ' ')
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/'/g, "'")
|
|
21
|
+
.replace(/&[a-z#][a-z0-9]+;/gi, ' ')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
import { formatChatHistoryAsString } from "../../utils";
|
|
26
|
+
import EventEmitter from "events";
|
|
27
|
+
import type {
|
|
28
|
+
Config,
|
|
29
|
+
ChatTurnMessage,
|
|
30
|
+
MetaSearchAgentType,
|
|
31
|
+
SearchingEvent,
|
|
32
|
+
} from "./meta-search-types";
|
|
33
|
+
import {
|
|
34
|
+
buildFallbackDocs,
|
|
35
|
+
rerankDocs,
|
|
36
|
+
processDocs,
|
|
37
|
+
normalizeSourcesOutput,
|
|
38
|
+
} from "./doc-utils";
|
|
39
|
+
import { groupAndSummarizeDocs } from "./link-summarizer";
|
|
40
|
+
|
|
41
|
+
export type { MetaSearchAgentType, Config } from "./meta-search-types";
|
|
42
|
+
|
|
43
|
+
const waitWithTimeout = async <T>(
|
|
44
|
+
promise: Promise<T>,
|
|
45
|
+
timeoutMs: number,
|
|
46
|
+
): Promise<T | undefined> => {
|
|
47
|
+
return Promise.race([
|
|
48
|
+
promise,
|
|
49
|
+
new Promise<undefined>((resolve) => {
|
|
50
|
+
setTimeout(() => resolve(undefined), timeoutMs);
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Substitutes `{key}` placeholders in a prompt template with the provided
|
|
57
|
+
* values, leaving unknown placeholders untouched.
|
|
58
|
+
*/
|
|
59
|
+
const interpolatePrompt = (
|
|
60
|
+
template: string,
|
|
61
|
+
vars: Record<string, string>,
|
|
62
|
+
): string => {
|
|
63
|
+
return Object.entries(vars).reduce(
|
|
64
|
+
(result, [key, value]) => result.split(`{${key}}`).join(value),
|
|
65
|
+
template,
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
class MetaSearchAgent implements MetaSearchAgentType {
|
|
70
|
+
private config: Config;
|
|
71
|
+
|
|
72
|
+
constructor(config: Config) {
|
|
73
|
+
this.config = config;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rephrases the user's query into a standalone search question (and any
|
|
78
|
+
* URLs to summarize), runs the web search, and returns the documents to
|
|
79
|
+
* use as answer context.
|
|
80
|
+
*/
|
|
81
|
+
private async retrieveSearchDocs(
|
|
82
|
+
llm: LanguageModel,
|
|
83
|
+
chatHistory: string,
|
|
84
|
+
query: string,
|
|
85
|
+
category: string = "general",
|
|
86
|
+
sourceExtractionEnabled = false,
|
|
87
|
+
thinkingTimeLimit = 0,
|
|
88
|
+
emitter?: EventEmitter,
|
|
89
|
+
): Promise<{ query: string; docs: Document[] }> {
|
|
90
|
+
const { text: retrieverOutput } = await generateText({
|
|
91
|
+
model: llm,
|
|
92
|
+
temperature: 0,
|
|
93
|
+
system: this.config.queryGeneratorPrompt,
|
|
94
|
+
messages: [
|
|
95
|
+
...this.config.queryGeneratorFewShots.map(([role, content]) => ({
|
|
96
|
+
role,
|
|
97
|
+
content,
|
|
98
|
+
})),
|
|
99
|
+
{
|
|
100
|
+
role: "user",
|
|
101
|
+
content: `
|
|
102
|
+
<conversation>
|
|
103
|
+
${chatHistory}
|
|
104
|
+
</conversation>
|
|
105
|
+
|
|
106
|
+
<query>
|
|
107
|
+
${query}
|
|
108
|
+
</query>
|
|
109
|
+
`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const linksOutputParser = new LineListOutputParser({ key: "links" });
|
|
115
|
+
const questionOutputParser = new LineOutputParser({ key: "question" });
|
|
116
|
+
|
|
117
|
+
const links = await linksOutputParser.parse(retrieverOutput);
|
|
118
|
+
let question =
|
|
119
|
+
(await questionOutputParser.parse(retrieverOutput)) ?? retrieverOutput;
|
|
120
|
+
|
|
121
|
+
if (question === "not_needed") {
|
|
122
|
+
question = "latest information";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (links.length > 0 && this.config.getDocumentsFromLinks) {
|
|
126
|
+
if (question.length === 0) question = "summarize";
|
|
127
|
+
|
|
128
|
+
const linkDocs = await this.config.getDocumentsFromLinks({ links });
|
|
129
|
+
const docs = await groupAndSummarizeDocs(llm, linkDocs, question);
|
|
130
|
+
|
|
131
|
+
return { query: question, docs };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
question = question.replace(/<think>.*?<\/think>/g, "");
|
|
135
|
+
if (!question || question.trim().length === 0) {
|
|
136
|
+
question = "latest information";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate and sanitize the query before sending to search APIs
|
|
140
|
+
// If the LLM returned a long response instead of a concise query, extract the first sentence
|
|
141
|
+
// or use the original user query as fallback
|
|
142
|
+
question = question.trim();
|
|
143
|
+
if (question.length > 500 || question.split(/[.!?]\s+/).length > 5) {
|
|
144
|
+
// Query is too long or contains too many sentences - likely the LLM returned a full response
|
|
145
|
+
console.warn(`[MetaSearchAgent] Query is too long (${question.length} chars), truncating or using fallback`);
|
|
146
|
+
|
|
147
|
+
// Try to extract first sentence as the query
|
|
148
|
+
const firstSentence = question.split(/[.!?]\s+/)[0].trim();
|
|
149
|
+
if (firstSentence.length > 0 && firstSentence.length < 200) {
|
|
150
|
+
question = firstSentence;
|
|
151
|
+
} else {
|
|
152
|
+
// Fallback to original user query
|
|
153
|
+
question = query.slice(0, 200);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Emit "searching" progress event so the client can show live status
|
|
158
|
+
const categoryLabel = this.config.activeEngines.length > 0
|
|
159
|
+
? this.config.activeEngines.slice(0, 2).join(", ")
|
|
160
|
+
: "Web";
|
|
161
|
+
const emitSearching = (status: SearchingEvent["status"], query: string, cat?: string) => {
|
|
162
|
+
emitter?.emit("data", JSON.stringify({
|
|
163
|
+
type: "searching",
|
|
164
|
+
data: { query, category: cat ?? categoryLabel, status } satisfies SearchingEvent,
|
|
165
|
+
}));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
emitSearching("running", question);
|
|
169
|
+
|
|
170
|
+
let res: { results: any[]; suggestions: string[] } = { results: [], suggestions: [] };
|
|
171
|
+
|
|
172
|
+
// If no search functions provided, return empty results
|
|
173
|
+
if (!this.config.searchSearxng && !this.config.searchTavily) {
|
|
174
|
+
res = { results: [], suggestions: [] };
|
|
175
|
+
} else {
|
|
176
|
+
const runSearxng = async () => {
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.config.searchSearxng!(question, {
|
|
179
|
+
language: "en",
|
|
180
|
+
engines: this.config.activeEngines,
|
|
181
|
+
categories: [category],
|
|
182
|
+
});
|
|
183
|
+
// Ensure result has the expected structure
|
|
184
|
+
return result && typeof result === 'object' && 'results' in result
|
|
185
|
+
? result
|
|
186
|
+
: { results: [], suggestions: [] };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error("[MetaSearchAgent] SearXNG search failed:", error);
|
|
189
|
+
return { results: [], suggestions: [] };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const isTavilyConfigured = this.config.isTavilyConfigured?.() ?? false;
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
isTavilyConfigured &&
|
|
197
|
+
this.config.activeEngines.length === 0 &&
|
|
198
|
+
category === "general"
|
|
199
|
+
) {
|
|
200
|
+
try {
|
|
201
|
+
const tavilyResult = await this.config.searchTavily!(question, { searchDepth: "basic", maxResults: 10 });
|
|
202
|
+
res = tavilyResult && typeof tavilyResult === 'object' && 'results' in tavilyResult
|
|
203
|
+
? tavilyResult
|
|
204
|
+
: { results: [], suggestions: [] };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Tavily search failed, falling back to SearXNG:", error);
|
|
207
|
+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
if (isTavilyConfigured && this.config.searchTavily) {
|
|
211
|
+
try {
|
|
212
|
+
res = await Promise.race([
|
|
213
|
+
runSearxng(),
|
|
214
|
+
new Promise<{ results: any[]; suggestions: string[] }>((_, reject) =>
|
|
215
|
+
setTimeout(() => reject(new Error("Timeout")), 10000)
|
|
216
|
+
)
|
|
217
|
+
]);
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
if (err.message === "Timeout") {
|
|
220
|
+
console.warn("[MetaSearchAgent] SearXNG search did not respond in 10 seconds, falling back to Tavily.");
|
|
221
|
+
try {
|
|
222
|
+
const tavilyResult = await this.config.searchTavily(question, { searchDepth: "basic", maxResults: 10 });
|
|
223
|
+
res = tavilyResult && typeof tavilyResult === 'object' && 'results' in tavilyResult
|
|
224
|
+
? tavilyResult
|
|
225
|
+
: { results: [], suggestions: [] };
|
|
226
|
+
} catch (tavilyErr) {
|
|
227
|
+
console.error("[MetaSearchAgent] Tavily fallback also failed, awaiting SearXNG directly:", tavilyErr);
|
|
228
|
+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.error("[MetaSearchAgent] SearXNG search failed, falling back to Tavily:", err);
|
|
232
|
+
try {
|
|
233
|
+
const tavilyResult = await this.config.searchTavily(question, { searchDepth: "basic", maxResults: 10 });
|
|
234
|
+
res = tavilyResult && typeof tavilyResult === 'object' && 'results' in tavilyResult
|
|
235
|
+
? tavilyResult
|
|
236
|
+
: { results: [], suggestions: [] };
|
|
237
|
+
} catch (tavilyErr) {
|
|
238
|
+
console.error("[MetaSearchAgent] Tavily fallback also failed:", tavilyErr);
|
|
239
|
+
res = { results: [], suggestions: [] };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
res = this.config.searchSearxng ? await runSearxng() : { results: [], suggestions: [] };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let documents: Document[] = (res?.results ?? []).map((result) => ({
|
|
250
|
+
pageContent:
|
|
251
|
+
result.content ||
|
|
252
|
+
(this.config.activeEngines.includes("youtube") ? result.title : ""),
|
|
253
|
+
metadata: {
|
|
254
|
+
title: result.title,
|
|
255
|
+
url: result.url,
|
|
256
|
+
source: result.source,
|
|
257
|
+
...(result.img_src && { img_src: result.img_src }),
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
if (documents.length === 0) {
|
|
262
|
+
documents = buildFallbackDocs(question);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
emitSearching("done", question);
|
|
266
|
+
|
|
267
|
+
// Determine extraction budget from thinkingTimeLimit (seconds).
|
|
268
|
+
// thinkingTimeLimit === 0 means unlimited; use server config.
|
|
269
|
+
let scrapeCount: number;
|
|
270
|
+
let perSourceTimeout: number;
|
|
271
|
+
|
|
272
|
+
if (thinkingTimeLimit > 0) {
|
|
273
|
+
// Spread the time budget across 3 sources
|
|
274
|
+
scrapeCount = 3;
|
|
275
|
+
perSourceTimeout = Math.max(2, Math.floor(thinkingTimeLimit / scrapeCount));
|
|
276
|
+
} else if (sourceExtractionEnabled) {
|
|
277
|
+
scrapeCount = 3;
|
|
278
|
+
// Default to 5 seconds if no config function available
|
|
279
|
+
perSourceTimeout = 5;
|
|
280
|
+
} else {
|
|
281
|
+
scrapeCount = 0;
|
|
282
|
+
perSourceTimeout = 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (scrapeCount > 0) {
|
|
286
|
+
const docsToScrape = documents.slice(0, scrapeCount);
|
|
287
|
+
emitSearching("running", `Extracting top ${docsToScrape.length} sources`, "extract");
|
|
288
|
+
|
|
289
|
+
const extractionTasks = this.config.scrapeURL ? docsToScrape.map(async (doc, idx) => {
|
|
290
|
+
const url = doc.metadata?.url;
|
|
291
|
+
if (!url || !this.config.scrapeURL) return;
|
|
292
|
+
try {
|
|
293
|
+
const result = await waitWithTimeout(
|
|
294
|
+
this.config.scrapeURL(url, { timeout: perSourceTimeout }),
|
|
295
|
+
perSourceTimeout * 1000 + 1500,
|
|
296
|
+
);
|
|
297
|
+
if (typeof result === "string" && result.length > 100) {
|
|
298
|
+
const text = htmlToText(result)
|
|
299
|
+
.replace(/(\r\n|\n|\r)/gm, " ")
|
|
300
|
+
.replace(/\s+/g, " ")
|
|
301
|
+
.trim()
|
|
302
|
+
.slice(0, 5000);
|
|
303
|
+
if (text.length > 100) {
|
|
304
|
+
documents[idx].pageContent = text;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
// Keep original snippet on scraping failure or timeout
|
|
309
|
+
}
|
|
310
|
+
}) : [];
|
|
311
|
+
|
|
312
|
+
await Promise.allSettled(extractionTasks);
|
|
313
|
+
emitSearching("done", `Extracting top ${docsToScrape.length} sources`, "extract");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { query: question, docs: documents };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Runs the full search-and-answer pipeline, emitting "sources",
|
|
321
|
+
* "response", "searching", "end", and "error" events on the emitter.
|
|
322
|
+
*/
|
|
323
|
+
private async runPipeline(
|
|
324
|
+
emitter: EventEmitter,
|
|
325
|
+
message: string,
|
|
326
|
+
history: ChatTurnMessage[],
|
|
327
|
+
llm: LanguageModel,
|
|
328
|
+
optimizationMode: "speed" | "balanced" | "quality",
|
|
329
|
+
fileIds: string[],
|
|
330
|
+
systemInstructions: string,
|
|
331
|
+
category: string,
|
|
332
|
+
sourceExtractionEnabled: boolean,
|
|
333
|
+
thinkingTimeLimit: number,
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
try {
|
|
336
|
+
let docs: Document[] | null = null;
|
|
337
|
+
let query = message;
|
|
338
|
+
|
|
339
|
+
if (this.config.searchWeb) {
|
|
340
|
+
const result = await this.retrieveSearchDocs(
|
|
341
|
+
llm,
|
|
342
|
+
formatChatHistoryAsString(history),
|
|
343
|
+
message,
|
|
344
|
+
category,
|
|
345
|
+
sourceExtractionEnabled,
|
|
346
|
+
thinkingTimeLimit,
|
|
347
|
+
emitter,
|
|
348
|
+
);
|
|
349
|
+
query = result.query;
|
|
350
|
+
docs = result.docs;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const sortedDocs = await rerankDocs(
|
|
354
|
+
query,
|
|
355
|
+
docs ?? [],
|
|
356
|
+
fileIds,
|
|
357
|
+
optimizationMode,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const sources = normalizeSourcesOutput(sortedDocs, message);
|
|
361
|
+
console.log("[MetaSearchAgent] emitting sources:", sources.length);
|
|
362
|
+
emitter.emit("data", JSON.stringify({ type: "sources", data: sources }));
|
|
363
|
+
|
|
364
|
+
const systemPrompt = interpolatePrompt(this.config.responsePrompt, {
|
|
365
|
+
systemInstructions,
|
|
366
|
+
context: processDocs(sortedDocs),
|
|
367
|
+
date: new Date().toISOString(),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = streamText({
|
|
371
|
+
model: llm,
|
|
372
|
+
temperature: 0.7,
|
|
373
|
+
system: systemPrompt,
|
|
374
|
+
messages: [...history, { role: "user", content: message }],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
let responseChunkCount = 0;
|
|
378
|
+
for await (const chunk of result.textStream) {
|
|
379
|
+
responseChunkCount += 1;
|
|
380
|
+
emitter.emit("data", JSON.stringify({ type: "response", data: chunk }));
|
|
381
|
+
}
|
|
382
|
+
console.log("[MetaSearchAgent] response stream ended, chunks:", responseChunkCount);
|
|
383
|
+
|
|
384
|
+
if (responseChunkCount === 0) {
|
|
385
|
+
const fallbackUrls = sources
|
|
386
|
+
.map((doc) => doc.metadata?.url)
|
|
387
|
+
.filter((url): url is string => typeof url === "string")
|
|
388
|
+
.slice(0, 5);
|
|
389
|
+
|
|
390
|
+
const fallbackMessage = [
|
|
391
|
+
"I couldn't generate a full answer, but I ran a web search and found these source URLs:",
|
|
392
|
+
...fallbackUrls.map((url) => `- ${url}`),
|
|
393
|
+
].join("\n");
|
|
394
|
+
|
|
395
|
+
emitter.emit("data", JSON.stringify({ type: "response", data: fallbackMessage }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
emitter.emit("end");
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
401
|
+
console.error("[MetaSearchAgent] caught error from AI SDK stream:", errMessage, err);
|
|
402
|
+
let userMessage = errMessage;
|
|
403
|
+
if (errMessage.includes("404") || errMessage.toLowerCase().includes("not found")) {
|
|
404
|
+
userMessage =
|
|
405
|
+
"The selected AI model was not found at the provider. Please go to Settings → Model Providers and select a different model.";
|
|
406
|
+
} else if (errMessage.includes("410")) {
|
|
407
|
+
userMessage =
|
|
408
|
+
"The selected AI model is no longer available (deprecated by the provider). Please go to Settings → Model Providers and select a different model.";
|
|
409
|
+
} else if (errMessage.includes("401") || errMessage.includes("authentication")) {
|
|
410
|
+
userMessage =
|
|
411
|
+
"Authentication failed with the AI provider. Please check your API key in Settings.";
|
|
412
|
+
} else if (errMessage.includes("429") || errMessage.includes("rate limit")) {
|
|
413
|
+
userMessage =
|
|
414
|
+
"Rate limit reached for the AI provider. Please wait a moment and try again.";
|
|
415
|
+
}
|
|
416
|
+
emitter.emit("error", JSON.stringify({ data: userMessage }));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async searchAndAnswer(
|
|
421
|
+
message: string,
|
|
422
|
+
history: ChatTurnMessage[],
|
|
423
|
+
llm: LanguageModel,
|
|
424
|
+
optimizationMode: "speed" | "balanced" | "quality",
|
|
425
|
+
fileIds: string[],
|
|
426
|
+
systemInstructions: string,
|
|
427
|
+
category: string = "general",
|
|
428
|
+
sourceExtractionEnabled = false,
|
|
429
|
+
thinkingTimeLimit = 0,
|
|
430
|
+
) {
|
|
431
|
+
const emitter = new EventEmitter();
|
|
432
|
+
|
|
433
|
+
// Start on the next macrotask so callers can attach their event
|
|
434
|
+
// listeners before the first "sources"/"response" event can fire.
|
|
435
|
+
setTimeout(() => {
|
|
436
|
+
this.runPipeline(
|
|
437
|
+
emitter,
|
|
438
|
+
message,
|
|
439
|
+
history,
|
|
440
|
+
llm,
|
|
441
|
+
optimizationMode,
|
|
442
|
+
fileIds,
|
|
443
|
+
systemInstructions,
|
|
444
|
+
category,
|
|
445
|
+
sourceExtractionEnabled,
|
|
446
|
+
thinkingTimeLimit,
|
|
447
|
+
);
|
|
448
|
+
}, 0);
|
|
449
|
+
|
|
450
|
+
return emitter;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export default MetaSearchAgent;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/search/index
|
|
3
|
+
* @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.
|
|
8
|
+
*/
|
|
9
|
+
import MetaSearchAgent from "./metaSearchAgent";
|
|
10
|
+
import prompts from "write-language/prompts/search-prompts";
|
|
11
|
+
import type { Config } from "./meta-search-types";
|
|
12
|
+
|
|
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>) => ({
|
|
18
|
+
webSearch: new MetaSearchAgent({
|
|
19
|
+
activeEngines: [],
|
|
20
|
+
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
21
|
+
responsePrompt: prompts.webSearchResponsePrompt,
|
|
22
|
+
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
23
|
+
rerank: true,
|
|
24
|
+
rerankThreshold: 0.3,
|
|
25
|
+
searchWeb: true,
|
|
26
|
+
...searchFunctions,
|
|
27
|
+
}),
|
|
28
|
+
academicSearch: new MetaSearchAgent({
|
|
29
|
+
activeEngines: ["arxiv", "google scholar", "pubmed"],
|
|
30
|
+
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
31
|
+
responsePrompt: prompts.webSearchResponsePrompt,
|
|
32
|
+
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
33
|
+
rerank: true,
|
|
34
|
+
rerankThreshold: 0,
|
|
35
|
+
searchWeb: true,
|
|
36
|
+
...searchFunctions,
|
|
37
|
+
}),
|
|
38
|
+
writingAssistant: new MetaSearchAgent({
|
|
39
|
+
activeEngines: [],
|
|
40
|
+
queryGeneratorPrompt: "",
|
|
41
|
+
queryGeneratorFewShots: [],
|
|
42
|
+
responsePrompt: prompts.writingAssistantPrompt,
|
|
43
|
+
rerank: true,
|
|
44
|
+
rerankThreshold: 0,
|
|
45
|
+
searchWeb: true,
|
|
46
|
+
...searchFunctions,
|
|
47
|
+
}),
|
|
48
|
+
wolframAlphaSearch: new MetaSearchAgent({
|
|
49
|
+
activeEngines: ["wolframalpha"],
|
|
50
|
+
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
51
|
+
responsePrompt: prompts.webSearchResponsePrompt,
|
|
52
|
+
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
53
|
+
rerank: false,
|
|
54
|
+
rerankThreshold: 0,
|
|
55
|
+
searchWeb: true,
|
|
56
|
+
...searchFunctions,
|
|
57
|
+
}),
|
|
58
|
+
youtubeSearch: new MetaSearchAgent({
|
|
59
|
+
activeEngines: ["youtube"],
|
|
60
|
+
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
61
|
+
responsePrompt: prompts.webSearchResponsePrompt,
|
|
62
|
+
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
63
|
+
rerank: true,
|
|
64
|
+
rerankThreshold: 0.3,
|
|
65
|
+
searchWeb: true,
|
|
66
|
+
...searchFunctions,
|
|
67
|
+
}),
|
|
68
|
+
redditSearch: new MetaSearchAgent({
|
|
69
|
+
activeEngines: ["reddit"],
|
|
70
|
+
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
71
|
+
responsePrompt: prompts.webSearchResponsePrompt,
|
|
72
|
+
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
73
|
+
rerank: true,
|
|
74
|
+
rerankThreshold: 0.3,
|
|
75
|
+
searchWeb: true,
|
|
76
|
+
...searchFunctions,
|
|
77
|
+
}),
|
|
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,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/chains/suggestionGeneratorAgent
|
|
3
|
+
* @description Generates follow-up question suggestions from a conversation
|
|
4
|
+
* using the Vercel AI SDK.
|
|
5
|
+
*/
|
|
6
|
+
import { generateText, type LanguageModel } from "ai";
|
|
7
|
+
import { LineListOutputParser } from "../../utils/outputParser";
|
|
8
|
+
import { formatChatHistoryAsString } from "../../utils";
|
|
9
|
+
import type { ChatTurnMessage } from "./meta-search-types";
|
|
10
|
+
|
|
11
|
+
const suggestionGeneratorPrompt = `
|
|
12
|
+
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
|
|
13
|
+
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
|
|
14
|
+
Make sure the suggestions are medium in length and are informative and relevant to the conversation.
|
|
15
|
+
|
|
16
|
+
Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example:
|
|
17
|
+
|
|
18
|
+
<suggestions>
|
|
19
|
+
Tell me more about SpaceX and their recent projects
|
|
20
|
+
What is the latest news on SpaceX?
|
|
21
|
+
Who is the CEO of SpaceX?
|
|
22
|
+
</suggestions>
|
|
23
|
+
|
|
24
|
+
Conversation:
|
|
25
|
+
{chat_history}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
type SuggestionGeneratorInput = {
|
|
29
|
+
chat_history: ChatTurnMessage[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const outputParser = new LineListOutputParser({
|
|
33
|
+
key: "suggestions",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const generateSuggestions = async (
|
|
37
|
+
input: SuggestionGeneratorInput,
|
|
38
|
+
llm: LanguageModel,
|
|
39
|
+
): Promise<string[]> => {
|
|
40
|
+
const prompt = suggestionGeneratorPrompt.replace(
|
|
41
|
+
"{chat_history}",
|
|
42
|
+
formatChatHistoryAsString(input.chat_history),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const { text } = await generateText({
|
|
46
|
+
model: llm,
|
|
47
|
+
temperature: 0,
|
|
48
|
+
prompt,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return outputParser.parse(text);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default generateSuggestions;
|