@framers/agentos-ext-web-search 1.1.0 → 1.2.0
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 +31 -188
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -50
- package/dist/index.js.map +1 -1
- package/dist/services/searchProvider.d.ts +89 -3
- package/dist/services/searchProvider.d.ts.map +1 -1
- package/dist/services/searchProvider.js +342 -26
- package/dist/services/searchProvider.js.map +1 -1
- package/dist/tools/factCheck.d.ts +46 -88
- package/dist/tools/factCheck.d.ts.map +1 -1
- package/dist/tools/factCheck.js +141 -170
- package/dist/tools/factCheck.js.map +1 -1
- package/dist/tools/researchAggregator.d.ts +37 -91
- package/dist/tools/researchAggregator.d.ts.map +1 -1
- package/dist/tools/researchAggregator.js +141 -175
- package/dist/tools/researchAggregator.js.map +1 -1
- package/dist/tools/webSearch.d.ts +31 -59
- package/dist/tools/webSearch.d.ts.map +1 -1
- package/dist/tools/webSearch.js +76 -87
- package/dist/tools/webSearch.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/manifest.json +14 -9
- package/package.json +26 -16
- package/.eslintrc.json +0 -21
- package/CHANGELOG.md +0 -16
- package/src/index.ts +0 -123
- package/src/services/searchProvider.ts +0 -416
- package/src/tools/factCheck.ts +0 -318
- package/src/tools/researchAggregator.ts +0 -297
- package/src/tools/webSearch.ts +0 -128
- package/src/types.ts +0 -41
- package/test/integration.spec.ts +0 -173
- package/test/searchProvider.spec.ts +0 -153
- package/test/webSearch.spec.ts +0 -128
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -32
package/src/tools/factCheck.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { ITool } from '@framers/agentos';
|
|
2
|
-
import { SearchProviderService } from '../services/searchProvider';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fact checking tool that verifies statements against web sources
|
|
6
|
-
*
|
|
7
|
-
* @class FactCheckTool
|
|
8
|
-
* @implements {ITool}
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* const tool = new FactCheckTool(searchService);
|
|
13
|
-
* const result = await tool.execute({
|
|
14
|
-
* statement: 'The Earth orbits the Sun',
|
|
15
|
-
* checkSources: true,
|
|
16
|
-
* confidence: 'high'
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export class FactCheckTool implements ITool {
|
|
21
|
-
public readonly id = 'factCheck';
|
|
22
|
-
public readonly name = 'Fact Check';
|
|
23
|
-
public readonly description = 'Verify facts and statements against web sources';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates an instance of FactCheckTool
|
|
27
|
-
* @param {SearchProviderService} searchService - The search provider service
|
|
28
|
-
*/
|
|
29
|
-
constructor(private searchService: SearchProviderService) {}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Executes fact checking on a statement
|
|
33
|
-
*
|
|
34
|
-
* @param {Object} input - The fact check parameters
|
|
35
|
-
* @param {string} input.statement - The statement to fact-check
|
|
36
|
-
* @param {boolean} [input.checkSources=true] - Whether to include source citations
|
|
37
|
-
* @param {string} [input.confidence='medium'] - Required confidence level
|
|
38
|
-
* @returns {Promise<{success: boolean, output?: any, error?: string}>} Fact check results
|
|
39
|
-
*/
|
|
40
|
-
async execute(input: {
|
|
41
|
-
statement: string;
|
|
42
|
-
checkSources?: boolean;
|
|
43
|
-
confidence?: 'low' | 'medium' | 'high';
|
|
44
|
-
}): Promise<{ success: boolean; output?: any; error?: string }> {
|
|
45
|
-
try {
|
|
46
|
-
const checkSources = input.checkSources !== false;
|
|
47
|
-
const requiredConfidence = input.confidence || 'medium';
|
|
48
|
-
|
|
49
|
-
// Search for information about the statement
|
|
50
|
-
const queries = this.generateFactCheckQueries(input.statement);
|
|
51
|
-
|
|
52
|
-
// Perform searches in parallel
|
|
53
|
-
const searchPromises = queries.map(query =>
|
|
54
|
-
this.searchService.search(query, { maxResults: 5 })
|
|
55
|
-
.catch(err => ({
|
|
56
|
-
provider: 'error',
|
|
57
|
-
results: [],
|
|
58
|
-
metadata: { query, error: err.message, timestamp: new Date().toISOString() }
|
|
59
|
-
}))
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const searchResults = await Promise.all(searchPromises);
|
|
63
|
-
|
|
64
|
-
// Analyze results for fact verification
|
|
65
|
-
const analysis = this.analyzeFactCheckResults(
|
|
66
|
-
input.statement,
|
|
67
|
-
searchResults,
|
|
68
|
-
requiredConfidence
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Compile sources if requested
|
|
72
|
-
const sources = checkSources ? this.compileSources(searchResults) : [];
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
success: true,
|
|
76
|
-
output: {
|
|
77
|
-
statement: input.statement,
|
|
78
|
-
verdict: analysis.verdict,
|
|
79
|
-
confidence: analysis.confidence,
|
|
80
|
-
explanation: analysis.explanation,
|
|
81
|
-
sources: sources,
|
|
82
|
-
contradictingSources: analysis.contradictingSources,
|
|
83
|
-
supportingSources: analysis.supportingSources,
|
|
84
|
-
timestamp: new Date().toISOString()
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
} catch (error: any) {
|
|
88
|
-
return {
|
|
89
|
-
success: false,
|
|
90
|
-
error: error.message
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Generates queries for fact-checking a statement
|
|
97
|
-
*
|
|
98
|
-
* @private
|
|
99
|
-
* @param {string} statement - The statement to check
|
|
100
|
-
* @returns {string[]} Array of search queries
|
|
101
|
-
*/
|
|
102
|
-
private generateFactCheckQueries(statement: string): string[] {
|
|
103
|
-
return [
|
|
104
|
-
statement, // Original statement
|
|
105
|
-
`"${statement}" fact check`, // Exact match with fact check
|
|
106
|
-
`${statement} true or false`, // Truth verification
|
|
107
|
-
`${statement} debunked myths`, // Check for debunking
|
|
108
|
-
`is it true that ${statement}` // Question format
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Analyzes search results to determine fact validity
|
|
114
|
-
*
|
|
115
|
-
* @private
|
|
116
|
-
* @param {string} statement - Original statement
|
|
117
|
-
* @param {any[]} searchResults - Search results to analyze
|
|
118
|
-
* @param {string} requiredConfidence - Required confidence level
|
|
119
|
-
* @returns {Object} Analysis results with verdict and confidence
|
|
120
|
-
*/
|
|
121
|
-
private analyzeFactCheckResults(
|
|
122
|
-
statement: string,
|
|
123
|
-
searchResults: any[],
|
|
124
|
-
requiredConfidence: string
|
|
125
|
-
): any {
|
|
126
|
-
let supportingCount = 0;
|
|
127
|
-
let contradictingCount = 0;
|
|
128
|
-
let neutralCount = 0;
|
|
129
|
-
const supportingSources: string[] = [];
|
|
130
|
-
const contradictingSources: string[] = [];
|
|
131
|
-
|
|
132
|
-
const statementLower = statement.toLowerCase();
|
|
133
|
-
const negativeIndicators = ['false', 'myth', 'debunked', 'incorrect', 'wrong', 'not true', 'fake'];
|
|
134
|
-
const positiveIndicators = ['true', 'correct', 'accurate', 'confirmed', 'verified', 'fact'];
|
|
135
|
-
|
|
136
|
-
for (const searchResult of searchResults) {
|
|
137
|
-
if (!searchResult.results) continue;
|
|
138
|
-
|
|
139
|
-
for (const result of searchResult.results) {
|
|
140
|
-
const contentLower = (result.title + ' ' + result.snippet).toLowerCase();
|
|
141
|
-
|
|
142
|
-
// Check for fact-checking sites
|
|
143
|
-
const isFactCheckSite = result.url.includes('snopes') ||
|
|
144
|
-
result.url.includes('factcheck') ||
|
|
145
|
-
result.url.includes('politifact');
|
|
146
|
-
|
|
147
|
-
const weight = isFactCheckSite ? 2 : 1; // Give more weight to fact-checking sites
|
|
148
|
-
|
|
149
|
-
// Analyze sentiment toward the statement
|
|
150
|
-
let hasNegative = negativeIndicators.some(ind => contentLower.includes(ind));
|
|
151
|
-
let hasPositive = positiveIndicators.some(ind => contentLower.includes(ind));
|
|
152
|
-
|
|
153
|
-
if (hasNegative && !hasPositive) {
|
|
154
|
-
contradictingCount += weight;
|
|
155
|
-
contradictingSources.push(result.url);
|
|
156
|
-
} else if (hasPositive && !hasNegative) {
|
|
157
|
-
supportingCount += weight;
|
|
158
|
-
supportingSources.push(result.url);
|
|
159
|
-
} else {
|
|
160
|
-
neutralCount += 1;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Calculate confidence and verdict
|
|
166
|
-
const total = supportingCount + contradictingCount + neutralCount;
|
|
167
|
-
let confidence: number;
|
|
168
|
-
let verdict: 'TRUE' | 'FALSE' | 'UNVERIFIED' | 'PARTIALLY TRUE';
|
|
169
|
-
let explanation: string;
|
|
170
|
-
|
|
171
|
-
if (total === 0) {
|
|
172
|
-
verdict = 'UNVERIFIED';
|
|
173
|
-
confidence = 0;
|
|
174
|
-
explanation = 'No relevant sources found to verify this statement.';
|
|
175
|
-
} else {
|
|
176
|
-
const supportRatio = supportingCount / total;
|
|
177
|
-
const contradictRatio = contradictingCount / total;
|
|
178
|
-
|
|
179
|
-
if (supportRatio > 0.7) {
|
|
180
|
-
verdict = 'TRUE';
|
|
181
|
-
confidence = supportRatio * 100;
|
|
182
|
-
explanation = `Strong evidence supports this statement (${supportingCount} supporting sources).`;
|
|
183
|
-
} else if (contradictRatio > 0.7) {
|
|
184
|
-
verdict = 'FALSE';
|
|
185
|
-
confidence = contradictRatio * 100;
|
|
186
|
-
explanation = `Strong evidence contradicts this statement (${contradictingCount} contradicting sources).`;
|
|
187
|
-
} else if (supportRatio > 0.4 && contradictRatio > 0.4) {
|
|
188
|
-
verdict = 'PARTIALLY TRUE';
|
|
189
|
-
confidence = 50;
|
|
190
|
-
explanation = `Mixed evidence with both supporting and contradicting sources.`;
|
|
191
|
-
} else {
|
|
192
|
-
verdict = 'UNVERIFIED';
|
|
193
|
-
confidence = Math.max(supportRatio, contradictRatio) * 100;
|
|
194
|
-
explanation = `Insufficient clear evidence to verify this statement.`;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Adjust confidence based on required level
|
|
199
|
-
const confidenceLevel = confidence > 80 ? 'high' : confidence > 50 ? 'medium' : 'low';
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
verdict,
|
|
203
|
-
confidence: Math.round(confidence),
|
|
204
|
-
confidenceLevel,
|
|
205
|
-
explanation,
|
|
206
|
-
supportingSources: supportingSources.slice(0, 5),
|
|
207
|
-
contradictingSources: contradictingSources.slice(0, 5),
|
|
208
|
-
meetsRequiredConfidence: this.meetsConfidenceRequirement(confidenceLevel, requiredConfidence)
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Checks if confidence level meets requirement
|
|
214
|
-
*
|
|
215
|
-
* @private
|
|
216
|
-
* @param {string} actual - Actual confidence level
|
|
217
|
-
* @param {string} required - Required confidence level
|
|
218
|
-
* @returns {boolean} True if requirement is met
|
|
219
|
-
*/
|
|
220
|
-
private meetsConfidenceRequirement(actual: string, required: string): boolean {
|
|
221
|
-
const levels = { low: 1, medium: 2, high: 3 };
|
|
222
|
-
return levels[actual as keyof typeof levels] >= levels[required as keyof typeof levels];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Compiles relevant sources from search results
|
|
227
|
-
*
|
|
228
|
-
* @private
|
|
229
|
-
* @param {any[]} searchResults - Search results to compile from
|
|
230
|
-
* @returns {any[]} Compiled sources with metadata
|
|
231
|
-
*/
|
|
232
|
-
private compileSources(searchResults: any[]): any[] {
|
|
233
|
-
const sources: any[] = [];
|
|
234
|
-
const seenUrls = new Set<string>();
|
|
235
|
-
|
|
236
|
-
for (const searchResult of searchResults) {
|
|
237
|
-
if (!searchResult.results) continue;
|
|
238
|
-
|
|
239
|
-
for (const result of searchResult.results) {
|
|
240
|
-
if (seenUrls.has(result.url)) continue;
|
|
241
|
-
seenUrls.add(result.url);
|
|
242
|
-
|
|
243
|
-
sources.push({
|
|
244
|
-
title: result.title,
|
|
245
|
-
url: result.url,
|
|
246
|
-
snippet: result.snippet,
|
|
247
|
-
provider: searchResult.provider,
|
|
248
|
-
query: searchResult.metadata.query
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return sources.slice(0, 10); // Limit to top 10 sources
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Validates input parameters
|
|
258
|
-
*
|
|
259
|
-
* @param {any} input - Input to validate
|
|
260
|
-
* @returns {{valid: boolean, errors: string[]}} Validation result
|
|
261
|
-
*/
|
|
262
|
-
validate(input: any): { valid: boolean; errors: string[] } {
|
|
263
|
-
const errors: string[] = [];
|
|
264
|
-
|
|
265
|
-
if (!input.statement) {
|
|
266
|
-
errors.push('Statement is required');
|
|
267
|
-
} else if (typeof input.statement !== 'string') {
|
|
268
|
-
errors.push('Statement must be a string');
|
|
269
|
-
} else if (input.statement.length < 5) {
|
|
270
|
-
errors.push('Statement must be at least 5 characters long');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (input.confidence !== undefined) {
|
|
274
|
-
if (!['low', 'medium', 'high'].includes(input.confidence)) {
|
|
275
|
-
errors.push('Confidence must be low, medium, or high');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (input.checkSources !== undefined && typeof input.checkSources !== 'boolean') {
|
|
280
|
-
errors.push('checkSources must be a boolean');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
valid: errors.length === 0,
|
|
285
|
-
errors
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Returns the JSON schema for the tool's input
|
|
291
|
-
*
|
|
292
|
-
* @returns {Object} JSON Schema object
|
|
293
|
-
*/
|
|
294
|
-
getSchema(): any {
|
|
295
|
-
return {
|
|
296
|
-
type: 'object',
|
|
297
|
-
required: ['statement'],
|
|
298
|
-
properties: {
|
|
299
|
-
statement: {
|
|
300
|
-
type: 'string',
|
|
301
|
-
description: 'The statement to fact-check',
|
|
302
|
-
minLength: 5
|
|
303
|
-
},
|
|
304
|
-
checkSources: {
|
|
305
|
-
type: 'boolean',
|
|
306
|
-
description: 'Whether to include source citations',
|
|
307
|
-
default: true
|
|
308
|
-
},
|
|
309
|
-
confidence: {
|
|
310
|
-
type: 'string',
|
|
311
|
-
description: 'Required confidence level for the verdict',
|
|
312
|
-
enum: ['low', 'medium', 'high'],
|
|
313
|
-
default: 'medium'
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
}
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { ITool } from '@framers/agentos';
|
|
2
|
-
import { SearchProviderService } from '../services/searchProvider';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Research aggregator tool that performs multiple searches and synthesizes results
|
|
6
|
-
*
|
|
7
|
-
* @class ResearchAggregatorTool
|
|
8
|
-
* @implements {ITool}
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* const tool = new ResearchAggregatorTool(searchService);
|
|
13
|
-
* const result = await tool.execute({
|
|
14
|
-
* topic: 'AI safety research',
|
|
15
|
-
* sources: 3,
|
|
16
|
-
* depth: 'comprehensive'
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export class ResearchAggregatorTool implements ITool {
|
|
21
|
-
public readonly id = 'researchAggregator';
|
|
22
|
-
public readonly name = 'Research Aggregator';
|
|
23
|
-
public readonly description = 'Aggregate research from multiple searches on a topic';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates an instance of ResearchAggregatorTool
|
|
27
|
-
* @param {SearchProviderService} searchService - The search provider service
|
|
28
|
-
*/
|
|
29
|
-
constructor(private searchService: SearchProviderService) {}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Executes research aggregation on a topic
|
|
33
|
-
*
|
|
34
|
-
* @param {Object} input - The research parameters
|
|
35
|
-
* @param {string} input.topic - The research topic
|
|
36
|
-
* @param {number} [input.sources=3] - Number of different search angles to explore
|
|
37
|
-
* @param {string} [input.depth='moderate'] - Research depth (quick, moderate, comprehensive)
|
|
38
|
-
* @returns {Promise<{success: boolean, output?: any, error?: string}>} Aggregated research results
|
|
39
|
-
*/
|
|
40
|
-
async execute(input: {
|
|
41
|
-
topic: string;
|
|
42
|
-
sources?: number;
|
|
43
|
-
depth?: 'quick' | 'moderate' | 'comprehensive';
|
|
44
|
-
}): Promise<{ success: boolean; output?: any; error?: string }> {
|
|
45
|
-
try {
|
|
46
|
-
const sources = input.sources || 3;
|
|
47
|
-
const depth = input.depth || 'moderate';
|
|
48
|
-
|
|
49
|
-
// Generate different search queries for comprehensive coverage
|
|
50
|
-
const queries = this.generateSearchQueries(input.topic, sources, depth);
|
|
51
|
-
|
|
52
|
-
// Perform searches in parallel
|
|
53
|
-
const searchPromises = queries.map(query =>
|
|
54
|
-
this.searchService.search(query, {
|
|
55
|
-
maxResults: depth === 'quick' ? 5 : depth === 'moderate' ? 10 : 15
|
|
56
|
-
}).catch(err => ({
|
|
57
|
-
provider: 'error',
|
|
58
|
-
results: [],
|
|
59
|
-
metadata: { query, error: err.message, timestamp: new Date().toISOString() }
|
|
60
|
-
}))
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const searchResults = await Promise.all(searchPromises);
|
|
64
|
-
|
|
65
|
-
// Aggregate and deduplicate results
|
|
66
|
-
const aggregated = this.aggregateResults(searchResults, input.topic);
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
success: true,
|
|
70
|
-
output: {
|
|
71
|
-
topic: input.topic,
|
|
72
|
-
sources: sources,
|
|
73
|
-
depth: depth,
|
|
74
|
-
queries: queries,
|
|
75
|
-
aggregatedResults: aggregated,
|
|
76
|
-
timestamp: new Date().toISOString()
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
} catch (error: any) {
|
|
80
|
-
return {
|
|
81
|
-
success: false,
|
|
82
|
-
error: error.message
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Generates diverse search queries for comprehensive research
|
|
89
|
-
*
|
|
90
|
-
* @private
|
|
91
|
-
* @param {string} topic - The research topic
|
|
92
|
-
* @param {number} count - Number of queries to generate
|
|
93
|
-
* @param {string} depth - Research depth level
|
|
94
|
-
* @returns {string[]} Array of search queries
|
|
95
|
-
*/
|
|
96
|
-
private generateSearchQueries(topic: string, count: number, depth: string): string[] {
|
|
97
|
-
const queries: string[] = [topic]; // Start with the original topic
|
|
98
|
-
|
|
99
|
-
const modifiers = {
|
|
100
|
-
quick: ['overview', 'summary'],
|
|
101
|
-
moderate: ['overview', 'recent developments', 'key concepts', 'applications'],
|
|
102
|
-
comprehensive: [
|
|
103
|
-
'overview',
|
|
104
|
-
'recent research',
|
|
105
|
-
'state of the art',
|
|
106
|
-
'challenges',
|
|
107
|
-
'future directions',
|
|
108
|
-
'industry applications',
|
|
109
|
-
'academic papers',
|
|
110
|
-
'expert opinions'
|
|
111
|
-
]
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const selectedModifiers = modifiers[depth as keyof typeof modifiers];
|
|
115
|
-
|
|
116
|
-
for (let i = 1; i < Math.min(count, selectedModifiers.length + 1); i++) {
|
|
117
|
-
if (selectedModifiers[i - 1]) {
|
|
118
|
-
queries.push(`${topic} ${selectedModifiers[i - 1]}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return queries;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Aggregates and deduplicates search results
|
|
127
|
-
*
|
|
128
|
-
* @private
|
|
129
|
-
* @param {any[]} searchResults - Array of search results from different queries
|
|
130
|
-
* @param {string} topic - The original research topic
|
|
131
|
-
* @returns {Object} Aggregated and organized results
|
|
132
|
-
*/
|
|
133
|
-
private aggregateResults(searchResults: any[], topic: string): any {
|
|
134
|
-
const seenUrls = new Set<string>();
|
|
135
|
-
const categories = {
|
|
136
|
-
overview: [] as any[],
|
|
137
|
-
research: [] as any[],
|
|
138
|
-
applications: [] as any[],
|
|
139
|
-
resources: [] as any[]
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
for (const searchResult of searchResults) {
|
|
143
|
-
if (!searchResult.results) continue;
|
|
144
|
-
|
|
145
|
-
for (const result of searchResult.results) {
|
|
146
|
-
// Skip duplicates
|
|
147
|
-
if (seenUrls.has(result.url)) continue;
|
|
148
|
-
seenUrls.add(result.url);
|
|
149
|
-
|
|
150
|
-
// Categorize based on content
|
|
151
|
-
const lowerTitle = result.title.toLowerCase();
|
|
152
|
-
const lowerSnippet = result.snippet.toLowerCase();
|
|
153
|
-
|
|
154
|
-
if (lowerTitle.includes('overview') || lowerTitle.includes('introduction') ||
|
|
155
|
-
lowerTitle.includes('what is')) {
|
|
156
|
-
categories.overview.push(result);
|
|
157
|
-
} else if (lowerTitle.includes('research') || lowerTitle.includes('study') ||
|
|
158
|
-
lowerTitle.includes('paper') || lowerTitle.includes('journal')) {
|
|
159
|
-
categories.research.push(result);
|
|
160
|
-
} else if (lowerTitle.includes('application') || lowerTitle.includes('use case') ||
|
|
161
|
-
lowerTitle.includes('implementation')) {
|
|
162
|
-
categories.applications.push(result);
|
|
163
|
-
} else {
|
|
164
|
-
categories.resources.push(result);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Calculate relevance scores
|
|
170
|
-
const scoredCategories = Object.entries(categories).reduce((acc, [key, items]) => {
|
|
171
|
-
acc[key] = items.map((item: any) => ({
|
|
172
|
-
...item,
|
|
173
|
-
relevanceScore: this.calculateRelevance(item, topic)
|
|
174
|
-
})).sort((a: any, b: any) => b.relevanceScore - a.relevanceScore);
|
|
175
|
-
return acc;
|
|
176
|
-
}, {} as any);
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
categories: scoredCategories,
|
|
180
|
-
totalResults: seenUrls.size,
|
|
181
|
-
topResults: this.getTopResults(scoredCategories, 10)
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Calculates relevance score for a search result
|
|
187
|
-
*
|
|
188
|
-
* @private
|
|
189
|
-
* @param {any} result - Search result object
|
|
190
|
-
* @param {string} topic - Original topic for comparison
|
|
191
|
-
* @returns {number} Relevance score (0-100)
|
|
192
|
-
*/
|
|
193
|
-
private calculateRelevance(result: any, topic: string): number {
|
|
194
|
-
let score = 50; // Base score
|
|
195
|
-
|
|
196
|
-
const topicWords = topic.toLowerCase().split(/\s+/);
|
|
197
|
-
const titleWords = result.title.toLowerCase();
|
|
198
|
-
const snippetWords = result.snippet.toLowerCase();
|
|
199
|
-
|
|
200
|
-
// Check title relevance
|
|
201
|
-
for (const word of topicWords) {
|
|
202
|
-
if (titleWords.includes(word)) score += 15;
|
|
203
|
-
if (snippetWords.includes(word)) score += 5;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Boost for certain indicators
|
|
207
|
-
if (titleWords.includes('official')) score += 10;
|
|
208
|
-
if (titleWords.includes('guide')) score += 5;
|
|
209
|
-
if (titleWords.includes('tutorial')) score += 5;
|
|
210
|
-
|
|
211
|
-
return Math.min(100, score);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Gets top results across all categories
|
|
216
|
-
*
|
|
217
|
-
* @private
|
|
218
|
-
* @param {any} categories - Categorized results
|
|
219
|
-
* @param {number} count - Number of top results to return
|
|
220
|
-
* @returns {any[]} Top results by relevance
|
|
221
|
-
*/
|
|
222
|
-
private getTopResults(categories: any, count: number): any[] {
|
|
223
|
-
const allResults: any[] = [];
|
|
224
|
-
|
|
225
|
-
for (const items of Object.values(categories)) {
|
|
226
|
-
allResults.push(...(items as any[]));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return allResults
|
|
230
|
-
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
231
|
-
.slice(0, count);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Validates input parameters
|
|
236
|
-
*
|
|
237
|
-
* @param {any} input - Input to validate
|
|
238
|
-
* @returns {{valid: boolean, errors: string[]}} Validation result
|
|
239
|
-
*/
|
|
240
|
-
validate(input: any): { valid: boolean; errors: string[] } {
|
|
241
|
-
const errors: string[] = [];
|
|
242
|
-
|
|
243
|
-
if (!input.topic) {
|
|
244
|
-
errors.push('Topic is required');
|
|
245
|
-
} else if (typeof input.topic !== 'string') {
|
|
246
|
-
errors.push('Topic must be a string');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (input.sources !== undefined) {
|
|
250
|
-
if (typeof input.sources !== 'number' || input.sources < 1 || input.sources > 10) {
|
|
251
|
-
errors.push('Sources must be a number between 1 and 10');
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (input.depth !== undefined) {
|
|
256
|
-
if (!['quick', 'moderate', 'comprehensive'].includes(input.depth)) {
|
|
257
|
-
errors.push('Depth must be quick, moderate, or comprehensive');
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
valid: errors.length === 0,
|
|
263
|
-
errors
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Returns the JSON schema for the tool's input
|
|
269
|
-
*
|
|
270
|
-
* @returns {Object} JSON Schema object
|
|
271
|
-
*/
|
|
272
|
-
getSchema(): any {
|
|
273
|
-
return {
|
|
274
|
-
type: 'object',
|
|
275
|
-
required: ['topic'],
|
|
276
|
-
properties: {
|
|
277
|
-
topic: {
|
|
278
|
-
type: 'string',
|
|
279
|
-
description: 'The research topic to aggregate information about'
|
|
280
|
-
},
|
|
281
|
-
sources: {
|
|
282
|
-
type: 'number',
|
|
283
|
-
description: 'Number of different search angles to explore',
|
|
284
|
-
default: 3,
|
|
285
|
-
minimum: 1,
|
|
286
|
-
maximum: 10
|
|
287
|
-
},
|
|
288
|
-
depth: {
|
|
289
|
-
type: 'string',
|
|
290
|
-
description: 'Research depth level',
|
|
291
|
-
enum: ['quick', 'moderate', 'comprehensive'],
|
|
292
|
-
default: 'moderate'
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|