@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
package/src/service.ts
ADDED
|
@@ -0,0 +1,2588 @@
|
|
|
1
|
+
import { elizaLogger, IAgentRuntime, ModelType, Service } from '@elizaos/core';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { ResearchEvaluator } from './evaluation/research-evaluator';
|
|
4
|
+
import { SearchResultProcessor } from './processing/result-processor';
|
|
5
|
+
import { RelevanceAnalyzer } from './processing/relevance-analyzer';
|
|
6
|
+
import { ResearchLogger } from './processing/research-logger';
|
|
7
|
+
import {
|
|
8
|
+
ContentExtractor,
|
|
9
|
+
createContentExtractor,
|
|
10
|
+
createSearchProvider,
|
|
11
|
+
SearchProvider,
|
|
12
|
+
} from './integrations';
|
|
13
|
+
import { createAcademicSearchProvider } from './integrations/factory';
|
|
14
|
+
import { PDFExtractor } from './integrations/content-extractors/pdf-extractor';
|
|
15
|
+
import {
|
|
16
|
+
EvaluationCriteriaGenerator,
|
|
17
|
+
QueryPlanner,
|
|
18
|
+
ResearchStrategyFactory,
|
|
19
|
+
} from './strategies/research-strategies';
|
|
20
|
+
import {
|
|
21
|
+
BibliographyEntry,
|
|
22
|
+
Citation,
|
|
23
|
+
DeepResearchBenchResult,
|
|
24
|
+
EvaluationMetrics,
|
|
25
|
+
EvaluationResults,
|
|
26
|
+
FactualClaim,
|
|
27
|
+
IterationRecord,
|
|
28
|
+
PerformanceMetrics,
|
|
29
|
+
PhaseTiming,
|
|
30
|
+
ReportSection,
|
|
31
|
+
ResearchConfig,
|
|
32
|
+
ResearchDepth,
|
|
33
|
+
ResearchDomain,
|
|
34
|
+
ResearchFinding,
|
|
35
|
+
ResearchMetadata,
|
|
36
|
+
ResearchPhase,
|
|
37
|
+
ResearchProgress,
|
|
38
|
+
ResearchProject,
|
|
39
|
+
ResearchSource,
|
|
40
|
+
ResearchStatus,
|
|
41
|
+
SearchResult,
|
|
42
|
+
SourceType,
|
|
43
|
+
TaskType,
|
|
44
|
+
VerificationStatus,
|
|
45
|
+
} from './types';
|
|
46
|
+
import fs from 'fs/promises';
|
|
47
|
+
import path from 'path';
|
|
48
|
+
import { ClaimVerifier } from './verification/claim-verifier';
|
|
49
|
+
import { RESEARCH_PROMPTS, formatPrompt, getPromptConfig } from './prompts/research-prompts';
|
|
50
|
+
|
|
51
|
+
// Factory for creating search providers and content extractors
|
|
52
|
+
class SearchProviderFactory {
|
|
53
|
+
private pdfExtractor: PDFExtractor;
|
|
54
|
+
|
|
55
|
+
constructor(private runtime: IAgentRuntime) {
|
|
56
|
+
this.pdfExtractor = new PDFExtractor();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getProvider(type: string): SearchProvider {
|
|
60
|
+
// Use academic search for academic domains
|
|
61
|
+
if (type === 'academic') {
|
|
62
|
+
return createAcademicSearchProvider(this.runtime);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const provider = createSearchProvider(type, this.runtime);
|
|
66
|
+
if (!provider) {
|
|
67
|
+
throw new Error('No search provider available');
|
|
68
|
+
}
|
|
69
|
+
return provider;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getContentExtractor(): ContentExtractor {
|
|
73
|
+
const extractor = createContentExtractor(this.runtime);
|
|
74
|
+
if (!extractor) {
|
|
75
|
+
throw new Error('No content extractor available');
|
|
76
|
+
}
|
|
77
|
+
return extractor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getPDFExtractor(): PDFExtractor {
|
|
81
|
+
return this.pdfExtractor;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_CONFIG: ResearchConfig = {
|
|
86
|
+
maxSearchResults: 30, // Increased for more comprehensive coverage
|
|
87
|
+
maxDepth: 5, // Deeper research iterations
|
|
88
|
+
timeout: 600000, // 10 minutes for thorough research
|
|
89
|
+
enableCitations: true,
|
|
90
|
+
enableImages: false,
|
|
91
|
+
searchProviders: ['web', 'academic', 'github'], // Include GitHub for code research by default
|
|
92
|
+
language: 'en',
|
|
93
|
+
researchDepth: ResearchDepth.DEEP, // Default to deep research
|
|
94
|
+
domain: ResearchDomain.GENERAL,
|
|
95
|
+
evaluationEnabled: true,
|
|
96
|
+
cacheEnabled: true,
|
|
97
|
+
parallelSearches: 5, // More parallel searches for efficiency
|
|
98
|
+
retryAttempts: 3,
|
|
99
|
+
qualityThreshold: 0.85, // Higher quality threshold
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export class ResearchService extends Service {
|
|
103
|
+
private projects: Map<string, ResearchProject> = new Map();
|
|
104
|
+
private activeResearch: Map<string, AbortController> = new Map();
|
|
105
|
+
private searchProviderFactory: SearchProviderFactory;
|
|
106
|
+
private queryPlanner: QueryPlanner;
|
|
107
|
+
private strategyFactory: ResearchStrategyFactory;
|
|
108
|
+
private criteriaGenerator: EvaluationCriteriaGenerator;
|
|
109
|
+
private evaluator: ResearchEvaluator;
|
|
110
|
+
private resultProcessor: SearchResultProcessor;
|
|
111
|
+
private relevanceAnalyzer: RelevanceAnalyzer;
|
|
112
|
+
private researchLogger: ResearchLogger;
|
|
113
|
+
private performanceData: Map<string, PerformanceMetrics> = new Map();
|
|
114
|
+
private claimVerifier: ClaimVerifier;
|
|
115
|
+
|
|
116
|
+
static serviceName = 'research';
|
|
117
|
+
public serviceName = 'research';
|
|
118
|
+
|
|
119
|
+
static async start(runtime: IAgentRuntime) {
|
|
120
|
+
const service = new ResearchService(runtime);
|
|
121
|
+
return service;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static async stop(runtime: IAgentRuntime) {
|
|
125
|
+
const service = new ResearchService(runtime);
|
|
126
|
+
await service.stop();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public capabilityDescription =
|
|
130
|
+
'PhD-level deep research across 22 domains with RACE/FACT evaluation';
|
|
131
|
+
|
|
132
|
+
constructor(runtime: IAgentRuntime) {
|
|
133
|
+
super();
|
|
134
|
+
this.runtime = runtime;
|
|
135
|
+
this.searchProviderFactory = new SearchProviderFactory(runtime);
|
|
136
|
+
this.queryPlanner = new QueryPlanner(runtime);
|
|
137
|
+
this.strategyFactory = new ResearchStrategyFactory(runtime);
|
|
138
|
+
this.criteriaGenerator = new EvaluationCriteriaGenerator(runtime);
|
|
139
|
+
this.evaluator = new ResearchEvaluator(runtime);
|
|
140
|
+
this.relevanceAnalyzer = new RelevanceAnalyzer(runtime);
|
|
141
|
+
this.researchLogger = new ResearchLogger(runtime);
|
|
142
|
+
this.resultProcessor = new SearchResultProcessor({
|
|
143
|
+
qualityThreshold: 0.4,
|
|
144
|
+
deduplicationThreshold: 0.85,
|
|
145
|
+
maxResults: 50,
|
|
146
|
+
prioritizeRecent: true,
|
|
147
|
+
diversityWeight: 0.3,
|
|
148
|
+
});
|
|
149
|
+
this.claimVerifier = new ClaimVerifier(
|
|
150
|
+
runtime,
|
|
151
|
+
this.searchProviderFactory.getContentExtractor()
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async createResearchProject(
|
|
156
|
+
query: string,
|
|
157
|
+
config?: Partial<ResearchConfig>
|
|
158
|
+
): Promise<ResearchProject> {
|
|
159
|
+
const projectId = uuidv4();
|
|
160
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
161
|
+
|
|
162
|
+
// Always use full research pipeline for best quality
|
|
163
|
+
elizaLogger.info(`[ResearchService] Starting comprehensive research for: ${query}`);
|
|
164
|
+
|
|
165
|
+
// Extract metadata from query
|
|
166
|
+
const metadata = await this.extractMetadata(query, mergedConfig);
|
|
167
|
+
|
|
168
|
+
const project: ResearchProject = {
|
|
169
|
+
id: projectId,
|
|
170
|
+
query,
|
|
171
|
+
status: ResearchStatus.PENDING,
|
|
172
|
+
phase: ResearchPhase.INITIALIZATION,
|
|
173
|
+
createdAt: Date.now(),
|
|
174
|
+
updatedAt: Date.now(),
|
|
175
|
+
findings: [],
|
|
176
|
+
sources: [],
|
|
177
|
+
metadata,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
this.projects.set(projectId, project);
|
|
181
|
+
|
|
182
|
+
// Start research asynchronously
|
|
183
|
+
this.startResearch(projectId, mergedConfig).catch((error) => {
|
|
184
|
+
elizaLogger.error(`Research failed for project ${projectId}:`, error);
|
|
185
|
+
project.status = ResearchStatus.FAILED;
|
|
186
|
+
project.error = error.message;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return project;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async extractMetadata(query: string, config: ResearchConfig): Promise<ResearchMetadata> {
|
|
193
|
+
// Extract domain if not provided
|
|
194
|
+
const domain = config.domain || (await this.extractDomain(query));
|
|
195
|
+
|
|
196
|
+
// Extract task type
|
|
197
|
+
const taskType = await this.extractTaskType(query);
|
|
198
|
+
|
|
199
|
+
// Select appropriate search providers based on domain and query
|
|
200
|
+
const selectedProviders = this.selectSearchProviders(domain, query);
|
|
201
|
+
|
|
202
|
+
// Update config with domain-specific providers (merge with user-specified ones)
|
|
203
|
+
const searchProviders = config.searchProviders?.length
|
|
204
|
+
? [...new Set([...config.searchProviders, ...selectedProviders])]
|
|
205
|
+
: selectedProviders;
|
|
206
|
+
|
|
207
|
+
// Update the config object for use in research
|
|
208
|
+
(config as any).searchProviders = searchProviders;
|
|
209
|
+
|
|
210
|
+
elizaLogger.info(`[ResearchService] Domain: ${domain}, Selected providers: ${searchProviders.join(', ')}`);
|
|
211
|
+
|
|
212
|
+
// Create query plan
|
|
213
|
+
const queryPlan = await this.queryPlanner.createQueryPlan(query, {
|
|
214
|
+
domain,
|
|
215
|
+
taskType,
|
|
216
|
+
depth: config.researchDepth,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Generate evaluation criteria
|
|
220
|
+
const evaluationCriteria = await this.criteriaGenerator.generateCriteria(query, domain);
|
|
221
|
+
|
|
222
|
+
// Initialize performance metrics
|
|
223
|
+
const performanceMetrics: PerformanceMetrics = {
|
|
224
|
+
totalDuration: 0,
|
|
225
|
+
phaseBreakdown: {} as Record<ResearchPhase, PhaseTiming>,
|
|
226
|
+
searchQueries: 0,
|
|
227
|
+
sourcesProcessed: 0,
|
|
228
|
+
tokensGenerated: 0,
|
|
229
|
+
cacheHits: 0,
|
|
230
|
+
parallelOperations: 0,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
domain,
|
|
235
|
+
taskType,
|
|
236
|
+
language: config.language,
|
|
237
|
+
depth: config.researchDepth,
|
|
238
|
+
queryPlan,
|
|
239
|
+
evaluationCriteria,
|
|
240
|
+
iterationHistory: [],
|
|
241
|
+
performanceMetrics,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private async extractDomain(query: string): Promise<ResearchDomain> {
|
|
246
|
+
// Use embeddings-based classification for more accurate domain detection
|
|
247
|
+
try {
|
|
248
|
+
if (this.runtime.useModel) {
|
|
249
|
+
// Create domain examples for similarity matching
|
|
250
|
+
const domainExamples = {
|
|
251
|
+
[ResearchDomain.PHYSICS]: [
|
|
252
|
+
"quantum mechanics and particle physics research",
|
|
253
|
+
"theoretical physics and relativity studies",
|
|
254
|
+
"condensed matter physics and thermodynamics"
|
|
255
|
+
],
|
|
256
|
+
[ResearchDomain.COMPUTER_SCIENCE]: [
|
|
257
|
+
"machine learning and artificial intelligence",
|
|
258
|
+
"software engineering and programming languages",
|
|
259
|
+
"algorithms and data structures research"
|
|
260
|
+
],
|
|
261
|
+
[ResearchDomain.BIOLOGY]: [
|
|
262
|
+
"molecular biology and genetics research",
|
|
263
|
+
"cell biology and biochemistry studies",
|
|
264
|
+
"evolutionary biology and ecology"
|
|
265
|
+
],
|
|
266
|
+
[ResearchDomain.MEDICINE]: [
|
|
267
|
+
"clinical medicine and patient treatment",
|
|
268
|
+
"medical research and drug development",
|
|
269
|
+
"healthcare and disease management"
|
|
270
|
+
],
|
|
271
|
+
[ResearchDomain.CHEMISTRY]: [
|
|
272
|
+
"organic chemistry and synthesis",
|
|
273
|
+
"analytical chemistry and spectroscopy",
|
|
274
|
+
"physical chemistry and materials"
|
|
275
|
+
],
|
|
276
|
+
[ResearchDomain.PSYCHOLOGY]: [
|
|
277
|
+
"cognitive psychology and behavior",
|
|
278
|
+
"clinical psychology and mental health",
|
|
279
|
+
"social psychology and human behavior"
|
|
280
|
+
],
|
|
281
|
+
[ResearchDomain.ECONOMICS]: [
|
|
282
|
+
"economic theory and market analysis",
|
|
283
|
+
"finance and monetary policy",
|
|
284
|
+
"economic development and trade"
|
|
285
|
+
],
|
|
286
|
+
[ResearchDomain.POLITICS]: [
|
|
287
|
+
"political theory and governance",
|
|
288
|
+
"international relations and diplomacy",
|
|
289
|
+
"public policy and administration"
|
|
290
|
+
]
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Get query embedding
|
|
294
|
+
const queryEmbedding = await this.runtime.useModel(ModelType.TEXT_EMBEDDING, {
|
|
295
|
+
text: query
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let bestDomain = ResearchDomain.GENERAL;
|
|
299
|
+
let bestSimilarity = 0;
|
|
300
|
+
|
|
301
|
+
// Compare with domain examples
|
|
302
|
+
for (const [domain, examples] of Object.entries(domainExamples)) {
|
|
303
|
+
for (const example of examples) {
|
|
304
|
+
try {
|
|
305
|
+
const exampleEmbedding = await this.runtime.useModel(ModelType.TEXT_EMBEDDING, {
|
|
306
|
+
text: example
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Calculate cosine similarity
|
|
310
|
+
const similarity = this.calculateCosineSimilarity(queryEmbedding as number[], exampleEmbedding as number[]);
|
|
311
|
+
|
|
312
|
+
if (similarity > bestSimilarity) {
|
|
313
|
+
bestSimilarity = similarity;
|
|
314
|
+
bestDomain = domain as ResearchDomain;
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
elizaLogger.debug(`Error processing domain example: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// If similarity is high enough, use embedding-based classification
|
|
323
|
+
if (bestSimilarity > 0.7) {
|
|
324
|
+
elizaLogger.info(`Domain classified via embeddings: ${bestDomain} (similarity: ${bestSimilarity.toFixed(3)})`);
|
|
325
|
+
return bestDomain;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
elizaLogger.warn('Error using embeddings for domain classification, falling back to LLM:', error);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fallback: Use LLM classification
|
|
333
|
+
if (this.runtime.useModel) {
|
|
334
|
+
const prompt = `Analyze this research query and determine the most appropriate research domain.
|
|
335
|
+
|
|
336
|
+
Query: "${query}"
|
|
337
|
+
|
|
338
|
+
Available domains:
|
|
339
|
+
${Object.values(ResearchDomain).map(d => `- ${d}`).join('\n')}
|
|
340
|
+
|
|
341
|
+
Consider:
|
|
342
|
+
- Primary subject matter and field of study
|
|
343
|
+
- Methodology and approach
|
|
344
|
+
- Target audience and applications
|
|
345
|
+
- Interdisciplinary connections
|
|
346
|
+
|
|
347
|
+
Respond with ONLY the domain name from the list above. Be precise.`;
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
351
|
+
messages: [
|
|
352
|
+
{
|
|
353
|
+
role: 'system',
|
|
354
|
+
content: 'You are an expert research domain classifier. Analyze the query and respond with only the most appropriate domain name from the provided list.'
|
|
355
|
+
},
|
|
356
|
+
{ role: 'user', content: prompt }
|
|
357
|
+
],
|
|
358
|
+
temperature: 0.1, // Low temperature for consistent classification
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const domainText = (
|
|
362
|
+
typeof response === 'string' ? response : (response as any).content || ''
|
|
363
|
+
).trim().toLowerCase();
|
|
364
|
+
|
|
365
|
+
// Exact match first
|
|
366
|
+
for (const domain of Object.values(ResearchDomain)) {
|
|
367
|
+
if (domainText === domain.toLowerCase()) {
|
|
368
|
+
elizaLogger.info(`Domain classified via LLM: ${domain}`);
|
|
369
|
+
return domain as ResearchDomain;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Partial match fallback
|
|
374
|
+
for (const domain of Object.values(ResearchDomain)) {
|
|
375
|
+
if (domainText.includes(domain.toLowerCase().replace('_', ' ')) ||
|
|
376
|
+
domainText.includes(domain.toLowerCase().replace('_', ''))) {
|
|
377
|
+
elizaLogger.info(`Domain classified via LLM (partial match): ${domain}`);
|
|
378
|
+
return domain as ResearchDomain;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
elizaLogger.warn(`Could not extract domain from LLM response: ${domainText}`);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
elizaLogger.warn('Error using LLM for domain extraction, falling back to heuristics:', error);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Final fallback: Simple keyword matching
|
|
389
|
+
const lowerQuery = query.toLowerCase();
|
|
390
|
+
const keywords = {
|
|
391
|
+
[ResearchDomain.PHYSICS]: ['quantum', 'physics', 'particle', 'relativity', 'thermodynamics'],
|
|
392
|
+
[ResearchDomain.COMPUTER_SCIENCE]: ['computer', 'software', 'algorithm', 'programming', 'ai', 'artificial intelligence', 'machine learning'],
|
|
393
|
+
[ResearchDomain.BIOLOGY]: ['biology', 'gene', 'cell', 'dna', 'evolution', 'organism'],
|
|
394
|
+
[ResearchDomain.MEDICINE]: ['medicine', 'health', 'disease', 'treatment', 'clinical', 'medical'],
|
|
395
|
+
[ResearchDomain.CHEMISTRY]: ['chemistry', 'chemical', 'molecule', 'synthesis', 'compound'],
|
|
396
|
+
[ResearchDomain.PSYCHOLOGY]: ['psychology', 'mental', 'behavior', 'cognitive', 'brain'],
|
|
397
|
+
[ResearchDomain.ECONOMICS]: ['economic', 'finance', 'market', 'currency', 'trade'],
|
|
398
|
+
[ResearchDomain.POLITICS]: ['political', 'government', 'policy', 'politics', 'governance']
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
for (const [domain, words] of Object.entries(keywords)) {
|
|
402
|
+
if (words.some(word => lowerQuery.includes(word))) {
|
|
403
|
+
elizaLogger.info(`Domain classified via keywords: ${domain}`);
|
|
404
|
+
return domain as ResearchDomain;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
elizaLogger.info('Domain classified as GENERAL (no specific match found)');
|
|
409
|
+
return ResearchDomain.GENERAL;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private calculateCosineSimilarity(a: number[], b: number[]): number {
|
|
413
|
+
if (a.length !== b.length) return 0;
|
|
414
|
+
|
|
415
|
+
let dotProduct = 0;
|
|
416
|
+
let normA = 0;
|
|
417
|
+
let normB = 0;
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < a.length; i++) {
|
|
420
|
+
dotProduct += a[i] * b[i];
|
|
421
|
+
normA += a[i] * a[i];
|
|
422
|
+
normB += b[i] * b[i];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
426
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private selectSearchProviders(domain: ResearchDomain, query: string): string[] {
|
|
430
|
+
const providers = new Set(['web']); // Always include web search
|
|
431
|
+
|
|
432
|
+
// Add domain-specific providers
|
|
433
|
+
switch (domain) {
|
|
434
|
+
case ResearchDomain.COMPUTER_SCIENCE:
|
|
435
|
+
case ResearchDomain.ENGINEERING:
|
|
436
|
+
providers.add('github');
|
|
437
|
+
providers.add('academic');
|
|
438
|
+
// Add package managers if the query mentions specific languages/packages
|
|
439
|
+
if (query.toLowerCase().includes('python') || query.toLowerCase().includes('pip') || query.toLowerCase().includes('pypi')) {
|
|
440
|
+
providers.add('pypi');
|
|
441
|
+
}
|
|
442
|
+
if (query.toLowerCase().includes('javascript') || query.toLowerCase().includes('node') || query.toLowerCase().includes('npm')) {
|
|
443
|
+
providers.add('npm');
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case ResearchDomain.PHYSICS:
|
|
448
|
+
case ResearchDomain.CHEMISTRY:
|
|
449
|
+
case ResearchDomain.BIOLOGY:
|
|
450
|
+
case ResearchDomain.MEDICINE:
|
|
451
|
+
case ResearchDomain.MATHEMATICS:
|
|
452
|
+
providers.add('academic');
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case ResearchDomain.ECONOMICS:
|
|
456
|
+
case ResearchDomain.POLITICS:
|
|
457
|
+
case ResearchDomain.PSYCHOLOGY:
|
|
458
|
+
providers.add('academic');
|
|
459
|
+
break;
|
|
460
|
+
|
|
461
|
+
default:
|
|
462
|
+
// For general research, include academic for scholarly sources
|
|
463
|
+
providers.add('academic');
|
|
464
|
+
|
|
465
|
+
// Check for code-related keywords in any domain
|
|
466
|
+
if (query.toLowerCase().match(/(code|programming|software|library|package|framework|api)/)) {
|
|
467
|
+
providers.add('github');
|
|
468
|
+
}
|
|
469
|
+
if (query.toLowerCase().match(/(python|pip|pypi)/)) {
|
|
470
|
+
providers.add('pypi');
|
|
471
|
+
}
|
|
472
|
+
if (query.toLowerCase().match(/(javascript|typescript|node|npm)/)) {
|
|
473
|
+
providers.add('npm');
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return Array.from(providers);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private async extractTaskType(query: string): Promise<TaskType> {
|
|
482
|
+
// Simple heuristic-based task type extraction for testing
|
|
483
|
+
const lowerQuery = query.toLowerCase();
|
|
484
|
+
|
|
485
|
+
// Check for task type keywords
|
|
486
|
+
if (lowerQuery.includes('compar') || lowerQuery.includes('versus') || lowerQuery.includes('vs')) {
|
|
487
|
+
return TaskType.COMPARATIVE;
|
|
488
|
+
}
|
|
489
|
+
if (lowerQuery.includes('analyz') || lowerQuery.includes('analysis') || lowerQuery.includes('examine')) {
|
|
490
|
+
return TaskType.ANALYTICAL;
|
|
491
|
+
}
|
|
492
|
+
if (lowerQuery.includes('synthes') || lowerQuery.includes('combin') || lowerQuery.includes('integrat')) {
|
|
493
|
+
return TaskType.SYNTHETIC;
|
|
494
|
+
}
|
|
495
|
+
if (lowerQuery.includes('evaluat') || lowerQuery.includes('assess') || lowerQuery.includes('judge')) {
|
|
496
|
+
return TaskType.EVALUATIVE;
|
|
497
|
+
}
|
|
498
|
+
if (lowerQuery.includes('predict') || lowerQuery.includes('forecast') || lowerQuery.includes('future')) {
|
|
499
|
+
return TaskType.PREDICTIVE;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// If we have a working runtime.useModel, use it for more accurate classification
|
|
503
|
+
if (this.runtime.useModel) {
|
|
504
|
+
const prompt = `Analyze this research query and determine the primary task type.
|
|
505
|
+
|
|
506
|
+
Query: "${query}"
|
|
507
|
+
|
|
508
|
+
Task Types:
|
|
509
|
+
- ${TaskType.EXPLORATORY}: General exploration of a topic
|
|
510
|
+
- ${TaskType.COMPARATIVE}: Comparing different items, approaches, or solutions
|
|
511
|
+
- ${TaskType.ANALYTICAL}: Deep analysis of a specific subject
|
|
512
|
+
- ${TaskType.SYNTHETIC}: Combining multiple perspectives or sources
|
|
513
|
+
- ${TaskType.EVALUATIVE}: Assessment or evaluation of something
|
|
514
|
+
- ${TaskType.PREDICTIVE}: Forecasting or predicting future trends
|
|
515
|
+
|
|
516
|
+
Respond with ONLY the task type (e.g., "analytical"). Be precise.`;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
520
|
+
messages: [
|
|
521
|
+
{
|
|
522
|
+
role: 'system',
|
|
523
|
+
content: 'You are a research task classifier. Respond with only the task type, nothing else.'
|
|
524
|
+
},
|
|
525
|
+
{ role: 'user', content: prompt }
|
|
526
|
+
],
|
|
527
|
+
temperature: 0.3,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const taskText = (
|
|
531
|
+
typeof response === 'string' ? response : (response as any).content || ''
|
|
532
|
+
).trim().toLowerCase();
|
|
533
|
+
|
|
534
|
+
// Check for exact matches
|
|
535
|
+
for (const taskType of Object.values(TaskType)) {
|
|
536
|
+
if (taskText === taskType.toLowerCase()) {
|
|
537
|
+
return taskType as TaskType;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check for keyword matches
|
|
542
|
+
if (taskText.includes('compar')) return TaskType.COMPARATIVE;
|
|
543
|
+
if (taskText.includes('analy')) return TaskType.ANALYTICAL;
|
|
544
|
+
if (taskText.includes('synth')) return TaskType.SYNTHETIC;
|
|
545
|
+
if (taskText.includes('eval')) return TaskType.EVALUATIVE;
|
|
546
|
+
if (taskText.includes('pred') || taskText.includes('forecast')) return TaskType.PREDICTIVE;
|
|
547
|
+
|
|
548
|
+
elizaLogger.warn(`Could not extract task type from response: ${taskText}`);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
elizaLogger.warn('Error using model for task type extraction, falling back to heuristics:', error);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return TaskType.EXPLORATORY;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private async startResearch(projectId: string, config: ResearchConfig): Promise<void> {
|
|
558
|
+
const project = this.projects.get(projectId);
|
|
559
|
+
if (!project) return;
|
|
560
|
+
|
|
561
|
+
const controller = new AbortController();
|
|
562
|
+
this.activeResearch.set(projectId, controller);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
project.status = ResearchStatus.ACTIVE;
|
|
566
|
+
const startTime = Date.now();
|
|
567
|
+
|
|
568
|
+
// Phase 1: Planning and Relevance Analysis
|
|
569
|
+
await this.updatePhase(project, ResearchPhase.PLANNING);
|
|
570
|
+
|
|
571
|
+
// Analyze query for relevance criteria
|
|
572
|
+
elizaLogger.info(`[ResearchService] Analyzing query relevance for: ${project.query}`);
|
|
573
|
+
const queryAnalysis = await this.relevanceAnalyzer.analyzeQueryRelevance(project.query);
|
|
574
|
+
|
|
575
|
+
// Initialize comprehensive logging
|
|
576
|
+
await this.researchLogger.initializeSession(projectId, project.query, queryAnalysis);
|
|
577
|
+
|
|
578
|
+
// Phase 2: Searching with Relevance Filtering
|
|
579
|
+
await this.updatePhase(project, ResearchPhase.SEARCHING);
|
|
580
|
+
await this.executeSearchWithRelevance(project, config, controller.signal, queryAnalysis);
|
|
581
|
+
|
|
582
|
+
// Phase 3: Analyzing with Relevance Verification
|
|
583
|
+
await this.updatePhase(project, ResearchPhase.ANALYZING);
|
|
584
|
+
await this.analyzeFindingsWithRelevance(project, config, queryAnalysis);
|
|
585
|
+
|
|
586
|
+
// Synthesize findings
|
|
587
|
+
await this.updatePhase(project, ResearchPhase.SYNTHESIZING);
|
|
588
|
+
await this.synthesizeFindings(project);
|
|
589
|
+
|
|
590
|
+
// Generate report (before evaluation)
|
|
591
|
+
await this.updatePhase(project, ResearchPhase.REPORTING);
|
|
592
|
+
await this.generateReport(project);
|
|
593
|
+
|
|
594
|
+
// Evaluate if configured (optional)
|
|
595
|
+
if (config.evaluationEnabled) {
|
|
596
|
+
try {
|
|
597
|
+
await this.updatePhase(project, ResearchPhase.EVALUATING);
|
|
598
|
+
await this.evaluateProject(project.id);
|
|
599
|
+
} catch (evalError) {
|
|
600
|
+
elizaLogger.warn('[ResearchService] Evaluation failed, but research completed:', evalError);
|
|
601
|
+
// Don't fail the entire research if evaluation fails
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Verify query answering and finalize logging
|
|
606
|
+
const queryAnswering = await this.relevanceAnalyzer.verifyQueryAnswering(project.findings, project.query);
|
|
607
|
+
await this.researchLogger.finalizeSession(projectId, queryAnswering.gaps, queryAnswering.recommendations);
|
|
608
|
+
|
|
609
|
+
// Complete
|
|
610
|
+
await this.updatePhase(project, ResearchPhase.COMPLETE);
|
|
611
|
+
project.status = ResearchStatus.COMPLETED;
|
|
612
|
+
project.completedAt = Date.now();
|
|
613
|
+
|
|
614
|
+
// Update performance metrics
|
|
615
|
+
const totalDuration = Date.now() - startTime;
|
|
616
|
+
if (project.metadata.performanceMetrics) {
|
|
617
|
+
project.metadata.performanceMetrics.totalDuration = totalDuration;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Log final summary
|
|
621
|
+
elizaLogger.info(`[ResearchService] Research completed for project ${projectId}:`, {
|
|
622
|
+
duration: totalDuration,
|
|
623
|
+
sources: project.sources.length,
|
|
624
|
+
findings: project.findings.length,
|
|
625
|
+
relevantFindings: project.findings.filter(f => f.relevance >= 0.7).length,
|
|
626
|
+
queryAnswering: queryAnswering.coverage
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if ((error as any).name === 'AbortError') {
|
|
630
|
+
project.status = ResearchStatus.PAUSED;
|
|
631
|
+
} else {
|
|
632
|
+
project.status = ResearchStatus.FAILED;
|
|
633
|
+
project.error = error instanceof Error ? error.message : String(error);
|
|
634
|
+
}
|
|
635
|
+
throw error;
|
|
636
|
+
} finally {
|
|
637
|
+
this.activeResearch.delete(projectId);
|
|
638
|
+
project.updatedAt = Date.now();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private deduplicateResults(results: SearchResult[]): SearchResult[] {
|
|
643
|
+
const seen = new Set<string>();
|
|
644
|
+
const unique: SearchResult[] = [];
|
|
645
|
+
|
|
646
|
+
for (const result of results) {
|
|
647
|
+
// Use URL as unique identifier
|
|
648
|
+
if (!seen.has(result.url)) {
|
|
649
|
+
seen.add(result.url);
|
|
650
|
+
unique.push(result);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Sort by score descending
|
|
655
|
+
return unique.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private async updatePhase(project: ResearchProject, phase: ResearchPhase): Promise<void> {
|
|
659
|
+
const previousPhase = project.phase;
|
|
660
|
+
project.phase = phase;
|
|
661
|
+
project.updatedAt = Date.now();
|
|
662
|
+
|
|
663
|
+
// Update phase timing
|
|
664
|
+
if (project.metadata.performanceMetrics) {
|
|
665
|
+
const phaseKey = previousPhase;
|
|
666
|
+
if (phaseKey && project.metadata.performanceMetrics.phaseBreakdown[phaseKey]) {
|
|
667
|
+
project.metadata.performanceMetrics.phaseBreakdown[phaseKey].endTime = Date.now();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
project.metadata.performanceMetrics.phaseBreakdown[phase] = {
|
|
671
|
+
startTime: Date.now(),
|
|
672
|
+
endTime: 0,
|
|
673
|
+
duration: 0,
|
|
674
|
+
retries: 0,
|
|
675
|
+
errors: [],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Emit progress
|
|
680
|
+
this.emitProgress(project, `Starting ${phase} phase`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private async executeSearchWithRelevance(
|
|
684
|
+
project: ResearchProject,
|
|
685
|
+
config: ResearchConfig,
|
|
686
|
+
signal: AbortSignal,
|
|
687
|
+
queryAnalysis: any
|
|
688
|
+
): Promise<void> {
|
|
689
|
+
const queryPlan = project.metadata.queryPlan;
|
|
690
|
+
|
|
691
|
+
// Use multiple search providers for comprehensive coverage
|
|
692
|
+
const isAcademicDomain = [
|
|
693
|
+
ResearchDomain.PHYSICS,
|
|
694
|
+
ResearchDomain.CHEMISTRY,
|
|
695
|
+
ResearchDomain.BIOLOGY,
|
|
696
|
+
ResearchDomain.MEDICINE,
|
|
697
|
+
ResearchDomain.COMPUTER_SCIENCE,
|
|
698
|
+
ResearchDomain.MATHEMATICS,
|
|
699
|
+
ResearchDomain.ENGINEERING,
|
|
700
|
+
ResearchDomain.PSYCHOLOGY,
|
|
701
|
+
].includes(project.metadata.domain);
|
|
702
|
+
|
|
703
|
+
const allResults: SearchResult[] = [];
|
|
704
|
+
|
|
705
|
+
// Always search web sources with relevance scoring
|
|
706
|
+
const webProvider = this.searchProviderFactory.getProvider('web');
|
|
707
|
+
const webResults = await webProvider.search(queryPlan.mainQuery, config.maxSearchResults);
|
|
708
|
+
allResults.push(...webResults);
|
|
709
|
+
|
|
710
|
+
// Also search academic sources if configured or if academic domain
|
|
711
|
+
if (config.searchProviders.includes('academic') || isAcademicDomain) {
|
|
712
|
+
try {
|
|
713
|
+
const academicProvider = this.searchProviderFactory.getProvider('academic');
|
|
714
|
+
const academicResults = await academicProvider.search(queryPlan.mainQuery, config.maxSearchResults);
|
|
715
|
+
allResults.push(...academicResults);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
elizaLogger.warn('Academic search failed, continuing with web results:', error);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Score search results for relevance BEFORE processing
|
|
722
|
+
elizaLogger.info(`[ResearchService] Scoring ${allResults.length} search results for relevance`);
|
|
723
|
+
const relevanceScores = new Map<string, any>();
|
|
724
|
+
|
|
725
|
+
for (const result of allResults) {
|
|
726
|
+
if (signal.aborted) break;
|
|
727
|
+
const relevanceScore = await this.relevanceAnalyzer.scoreSearchResultRelevance(result, queryAnalysis);
|
|
728
|
+
relevanceScores.set(result.url, relevanceScore);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Log search results with relevance scores
|
|
732
|
+
await this.researchLogger.logSearch(
|
|
733
|
+
project.id,
|
|
734
|
+
queryPlan.mainQuery,
|
|
735
|
+
'web+academic',
|
|
736
|
+
allResults,
|
|
737
|
+
relevanceScores
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
// Filter and sort by relevance score (minimum threshold 0.5)
|
|
741
|
+
const relevantResults = allResults
|
|
742
|
+
.filter(result => (relevanceScores.get(result.url)?.score || 0) >= 0.5)
|
|
743
|
+
.sort((a, b) => (relevanceScores.get(b.url)?.score || 0) - (relevanceScores.get(a.url)?.score || 0));
|
|
744
|
+
|
|
745
|
+
const mainResults = relevantResults.slice(0, config.maxSearchResults);
|
|
746
|
+
|
|
747
|
+
elizaLogger.info(`[ResearchService] Filtered to ${mainResults.length}/${allResults.length} relevant results (threshold >= 0.5)`);
|
|
748
|
+
|
|
749
|
+
// Process main results
|
|
750
|
+
for (const result of mainResults) {
|
|
751
|
+
if (signal.aborted) break;
|
|
752
|
+
|
|
753
|
+
const source = await this.processSearchResult(result, project);
|
|
754
|
+
if (source) {
|
|
755
|
+
project.sources.push(source);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Execute sub-queries
|
|
760
|
+
for (const subQuery of queryPlan.subQueries) {
|
|
761
|
+
if (signal.aborted) break;
|
|
762
|
+
|
|
763
|
+
// Check dependencies
|
|
764
|
+
const dependenciesMet = subQuery.dependsOn.every(
|
|
765
|
+
(depId) => queryPlan.subQueries.find((sq) => sq.id === depId)?.completed
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
if (!dependenciesMet) continue;
|
|
769
|
+
|
|
770
|
+
const subResults = await webProvider.search(
|
|
771
|
+
subQuery.query,
|
|
772
|
+
Math.floor(config.maxSearchResults / 2)
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
for (const result of subResults) {
|
|
776
|
+
if (signal.aborted) break;
|
|
777
|
+
|
|
778
|
+
const source = await this.processSearchResult(result, project);
|
|
779
|
+
if (source) {
|
|
780
|
+
project.sources.push(source);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
subQuery.completed = true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Update iteration history
|
|
788
|
+
const iteration: IterationRecord = {
|
|
789
|
+
iteration: project.metadata.iterationHistory.length + 1,
|
|
790
|
+
timestamp: Date.now(),
|
|
791
|
+
queriesUsed: [queryPlan.mainQuery, ...queryPlan.subQueries.map((sq) => sq.query)],
|
|
792
|
+
sourcesFound: project.sources.length,
|
|
793
|
+
findingsExtracted: 0,
|
|
794
|
+
qualityScore: 0,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
project.metadata.iterationHistory.push(iteration);
|
|
798
|
+
|
|
799
|
+
// Adaptive refinement if needed
|
|
800
|
+
if (queryPlan.adaptiveRefinement && project.sources.length < queryPlan.expectedSources) {
|
|
801
|
+
await this.performAdaptiveRefinement(project, config, signal);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private async processSearchResult(
|
|
806
|
+
result: SearchResult,
|
|
807
|
+
project: ResearchProject
|
|
808
|
+
): Promise<ResearchSource | null> {
|
|
809
|
+
try {
|
|
810
|
+
// Skip error results from mock providers
|
|
811
|
+
if ((result.metadata as any)?.error === true) {
|
|
812
|
+
elizaLogger.warn(`[ResearchService] Skipping error result: ${result.title}`);
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Extract content if not already present
|
|
817
|
+
let fullContent = result.content;
|
|
818
|
+
if (!fullContent) {
|
|
819
|
+
// Check if it's a PDF
|
|
820
|
+
if (PDFExtractor.isPDFUrl(result.url)) {
|
|
821
|
+
const pdfExtractor = this.searchProviderFactory.getPDFExtractor();
|
|
822
|
+
const pdfContent = await pdfExtractor.extractFromURL(result.url);
|
|
823
|
+
fullContent = pdfContent?.markdown || pdfContent?.content || '';
|
|
824
|
+
} else {
|
|
825
|
+
const contentExtractor = this.searchProviderFactory.getContentExtractor();
|
|
826
|
+
const extracted = await contentExtractor.extractContent(result.url);
|
|
827
|
+
fullContent = extracted.content;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Categorize source
|
|
832
|
+
const sourceType = this.categorizeSource(result);
|
|
833
|
+
|
|
834
|
+
const source: ResearchSource = {
|
|
835
|
+
id: uuidv4(),
|
|
836
|
+
url: result.url,
|
|
837
|
+
title: result.title,
|
|
838
|
+
snippet: result.snippet,
|
|
839
|
+
fullContent,
|
|
840
|
+
accessedAt: Date.now(),
|
|
841
|
+
type: sourceType,
|
|
842
|
+
reliability: await this.assessReliability(result, sourceType),
|
|
843
|
+
domain: result.metadata?.domain,
|
|
844
|
+
author: result.metadata?.author ? [result.metadata.author].flat() : undefined,
|
|
845
|
+
publishDate: result.metadata?.publishDate,
|
|
846
|
+
metadata: {
|
|
847
|
+
language: result.metadata?.language || 'en',
|
|
848
|
+
journal: result.metadata?.type === 'academic' ? result.metadata.domain : undefined,
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
return source;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
elizaLogger.warn(`Failed to process search result ${result.url}:`, error);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private categorizeSource(result: SearchResult): SourceType {
|
|
860
|
+
const url = result.url.toLowerCase();
|
|
861
|
+
const metadata = result.metadata;
|
|
862
|
+
|
|
863
|
+
if (url.includes('arxiv.org') || url.includes('pubmed') || url.includes('.edu')) {
|
|
864
|
+
return SourceType.ACADEMIC;
|
|
865
|
+
}
|
|
866
|
+
if (metadata?.type === 'news' || url.includes('news')) {
|
|
867
|
+
return SourceType.NEWS;
|
|
868
|
+
}
|
|
869
|
+
if (url.includes('github.com') || url.includes('docs.')) {
|
|
870
|
+
return SourceType.TECHNICAL;
|
|
871
|
+
}
|
|
872
|
+
if (url.includes('.gov')) {
|
|
873
|
+
return SourceType.GOVERNMENT;
|
|
874
|
+
}
|
|
875
|
+
if (url.includes('.org') && !url.includes('wikipedia')) {
|
|
876
|
+
return SourceType.ORGANIZATION;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return SourceType.WEB;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private async assessReliability(result: SearchResult, sourceType: SourceType): Promise<number> {
|
|
883
|
+
let baseScore = 0.5;
|
|
884
|
+
|
|
885
|
+
// Adjust based on source type
|
|
886
|
+
switch (sourceType) {
|
|
887
|
+
case SourceType.ACADEMIC:
|
|
888
|
+
baseScore = 0.9;
|
|
889
|
+
break;
|
|
890
|
+
case SourceType.GOVERNMENT:
|
|
891
|
+
baseScore = 0.85;
|
|
892
|
+
break;
|
|
893
|
+
case SourceType.TECHNICAL:
|
|
894
|
+
baseScore = 0.8;
|
|
895
|
+
break;
|
|
896
|
+
case SourceType.NEWS:
|
|
897
|
+
baseScore = 0.7;
|
|
898
|
+
break;
|
|
899
|
+
case SourceType.ORGANIZATION:
|
|
900
|
+
baseScore = 0.75;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Adjust based on metadata
|
|
905
|
+
if (result.metadata?.author) baseScore += 0.05;
|
|
906
|
+
if (result.metadata?.publishDate) baseScore += 0.05;
|
|
907
|
+
|
|
908
|
+
return Math.min(baseScore, 1.0);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private async performAdaptiveRefinement(
|
|
912
|
+
project: ResearchProject,
|
|
913
|
+
config: ResearchConfig,
|
|
914
|
+
signal: AbortSignal
|
|
915
|
+
): Promise<void> {
|
|
916
|
+
const currentFindings = project.findings.slice(0, 5).map((f) => f.content);
|
|
917
|
+
const refinedQueries = await this.queryPlanner.refineQuery(
|
|
918
|
+
project.query,
|
|
919
|
+
currentFindings,
|
|
920
|
+
project.metadata.iterationHistory.length
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
const searchProvider = this.searchProviderFactory.getProvider(config.searchProviders[0]);
|
|
924
|
+
|
|
925
|
+
for (const query of refinedQueries) {
|
|
926
|
+
if (signal.aborted) break;
|
|
927
|
+
|
|
928
|
+
const results = await searchProvider.search(query, Math.floor(config.maxSearchResults / 3));
|
|
929
|
+
|
|
930
|
+
for (const result of results) {
|
|
931
|
+
const source = await this.processSearchResult(result, project);
|
|
932
|
+
if (source) {
|
|
933
|
+
project.sources.push(source);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private async analyzeFindings(project: ResearchProject, config: ResearchConfig): Promise<void> {
|
|
940
|
+
elizaLogger.info(`[ResearchService] Analyzing ${project.sources.length} sources`);
|
|
941
|
+
|
|
942
|
+
for (const source of project.sources) {
|
|
943
|
+
// Use fullContent if available, otherwise fall back to snippet
|
|
944
|
+
const contentToAnalyze = source.fullContent || source.snippet || source.title;
|
|
945
|
+
|
|
946
|
+
if (!contentToAnalyze) {
|
|
947
|
+
elizaLogger.warn(`[ResearchService] No content available for source: ${source.url}`);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Extract key findings
|
|
952
|
+
const findings = await this.extractFindings(source, project.query, contentToAnalyze);
|
|
953
|
+
|
|
954
|
+
// Extract factual claims
|
|
955
|
+
const claims = await this.extractFactualClaims(source, contentToAnalyze);
|
|
956
|
+
|
|
957
|
+
// Create research findings
|
|
958
|
+
for (const finding of findings) {
|
|
959
|
+
const researchFinding: ResearchFinding = {
|
|
960
|
+
id: uuidv4(),
|
|
961
|
+
content: finding.content,
|
|
962
|
+
source,
|
|
963
|
+
relevance: finding.relevance,
|
|
964
|
+
confidence: finding.confidence,
|
|
965
|
+
timestamp: Date.now(),
|
|
966
|
+
category: finding.category,
|
|
967
|
+
citations: [],
|
|
968
|
+
factualClaims: claims.filter((c) =>
|
|
969
|
+
finding.content.toLowerCase().includes(c.statement.substring(0, 30).toLowerCase())
|
|
970
|
+
),
|
|
971
|
+
relatedFindings: [],
|
|
972
|
+
verificationStatus: VerificationStatus.PENDING,
|
|
973
|
+
extractionMethod: source.fullContent ? 'llm-extraction' : 'snippet-extraction',
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
project.findings.push(researchFinding);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
elizaLogger.info(`[ResearchService] Extracted ${project.findings.length} findings`);
|
|
981
|
+
|
|
982
|
+
// Update quality score
|
|
983
|
+
const lastIteration =
|
|
984
|
+
project.metadata.iterationHistory[project.metadata.iterationHistory.length - 1];
|
|
985
|
+
if (lastIteration) {
|
|
986
|
+
lastIteration.findingsExtracted = project.findings.length;
|
|
987
|
+
lastIteration.qualityScore = this.calculateQualityScore(project);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private async extractFindings(
|
|
992
|
+
source: ResearchSource,
|
|
993
|
+
query: string,
|
|
994
|
+
content: string
|
|
995
|
+
): Promise<Array<{ content: string; relevance: number; confidence: number; category: string }>> {
|
|
996
|
+
const prompt = `Extract key findings from this source that are relevant to the research query.
|
|
997
|
+
|
|
998
|
+
Research Query: "${query}"
|
|
999
|
+
Source: ${source.title}
|
|
1000
|
+
URL: ${source.url}
|
|
1001
|
+
Content: ${content.substring(0, 3000)}...
|
|
1002
|
+
|
|
1003
|
+
For each finding:
|
|
1004
|
+
1. Extract the specific finding/insight
|
|
1005
|
+
2. Rate relevance to query (0-1)
|
|
1006
|
+
3. Rate confidence in the finding (0-1)
|
|
1007
|
+
4. Categorize (fact, opinion, data, theory, method, result)
|
|
1008
|
+
|
|
1009
|
+
Format as JSON array:
|
|
1010
|
+
[{
|
|
1011
|
+
"content": "finding text",
|
|
1012
|
+
"relevance": 0.9,
|
|
1013
|
+
"confidence": 0.8,
|
|
1014
|
+
"category": "fact"
|
|
1015
|
+
}]`;
|
|
1016
|
+
|
|
1017
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1018
|
+
messages: [
|
|
1019
|
+
{
|
|
1020
|
+
role: 'system',
|
|
1021
|
+
content: 'You are a research analyst extracting key findings from sources.',
|
|
1022
|
+
},
|
|
1023
|
+
{ role: 'user', content: prompt },
|
|
1024
|
+
],
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
const responseContent = typeof response === 'string' ? response : (response as any).content || '';
|
|
1029
|
+
|
|
1030
|
+
// Try to extract JSON from the response
|
|
1031
|
+
const jsonMatch = responseContent.match(/\[[\s\S]*\]/);
|
|
1032
|
+
if (jsonMatch) {
|
|
1033
|
+
const findings = JSON.parse(jsonMatch[0]);
|
|
1034
|
+
// Validate findings structure
|
|
1035
|
+
if (Array.isArray(findings) && findings.length > 0) {
|
|
1036
|
+
return findings;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// If no valid JSON found, throw error instead of creating fake findings
|
|
1041
|
+
throw new Error(`Failed to extract valid findings from LLM response. Response: ${responseContent.substring(0, 200)}`);
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
elizaLogger.error('[ResearchService] Failed to extract findings from source:', {
|
|
1044
|
+
sourceUrl: source.url,
|
|
1045
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1046
|
+
contentLength: content.length
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// Return empty array instead of fake findings - let the caller handle the failure
|
|
1050
|
+
return [];
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private async extractFactualClaims(source: ResearchSource, content: string): Promise<FactualClaim[]> {
|
|
1055
|
+
const prompt = `Extract specific factual claims from this source that can be verified.
|
|
1056
|
+
|
|
1057
|
+
Source: ${source.title}
|
|
1058
|
+
Content: ${content.substring(0, 2000)}...
|
|
1059
|
+
|
|
1060
|
+
For each factual claim:
|
|
1061
|
+
1. Extract the exact statement
|
|
1062
|
+
2. Identify supporting evidence in the text
|
|
1063
|
+
3. Rate confidence (0-1)
|
|
1064
|
+
|
|
1065
|
+
Format as JSON array:
|
|
1066
|
+
[{
|
|
1067
|
+
"statement": "claim text",
|
|
1068
|
+
"evidence": ["supporting text 1", "supporting text 2"],
|
|
1069
|
+
"confidence": 0.9
|
|
1070
|
+
}]`;
|
|
1071
|
+
|
|
1072
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1073
|
+
messages: [
|
|
1074
|
+
{
|
|
1075
|
+
role: 'system',
|
|
1076
|
+
content: 'You are a fact-checker extracting verifiable claims from sources.',
|
|
1077
|
+
},
|
|
1078
|
+
{ role: 'user', content: prompt },
|
|
1079
|
+
],
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
const responseContent = typeof response === 'string' ? response : (response as any).content || '';
|
|
1084
|
+
// Try to extract JSON from the response
|
|
1085
|
+
const jsonMatch = responseContent.match(/\[[\s\S]*\]/);
|
|
1086
|
+
if (jsonMatch) {
|
|
1087
|
+
const claims = JSON.parse(jsonMatch[0]);
|
|
1088
|
+
return claims.map((claim: any) => ({
|
|
1089
|
+
id: uuidv4(),
|
|
1090
|
+
statement: claim.statement,
|
|
1091
|
+
supportingEvidence: claim.evidence || [],
|
|
1092
|
+
sourceUrls: [source.url],
|
|
1093
|
+
verificationStatus: VerificationStatus.UNVERIFIED,
|
|
1094
|
+
confidenceScore: claim.confidence || 0.5,
|
|
1095
|
+
relatedClaims: [],
|
|
1096
|
+
}));
|
|
1097
|
+
}
|
|
1098
|
+
return [];
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
elizaLogger.warn('[ResearchService] Failed to parse claims');
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private calculateQualityScore(project: ResearchProject): number {
|
|
1106
|
+
const sourceQuality =
|
|
1107
|
+
project.sources.reduce((sum, s) => sum + s.reliability, 0) / project.sources.length;
|
|
1108
|
+
const findingQuality =
|
|
1109
|
+
project.findings.reduce((sum, f) => sum + f.relevance * f.confidence, 0) /
|
|
1110
|
+
project.findings.length;
|
|
1111
|
+
const coverage = Math.min(
|
|
1112
|
+
project.sources.length / project.metadata.queryPlan.expectedSources,
|
|
1113
|
+
1
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
return sourceQuality * 0.3 + findingQuality * 0.5 + coverage * 0.2;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private async synthesizeFindings(project: ResearchProject): Promise<void> {
|
|
1120
|
+
elizaLogger.info(`[ResearchService] Starting synthesis for ${project.findings.length} findings`);
|
|
1121
|
+
|
|
1122
|
+
// Group findings by category
|
|
1123
|
+
const categories = new Map<string, ResearchFinding[]>();
|
|
1124
|
+
for (const finding of project.findings) {
|
|
1125
|
+
const existing = categories.get(finding.category) || [];
|
|
1126
|
+
existing.push(finding);
|
|
1127
|
+
categories.set(finding.category, existing);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Synthesize by category
|
|
1131
|
+
const categoryAnalysis: Record<string, string> = {};
|
|
1132
|
+
for (const [category, findings] of categories) {
|
|
1133
|
+
const synthesis = await this.synthesizeCategory(category, findings);
|
|
1134
|
+
categoryAnalysis[category] = synthesis;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Overall synthesis
|
|
1138
|
+
const overallSynthesis = await this.createOverallSynthesis(project, categoryAnalysis);
|
|
1139
|
+
|
|
1140
|
+
// Update metadata
|
|
1141
|
+
project.metadata.categoryAnalysis = categoryAnalysis;
|
|
1142
|
+
project.metadata.synthesis = overallSynthesis;
|
|
1143
|
+
|
|
1144
|
+
elizaLogger.info(`[ResearchService] Synthesis completed. Overall synthesis length: ${overallSynthesis.length} characters`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
private async synthesizeCategory(category: string, findings: ResearchFinding[]): Promise<string> {
|
|
1148
|
+
const findingTexts = findings.map((f) => f.content).join('\n\n');
|
|
1149
|
+
|
|
1150
|
+
const prompt = `Synthesize these ${category} findings into a coherent summary:
|
|
1151
|
+
|
|
1152
|
+
${findingTexts}
|
|
1153
|
+
|
|
1154
|
+
Create a comprehensive synthesis that:
|
|
1155
|
+
1. Identifies common themes
|
|
1156
|
+
2. Notes contradictions or debates
|
|
1157
|
+
3. Highlights key insights
|
|
1158
|
+
4. Maintains academic rigor`;
|
|
1159
|
+
|
|
1160
|
+
elizaLogger.debug(`[ResearchService] Calling LLM for category synthesis with prompt length: ${prompt.length}`);
|
|
1161
|
+
|
|
1162
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1163
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const result = typeof response === 'string' ? response : (response as any).content || '';
|
|
1167
|
+
elizaLogger.debug(`[ResearchService] Category synthesis response length: ${result.length}`);
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
private async createOverallSynthesis(
|
|
1172
|
+
project: ResearchProject,
|
|
1173
|
+
categoryAnalysis: Record<string, string>
|
|
1174
|
+
): Promise<string> {
|
|
1175
|
+
const prompt = `Create an overall synthesis of this research project:
|
|
1176
|
+
|
|
1177
|
+
Research Query: "${project.query}"
|
|
1178
|
+
Domain: ${project.metadata.domain}
|
|
1179
|
+
Task Type: ${project.metadata.taskType}
|
|
1180
|
+
|
|
1181
|
+
Category Analyses:
|
|
1182
|
+
${Object.entries(categoryAnalysis)
|
|
1183
|
+
.map(([cat, analysis]) => `${cat}:\n${analysis}`)
|
|
1184
|
+
.join('\n\n')}
|
|
1185
|
+
|
|
1186
|
+
Create a comprehensive synthesis that:
|
|
1187
|
+
1. Answers the original research question
|
|
1188
|
+
2. Integrates insights across categories
|
|
1189
|
+
3. Identifies knowledge gaps
|
|
1190
|
+
4. Suggests future research directions`;
|
|
1191
|
+
|
|
1192
|
+
elizaLogger.debug(`[ResearchService] Calling LLM for overall synthesis with prompt length: ${prompt.length}`);
|
|
1193
|
+
|
|
1194
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1195
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const result = typeof response === 'string' ? response : (response as any).content || '';
|
|
1199
|
+
elizaLogger.debug(`[ResearchService] Overall synthesis response length: ${result.length}`);
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private async generateReport(project: ResearchProject): Promise<void> {
|
|
1204
|
+
try {
|
|
1205
|
+
elizaLogger.info(`[ResearchService] Generating comprehensive report for project ${project.id}`);
|
|
1206
|
+
|
|
1207
|
+
// Use new prompt templates for better structure
|
|
1208
|
+
const queryAnalysis = await this.analyzeQueryForReport(project);
|
|
1209
|
+
|
|
1210
|
+
// Step 1: Generate initial comprehensive report
|
|
1211
|
+
const initialSections = await this.generateComprehensiveReport(project);
|
|
1212
|
+
|
|
1213
|
+
// Step 2: Extract and verify claims from initial report
|
|
1214
|
+
const claims = await this.extractClaimsFromReport(initialSections, project);
|
|
1215
|
+
const verificationResults = await this.verifyClaimsWithSources(claims, project);
|
|
1216
|
+
|
|
1217
|
+
// Step 3: Enhance report with verification results and detailed analysis
|
|
1218
|
+
const enhancedSections = await this.enhanceReportWithDetailedAnalysis(
|
|
1219
|
+
project,
|
|
1220
|
+
initialSections,
|
|
1221
|
+
verificationResults
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
// Step 4: Add executive summary and finalize
|
|
1225
|
+
const executiveSummary = await this.generateExecutiveSummary(project, verificationResults);
|
|
1226
|
+
|
|
1227
|
+
// Build final report with citations and bibliography
|
|
1228
|
+
const fullReport = this.buildFinalReport(executiveSummary, enhancedSections, project);
|
|
1229
|
+
|
|
1230
|
+
// Build proper ResearchReport structure
|
|
1231
|
+
const wordCount = fullReport.split(' ').length;
|
|
1232
|
+
const readingTime = Math.ceil(wordCount / 200);
|
|
1233
|
+
|
|
1234
|
+
project.report = {
|
|
1235
|
+
id: uuidv4(),
|
|
1236
|
+
title: `Research Report: ${project.query}`,
|
|
1237
|
+
abstract: executiveSummary.substring(0, 300) + '...',
|
|
1238
|
+
summary: executiveSummary,
|
|
1239
|
+
sections: enhancedSections,
|
|
1240
|
+
citations: this.extractAllCitations(project),
|
|
1241
|
+
bibliography: this.createBibliography(project),
|
|
1242
|
+
generatedAt: Date.now(),
|
|
1243
|
+
wordCount,
|
|
1244
|
+
readingTime,
|
|
1245
|
+
evaluationMetrics: {
|
|
1246
|
+
raceScore: {
|
|
1247
|
+
overall: 0,
|
|
1248
|
+
comprehensiveness: 0,
|
|
1249
|
+
depth: 0,
|
|
1250
|
+
instructionFollowing: 0,
|
|
1251
|
+
readability: 0,
|
|
1252
|
+
breakdown: [],
|
|
1253
|
+
},
|
|
1254
|
+
factScore: {
|
|
1255
|
+
citationAccuracy: 0,
|
|
1256
|
+
effectiveCitations: 0,
|
|
1257
|
+
totalCitations: 0,
|
|
1258
|
+
verifiedCitations: 0,
|
|
1259
|
+
disputedCitations: 0,
|
|
1260
|
+
citationCoverage: 0,
|
|
1261
|
+
sourceCredibility: 0,
|
|
1262
|
+
breakdown: [],
|
|
1263
|
+
},
|
|
1264
|
+
timestamp: Date.now(),
|
|
1265
|
+
evaluatorVersion: '1.0',
|
|
1266
|
+
},
|
|
1267
|
+
exportFormats: [
|
|
1268
|
+
{ format: 'json', generated: false },
|
|
1269
|
+
{ format: 'markdown', generated: false },
|
|
1270
|
+
{ format: 'deepresearch', generated: false },
|
|
1271
|
+
],
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// Save to file
|
|
1275
|
+
await this.saveReportToFile(project);
|
|
1276
|
+
|
|
1277
|
+
elizaLogger.info('[ResearchService] Report generation complete');
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
elizaLogger.error('[ResearchService] Report generation failed:', error);
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
private async analyzeQueryForReport(project: ResearchProject): Promise<any> {
|
|
1285
|
+
const prompt = formatPrompt(RESEARCH_PROMPTS.QUERY_ANALYSIS, { query: project.query });
|
|
1286
|
+
|
|
1287
|
+
const config = getPromptConfig('analysis');
|
|
1288
|
+
const response = await this.runtime.useModel(config.modelType, {
|
|
1289
|
+
messages: [
|
|
1290
|
+
{ role: 'system', content: 'You are an expert research analyst.' },
|
|
1291
|
+
{ role: 'user', content: prompt }
|
|
1292
|
+
],
|
|
1293
|
+
temperature: config.temperature,
|
|
1294
|
+
max_tokens: config.maxTokens,
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
const content = typeof response === 'string' ? response : response.content || '';
|
|
1299
|
+
return JSON.parse(content);
|
|
1300
|
+
} catch {
|
|
1301
|
+
return { query: project.query, concepts: [], dimensions: [] };
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private async extractClaimsFromReport(
|
|
1306
|
+
sections: ReportSection[],
|
|
1307
|
+
project: ResearchProject
|
|
1308
|
+
): Promise<FactualClaim[]> {
|
|
1309
|
+
const claims: FactualClaim[] = [];
|
|
1310
|
+
|
|
1311
|
+
for (const section of sections) {
|
|
1312
|
+
const sectionClaims = await this.extractClaimsFromText(section.content, project.sources);
|
|
1313
|
+
claims.push(...sectionClaims);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
return claims;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
private async extractClaimsFromText(
|
|
1320
|
+
text: string,
|
|
1321
|
+
sources: ResearchSource[]
|
|
1322
|
+
): Promise<FactualClaim[]> {
|
|
1323
|
+
const prompt = formatPrompt(RESEARCH_PROMPTS.CLAIM_EXTRACTION, {
|
|
1324
|
+
text,
|
|
1325
|
+
sourceCount: sources.length
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
const config = getPromptConfig('extraction');
|
|
1329
|
+
const response = await this.runtime.useModel(config.modelType, {
|
|
1330
|
+
messages: [
|
|
1331
|
+
{ role: 'system', content: 'Extract specific, verifiable claims from the text.' },
|
|
1332
|
+
{ role: 'user', content: prompt }
|
|
1333
|
+
],
|
|
1334
|
+
temperature: config.temperature,
|
|
1335
|
+
max_tokens: config.maxTokens,
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
try {
|
|
1339
|
+
const content = typeof response === 'string' ? response : response.content || '';
|
|
1340
|
+
const extractedClaims = JSON.parse(content).claims || [];
|
|
1341
|
+
|
|
1342
|
+
return extractedClaims.map((claim: any) => ({
|
|
1343
|
+
statement: claim.statement,
|
|
1344
|
+
confidence: claim.confidence || 0.5,
|
|
1345
|
+
sourceUrls: claim.sources || [],
|
|
1346
|
+
supportingEvidence: claim.evidence || [],
|
|
1347
|
+
category: claim.category || 'general'
|
|
1348
|
+
}));
|
|
1349
|
+
} catch {
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private async verifyClaimsWithSources(
|
|
1355
|
+
claims: FactualClaim[],
|
|
1356
|
+
project: ResearchProject
|
|
1357
|
+
): Promise<Map<string, any>> {
|
|
1358
|
+
const verificationResults = new Map();
|
|
1359
|
+
|
|
1360
|
+
// Group claims by primary source for efficient verification
|
|
1361
|
+
const claimsBySource = new Map<string, FactualClaim[]>();
|
|
1362
|
+
|
|
1363
|
+
for (const claim of claims) {
|
|
1364
|
+
const primaryUrl = claim.sourceUrls[0];
|
|
1365
|
+
if (primaryUrl) {
|
|
1366
|
+
if (!claimsBySource.has(primaryUrl)) {
|
|
1367
|
+
claimsBySource.set(primaryUrl, []);
|
|
1368
|
+
}
|
|
1369
|
+
claimsBySource.get(primaryUrl)!.push(claim);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Verify claims batch by source
|
|
1374
|
+
for (const [sourceUrl, sourceClaims] of claimsBySource) {
|
|
1375
|
+
const source = project.sources.find(s => s.url === sourceUrl);
|
|
1376
|
+
if (source) {
|
|
1377
|
+
const results = await this.claimVerifier.batchVerifyClaims(
|
|
1378
|
+
sourceClaims.map(claim => ({ claim, primarySource: source })),
|
|
1379
|
+
project.sources
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1382
|
+
results.forEach((result, index) => {
|
|
1383
|
+
verificationResults.set(sourceClaims[index].statement, result);
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return verificationResults;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private buildFinalReport(
|
|
1392
|
+
executiveSummary: string,
|
|
1393
|
+
sections: ReportSection[],
|
|
1394
|
+
project: ResearchProject
|
|
1395
|
+
): string {
|
|
1396
|
+
const reportParts = [
|
|
1397
|
+
`# ${project.query}`,
|
|
1398
|
+
`\n_Generated on ${new Date().toISOString()}_\n`,
|
|
1399
|
+
`## Executive Summary\n\n${executiveSummary}\n`,
|
|
1400
|
+
];
|
|
1401
|
+
|
|
1402
|
+
// Add main sections
|
|
1403
|
+
for (const section of sections) {
|
|
1404
|
+
reportParts.push(`## ${section.heading}\n\n${section.content}\n`);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Add methodology section
|
|
1408
|
+
const methodology = this.generateMethodologySection(project);
|
|
1409
|
+
reportParts.push(`## Research Methodology\n\n${methodology}\n`);
|
|
1410
|
+
|
|
1411
|
+
// Add references
|
|
1412
|
+
reportParts.push('## References\n');
|
|
1413
|
+
const bibliography = this.createBibliography(project);
|
|
1414
|
+
bibliography.forEach((entry, idx) => {
|
|
1415
|
+
reportParts.push(`${idx + 1}. ${entry.citation}`);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
return reportParts.join('\n');
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private async saveReportToFile(project: ResearchProject): Promise<void> {
|
|
1422
|
+
try {
|
|
1423
|
+
// Create logs directory
|
|
1424
|
+
const logsDir = path.join(process.cwd(), 'research_logs');
|
|
1425
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
1426
|
+
|
|
1427
|
+
// Generate filename with timestamp and sanitized query
|
|
1428
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1429
|
+
const sanitizedQuery = project.query
|
|
1430
|
+
.toLowerCase()
|
|
1431
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1432
|
+
.substring(0, 50);
|
|
1433
|
+
const filename = `${timestamp}_${sanitizedQuery}.md`;
|
|
1434
|
+
const filepath = path.join(logsDir, filename);
|
|
1435
|
+
|
|
1436
|
+
// Export as markdown
|
|
1437
|
+
const markdownContent = this.exportAsMarkdown(project);
|
|
1438
|
+
|
|
1439
|
+
// Add metadata header
|
|
1440
|
+
const fullContent = `---
|
|
1441
|
+
id: ${project.id}
|
|
1442
|
+
query: ${project.query}
|
|
1443
|
+
status: ${project.status}
|
|
1444
|
+
domain: ${project.metadata.domain}
|
|
1445
|
+
taskType: ${project.metadata.taskType}
|
|
1446
|
+
createdAt: ${new Date(project.createdAt).toISOString()}
|
|
1447
|
+
completedAt: ${project.completedAt ? new Date(project.completedAt).toISOString() : 'In Progress'}
|
|
1448
|
+
sources: ${project.sources.length}
|
|
1449
|
+
findings: ${project.findings.length}
|
|
1450
|
+
---
|
|
1451
|
+
|
|
1452
|
+
${markdownContent}
|
|
1453
|
+
|
|
1454
|
+
## Metadata
|
|
1455
|
+
|
|
1456
|
+
- **Research Domain**: ${project.metadata.domain}
|
|
1457
|
+
- **Task Type**: ${project.metadata.taskType}
|
|
1458
|
+
- **Research Depth**: ${project.metadata.depth}
|
|
1459
|
+
- **Sources Analyzed**: ${project.sources.length}
|
|
1460
|
+
- **Key Findings**: ${project.findings.length}
|
|
1461
|
+
- **Word Count**: ${project.report?.wordCount || 'N/A'}
|
|
1462
|
+
- **Estimated Reading Time**: ${project.report?.readingTime || 'N/A'} minutes
|
|
1463
|
+
|
|
1464
|
+
## Source URLs
|
|
1465
|
+
|
|
1466
|
+
${project.sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join('\n')}
|
|
1467
|
+
`;
|
|
1468
|
+
|
|
1469
|
+
await fs.writeFile(filepath, fullContent, 'utf-8');
|
|
1470
|
+
elizaLogger.info(`[ResearchService] Report saved to: ${filepath}`);
|
|
1471
|
+
|
|
1472
|
+
// Also save JSON version
|
|
1473
|
+
const jsonFilepath = filepath.replace('.md', '.json');
|
|
1474
|
+
await fs.writeFile(jsonFilepath, JSON.stringify(project, null, 2), 'utf-8');
|
|
1475
|
+
elizaLogger.info(`[ResearchService] JSON data saved to: ${jsonFilepath}`);
|
|
1476
|
+
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
elizaLogger.error('[ResearchService] Failed to save report to file:', error);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* PASS 1: Generate comprehensive initial report sections
|
|
1484
|
+
* Creates detailed sections for each category with thorough analysis
|
|
1485
|
+
*/
|
|
1486
|
+
private async generateComprehensiveReport(project: ResearchProject): Promise<ReportSection[]> {
|
|
1487
|
+
elizaLogger.info(`[ResearchService] PASS 1: Generating comprehensive initial report for ${project.findings.length} findings`);
|
|
1488
|
+
|
|
1489
|
+
const sections: ReportSection[] = [];
|
|
1490
|
+
|
|
1491
|
+
// Group findings by category for organized analysis
|
|
1492
|
+
const categories = new Map<string, ResearchFinding[]>();
|
|
1493
|
+
for (const finding of project.findings) {
|
|
1494
|
+
const existing = categories.get(finding.category) || [];
|
|
1495
|
+
existing.push(finding);
|
|
1496
|
+
categories.set(finding.category, existing);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
elizaLogger.info(`[ResearchService] Found ${categories.size} categories: ${Array.from(categories.keys()).join(', ')}`);
|
|
1500
|
+
|
|
1501
|
+
// Create executive summary
|
|
1502
|
+
const executiveSummary = await this.generateExecutiveSummary(project, new Map());
|
|
1503
|
+
sections.push({
|
|
1504
|
+
id: 'executive-summary',
|
|
1505
|
+
heading: 'Executive Summary',
|
|
1506
|
+
level: 0,
|
|
1507
|
+
content: executiveSummary,
|
|
1508
|
+
findings: [],
|
|
1509
|
+
citations: [],
|
|
1510
|
+
metadata: {
|
|
1511
|
+
wordCount: executiveSummary.split(' ').length,
|
|
1512
|
+
citationDensity: 0,
|
|
1513
|
+
readabilityScore: 0,
|
|
1514
|
+
keyTerms: [],
|
|
1515
|
+
},
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// Generate comprehensive sections for each category
|
|
1519
|
+
for (const [category, findings] of categories.entries()) {
|
|
1520
|
+
elizaLogger.info(`[ResearchService] PASS 1: Generating comprehensive analysis for category: ${category} (${findings.length} findings)`);
|
|
1521
|
+
|
|
1522
|
+
const categoryAnalysis = await this.generateDetailedCategoryAnalysis(category, findings, project.query);
|
|
1523
|
+
|
|
1524
|
+
sections.push({
|
|
1525
|
+
id: `comprehensive-${category}`,
|
|
1526
|
+
heading: this.formatCategoryHeading(category),
|
|
1527
|
+
level: 1,
|
|
1528
|
+
content: categoryAnalysis,
|
|
1529
|
+
findings: findings.map(f => f.id),
|
|
1530
|
+
citations: this.extractCitations(findings),
|
|
1531
|
+
metadata: {
|
|
1532
|
+
wordCount: categoryAnalysis.split(' ').length,
|
|
1533
|
+
citationDensity: findings.length / (categoryAnalysis.split(' ').length / 100),
|
|
1534
|
+
readabilityScore: 0,
|
|
1535
|
+
keyTerms: [],
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Generate methodology section
|
|
1541
|
+
const methodology = await this.generateMethodologySection(project);
|
|
1542
|
+
sections.push({
|
|
1543
|
+
id: 'methodology',
|
|
1544
|
+
heading: 'Research Methodology',
|
|
1545
|
+
level: 1,
|
|
1546
|
+
content: methodology,
|
|
1547
|
+
findings: [],
|
|
1548
|
+
citations: [],
|
|
1549
|
+
metadata: {
|
|
1550
|
+
wordCount: methodology.split(' ').length,
|
|
1551
|
+
citationDensity: 0,
|
|
1552
|
+
readabilityScore: 0,
|
|
1553
|
+
keyTerms: [],
|
|
1554
|
+
},
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// Generate implications and future work
|
|
1558
|
+
const implications = await this.generateImplicationsSection(project);
|
|
1559
|
+
sections.push({
|
|
1560
|
+
id: 'implications',
|
|
1561
|
+
heading: 'Implications and Future Directions',
|
|
1562
|
+
level: 1,
|
|
1563
|
+
content: implications,
|
|
1564
|
+
findings: [],
|
|
1565
|
+
citations: [],
|
|
1566
|
+
metadata: {
|
|
1567
|
+
wordCount: implications.split(' ').length,
|
|
1568
|
+
citationDensity: 0,
|
|
1569
|
+
readabilityScore: 0,
|
|
1570
|
+
keyTerms: [],
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
const totalWords = sections.reduce((sum, s) => sum + s.metadata.wordCount, 0);
|
|
1575
|
+
elizaLogger.info(`[ResearchService] PASS 1 completed: Generated ${sections.length} sections with ${totalWords} total words`);
|
|
1576
|
+
|
|
1577
|
+
return sections;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* PASS 2: Enhance report with detailed source analysis
|
|
1582
|
+
* Identifies top sources, extracts detailed content, and performs comprehensive rewrite
|
|
1583
|
+
*/
|
|
1584
|
+
private async enhanceReportWithDetailedAnalysis(project: ResearchProject, initialSections: ReportSection[], verificationResults: Map<string, any>): Promise<ReportSection[]> {
|
|
1585
|
+
elizaLogger.info(`[ResearchService] PASS 2: Beginning detailed source analysis enhancement`);
|
|
1586
|
+
|
|
1587
|
+
// Step 1: Identify top 10 sources
|
|
1588
|
+
const topSources = this.identifyTopSources(project, 10);
|
|
1589
|
+
elizaLogger.info(`[ResearchService] PASS 2: Identified top ${topSources.length} sources for detailed analysis`);
|
|
1590
|
+
|
|
1591
|
+
// Step 2: Extract 10k words from each top source
|
|
1592
|
+
const detailedSourceContent = await this.extractDetailedSourceContent(topSources);
|
|
1593
|
+
elizaLogger.info(`[ResearchService] PASS 2: Extracted detailed content from ${detailedSourceContent.size} sources`);
|
|
1594
|
+
|
|
1595
|
+
// Step 3: Enhance each section with detailed analysis
|
|
1596
|
+
const enhancedSections: ReportSection[] = [];
|
|
1597
|
+
|
|
1598
|
+
for (const section of initialSections) {
|
|
1599
|
+
elizaLogger.info(`[ResearchService] PASS 2: Enhancing section "${section.heading}" with detailed analysis`);
|
|
1600
|
+
|
|
1601
|
+
const enhancedContent = await this.enhanceSection(section, detailedSourceContent, project);
|
|
1602
|
+
const enhancedSection = {
|
|
1603
|
+
...section,
|
|
1604
|
+
content: enhancedContent,
|
|
1605
|
+
metadata: {
|
|
1606
|
+
...section.metadata,
|
|
1607
|
+
wordCount: enhancedContent.split(' ').length,
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
enhancedSections.push(enhancedSection);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Step 4: Add detailed source analysis section
|
|
1615
|
+
const detailedAnalysis = await this.generateDetailedSourceAnalysis(detailedSourceContent, project);
|
|
1616
|
+
enhancedSections.push({
|
|
1617
|
+
id: 'detailed-source-analysis',
|
|
1618
|
+
heading: 'Detailed Source Analysis',
|
|
1619
|
+
level: 1,
|
|
1620
|
+
content: detailedAnalysis,
|
|
1621
|
+
findings: [],
|
|
1622
|
+
citations: [],
|
|
1623
|
+
metadata: {
|
|
1624
|
+
wordCount: detailedAnalysis.split(' ').length,
|
|
1625
|
+
citationDensity: 0,
|
|
1626
|
+
readabilityScore: 0,
|
|
1627
|
+
keyTerms: [],
|
|
1628
|
+
},
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
const totalWords = enhancedSections.reduce((sum, s) => sum + s.metadata.wordCount, 0);
|
|
1632
|
+
elizaLogger.info(`[ResearchService] PASS 2 completed: Enhanced ${enhancedSections.length} sections with ${totalWords} total words`);
|
|
1633
|
+
|
|
1634
|
+
return enhancedSections;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
private async generateExecutiveSummary(project: ResearchProject, verificationResults: Map<string, any>): Promise<string> {
|
|
1638
|
+
const findingsSample = project.findings.slice(0, 10).map(f => f.content).join('\n\n');
|
|
1639
|
+
|
|
1640
|
+
const prompt = `Create a comprehensive executive summary for this research project.
|
|
1641
|
+
|
|
1642
|
+
Research Query: "${project.query}"
|
|
1643
|
+
|
|
1644
|
+
Key Findings Sample:
|
|
1645
|
+
${findingsSample}
|
|
1646
|
+
|
|
1647
|
+
Total Sources: ${project.sources.length}
|
|
1648
|
+
Total Findings: ${project.findings.length}
|
|
1649
|
+
|
|
1650
|
+
Create a 400-500 word executive summary that:
|
|
1651
|
+
1. States the research objective clearly
|
|
1652
|
+
2. Summarizes the methodology used
|
|
1653
|
+
3. Highlights the most significant findings
|
|
1654
|
+
4. Discusses key implications
|
|
1655
|
+
5. Provides actionable insights
|
|
1656
|
+
|
|
1657
|
+
Focus on being comprehensive yet accessible, suitable for both technical and non-technical audiences.`;
|
|
1658
|
+
|
|
1659
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1660
|
+
messages: [
|
|
1661
|
+
{ role: 'system', content: 'You are a research analyst creating executive summaries for comprehensive research reports.' },
|
|
1662
|
+
{ role: 'user', content: prompt }
|
|
1663
|
+
],
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
return typeof response === 'string' ? response : (response as any).content || '';
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
private async generateDetailedCategoryAnalysis(category: string, findings: ResearchFinding[], originalQuery: string): Promise<string> {
|
|
1670
|
+
const findingTexts = findings.map(f => f.content).join('\n\n');
|
|
1671
|
+
|
|
1672
|
+
const prompt = `Create a comprehensive analysis for the category "${category}" based on these research findings.
|
|
1673
|
+
|
|
1674
|
+
Original Research Query: "${originalQuery}"
|
|
1675
|
+
|
|
1676
|
+
Findings in this category:
|
|
1677
|
+
${findingTexts}
|
|
1678
|
+
|
|
1679
|
+
Create a detailed 800-1200 word analysis that:
|
|
1680
|
+
1. Introduces the category and its relevance to the research question
|
|
1681
|
+
2. Analyzes patterns and themes across findings
|
|
1682
|
+
3. Discusses methodological approaches mentioned
|
|
1683
|
+
4. Identifies consensus and disagreements in the literature
|
|
1684
|
+
5. Evaluates the strength of evidence
|
|
1685
|
+
6. Discusses limitations and gaps
|
|
1686
|
+
7. Connects findings to broader implications
|
|
1687
|
+
8. Suggests areas for future research
|
|
1688
|
+
|
|
1689
|
+
Use a scholarly tone with clear subsections. Be thorough and analytical.`;
|
|
1690
|
+
|
|
1691
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1692
|
+
messages: [
|
|
1693
|
+
{ role: 'system', content: 'You are a research analyst writing comprehensive literature reviews.' },
|
|
1694
|
+
{ role: 'user', content: prompt }
|
|
1695
|
+
],
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
return typeof response === 'string' ? response : (response as any).content || '';
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private async generateMethodologySection(project: ResearchProject): Promise<string> {
|
|
1702
|
+
const searchProviders = project.sources.map(s => s.url.split('.')[1] || 'unknown').slice(0, 5);
|
|
1703
|
+
const domains = [...new Set(project.sources.map(s => s.type))];
|
|
1704
|
+
|
|
1705
|
+
const prompt = `Create a comprehensive methodology section for this research project.
|
|
1706
|
+
|
|
1707
|
+
Research Query: "${project.query}"
|
|
1708
|
+
Sources Analyzed: ${project.sources.length}
|
|
1709
|
+
Search Domains: ${domains.join(', ')}
|
|
1710
|
+
Key Findings: ${project.findings.length}
|
|
1711
|
+
|
|
1712
|
+
Create a 400-600 word methodology section that describes:
|
|
1713
|
+
1. Research approach and design
|
|
1714
|
+
2. Search strategy and keywords used
|
|
1715
|
+
3. Source selection criteria
|
|
1716
|
+
4. Data extraction methods
|
|
1717
|
+
5. Quality assessment procedures
|
|
1718
|
+
6. Analysis framework
|
|
1719
|
+
7. Limitations and potential biases
|
|
1720
|
+
|
|
1721
|
+
Be specific about the systematic approach taken and justify methodological choices.`;
|
|
1722
|
+
|
|
1723
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1724
|
+
messages: [
|
|
1725
|
+
{ role: 'system', content: 'You are a research methodologist describing systematic research approaches.' },
|
|
1726
|
+
{ role: 'user', content: prompt }
|
|
1727
|
+
],
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
return typeof response === 'string' ? response : (response as any).content || '';
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
private async generateImplicationsSection(project: ResearchProject): Promise<string> {
|
|
1734
|
+
const keyFindings = project.findings
|
|
1735
|
+
.sort((a, b) => (b.relevance * b.confidence) - (a.relevance * a.confidence))
|
|
1736
|
+
.slice(0, 8)
|
|
1737
|
+
.map(f => f.content)
|
|
1738
|
+
.join('\n\n');
|
|
1739
|
+
|
|
1740
|
+
const prompt = `Create a comprehensive implications and future directions section.
|
|
1741
|
+
|
|
1742
|
+
Research Query: "${project.query}"
|
|
1743
|
+
|
|
1744
|
+
Key Findings:
|
|
1745
|
+
${keyFindings}
|
|
1746
|
+
|
|
1747
|
+
Create a 600-800 word section that:
|
|
1748
|
+
1. Discusses theoretical implications
|
|
1749
|
+
2. Identifies practical applications
|
|
1750
|
+
3. Considers policy implications (if relevant)
|
|
1751
|
+
4. Addresses methodological contributions
|
|
1752
|
+
5. Suggests specific future research directions
|
|
1753
|
+
6. Discusses potential real-world impact
|
|
1754
|
+
7. Identifies research gaps that need attention
|
|
1755
|
+
8. Proposes concrete next steps
|
|
1756
|
+
|
|
1757
|
+
Be forward-looking and actionable while grounding recommendations in the evidence found.`;
|
|
1758
|
+
|
|
1759
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1760
|
+
messages: [
|
|
1761
|
+
{ role: 'system', content: 'You are a research strategist identifying implications and future research directions.' },
|
|
1762
|
+
{ role: 'user', content: prompt }
|
|
1763
|
+
],
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
return typeof response === 'string' ? response : (response as any).content || '';
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
private identifyTopSources(project: ResearchProject, count: number): ResearchSource[] {
|
|
1770
|
+
// Score sources based on multiple criteria
|
|
1771
|
+
const scoredSources = project.sources.map(source => {
|
|
1772
|
+
const findingsFromSource = project.findings.filter(f => f.source.id === source.id);
|
|
1773
|
+
const avgRelevance = findingsFromSource.reduce((sum, f) => sum + f.relevance, 0) / Math.max(findingsFromSource.length, 1);
|
|
1774
|
+
const avgConfidence = findingsFromSource.reduce((sum, f) => sum + f.confidence, 0) / Math.max(findingsFromSource.length, 1);
|
|
1775
|
+
const contentLength = source.fullContent?.length || source.snippet?.length || 0;
|
|
1776
|
+
|
|
1777
|
+
// Scoring formula: findings count + avg relevance + avg confidence + content richness + source reliability
|
|
1778
|
+
const score = (findingsFromSource.length * 2) + avgRelevance + avgConfidence + (contentLength > 5000 ? 1 : 0) + source.reliability;
|
|
1779
|
+
|
|
1780
|
+
return { source, score, findingsCount: findingsFromSource.length };
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
// Sort by score and return top sources
|
|
1784
|
+
return scoredSources
|
|
1785
|
+
.sort((a, b) => b.score - a.score)
|
|
1786
|
+
.slice(0, count)
|
|
1787
|
+
.map(s => {
|
|
1788
|
+
elizaLogger.info(`[ResearchService] Top source: ${s.source.title} (Score: ${s.score.toFixed(2)}, Findings: ${s.findingsCount})`);
|
|
1789
|
+
return s.source;
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
private async extractDetailedSourceContent(sources: ResearchSource[]): Promise<Map<string, string>> {
|
|
1794
|
+
const detailedContent = new Map<string, string>();
|
|
1795
|
+
|
|
1796
|
+
for (const source of sources) {
|
|
1797
|
+
try {
|
|
1798
|
+
elizaLogger.info(`[ResearchService] Extracting detailed content from: ${source.title}`);
|
|
1799
|
+
|
|
1800
|
+
let content = source.fullContent || source.snippet || '';
|
|
1801
|
+
|
|
1802
|
+
// If we need more content, try to re-extract with higher limits
|
|
1803
|
+
if (content.length < 8000 && source.url) {
|
|
1804
|
+
elizaLogger.info(`[ResearchService] Re-extracting with higher limits for: ${source.url}`);
|
|
1805
|
+
const extractor = this.searchProviderFactory.getContentExtractor();
|
|
1806
|
+
const extractedContent = await extractor.extractContent(source.url);
|
|
1807
|
+
const extractedText = typeof extractedContent === 'string' ? extractedContent : extractedContent?.content || '';
|
|
1808
|
+
if (extractedText && extractedText.length > content.length) {
|
|
1809
|
+
content = extractedText;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Take first 10k words
|
|
1814
|
+
const words = content.split(/\s+/).slice(0, 10000);
|
|
1815
|
+
const detailedText = words.join(' ');
|
|
1816
|
+
|
|
1817
|
+
detailedContent.set(source.id, detailedText);
|
|
1818
|
+
elizaLogger.info(`[ResearchService] Extracted ${detailedText.length} characters from ${source.title}`);
|
|
1819
|
+
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
elizaLogger.warn(`[ResearchService] Failed to extract detailed content from ${source.title}:`, error);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return detailedContent;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
private async enhanceSection(section: ReportSection, detailedContent: Map<string, string>, project: ResearchProject): Promise<string> {
|
|
1829
|
+
// Get relevant detailed content for this section
|
|
1830
|
+
const relevantSources: string[] = [];
|
|
1831
|
+
for (const findingId of section.findings) {
|
|
1832
|
+
const finding = project.findings.find(f => f.id === findingId);
|
|
1833
|
+
if (finding && detailedContent.has(finding.source.id)) {
|
|
1834
|
+
relevantSources.push(detailedContent.get(finding.source.id)!);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (relevantSources.length === 0) {
|
|
1839
|
+
elizaLogger.info(`[ResearchService] No detailed content available for section: ${section.heading}`);
|
|
1840
|
+
return section.content;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const combinedDetailedContent = relevantSources.join('\n\n---\n\n').substring(0, 15000);
|
|
1844
|
+
|
|
1845
|
+
const prompt = `Enhance this research section with detailed analysis from additional source material.
|
|
1846
|
+
|
|
1847
|
+
Original Section: "${section.heading}"
|
|
1848
|
+
Original Content:
|
|
1849
|
+
${section.content}
|
|
1850
|
+
|
|
1851
|
+
Detailed Source Material (first 15k chars):
|
|
1852
|
+
${combinedDetailedContent}
|
|
1853
|
+
|
|
1854
|
+
Your task:
|
|
1855
|
+
1. Rewrite the section to be more comprehensive and detailed
|
|
1856
|
+
2. Incorporate specific details, examples, and evidence from the detailed source material
|
|
1857
|
+
3. Add nuanced analysis and insights not present in the original
|
|
1858
|
+
4. Correct any potential inaccuracies using the detailed sources
|
|
1859
|
+
5. Expand the discussion while maintaining focus and relevance
|
|
1860
|
+
6. Aim for 800-1200 words for major sections, 400-600 for smaller ones
|
|
1861
|
+
|
|
1862
|
+
Maintain the academic tone and ensure all claims are well-supported by the source material.`;
|
|
1863
|
+
|
|
1864
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1865
|
+
messages: [
|
|
1866
|
+
{ role: 'system', content: 'You are a research analyst enhancing reports with detailed source analysis.' },
|
|
1867
|
+
{ role: 'user', content: prompt }
|
|
1868
|
+
],
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
const enhancedContent = typeof response === 'string' ? response : (response as any).content || section.content;
|
|
1872
|
+
elizaLogger.info(`[ResearchService] Enhanced section "${section.heading}" from ${section.content.length} to ${enhancedContent.length} characters`);
|
|
1873
|
+
|
|
1874
|
+
return enhancedContent;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private async generateDetailedSourceAnalysis(detailedContent: Map<string, string>, project: ResearchProject): Promise<string> {
|
|
1878
|
+
const sourceAnalyses: string[] = [];
|
|
1879
|
+
|
|
1880
|
+
for (const [sourceId, content] of detailedContent.entries()) {
|
|
1881
|
+
const source = project.sources.find(s => s.id === sourceId);
|
|
1882
|
+
if (!source) continue;
|
|
1883
|
+
|
|
1884
|
+
const findings = project.findings.filter(f => f.source.id === sourceId);
|
|
1885
|
+
|
|
1886
|
+
const analysisPrompt = `Conduct a detailed analysis of this research source.
|
|
1887
|
+
|
|
1888
|
+
Source: ${source.title}
|
|
1889
|
+
URL: ${source.url}
|
|
1890
|
+
Findings Extracted: ${findings.length}
|
|
1891
|
+
|
|
1892
|
+
Source Content (first 5k chars):
|
|
1893
|
+
${content.substring(0, 5000)}
|
|
1894
|
+
|
|
1895
|
+
Create a comprehensive analysis (300-400 words) that:
|
|
1896
|
+
1. Evaluates the credibility and authority of the source
|
|
1897
|
+
2. Assesses the methodology used (if applicable)
|
|
1898
|
+
3. Discusses the strength of evidence presented
|
|
1899
|
+
4. Identifies key contributions to the research question
|
|
1900
|
+
5. Notes any limitations or biases
|
|
1901
|
+
6. Compares findings with other sources in the literature
|
|
1902
|
+
|
|
1903
|
+
Be critical yet fair in your assessment.`;
|
|
1904
|
+
|
|
1905
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
1906
|
+
messages: [
|
|
1907
|
+
{ role: 'system', content: 'You are a research analyst conducting detailed source evaluations.' },
|
|
1908
|
+
{ role: 'user', content: analysisPrompt }
|
|
1909
|
+
],
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
const analysis = typeof response === 'string' ? response : (response as any).content || '';
|
|
1913
|
+
sourceAnalyses.push(`### ${source.title}\n\n${analysis}`);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const fullAnalysis = `This section provides detailed analysis of the top sources identified for this research project, offering critical evaluation of their methodology, findings, and contributions to our understanding of the research question.
|
|
1917
|
+
|
|
1918
|
+
${sourceAnalyses.join('\n\n')}
|
|
1919
|
+
|
|
1920
|
+
## Summary of Source Quality
|
|
1921
|
+
|
|
1922
|
+
Based on the detailed analysis above, the sources demonstrate varying levels of methodological rigor and relevance to the research question. The majority provide valuable insights through peer-reviewed research, while some offer practical perspectives from industry applications. This diversity of source types strengthens the overall evidence base for our conclusions.`;
|
|
1923
|
+
|
|
1924
|
+
return fullAnalysis;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
private formatCategoryHeading(category: string): string {
|
|
1928
|
+
return category.charAt(0).toUpperCase() + category.slice(1).replace(/_/g, ' ');
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
private extractCitations(findings: ResearchFinding[]): Citation[] {
|
|
1932
|
+
const citations: Citation[] = [];
|
|
1933
|
+
|
|
1934
|
+
for (const finding of findings) {
|
|
1935
|
+
for (const claim of finding.factualClaims) {
|
|
1936
|
+
citations.push({
|
|
1937
|
+
id: uuidv4(),
|
|
1938
|
+
text: claim.statement,
|
|
1939
|
+
source: finding.source,
|
|
1940
|
+
confidence: claim.confidenceScore,
|
|
1941
|
+
verificationStatus: claim.verificationStatus,
|
|
1942
|
+
context: finding.content,
|
|
1943
|
+
usageCount: 1,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
return citations;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
private extractAllCitations(project: ResearchProject): Citation[] {
|
|
1952
|
+
return this.extractCitations(project.findings);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private createBibliography(project: ResearchProject): BibliographyEntry[] {
|
|
1956
|
+
const entries: BibliographyEntry[] = [];
|
|
1957
|
+
const processedUrls = new Set<string>();
|
|
1958
|
+
|
|
1959
|
+
for (const source of project.sources) {
|
|
1960
|
+
if (processedUrls.has(source.url)) continue;
|
|
1961
|
+
processedUrls.add(source.url);
|
|
1962
|
+
|
|
1963
|
+
const authors = source.author?.join(', ') || 'Unknown';
|
|
1964
|
+
const year = source.publishDate ? new Date(source.publishDate).getFullYear() : 'n.d.';
|
|
1965
|
+
const citation = `${authors} (${year}). ${source.title}. Retrieved from ${source.url}`;
|
|
1966
|
+
|
|
1967
|
+
entries.push({
|
|
1968
|
+
id: source.id,
|
|
1969
|
+
citation,
|
|
1970
|
+
format: 'APA',
|
|
1971
|
+
source,
|
|
1972
|
+
accessCount: project.findings.filter((f) => f.source.id === source.id).length,
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
return entries.sort((a, b) => a.citation.localeCompare(b.citation));
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
async evaluateProject(projectId: string): Promise<EvaluationResults> {
|
|
1980
|
+
const project = this.projects.get(projectId);
|
|
1981
|
+
if (!project || !project.report) {
|
|
1982
|
+
throw new Error('Project not found or report not generated');
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const evaluationMetrics = await this.evaluator.evaluateProject(
|
|
1986
|
+
project,
|
|
1987
|
+
project.metadata.evaluationCriteria
|
|
1988
|
+
);
|
|
1989
|
+
|
|
1990
|
+
// Construct the full EvaluationResults object
|
|
1991
|
+
const evaluation: EvaluationResults = {
|
|
1992
|
+
projectId: project.id,
|
|
1993
|
+
raceEvaluation: {
|
|
1994
|
+
scores: evaluationMetrics.raceScore,
|
|
1995
|
+
detailedFeedback: [],
|
|
1996
|
+
},
|
|
1997
|
+
factEvaluation: {
|
|
1998
|
+
scores: evaluationMetrics.factScore,
|
|
1999
|
+
citationMap: {
|
|
2000
|
+
claims: new Map(),
|
|
2001
|
+
sources: new Map(),
|
|
2002
|
+
verification: new Map(),
|
|
2003
|
+
},
|
|
2004
|
+
verificationDetails: [],
|
|
2005
|
+
},
|
|
2006
|
+
overallScore: evaluationMetrics.raceScore.overall,
|
|
2007
|
+
recommendations: this.generateRecommendations(evaluationMetrics),
|
|
2008
|
+
timestamp: Date.now(),
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// Update project with evaluation results
|
|
2012
|
+
project.evaluationResults = evaluation;
|
|
2013
|
+
|
|
2014
|
+
// Update report metrics
|
|
2015
|
+
if (project.report) {
|
|
2016
|
+
project.report.evaluationMetrics = {
|
|
2017
|
+
raceScore: evaluationMetrics.raceScore,
|
|
2018
|
+
factScore: evaluationMetrics.factScore,
|
|
2019
|
+
timestamp: evaluation.timestamp,
|
|
2020
|
+
evaluatorVersion: '1.0',
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
return evaluation;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
private generateRecommendations(metrics: EvaluationMetrics): string[] {
|
|
2028
|
+
const recommendations: string[] = [];
|
|
2029
|
+
const race = metrics.raceScore;
|
|
2030
|
+
const fact = metrics.factScore;
|
|
2031
|
+
|
|
2032
|
+
if (race.comprehensiveness < 0.7) {
|
|
2033
|
+
recommendations.push('Expand coverage to include more aspects of the research topic');
|
|
2034
|
+
}
|
|
2035
|
+
if (race.depth < 0.7) {
|
|
2036
|
+
recommendations.push('Provide deeper analysis and more detailed explanations');
|
|
2037
|
+
}
|
|
2038
|
+
if (race.readability < 0.7) {
|
|
2039
|
+
recommendations.push('Improve clarity and structure for better readability');
|
|
2040
|
+
}
|
|
2041
|
+
if (fact.citationAccuracy < 0.7) {
|
|
2042
|
+
recommendations.push('Verify citations and ensure claims are properly supported');
|
|
2043
|
+
}
|
|
2044
|
+
if (fact.effectiveCitations < fact.totalCitations * 0.8) {
|
|
2045
|
+
recommendations.push('Remove duplicate or redundant citations');
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return recommendations;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
async exportProject(
|
|
2052
|
+
projectId: string,
|
|
2053
|
+
format: 'json' | 'markdown' | 'deepresearch'
|
|
2054
|
+
): Promise<string> {
|
|
2055
|
+
const project = this.projects.get(projectId);
|
|
2056
|
+
if (!project || !project.report) {
|
|
2057
|
+
throw new Error('Project not found or report not generated');
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
switch (format) {
|
|
2061
|
+
case 'json':
|
|
2062
|
+
return JSON.stringify(project, null, 2);
|
|
2063
|
+
|
|
2064
|
+
case 'markdown':
|
|
2065
|
+
return this.exportAsMarkdown(project);
|
|
2066
|
+
|
|
2067
|
+
case 'deepresearch':
|
|
2068
|
+
return JSON.stringify(this.exportAsDeepResearchBench(project));
|
|
2069
|
+
|
|
2070
|
+
default:
|
|
2071
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
private exportAsMarkdown(project: ResearchProject): string {
|
|
2076
|
+
if (!project.report) return '';
|
|
2077
|
+
|
|
2078
|
+
let markdown = `# ${project.report.title}\n\n`;
|
|
2079
|
+
markdown += `**Generated:** ${new Date(project.report.generatedAt).toISOString()}\n\n`;
|
|
2080
|
+
markdown += `## Abstract\n\n${project.report.abstract}\n\n`;
|
|
2081
|
+
|
|
2082
|
+
for (const section of project.report.sections) {
|
|
2083
|
+
const heading = '#'.repeat(section.level + 1);
|
|
2084
|
+
markdown += `${heading} ${section.heading}\n\n`;
|
|
2085
|
+
markdown += `${section.content}\n\n`;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
markdown += `## References\n\n`;
|
|
2089
|
+
for (const entry of project.report.bibliography) {
|
|
2090
|
+
markdown += `- ${entry.citation}\n`;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return markdown;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
private exportAsDeepResearchBench(project: ResearchProject): DeepResearchBenchResult {
|
|
2097
|
+
if (!project.report) throw new Error('Report not generated');
|
|
2098
|
+
|
|
2099
|
+
const article = project.report.sections.map((s) => `${s.heading}\n\n${s.content}`).join('\n\n');
|
|
2100
|
+
|
|
2101
|
+
return {
|
|
2102
|
+
id: project.id,
|
|
2103
|
+
prompt: project.query,
|
|
2104
|
+
article,
|
|
2105
|
+
metadata: {
|
|
2106
|
+
domain: project.metadata.domain,
|
|
2107
|
+
taskType: project.metadata.taskType,
|
|
2108
|
+
generatedAt: new Date().toISOString(),
|
|
2109
|
+
modelVersion: 'eliza-research-1.0',
|
|
2110
|
+
evaluationScores: project.report.evaluationMetrics ? {
|
|
2111
|
+
race: project.report.evaluationMetrics.raceScore,
|
|
2112
|
+
fact: project.report.evaluationMetrics.factScore,
|
|
2113
|
+
} : {
|
|
2114
|
+
race: {
|
|
2115
|
+
overall: 0,
|
|
2116
|
+
comprehensiveness: 0,
|
|
2117
|
+
depth: 0,
|
|
2118
|
+
instructionFollowing: 0,
|
|
2119
|
+
readability: 0,
|
|
2120
|
+
breakdown: [],
|
|
2121
|
+
},
|
|
2122
|
+
fact: {
|
|
2123
|
+
citationAccuracy: 0,
|
|
2124
|
+
effectiveCitations: 0,
|
|
2125
|
+
totalCitations: 0,
|
|
2126
|
+
verifiedCitations: 0,
|
|
2127
|
+
disputedCitations: 0,
|
|
2128
|
+
citationCoverage: 0,
|
|
2129
|
+
sourceCredibility: 0,
|
|
2130
|
+
breakdown: [],
|
|
2131
|
+
},
|
|
2132
|
+
},
|
|
2133
|
+
},
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
async compareProjects(projectIds: string[]): Promise<any> {
|
|
2138
|
+
const projects = projectIds
|
|
2139
|
+
.map((id) => this.projects.get(id))
|
|
2140
|
+
.filter(Boolean) as ResearchProject[];
|
|
2141
|
+
|
|
2142
|
+
if (projects.length < 2) {
|
|
2143
|
+
throw new Error('Need at least 2 projects to compare');
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Calculate similarity
|
|
2147
|
+
const similarity = await this.calculateProjectSimilarity(projects);
|
|
2148
|
+
|
|
2149
|
+
// Find differences
|
|
2150
|
+
const differences = await this.findProjectDifferences(projects);
|
|
2151
|
+
|
|
2152
|
+
// Extract unique insights
|
|
2153
|
+
const uniqueInsights = await this.extractUniqueInsights(projects);
|
|
2154
|
+
|
|
2155
|
+
// Compare quality
|
|
2156
|
+
const qualityComparison = this.compareProjectQuality(projects);
|
|
2157
|
+
|
|
2158
|
+
return {
|
|
2159
|
+
projectIds,
|
|
2160
|
+
similarity,
|
|
2161
|
+
differences,
|
|
2162
|
+
uniqueInsights,
|
|
2163
|
+
qualityComparison,
|
|
2164
|
+
recommendation: await this.generateComparisonRecommendation(
|
|
2165
|
+
projects,
|
|
2166
|
+
similarity,
|
|
2167
|
+
differences
|
|
2168
|
+
),
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
private async calculateProjectSimilarity(projects: ResearchProject[]): Promise<number> {
|
|
2173
|
+
// Compare domains, findings overlap, source overlap
|
|
2174
|
+
const domainMatch = projects.every((p) => p.metadata.domain === projects[0].metadata.domain)
|
|
2175
|
+
? 0.2
|
|
2176
|
+
: 0;
|
|
2177
|
+
|
|
2178
|
+
// Compare findings
|
|
2179
|
+
const allFindings = projects.flatMap((p) => p.findings.map((f) => f.content));
|
|
2180
|
+
const uniqueFindings = new Set(allFindings);
|
|
2181
|
+
const overlapRatio = 1 - uniqueFindings.size / allFindings.length;
|
|
2182
|
+
|
|
2183
|
+
return domainMatch + overlapRatio * 0.8;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
private async findProjectDifferences(projects: ResearchProject[]): Promise<string[]> {
|
|
2187
|
+
const differences: string[] = [];
|
|
2188
|
+
|
|
2189
|
+
// Domain differences
|
|
2190
|
+
const domains = new Set(projects.map((p) => p.metadata.domain));
|
|
2191
|
+
if (domains.size > 1) {
|
|
2192
|
+
differences.push(
|
|
2193
|
+
`Projects span ${domains.size} different domains: ${Array.from(domains).join(', ')}`
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// Approach differences
|
|
2198
|
+
const taskTypes = new Set(projects.map((p) => p.metadata.taskType));
|
|
2199
|
+
if (taskTypes.size > 1) {
|
|
2200
|
+
differences.push(`Different research approaches used: ${Array.from(taskTypes).join(', ')}`);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// Source type differences
|
|
2204
|
+
const sourceTypes = projects.map((p) => {
|
|
2205
|
+
const types = new Set(p.sources.map((s) => s.type));
|
|
2206
|
+
return Array.from(types);
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
return differences;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
private async extractUniqueInsights(
|
|
2213
|
+
projects: ResearchProject[]
|
|
2214
|
+
): Promise<Record<string, string[]>> {
|
|
2215
|
+
const insights: Record<string, string[]> = {};
|
|
2216
|
+
|
|
2217
|
+
for (const project of projects) {
|
|
2218
|
+
const projectInsights = project.findings
|
|
2219
|
+
.filter((f) => f.relevance > 0.8 && f.confidence > 0.8)
|
|
2220
|
+
.slice(0, 3)
|
|
2221
|
+
.map((f) => f.content.substring(0, 200) + '...');
|
|
2222
|
+
|
|
2223
|
+
insights[project.id] = projectInsights;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return insights;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
private compareProjectQuality(projects: ResearchProject[]): any[] {
|
|
2230
|
+
return projects.map((p) => ({
|
|
2231
|
+
projectId: p.id,
|
|
2232
|
+
sourceCount: p.sources.length,
|
|
2233
|
+
findingCount: p.findings.length,
|
|
2234
|
+
avgSourceReliability: p.sources.reduce((sum, s) => sum + s.reliability, 0) / p.sources.length,
|
|
2235
|
+
evaluationScore: p.evaluationResults?.overallScore || 0,
|
|
2236
|
+
}));
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
private async generateComparisonRecommendation(
|
|
2240
|
+
projects: ResearchProject[],
|
|
2241
|
+
similarity: number,
|
|
2242
|
+
differences: string[]
|
|
2243
|
+
): Promise<string> {
|
|
2244
|
+
if (similarity > 0.8) {
|
|
2245
|
+
return 'These projects are highly similar and could be merged or one could be used as validation for the other.';
|
|
2246
|
+
} else if (similarity > 0.5) {
|
|
2247
|
+
return 'These projects have moderate overlap but explore different aspects. Consider synthesizing insights from both.';
|
|
2248
|
+
} else {
|
|
2249
|
+
return 'These projects are quite different and provide complementary perspectives on related topics.';
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async addRefinedQueries(projectId: string, queries: string[]): Promise<void> {
|
|
2254
|
+
const project = this.projects.get(projectId);
|
|
2255
|
+
if (!project) throw new Error('Project not found');
|
|
2256
|
+
|
|
2257
|
+
// Add as new sub-queries
|
|
2258
|
+
const newSubQueries = queries.map((query, index) => ({
|
|
2259
|
+
id: `refined_${Date.now()}_${index}`,
|
|
2260
|
+
query,
|
|
2261
|
+
purpose: 'Refined based on initial findings',
|
|
2262
|
+
priority: 2,
|
|
2263
|
+
dependsOn: [],
|
|
2264
|
+
searchProviders: ['web'],
|
|
2265
|
+
expectedResultType:
|
|
2266
|
+
project.metadata.taskType === TaskType.ANALYTICAL
|
|
2267
|
+
? ('theoretical' as any)
|
|
2268
|
+
: ('factual' as any),
|
|
2269
|
+
completed: false,
|
|
2270
|
+
}));
|
|
2271
|
+
|
|
2272
|
+
project.metadata.queryPlan.subQueries.push(...newSubQueries);
|
|
2273
|
+
|
|
2274
|
+
// Continue research if active
|
|
2275
|
+
if (project.status === ResearchStatus.ACTIVE) {
|
|
2276
|
+
const controller = this.activeResearch.get(projectId);
|
|
2277
|
+
if (controller) {
|
|
2278
|
+
// Execute new queries
|
|
2279
|
+
const config = { ...DEFAULT_CONFIG, domain: project.metadata.domain };
|
|
2280
|
+
await this.executeSearchWithRelevance(project, config, controller.signal, {});
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
async pauseResearch(projectId: string): Promise<void> {
|
|
2286
|
+
const controller = this.activeResearch.get(projectId);
|
|
2287
|
+
if (controller) {
|
|
2288
|
+
controller.abort();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
const project = this.projects.get(projectId);
|
|
2292
|
+
if (project) {
|
|
2293
|
+
project.status = ResearchStatus.PAUSED;
|
|
2294
|
+
project.updatedAt = Date.now();
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
async resumeResearch(projectId: string): Promise<void> {
|
|
2299
|
+
const project = this.projects.get(projectId);
|
|
2300
|
+
if (!project || project.status !== ResearchStatus.PAUSED) {
|
|
2301
|
+
throw new Error('Project not found or not paused');
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// Restart research from current phase
|
|
2305
|
+
const config = { ...DEFAULT_CONFIG, domain: project.metadata.domain };
|
|
2306
|
+
this.startResearch(projectId, config).catch((error) => {
|
|
2307
|
+
elizaLogger.error(`Failed to resume research ${projectId}:`, error);
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
private emitProgress(project: ResearchProject, message: string): void {
|
|
2312
|
+
const progress: ResearchProgress = {
|
|
2313
|
+
projectId: project.id,
|
|
2314
|
+
phase: project.phase,
|
|
2315
|
+
message,
|
|
2316
|
+
progress: this.calculateProgress(project),
|
|
2317
|
+
timestamp: Date.now(),
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// In a real implementation, this would emit to event system
|
|
2321
|
+
elizaLogger.info(`Research progress: ${JSON.stringify(progress)}`);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
private calculateProgress(project: ResearchProject): number {
|
|
2325
|
+
const phases = Object.values(ResearchPhase);
|
|
2326
|
+
const currentIndex = phases.indexOf(project.phase);
|
|
2327
|
+
return (currentIndex / (phases.length - 1)) * 100;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
private async analyzeFindingsWithRelevance(
|
|
2331
|
+
project: ResearchProject,
|
|
2332
|
+
config: ResearchConfig,
|
|
2333
|
+
queryAnalysis: any
|
|
2334
|
+
): Promise<void> {
|
|
2335
|
+
elizaLogger.info(`[ResearchService] Analyzing ${project.sources.length} sources with relevance verification`);
|
|
2336
|
+
|
|
2337
|
+
for (const source of project.sources) {
|
|
2338
|
+
// Use fullContent if available, otherwise fall back to snippet
|
|
2339
|
+
const contentToAnalyze = source.fullContent || source.snippet || source.title;
|
|
2340
|
+
|
|
2341
|
+
if (!contentToAnalyze) {
|
|
2342
|
+
elizaLogger.warn(`[ResearchService] No content available for source: ${source.url}`);
|
|
2343
|
+
await this.researchLogger.logContentExtraction(
|
|
2344
|
+
project.id,
|
|
2345
|
+
source.url,
|
|
2346
|
+
source.title,
|
|
2347
|
+
'none',
|
|
2348
|
+
false,
|
|
2349
|
+
0,
|
|
2350
|
+
'No content available'
|
|
2351
|
+
);
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// Log content extraction success
|
|
2356
|
+
await this.researchLogger.logContentExtraction(
|
|
2357
|
+
project.id,
|
|
2358
|
+
source.url,
|
|
2359
|
+
source.title,
|
|
2360
|
+
source.fullContent ? 'content-extractor' : 'snippet',
|
|
2361
|
+
true,
|
|
2362
|
+
contentToAnalyze.length
|
|
2363
|
+
);
|
|
2364
|
+
|
|
2365
|
+
// Extract findings with relevance analysis
|
|
2366
|
+
const findings = await this.extractFindingsWithRelevance(
|
|
2367
|
+
source,
|
|
2368
|
+
project.query,
|
|
2369
|
+
contentToAnalyze,
|
|
2370
|
+
queryAnalysis
|
|
2371
|
+
);
|
|
2372
|
+
|
|
2373
|
+
// Score findings for relevance
|
|
2374
|
+
const findingRelevanceScores = new Map<string, any>();
|
|
2375
|
+
for (const finding of findings) {
|
|
2376
|
+
const relevanceScore = await this.relevanceAnalyzer.scoreFindingRelevance(
|
|
2377
|
+
{
|
|
2378
|
+
id: uuidv4(),
|
|
2379
|
+
content: finding.content,
|
|
2380
|
+
source,
|
|
2381
|
+
relevance: finding.relevance,
|
|
2382
|
+
confidence: finding.confidence,
|
|
2383
|
+
timestamp: Date.now(),
|
|
2384
|
+
category: finding.category,
|
|
2385
|
+
citations: [],
|
|
2386
|
+
factualClaims: [],
|
|
2387
|
+
relatedFindings: [],
|
|
2388
|
+
verificationStatus: VerificationStatus.PENDING,
|
|
2389
|
+
extractionMethod: 'llm-extraction'
|
|
2390
|
+
},
|
|
2391
|
+
queryAnalysis,
|
|
2392
|
+
project.query
|
|
2393
|
+
);
|
|
2394
|
+
findingRelevanceScores.set(finding.content, relevanceScore);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Log finding extraction
|
|
2398
|
+
await this.researchLogger.logFindingExtraction(
|
|
2399
|
+
project.id,
|
|
2400
|
+
source.url,
|
|
2401
|
+
contentToAnalyze.length,
|
|
2402
|
+
findings,
|
|
2403
|
+
findingRelevanceScores
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
// Only keep findings with good relevance scores (>= 0.6)
|
|
2407
|
+
const relevantFindings = findings.filter(finding => {
|
|
2408
|
+
const relevanceScore = findingRelevanceScores.get(finding.content);
|
|
2409
|
+
return (relevanceScore?.score || finding.relevance) >= 0.6;
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
elizaLogger.info(`[ResearchService] Kept ${relevantFindings.length}/${findings.length} relevant findings from ${source.title}`);
|
|
2413
|
+
|
|
2414
|
+
// Extract factual claims for relevant findings
|
|
2415
|
+
const claims = await this.extractFactualClaims(source, contentToAnalyze);
|
|
2416
|
+
|
|
2417
|
+
// Create research findings with enhanced relevance information
|
|
2418
|
+
for (const finding of relevantFindings) {
|
|
2419
|
+
const relevanceScore = findingRelevanceScores.get(finding.content);
|
|
2420
|
+
|
|
2421
|
+
const researchFinding: ResearchFinding = {
|
|
2422
|
+
id: uuidv4(),
|
|
2423
|
+
content: finding.content,
|
|
2424
|
+
source,
|
|
2425
|
+
relevance: relevanceScore?.score || finding.relevance,
|
|
2426
|
+
confidence: finding.confidence,
|
|
2427
|
+
timestamp: Date.now(),
|
|
2428
|
+
category: finding.category,
|
|
2429
|
+
citations: [],
|
|
2430
|
+
factualClaims: claims.filter((c) =>
|
|
2431
|
+
finding.content.toLowerCase().includes(c.statement.substring(0, 30).toLowerCase())
|
|
2432
|
+
),
|
|
2433
|
+
relatedFindings: [],
|
|
2434
|
+
verificationStatus: VerificationStatus.PENDING,
|
|
2435
|
+
extractionMethod: source.fullContent ? 'llm-extraction-with-relevance' : 'snippet-extraction-with-relevance',
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
project.findings.push(researchFinding);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
elizaLogger.info(`[ResearchService] Extracted ${project.findings.length} relevant findings`);
|
|
2443
|
+
|
|
2444
|
+
// Update quality score
|
|
2445
|
+
const lastIteration =
|
|
2446
|
+
project.metadata.iterationHistory[project.metadata.iterationHistory.length - 1];
|
|
2447
|
+
if (lastIteration) {
|
|
2448
|
+
lastIteration.findingsExtracted = project.findings.length;
|
|
2449
|
+
lastIteration.qualityScore = this.calculateQualityScore(project);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
private async extractFindingsWithRelevance(
|
|
2454
|
+
source: ResearchSource,
|
|
2455
|
+
query: string,
|
|
2456
|
+
content: string,
|
|
2457
|
+
queryAnalysis: any
|
|
2458
|
+
): Promise<Array<{ content: string; relevance: number; confidence: number; category: string }>> {
|
|
2459
|
+
const prompt = `Extract key findings from this source that DIRECTLY address the research query.
|
|
2460
|
+
|
|
2461
|
+
Research Query: "${query}"
|
|
2462
|
+
Query Intent: ${queryAnalysis.queryIntent}
|
|
2463
|
+
Key Topics: ${queryAnalysis.keyTopics.join(', ')}
|
|
2464
|
+
Required Elements: ${queryAnalysis.requiredElements.join(', ')}
|
|
2465
|
+
|
|
2466
|
+
Source: ${source.title}
|
|
2467
|
+
URL: ${source.url}
|
|
2468
|
+
Content: ${content.substring(0, 3000)}...
|
|
2469
|
+
|
|
2470
|
+
CRITICAL INSTRUCTIONS:
|
|
2471
|
+
1. Only extract findings that DIRECTLY relate to the research query
|
|
2472
|
+
2. Each finding must address at least one key topic or required element
|
|
2473
|
+
3. Rate relevance strictly - only high relevance should get scores > 0.7
|
|
2474
|
+
4. Focus on actionable insights that help answer the original question
|
|
2475
|
+
5. Avoid generic or tangential information
|
|
2476
|
+
|
|
2477
|
+
For each finding:
|
|
2478
|
+
1. Extract the specific finding/insight that addresses the query
|
|
2479
|
+
2. Rate relevance to query (0-1) - be strict, only highly relevant content should score > 0.7
|
|
2480
|
+
3. Rate confidence in the finding (0-1)
|
|
2481
|
+
4. Categorize appropriately
|
|
2482
|
+
|
|
2483
|
+
Format as JSON array:
|
|
2484
|
+
[{
|
|
2485
|
+
"content": "specific finding that addresses the query",
|
|
2486
|
+
"relevance": 0.9,
|
|
2487
|
+
"confidence": 0.8,
|
|
2488
|
+
"category": "fact"
|
|
2489
|
+
}]`;
|
|
2490
|
+
|
|
2491
|
+
elizaLogger.debug(`[ResearchService] Calling LLM for finding extraction:`, {
|
|
2492
|
+
sourceTitle: source.title,
|
|
2493
|
+
contentLength: content.length,
|
|
2494
|
+
promptLength: prompt.length
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
|
|
2498
|
+
messages: [
|
|
2499
|
+
{
|
|
2500
|
+
role: 'system',
|
|
2501
|
+
content: 'You are a research analyst extracting only the most relevant findings that directly address the research query. Be strict about relevance.',
|
|
2502
|
+
},
|
|
2503
|
+
{ role: 'user', content: prompt },
|
|
2504
|
+
],
|
|
2505
|
+
temperature: 0.3, // Lower temperature for more focused extraction
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
elizaLogger.debug(`[ResearchService] LLM response received:`, {
|
|
2509
|
+
responseType: typeof response,
|
|
2510
|
+
responseLength: response ? String(response).length : 0,
|
|
2511
|
+
responsePreview: response ? String(response).substring(0, 200) : 'null'
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
try {
|
|
2515
|
+
const responseContent = typeof response === 'string' ? response : (response as any).content || '';
|
|
2516
|
+
|
|
2517
|
+
// Try to extract JSON from the response
|
|
2518
|
+
const jsonMatch = responseContent.match(/\[[\s\S]*\]/);
|
|
2519
|
+
if (jsonMatch) {
|
|
2520
|
+
const findings = JSON.parse(jsonMatch[0]);
|
|
2521
|
+
// Validate findings structure and filter for quality
|
|
2522
|
+
if (Array.isArray(findings) && findings.length > 0) {
|
|
2523
|
+
// Additional filtering for relevance in the extraction phase
|
|
2524
|
+
const relevantFindings = findings.filter(f =>
|
|
2525
|
+
f.content &&
|
|
2526
|
+
f.content.length > 20 && // Minimum content length
|
|
2527
|
+
f.relevance >= 0.5 && // Minimum relevance threshold
|
|
2528
|
+
f.category &&
|
|
2529
|
+
typeof f.confidence === 'number'
|
|
2530
|
+
);
|
|
2531
|
+
|
|
2532
|
+
elizaLogger.debug(`[ResearchService] Extracted ${relevantFindings.length}/${findings.length} quality findings from ${source.title}`);
|
|
2533
|
+
return relevantFindings;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// If no valid JSON found, throw error instead of creating fake findings
|
|
2538
|
+
throw new Error(`Failed to extract valid findings from LLM response. Response: ${responseContent.substring(0, 200)}`);
|
|
2539
|
+
} catch (e) {
|
|
2540
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2541
|
+
const errorStack = e instanceof Error ? e.stack : undefined;
|
|
2542
|
+
|
|
2543
|
+
elizaLogger.error('[ResearchService] Failed to extract relevant findings from source:', {
|
|
2544
|
+
sourceUrl: source.url,
|
|
2545
|
+
sourceTitle: source.title,
|
|
2546
|
+
error: errorMessage,
|
|
2547
|
+
contentLength: content.length,
|
|
2548
|
+
contentPreview: content.substring(0, 200),
|
|
2549
|
+
stack: errorStack,
|
|
2550
|
+
fullError: e
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
console.error(`[DETAILED ERROR] Finding extraction failed for ${source.title}:`, {
|
|
2554
|
+
error: errorMessage,
|
|
2555
|
+
stack: errorStack,
|
|
2556
|
+
contentLength: content.length
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
// Return empty array instead of fake findings - let the caller handle the failure
|
|
2560
|
+
return [];
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// Service lifecycle methods
|
|
2565
|
+
async stop(): Promise<void> {
|
|
2566
|
+
// Abort all active research
|
|
2567
|
+
for (const [projectId, controller] of this.activeResearch) {
|
|
2568
|
+
controller.abort();
|
|
2569
|
+
const project = this.projects.get(projectId);
|
|
2570
|
+
if (project) {
|
|
2571
|
+
project.status = ResearchStatus.PAUSED;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
this.activeResearch.clear();
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
async getProject(projectId: string): Promise<ResearchProject | undefined> {
|
|
2578
|
+
return this.projects.get(projectId);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
async getAllProjects(): Promise<ResearchProject[]> {
|
|
2582
|
+
return Array.from(this.projects.values());
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
async getActiveProjects(): Promise<ResearchProject[]> {
|
|
2586
|
+
return Array.from(this.projects.values()).filter((p) => p.status === ResearchStatus.ACTIVE);
|
|
2587
|
+
}
|
|
2588
|
+
}
|