@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,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
|
+
});
|