@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,87 @@
1
+ import { elizaLogger } from '@elizaos/core';
2
+ import { SearchResult } from '../../types';
3
+
4
+ export class StagehandGoogleSearchProvider {
5
+ public readonly name = 'StagehandGoogle';
6
+
7
+ constructor(private stagehandService: any) {}
8
+
9
+ async search(query: string, maxResults: number = 10): Promise<SearchResult[]> {
10
+ try {
11
+ elizaLogger.info(`[StagehandGoogle] Searching for: ${query}`);
12
+
13
+ // Get or create a Stagehand session
14
+ const session = await this.stagehandService.getCurrentSession() ||
15
+ await this.stagehandService.createSession(`search-${Date.now()}`);
16
+
17
+ // Navigate to Google
18
+ await session.page.goto('https://www.google.com', { waitUntil: 'networkidle' });
19
+
20
+ // Accept cookies if needed (for EU users)
21
+ try {
22
+ await session.page.click('button#L2AGLb', { timeout: 2000 });
23
+ } catch (e) {
24
+ // Cookie banner might not be present
25
+ }
26
+
27
+ // Type search query
28
+ await session.stagehand.act({
29
+ action: 'type',
30
+ selector: 'textarea[name="q"], input[name="q"]',
31
+ text: query
32
+ });
33
+
34
+ // Submit search
35
+ await session.page.keyboard.press('Enter');
36
+ await session.page.waitForNavigation({ waitUntil: 'networkidle' });
37
+
38
+ // Extract search results using Stagehand's AI extraction
39
+ const searchResults = await session.stagehand.extract({
40
+ instruction: `Extract the top ${maxResults} organic search results.
41
+ For each result, get the title, URL, and snippet/description.
42
+ Skip ads, "People also ask", and other non-organic results.`,
43
+ schema: {
44
+ results: [{
45
+ title: 'string',
46
+ url: 'string',
47
+ snippet: 'string'
48
+ }]
49
+ }
50
+ });
51
+
52
+ if (!searchResults.results || searchResults.results.length === 0) {
53
+ // Fallback to manual extraction
54
+ const results = await session.page.evaluate(() => {
55
+ const items: any[] = [];
56
+ const searchResults = document.querySelectorAll('div[data-async-context] > div');
57
+
58
+ searchResults.forEach((result) => {
59
+ const titleElement = result.querySelector('h3');
60
+ const linkElement = result.querySelector('a[href]');
61
+ const snippetElement = result.querySelector('span[style*="-webkit-line-clamp"]');
62
+
63
+ if (titleElement && linkElement) {
64
+ items.push({
65
+ title: titleElement.textContent || '',
66
+ url: linkElement.getAttribute('href') || '',
67
+ snippet: snippetElement?.textContent || ''
68
+ });
69
+ }
70
+ });
71
+
72
+ return items;
73
+ });
74
+
75
+ elizaLogger.info(`[StagehandGoogle] Found ${results.length} results via DOM extraction`);
76
+ return results.slice(0, maxResults);
77
+ }
78
+
79
+ elizaLogger.info(`[StagehandGoogle] Found ${searchResults.results.length} results via AI extraction`);
80
+ return searchResults.results.slice(0, maxResults);
81
+
82
+ } catch (error) {
83
+ elizaLogger.error('[StagehandGoogle] Search error:', error);
84
+ throw error;
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,187 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { SearchResult } from '../../types';
3
+ import { elizaLogger } from '@elizaos/core';
4
+ import { z } from 'zod';
5
+
6
+ // Tavily API response schema validation
7
+ const TavilyResultSchema = z.object({
8
+ title: z.string().optional(),
9
+ url: z.string(),
10
+ content: z.string().optional(),
11
+ snippet: z.string().optional(),
12
+ raw_content: z.string().nullable().optional(),
13
+ score: z.number().optional(),
14
+ });
15
+
16
+ const TavilyResponseSchema = z.object({
17
+ query: z.string(),
18
+ results: z.array(TavilyResultSchema),
19
+ answer: z.string().optional(),
20
+ follow_up_questions: z.array(z.string()).nullable().optional(),
21
+ images: z
22
+ .array(
23
+ z.union([
24
+ z.string(),
25
+ z.object({
26
+ url: z.string(),
27
+ description: z.string().optional(),
28
+ })
29
+ ])
30
+ )
31
+ .optional(),
32
+ });
33
+
34
+ export interface TavilyConfig {
35
+ apiKey: string;
36
+ searchDepth?: 'basic' | 'advanced';
37
+ includeAnswer?: boolean;
38
+ includeRawContent?: boolean;
39
+ maxResults?: number;
40
+ includeImages?: boolean;
41
+ useCache?: boolean;
42
+ }
43
+
44
+ export class TavilySearchProvider {
45
+ private readonly apiKey: string;
46
+ private readonly baseUrl = 'https://api.tavily.com/search';
47
+ private readonly config: TavilyConfig;
48
+
49
+ constructor(config: TavilyConfig) {
50
+ if (!config.apiKey) {
51
+ throw new Error('Tavily API key is required');
52
+ }
53
+ this.apiKey = config.apiKey;
54
+ this.config = {
55
+ searchDepth: 'advanced',
56
+ includeAnswer: true,
57
+ includeRawContent: true,
58
+ maxResults: 10,
59
+ includeImages: false,
60
+ useCache: true,
61
+ ...config,
62
+ };
63
+ }
64
+
65
+ async search(query: string, maxResults?: number): Promise<SearchResult[]> {
66
+ const startTime = Date.now();
67
+
68
+ try {
69
+ elizaLogger.info(`[Tavily] Searching for: ${query}`);
70
+
71
+ const response = await axios.post(
72
+ this.baseUrl,
73
+ {
74
+ api_key: this.apiKey,
75
+ query,
76
+ search_depth: this.config.searchDepth,
77
+ include_answer: this.config.includeAnswer,
78
+ include_raw_content: this.config.includeRawContent,
79
+ max_results: maxResults || this.config.maxResults,
80
+ include_images: this.config.includeImages,
81
+ },
82
+ {
83
+ timeout: 30000, // 30 second timeout
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ }
88
+ );
89
+
90
+ // Check if response has error detail
91
+ if (response.data?.detail?.error) {
92
+ throw new Error(response.data.detail.error);
93
+ }
94
+
95
+ // Check if response has an error field
96
+ if (response.data?.error) {
97
+ throw new Error(response.data.error);
98
+ }
99
+
100
+ // Validate response
101
+ const validatedData = TavilyResponseSchema.parse(response.data);
102
+
103
+ const results: SearchResult[] = validatedData.results.map((result) => ({
104
+ title: result.title || new URL(result.url).hostname || 'Untitled',
105
+ url: result.url,
106
+ snippet: result.snippet || result.content?.substring(0, 200) || '',
107
+ content: result.raw_content || result.content,
108
+ score: result.score || 0.5,
109
+ provider: 'tavily',
110
+ metadata: {
111
+ language: 'en',
112
+ domain: new URL(result.url).hostname,
113
+ },
114
+ }));
115
+
116
+ const duration = Date.now() - startTime;
117
+ elizaLogger.info(`[Tavily] Found ${results.length} results in ${duration}ms`);
118
+
119
+ return results;
120
+ } catch (error) {
121
+ const duration = Date.now() - startTime;
122
+
123
+ if (axios.isAxiosError(error)) {
124
+ const axiosError = error as AxiosError;
125
+
126
+ // Handle specific error cases
127
+ if (axiosError.response?.status === 401) {
128
+ elizaLogger.error('[Tavily] Invalid API key');
129
+ throw new Error('Invalid Tavily API key');
130
+ } else if (axiosError.response?.status === 429) {
131
+ elizaLogger.error('[Tavily] Rate limit exceeded');
132
+ throw new Error('Tavily rate limit exceeded');
133
+ } else if (axiosError.code === 'ECONNABORTED') {
134
+ elizaLogger.error(`[Tavily] Request timeout after ${duration}ms`);
135
+ throw new Error('Tavily search timeout');
136
+ }
137
+
138
+ elizaLogger.error(`[Tavily] API error: ${axiosError.message}`, {
139
+ status: axiosError.response?.status,
140
+ data: axiosError.response?.data,
141
+ });
142
+ } else if (error instanceof z.ZodError) {
143
+ elizaLogger.error('[Tavily] Invalid response format:', error.issues);
144
+ throw new Error('Invalid Tavily API response format');
145
+ } else {
146
+ elizaLogger.error('[Tavily] Unknown error:', error);
147
+ }
148
+
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ async searchWithRetry(query: string, maxRetries: number = 3): Promise<SearchResult[]> {
154
+ let lastError: Error | null = null;
155
+
156
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
157
+ try {
158
+ return await this.search(query);
159
+ } catch (error) {
160
+ lastError = error as Error;
161
+
162
+ if (attempt < maxRetries) {
163
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
164
+ elizaLogger.warn(`[Tavily] Retry attempt ${attempt} after ${delay}ms`);
165
+ await new Promise((resolve) => setTimeout(resolve, delay));
166
+ }
167
+ }
168
+ }
169
+
170
+ throw lastError || new Error('Search failed after retries');
171
+ }
172
+
173
+ // Get current API usage (if Tavily provides this endpoint)
174
+ async getUsage(): Promise<{ searches: number; limit: number } | null> {
175
+ try {
176
+ // Note: This is a hypothetical endpoint - check Tavily docs
177
+ const response = await axios.get('https://api.tavily.com/usage', {
178
+ headers: { Authorization: `Bearer ${this.apiKey}` },
179
+ });
180
+
181
+ return response.data;
182
+ } catch (error) {
183
+ elizaLogger.warn('[Tavily] Could not fetch usage data');
184
+ return null;
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,353 @@
1
+ import { elizaLogger, IAgentRuntime, ModelType } from '@elizaos/core';
2
+ import { SearchResult, ResearchSource, ResearchFinding } from '../types';
3
+
4
+ export interface RelevanceScore {
5
+ score: number; // 0-1
6
+ reasoning: string;
7
+ queryAlignment: number; // How well it addresses the query
8
+ topicRelevance: number; // How relevant to the topic
9
+ specificity: number; // How specific vs generic
10
+ }
11
+
12
+ export interface RelevanceAnalysis {
13
+ queryIntent: string;
14
+ keyTopics: string[];
15
+ requiredElements: string[];
16
+ exclusionCriteria: string[];
17
+ }
18
+
19
+ /**
20
+ * Analyzes and scores relevance of search results and findings to the original research query
21
+ */
22
+ export class RelevanceAnalyzer {
23
+ constructor(private runtime: IAgentRuntime) {}
24
+
25
+ /**
26
+ * Analyze the research query to understand what constitutes relevance
27
+ */
28
+ async analyzeQueryRelevance(query: string): Promise<RelevanceAnalysis> {
29
+ elizaLogger.info(`[RelevanceAnalyzer] Analyzing query intent: ${query}`);
30
+
31
+ const prompt = `Analyze this research query to define what makes a source or finding relevant:
32
+
33
+ Query: "${query}"
34
+
35
+ Extract:
36
+ 1. Query Intent: What is the user really asking for?
37
+ 2. Key Topics: Core topics that MUST be addressed
38
+ 3. Required Elements: Specific elements that relevant sources should contain
39
+ 4. Exclusion Criteria: What should be filtered out as irrelevant
40
+
41
+ Format as JSON:
42
+ {
43
+ "queryIntent": "clear statement of what user wants",
44
+ "keyTopics": ["topic1", "topic2", "topic3"],
45
+ "requiredElements": ["element1", "element2"],
46
+ "exclusionCriteria": ["avoid1", "avoid2"]
47
+ }`;
48
+
49
+ try {
50
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
51
+ messages: [
52
+ {
53
+ role: 'system',
54
+ content: 'You are a research query analyst. Extract query intent and relevance criteria precisely.'
55
+ },
56
+ { role: 'user', content: prompt }
57
+ ],
58
+ temperature: 0.3,
59
+ });
60
+
61
+ const responseContent = typeof response === 'string' ? response : (response as any).content || '';
62
+ const jsonMatch = responseContent.match(/\{[\s\S]*\}/);
63
+
64
+ if (jsonMatch) {
65
+ const analysis = JSON.parse(jsonMatch[0]);
66
+ elizaLogger.info(`[RelevanceAnalyzer] Query analysis complete:`, {
67
+ intent: analysis.queryIntent,
68
+ keyTopicsCount: analysis.keyTopics?.length || 0,
69
+ requiredElementsCount: analysis.requiredElements?.length || 0
70
+ });
71
+ return analysis;
72
+ }
73
+ } catch (error) {
74
+ elizaLogger.error('[RelevanceAnalyzer] Failed to analyze query relevance:', error);
75
+ }
76
+
77
+ // Fallback analysis
78
+ return {
79
+ queryIntent: query,
80
+ keyTopics: this.extractKeywordsFromQuery(query),
81
+ requiredElements: [],
82
+ exclusionCriteria: []
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Score search result relevance before content extraction
88
+ */
89
+ async scoreSearchResultRelevance(
90
+ result: SearchResult,
91
+ queryAnalysis: RelevanceAnalysis
92
+ ): Promise<RelevanceScore> {
93
+ elizaLogger.debug(`[RelevanceAnalyzer] Scoring search result: ${result.title}`);
94
+
95
+ const prompt = `Score the relevance of this search result to the research query:
96
+
97
+ QUERY INTENT: ${queryAnalysis.queryIntent}
98
+ KEY TOPICS: ${queryAnalysis.keyTopics.join(', ')}
99
+ REQUIRED ELEMENTS: ${queryAnalysis.requiredElements.join(', ')}
100
+
101
+ SEARCH RESULT:
102
+ Title: ${result.title}
103
+ Snippet: ${result.snippet}
104
+ URL: ${result.url}
105
+
106
+ Rate 0-1 for each dimension:
107
+ 1. Query Alignment: How directly does this address the query intent?
108
+ 2. Topic Relevance: How well does it cover the key topics?
109
+ 3. Specificity: How specific (vs generic) is the content to the query?
110
+
111
+ Provide reasoning for the scores.
112
+
113
+ Format as JSON:
114
+ {
115
+ "queryAlignment": 0.8,
116
+ "topicRelevance": 0.9,
117
+ "specificity": 0.7,
118
+ "reasoning": "detailed explanation of why this result is/isn't relevant",
119
+ "score": 0.8
120
+ }`;
121
+
122
+ try {
123
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
124
+ messages: [
125
+ {
126
+ role: 'system',
127
+ content: 'You are a search result relevance scorer. Be critical - only high relevance should get high scores.'
128
+ },
129
+ { role: 'user', content: prompt }
130
+ ],
131
+ temperature: 0.2,
132
+ });
133
+
134
+ const responseContent = typeof response === 'string' ? response : (response as any).content || '';
135
+ const jsonMatch = responseContent.match(/\{[\s\S]*\}/);
136
+
137
+ if (jsonMatch) {
138
+ const score = JSON.parse(jsonMatch[0]);
139
+ const finalScore = (score.queryAlignment + score.topicRelevance + score.specificity) / 3;
140
+
141
+ elizaLogger.debug(`[RelevanceAnalyzer] Search result scored:`, {
142
+ url: result.url,
143
+ score: finalScore,
144
+ breakdown: {
145
+ queryAlignment: score.queryAlignment,
146
+ topicRelevance: score.topicRelevance,
147
+ specificity: score.specificity
148
+ }
149
+ });
150
+
151
+ return {
152
+ score: finalScore,
153
+ reasoning: score.reasoning,
154
+ queryAlignment: score.queryAlignment,
155
+ topicRelevance: score.topicRelevance,
156
+ specificity: score.specificity
157
+ };
158
+ }
159
+ } catch (error) {
160
+ elizaLogger.error('[RelevanceAnalyzer] Failed to score search result:', error);
161
+ }
162
+
163
+ // Fallback: Simple keyword matching
164
+ const titleScore = this.calculateKeywordScore(result.title, queryAnalysis.keyTopics);
165
+ const snippetScore = this.calculateKeywordScore(result.snippet, queryAnalysis.keyTopics);
166
+ const fallbackScore = (titleScore + snippetScore) / 2;
167
+
168
+ return {
169
+ score: fallbackScore,
170
+ reasoning: `Fallback keyword scoring: title=${titleScore.toFixed(2)}, snippet=${snippetScore.toFixed(2)}`,
171
+ queryAlignment: fallbackScore,
172
+ topicRelevance: fallbackScore,
173
+ specificity: 0.5
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Score finding relevance after extraction
179
+ */
180
+ async scoreFindingRelevance(
181
+ finding: ResearchFinding,
182
+ queryAnalysis: RelevanceAnalysis,
183
+ originalQuery: string
184
+ ): Promise<RelevanceScore> {
185
+ elizaLogger.debug(`[RelevanceAnalyzer] Scoring finding relevance`);
186
+
187
+ const prompt = `Score how well this research finding answers the original query:
188
+
189
+ ORIGINAL QUERY: "${originalQuery}"
190
+ QUERY INTENT: ${queryAnalysis.queryIntent}
191
+ KEY TOPICS: ${queryAnalysis.keyTopics.join(', ')}
192
+
193
+ FINDING:
194
+ Content: ${finding.content}
195
+ Category: ${finding.category}
196
+ Source: ${finding.source.title}
197
+
198
+ Critical Assessment:
199
+ 1. Does this finding DIRECTLY address the query intent?
200
+ 2. Does it cover the key topics meaningfully?
201
+ 3. Is it specific to the query or generic information?
202
+ 4. Would this help someone answer the original question?
203
+
204
+ Rate 0-1 for each dimension and overall relevance.
205
+
206
+ Format as JSON:
207
+ {
208
+ "queryAlignment": 0.8,
209
+ "topicRelevance": 0.9,
210
+ "specificity": 0.7,
211
+ "reasoning": "detailed explanation of relevance to original query",
212
+ "score": 0.8
213
+ }`;
214
+
215
+ try {
216
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
217
+ messages: [
218
+ {
219
+ role: 'system',
220
+ content: 'You are a research finding relevance judge. Be strict - only findings that directly address the query should score high.'
221
+ },
222
+ { role: 'user', content: prompt }
223
+ ],
224
+ temperature: 0.2,
225
+ });
226
+
227
+ const responseContent = typeof response === 'string' ? response : (response as any).content || '';
228
+ const jsonMatch = responseContent.match(/\{[\s\S]*\}/);
229
+
230
+ if (jsonMatch) {
231
+ const score = JSON.parse(jsonMatch[0]);
232
+ const finalScore = (score.queryAlignment + score.topicRelevance + score.specificity) / 3;
233
+
234
+ elizaLogger.debug(`[RelevanceAnalyzer] Finding scored:`, {
235
+ score: finalScore,
236
+ category: finding.category,
237
+ sourceUrl: finding.source.url
238
+ });
239
+
240
+ return {
241
+ score: finalScore,
242
+ reasoning: score.reasoning,
243
+ queryAlignment: score.queryAlignment,
244
+ topicRelevance: score.topicRelevance,
245
+ specificity: score.specificity
246
+ };
247
+ }
248
+ } catch (error) {
249
+ elizaLogger.error('[RelevanceAnalyzer] Failed to score finding:', error);
250
+ }
251
+
252
+ // Fallback scoring
253
+ const keywordScore = this.calculateKeywordScore(finding.content, queryAnalysis.keyTopics);
254
+ return {
255
+ score: keywordScore,
256
+ reasoning: `Fallback keyword scoring: ${keywordScore.toFixed(2)}`,
257
+ queryAlignment: keywordScore,
258
+ topicRelevance: keywordScore,
259
+ specificity: 0.5
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Verify that extracted findings actually answer the research query
265
+ */
266
+ async verifyQueryAnswering(
267
+ findings: ResearchFinding[],
268
+ originalQuery: string
269
+ ): Promise<{
270
+ coverage: number;
271
+ gaps: string[];
272
+ recommendations: string[];
273
+ }> {
274
+ elizaLogger.info(`[RelevanceAnalyzer] Verifying query answering for ${findings.length} findings`);
275
+
276
+ const findingSummaries = findings
277
+ .slice(0, 20) // Limit for prompt size
278
+ .map((f, i) => `${i + 1}. ${f.content.substring(0, 200)}...`)
279
+ .join('\n');
280
+
281
+ const prompt = `Assess how well these research findings answer the original query:
282
+
283
+ ORIGINAL QUERY: "${originalQuery}"
284
+
285
+ FINDINGS:
286
+ ${findingSummaries}
287
+
288
+ Assessment:
289
+ 1. Coverage Score (0-1): How well do these findings collectively answer the query?
290
+ 2. Gaps: What important aspects of the query are NOT addressed?
291
+ 3. Recommendations: What additional research is needed?
292
+
293
+ Format as JSON:
294
+ {
295
+ "coverage": 0.7,
296
+ "gaps": ["gap1", "gap2"],
297
+ "recommendations": ["rec1", "rec2"]
298
+ }`;
299
+
300
+ try {
301
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
302
+ messages: [
303
+ {
304
+ role: 'system',
305
+ content: 'You are a research completeness assessor. Evaluate if findings actually answer the research question.'
306
+ },
307
+ { role: 'user', content: prompt }
308
+ ],
309
+ temperature: 0.3,
310
+ });
311
+
312
+ const responseContent = typeof response === 'string' ? response : (response as any).content || '';
313
+ const jsonMatch = responseContent.match(/\{[\s\S]*\}/);
314
+
315
+ if (jsonMatch) {
316
+ const assessment = JSON.parse(jsonMatch[0]);
317
+ elizaLogger.info(`[RelevanceAnalyzer] Query answering assessment:`, {
318
+ coverage: assessment.coverage,
319
+ gapsCount: assessment.gaps?.length || 0,
320
+ recommendationsCount: assessment.recommendations?.length || 0
321
+ });
322
+ return assessment;
323
+ }
324
+ } catch (error) {
325
+ elizaLogger.error('[RelevanceAnalyzer] Failed to verify query answering:', error);
326
+ }
327
+
328
+ return {
329
+ coverage: findings.length > 0 ? 0.5 : 0,
330
+ gaps: ['Unable to assess coverage'],
331
+ recommendations: ['Manual review recommended']
332
+ };
333
+ }
334
+
335
+ private extractKeywordsFromQuery(query: string): string[] {
336
+ // Simple keyword extraction as fallback
337
+ const words = query.toLowerCase()
338
+ .replace(/[^\w\s]/g, ' ')
339
+ .split(/\s+/)
340
+ .filter(word => word.length > 3)
341
+ .filter(word => !['what', 'how', 'why', 'when', 'where', 'which', 'that', 'this', 'with', 'from', 'they', 'have', 'been', 'will', 'are'].includes(word));
342
+
343
+ return words.slice(0, 5);
344
+ }
345
+
346
+ private calculateKeywordScore(text: string, keywords: string[]): number {
347
+ if (!keywords.length) return 0.5;
348
+
349
+ const lowerText = text.toLowerCase();
350
+ const matches = keywords.filter(keyword => lowerText.includes(keyword.toLowerCase()));
351
+ return matches.length / keywords.length;
352
+ }
353
+ }