export default class ResearchAnalyzer extends AgenticSystem {
@field thinking: stream<string>('');
async analyze(findings: Finding[], questions: ResearchQuestion[]): Promise<AnalysisResult> {
this.thinking.append(`Analyzing ${findings.length} findings...\n`);
// Score relevance
const scored = await this.scoreRelevance(findings, questions);
this.thinking.append(`Scored relevance for all findings\n`);
// Filter low-quality findings
const filtered = scored.filter(f => f.relevance > 0.5 && f.source.reliability > 0.5);
this.thinking.append(`${filtered.length} findings passed quality filter\n`);
// Extract key facts
const facts = await this.extractFacts(filtered);
this.thinking.append(`Extracted ${facts.length} key facts\n`);
// Identify contradictions
const contradictions = await this.findContradictions(facts);
if (contradictions.length > 0) {
this.thinking.append(`Found ${contradictions.length} contradictions to resolve\n`);
}
// Identify gaps
const gaps = await this.identifyGaps(questions, facts);
this.thinking.append(`Identified ${gaps.length} remaining gaps\n`);
return {
findings: filtered,
facts,
contradictions,
gaps,
confidence: this.calculateOverallConfidence(filtered, facts, gaps)
};
}
private async scoreRelevance(findings: Finding[], questions: ResearchQuestion[]): Promise<Finding[]> {
const questionText = questions.map(q => q.question).join('\n');
return Promise.all(findings.map(async finding => {
const completion = await llm.complete([
{ role: 'system', content: 'Score how relevant this finding is to the research questions. Return a number 0-1.' },
{ role: 'user', content: `Questions:\n${questionText}\n\nFinding: ${finding.content}` }
]);
const raw = (completion.text ?? completion.choices?.[0]?.message?.content ?? '').trim();
const numeric = Number.parseFloat(raw.match(/[0-9.]+/)?.[0] ?? '0.5');
const score = Math.min(1, Math.max(0, numeric || 0.5));
return {
...finding,
relevance: score
};
}));
}
private async extractFacts(findings: Finding[]): Promise<Fact[]> {
const completion = await llm.complete([
{ role: 'system', content: `Extract discrete, verifiable facts from these findings.
Each fact should be:
- A single, specific claim
- Attributable to a source
- Verifiable or falsifiable
Return as JSON array: [{"fact": "...", "sourceIndex": 0}]` },
{ role: 'user', content: findings.map((f, i) => `[${i}] ${f.content}`).join('\n\n') }
]);
const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
const extracted = JSON.parse(text) as { fact: string; sourceIndex: number }[];
// In production you would validate that sourceIndex is in range
return extracted.map(e => ({
content: e.fact,
source: findings[e.sourceIndex]?.source,
confidence: findings[e.sourceIndex]?.source.reliability || 0.5
}));
}
private async findContradictions(facts: Fact[]): Promise<Contradiction[]> {
if (facts.length < 2) return [];
const completion = await llm.complete([
{ role: 'system', content: `Identify any contradictions between these facts.
Return as JSON array: [{"fact1Index": 0, "fact2Index": 1, "description": "..."}]
Return empty array [] if no contradictions.` },
{ role: 'user', content: facts.map((f, i) => `[${i}] ${f.content}`).join('\n') }
]);
const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
const contradictions = JSON.parse(text) as { fact1Index: number; fact2Index: number; description: string }[];
return contradictions.map(c => ({
fact1: facts[c.fact1Index],
fact2: facts[c.fact2Index],
description: c.description
}));
}
private async identifyGaps(questions: ResearchQuestion[], facts: Fact[]): Promise<string[]> {
const completion = await llm.complete([
{ role: 'system', content: 'Identify which research questions are not adequately answered by the available facts. Return one gap per line, no bullets or numbering.' },
{ role: 'user', content: `Questions:\n${questions.map(q => `- ${q.question}`).join('\n')}\n\nFacts:\n${facts.map(f => `- ${f.content}`).join('\n')}\n\nWhat questions remain unanswered or inadequately answered?` }
]);
const response = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
return response
.split('\n')
.map(line => line.trim())
.filter(Boolean);
}
private calculateOverallConfidence(findings: Finding[], facts: Fact[], gaps: string[]): number {
if (facts.length === 0) return 0;
const avgSourceReliability = findings.reduce((sum, f) => sum + f.source.reliability, 0) / findings.length;
const avgFactConfidence = facts.reduce((sum, f) => sum + f.confidence, 0) / facts.length;
const gapPenalty = Math.min(0.3, gaps.length * 0.1);
return Math.max(0, (avgSourceReliability + avgFactConfidence) / 2 - gapPenalty);
}
}
interface Fact {
content: string;
source: Source;
confidence: number;
}
interface Contradiction {
fact1: Fact;
fact2: Fact;
description: string;
}
interface AnalysisResult {
findings: Finding[];
facts: Fact[];
contradictions: Contradiction[];
gaps: string[];
confidence: number;
}