@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.
Files changed (71) hide show
  1. package/README.md +400 -0
  2. package/dist/index.cjs +9366 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +9284 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +80 -0
  7. package/src/__tests__/action-chaining.test.ts +532 -0
  8. package/src/__tests__/actions.test.ts +118 -0
  9. package/src/__tests__/cache-rate-limiter.test.ts +303 -0
  10. package/src/__tests__/content-extractors.test.ts +26 -0
  11. package/src/__tests__/deepresearch-bench-integration.test.ts +520 -0
  12. package/src/__tests__/deepresearch-bench-simplified.e2e.test.ts +290 -0
  13. package/src/__tests__/deepresearch-bench.e2e.test.ts +376 -0
  14. package/src/__tests__/e2e.test.ts +1870 -0
  15. package/src/__tests__/multi-benchmark-runner.ts +427 -0
  16. package/src/__tests__/providers.test.ts +156 -0
  17. package/src/__tests__/real-world.e2e.test.ts +788 -0
  18. package/src/__tests__/research-scenarios.test.ts +755 -0
  19. package/src/__tests__/research.e2e.test.ts +704 -0
  20. package/src/__tests__/research.test.ts +174 -0
  21. package/src/__tests__/search-providers.test.ts +174 -0
  22. package/src/__tests__/single-benchmark-runner.ts +735 -0
  23. package/src/__tests__/test-search-providers.ts +171 -0
  24. package/src/__tests__/verify-apis.test.ts +82 -0
  25. package/src/actions.ts +1677 -0
  26. package/src/benchmark/deepresearch-benchmark.ts +369 -0
  27. package/src/evaluation/research-evaluator.ts +444 -0
  28. package/src/examples/api-integration.md +498 -0
  29. package/src/examples/browserbase-integration.md +132 -0
  30. package/src/examples/debug-research-query.ts +162 -0
  31. package/src/examples/defi-code-scenarios.md +536 -0
  32. package/src/examples/defi-implementation-guide.md +454 -0
  33. package/src/examples/eliza-research-example.ts +142 -0
  34. package/src/examples/fix-renewable-energy-research.ts +209 -0
  35. package/src/examples/research-scenarios.md +408 -0
  36. package/src/examples/run-complete-renewable-research.ts +303 -0
  37. package/src/examples/run-deep-research.ts +352 -0
  38. package/src/examples/run-logged-research.ts +304 -0
  39. package/src/examples/run-real-research.ts +151 -0
  40. package/src/examples/save-research-output.ts +133 -0
  41. package/src/examples/test-file-logging.ts +199 -0
  42. package/src/examples/test-real-research.ts +67 -0
  43. package/src/examples/test-renewable-energy-research.ts +229 -0
  44. package/src/index.ts +28 -0
  45. package/src/integrations/cache.ts +128 -0
  46. package/src/integrations/content-extractors/firecrawl.ts +314 -0
  47. package/src/integrations/content-extractors/pdf-extractor.ts +350 -0
  48. package/src/integrations/content-extractors/playwright.ts +420 -0
  49. package/src/integrations/factory.ts +419 -0
  50. package/src/integrations/index.ts +18 -0
  51. package/src/integrations/rate-limiter.ts +181 -0
  52. package/src/integrations/search-providers/academic.ts +290 -0
  53. package/src/integrations/search-providers/exa.ts +205 -0
  54. package/src/integrations/search-providers/npm.ts +330 -0
  55. package/src/integrations/search-providers/pypi.ts +211 -0
  56. package/src/integrations/search-providers/serpapi.ts +277 -0
  57. package/src/integrations/search-providers/serper.ts +358 -0
  58. package/src/integrations/search-providers/stagehand-google.ts +87 -0
  59. package/src/integrations/search-providers/tavily.ts +187 -0
  60. package/src/processing/relevance-analyzer.ts +353 -0
  61. package/src/processing/research-logger.ts +450 -0
  62. package/src/processing/result-processor.ts +372 -0
  63. package/src/prompts/research-prompts.ts +419 -0
  64. package/src/providers/cacheProvider.ts +164 -0
  65. package/src/providers.ts +173 -0
  66. package/src/service.ts +2588 -0
  67. package/src/services/swe-bench.ts +286 -0
  68. package/src/strategies/research-strategies.ts +790 -0
  69. package/src/types/pdf-parse.d.ts +34 -0
  70. package/src/types.ts +551 -0
  71. 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
+ }