@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,330 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { SearchResult } from '../../types';
3
+ import { elizaLogger } from '@elizaos/core';
4
+ import { z } from 'zod';
5
+
6
+ // NPM Registry API response schema validation
7
+ const NPMPackageSchema = z.object({
8
+ name: z.string(),
9
+ description: z.string().optional(),
10
+ version: z.string(),
11
+ keywords: z.array(z.string()).optional(),
12
+ author: z.union([
13
+ z.string(),
14
+ z.object({
15
+ name: z.string(),
16
+ email: z.string().optional(),
17
+ })
18
+ ]).optional(),
19
+ maintainers: z.array(z.object({
20
+ name: z.string(),
21
+ email: z.string().optional(),
22
+ })).optional(),
23
+ repository: z.union([
24
+ z.string(),
25
+ z.object({
26
+ type: z.string().optional(),
27
+ url: z.string(),
28
+ })
29
+ ]).optional(),
30
+ homepage: z.string().optional(),
31
+ license: z.string().optional(),
32
+ readme: z.string().optional(),
33
+ 'dist-tags': z.object({
34
+ latest: z.string(),
35
+ }).optional(),
36
+ time: z.record(z.string()).optional(),
37
+ });
38
+
39
+ const NPMSearchSchema = z.object({
40
+ objects: z.array(z.object({
41
+ package: z.object({
42
+ name: z.string(),
43
+ version: z.string(),
44
+ description: z.string().optional(),
45
+ keywords: z.array(z.string()).optional(),
46
+ author: z.union([
47
+ z.string(),
48
+ z.object({
49
+ name: z.string(),
50
+ email: z.string().optional(),
51
+ })
52
+ ]).optional(),
53
+ maintainers: z.array(z.object({
54
+ username: z.string(),
55
+ email: z.string().optional(),
56
+ })).optional(),
57
+ repository: z.union([
58
+ z.string(),
59
+ z.object({
60
+ type: z.string().optional(),
61
+ url: z.string(),
62
+ })
63
+ ]).optional(),
64
+ links: z.object({
65
+ npm: z.string().optional(),
66
+ homepage: z.string().optional(),
67
+ repository: z.string().optional(),
68
+ bugs: z.string().optional(),
69
+ }).optional(),
70
+ }),
71
+ score: z.object({
72
+ final: z.number(),
73
+ detail: z.object({
74
+ quality: z.number(),
75
+ popularity: z.number(),
76
+ maintenance: z.number(),
77
+ }),
78
+ }),
79
+ searchScore: z.number().optional(),
80
+ })),
81
+ total: z.number(),
82
+ time: z.string(),
83
+ });
84
+
85
+ export interface NPMConfig {
86
+ maxResults?: number;
87
+ includeDetails?: boolean;
88
+ quality?: number; // Minimum quality score (0-1)
89
+ popularity?: number; // Minimum popularity score (0-1)
90
+ }
91
+
92
+ export class NPMSearchProvider {
93
+ public readonly name = 'npm';
94
+ private readonly registryUrl = 'https://registry.npmjs.org';
95
+ private readonly searchUrl = 'https://registry.npmjs.com/-/v1/search';
96
+ private readonly config: NPMConfig;
97
+
98
+ constructor(config: NPMConfig = {}) {
99
+ this.config = {
100
+ maxResults: 20,
101
+ includeDetails: true,
102
+ quality: 0.3,
103
+ popularity: 0.1,
104
+ ...config,
105
+ };
106
+ }
107
+
108
+ async search(query: string, maxResults?: number): Promise<SearchResult[]> {
109
+ const startTime = Date.now();
110
+ const limit = Math.min(maxResults || this.config.maxResults || 20, 100); // NPM search API limit
111
+
112
+ // Handle empty query
113
+ if (!query || query.trim().length === 0) {
114
+ elizaLogger.debug('[NPM] Empty query, returning no results');
115
+ return [];
116
+ }
117
+
118
+ try {
119
+ elizaLogger.info(`[NPM] Searching for: ${query}`);
120
+
121
+ const searchResponse = await axios.get(this.searchUrl, {
122
+ params: {
123
+ text: query,
124
+ size: limit,
125
+ quality: this.config.quality,
126
+ popularity: this.config.popularity,
127
+ },
128
+ headers: {
129
+ 'User-Agent': 'ElizaOS-Research-Agent/1.0',
130
+ },
131
+ timeout: 10000,
132
+ });
133
+
134
+ const searchData = NPMSearchSchema.parse(searchResponse.data);
135
+
136
+ // Get detailed information if requested
137
+ let results: SearchResult[] = [];
138
+
139
+ if (this.config.includeDetails) {
140
+ // Get detailed package info for top results
141
+ const topPackages = searchData.objects.slice(0, Math.min(limit, 10));
142
+ const detailedResults = await Promise.all(
143
+ topPackages.map(async (item) => {
144
+ const details = await this.getPackageDetails(item.package.name);
145
+ return this.convertToSearchResult(item, details);
146
+ })
147
+ );
148
+ results = detailedResults.filter(Boolean);
149
+ } else {
150
+ // Use search results directly
151
+ results = searchData.objects
152
+ .slice(0, limit)
153
+ .map((item) => this.convertToSearchResult(item, null));
154
+ }
155
+
156
+ const duration = Date.now() - startTime;
157
+ elizaLogger.info(`[NPM] Found ${results.length} results in ${duration}ms`);
158
+
159
+ return results;
160
+ } catch (error) {
161
+ const duration = Date.now() - startTime;
162
+ elizaLogger.error(`[NPM] Search failed after ${duration}ms:`, error);
163
+
164
+ // Wrap the error to prevent serialization issues
165
+ if (axios.isAxiosError(error)) {
166
+ throw new Error(`NPM search failed: ${error.message} (${error.response?.status || 'no status'})`);
167
+ }
168
+ throw new Error(`NPM search failed: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ }
171
+
172
+ private async getPackageDetails(packageName: string): Promise<any | null> {
173
+ try {
174
+ const response = await axios.get(`${this.registryUrl}/${encodeURIComponent(packageName)}`, {
175
+ headers: {
176
+ 'User-Agent': 'ElizaOS-Research-Agent/1.0',
177
+ },
178
+ timeout: 5000,
179
+ });
180
+
181
+ return NPMPackageSchema.parse(response.data);
182
+ } catch (error) {
183
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
184
+ elizaLogger.debug(`[NPM] Package ${packageName} not found`);
185
+ } else {
186
+ elizaLogger.warn(`[NPM] Failed to get details for ${packageName}:`,
187
+ error instanceof Error ? error.message : String(error));
188
+ }
189
+ return null;
190
+ }
191
+ }
192
+
193
+ private convertToSearchResult(item: any, details?: any): SearchResult {
194
+ const pkg = item.package;
195
+ const score = item.score || { final: 0.5, detail: { quality: 0.5, popularity: 0.5, maintenance: 0.5 } };
196
+
197
+ // Use detailed info if available, fallback to search result data
198
+ const packageData = details || pkg;
199
+ const packageUrl = `https://www.npmjs.com/package/${pkg.name}`;
200
+
201
+ // Extract author information
202
+ let author = '';
203
+ if (packageData.author) {
204
+ if (typeof packageData.author === 'string') {
205
+ author = packageData.author;
206
+ } else if (packageData.author.name) {
207
+ author = packageData.author.name;
208
+ }
209
+ }
210
+
211
+ // Extract repository URL
212
+ let repositoryUrl = '';
213
+ if (packageData.repository) {
214
+ if (typeof packageData.repository === 'string') {
215
+ repositoryUrl = packageData.repository;
216
+ } else if (packageData.repository.url) {
217
+ repositoryUrl = packageData.repository.url
218
+ .replace(/^git\+/, '')
219
+ .replace(/\.git$/, '')
220
+ .replace(/^git:\/\//, 'https://');
221
+ }
222
+ }
223
+
224
+ // Create comprehensive description
225
+ let description = pkg.description || packageData.description || '';
226
+ if (pkg.keywords && pkg.keywords.length > 0) {
227
+ description += `\nKeywords: ${pkg.keywords.join(', ')}`;
228
+ }
229
+ if (packageData.license) {
230
+ description += `\nLicense: ${packageData.license}`;
231
+ }
232
+
233
+ // Add quality metrics
234
+ let qualityInfo = '';
235
+ if (score.detail) {
236
+ qualityInfo = `\nQuality: ${(score.detail.quality * 100).toFixed(0)}% | ` +
237
+ `Popularity: ${(score.detail.popularity * 100).toFixed(0)}% | ` +
238
+ `Maintenance: ${(score.detail.maintenance * 100).toFixed(0)}%`;
239
+ }
240
+
241
+ return {
242
+ title: `${pkg.name} v${pkg.version}`,
243
+ url: packageUrl,
244
+ snippet: description.substring(0, 300) + (description.length > 300 ? '...' : ''),
245
+ content: description + qualityInfo,
246
+ score: score.final,
247
+ provider: 'npm',
248
+ metadata: {
249
+ language: 'javascript',
250
+ author: author ? [author] : [],
251
+ type: 'package',
252
+ domain: 'npmjs.com',
253
+ },
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Get specific package information by name
259
+ */
260
+ async getPackage(packageName: string): Promise<SearchResult | null> {
261
+ try {
262
+ const pkg = await this.getPackageDetails(packageName);
263
+ if (!pkg) return null;
264
+
265
+ // Create a mock search item for conversion
266
+ const mockSearchItem = {
267
+ package: {
268
+ name: pkg.name,
269
+ version: pkg['dist-tags']?.latest || pkg.version,
270
+ description: pkg.description,
271
+ keywords: pkg.keywords,
272
+ author: pkg.author,
273
+ links: {
274
+ npm: `https://www.npmjs.com/package/${pkg.name}`,
275
+ homepage: pkg.homepage,
276
+ },
277
+ },
278
+ score: {
279
+ final: 1.0, // Assume high relevance for direct lookup
280
+ detail: {
281
+ quality: 0.8,
282
+ popularity: 0.7,
283
+ maintenance: 0.8,
284
+ },
285
+ },
286
+ };
287
+
288
+ return this.convertToSearchResult(mockSearchItem, pkg);
289
+ } catch (error) {
290
+ elizaLogger.error(`[NPM] Failed to get package ${packageName}:`, error);
291
+ return null;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Search for packages by scope (e.g., @types, @angular)
297
+ */
298
+ async searchByScope(scope: string, maxResults?: number): Promise<SearchResult[]> {
299
+ const query = `scope:${scope}`;
300
+ return this.search(query, maxResults);
301
+ }
302
+
303
+ /**
304
+ * Search for packages by maintainer
305
+ */
306
+ async searchByMaintainer(maintainer: string, maxResults?: number): Promise<SearchResult[]> {
307
+ const query = `maintainer:${maintainer}`;
308
+ return this.search(query, maxResults);
309
+ }
310
+
311
+ /**
312
+ * Search for packages with specific keywords
313
+ */
314
+ async searchByKeywords(keywords: string[], maxResults?: number): Promise<SearchResult[]> {
315
+ const query = `keywords:${keywords.join(',')}`;
316
+ return this.search(query, maxResults);
317
+ }
318
+
319
+ /**
320
+ * Get trending/popular packages in a category
321
+ */
322
+ async getTrendingPackages(category?: string, maxResults?: number): Promise<SearchResult[]> {
323
+ let query = 'boost-exact:false';
324
+ if (category) {
325
+ query += ` ${category}`;
326
+ }
327
+
328
+ return this.search(query, maxResults);
329
+ }
330
+ }
@@ -0,0 +1,211 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { SearchResult } from '../../types';
3
+ import { elizaLogger } from '@elizaos/core';
4
+ import { z } from 'zod';
5
+
6
+ // PyPI API response schema validation
7
+ const PyPIPackageSchema = z.object({
8
+ info: z.object({
9
+ name: z.string(),
10
+ summary: z.string().optional(),
11
+ description: z.string().optional(),
12
+ home_page: z.string().optional(),
13
+ author: z.string().optional(),
14
+ author_email: z.string().optional(),
15
+ maintainer: z.string().optional(),
16
+ version: z.string(),
17
+ keywords: z.string().optional(),
18
+ license: z.string().optional(),
19
+ classifiers: z.array(z.string()).optional(),
20
+ project_urls: z.record(z.string()).optional(),
21
+ }),
22
+ urls: z.array(z.object({
23
+ filename: z.string(),
24
+ url: z.string(),
25
+ upload_time: z.string().optional(),
26
+ })).optional(),
27
+ });
28
+
29
+ const PyPISearchSchema = z.object({
30
+ projects: z.array(z.object({
31
+ name: z.string(),
32
+ version: z.string(),
33
+ description: z.string().optional(),
34
+ })),
35
+ });
36
+
37
+ export interface PyPIConfig {
38
+ maxResults?: number;
39
+ includeClassifiers?: boolean;
40
+ }
41
+
42
+ export class PyPISearchProvider {
43
+ public readonly name = 'pypi';
44
+ private readonly baseUrl = 'https://pypi.org/pypi';
45
+ private readonly searchUrl = 'https://pypi.org/search/';
46
+ private readonly config: PyPIConfig;
47
+
48
+ constructor(config: PyPIConfig = {}) {
49
+ this.config = {
50
+ maxResults: 20,
51
+ includeClassifiers: true,
52
+ ...config,
53
+ };
54
+ }
55
+
56
+ async search(query: string, maxResults?: number): Promise<SearchResult[]> {
57
+ const startTime = Date.now();
58
+ const limit = maxResults || this.config.maxResults || 20;
59
+
60
+ // Handle empty query
61
+ if (!query || query.trim().length === 0) {
62
+ elizaLogger.debug('[PyPI] Empty query, returning no results');
63
+ return [];
64
+ }
65
+
66
+ try {
67
+ elizaLogger.info(`[PyPI] Searching for: ${query}`);
68
+
69
+ // Search PyPI (no official search API, using HTML scraping as fallback)
70
+ // For production use, consider using a proper PyPI API client
71
+ const searchUrl = `https://pypi.org/search/?q=${encodeURIComponent(query)}`;
72
+ const searchResponse = await axios.get(searchUrl, {
73
+ headers: {
74
+ 'User-Agent': 'ElizaOS-Research-Agent/1.0',
75
+ },
76
+ timeout: 10000,
77
+ });
78
+
79
+ // Extract package names from search results
80
+ const packageMatches = searchResponse.data.match(
81
+ /href="\/project\/([^\/]+)\//g
82
+ ) || [];
83
+
84
+ const packages = packageMatches
85
+ .map((match: string) => {
86
+ const name = match.match(/href="\/project\/([^\/]+)\//)?.[1];
87
+ return name ? { name } : null;
88
+ })
89
+ .filter(Boolean)
90
+ .slice(0, limit);
91
+
92
+ // Get detailed information for each package
93
+ const results = await Promise.all(
94
+ packages.map(async (pkg: any, index: number) => {
95
+ const details = await this.getPackageDetails(pkg.name);
96
+ if (details) {
97
+ return this.convertToSearchResult(details, index);
98
+ }
99
+ return null;
100
+ })
101
+ );
102
+
103
+ const validResults = results.filter(Boolean) as SearchResult[];
104
+ const duration = Date.now() - startTime;
105
+ elizaLogger.info(`[PyPI] Found ${validResults.length} results in ${duration}ms`);
106
+
107
+ return validResults;
108
+ } catch (error) {
109
+ const duration = Date.now() - startTime;
110
+ elizaLogger.error(`[PyPI] Search failed after ${duration}ms:`, error);
111
+
112
+ // Wrap the error to prevent serialization issues
113
+ if (axios.isAxiosError(error)) {
114
+ throw new Error(`PyPI search failed: ${error.message} (${error.response?.status || 'no status'})`);
115
+ }
116
+ throw new Error(`PyPI search failed: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ }
119
+
120
+ private async getPackageDetails(packageName: string): Promise<any | null> {
121
+ try {
122
+ const response = await axios.get(`${this.baseUrl}/${packageName}/json`, {
123
+ headers: {
124
+ 'User-Agent': 'ElizaOS-Research-Agent/1.0',
125
+ },
126
+ timeout: 5000,
127
+ });
128
+
129
+ return PyPIPackageSchema.parse(response.data);
130
+ } catch (error) {
131
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
132
+ elizaLogger.debug(`[PyPI] Package ${packageName} not found`);
133
+ } else {
134
+ elizaLogger.warn(`[PyPI] Failed to get details for ${packageName}:`,
135
+ error instanceof Error ? error.message : String(error));
136
+ }
137
+ return null;
138
+ }
139
+ }
140
+
141
+ private convertToSearchResult(pkg: any, index: number): SearchResult {
142
+ const info = pkg.info;
143
+ const packageUrl = `https://pypi.org/project/${info.name}/`;
144
+
145
+ // Create a comprehensive description
146
+ let description = info.summary || info.description || '';
147
+ if (info.keywords) {
148
+ description += `\nKeywords: ${info.keywords}`;
149
+ }
150
+ if (info.license) {
151
+ description += `\nLicense: ${info.license}`;
152
+ }
153
+
154
+ // Extract relevant classifiers for additional context
155
+ let classifiers = '';
156
+ if (this.config.includeClassifiers && info.classifiers) {
157
+ const relevantClassifiers = info.classifiers
158
+ .filter((c: string) =>
159
+ c.includes('Development Status') ||
160
+ c.includes('Intended Audience') ||
161
+ c.includes('Programming Language') ||
162
+ c.includes('Topic')
163
+ )
164
+ .slice(0, 5);
165
+
166
+ if (relevantClassifiers.length > 0) {
167
+ classifiers = `\nClassifiers: ${relevantClassifiers.join(', ')}`;
168
+ }
169
+ }
170
+
171
+ return {
172
+ title: `${info.name} v${info.version}`,
173
+ url: packageUrl,
174
+ snippet: description.substring(0, 300) + (description.length > 300 ? '...' : ''),
175
+ content: description + classifiers,
176
+ score: Math.max(0.1, 1.0 - (index * 0.05)), // Decrease score based on result order
177
+ provider: 'pypi',
178
+ metadata: {
179
+ language: 'python',
180
+ author: info.author || info.maintainer ? [info.author || info.maintainer] : [],
181
+ type: 'package',
182
+ domain: 'pypi.org',
183
+ },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Get specific package information by name
189
+ */
190
+ async getPackage(packageName: string): Promise<SearchResult | null> {
191
+ try {
192
+ const pkg = await this.getPackageDetails(packageName);
193
+ if (!pkg) return null;
194
+
195
+ return this.convertToSearchResult(pkg, 0);
196
+ } catch (error) {
197
+ elizaLogger.error(`[PyPI] Failed to get package ${packageName}:`, error);
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Search for packages by category/classifier
204
+ */
205
+ async searchByCategory(category: string, maxResults?: number): Promise<SearchResult[]> {
206
+ // This would require scraping PyPI's browse page or using alternative APIs
207
+ // For now, return empty array and log the limitation
208
+ elizaLogger.warn('[PyPI] Category search not implemented - use keyword search instead');
209
+ return [];
210
+ }
211
+ }