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,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Specialized tools for AI agents to interact with the QwkSearch API.
|
|
3
|
+
* Provides web search, content extraction, and AI response generation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// @ts-nocheck
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import * as QwkSearch from 'qwksearch-api-client'; // Update this path to match your QwkSearch module location
|
|
9
|
+
|
|
10
|
+
// Configuration for QwkSearch API
|
|
11
|
+
const QWKSEARCH_CONFIG = {
|
|
12
|
+
baseURL: typeof process !== "undefined" && process?.env.QWKSEARCH_URL || 'https://app.qwksearch.com/api',
|
|
13
|
+
apiKey: typeof process !== "undefined" && process?.env.QWKSEARCH_API_KEY || null,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List of tools available for agent usage, including their schemas and implementation.
|
|
18
|
+
*/
|
|
19
|
+
export const AGENT_TOOLS = [
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
name: "web_search",
|
|
23
|
+
description:
|
|
24
|
+
"Search the web for information on any topic using QwkSearch API. Input: search query string and optional category. Returns relevant search results with titles, descriptions, and URLs from 100+ sources via SearXNG metasearch engine.",
|
|
25
|
+
schema: z.object({
|
|
26
|
+
query: z.string(),
|
|
27
|
+
category: z.enum(["general", "news", "videos", "images", "science", "files", "it"]).optional().default("general"),
|
|
28
|
+
recency: z.enum(["none", "day", "week", "month", "year"]).optional().default("none"),
|
|
29
|
+
page: z.number().optional().default(1),
|
|
30
|
+
language: z.string().optional().default("en-US"),
|
|
31
|
+
public: z.boolean().optional().default(false),
|
|
32
|
+
timeout: z.number().optional().default(10),
|
|
33
|
+
baseURL: z.string().optional(),
|
|
34
|
+
apiKey: z.string().optional()
|
|
35
|
+
}),
|
|
36
|
+
func: async ({ query, category = "general", recency = "none", page = 1, language = "en-US", public: isPublic = false, timeout = 10, baseURL, apiKey }) => {
|
|
37
|
+
try {
|
|
38
|
+
// Use provided config or fall back to environment/defaults
|
|
39
|
+
const baseUrl = baseURL || QWKSEARCH_CONFIG.baseURL;
|
|
40
|
+
const headers = apiKey || QWKSEARCH_CONFIG.apiKey ? { 'x-api-key': apiKey || QWKSEARCH_CONFIG.apiKey } : undefined;
|
|
41
|
+
|
|
42
|
+
const result = await QwkSearch.searchWeb({
|
|
43
|
+
query: {
|
|
44
|
+
q: query,
|
|
45
|
+
cat: category,
|
|
46
|
+
recency: recency,
|
|
47
|
+
page: page,
|
|
48
|
+
lang: language,
|
|
49
|
+
public: isPublic,
|
|
50
|
+
timeout: timeout
|
|
51
|
+
},
|
|
52
|
+
baseUrl: baseUrl,
|
|
53
|
+
...(headers && { headers })
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!result.data || !result.data.results || result.data.results.length === 0) {
|
|
57
|
+
return `No search results found for "${query}". Please try a different search term.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let resultText = `Web search results for "${query}" (${category} category):\n\n`;
|
|
61
|
+
|
|
62
|
+
result.data.results.forEach((searchResult, index) => {
|
|
63
|
+
resultText += `${index + 1}. ${searchResult.title}\n`;
|
|
64
|
+
resultText += ` URL: ${searchResult.url}\n`;
|
|
65
|
+
if (searchResult.domain) {
|
|
66
|
+
resultText += ` Domain: ${searchResult.domain}\n`;
|
|
67
|
+
}
|
|
68
|
+
if (searchResult.snippet) {
|
|
69
|
+
resultText += ` Description: ${searchResult.snippet}\n`;
|
|
70
|
+
}
|
|
71
|
+
if (searchResult.engines && searchResult.engines.length > 0) {
|
|
72
|
+
resultText += ` Sources: ${searchResult.engines.join(", ")}\n`;
|
|
73
|
+
}
|
|
74
|
+
resultText += `\n`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
resultText += `Found ${result.data.results.length} results from multiple search engines. This is the complete search information.`;
|
|
78
|
+
|
|
79
|
+
return resultText;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return `Unable to perform web search for "${query}". Error: ${error.message}`;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "extract_page",
|
|
87
|
+
description:
|
|
88
|
+
"Extract and summarize content from a web page using QwkSearch API. Supports articles, PDFs, and YouTube videos. Uses Mozilla Readability and Postlight Mercury algorithms with 100+ custom adapters for major sites. Input: URL of the page to extract. Returns structured content with citation information.",
|
|
89
|
+
schema: z.object({
|
|
90
|
+
url: z.string().url(),
|
|
91
|
+
images: z.boolean().optional().default(true),
|
|
92
|
+
links: z.boolean().optional().default(true),
|
|
93
|
+
formatting: z.boolean().optional().default(true),
|
|
94
|
+
absoluteURLs: z.boolean().optional().default(true),
|
|
95
|
+
timeout: z.number().min(1).max(30).optional().default(10),
|
|
96
|
+
baseURL: z.string().optional(),
|
|
97
|
+
apiKey: z.string().optional()
|
|
98
|
+
}),
|
|
99
|
+
func: async ({ url, images = true, links = true, formatting = true, absoluteURLs = true, timeout = 10, baseURL, apiKey }) => {
|
|
100
|
+
try {
|
|
101
|
+
// Use provided config or fall back to environment/defaults
|
|
102
|
+
const baseUrl = baseURL || QWKSEARCH_CONFIG.baseURL;
|
|
103
|
+
const headers = apiKey || QWKSEARCH_CONFIG.apiKey ? { 'x-api-key': apiKey || QWKSEARCH_CONFIG.apiKey } : undefined;
|
|
104
|
+
|
|
105
|
+
const result = await QwkSearch.extractContent({
|
|
106
|
+
query: {
|
|
107
|
+
url: url,
|
|
108
|
+
images: images,
|
|
109
|
+
links: links,
|
|
110
|
+
formatting: formatting,
|
|
111
|
+
absoluteURLs: absoluteURLs,
|
|
112
|
+
timeout: timeout
|
|
113
|
+
},
|
|
114
|
+
baseUrl: baseUrl,
|
|
115
|
+
...(headers && { headers })
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!result.data) {
|
|
119
|
+
return `No content could be extracted from "${url}". Please check the URL and try again.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = result.data;
|
|
123
|
+
|
|
124
|
+
let resultText = `Content extracted from: ${data.url || url}\n\n`;
|
|
125
|
+
|
|
126
|
+
if (data.title) {
|
|
127
|
+
resultText += `Title: ${data.title}\n\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (data.author) {
|
|
131
|
+
resultText += `Author: ${data.author}\n`;
|
|
132
|
+
if (data.author_cite) {
|
|
133
|
+
resultText += `Author (Citation Format): ${data.author_cite}\n`;
|
|
134
|
+
}
|
|
135
|
+
if (data.author_type) {
|
|
136
|
+
resultText += `Author Type: ${data.author_type}\n`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (data.date) {
|
|
141
|
+
resultText += `Publication Date: ${data.date}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (data.source) {
|
|
145
|
+
resultText += `Source: ${data.source}\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (data.word_count) {
|
|
149
|
+
resultText += `Word Count: ${data.word_count}\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (data.cite) {
|
|
153
|
+
resultText += `\nCitation (APA Format): ${data.cite}\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (data.html) {
|
|
157
|
+
resultText += `\nContent:\n${data.html}\n\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resultText += `This is the complete page extraction information.`;
|
|
161
|
+
|
|
162
|
+
return resultText;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return `Unable to extract content from "${url}". Error: ${error.message}`;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "generate_ai_response",
|
|
170
|
+
description:
|
|
171
|
+
"Generate AI language model responses using QwkSearch API with various agent templates. Supports multiple providers (Groq, OpenAI, Anthropic, etc.) and agent types for different tasks like summarization, question answering, and content generation.",
|
|
172
|
+
schema: z.object({
|
|
173
|
+
provider: z.enum(["groq", "openai", "anthropic", "together", "xai", "google", "perplexity", "cloudflare"]),
|
|
174
|
+
key: z.string().optional(),
|
|
175
|
+
agent: z.enum([
|
|
176
|
+
"question",
|
|
177
|
+
"summarize-bullets",
|
|
178
|
+
"summarize",
|
|
179
|
+
"suggest-followups",
|
|
180
|
+
"answer-cite-sources",
|
|
181
|
+
"query-resolution",
|
|
182
|
+
"knowledge-graph-nodes",
|
|
183
|
+
"summary-longtext"
|
|
184
|
+
]).optional().default("question"),
|
|
185
|
+
model: z.string().optional().default("meta-llama/llama-4-maverick-17b-128e-instruct"),
|
|
186
|
+
temperature: z.number().min(0).max(2).optional().default(0.7),
|
|
187
|
+
html: z.boolean().optional().default(true),
|
|
188
|
+
query: z.string().optional(),
|
|
189
|
+
chat_history: z.string().optional(),
|
|
190
|
+
article: z.string().optional(),
|
|
191
|
+
baseURL: z.string().optional(),
|
|
192
|
+
apiKey: z.string().optional()
|
|
193
|
+
}),
|
|
194
|
+
func: async ({ provider, key, agent = "question", model = "meta-llama/llama-4-maverick-17b-128e-instruct", temperature = 0.7, html = true, query, chat_history, article, baseURL, apiKey }) => {
|
|
195
|
+
try {
|
|
196
|
+
// Use provided config or fall back to environment/defaults
|
|
197
|
+
const baseUrl = baseURL || QWKSEARCH_CONFIG.baseURL;
|
|
198
|
+
const headers = apiKey || QWKSEARCH_CONFIG.apiKey ? { 'x-api-key': apiKey || QWKSEARCH_CONFIG.apiKey } : undefined;
|
|
199
|
+
|
|
200
|
+
const requestBody = {
|
|
201
|
+
agent,
|
|
202
|
+
provider,
|
|
203
|
+
model,
|
|
204
|
+
html,
|
|
205
|
+
temperature
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (key) {
|
|
209
|
+
requestBody.key = key;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (query) {
|
|
213
|
+
requestBody.query = query;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (chat_history) {
|
|
217
|
+
requestBody.chat_history = chat_history;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (article) {
|
|
221
|
+
requestBody.article = article;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await QwkSearch.writeLanguage({
|
|
225
|
+
body: requestBody,
|
|
226
|
+
baseUrl: baseUrl,
|
|
227
|
+
...(headers && { headers })
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!result.data) {
|
|
231
|
+
return `No response generated. Please check your input parameters and try again.`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let resultText = `AI Response (${agent} agent, ${provider} provider):\n\n`;
|
|
235
|
+
|
|
236
|
+
if (result.data.content) {
|
|
237
|
+
resultText += result.data.content;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (result.data.extract) {
|
|
241
|
+
resultText += `\n\nStructured Extract:\n${JSON.stringify(result.data.extract, null, 2)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
resultText += `\n\nThis is the complete AI-generated response.`;
|
|
245
|
+
|
|
246
|
+
return resultText;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return `Unable to generate AI response. Error: ${error.message}`;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "render_page_with_javascript",
|
|
254
|
+
description:
|
|
255
|
+
"Render a web page with JavaScript execution using Cloudflare Browser Rendering. Use this for JavaScript-heavy sites (SPAs, React apps), pages behind bot protection (Cloudflare, reCAPTCHA), or when extract_page fails. Returns fully rendered HTML with JavaScript executed. Slower but more complete than extract_page.",
|
|
256
|
+
schema: z.object({
|
|
257
|
+
url: z.string().url(),
|
|
258
|
+
blockImages: z.boolean().optional().default(true),
|
|
259
|
+
wait: z.number().min(0).max(10000).optional().default(0),
|
|
260
|
+
timeout: z.number().min(1000).max(60000).optional().default(30000),
|
|
261
|
+
waitUntil: z.enum(["domcontentloaded", "load", "networkidle0", "networkidle2"]).optional().default("networkidle2"),
|
|
262
|
+
bypassCaptcha: z.boolean().optional().default(true),
|
|
263
|
+
sessionId: z.string().optional().default("default"),
|
|
264
|
+
format: z.enum(["html", "json"]).optional().default("html"),
|
|
265
|
+
scraperUrl: z.string().optional(),
|
|
266
|
+
scraperApiKey: z.string().optional()
|
|
267
|
+
}),
|
|
268
|
+
func: async ({ url, blockImages = true, wait = 0, timeout = 30000, waitUntil = "networkidle2", bypassCaptcha = true, sessionId = "default", format = "html", scraperUrl, scraperApiKey }) => {
|
|
269
|
+
try {
|
|
270
|
+
const scraperEndpoint = scraperUrl || (typeof process !== "undefined" && process?.env?.SCRAPER_URL) || 'https://scraper.qwksearch.workers.dev';
|
|
271
|
+
const apiKey = scraperApiKey || (typeof process !== "undefined" && process?.env?.SCRAPER_API_KEY);
|
|
272
|
+
|
|
273
|
+
const requestUrl = new URL('/api/render', scraperEndpoint);
|
|
274
|
+
|
|
275
|
+
const body = {
|
|
276
|
+
url,
|
|
277
|
+
blockImages,
|
|
278
|
+
wait,
|
|
279
|
+
timeout,
|
|
280
|
+
waitUntil,
|
|
281
|
+
bypassCaptcha,
|
|
282
|
+
sessionId,
|
|
283
|
+
format
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const headers: Record<string, string> = {
|
|
287
|
+
'Content-Type': 'application/json',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (apiKey) {
|
|
291
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const response = await fetch(requestUrl.toString(), {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers,
|
|
297
|
+
body: JSON.stringify(body),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const errorText = await response.text();
|
|
302
|
+
return `Failed to render page: ${errorText}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (format === 'json') {
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
let resultText = `Page rendered successfully: ${data.url}\n\n`;
|
|
308
|
+
if (data.title) {
|
|
309
|
+
resultText += `Title: ${data.title}\n`;
|
|
310
|
+
}
|
|
311
|
+
resultText += `Load Time: ${data.loadTime}ms\n`;
|
|
312
|
+
if (data.challengeBypassed) {
|
|
313
|
+
resultText += `Challenge Bypassed: Yes (${data.retryCount} retries)\n`;
|
|
314
|
+
}
|
|
315
|
+
resultText += `\nRendered HTML Content:\n${data.html}\n\n`;
|
|
316
|
+
resultText += `This is the complete rendered page content.`;
|
|
317
|
+
return resultText;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const html = await response.text();
|
|
321
|
+
return `Page rendered successfully.\n\nHTML Content:\n${html}`;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return `Unable to render page "${url}". Error: ${error.message}`;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/search/doc-utils
|
|
3
|
+
* @description Document utilities: fallback docs, reranking, and formatting.
|
|
4
|
+
*/
|
|
5
|
+
import type { Document } from "./document";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
export function buildFallbackDocs(query: string): Document[] {
|
|
10
|
+
const trimmedQuery = (query || "").trim();
|
|
11
|
+
const searchQuery = trimmedQuery.length > 0 ? trimmedQuery : "web search";
|
|
12
|
+
const fallbackUrl = `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}`;
|
|
13
|
+
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
pageContent:
|
|
17
|
+
"No indexed sources were returned by the configured search providers for this query.",
|
|
18
|
+
metadata: {
|
|
19
|
+
title: `Search results for: ${searchQuery}`,
|
|
20
|
+
url: fallbackUrl,
|
|
21
|
+
source: "Google Search",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeSourcesOutput(output: unknown, query: string): Document[] {
|
|
28
|
+
if (Array.isArray(output) && output.length > 0) {
|
|
29
|
+
return output as Document[];
|
|
30
|
+
}
|
|
31
|
+
return buildFallbackDocs(query);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function rerankDocs(
|
|
35
|
+
query: string,
|
|
36
|
+
docs: Document[],
|
|
37
|
+
fileIds: string[],
|
|
38
|
+
optimizationMode: "speed" | "balanced" | "quality",
|
|
39
|
+
): Promise<Document[]> {
|
|
40
|
+
if (docs.length === 0 && fileIds.length === 0) {
|
|
41
|
+
return docs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const filesData = fileIds.map((file) => {
|
|
45
|
+
const filePath = path.join(process.cwd(), "uploads", file);
|
|
46
|
+
const contentPath = filePath + "-extracted.json";
|
|
47
|
+
const content = JSON.parse(fs.readFileSync(contentPath, "utf8"));
|
|
48
|
+
return { fileName: content.title, content: content.content };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (query.toLocaleLowerCase() === "summarize") {
|
|
52
|
+
return docs.slice(0, 15);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const docsWithContent = docs.filter(
|
|
56
|
+
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const fileDocs: Document[] = filesData.map((fileData) => ({
|
|
60
|
+
pageContent: fileData.content,
|
|
61
|
+
metadata: { title: fileData.fileName, url: "File" },
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Combine file docs with web results, cap at 15
|
|
65
|
+
return [...fileDocs, ...docsWithContent].slice(0, 15);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function processDocs(docs: Document[]): string {
|
|
69
|
+
return docs
|
|
70
|
+
.map(
|
|
71
|
+
(_, index) =>
|
|
72
|
+
`${index + 1}. ${docs[index].metadata.title} ${docs[index].pageContent}`,
|
|
73
|
+
)
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/search/document
|
|
3
|
+
* @description Minimal document shape shared across the search pipeline.
|
|
4
|
+
* A document is a chunk of page content plus citation metadata (title, url, source, etc.).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface Document<
|
|
8
|
+
Metadata extends Record<string, any> = Record<string, any>,
|
|
9
|
+
> {
|
|
10
|
+
pageContent: string;
|
|
11
|
+
metadata: Metadata;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Splits text into overlapping chunks on whitespace boundaries.
|
|
16
|
+
* Default chunkSize 1000, chunkOverlap 200.
|
|
17
|
+
*/
|
|
18
|
+
export function splitTextIntoChunks(
|
|
19
|
+
text: string,
|
|
20
|
+
chunkSize = 1000,
|
|
21
|
+
chunkOverlap = 200,
|
|
22
|
+
): string[] {
|
|
23
|
+
const trimmed = (text || "").trim();
|
|
24
|
+
if (trimmed.length <= chunkSize) {
|
|
25
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const chunks: string[] = [];
|
|
29
|
+
let start = 0;
|
|
30
|
+
|
|
31
|
+
while (start < trimmed.length) {
|
|
32
|
+
let end = Math.min(start + chunkSize, trimmed.length);
|
|
33
|
+
|
|
34
|
+
// Break on the last whitespace inside the window so words stay intact
|
|
35
|
+
if (end < trimmed.length) {
|
|
36
|
+
const lastSpace = trimmed.lastIndexOf(" ", end);
|
|
37
|
+
if (lastSpace > start) end = lastSpace;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
chunks.push(trimmed.slice(start, end).trim());
|
|
41
|
+
|
|
42
|
+
if (end >= trimmed.length) break;
|
|
43
|
+
start = Math.max(end - chunkOverlap, start + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module agent-toolkit/tools/search
|
|
3
|
+
* @description Meta search agent tools for AI-powered web search and research
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { default as MetaSearchAgent } from "./metaSearchAgent";
|
|
7
|
+
export type { MetaSearchAgentType, Config, ChatTurnMessage, SearchingEvent, FewShotExample } from "./meta-search-types";
|
|
8
|
+
export { searchHandlers, createSearchHandlers } from "./search-handlers";
|
|
9
|
+
export { default as generateSuggestions } from "./suggestionGeneratorAgent";
|
|
10
|
+
export { groupAndSummarizeDocs } from "./link-summarizer";
|
|
11
|
+
export type { Document } from "./document";
|
|
12
|
+
export { splitTextIntoChunks } from "./document";
|
|
13
|
+
export { buildFallbackDocs, rerankDocs, processDocs, normalizeSourcesOutput } from "./doc-utils";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/search/link-summarizer
|
|
3
|
+
* @description Groups link documents by URL then summarizes each group with an LLM.
|
|
4
|
+
*/
|
|
5
|
+
import { generateText, type LanguageModel } from "ai";
|
|
6
|
+
import type { Document } from "./document";
|
|
7
|
+
import { buildFallbackDocs } from "./doc-utils";
|
|
8
|
+
|
|
9
|
+
const SUMMARIZER_PROMPT = `You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
|
|
10
|
+
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
|
|
11
|
+
If the query is "summarize", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
|
|
12
|
+
|
|
13
|
+
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
|
|
14
|
+
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
|
|
15
|
+
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
|
|
16
|
+
|
|
17
|
+
The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag.
|
|
18
|
+
|
|
19
|
+
<example>
|
|
20
|
+
1. \`<text>
|
|
21
|
+
Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
|
|
22
|
+
It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
|
|
23
|
+
by using containers.
|
|
24
|
+
</text>
|
|
25
|
+
|
|
26
|
+
<query>
|
|
27
|
+
What is Docker and how does it work?
|
|
28
|
+
</query>
|
|
29
|
+
|
|
30
|
+
Response:
|
|
31
|
+
Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
|
|
32
|
+
deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
|
|
33
|
+
any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
|
|
34
|
+
\`
|
|
35
|
+
2. \`<text>
|
|
36
|
+
The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general
|
|
37
|
+
relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based
|
|
38
|
+
on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by
|
|
39
|
+
Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity.
|
|
40
|
+
General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical
|
|
41
|
+
realm, including astronomy.
|
|
42
|
+
</text>
|
|
43
|
+
|
|
44
|
+
<query>
|
|
45
|
+
summarize
|
|
46
|
+
</query>
|
|
47
|
+
|
|
48
|
+
Response:
|
|
49
|
+
The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special
|
|
50
|
+
relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its
|
|
51
|
+
relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in
|
|
52
|
+
1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe.
|
|
53
|
+
\`
|
|
54
|
+
</example>
|
|
55
|
+
|
|
56
|
+
Everything below is the actual data you will be working with. Good luck!
|
|
57
|
+
|
|
58
|
+
<query>
|
|
59
|
+
{question}
|
|
60
|
+
</query>
|
|
61
|
+
|
|
62
|
+
<text>
|
|
63
|
+
{content}
|
|
64
|
+
</text>
|
|
65
|
+
|
|
66
|
+
Make sure to answer the query in the summary.`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Groups fetched link documents by URL (max 10 chunks per URL), then
|
|
70
|
+
* summarizes each group with the LLM in parallel.
|
|
71
|
+
*/
|
|
72
|
+
export async function groupAndSummarizeDocs(
|
|
73
|
+
llm: LanguageModel,
|
|
74
|
+
linkDocs: Document[],
|
|
75
|
+
question: string,
|
|
76
|
+
): Promise<Document[]> {
|
|
77
|
+
// Group chunks by URL, capping at 10 chunks each
|
|
78
|
+
const docGroups: Document[] = [];
|
|
79
|
+
|
|
80
|
+
for (const doc of linkDocs) {
|
|
81
|
+
const existing = docGroups.find(
|
|
82
|
+
(d) => d.metadata.url === doc.metadata.url && d.metadata.totalDocs < 10,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!existing) {
|
|
86
|
+
docGroups.push({ ...doc, metadata: { ...doc.metadata, totalDocs: 1 } });
|
|
87
|
+
} else {
|
|
88
|
+
existing.pageContent += `\n\n${doc.pageContent}`;
|
|
89
|
+
existing.metadata.totalDocs += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Summarize each group in parallel
|
|
94
|
+
const docs: Document[] = [];
|
|
95
|
+
|
|
96
|
+
await Promise.all(
|
|
97
|
+
docGroups.map(async (doc) => {
|
|
98
|
+
const prompt = SUMMARIZER_PROMPT.replace("{question}", question).replace(
|
|
99
|
+
"{content}",
|
|
100
|
+
doc.pageContent,
|
|
101
|
+
);
|
|
102
|
+
const { text } = await generateText({ model: llm, prompt });
|
|
103
|
+
|
|
104
|
+
docs.push({
|
|
105
|
+
pageContent: text,
|
|
106
|
+
metadata: { title: doc.metadata.title, url: doc.metadata.url },
|
|
107
|
+
});
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return docs.length > 0 ? docs : buildFallbackDocs(question);
|
|
112
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module research/search/meta-search-types
|
|
3
|
+
* @description Shared types for the MetaSearchAgent.
|
|
4
|
+
*/
|
|
5
|
+
import type { LanguageModel } from "ai";
|
|
6
|
+
import type EventEmitter from "events";
|
|
7
|
+
|
|
8
|
+
/** A single conversation turn, matching the Vercel AI SDK message shape. */
|
|
9
|
+
export interface ChatTurnMessage {
|
|
10
|
+
role: "user" | "assistant" | "system";
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A `[role, content]` tuple used for few-shot prompt examples. */
|
|
15
|
+
export type FewShotExample = [role: "user" | "assistant", content: string];
|
|
16
|
+
|
|
17
|
+
export interface MetaSearchAgentType {
|
|
18
|
+
searchAndAnswer: (
|
|
19
|
+
message: string,
|
|
20
|
+
history: ChatTurnMessage[],
|
|
21
|
+
llm: LanguageModel,
|
|
22
|
+
optimizationMode: "speed" | "balanced" | "quality",
|
|
23
|
+
fileIds: string[],
|
|
24
|
+
systemInstructions: string,
|
|
25
|
+
category?: string,
|
|
26
|
+
sourceExtractionEnabled?: boolean,
|
|
27
|
+
thinkingTimeLimit?: number,
|
|
28
|
+
) => Promise<EventEmitter>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Emitted on the EventEmitter data channel to report live search progress. */
|
|
32
|
+
export interface SearchingEvent {
|
|
33
|
+
query: string;
|
|
34
|
+
/** Display label shown in the UI (e.g. "Academic · max 10"). */
|
|
35
|
+
category?: string;
|
|
36
|
+
status: "running" | "done";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Config {
|
|
40
|
+
searchWeb: boolean;
|
|
41
|
+
rerank: boolean;
|
|
42
|
+
rerankThreshold: number;
|
|
43
|
+
queryGeneratorPrompt: string;
|
|
44
|
+
queryGeneratorFewShots: FewShotExample[];
|
|
45
|
+
responsePrompt: string;
|
|
46
|
+
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>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type BasicChainInput = {
|
|
60
|
+
chat_history: ChatTurnMessage[];
|
|
61
|
+
query: string;
|
|
62
|
+
};
|