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