@elizaos/plugin-research 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +400 -0
  2. package/dist/index.cjs +9366 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +9284 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +80 -0
  7. package/src/__tests__/action-chaining.test.ts +532 -0
  8. package/src/__tests__/actions.test.ts +118 -0
  9. package/src/__tests__/cache-rate-limiter.test.ts +303 -0
  10. package/src/__tests__/content-extractors.test.ts +26 -0
  11. package/src/__tests__/deepresearch-bench-integration.test.ts +520 -0
  12. package/src/__tests__/deepresearch-bench-simplified.e2e.test.ts +290 -0
  13. package/src/__tests__/deepresearch-bench.e2e.test.ts +376 -0
  14. package/src/__tests__/e2e.test.ts +1870 -0
  15. package/src/__tests__/multi-benchmark-runner.ts +427 -0
  16. package/src/__tests__/providers.test.ts +156 -0
  17. package/src/__tests__/real-world.e2e.test.ts +788 -0
  18. package/src/__tests__/research-scenarios.test.ts +755 -0
  19. package/src/__tests__/research.e2e.test.ts +704 -0
  20. package/src/__tests__/research.test.ts +174 -0
  21. package/src/__tests__/search-providers.test.ts +174 -0
  22. package/src/__tests__/single-benchmark-runner.ts +735 -0
  23. package/src/__tests__/test-search-providers.ts +171 -0
  24. package/src/__tests__/verify-apis.test.ts +82 -0
  25. package/src/actions.ts +1677 -0
  26. package/src/benchmark/deepresearch-benchmark.ts +369 -0
  27. package/src/evaluation/research-evaluator.ts +444 -0
  28. package/src/examples/api-integration.md +498 -0
  29. package/src/examples/browserbase-integration.md +132 -0
  30. package/src/examples/debug-research-query.ts +162 -0
  31. package/src/examples/defi-code-scenarios.md +536 -0
  32. package/src/examples/defi-implementation-guide.md +454 -0
  33. package/src/examples/eliza-research-example.ts +142 -0
  34. package/src/examples/fix-renewable-energy-research.ts +209 -0
  35. package/src/examples/research-scenarios.md +408 -0
  36. package/src/examples/run-complete-renewable-research.ts +303 -0
  37. package/src/examples/run-deep-research.ts +352 -0
  38. package/src/examples/run-logged-research.ts +304 -0
  39. package/src/examples/run-real-research.ts +151 -0
  40. package/src/examples/save-research-output.ts +133 -0
  41. package/src/examples/test-file-logging.ts +199 -0
  42. package/src/examples/test-real-research.ts +67 -0
  43. package/src/examples/test-renewable-energy-research.ts +229 -0
  44. package/src/index.ts +28 -0
  45. package/src/integrations/cache.ts +128 -0
  46. package/src/integrations/content-extractors/firecrawl.ts +314 -0
  47. package/src/integrations/content-extractors/pdf-extractor.ts +350 -0
  48. package/src/integrations/content-extractors/playwright.ts +420 -0
  49. package/src/integrations/factory.ts +419 -0
  50. package/src/integrations/index.ts +18 -0
  51. package/src/integrations/rate-limiter.ts +181 -0
  52. package/src/integrations/search-providers/academic.ts +290 -0
  53. package/src/integrations/search-providers/exa.ts +205 -0
  54. package/src/integrations/search-providers/npm.ts +330 -0
  55. package/src/integrations/search-providers/pypi.ts +211 -0
  56. package/src/integrations/search-providers/serpapi.ts +277 -0
  57. package/src/integrations/search-providers/serper.ts +358 -0
  58. package/src/integrations/search-providers/stagehand-google.ts +87 -0
  59. package/src/integrations/search-providers/tavily.ts +187 -0
  60. package/src/processing/relevance-analyzer.ts +353 -0
  61. package/src/processing/research-logger.ts +450 -0
  62. package/src/processing/result-processor.ts +372 -0
  63. package/src/prompts/research-prompts.ts +419 -0
  64. package/src/providers/cacheProvider.ts +164 -0
  65. package/src/providers.ts +173 -0
  66. package/src/service.ts +2588 -0
  67. package/src/services/swe-bench.ts +286 -0
  68. package/src/strategies/research-strategies.ts +790 -0
  69. package/src/types/pdf-parse.d.ts +34 -0
  70. package/src/types.ts +551 -0
  71. package/src/verification/claim-verifier.ts +443 -0
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
+ }