import { z } from 'zod';
import { SonarBenchmarkService, BenchmarkData, BenchmarkRequest } from './sonar-benchmark-service.js';
import { createLogger } from '../utils/logger.js';
import { getBenchmarks, IndustryType } from '../core/benchmarks/index.js';
import { rateLimiters } from '../utils/rate-limiter.js';
// FMP (Financial Modeling Prep) API types
interface FMPFinancialData {
symbol: string;
revenue: number;
operatingExpenses: number;
netIncome: number;
employeeCount?: number;
industry?: string;
}
// Aggregated benchmark result
export const AggregatedBenchmarkSchema = z.object({
metric: z.string(),
value: z.number(),
unit: z.string(),
confidence: z.number().min(0).max(1),
sources: z.array(z.object({
name: z.string(),
value: z.number(),
date: z.string(),
weight: z.number()
})),
consensus: z.enum(['strong', 'moderate', 'weak']),
recommendedValue: z.number(),
range: z.object({
min: z.number(),
max: z.number(),
p25: z.number(),
p75: z.number()
})
});
export type AggregatedBenchmark = z.infer<typeof AggregatedBenchmarkSchema>;
interface BenchmarkSource {
name: string;
fetchData: (request: BenchmarkRequest) => Promise<BenchmarkData[]>;
weight: number; // Weight for consensus calculation
priority: number; // Priority for fallback (1 = highest)
}
export class BenchmarkAggregator {
private logger = createLogger({ service: 'BenchmarkAggregator' });
private sonarService?: SonarBenchmarkService;
private sources: BenchmarkSource[] = [];
private fmpApiKey?: string;
constructor(config?: {
sonarApiKey?: string;
fmpApiKey?: string;
enableSonar?: boolean;
enableFMP?: boolean;
}) {
// Initialize Sonar service if API key provided
if (config?.sonarApiKey && config?.enableSonar !== false) {
this.sonarService = new SonarBenchmarkService({
apiKey: config.sonarApiKey
});
this.sources.push({
name: 'Perplexity Sonar',
fetchData: (req) => this.sonarService!.fetchBenchmarks(req),
weight: 0.4, // High weight for real-time data
priority: 1
});
}
// Store FMP API key if provided
if (config?.fmpApiKey && config?.enableFMP !== false) {
this.fmpApiKey = config.fmpApiKey;
this.sources.push({
name: 'Financial Modeling Prep',
fetchData: (req) => this.fetchFMPBenchmarks(req),
weight: 0.3,
priority: 2
});
}
// Always include static benchmarks as fallback
this.sources.push({
name: 'Static Benchmarks',
fetchData: (req) => this.fetchStaticBenchmarks(req),
weight: 0.3,
priority: 3
});
// Sort sources by priority
this.sources.sort((a, b) => a.priority - b.priority);
}
/**
* Aggregate benchmarks from all available sources
*/
async aggregateBenchmarks(request: BenchmarkRequest): Promise<AggregatedBenchmark[]> {
this.logger.info('Starting benchmark aggregation', {
industry: request.industry,
sources: this.sources.map(s => s.name)
});
// Fetch data from all sources in parallel
const sourceResults = await Promise.allSettled(
this.sources.map(async source => {
const startTime = Date.now();
try {
const data = await source.fetchData(request);
const duration = Date.now() - startTime;
this.logger.debug('Source fetch completed', {
source: source.name,
count: data.length,
duration
});
return { source, data };
} catch (error) {
this.logger.error(`Source fetch failed: ${source.name}`, error as Error);
throw error;
}
})
);
// Collect successful results
const successfulResults = sourceResults
.filter((result): result is PromiseFulfilledResult<{ source: BenchmarkSource; data: BenchmarkData[] }> =>
result.status === 'fulfilled'
)
.map(result => result.value);
if (successfulResults.length === 0) {
throw new Error('All benchmark sources failed');
}
// Group benchmarks by metric
const metricGroups = this.groupByMetric(successfulResults);
// Aggregate each metric group
const aggregatedBenchmarks: AggregatedBenchmark[] = [];
for (const [metric, sources] of metricGroups.entries()) {
const aggregated = this.aggregateMetric(metric, sources);
aggregatedBenchmarks.push(aggregated);
}
this.logger.info('Benchmark aggregation completed', {
metricsCount: aggregatedBenchmarks.length,
averageConfidence: this.calculateAverageConfidence(aggregatedBenchmarks)
});
return aggregatedBenchmarks;
}
/**
* Fetch benchmarks from Financial Modeling Prep API
*/
private async fetchFMPBenchmarks(request: BenchmarkRequest): Promise<BenchmarkData[]> {
if (!this.fmpApiKey) {
throw new Error('FMP API key not configured');
}
// Map our industries to FMP sectors
const sectorMap: Record<string, string> = {
financial_services: 'Financial Services',
healthcare: 'Healthcare',
retail: 'Consumer Cyclical',
manufacturing: 'Industrials',
technology: 'Technology'
};
const sector = sectorMap[request.industry] || 'Technology';
try {
// Use rate limiter for FMP API calls
const data = await rateLimiters.fmp.executeWithRateLimit(
async () => {
// Fetch industry averages from FMP
const response = await fetch(
`https://financialmodelingprep.com/api/v3/sector-performance?apikey=${this.fmpApiKey}`
);
if (!response.ok) {
throw new Error(`FMP API error: ${response.status}`);
}
return response.json();
},
{ priority: 'normal', timeout: 15000 }
);
// Convert FMP data to our benchmark format
const benchmarks: BenchmarkData[] = [];
// Find sector data
const sectorData = (data as any[]).find((s: any) => s.sector === sector);
if (sectorData) {
// Revenue growth as a proxy for ROI potential
if (sectorData.revenueGrowth) {
benchmarks.push({
industry: request.industry,
metric: 'revenue growth',
value: Math.abs(sectorData.revenueGrowth * 100),
unit: '%',
source: 'Financial Modeling Prep',
date: new Date().toISOString().split('T')[0],
confidence: 0.8
});
}
// Operating margin as efficiency metric
if (sectorData.operatingMargin) {
benchmarks.push({
industry: request.industry,
metric: 'operating efficiency',
value: Math.abs(sectorData.operatingMargin * 100),
unit: '%',
source: 'Financial Modeling Prep',
date: new Date().toISOString().split('T')[0],
confidence: 0.8
});
}
}
return benchmarks;
} catch (error) {
this.logger.error('FMP API fetch failed', error as Error);
return [];
}
}
/**
* Fetch static benchmarks from local data
*/
private async fetchStaticBenchmarks(request: BenchmarkRequest): Promise<BenchmarkData[]> {
const industryBenchmarks = await getBenchmarks(request.industry as IndustryType);
const benchmarks: BenchmarkData[] = [];
// Convert static benchmarks to our format
if (industryBenchmarks) {
// Automation savings
benchmarks.push({
industry: request.industry,
metric: 'automation rate',
value: industryBenchmarks.automationSavings.percentage * 100,
unit: '%',
source: 'Static Industry Data',
date: new Date().toISOString().split('T')[0],
confidence: industryBenchmarks.automationSavings.confidence
});
// Error reduction
benchmarks.push({
industry: request.industry,
metric: 'error reduction',
value: industryBenchmarks.errorReduction.percentage * 100,
unit: '%',
source: 'Static Industry Data',
date: new Date().toISOString().split('T')[0],
confidence: industryBenchmarks.errorReduction.confidence
});
// Implementation timeline
benchmarks.push({
industry: request.industry,
metric: 'implementation time',
value: industryBenchmarks.implementationTimeline.typicalMonths,
unit: 'months',
source: 'Static Industry Data',
date: new Date().toISOString().split('T')[0],
confidence: industryBenchmarks.implementationTimeline.confidence
});
// Note: Project-type specific benchmarks could be added here
// if the getBenchmarks function is enhanced to return projectTypes data
}
return benchmarks;
}
/**
* Group benchmarks by metric name
*/
private groupByMetric(
results: Array<{ source: BenchmarkSource; data: BenchmarkData[] }>
): Map<string, Array<{ source: BenchmarkSource; benchmark: BenchmarkData }>> {
const groups = new Map<string, Array<{ source: BenchmarkSource; benchmark: BenchmarkData }>>();
for (const { source, data } of results) {
for (const benchmark of data) {
const normalizedMetric = this.normalizeMetricName(benchmark.metric);
if (!groups.has(normalizedMetric)) {
groups.set(normalizedMetric, []);
}
groups.get(normalizedMetric)!.push({ source, benchmark });
}
}
return groups;
}
/**
* Normalize metric names for grouping
*/
private normalizeMetricName(metric: string): string {
return metric
.toLowerCase()
.replace(/[_-]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Aggregate multiple sources for a single metric
*/
private aggregateMetric(
metric: string,
sources: Array<{ source: BenchmarkSource; benchmark: BenchmarkData }>
): AggregatedBenchmark {
// Calculate weighted average
let weightedSum = 0;
let totalWeight = 0;
const sourceDetails: any[] = [];
const values: number[] = [];
for (const { source, benchmark } of sources) {
const weight = source.weight * benchmark.confidence;
weightedSum += benchmark.value * weight;
totalWeight += weight;
values.push(benchmark.value);
sourceDetails.push({
name: source.name,
value: benchmark.value,
date: benchmark.date,
weight
});
}
const recommendedValue = totalWeight > 0 ? weightedSum / totalWeight : values[0];
// Calculate range statistics
values.sort((a, b) => a - b);
const range = {
min: Math.min(...values),
max: Math.max(...values),
p25: this.percentile(values, 0.25),
p75: this.percentile(values, 0.75)
};
// Determine consensus strength
const variance = this.calculateVariance(values);
const coefficientOfVariation = Math.sqrt(variance) / recommendedValue;
let consensus: 'strong' | 'moderate' | 'weak';
if (coefficientOfVariation < 0.1) {
consensus = 'strong';
} else if (coefficientOfVariation < 0.25) {
consensus = 'moderate';
} else {
consensus = 'weak';
}
// Calculate overall confidence
const confidence = Math.min(
0.95,
totalWeight / sources.length * (consensus === 'strong' ? 1.2 : consensus === 'moderate' ? 1.0 : 0.8)
);
return {
metric,
value: recommendedValue,
unit: sources[0].benchmark.unit,
confidence,
sources: sourceDetails,
consensus,
recommendedValue,
range
};
}
/**
* Calculate percentile
*/
private percentile(sortedValues: number[], p: number): number {
const index = p * (sortedValues.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
const weight = index % 1;
if (lower === upper) {
return sortedValues[lower];
}
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
}
/**
* Calculate variance
*/
private calculateVariance(values: number[]): number {
const mean = values.reduce((a, b) => a + b, 0) / values.length;
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
return squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
}
/**
* Calculate average confidence across all benchmarks
*/
private calculateAverageConfidence(benchmarks: AggregatedBenchmark[]): number {
if (benchmarks.length === 0) return 0;
const sum = benchmarks.reduce((acc, b) => acc + b.confidence, 0);
return sum / benchmarks.length;
}
/**
* Get industry-specific adjustment factors
*/
async getIndustryAdjustments(
industry: string,
companySize?: string
): Promise<{
sizeMultiplier: number;
complexityFactor: number;
riskAdjustment: number;
}> {
// Size adjustments
const sizeMultipliers: Record<string, number> = {
small: 0.85, // Smaller companies typically see lower absolute ROI
medium: 1.0, // Baseline
large: 1.15, // Larger scale benefits
enterprise: 1.25 // Maximum scale benefits
};
// Industry complexity factors
const complexityFactors: Record<string, number> = {
financial_services: 1.3, // High regulation
healthcare: 1.4, // Highest regulation
retail: 0.9, // Lower complexity
manufacturing: 1.1, // Moderate complexity
technology: 0.8, // Lowest barriers
education: 1.0, // Baseline
government: 1.5, // Highest complexity
other: 1.0 // Baseline
};
return {
sizeMultiplier: sizeMultipliers[companySize || 'medium'] || 1.0,
complexityFactor: complexityFactors[industry] || 1.0,
riskAdjustment: 1.0 // Could be enhanced with Sonar data
};
}
}