@elizaos/plugin-research 0.1.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 +400 -0
- package/dist/index.cjs +9366 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +9284 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/src/__tests__/action-chaining.test.ts +532 -0
- package/src/__tests__/actions.test.ts +118 -0
- package/src/__tests__/cache-rate-limiter.test.ts +303 -0
- package/src/__tests__/content-extractors.test.ts +26 -0
- package/src/__tests__/deepresearch-bench-integration.test.ts +520 -0
- package/src/__tests__/deepresearch-bench-simplified.e2e.test.ts +290 -0
- package/src/__tests__/deepresearch-bench.e2e.test.ts +376 -0
- package/src/__tests__/e2e.test.ts +1870 -0
- package/src/__tests__/multi-benchmark-runner.ts +427 -0
- package/src/__tests__/providers.test.ts +156 -0
- package/src/__tests__/real-world.e2e.test.ts +788 -0
- package/src/__tests__/research-scenarios.test.ts +755 -0
- package/src/__tests__/research.e2e.test.ts +704 -0
- package/src/__tests__/research.test.ts +174 -0
- package/src/__tests__/search-providers.test.ts +174 -0
- package/src/__tests__/single-benchmark-runner.ts +735 -0
- package/src/__tests__/test-search-providers.ts +171 -0
- package/src/__tests__/verify-apis.test.ts +82 -0
- package/src/actions.ts +1677 -0
- package/src/benchmark/deepresearch-benchmark.ts +369 -0
- package/src/evaluation/research-evaluator.ts +444 -0
- package/src/examples/api-integration.md +498 -0
- package/src/examples/browserbase-integration.md +132 -0
- package/src/examples/debug-research-query.ts +162 -0
- package/src/examples/defi-code-scenarios.md +536 -0
- package/src/examples/defi-implementation-guide.md +454 -0
- package/src/examples/eliza-research-example.ts +142 -0
- package/src/examples/fix-renewable-energy-research.ts +209 -0
- package/src/examples/research-scenarios.md +408 -0
- package/src/examples/run-complete-renewable-research.ts +303 -0
- package/src/examples/run-deep-research.ts +352 -0
- package/src/examples/run-logged-research.ts +304 -0
- package/src/examples/run-real-research.ts +151 -0
- package/src/examples/save-research-output.ts +133 -0
- package/src/examples/test-file-logging.ts +199 -0
- package/src/examples/test-real-research.ts +67 -0
- package/src/examples/test-renewable-energy-research.ts +229 -0
- package/src/index.ts +28 -0
- package/src/integrations/cache.ts +128 -0
- package/src/integrations/content-extractors/firecrawl.ts +314 -0
- package/src/integrations/content-extractors/pdf-extractor.ts +350 -0
- package/src/integrations/content-extractors/playwright.ts +420 -0
- package/src/integrations/factory.ts +419 -0
- package/src/integrations/index.ts +18 -0
- package/src/integrations/rate-limiter.ts +181 -0
- package/src/integrations/search-providers/academic.ts +290 -0
- package/src/integrations/search-providers/exa.ts +205 -0
- package/src/integrations/search-providers/npm.ts +330 -0
- package/src/integrations/search-providers/pypi.ts +211 -0
- package/src/integrations/search-providers/serpapi.ts +277 -0
- package/src/integrations/search-providers/serper.ts +358 -0
- package/src/integrations/search-providers/stagehand-google.ts +87 -0
- package/src/integrations/search-providers/tavily.ts +187 -0
- package/src/processing/relevance-analyzer.ts +353 -0
- package/src/processing/research-logger.ts +450 -0
- package/src/processing/result-processor.ts +372 -0
- package/src/prompts/research-prompts.ts +419 -0
- package/src/providers/cacheProvider.ts +164 -0
- package/src/providers.ts +173 -0
- package/src/service.ts +2588 -0
- package/src/services/swe-bench.ts +286 -0
- package/src/strategies/research-strategies.ts +790 -0
- package/src/types/pdf-parse.d.ts +34 -0
- package/src/types.ts +551 -0
- package/src/verification/claim-verifier.ts +443 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { elizaLogger } from '@elizaos/core';
|
|
2
|
+
import { SearchResult } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ProcessingConfig {
|
|
5
|
+
deduplicationThreshold: number; // Similarity threshold for duplicate detection
|
|
6
|
+
qualityThreshold: number; // Minimum quality score to include
|
|
7
|
+
maxResults: number; // Maximum results to return
|
|
8
|
+
prioritizeRecent: boolean; // Prioritize more recent sources
|
|
9
|
+
diversityWeight: number; // Weight for domain diversity
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ProcessedResults {
|
|
13
|
+
results: SearchResult[];
|
|
14
|
+
duplicatesRemoved: number;
|
|
15
|
+
qualityFiltered: number;
|
|
16
|
+
totalProcessed: number;
|
|
17
|
+
diversityScore: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SearchResultProcessor {
|
|
21
|
+
private config: ProcessingConfig;
|
|
22
|
+
|
|
23
|
+
constructor(config: Partial<ProcessingConfig> = {}) {
|
|
24
|
+
this.config = {
|
|
25
|
+
deduplicationThreshold: 0.85,
|
|
26
|
+
qualityThreshold: 0.3,
|
|
27
|
+
maxResults: 50,
|
|
28
|
+
prioritizeRecent: true,
|
|
29
|
+
diversityWeight: 0.3,
|
|
30
|
+
...config,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async processResults(results: SearchResult[]): Promise<ProcessedResults> {
|
|
35
|
+
elizaLogger.info(`Processing ${results.length} search results`);
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
let processed = [...results];
|
|
39
|
+
let duplicatesRemoved = 0;
|
|
40
|
+
let qualityFiltered = 0;
|
|
41
|
+
|
|
42
|
+
// Step 1: Quality filtering
|
|
43
|
+
const beforeQuality = processed.length;
|
|
44
|
+
processed = this.filterByQuality(processed);
|
|
45
|
+
qualityFiltered = beforeQuality - processed.length;
|
|
46
|
+
|
|
47
|
+
// Step 2: Deduplication
|
|
48
|
+
const beforeDedup = processed.length;
|
|
49
|
+
processed = await this.deduplicateResults(processed);
|
|
50
|
+
duplicatesRemoved = beforeDedup - processed.length;
|
|
51
|
+
|
|
52
|
+
// Step 3: Ranking and scoring
|
|
53
|
+
processed = this.rankResults(processed);
|
|
54
|
+
|
|
55
|
+
// Step 4: Diversity optimization
|
|
56
|
+
processed = this.optimizeForDiversity(processed);
|
|
57
|
+
|
|
58
|
+
// Step 5: Limit results
|
|
59
|
+
processed = processed.slice(0, this.config.maxResults);
|
|
60
|
+
|
|
61
|
+
const diversityScore = this.calculateDiversityScore(processed);
|
|
62
|
+
const duration = Date.now() - startTime;
|
|
63
|
+
|
|
64
|
+
elizaLogger.info(
|
|
65
|
+
`Result processing completed in ${duration}ms: ` +
|
|
66
|
+
`${processed.length} final results (removed ${duplicatesRemoved} duplicates, ` +
|
|
67
|
+
`${qualityFiltered} low-quality), diversity: ${diversityScore.toFixed(2)}`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
results: processed,
|
|
72
|
+
duplicatesRemoved,
|
|
73
|
+
qualityFiltered,
|
|
74
|
+
totalProcessed: results.length,
|
|
75
|
+
diversityScore,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private filterByQuality(results: SearchResult[]): SearchResult[] {
|
|
80
|
+
return results.filter(result => {
|
|
81
|
+
// Basic quality checks
|
|
82
|
+
if (!result.title || !result.content || result.content.length < 50) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Score-based filtering
|
|
87
|
+
if (result.score && result.score < this.config.qualityThreshold) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Content quality checks
|
|
92
|
+
if (this.isLowQualityContent(result)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private isLowQualityContent(result: SearchResult): boolean {
|
|
101
|
+
if (!result.content) return true;
|
|
102
|
+
const content = result.content.toLowerCase();
|
|
103
|
+
const title = result.title.toLowerCase();
|
|
104
|
+
|
|
105
|
+
// Check for spam indicators
|
|
106
|
+
const spamIndicators = [
|
|
107
|
+
'click here',
|
|
108
|
+
'buy now',
|
|
109
|
+
'limited time',
|
|
110
|
+
'free download',
|
|
111
|
+
'miracle cure',
|
|
112
|
+
'get rich quick',
|
|
113
|
+
'weight loss',
|
|
114
|
+
'casino',
|
|
115
|
+
'porn',
|
|
116
|
+
'xxx',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (spamIndicators.some(indicator => content.includes(indicator) || title.includes(indicator))) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for very repetitive content
|
|
124
|
+
const words = content.split(/\s+/);
|
|
125
|
+
const wordCounts = words.reduce((counts, word) => {
|
|
126
|
+
counts[word] = (counts[word] || 0) + 1;
|
|
127
|
+
return counts;
|
|
128
|
+
}, {} as Record<string, number>);
|
|
129
|
+
|
|
130
|
+
const maxRepetition = Math.max(...Object.values(wordCounts));
|
|
131
|
+
if (maxRepetition > words.length * 0.1) { // More than 10% repetition
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check for insufficient substantive content
|
|
136
|
+
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
|
137
|
+
if (sentences.length < 3) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async deduplicateResults(results: SearchResult[]): Promise<SearchResult[]> {
|
|
145
|
+
const deduplicated: SearchResult[] = [];
|
|
146
|
+
const processed = new Set<string>();
|
|
147
|
+
|
|
148
|
+
for (const result of results) {
|
|
149
|
+
let isDuplicate = false;
|
|
150
|
+
|
|
151
|
+
// URL-based deduplication (exact match)
|
|
152
|
+
if (processed.has(result.url)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Content similarity-based deduplication
|
|
157
|
+
for (const existing of deduplicated) {
|
|
158
|
+
const similarity = this.calculateContentSimilarity(result, existing);
|
|
159
|
+
if (similarity > this.config.deduplicationThreshold) {
|
|
160
|
+
isDuplicate = true;
|
|
161
|
+
// Keep the one with higher score or more recent
|
|
162
|
+
if (this.shouldReplaceExisting(result, existing)) {
|
|
163
|
+
const index = deduplicated.indexOf(existing);
|
|
164
|
+
deduplicated[index] = result;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isDuplicate) {
|
|
171
|
+
deduplicated.push(result);
|
|
172
|
+
processed.add(result.url);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return deduplicated;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private calculateContentSimilarity(a: SearchResult, b: SearchResult): number {
|
|
180
|
+
// Calculate similarity based on title and content
|
|
181
|
+
const titleSim = this.calculateTextSimilarity(a.title, b.title);
|
|
182
|
+
const contentSim = this.calculateTextSimilarity(
|
|
183
|
+
(a.content || '').substring(0, 500),
|
|
184
|
+
(b.content || '').substring(0, 500)
|
|
185
|
+
);
|
|
186
|
+
const snippetSim = this.calculateTextSimilarity(a.snippet || '', b.snippet || '');
|
|
187
|
+
|
|
188
|
+
// Weighted average
|
|
189
|
+
return titleSim * 0.4 + contentSim * 0.4 + snippetSim * 0.2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private calculateTextSimilarity(text1: string, text2: string): number {
|
|
193
|
+
// Simple Jaccard similarity based on word overlap
|
|
194
|
+
const words1 = new Set(text1.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
195
|
+
const words2 = new Set(text2.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
196
|
+
|
|
197
|
+
const intersection = new Set([...words1].filter(w => words2.has(w)));
|
|
198
|
+
const union = new Set([...words1, ...words2]);
|
|
199
|
+
|
|
200
|
+
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private shouldReplaceExisting(newResult: SearchResult, existing: SearchResult): boolean {
|
|
204
|
+
// Prefer higher scored results
|
|
205
|
+
if (newResult.score && existing.score && newResult.score > existing.score) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Prefer more recent sources if configured
|
|
210
|
+
if (this.config.prioritizeRecent) {
|
|
211
|
+
const newDate = this.extractDate(newResult);
|
|
212
|
+
const existingDate = this.extractDate(existing);
|
|
213
|
+
|
|
214
|
+
if (newDate && existingDate && newDate > existingDate) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Prefer longer, more substantive content
|
|
220
|
+
if (newResult.content && existing.content && newResult.content.length > existing.content.length * 1.5) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private extractDate(result: SearchResult): Date | null {
|
|
228
|
+
// Try to extract date from metadata
|
|
229
|
+
if (result.metadata?.publishDate) {
|
|
230
|
+
return new Date(result.metadata.publishDate);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Try to extract from content
|
|
234
|
+
if (result.content) {
|
|
235
|
+
const dateMatches = result.content.match(/\b(20\d{2}[-/]\d{1,2}[-/]\d{1,2}|\d{1,2}[-/]\d{1,2}[-/]20\d{2})\b/);
|
|
236
|
+
if (dateMatches) {
|
|
237
|
+
return new Date(dateMatches[0]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private rankResults(results: SearchResult[]): SearchResult[] {
|
|
245
|
+
return results.map(result => {
|
|
246
|
+
let score = result.score || 0.5;
|
|
247
|
+
|
|
248
|
+
// Boost academic sources
|
|
249
|
+
if (result.provider === 'academic' || this.isAcademicSource(result)) {
|
|
250
|
+
score *= 1.3;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Boost reputable domains
|
|
254
|
+
if (this.isReputableDomain(result.url)) {
|
|
255
|
+
score *= 1.2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Boost recent content
|
|
259
|
+
const date = this.extractDate(result);
|
|
260
|
+
if (date) {
|
|
261
|
+
const ageInDays = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
|
|
262
|
+
if (ageInDays < 365) {
|
|
263
|
+
score *= 1.1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Boost longer, more comprehensive content
|
|
268
|
+
if (result.content && result.content.length > 2000) {
|
|
269
|
+
score *= 1.1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
...result,
|
|
274
|
+
score: Math.min(score, 1.0), // Cap at 1.0
|
|
275
|
+
};
|
|
276
|
+
}).sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private isAcademicSource(result: SearchResult): boolean {
|
|
280
|
+
const academicDomains = [
|
|
281
|
+
'scholar.google.com',
|
|
282
|
+
'arxiv.org',
|
|
283
|
+
'pubmed.ncbi.nlm.nih.gov',
|
|
284
|
+
'ieee.org',
|
|
285
|
+
'acm.org',
|
|
286
|
+
'springer.com',
|
|
287
|
+
'sciencedirect.com',
|
|
288
|
+
'wiley.com',
|
|
289
|
+
'nature.com',
|
|
290
|
+
'science.org',
|
|
291
|
+
'.edu',
|
|
292
|
+
'.ac.uk',
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return academicDomains.some(domain => result.url.includes(domain));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private isReputableDomain(url: string): boolean {
|
|
299
|
+
const reputableDomains = [
|
|
300
|
+
'wikipedia.org',
|
|
301
|
+
'britannica.com',
|
|
302
|
+
'stanford.edu',
|
|
303
|
+
'mit.edu',
|
|
304
|
+
'harvard.edu',
|
|
305
|
+
'nytimes.com',
|
|
306
|
+
'bbc.com',
|
|
307
|
+
'reuters.com',
|
|
308
|
+
'ap.org',
|
|
309
|
+
'cnn.com',
|
|
310
|
+
'washingtonpost.com',
|
|
311
|
+
'theguardian.com',
|
|
312
|
+
'economist.com',
|
|
313
|
+
'github.com',
|
|
314
|
+
'stackoverflow.com',
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
return reputableDomains.some(domain => url.includes(domain));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private optimizeForDiversity(results: SearchResult[]): SearchResult[] {
|
|
321
|
+
if (this.config.diversityWeight === 0) {
|
|
322
|
+
return results;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const diversified: SearchResult[] = [];
|
|
326
|
+
const domainCounts = new Map<string, number>();
|
|
327
|
+
const providerCounts = new Map<string, number>();
|
|
328
|
+
|
|
329
|
+
for (const result of results) {
|
|
330
|
+
const domain = new URL(result.url).hostname;
|
|
331
|
+
const provider = result.provider || 'unknown';
|
|
332
|
+
|
|
333
|
+
const domainCount = domainCounts.get(domain) || 0;
|
|
334
|
+
const providerCount = providerCounts.get(provider) || 0;
|
|
335
|
+
|
|
336
|
+
// Apply diversity penalty for over-represented domains/providers
|
|
337
|
+
let diversityPenalty = 1.0;
|
|
338
|
+
if (domainCount > 2) {
|
|
339
|
+
diversityPenalty *= 0.8;
|
|
340
|
+
}
|
|
341
|
+
if (providerCount > 5) {
|
|
342
|
+
diversityPenalty *= 0.9;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const adjustedScore = (result.score || 0.5) *
|
|
346
|
+
(1 - this.config.diversityWeight + this.config.diversityWeight * diversityPenalty);
|
|
347
|
+
|
|
348
|
+
diversified.push({
|
|
349
|
+
...result,
|
|
350
|
+
score: adjustedScore,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
domainCounts.set(domain, domainCount + 1);
|
|
354
|
+
providerCounts.set(provider, providerCount + 1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return diversified.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private calculateDiversityScore(results: SearchResult[]): number {
|
|
361
|
+
if (results.length === 0) return 0;
|
|
362
|
+
|
|
363
|
+
const domains = new Set(results.map(r => new URL(r.url).hostname));
|
|
364
|
+
const providers = new Set(results.map(r => r.provider));
|
|
365
|
+
|
|
366
|
+
// Calculate diversity as ratio of unique domains/providers to total results
|
|
367
|
+
const domainDiversity = domains.size / results.length;
|
|
368
|
+
const providerDiversity = providers.size / results.length;
|
|
369
|
+
|
|
370
|
+
return (domainDiversity + providerDiversity) / 2;
|
|
371
|
+
}
|
|
372
|
+
}
|