@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,174 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ResearchService } from '../service';
|
|
3
|
+
import { ResearchStatus, ResearchPhase, ResearchProject } from '../types';
|
|
4
|
+
import { IAgentRuntime, ModelType } from '@elizaos/core';
|
|
5
|
+
|
|
6
|
+
// Mock runtime
|
|
7
|
+
const createMockRuntime = () => ({
|
|
8
|
+
getSetting: vi.fn((key: string) => {
|
|
9
|
+
const settings: Record<string, string> = {
|
|
10
|
+
RESEARCH_MAX_RESULTS: '10',
|
|
11
|
+
RESEARCH_TIMEOUT: '300000',
|
|
12
|
+
RESEARCH_ENABLE_CITATIONS: 'true',
|
|
13
|
+
RESEARCH_ENABLE_IMAGES: 'true',
|
|
14
|
+
RESEARCH_LANGUAGE: 'en',
|
|
15
|
+
};
|
|
16
|
+
return settings[key] || '';
|
|
17
|
+
}),
|
|
18
|
+
useModel: vi.fn(async (type: any, params: any) => {
|
|
19
|
+
// Mock LLM responses based on the prompt
|
|
20
|
+
if (params.messages[0].content.includes('research plan')) {
|
|
21
|
+
return 'Research plan: 1. Search for recent developments 2. Analyze key findings 3. Synthesize information';
|
|
22
|
+
}
|
|
23
|
+
if (params.messages[0].content.includes('search queries')) {
|
|
24
|
+
return 'quantum computing breakthroughs 2024\nlatest quantum computing advances\nquantum computing research papers';
|
|
25
|
+
}
|
|
26
|
+
if (params.messages[0].content.includes('relevance')) {
|
|
27
|
+
return '0.85';
|
|
28
|
+
}
|
|
29
|
+
if (params.messages[0].content.includes('Analyze')) {
|
|
30
|
+
return 'Key insights: Significant breakthroughs in quantum error correction';
|
|
31
|
+
}
|
|
32
|
+
if (params.messages[0].content.includes('report')) {
|
|
33
|
+
return 'Executive Summary: This research explores recent quantum computing advances...';
|
|
34
|
+
}
|
|
35
|
+
return 'Mock response';
|
|
36
|
+
}),
|
|
37
|
+
} as unknown as IAgentRuntime);
|
|
38
|
+
|
|
39
|
+
describe('ResearchService', () => {
|
|
40
|
+
let runtime: IAgentRuntime;
|
|
41
|
+
let service: ResearchService;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
runtime = createMockRuntime();
|
|
45
|
+
service = new ResearchService(runtime);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('service initialization', () => {
|
|
49
|
+
it('should create service with default config', () => {
|
|
50
|
+
expect(service).toBeDefined();
|
|
51
|
+
expect(service.capabilityDescription).toContain('PhD-level deep research');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should create service with custom config', () => {
|
|
55
|
+
const customService = new ResearchService(runtime);
|
|
56
|
+
expect(customService).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('createResearchProject', () => {
|
|
61
|
+
it('should create a new research project', async () => {
|
|
62
|
+
const query = 'quantum computing breakthroughs in 2024';
|
|
63
|
+
const project = await service.createResearchProject(query);
|
|
64
|
+
|
|
65
|
+
expect(project).toBeDefined();
|
|
66
|
+
expect(project.id).toBeDefined();
|
|
67
|
+
expect(project.query).toBe(query);
|
|
68
|
+
expect([ResearchStatus.PENDING, ResearchStatus.ACTIVE]).toContain(project.status);
|
|
69
|
+
expect([ResearchPhase.INITIALIZATION, ResearchPhase.PLANNING, ResearchPhase.SEARCHING]).toContain(project.phase);
|
|
70
|
+
expect(project.findings).toEqual([]);
|
|
71
|
+
expect(project.sources).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should create project with custom config', async () => {
|
|
75
|
+
const query = 'AI healthcare impact';
|
|
76
|
+
const config = { maxSearchResults: 5, language: 'es' };
|
|
77
|
+
const project = await service.createResearchProject(query, config);
|
|
78
|
+
|
|
79
|
+
expect(project.metadata.language).toBe(config.language);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('project lifecycle', () => {
|
|
84
|
+
it('should get project by id', async () => {
|
|
85
|
+
const project = await service.createResearchProject('test query');
|
|
86
|
+
const retrieved = await service.getProject(project.id);
|
|
87
|
+
|
|
88
|
+
expect(retrieved).toBeDefined();
|
|
89
|
+
expect(retrieved?.id).toBe(project.id);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should get all projects', async () => {
|
|
93
|
+
await service.createResearchProject('query 1');
|
|
94
|
+
await service.createResearchProject('query 2');
|
|
95
|
+
|
|
96
|
+
const projects = await service.getAllProjects();
|
|
97
|
+
expect(projects.length).toBeGreaterThanOrEqual(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should get active projects', async () => {
|
|
101
|
+
const project = await service.createResearchProject('active query');
|
|
102
|
+
|
|
103
|
+
// Wait a bit for the project to start
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
105
|
+
|
|
106
|
+
// Check if it's active or already completed (both are valid)
|
|
107
|
+
const activeProjects = await service.getActiveProjects();
|
|
108
|
+
const projectStatus = await service.getProject(project.id);
|
|
109
|
+
|
|
110
|
+
// Either the project should be in active projects, or it should have completed/failed
|
|
111
|
+
const isActive = activeProjects.some(p => p.id === project.id);
|
|
112
|
+
const isCompleted = projectStatus?.status === ResearchStatus.COMPLETED;
|
|
113
|
+
const isFailed = projectStatus?.status === ResearchStatus.FAILED;
|
|
114
|
+
const isPending = projectStatus?.status === ResearchStatus.PENDING;
|
|
115
|
+
|
|
116
|
+
// The test passes if the project is in any valid state
|
|
117
|
+
expect(isActive || isCompleted || isFailed || isPending).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should pause and resume research', async () => {
|
|
121
|
+
const project = await service.createResearchProject('pausable query');
|
|
122
|
+
|
|
123
|
+
// Wait for project to become active
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
125
|
+
|
|
126
|
+
await service.pauseResearch(project.id);
|
|
127
|
+
const paused = await service.getProject(project.id);
|
|
128
|
+
expect(paused?.status).toBe(ResearchStatus.PAUSED);
|
|
129
|
+
|
|
130
|
+
await service.resumeResearch(project.id);
|
|
131
|
+
// Status will change back to active once resumed
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('research phases', () => {
|
|
136
|
+
it('should progress through research phases', async () => {
|
|
137
|
+
const project = await service.createResearchProject('phase test query');
|
|
138
|
+
|
|
139
|
+
// Monitor phase progression
|
|
140
|
+
const phases: ResearchPhase[] = [];
|
|
141
|
+
const checkPhase = async () => {
|
|
142
|
+
const p = await service.getProject(project.id);
|
|
143
|
+
if (p && !phases.includes(p.phase)) {
|
|
144
|
+
phases.push(p.phase);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Check phases over time
|
|
149
|
+
for (let i = 0; i < 10; i++) {
|
|
150
|
+
await checkPhase();
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
expect(phases.length).toBeGreaterThanOrEqual(1);
|
|
155
|
+
// First observed phase should be one of the early phases
|
|
156
|
+
const earlyPhases = [ResearchPhase.INITIALIZATION, ResearchPhase.PLANNING, ResearchPhase.SEARCHING];
|
|
157
|
+
expect(earlyPhases).toContain(phases[0]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('error handling', () => {
|
|
162
|
+
it('should handle invalid project id', async () => {
|
|
163
|
+
const project = await service.getProject('invalid-id');
|
|
164
|
+
expect(project).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle pause on non-existent project', async () => {
|
|
168
|
+
// The service should handle this gracefully without throwing
|
|
169
|
+
await service.pauseResearch('invalid-id');
|
|
170
|
+
// If we get here, no error was thrown
|
|
171
|
+
expect(true).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { TavilySearchProvider } from '../integrations/search-providers/tavily';
|
|
3
|
+
import { SerperSearchProvider } from '../integrations/search-providers/serper';
|
|
4
|
+
import { AcademicSearchProvider } from '../integrations/search-providers/academic';
|
|
5
|
+
import { ExaSearchProvider } from '../integrations/search-providers/exa';
|
|
6
|
+
import { SerpAPISearchProvider } from '../integrations/search-providers/serpapi';
|
|
7
|
+
import { NPMSearchProvider } from '../integrations/search-providers/npm';
|
|
8
|
+
import { PyPISearchProvider } from '../integrations/search-providers/pypi';
|
|
9
|
+
|
|
10
|
+
describe('Search Providers - Real Implementation Tests', () => {
|
|
11
|
+
it('should initialize Tavily provider', () => {
|
|
12
|
+
const provider = new TavilySearchProvider({ apiKey: 'test-key' });
|
|
13
|
+
expect(provider).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should initialize Serper provider', () => {
|
|
17
|
+
const provider = new SerperSearchProvider({ apiKey: 'test-key' });
|
|
18
|
+
expect(provider).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should initialize Academic provider', () => {
|
|
22
|
+
const provider = new AcademicSearchProvider({ semanticScholarApiKey: 'test-key' });
|
|
23
|
+
expect(provider).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should initialize Exa provider', () => {
|
|
27
|
+
const provider = new ExaSearchProvider({ apiKey: 'test-key' });
|
|
28
|
+
expect(provider).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should initialize SerpAPI provider', () => {
|
|
32
|
+
const provider = new SerpAPISearchProvider({ apiKey: 'test-key' });
|
|
33
|
+
expect(provider).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle missing API key gracefully', () => {
|
|
37
|
+
// Academic provider works without API key (falls back to public access)
|
|
38
|
+
const provider = new AcademicSearchProvider({});
|
|
39
|
+
expect(provider).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('NPM Search Provider', () => {
|
|
43
|
+
it('should initialize without API key (public access)', () => {
|
|
44
|
+
const provider = new NPMSearchProvider();
|
|
45
|
+
expect(provider).toBeDefined();
|
|
46
|
+
expect(provider.name).toBe('npm');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should have search method', () => {
|
|
50
|
+
const provider = new NPMSearchProvider();
|
|
51
|
+
expect(typeof provider.search).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle empty query', async () => {
|
|
55
|
+
const provider = new NPMSearchProvider();
|
|
56
|
+
try {
|
|
57
|
+
const results = await provider.search('', 5);
|
|
58
|
+
expect(Array.isArray(results)).toBe(true);
|
|
59
|
+
expect(results.length).toBe(0);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Handle network errors gracefully
|
|
62
|
+
console.warn('NPM search failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
63
|
+
expect(true).toBe(true); // Skip test on network failure
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should search for real packages', async () => {
|
|
68
|
+
const provider = new NPMSearchProvider();
|
|
69
|
+
try {
|
|
70
|
+
const results = await provider.search('react', 3);
|
|
71
|
+
expect(Array.isArray(results)).toBe(true);
|
|
72
|
+
|
|
73
|
+
if (results.length > 0) {
|
|
74
|
+
const result = results[0];
|
|
75
|
+
expect(result).toHaveProperty('title');
|
|
76
|
+
expect(result).toHaveProperty('url');
|
|
77
|
+
expect(result).toHaveProperty('content');
|
|
78
|
+
expect(typeof result.title).toBe('string');
|
|
79
|
+
expect(typeof result.url).toBe('string');
|
|
80
|
+
expect(typeof result.content).toBe('string');
|
|
81
|
+
expect(result.url).toContain('npmjs.com');
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Handle network errors gracefully
|
|
85
|
+
console.warn('NPM search failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
86
|
+
expect(true).toBe(true); // Skip test on network failure
|
|
87
|
+
}
|
|
88
|
+
}, 10000);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('PyPI Search Provider', () => {
|
|
92
|
+
it('should initialize without API key (public access)', () => {
|
|
93
|
+
const provider = new PyPISearchProvider();
|
|
94
|
+
expect(provider).toBeDefined();
|
|
95
|
+
expect(provider.name).toBe('pypi');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should have search method', () => {
|
|
99
|
+
const provider = new PyPISearchProvider();
|
|
100
|
+
expect(typeof provider.search).toBe('function');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle empty query', async () => {
|
|
104
|
+
const provider = new PyPISearchProvider();
|
|
105
|
+
try {
|
|
106
|
+
const results = await provider.search('', 5);
|
|
107
|
+
expect(Array.isArray(results)).toBe(true);
|
|
108
|
+
expect(results.length).toBe(0);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Handle network errors gracefully
|
|
111
|
+
console.warn('PyPI search failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
112
|
+
expect(true).toBe(true); // Skip test on network failure
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should search for real packages', async () => {
|
|
117
|
+
const provider = new PyPISearchProvider();
|
|
118
|
+
try {
|
|
119
|
+
const results = await provider.search('numpy', 3);
|
|
120
|
+
expect(Array.isArray(results)).toBe(true);
|
|
121
|
+
|
|
122
|
+
if (results.length > 0) {
|
|
123
|
+
const result = results[0];
|
|
124
|
+
expect(result).toHaveProperty('title');
|
|
125
|
+
expect(result).toHaveProperty('url');
|
|
126
|
+
expect(result).toHaveProperty('content');
|
|
127
|
+
expect(typeof result.title).toBe('string');
|
|
128
|
+
expect(typeof result.url).toBe('string');
|
|
129
|
+
expect(typeof result.content).toBe('string');
|
|
130
|
+
expect(result.url).toContain('pypi.org');
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// Handle network errors gracefully
|
|
134
|
+
console.warn('PyPI search failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
135
|
+
expect(true).toBe(true); // Skip test on network failure
|
|
136
|
+
}
|
|
137
|
+
}, 10000);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('Search Provider Consistency', () => {
|
|
141
|
+
it('should return consistent result format across providers', async () => {
|
|
142
|
+
const npmProvider = new NPMSearchProvider();
|
|
143
|
+
const pypiProvider = new PyPISearchProvider();
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const npmResults = await npmProvider.search('react', 1);
|
|
147
|
+
const pypiResults = await pypiProvider.search('numpy', 1);
|
|
148
|
+
|
|
149
|
+
// Both should return arrays
|
|
150
|
+
expect(Array.isArray(npmResults)).toBe(true);
|
|
151
|
+
expect(Array.isArray(pypiResults)).toBe(true);
|
|
152
|
+
|
|
153
|
+
// If results exist, they should have consistent structure
|
|
154
|
+
if (npmResults.length > 0 && pypiResults.length > 0) {
|
|
155
|
+
const npmResult = npmResults[0];
|
|
156
|
+
const pypiResult = pypiResults[0];
|
|
157
|
+
|
|
158
|
+
// Both should have required fields
|
|
159
|
+
expect(npmResult).toHaveProperty('title');
|
|
160
|
+
expect(npmResult).toHaveProperty('url');
|
|
161
|
+
expect(npmResult).toHaveProperty('content');
|
|
162
|
+
|
|
163
|
+
expect(pypiResult).toHaveProperty('title');
|
|
164
|
+
expect(pypiResult).toHaveProperty('url');
|
|
165
|
+
expect(pypiResult).toHaveProperty('content');
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Handle network errors gracefully
|
|
169
|
+
console.warn('Search consistency test failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
170
|
+
expect(true).toBe(true); // Skip test on network failure
|
|
171
|
+
}
|
|
172
|
+
}, 15000);
|
|
173
|
+
});
|
|
174
|
+
});
|