@framers/agentos-ext-web-search 1.1.0 → 1.1.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 +31 -188
- package/dist/index.d.ts +14 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -50
- package/dist/index.js.map +1 -1
- package/dist/services/searchProvider.d.ts +54 -2
- package/dist/services/searchProvider.d.ts.map +1 -1
- package/dist/services/searchProvider.js +168 -17
- 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 +134 -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 +134 -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 +69 -87
- package/dist/tools/webSearch.js.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/manifest.json +11 -6
- package/package.json +2 -2
- package/src/index.ts +64 -46
- package/src/services/searchProvider.ts +231 -25
- package/src/tools/factCheck.ts +206 -224
- package/src/tools/researchAggregator.ts +209 -217
- package/src/tools/webSearch.ts +103 -102
- package/test/integration.spec.ts +240 -39
- package/test/searchProvider.spec.ts +263 -18
- package/test/webSearch.spec.ts +129 -27
- package/tsconfig.json +7 -2
|
@@ -1,101 +1,176 @@
|
|
|
1
|
-
import { ITool } from '@framers/agentos';
|
|
2
|
-
import { SearchProviderService } from '../services/searchProvider';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* Research
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
* ```
|
|
2
|
+
* Research Aggregator Tool — runs multiple searches on a topic and aggregates results.
|
|
3
|
+
*
|
|
4
|
+
* Updated to conform to AgentOS `ITool` (inputSchema + ToolExecutionContext).
|
|
5
|
+
*
|
|
6
|
+
* @module @framers/agentos-ext-web-search
|
|
19
7
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
8
|
+
|
|
9
|
+
import type { ITool, JSONSchemaObject, ToolExecutionContext, ToolExecutionResult } from '@framers/agentos';
|
|
10
|
+
import type { ProviderResponse, MultiSearchResponse } from '../services/searchProvider.js';
|
|
11
|
+
import { SearchProviderService } from '../services/searchProvider.js';
|
|
12
|
+
|
|
13
|
+
export interface ResearchAggregatorInput {
|
|
14
|
+
topic: string;
|
|
15
|
+
sources?: number;
|
|
16
|
+
depth?: 'quick' | 'moderate' | 'comprehensive';
|
|
17
|
+
multiSearch?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResearchAggregatorOutput {
|
|
21
|
+
topic: string;
|
|
22
|
+
sources: number;
|
|
23
|
+
depth: 'quick' | 'moderate' | 'comprehensive';
|
|
24
|
+
queries: string[];
|
|
25
|
+
aggregatedResults: any;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ResearchAggregatorTool implements ITool<ResearchAggregatorInput, ResearchAggregatorOutput> {
|
|
30
|
+
public readonly id = 'research-aggregator-v1';
|
|
31
|
+
/** Tool call name used by the LLM / ToolExecutor. */
|
|
32
|
+
public readonly name = 'research_aggregate';
|
|
33
|
+
public readonly displayName = 'Research Aggregator';
|
|
34
|
+
public readonly description = 'Aggregate research from multiple web searches on a topic (deduplicated + categorized).';
|
|
35
|
+
public readonly category = 'research';
|
|
36
|
+
public readonly hasSideEffects = false;
|
|
37
|
+
|
|
38
|
+
public readonly inputSchema: JSONSchemaObject = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
required: ['topic'],
|
|
41
|
+
properties: {
|
|
42
|
+
topic: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'The research topic to aggregate information about',
|
|
45
|
+
},
|
|
46
|
+
sources: {
|
|
47
|
+
type: 'integer',
|
|
48
|
+
description: 'Number of different search angles to explore',
|
|
49
|
+
default: 3,
|
|
50
|
+
minimum: 1,
|
|
51
|
+
maximum: 10,
|
|
52
|
+
},
|
|
53
|
+
depth: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Research depth level',
|
|
56
|
+
enum: ['quick', 'moderate', 'comprehensive'],
|
|
57
|
+
default: 'moderate',
|
|
58
|
+
},
|
|
59
|
+
multiSearch: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description:
|
|
62
|
+
'When true, each search query fans out to ALL available providers in parallel, returning merged and deduplicated results ranked by cross-provider agreement.',
|
|
63
|
+
default: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
public readonly requiredCapabilities = ['capability:web_search'];
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
private readonly searchService: SearchProviderService,
|
|
73
|
+
private readonly defaultMultiSearch: boolean = false,
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
async execute(
|
|
77
|
+
input: ResearchAggregatorInput,
|
|
78
|
+
_context: ToolExecutionContext,
|
|
79
|
+
): Promise<ToolExecutionResult<ResearchAggregatorOutput>> {
|
|
45
80
|
try {
|
|
46
81
|
const sources = input.sources || 3;
|
|
47
82
|
const depth = input.depth || 'moderate';
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
const useMultiSearch = input.multiSearch ?? this.defaultMultiSearch;
|
|
84
|
+
|
|
50
85
|
const queries = this.generateSearchQueries(input.topic, sources, depth);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
|
|
87
|
+
const perQueryMaxResults = depth === 'quick' ? 5 : depth === 'moderate' ? 10 : 15;
|
|
88
|
+
|
|
89
|
+
let searchResults: ProviderResponse[];
|
|
90
|
+
|
|
91
|
+
if (useMultiSearch) {
|
|
92
|
+
// Fan out each query to ALL providers in parallel, then normalize into ProviderResponse shape
|
|
93
|
+
const multiPromises = queries.map((query) =>
|
|
94
|
+
this.searchService
|
|
95
|
+
.multiSearch(query, { maxResults: perQueryMaxResults })
|
|
96
|
+
.then((multi): ProviderResponse => ({
|
|
97
|
+
provider: 'multi',
|
|
98
|
+
results: multi.results,
|
|
99
|
+
metadata: { query, timestamp: multi.metadata.timestamp },
|
|
100
|
+
}))
|
|
101
|
+
.catch((err) => ({
|
|
102
|
+
provider: 'error',
|
|
103
|
+
results: [],
|
|
104
|
+
metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
|
|
105
|
+
}))
|
|
106
|
+
);
|
|
107
|
+
searchResults = (await Promise.all(multiPromises)) as ProviderResponse[];
|
|
108
|
+
} else {
|
|
109
|
+
const searchPromises = queries.map((query) =>
|
|
110
|
+
this.searchService
|
|
111
|
+
.search(query, { maxResults: perQueryMaxResults })
|
|
112
|
+
.catch((err) => ({
|
|
113
|
+
provider: 'error',
|
|
114
|
+
results: [],
|
|
115
|
+
metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
|
|
116
|
+
}))
|
|
117
|
+
);
|
|
118
|
+
searchResults = (await Promise.all(searchPromises)) as ProviderResponse[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const aggregatedResults = this.aggregateResults(searchResults, input.topic);
|
|
122
|
+
|
|
68
123
|
return {
|
|
69
124
|
success: true,
|
|
70
125
|
output: {
|
|
71
126
|
topic: input.topic,
|
|
72
|
-
sources
|
|
73
|
-
depth
|
|
74
|
-
queries
|
|
75
|
-
aggregatedResults
|
|
76
|
-
timestamp: new Date().toISOString()
|
|
77
|
-
}
|
|
127
|
+
sources,
|
|
128
|
+
depth,
|
|
129
|
+
queries,
|
|
130
|
+
aggregatedResults,
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
},
|
|
78
133
|
};
|
|
79
134
|
} catch (error: any) {
|
|
80
|
-
return {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
135
|
+
return { success: false, error: error?.message || String(error) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
validateArgs(input: Record<string, any>): { isValid: boolean; errors?: any[] } {
|
|
140
|
+
const errors: string[] = [];
|
|
141
|
+
|
|
142
|
+
if (!input.topic) {
|
|
143
|
+
errors.push('Topic is required');
|
|
144
|
+
} else if (typeof input.topic !== 'string') {
|
|
145
|
+
errors.push('Topic must be a string');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (input.sources !== undefined) {
|
|
149
|
+
if (typeof input.sources !== 'number' || input.sources < 1 || input.sources > 10) {
|
|
150
|
+
errors.push('Sources must be a number between 1 and 10');
|
|
151
|
+
}
|
|
84
152
|
}
|
|
153
|
+
|
|
154
|
+
if (input.depth !== undefined) {
|
|
155
|
+
if (!['quick', 'moderate', 'comprehensive'].includes(input.depth)) {
|
|
156
|
+
errors.push('Depth must be quick, moderate, or comprehensive');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (input.multiSearch !== undefined && typeof input.multiSearch !== 'boolean') {
|
|
161
|
+
errors.push('multiSearch must be a boolean');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return errors.length === 0 ? { isValid: true } : { isValid: false, errors };
|
|
85
165
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
*/
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Internals (ported from the previous implementation)
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
96
171
|
private generateSearchQueries(topic: string, count: number, depth: string): string[] {
|
|
97
|
-
const queries: string[] = [topic];
|
|
98
|
-
|
|
172
|
+
const queries: string[] = [topic];
|
|
173
|
+
|
|
99
174
|
const modifiers = {
|
|
100
175
|
quick: ['overview', 'summary'],
|
|
101
176
|
moderate: ['overview', 'recent developments', 'key concepts', 'applications'],
|
|
@@ -107,191 +182,108 @@ export class ResearchAggregatorTool implements ITool {
|
|
|
107
182
|
'future directions',
|
|
108
183
|
'industry applications',
|
|
109
184
|
'academic papers',
|
|
110
|
-
'expert opinions'
|
|
111
|
-
]
|
|
185
|
+
'expert opinions',
|
|
186
|
+
],
|
|
112
187
|
};
|
|
113
|
-
|
|
188
|
+
|
|
114
189
|
const selectedModifiers = modifiers[depth as keyof typeof modifiers];
|
|
115
|
-
|
|
190
|
+
|
|
116
191
|
for (let i = 1; i < Math.min(count, selectedModifiers.length + 1); i++) {
|
|
117
192
|
if (selectedModifiers[i - 1]) {
|
|
118
193
|
queries.push(`${topic} ${selectedModifiers[i - 1]}`);
|
|
119
194
|
}
|
|
120
195
|
}
|
|
121
|
-
|
|
196
|
+
|
|
122
197
|
return queries;
|
|
123
198
|
}
|
|
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
|
-
*/
|
|
199
|
+
|
|
133
200
|
private aggregateResults(searchResults: any[], topic: string): any {
|
|
134
201
|
const seenUrls = new Set<string>();
|
|
135
202
|
const categories = {
|
|
136
203
|
overview: [] as any[],
|
|
137
204
|
research: [] as any[],
|
|
138
205
|
applications: [] as any[],
|
|
139
|
-
resources: [] as any[]
|
|
206
|
+
resources: [] as any[],
|
|
140
207
|
};
|
|
141
|
-
|
|
208
|
+
|
|
142
209
|
for (const searchResult of searchResults) {
|
|
143
210
|
if (!searchResult.results) continue;
|
|
144
|
-
|
|
211
|
+
|
|
145
212
|
for (const result of searchResult.results) {
|
|
146
|
-
// Skip duplicates
|
|
147
213
|
if (seenUrls.has(result.url)) continue;
|
|
148
214
|
seenUrls.add(result.url);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (lowerTitle.includes('overview') || lowerTitle.includes('introduction') ||
|
|
155
|
-
lowerTitle.includes('what is')) {
|
|
215
|
+
|
|
216
|
+
const lowerTitle = String(result.title || '').toLowerCase();
|
|
217
|
+
|
|
218
|
+
if (lowerTitle.includes('overview') || lowerTitle.includes('introduction') || lowerTitle.includes('what is')) {
|
|
156
219
|
categories.overview.push(result);
|
|
157
|
-
} else if (
|
|
158
|
-
|
|
220
|
+
} else if (
|
|
221
|
+
lowerTitle.includes('research') ||
|
|
222
|
+
lowerTitle.includes('study') ||
|
|
223
|
+
lowerTitle.includes('paper') ||
|
|
224
|
+
lowerTitle.includes('journal')
|
|
225
|
+
) {
|
|
159
226
|
categories.research.push(result);
|
|
160
|
-
} else if (
|
|
161
|
-
|
|
227
|
+
} else if (
|
|
228
|
+
lowerTitle.includes('application') ||
|
|
229
|
+
lowerTitle.includes('use case') ||
|
|
230
|
+
lowerTitle.includes('implementation')
|
|
231
|
+
) {
|
|
162
232
|
categories.applications.push(result);
|
|
163
233
|
} else {
|
|
164
234
|
categories.resources.push(result);
|
|
165
235
|
}
|
|
166
236
|
}
|
|
167
237
|
}
|
|
168
|
-
|
|
169
|
-
// Calculate relevance scores
|
|
238
|
+
|
|
170
239
|
const scoredCategories = Object.entries(categories).reduce((acc, [key, items]) => {
|
|
171
|
-
acc[key] = items
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
240
|
+
acc[key] = items
|
|
241
|
+
.map((item: any) => ({
|
|
242
|
+
...item,
|
|
243
|
+
relevanceScore: this.calculateRelevance(item, topic),
|
|
244
|
+
}))
|
|
245
|
+
.sort((a: any, b: any) => b.relevanceScore - a.relevanceScore);
|
|
175
246
|
return acc;
|
|
176
247
|
}, {} as any);
|
|
177
|
-
|
|
248
|
+
|
|
178
249
|
return {
|
|
179
250
|
categories: scoredCategories,
|
|
180
251
|
totalResults: seenUrls.size,
|
|
181
|
-
topResults: this.getTopResults(scoredCategories, 10)
|
|
252
|
+
topResults: this.getTopResults(scoredCategories, 10),
|
|
182
253
|
};
|
|
183
254
|
}
|
|
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
|
-
*/
|
|
255
|
+
|
|
193
256
|
private calculateRelevance(result: any, topic: string): number {
|
|
194
|
-
let score = 50;
|
|
195
|
-
|
|
257
|
+
let score = 50;
|
|
258
|
+
|
|
196
259
|
const topicWords = topic.toLowerCase().split(/\s+/);
|
|
197
|
-
const titleWords = result.title.toLowerCase();
|
|
198
|
-
const snippetWords = result.snippet.toLowerCase();
|
|
199
|
-
|
|
200
|
-
// Check title relevance
|
|
260
|
+
const titleWords = String(result.title || '').toLowerCase();
|
|
261
|
+
const snippetWords = String(result.snippet || '').toLowerCase();
|
|
262
|
+
|
|
201
263
|
for (const word of topicWords) {
|
|
202
264
|
if (titleWords.includes(word)) score += 15;
|
|
203
265
|
if (snippetWords.includes(word)) score += 5;
|
|
204
266
|
}
|
|
205
|
-
|
|
206
|
-
// Boost for certain indicators
|
|
267
|
+
|
|
207
268
|
if (titleWords.includes('official')) score += 10;
|
|
208
269
|
if (titleWords.includes('guide')) score += 5;
|
|
209
270
|
if (titleWords.includes('tutorial')) score += 5;
|
|
210
|
-
|
|
271
|
+
|
|
272
|
+
// Bonus for multi-search results with cross-provider agreement
|
|
273
|
+
if (typeof result.confidenceScore === 'number') {
|
|
274
|
+
score += (result.confidenceScore / 100) * 20;
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
return Math.min(100, score);
|
|
212
278
|
}
|
|
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
|
-
*/
|
|
279
|
+
|
|
222
280
|
private getTopResults(categories: any, count: number): any[] {
|
|
223
281
|
const allResults: any[] = [];
|
|
224
|
-
|
|
282
|
+
|
|
225
283
|
for (const items of Object.values(categories)) {
|
|
226
284
|
allResults.push(...(items as any[]));
|
|
227
285
|
}
|
|
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
|
-
};
|
|
286
|
+
|
|
287
|
+
return allResults.sort((a, b) => b.relevanceScore - a.relevanceScore).slice(0, count);
|
|
296
288
|
}
|
|
297
|
-
}
|
|
289
|
+
}
|