@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,277 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { SearchResult } from '../../types';
|
|
3
|
+
import { elizaLogger } from '@elizaos/core';
|
|
4
|
+
|
|
5
|
+
export interface SerpAPIConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
country?: string;
|
|
8
|
+
location?: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
num?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SerpAPISearchProvider {
|
|
14
|
+
private readonly apiKey: string;
|
|
15
|
+
private readonly baseUrl = 'https://serpapi.com/search';
|
|
16
|
+
private readonly config: SerpAPIConfig;
|
|
17
|
+
public readonly name = 'SerpAPI';
|
|
18
|
+
|
|
19
|
+
constructor(config: SerpAPIConfig) {
|
|
20
|
+
if (!config.apiKey) {
|
|
21
|
+
throw new Error('SerpAPI key is required');
|
|
22
|
+
}
|
|
23
|
+
this.apiKey = config.apiKey;
|
|
24
|
+
this.config = {
|
|
25
|
+
country: 'us',
|
|
26
|
+
language: 'en',
|
|
27
|
+
num: 10,
|
|
28
|
+
...config,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async search(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
33
|
+
try {
|
|
34
|
+
elizaLogger.info(`[SerpAPI] Searching for: ${query}`);
|
|
35
|
+
|
|
36
|
+
const params: any = {
|
|
37
|
+
q: query,
|
|
38
|
+
api_key: this.apiKey,
|
|
39
|
+
engine: 'google',
|
|
40
|
+
num: maxResults || this.config.num,
|
|
41
|
+
gl: this.config.country,
|
|
42
|
+
hl: this.config.language,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Add location if specified
|
|
46
|
+
if (this.config.location) {
|
|
47
|
+
params['location'] = this.config.location;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const response = await axios.get(this.baseUrl, {
|
|
51
|
+
params,
|
|
52
|
+
timeout: 20000,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const data = response.data;
|
|
56
|
+
const results: SearchResult[] = [];
|
|
57
|
+
|
|
58
|
+
// Add answer box if available
|
|
59
|
+
if (data.answer_box?.answer) {
|
|
60
|
+
results.push({
|
|
61
|
+
title: data.answer_box.title || 'Direct Answer',
|
|
62
|
+
url: data.answer_box.link || '',
|
|
63
|
+
snippet: data.answer_box.answer,
|
|
64
|
+
score: 1.0,
|
|
65
|
+
provider: 'serpapi',
|
|
66
|
+
metadata: {
|
|
67
|
+
language: this.config.language || 'en',
|
|
68
|
+
type: 'answer_box',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Add knowledge graph if available
|
|
74
|
+
if (data.knowledge_graph) {
|
|
75
|
+
const kg = data.knowledge_graph;
|
|
76
|
+
results.push({
|
|
77
|
+
title: kg.title,
|
|
78
|
+
url: kg.source?.link || '',
|
|
79
|
+
snippet: kg.description || '',
|
|
80
|
+
content: JSON.stringify({
|
|
81
|
+
...kg,
|
|
82
|
+
source: 'knowledge_graph',
|
|
83
|
+
}),
|
|
84
|
+
score: 0.9,
|
|
85
|
+
provider: 'serpapi',
|
|
86
|
+
metadata: {
|
|
87
|
+
language: this.config.language || 'en',
|
|
88
|
+
type: 'knowledge_graph',
|
|
89
|
+
// Store kgmid in a generic property
|
|
90
|
+
...kg.kgmid ? { kgmid: kg.kgmid } : {},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add organic results
|
|
96
|
+
if (data.organic_results) {
|
|
97
|
+
results.push(
|
|
98
|
+
...data.organic_results.slice(0, maxResults || this.config.num).map((result: any, index: number) => ({
|
|
99
|
+
title: result.title || 'Untitled',
|
|
100
|
+
url: result.link,
|
|
101
|
+
snippet: result.snippet || '',
|
|
102
|
+
score: 0.8 - index * 0.05,
|
|
103
|
+
provider: 'serpapi',
|
|
104
|
+
metadata: {
|
|
105
|
+
language: this.config.language || 'en',
|
|
106
|
+
position: result.position,
|
|
107
|
+
date: result.date,
|
|
108
|
+
source: result.source,
|
|
109
|
+
cached_page_link: result.cached_page_link,
|
|
110
|
+
},
|
|
111
|
+
}))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
elizaLogger.info(`[SerpAPI] Found ${results.length} results`);
|
|
116
|
+
return results.slice(0, maxResults || this.config.num);
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
if (axios.isAxiosError(error)) {
|
|
119
|
+
if (error.response?.status === 401) {
|
|
120
|
+
elizaLogger.error('[SerpAPI] Invalid API key');
|
|
121
|
+
throw new Error('Invalid SerpAPI key');
|
|
122
|
+
} else if (error.response?.status === 429) {
|
|
123
|
+
elizaLogger.error('[SerpAPI] Rate limit exceeded');
|
|
124
|
+
throw new Error('SerpAPI rate limit exceeded');
|
|
125
|
+
}
|
|
126
|
+
elizaLogger.error(`[SerpAPI] API error: ${error.message}`, {
|
|
127
|
+
status: error.response?.status,
|
|
128
|
+
data: error.response?.data,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async searchNews(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
136
|
+
try {
|
|
137
|
+
elizaLogger.info(`[SerpAPI] Searching news for: ${query}`);
|
|
138
|
+
|
|
139
|
+
const params = {
|
|
140
|
+
q: query,
|
|
141
|
+
api_key: this.apiKey,
|
|
142
|
+
engine: 'google',
|
|
143
|
+
tbm: 'nws', // News search
|
|
144
|
+
num: maxResults || this.config.num,
|
|
145
|
+
gl: this.config.country,
|
|
146
|
+
hl: this.config.language,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const response = await axios.get(this.baseUrl, {
|
|
150
|
+
params,
|
|
151
|
+
timeout: 20000,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const data = response.data;
|
|
155
|
+
const results: SearchResult[] = [];
|
|
156
|
+
|
|
157
|
+
if (data.news_results) {
|
|
158
|
+
results.push(
|
|
159
|
+
...data.news_results.map((item: any, index: number) => ({
|
|
160
|
+
title: item.title,
|
|
161
|
+
url: item.link,
|
|
162
|
+
snippet: item.snippet || '',
|
|
163
|
+
score: 0.8 - index * 0.05,
|
|
164
|
+
provider: 'serpapi',
|
|
165
|
+
metadata: {
|
|
166
|
+
language: this.config.language || 'en',
|
|
167
|
+
type: 'news',
|
|
168
|
+
date: item.date,
|
|
169
|
+
source: item.source,
|
|
170
|
+
thumbnail: item.thumbnail,
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
elizaLogger.info(`[SerpAPI] Found ${results.length} news results`);
|
|
177
|
+
return results;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
elizaLogger.error('[SerpAPI] News search error:', error);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async searchScholar(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
185
|
+
try {
|
|
186
|
+
elizaLogger.info(`[SerpAPI] Searching Google Scholar for: ${query}`);
|
|
187
|
+
|
|
188
|
+
const params = {
|
|
189
|
+
q: query,
|
|
190
|
+
api_key: this.apiKey,
|
|
191
|
+
engine: 'google_scholar',
|
|
192
|
+
num: maxResults || this.config.num,
|
|
193
|
+
hl: this.config.language,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const response = await axios.get(this.baseUrl, {
|
|
197
|
+
params,
|
|
198
|
+
timeout: 20000,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const data = response.data;
|
|
202
|
+
const results: SearchResult[] = [];
|
|
203
|
+
|
|
204
|
+
if (data.organic_results) {
|
|
205
|
+
results.push(
|
|
206
|
+
...data.organic_results.map((item: any, index: number) => ({
|
|
207
|
+
title: item.title,
|
|
208
|
+
url: item.link,
|
|
209
|
+
snippet: item.snippet || item.publication_info?.summary || '',
|
|
210
|
+
content: JSON.stringify({
|
|
211
|
+
authors: item.publication_info?.authors,
|
|
212
|
+
cited_by: item.inline_links?.cited_by?.total,
|
|
213
|
+
related_pages_link: item.inline_links?.related_pages_link,
|
|
214
|
+
pdf_link: item.resources?.find((r: any) => r.file_format === 'PDF')?.link,
|
|
215
|
+
type: 'academic',
|
|
216
|
+
}),
|
|
217
|
+
score: 0.9 - index * 0.05,
|
|
218
|
+
provider: 'serpapi',
|
|
219
|
+
metadata: {
|
|
220
|
+
language: this.config.language || 'en',
|
|
221
|
+
type: 'academic',
|
|
222
|
+
author: item.publication_info?.authors?.join(', '),
|
|
223
|
+
publishDate: item.publication_info?.summary,
|
|
224
|
+
citations: item.inline_links?.cited_by?.total,
|
|
225
|
+
},
|
|
226
|
+
}))
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
elizaLogger.info(`[SerpAPI] Found ${results.length} scholar results`);
|
|
231
|
+
return results;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
elizaLogger.error('[SerpAPI] Scholar search error:', error);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async searchImages(query: string, maxResults?: number): Promise<Array<{ url: string; title: string; source: string }>> {
|
|
239
|
+
try {
|
|
240
|
+
elizaLogger.info(`[SerpAPI] Searching images for: ${query}`);
|
|
241
|
+
|
|
242
|
+
const params = {
|
|
243
|
+
q: query,
|
|
244
|
+
api_key: this.apiKey,
|
|
245
|
+
engine: 'google',
|
|
246
|
+
tbm: 'isch', // Image search
|
|
247
|
+
num: maxResults || 10,
|
|
248
|
+
gl: this.config.country,
|
|
249
|
+
hl: this.config.language,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const response = await axios.get(this.baseUrl, {
|
|
253
|
+
params,
|
|
254
|
+
timeout: 20000,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const data = response.data;
|
|
258
|
+
const results: Array<{ url: string; title: string; source: string }> = [];
|
|
259
|
+
|
|
260
|
+
if (data.images_results) {
|
|
261
|
+
results.push(
|
|
262
|
+
...data.images_results.slice(0, maxResults || 10).map((img: any) => ({
|
|
263
|
+
url: img.original || img.link,
|
|
264
|
+
title: img.title || 'Untitled',
|
|
265
|
+
source: img.source || img.link,
|
|
266
|
+
}))
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
elizaLogger.info(`[SerpAPI] Found ${results.length} image results`);
|
|
271
|
+
return results;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
elizaLogger.error('[SerpAPI] Image search error:', error);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
import { SearchResult } from '../../types';
|
|
3
|
+
import { elizaLogger } from '@elizaos/core';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
// Serper API response schema validation
|
|
7
|
+
const SerperOrganicResultSchema = z.object({
|
|
8
|
+
title: z.string(),
|
|
9
|
+
link: z.string(),
|
|
10
|
+
snippet: z.string().optional(),
|
|
11
|
+
position: z.number(),
|
|
12
|
+
date: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const SerperKnowledgeGraphSchema = z.object({
|
|
16
|
+
title: z.string(),
|
|
17
|
+
type: z.string().optional(),
|
|
18
|
+
website: z.string().optional(),
|
|
19
|
+
description: z.string().optional(),
|
|
20
|
+
descriptionSource: z.string().optional(),
|
|
21
|
+
imageUrl: z.string().optional(),
|
|
22
|
+
attributes: z.record(z.string()).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const SerperResponseSchema = z.object({
|
|
26
|
+
searchParameters: z.object({
|
|
27
|
+
q: z.string(),
|
|
28
|
+
type: z.string().optional(),
|
|
29
|
+
engine: z.string().optional(),
|
|
30
|
+
}),
|
|
31
|
+
organic: z.array(SerperOrganicResultSchema).optional(),
|
|
32
|
+
knowledgeGraph: SerperKnowledgeGraphSchema.optional(),
|
|
33
|
+
answerBox: z
|
|
34
|
+
.object({
|
|
35
|
+
title: z.string().optional(),
|
|
36
|
+
answer: z.string().optional(),
|
|
37
|
+
snippet: z.string().optional(),
|
|
38
|
+
link: z.string().optional(),
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
searchInformation: z
|
|
42
|
+
.object({
|
|
43
|
+
totalResults: z.string().optional(),
|
|
44
|
+
timeTaken: z.number().optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export interface SerperConfig {
|
|
50
|
+
apiKey: string;
|
|
51
|
+
country?: string;
|
|
52
|
+
location?: string;
|
|
53
|
+
language?: string;
|
|
54
|
+
num?: number;
|
|
55
|
+
autocorrect?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class SerperSearchProvider {
|
|
59
|
+
private readonly apiKey: string;
|
|
60
|
+
private readonly baseUrl = 'https://google.serper.dev/search';
|
|
61
|
+
private readonly config: SerperConfig;
|
|
62
|
+
|
|
63
|
+
constructor(config: SerperConfig) {
|
|
64
|
+
if (!config.apiKey) {
|
|
65
|
+
throw new Error('Serper API key is required');
|
|
66
|
+
}
|
|
67
|
+
this.apiKey = config.apiKey;
|
|
68
|
+
this.config = {
|
|
69
|
+
country: 'us',
|
|
70
|
+
language: 'en',
|
|
71
|
+
num: 10,
|
|
72
|
+
autocorrect: true,
|
|
73
|
+
...config,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async search(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
elizaLogger.info(`[Serper] Searching for: ${query}`);
|
|
82
|
+
|
|
83
|
+
const response = await axios.post(
|
|
84
|
+
this.baseUrl,
|
|
85
|
+
{
|
|
86
|
+
q: query,
|
|
87
|
+
num: maxResults || this.config.num,
|
|
88
|
+
gl: this.config.country,
|
|
89
|
+
hl: this.config.language,
|
|
90
|
+
autocorrect: this.config.autocorrect,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
headers: {
|
|
94
|
+
'X-API-KEY': this.apiKey,
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
timeout: 20000, // 20 second timeout
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Validate response
|
|
102
|
+
const validatedData = SerperResponseSchema.parse(response.data);
|
|
103
|
+
|
|
104
|
+
const results: SearchResult[] = [];
|
|
105
|
+
|
|
106
|
+
// Add answer box if available
|
|
107
|
+
if (validatedData.answerBox?.answer) {
|
|
108
|
+
results.push({
|
|
109
|
+
title: validatedData.answerBox.title || 'Direct Answer',
|
|
110
|
+
url: validatedData.answerBox.link || '',
|
|
111
|
+
snippet: validatedData.answerBox.answer,
|
|
112
|
+
content: validatedData.answerBox.snippet || validatedData.answerBox.answer,
|
|
113
|
+
score: 1.0, // Answer box has highest relevance
|
|
114
|
+
provider: 'serper',
|
|
115
|
+
metadata: {
|
|
116
|
+
language: this.config.language || 'en',
|
|
117
|
+
type: 'answer_box',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add knowledge graph if available
|
|
123
|
+
if (validatedData.knowledgeGraph) {
|
|
124
|
+
results.push({
|
|
125
|
+
title: validatedData.knowledgeGraph.title,
|
|
126
|
+
url: validatedData.knowledgeGraph.website || '',
|
|
127
|
+
snippet: validatedData.knowledgeGraph.description || '',
|
|
128
|
+
content: JSON.stringify({
|
|
129
|
+
...validatedData.knowledgeGraph,
|
|
130
|
+
source: 'knowledge_graph',
|
|
131
|
+
}),
|
|
132
|
+
score: 0.9, // Knowledge graph is highly relevant
|
|
133
|
+
provider: 'serper',
|
|
134
|
+
metadata: {
|
|
135
|
+
language: this.config.language || 'en',
|
|
136
|
+
type: 'knowledge_graph',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add organic results
|
|
142
|
+
if (validatedData.organic) {
|
|
143
|
+
results.push(
|
|
144
|
+
...validatedData.organic.map((result, index) => ({
|
|
145
|
+
title: result.title,
|
|
146
|
+
url: result.link,
|
|
147
|
+
snippet: result.snippet || '',
|
|
148
|
+
content: undefined, // Serper doesn't provide full content
|
|
149
|
+
score: 0.8 - index * 0.05, // Decreasing score by position
|
|
150
|
+
provider: 'serper',
|
|
151
|
+
metadata: {
|
|
152
|
+
language: this.config.language || 'en',
|
|
153
|
+
position: result.position,
|
|
154
|
+
date: result.date,
|
|
155
|
+
},
|
|
156
|
+
}))
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const duration = Date.now() - startTime;
|
|
161
|
+
elizaLogger.info(`[Serper] Found ${results.length} results in ${duration}ms`);
|
|
162
|
+
|
|
163
|
+
return results.slice(0, maxResults || this.config.num);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const duration = Date.now() - startTime;
|
|
166
|
+
|
|
167
|
+
if (axios.isAxiosError(error)) {
|
|
168
|
+
const axiosError = error as AxiosError;
|
|
169
|
+
|
|
170
|
+
// Handle specific error cases
|
|
171
|
+
if (axiosError.response?.status === 401) {
|
|
172
|
+
elizaLogger.error('[Serper] Invalid API key');
|
|
173
|
+
throw new Error('Invalid Serper API key');
|
|
174
|
+
} else if (axiosError.response?.status === 429) {
|
|
175
|
+
elizaLogger.error('[Serper] Rate limit exceeded');
|
|
176
|
+
throw new Error('Serper rate limit exceeded');
|
|
177
|
+
} else if (axiosError.response?.status === 403) {
|
|
178
|
+
elizaLogger.error('[Serper] Forbidden - check API key permissions', {
|
|
179
|
+
data: axiosError.response?.data,
|
|
180
|
+
headers: axiosError.response?.headers
|
|
181
|
+
});
|
|
182
|
+
throw new Error(`Serper API access forbidden: ${JSON.stringify(axiosError.response?.data)}`);
|
|
183
|
+
} else if (axiosError.code === 'ECONNABORTED') {
|
|
184
|
+
elizaLogger.error(`[Serper] Request timeout after ${duration}ms`);
|
|
185
|
+
throw new Error('Serper search timeout');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
elizaLogger.error(`[Serper] API error: ${axiosError.message}`, {
|
|
189
|
+
status: axiosError.response?.status,
|
|
190
|
+
data: axiosError.response?.data,
|
|
191
|
+
});
|
|
192
|
+
} else if (error instanceof z.ZodError) {
|
|
193
|
+
elizaLogger.error('[Serper] Invalid response format:', error.issues);
|
|
194
|
+
throw new Error('Invalid Serper API response format');
|
|
195
|
+
} else {
|
|
196
|
+
elizaLogger.error('[Serper] Unknown error:', error);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async searchNews(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
elizaLogger.info(`[Serper] Searching news for: ${query}`);
|
|
208
|
+
|
|
209
|
+
const response = await axios.post(
|
|
210
|
+
'https://google.serper.dev/news',
|
|
211
|
+
{
|
|
212
|
+
q: query,
|
|
213
|
+
num: maxResults || this.config.num,
|
|
214
|
+
gl: this.config.country,
|
|
215
|
+
hl: this.config.language,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
headers: {
|
|
219
|
+
'X-API-KEY': this.apiKey,
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
timeout: 20000,
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const results: SearchResult[] =
|
|
227
|
+
response.data.news?.map((item: any, index: number) => ({
|
|
228
|
+
title: item.title,
|
|
229
|
+
url: item.link,
|
|
230
|
+
snippet: item.snippet || '',
|
|
231
|
+
content: JSON.stringify({
|
|
232
|
+
date: item.date,
|
|
233
|
+
source: item.source,
|
|
234
|
+
imageUrl: item.imageUrl,
|
|
235
|
+
}),
|
|
236
|
+
score: 0.8 - index * 0.05,
|
|
237
|
+
provider: 'serper',
|
|
238
|
+
metadata: {
|
|
239
|
+
language: this.config.language || 'en',
|
|
240
|
+
type: 'news',
|
|
241
|
+
date: item.date,
|
|
242
|
+
source: item.source,
|
|
243
|
+
},
|
|
244
|
+
})) || [];
|
|
245
|
+
|
|
246
|
+
const duration = Date.now() - startTime;
|
|
247
|
+
elizaLogger.info(`[Serper] Found ${results.length} news results in ${duration}ms`);
|
|
248
|
+
|
|
249
|
+
return results;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
elizaLogger.error('[Serper] News search error:', error);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async searchImages(
|
|
257
|
+
query: string,
|
|
258
|
+
maxResults?: number
|
|
259
|
+
): Promise<Array<{ url: string; title: string; source: string }>> {
|
|
260
|
+
try {
|
|
261
|
+
elizaLogger.info(`[Serper] Searching images for: ${query}`);
|
|
262
|
+
|
|
263
|
+
const response = await axios.post(
|
|
264
|
+
'https://google.serper.dev/images',
|
|
265
|
+
{
|
|
266
|
+
q: query,
|
|
267
|
+
num: maxResults || 10,
|
|
268
|
+
gl: this.config.country,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
headers: {
|
|
272
|
+
'X-API-KEY': this.apiKey,
|
|
273
|
+
'Content-Type': 'application/json',
|
|
274
|
+
},
|
|
275
|
+
timeout: 20000,
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
response.data.images?.map((img: any) => ({
|
|
281
|
+
url: img.imageUrl,
|
|
282
|
+
title: img.title,
|
|
283
|
+
source: img.source,
|
|
284
|
+
})) || []
|
|
285
|
+
);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
elizaLogger.error('[Serper] Image search error:', error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async searchScholar(query: string, maxResults?: number): Promise<SearchResult[]> {
|
|
293
|
+
try {
|
|
294
|
+
elizaLogger.info(`[Serper] Searching Google Scholar for: ${query}`);
|
|
295
|
+
|
|
296
|
+
const response = await axios.post(
|
|
297
|
+
'https://google.serper.dev/scholar',
|
|
298
|
+
{
|
|
299
|
+
q: query,
|
|
300
|
+
num: maxResults || this.config.num,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
headers: {
|
|
304
|
+
'X-API-KEY': this.apiKey,
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
},
|
|
307
|
+
timeout: 20000,
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const results: SearchResult[] =
|
|
312
|
+
response.data.organic?.map((item: any, index: number) => ({
|
|
313
|
+
title: item.title,
|
|
314
|
+
url: item.link,
|
|
315
|
+
snippet: item.snippet || item.publication_info?.summary || '',
|
|
316
|
+
content: JSON.stringify({
|
|
317
|
+
authors: item.publication_info?.authors,
|
|
318
|
+
year: item.year,
|
|
319
|
+
citations: item.inline_links?.cited_by?.total,
|
|
320
|
+
type: 'academic',
|
|
321
|
+
}),
|
|
322
|
+
score: 0.9 - index * 0.05, // Academic results have higher base score
|
|
323
|
+
provider: 'serper',
|
|
324
|
+
metadata: {
|
|
325
|
+
language: this.config.language || 'en',
|
|
326
|
+
type: 'academic',
|
|
327
|
+
author: item.publication_info?.authors,
|
|
328
|
+
publishDate: item.year,
|
|
329
|
+
},
|
|
330
|
+
})) || [];
|
|
331
|
+
|
|
332
|
+
elizaLogger.info(`[Serper] Found ${results.length} scholar results`);
|
|
333
|
+
|
|
334
|
+
return results;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
elizaLogger.error('[Serper] Scholar search error:', error);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get current API usage
|
|
342
|
+
async getUsage(): Promise<{ searches: number; limit: number; remaining: number } | null> {
|
|
343
|
+
try {
|
|
344
|
+
const response = await axios.get('https://google.serper.dev/account', {
|
|
345
|
+
headers: { 'X-API-KEY': this.apiKey },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
searches: response.data.searches || 0,
|
|
350
|
+
limit: response.data.limit || 0,
|
|
351
|
+
remaining: response.data.remaining || 0,
|
|
352
|
+
};
|
|
353
|
+
} catch (error) {
|
|
354
|
+
elizaLogger.warn('[Serper] Could not fetch usage data');
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|