@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,118 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { startResearchAction, checkResearchStatusAction } from '../actions';
3
+ import { ResearchService } from '../service';
4
+ import { IAgentRuntime, Memory, State, UUID } from '@elizaos/core';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+
7
+ // Create a real runtime for testing
8
+ function createTestRuntime(): IAgentRuntime {
9
+ const researchService = new ResearchService({
10
+ getSetting: (key: string) => {
11
+ if (key.includes('API_KEY')) return 'test-key';
12
+ return null;
13
+ },
14
+ useModel: async () => 'mock response',
15
+ logger: {
16
+ info: () => {},
17
+ warn: () => {},
18
+ error: () => {},
19
+ debug: () => {},
20
+ },
21
+ } as any);
22
+
23
+ return {
24
+ agentId: uuidv4() as UUID,
25
+ character: {
26
+ name: 'TestAgent',
27
+ bio: ['Test bio'],
28
+ system: 'Test system prompt',
29
+ messageExamples: [],
30
+ postExamples: [],
31
+ topics: [],
32
+ adjectives: [],
33
+ knowledge: [],
34
+ clients: [],
35
+ plugins: [],
36
+ },
37
+ getSetting: (key: string) => {
38
+ if (key.includes('API_KEY')) return 'test-key';
39
+ return null;
40
+ },
41
+ getService: (name: string) => {
42
+ if (name === 'research') return researchService;
43
+ return null;
44
+ },
45
+ useModel: async () => 'mock response',
46
+ composeState: async () => ({ values: {}, data: {}, text: '' }),
47
+ updateState: async () => true,
48
+ messageManager: {
49
+ createMemory: async () => true,
50
+ getMemories: async () => [],
51
+ updateMemory: async () => true,
52
+ deleteMemory: async () => true,
53
+ searchMemories: async () => [],
54
+ getLastMessages: async () => [],
55
+ },
56
+ actions: [],
57
+ providers: [],
58
+ evaluators: [],
59
+ logger: {
60
+ info: () => {},
61
+ warn: () => {},
62
+ error: () => {},
63
+ debug: () => {},
64
+ },
65
+ } as unknown as IAgentRuntime;
66
+ }
67
+
68
+ describe('Research Actions - Real Implementation Tests', () => {
69
+ it('should validate actions correctly', async () => {
70
+ const runtime = createTestRuntime();
71
+ const message: Memory = {
72
+ id: uuidv4() as UUID,
73
+ entityId: uuidv4() as UUID,
74
+ roomId: uuidv4() as UUID,
75
+ agentId: uuidv4() as UUID,
76
+ content: { text: 'start research test topic' },
77
+ createdAt: Date.now(),
78
+ } as Memory;
79
+
80
+ // Test validation with service available
81
+ const isValid = await startResearchAction.validate(runtime, message);
82
+ expect(isValid).toBe(true);
83
+
84
+ // Test validation without service
85
+ const runtimeNoService = { ...runtime, getService: () => null } as IAgentRuntime;
86
+ const isInvalid = await startResearchAction.validate(runtimeNoService, message);
87
+ expect(isInvalid).toBe(false);
88
+ });
89
+
90
+ it('should handle research action correctly', async () => {
91
+ const runtime = createTestRuntime();
92
+ const message: Memory = {
93
+ id: uuidv4() as UUID,
94
+ entityId: uuidv4() as UUID,
95
+ roomId: uuidv4() as UUID,
96
+ agentId: uuidv4() as UUID,
97
+ content: { text: 'research quantum computing' },
98
+ createdAt: Date.now(),
99
+ } as Memory;
100
+
101
+ const responses: any[] = [];
102
+ const callback = async (response: any) => {
103
+ responses.push(response);
104
+ return [];
105
+ };
106
+
107
+ const result = await startResearchAction.handler(
108
+ runtime,
109
+ message,
110
+ { values: {}, data: {}, text: '' },
111
+ {},
112
+ callback
113
+ );
114
+
115
+ // The handler should return something (even if research fails)
116
+ expect(result).toBeDefined();
117
+ });
118
+ });
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { CachedSearchProvider, withCache } from '../integrations/cache';
3
+ import { RateLimitedProvider, AdaptiveRateLimiter, createRateLimitedProvider } from '../integrations/rate-limiter';
4
+ import { SearchProvider } from '../integrations/rate-limiter';
5
+ import { SearchResult } from '../types';
6
+
7
+ // Mock search provider
8
+ class MockSearchProvider implements SearchProvider {
9
+ name = 'MockProvider';
10
+ searchCount = 0;
11
+
12
+ constructor(private results: SearchResult[] = []) {}
13
+
14
+ async search(query: string, maxResults?: number): Promise<SearchResult[]> {
15
+ this.searchCount++;
16
+ return this.results.slice(0, maxResults);
17
+ }
18
+ }
19
+
20
+ describe('CachedSearchProvider', () => {
21
+ let mockProvider: MockSearchProvider;
22
+ let cachedProvider: CachedSearchProvider;
23
+
24
+ beforeEach(() => {
25
+ const mockResults: SearchResult[] = [
26
+ {
27
+ title: 'Test Result 1',
28
+ url: 'https://example.com/1',
29
+ snippet: 'Test snippet 1',
30
+ score: 0.9,
31
+ provider: 'mock',
32
+ metadata: { type: 'web', language: 'en' }
33
+ },
34
+ {
35
+ title: 'Test Result 2',
36
+ url: 'https://example.com/2',
37
+ snippet: 'Test snippet 2',
38
+ score: 0.8,
39
+ provider: 'mock',
40
+ metadata: { type: 'web', language: 'en' }
41
+ }
42
+ ];
43
+
44
+ mockProvider = new MockSearchProvider(mockResults);
45
+ cachedProvider = new CachedSearchProvider(mockProvider, {
46
+ ttlMinutes: 60,
47
+ maxSize: 100
48
+ });
49
+ });
50
+
51
+ it('should cache search results', async () => {
52
+ // First search - should hit the provider
53
+ const results1 = await cachedProvider.search('test query', 10);
54
+ expect(mockProvider.searchCount).toBe(1);
55
+ expect(results1).toHaveLength(2);
56
+
57
+ // Second search with same query - should hit cache
58
+ const results2 = await cachedProvider.search('test query', 10);
59
+ expect(mockProvider.searchCount).toBe(1); // Not incremented
60
+ expect(results2).toEqual(results1);
61
+ });
62
+
63
+ it('should respect TTL eventually', async () => {
64
+ // Create provider with very short TTL
65
+ const shortTTLProvider = new CachedSearchProvider(mockProvider, {
66
+ ttlMinutes: 0.001, // Very short TTL (0.06 seconds)
67
+ maxSize: 100
68
+ });
69
+
70
+ // First search
71
+ await shortTTLProvider.search('test query');
72
+ expect(mockProvider.searchCount).toBe(1);
73
+
74
+ // Wait for TTL to expire
75
+ await new Promise(resolve => setTimeout(resolve, 100));
76
+
77
+ // Should hit provider again
78
+ await shortTTLProvider.search('test query');
79
+ expect(mockProvider.searchCount).toBe(2);
80
+ });
81
+
82
+ it('should cache different queries separately', async () => {
83
+ await cachedProvider.search('query 1');
84
+ await cachedProvider.search('query 2');
85
+ expect(mockProvider.searchCount).toBe(2);
86
+
87
+ // Repeat queries - should hit cache
88
+ await cachedProvider.search('query 1');
89
+ await cachedProvider.search('query 2');
90
+ expect(mockProvider.searchCount).toBe(2);
91
+ });
92
+
93
+ it('should handle different maxResults parameters', async () => {
94
+ await cachedProvider.search('query', 5);
95
+ await cachedProvider.search('query', 10);
96
+ expect(mockProvider.searchCount).toBe(2); // Different cache keys
97
+ });
98
+
99
+ it('should clear cache', async () => {
100
+ await cachedProvider.search('query');
101
+ expect(mockProvider.searchCount).toBe(1);
102
+
103
+ cachedProvider.clear();
104
+
105
+ await cachedProvider.search('query');
106
+ expect(mockProvider.searchCount).toBe(2);
107
+ });
108
+
109
+ it('should work with withCache helper', async () => {
110
+ const cached = withCache(mockProvider, 30);
111
+
112
+ await cached.search('test');
113
+ await cached.search('test');
114
+
115
+ expect(mockProvider.searchCount).toBe(1);
116
+ });
117
+ });
118
+
119
+ describe('RateLimitedProvider', () => {
120
+ let mockProvider: MockSearchProvider;
121
+ let rateLimitedProvider: RateLimitedProvider;
122
+
123
+ beforeEach(() => {
124
+ mockProvider = new MockSearchProvider([
125
+ { title: 'Result', url: 'https://example.com', snippet: 'Test', score: 0.9, provider: 'mock', metadata: { language: 'en' } }
126
+ ]);
127
+ });
128
+
129
+ it('should allow requests within rate limit', async () => {
130
+ rateLimitedProvider = new RateLimitedProvider(mockProvider, {
131
+ tokensPerInterval: 3,
132
+ interval: 'minute'
133
+ });
134
+
135
+ // Make 3 requests - all should succeed immediately
136
+ await rateLimitedProvider.search('query1');
137
+ await rateLimitedProvider.search('query2');
138
+ await rateLimitedProvider.search('query3');
139
+
140
+ expect(mockProvider.searchCount).toBe(3);
141
+ });
142
+
143
+ it('should block when rate limit exceeded', async () => {
144
+ rateLimitedProvider = new RateLimitedProvider(mockProvider, {
145
+ tokensPerInterval: 2,
146
+ interval: 'second'
147
+ });
148
+
149
+ // First two requests succeed
150
+ await rateLimitedProvider.search('query1');
151
+ await rateLimitedProvider.search('query2');
152
+ expect(mockProvider.searchCount).toBe(2);
153
+
154
+ // Third request should be delayed
155
+ const start = Date.now();
156
+ await rateLimitedProvider.search('query3');
157
+ const duration = Date.now() - start;
158
+
159
+ // Should have waited for the rate limit window to refresh
160
+ expect(duration).toBeGreaterThan(400); // At least 400ms wait
161
+ expect(mockProvider.searchCount).toBe(3);
162
+ });
163
+
164
+ it('should work with createRateLimitedProvider helper', async () => {
165
+ const limited = createRateLimitedProvider(mockProvider, {
166
+ requestsPerMinute: 60
167
+ });
168
+
169
+ await limited.search('query1');
170
+ await limited.search('query2');
171
+
172
+ expect(mockProvider.searchCount).toBe(2);
173
+ });
174
+
175
+ it('should handle per-hour limits', async () => {
176
+ const limited = createRateLimitedProvider(mockProvider, {
177
+ requestsPerHour: 3600
178
+ });
179
+
180
+ // Should allow many requests
181
+ for (let i = 0; i < 10; i++) {
182
+ await limited.search(`query${i}`);
183
+ }
184
+ expect(mockProvider.searchCount).toBe(10);
185
+ });
186
+
187
+ it('should handle per-day limits', async () => {
188
+ const limited = createRateLimitedProvider(mockProvider, {
189
+ requestsPerDay: 86400
190
+ });
191
+
192
+ // Should allow many requests
193
+ for (let i = 0; i < 10; i++) {
194
+ await limited.search(`query${i}`);
195
+ }
196
+ expect(mockProvider.searchCount).toBe(10);
197
+ });
198
+ });
199
+
200
+ describe('AdaptiveRateLimiter', () => {
201
+ let mockProvider: MockSearchProvider;
202
+ let adaptiveLimiter: AdaptiveRateLimiter;
203
+
204
+ beforeEach(() => {
205
+ mockProvider = new MockSearchProvider([
206
+ { title: 'Result', url: 'https://example.com', snippet: 'Test', score: 0.9, provider: 'mock', metadata: { language: 'en' } }
207
+ ]);
208
+
209
+ adaptiveLimiter = new AdaptiveRateLimiter(mockProvider, {
210
+ tokensPerInterval: 10,
211
+ interval: 'minute'
212
+ });
213
+ });
214
+
215
+ it('should handle rate limit errors with backoff', async () => {
216
+ // Mock provider that throws rate limit errors first time, then succeeds
217
+ const errorProvider = {
218
+ name: 'ErrorProvider',
219
+ searchCount: 0,
220
+ async search(): Promise<SearchResult[]> {
221
+ this.searchCount++;
222
+ if (this.searchCount === 1) {
223
+ throw new Error('429 rate limit exceeded');
224
+ }
225
+ return [{
226
+ title: 'Success after rate limit',
227
+ url: 'https://example.com',
228
+ snippet: 'Worked on retry',
229
+ score: 0.9,
230
+ provider: 'mock',
231
+ metadata: { language: 'en' }
232
+ }];
233
+ }
234
+ };
235
+
236
+ const limiter = new AdaptiveRateLimiter(errorProvider, {
237
+ tokensPerInterval: 100,
238
+ interval: 'minute'
239
+ });
240
+
241
+ // Should succeed after one retry
242
+ const result = await limiter.search('query');
243
+ expect(result).toHaveLength(1);
244
+ expect(errorProvider.searchCount).toBe(2); // First attempt + retry
245
+ });
246
+
247
+ it('should track statistics', async () => {
248
+ // Successful requests
249
+ await adaptiveLimiter.search('query1');
250
+ await adaptiveLimiter.search('query2');
251
+
252
+ const stats = adaptiveLimiter.getStats();
253
+ expect(stats.successCount).toBe(2);
254
+ expect(stats.errorCount).toBe(0);
255
+ });
256
+
257
+ it('should reduce error count on success', async () => {
258
+ // Test that successful requests reduce error count
259
+ const mockResults = [{
260
+ title: 'Success',
261
+ url: 'https://example.com',
262
+ snippet: 'Test result',
263
+ score: 0.9,
264
+ provider: 'mock',
265
+ metadata: { language: 'en' }
266
+ }];
267
+
268
+ const successProvider = new MockSearchProvider(mockResults);
269
+ const limiter = new AdaptiveRateLimiter(successProvider, {
270
+ tokensPerInterval: 100,
271
+ interval: 'minute'
272
+ });
273
+
274
+ // Make successful requests
275
+ await limiter.search('query1');
276
+ await limiter.search('query2');
277
+
278
+ const stats = limiter.getStats();
279
+ expect(stats.successCount).toBe(2);
280
+ expect(stats.errorCount).toBe(0);
281
+ });
282
+ });
283
+
284
+ describe('Combined Cache and Rate Limiting', () => {
285
+ it('should work together', async () => {
286
+ const mockProvider = new MockSearchProvider([
287
+ { title: 'Result', url: 'https://example.com', snippet: 'Test', score: 0.9, provider: 'mock', metadata: { language: 'en' } }
288
+ ]);
289
+
290
+ // Apply rate limiting first, then caching
291
+ const rateLimited = createRateLimitedProvider(mockProvider, {
292
+ requestsPerMinute: 60
293
+ });
294
+ const cached = withCache(rateLimited, 30);
295
+
296
+ // Multiple searches for same query - should only hit provider once
297
+ await cached.search('test');
298
+ await cached.search('test');
299
+ await cached.search('test');
300
+
301
+ expect(mockProvider.searchCount).toBe(1);
302
+ });
303
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { FirecrawlContentExtractor } from '../integrations/content-extractors/firecrawl';
3
+ import { PlaywrightContentExtractor } from '../integrations/content-extractors/playwright';
4
+
5
+ describe('Content Extractors - Real Implementation Tests', () => {
6
+ it('should initialize Firecrawl extractor', () => {
7
+ const extractor = new FirecrawlContentExtractor({
8
+ apiKey: 'test-key',
9
+ includeMarkdown: true,
10
+ });
11
+ expect(extractor).toBeDefined();
12
+ });
13
+
14
+ it('should initialize Playwright extractor', () => {
15
+ const extractor = new PlaywrightContentExtractor();
16
+ expect(extractor).toBeDefined();
17
+ });
18
+
19
+ it('should handle missing API key for Firecrawl', () => {
20
+ expect(() => {
21
+ new FirecrawlContentExtractor({
22
+ apiKey: '',
23
+ });
24
+ }).toThrow('Firecrawl API key is required');
25
+ });
26
+ });