chat-agent-toolkit 1.2.426 → 1.2.428
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 +4 -0
- package/dist/chat-agent-toolkit/src/tools/qwksearch-api-tools.d.ts +1 -1
- package/dist/chat-agent-toolkit/src/tools/search/meta-search-types.d.ts +6 -1
- package/dist/chat-agent-toolkit/src/tools/search/metaSearchAgent.d.ts +23 -4
- package/dist/research-agent.cjs.js +1 -1
- package/dist/research-agent.cjs.js.map +1 -1
- package/dist/research-agent.es.js +1 -1
- package/dist/research-agent.es.js.map +1 -1
- package/package.json +2 -3
- package/src/tools/search/meta-search-types.d.ts +3 -1
- package/src/tools/search/meta-search-types.ts +5 -0
- package/src/tools/search/metaSearchAgent.d.ts +16 -6
- package/src/tools/search/metaSearchAgent.ts +140 -56
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chat-agent-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.428",
|
|
4
4
|
"description": "Multi-provider AI agent toolkit: generate language responses, search the web, extract content, and manage memory across 10+ LLM providers.",
|
|
5
5
|
"author": "vtempest <grokthiscontact@gmail.com>",
|
|
6
6
|
"license": "rights.institute/PROSPER",
|
|
@@ -94,11 +94,10 @@
|
|
|
94
94
|
"@mastra/memory": "^1.28.3",
|
|
95
95
|
"ai": "^7.0.94",
|
|
96
96
|
"drizzle-orm": "^0.45.2",
|
|
97
|
-
"prismjs": "^1.30.0",
|
|
98
97
|
"html-entities": "^2.6.0",
|
|
99
98
|
"manage-storage": "^0.0.47",
|
|
100
99
|
"marked": "^18.0.12",
|
|
101
|
-
"qwksearch-api-client": "^1.0.
|
|
100
|
+
"qwksearch-api-client": "^1.0.238",
|
|
102
101
|
"write-language": "^0.1.153",
|
|
103
102
|
"zod": "^4.5.4"
|
|
104
103
|
},
|
|
@@ -20,7 +20,9 @@ export interface DrainableEmitter extends EventEmitter {
|
|
|
20
20
|
export interface MetaSearchAgentType {
|
|
21
21
|
searchAndAnswer: (message: string, history: ChatTurnMessage[], llm: LanguageModel, optimizationMode: "speed" | "balanced" | "quality", fileIds: string[], systemInstructions: string, category?: string, sourceExtractionEnabled?: boolean, thinkingTimeLimit?: number,
|
|
22
22
|
/** User-authored replacement for the focus mode's query-expansion prompt. */
|
|
23
|
-
queryExpansionPrompt?: string
|
|
23
|
+
queryExpansionPrompt?: string,
|
|
24
|
+
/** Rephrase the message with the LLM before searching. Off by default. */
|
|
25
|
+
queryExpansionEnabled?: boolean) => Promise<DrainableEmitter>;
|
|
24
26
|
}
|
|
25
27
|
/** Emitted on the EventEmitter data channel to report live search progress. */
|
|
26
28
|
export interface SearchingEvent {
|
|
@@ -41,6 +41,11 @@ export interface MetaSearchAgentType {
|
|
|
41
41
|
thinkingTimeLimit?: number,
|
|
42
42
|
/** User-authored replacement for the focus mode's query-expansion prompt. */
|
|
43
43
|
queryExpansionPrompt?: string,
|
|
44
|
+
/**
|
|
45
|
+
* Rephrase the message with the LLM before searching. Off by default: the
|
|
46
|
+
* message is searched as typed, which saves a model round trip.
|
|
47
|
+
*/
|
|
48
|
+
queryExpansionEnabled?: boolean,
|
|
44
49
|
) => Promise<DrainableEmitter>;
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Orchestrator for complex research queries.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Runs the web search, streams the result list to the client, and synthesizes
|
|
4
|
+
* an answer from the result titles and snippets. LLM query expansion before
|
|
5
|
+
* the search is opt-in.
|
|
5
6
|
*/
|
|
6
7
|
import { type LanguageModel } from "ai";
|
|
7
8
|
import type { Config, ChatTurnMessage, DrainableEmitter, MetaSearchAgentType } from "./meta-search-types";
|
|
@@ -10,16 +11,25 @@ declare class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
10
11
|
private config;
|
|
11
12
|
constructor(config: Config);
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
* URLs to summarize
|
|
15
|
-
|
|
14
|
+
* Asks the LLM to rephrase the message into a standalone search question,
|
|
15
|
+
* and to pick out any URLs to summarize. Only runs when query expansion is on.
|
|
16
|
+
*/
|
|
17
|
+
private expandQuery;
|
|
18
|
+
/**
|
|
19
|
+
* Runs the web search and returns its results as answer-context documents:
|
|
20
|
+
* each one the result's title and search snippet, nothing fetched yet.
|
|
16
21
|
*/
|
|
17
22
|
private retrieveSearchDocs;
|
|
23
|
+
/**
|
|
24
|
+
* Replaces the search snippets of the top web results with text fetched
|
|
25
|
+
* from their pages, after the result list has been sent to the client.
|
|
26
|
+
*/
|
|
27
|
+
private extractTopSources;
|
|
18
28
|
/**
|
|
19
29
|
* Runs the full search-and-answer pipeline, emitting "sources",
|
|
20
30
|
* "response", "searching", "end", and "error" events on the emitter.
|
|
21
31
|
*/
|
|
22
32
|
private runPipeline;
|
|
23
|
-
searchAndAnswer(message: string, history: ChatTurnMessage[], llm: LanguageModel, optimizationMode: "speed" | "balanced" | "quality", fileIds: string[], systemInstructions: string, category?: string, sourceExtractionEnabled?: boolean, thinkingTimeLimit?: number, queryExpansionPrompt?: string): Promise<DrainableEmitter>;
|
|
33
|
+
searchAndAnswer(message: string, history: ChatTurnMessage[], llm: LanguageModel, optimizationMode: "speed" | "balanced" | "quality", fileIds: string[], systemInstructions: string, category?: string, sourceExtractionEnabled?: boolean, thinkingTimeLimit?: number, queryExpansionPrompt?: string, queryExpansionEnabled?: boolean): Promise<DrainableEmitter>;
|
|
24
34
|
}
|
|
25
35
|
export default MetaSearchAgent;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Orchestrator for complex research queries.
|
|
3
|
-
*
|
|
4
|
-
* LLMs through the Vercel
|
|
3
|
+
* Runs the web search, streams the result list to the client, and synthesizes
|
|
4
|
+
* an answer from the result titles and snippets using LLMs through the Vercel
|
|
5
|
+
* AI SDK (generateText/streamText). LLM query expansion before the search is
|
|
6
|
+
* opt-in: by default the user's message is searched as typed, so the first
|
|
7
|
+
* model call is the answer itself.
|
|
5
8
|
*/
|
|
6
9
|
import { generateText, streamText, type LanguageModel } from "ai";
|
|
7
10
|
import { LineOutputParser, LineListOutputParser } from "../../utils/outputParser";
|
|
@@ -82,6 +85,30 @@ const interpolatePrompt = (
|
|
|
82
85
|
);
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
/** Matches the http(s) URLs a user pastes into a message. */
|
|
89
|
+
const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/gi;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Keeps a search query to something a search engine can use. A rephraser that
|
|
93
|
+
* answered instead of rephrasing, or a pasted essay, becomes its first
|
|
94
|
+
* sentence — or, failing that, the start of the user's own message.
|
|
95
|
+
*/
|
|
96
|
+
const toSearchQuery = (question: string, fallback: string): string => {
|
|
97
|
+
question = question.replace(/<think>.*?<\/think>/g, "").trim();
|
|
98
|
+
if (question.length === 0) return "latest information";
|
|
99
|
+
|
|
100
|
+
if (question.length > 500 || question.split(/[.!?]\s+/).length > 5) {
|
|
101
|
+
console.warn(`[MetaSearchAgent] Query is too long (${question.length} chars), truncating or using fallback`);
|
|
102
|
+
const firstSentence = question.split(/[.!?]\s+/)[0].trim();
|
|
103
|
+
if (firstSentence.length > 0 && firstSentence.length < 200) {
|
|
104
|
+
return firstSentence;
|
|
105
|
+
}
|
|
106
|
+
return fallback.slice(0, 200);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return question;
|
|
110
|
+
};
|
|
111
|
+
|
|
85
112
|
class MetaSearchAgent implements MetaSearchAgentType {
|
|
86
113
|
private config: Config;
|
|
87
114
|
|
|
@@ -90,20 +117,16 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
/**
|
|
93
|
-
*
|
|
94
|
-
* URLs to summarize
|
|
95
|
-
*
|
|
120
|
+
* Asks the LLM to rephrase the message into a standalone search question,
|
|
121
|
+
* and to pick out any URLs to summarize. This is a full model round trip
|
|
122
|
+
* before the search can start, so it only runs when query expansion is on.
|
|
96
123
|
*/
|
|
97
|
-
private async
|
|
124
|
+
private async expandQuery(
|
|
98
125
|
llm: LanguageModel,
|
|
99
126
|
chatHistory: string,
|
|
100
127
|
query: string,
|
|
101
|
-
category: string = "general",
|
|
102
|
-
sourceExtractionEnabled = false,
|
|
103
|
-
thinkingTimeLimit = 0,
|
|
104
|
-
emitter?: EventEmitter,
|
|
105
128
|
queryGeneratorPromptOverride?: string,
|
|
106
|
-
): Promise<{
|
|
129
|
+
): Promise<{ question: string; links: string[] }> {
|
|
107
130
|
// A user-authored query-expansion prompt (Settings → Search Settings)
|
|
108
131
|
// replaces the focus mode's built-in one. Blank or whitespace-only text
|
|
109
132
|
// means "use the built-in prompt".
|
|
@@ -146,37 +169,55 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
146
169
|
question = "latest information";
|
|
147
170
|
}
|
|
148
171
|
|
|
172
|
+
return { question, links };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Runs the web search and returns its results as answer-context documents:
|
|
177
|
+
* each one the result's title and search snippet, nothing fetched yet.
|
|
178
|
+
*
|
|
179
|
+
* The message is searched as typed unless `queryExpansionEnabled` is set,
|
|
180
|
+
* in which case the LLM rephrases it first (see {@link expandQuery}).
|
|
181
|
+
* `fromLinks` marks documents built from URLs in the message rather than a
|
|
182
|
+
* search, which already carry their pages' text.
|
|
183
|
+
*/
|
|
184
|
+
private async retrieveSearchDocs(
|
|
185
|
+
llm: LanguageModel,
|
|
186
|
+
chatHistory: string,
|
|
187
|
+
query: string,
|
|
188
|
+
category: string = "general",
|
|
189
|
+
emitter?: EventEmitter,
|
|
190
|
+
queryGeneratorPromptOverride?: string,
|
|
191
|
+
queryExpansionEnabled = false,
|
|
192
|
+
): Promise<{ query: string; docs: Document[]; fromLinks?: boolean }> {
|
|
193
|
+
let question: string;
|
|
194
|
+
let links: string[];
|
|
195
|
+
|
|
196
|
+
if (queryExpansionEnabled) {
|
|
197
|
+
({ question, links } = await this.expandQuery(
|
|
198
|
+
llm,
|
|
199
|
+
chatHistory,
|
|
200
|
+
query,
|
|
201
|
+
queryGeneratorPromptOverride,
|
|
202
|
+
));
|
|
203
|
+
} else {
|
|
204
|
+
links = query.match(URL_PATTERN) ?? [];
|
|
205
|
+
question =
|
|
206
|
+
links.length > 0 && this.config.getDocumentsFromLinks
|
|
207
|
+
? query.replace(URL_PATTERN, " ").replace(/\s+/g, " ").trim()
|
|
208
|
+
: query;
|
|
209
|
+
}
|
|
210
|
+
|
|
149
211
|
if (links.length > 0 && this.config.getDocumentsFromLinks) {
|
|
150
212
|
if (question.length === 0) question = "summarize";
|
|
151
213
|
|
|
152
214
|
const linkDocs = await this.config.getDocumentsFromLinks({ links });
|
|
153
215
|
const docs = await groupAndSummarizeDocs(llm, linkDocs, question);
|
|
154
216
|
|
|
155
|
-
return { query: question, docs };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
question = question.replace(/<think>.*?<\/think>/g, "");
|
|
159
|
-
if (!question || question.trim().length === 0) {
|
|
160
|
-
question = "latest information";
|
|
217
|
+
return { query: question, docs, fromLinks: true };
|
|
161
218
|
}
|
|
162
219
|
|
|
163
|
-
|
|
164
|
-
// If the LLM returned a long response instead of a concise query, extract the first sentence
|
|
165
|
-
// or use the original user query as fallback
|
|
166
|
-
question = question.trim();
|
|
167
|
-
if (question.length > 500 || question.split(/[.!?]\s+/).length > 5) {
|
|
168
|
-
// Query is too long or contains too many sentences - likely the LLM returned a full response
|
|
169
|
-
console.warn(`[MetaSearchAgent] Query is too long (${question.length} chars), truncating or using fallback`);
|
|
170
|
-
|
|
171
|
-
// Try to extract first sentence as the query
|
|
172
|
-
const firstSentence = question.split(/[.!?]\s+/)[0].trim();
|
|
173
|
-
if (firstSentence.length > 0 && firstSentence.length < 200) {
|
|
174
|
-
question = firstSentence;
|
|
175
|
-
} else {
|
|
176
|
-
// Fallback to original user query
|
|
177
|
-
question = query.slice(0, 200);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
220
|
+
question = toSearchQuery(question, query);
|
|
180
221
|
|
|
181
222
|
// Emit "searching" progress event so the client can show live status
|
|
182
223
|
const categoryLabel = this.config.activeEngines.length > 0
|
|
@@ -288,6 +329,23 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
288
329
|
|
|
289
330
|
emitSearching("done", question);
|
|
290
331
|
|
|
332
|
+
return { query: question, docs: documents };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Replaces the search snippets of the top web results with text fetched
|
|
337
|
+
* from their pages, in place, when the user has turned source extraction on
|
|
338
|
+
* or set a thinking-time budget. Uploaded files are left alone.
|
|
339
|
+
*
|
|
340
|
+
* This runs after the result list has been sent to the client, so the
|
|
341
|
+
* sources appear while the pages are still loading.
|
|
342
|
+
*/
|
|
343
|
+
private async extractTopSources(
|
|
344
|
+
documents: Document[],
|
|
345
|
+
sourceExtractionEnabled: boolean,
|
|
346
|
+
thinkingTimeLimit: number,
|
|
347
|
+
emitter: EventEmitter,
|
|
348
|
+
): Promise<void> {
|
|
291
349
|
// Determine extraction budget from thinkingTimeLimit (seconds).
|
|
292
350
|
// thinkingTimeLimit === 0 means unlimited; use server config.
|
|
293
351
|
let scrapeCount: number;
|
|
@@ -307,10 +365,18 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
307
365
|
}
|
|
308
366
|
|
|
309
367
|
if (scrapeCount > 0) {
|
|
310
|
-
const
|
|
311
|
-
|
|
368
|
+
const emitExtracting = (status: SearchingEvent["status"], query: string) => {
|
|
369
|
+
emitter.emit("data", JSON.stringify({
|
|
370
|
+
type: "searching",
|
|
371
|
+
data: { query, category: "extract", status } satisfies SearchingEvent,
|
|
372
|
+
}));
|
|
373
|
+
};
|
|
374
|
+
const docsToScrape = documents
|
|
375
|
+
.filter((doc) => doc.metadata?.url && doc.metadata.url !== "File")
|
|
376
|
+
.slice(0, scrapeCount);
|
|
377
|
+
emitExtracting("running", `Extracting top ${docsToScrape.length} sources`);
|
|
312
378
|
|
|
313
|
-
const extractionTasks = this.config.scrapeURL ? docsToScrape.map(async (doc
|
|
379
|
+
const extractionTasks = this.config.scrapeURL ? docsToScrape.map(async (doc) => {
|
|
314
380
|
const url = doc.metadata?.url;
|
|
315
381
|
if (!url || !this.config.scrapeURL) return;
|
|
316
382
|
try {
|
|
@@ -325,7 +391,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
325
391
|
.trim()
|
|
326
392
|
.slice(0, 5000);
|
|
327
393
|
if (text.length > 100) {
|
|
328
|
-
|
|
394
|
+
doc.pageContent = text;
|
|
329
395
|
}
|
|
330
396
|
}
|
|
331
397
|
} catch {
|
|
@@ -334,10 +400,8 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
334
400
|
}) : [];
|
|
335
401
|
|
|
336
402
|
await Promise.allSettled(extractionTasks);
|
|
337
|
-
|
|
403
|
+
emitExtracting("done", `Extracting top ${docsToScrape.length} sources`);
|
|
338
404
|
}
|
|
339
|
-
|
|
340
|
-
return { query: question, docs: documents };
|
|
341
405
|
}
|
|
342
406
|
|
|
343
407
|
/**
|
|
@@ -356,10 +420,28 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
356
420
|
sourceExtractionEnabled: boolean,
|
|
357
421
|
thinkingTimeLimit: number,
|
|
358
422
|
queryExpansionPrompt?: string,
|
|
423
|
+
queryExpansionEnabled = false,
|
|
359
424
|
): Promise<void> {
|
|
360
425
|
try {
|
|
426
|
+
const r2Credentials: R2CredentialsInput | undefined = process.env.R2_ACCOUNT_ID
|
|
427
|
+
? {
|
|
428
|
+
accountId: process.env.R2_ACCOUNT_ID,
|
|
429
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID || "",
|
|
430
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || "",
|
|
431
|
+
bucket: process.env.R2_UPLOADS_BUCKET || "qwksearch-uploads",
|
|
432
|
+
}
|
|
433
|
+
: undefined;
|
|
434
|
+
|
|
435
|
+
// Resolved once and shared below: the answer context needs the text and
|
|
436
|
+
// the message needs the images, and fetching each attachment twice is how
|
|
437
|
+
// a handful of large uploads used to reach the isolate's memory ceiling.
|
|
438
|
+
// Started before the search so the two waits overlap; loadUploads never
|
|
439
|
+
// rejects (a file it cannot resolve is dropped).
|
|
440
|
+
const uploadsPromise = loadUploads(fileIds, r2Credentials);
|
|
441
|
+
|
|
361
442
|
let docs: Document[] | null = null;
|
|
362
443
|
let query = message;
|
|
444
|
+
let fromLinks = false;
|
|
363
445
|
|
|
364
446
|
if (this.config.searchWeb) {
|
|
365
447
|
const result = await this.retrieveSearchDocs(
|
|
@@ -367,28 +449,16 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
367
449
|
formatChatHistoryAsString(history),
|
|
368
450
|
message,
|
|
369
451
|
category,
|
|
370
|
-
sourceExtractionEnabled,
|
|
371
|
-
thinkingTimeLimit,
|
|
372
452
|
emitter,
|
|
373
453
|
queryExpansionPrompt,
|
|
454
|
+
queryExpansionEnabled,
|
|
374
455
|
);
|
|
375
456
|
query = result.query;
|
|
376
457
|
docs = result.docs;
|
|
458
|
+
fromLinks = result.fromLinks ?? false;
|
|
377
459
|
}
|
|
378
460
|
|
|
379
|
-
const
|
|
380
|
-
? {
|
|
381
|
-
accountId: process.env.R2_ACCOUNT_ID,
|
|
382
|
-
accessKeyId: process.env.R2_ACCESS_KEY_ID || "",
|
|
383
|
-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || "",
|
|
384
|
-
bucket: process.env.R2_UPLOADS_BUCKET || "qwksearch-uploads",
|
|
385
|
-
}
|
|
386
|
-
: undefined;
|
|
387
|
-
|
|
388
|
-
// Resolved once and shared below: the answer context needs the text and
|
|
389
|
-
// the message needs the images, and fetching each attachment twice is how
|
|
390
|
-
// a handful of large uploads used to reach the isolate's memory ceiling.
|
|
391
|
-
const uploads = await loadUploads(fileIds, r2Credentials);
|
|
461
|
+
const uploads = await uploadsPromise;
|
|
392
462
|
|
|
393
463
|
const sortedDocs = await rerankDocs(
|
|
394
464
|
query,
|
|
@@ -403,6 +473,18 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
403
473
|
console.log("[MetaSearchAgent] emitting sources:", sources.length);
|
|
404
474
|
emitter.emit("data", JSON.stringify({ type: "sources", data: sources }));
|
|
405
475
|
|
|
476
|
+
// The result list is on screen; only now spend time fetching pages, and
|
|
477
|
+
// only when the user asked for it. By default the answer is written from
|
|
478
|
+
// the result titles and snippets alone.
|
|
479
|
+
if (this.config.searchWeb && !fromLinks) {
|
|
480
|
+
await this.extractTopSources(
|
|
481
|
+
sortedDocs,
|
|
482
|
+
sourceExtractionEnabled,
|
|
483
|
+
thinkingTimeLimit,
|
|
484
|
+
emitter,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
406
488
|
const systemPrompt = interpolatePrompt(this.config.responsePrompt, {
|
|
407
489
|
systemInstructions,
|
|
408
490
|
context: processDocs(sortedDocs),
|
|
@@ -490,6 +572,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
490
572
|
sourceExtractionEnabled = false,
|
|
491
573
|
thinkingTimeLimit = 0,
|
|
492
574
|
queryExpansionPrompt?: string,
|
|
575
|
+
queryExpansionEnabled = false,
|
|
493
576
|
) {
|
|
494
577
|
const emitter = new EventEmitter();
|
|
495
578
|
|
|
@@ -508,6 +591,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|
|
508
591
|
sourceExtractionEnabled,
|
|
509
592
|
thinkingTimeLimit,
|
|
510
593
|
queryExpansionPrompt,
|
|
594
|
+
queryExpansionEnabled,
|
|
511
595
|
);
|
|
512
596
|
}, 0);
|
|
513
597
|
|