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