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