ML Orchestration Engine - Decision Intelligence Backbone
ML Orchestration Engine: Decision Intelligence Backbone
Version: 1.0 Status: Specification - Phase 1 & 2 Implementation Scope: Probabilistic reasoning, scenario robustness analysis, and value-of-information calculation Target Completion: 10 weeks (Phase 1: Weeks 1-4, Phase 2: Weeks 5-8, Integration: Weeks 9-10)
1. Executive Summary
Problem Statement
ChainAlign currently provides three powerful but isolated capabilities:
- Constraint validation (deterministic: feasible or not)
- Monte Carlo simulation (probabilistic: what could happen)
- LLM reasoning (narrative: what does it mean)
These work independently. Users see either/or outputs: "Plan is feasible" OR "Here are possible outcomes" OR "This is the business impact."
Gap: Users need integrated intelligence that says: "Here's what will probably happen (probabilistic), here's when it breaks (stress testing), here's why it matters (LLM explanation), and here's how certain we are (confidence bounds)."
Consequence: Decisions lack explicit uncertainty quantification and robustness validation, reducing user trust and missing opportunities to mitigate risks.
Solution: ML Orchestration Engine
A unified architecture that orchestrates decision theory concepts (from "Algorithms for Decision Making") with ChainAlign's existing engines:
- Probabilistic Confidence Propagation - Quantify uncertainty at each decision point
- Scenario Robustness Analysis - Identify failure modes and conditions
- Value of Information (VOI) - Prioritize which data to integrate based on decision impact
- Bayesian Reasoning - Model causal relationships and condition probabilities
- Feedback Loop - Learn from user decisions and improve future predictions
Core Principle: Orchestration is the moat. Not ML alone, not LLM alone, but intelligent routing through the right combination of engines to produce decisions users can trust and act on.
2. Architecture Overview
2.1 System Context
Input Layer (Data & Context)
↓
Evaluation Pipeline
├─ Constraint Intelligence Engine (existing) ← enhanced
├─ Monte Carlo Service (existing) ← enhanced
├─ BayesianReasoningService (new)
├─ ScenarioRobustnessService (new)
├─ DataSourceImpactService (new)
└─ AIManager/LLM Layer (existing) ← enhanced for orchestration
↓
Output Layer (Insights & Recommendations)
├─ Confidence-annotated constraints
├─ Stress-tested scenarios
├─ VOI-ranked data sources
├─ Failure mode analysis
└─ McKinsey-style narratives with caveats
2.2 Core Services (Phased)
Phase 1 (Weeks 1-4): Probabilistic Reasoning & Robustness
- BayesianReasoningService - Small Bayesian networks for high-risk decisions
- ScenarioRobustnessService - Stress testing and failure mode analysis
- ConstraintIntelligenceEngine (Enhanced) - Probabilistic constraint checking
Phase 2 (Weeks 5-8): Value of Information & Learning
- DataSourceImpactService - VOI calculation through empirical backtesting
- FeedbackCapture & OnlineLearning - Log decisions, measure outcomes, retrain
- MultiObjectiveOptimizer - Pareto frontier analysis for conflicting objectives
Integration (Weeks 9-10)
- DecisionOrchestrationService - Routes decisions through appropriate services
- Pipeline validation & demo integration
3. Phase 1: Probabilistic Reasoning & Robustness Analysis
3.1 BayesianReasoningService
Purpose: Model causal relationships and compute conditional probabilities for high-impact decision nodes.
Design Philosophy: Don't build one giant Bayesian network. Build 3-5 micro-networks for specific high-uncertainty decisions:
- Supplier lead time prediction
- Demand forecast accuracy
- Stockout risk quantification
- Forecast error bounds
Architecture:
// backend/src/services/BayesianReasoningService.js
class BayesianReasoningService {
/**
* Computes conditional probability P(outcome | evidence)
* for a specific decision context
*
* @param {string} decisionContext - e.g., 'supplier_delay', 'demand_forecast'
* @param {Object} evidence - Current known facts {supplier_capacity, historical_lead_time, ...}
* @param {string} queryVariable - What are we trying to predict?
*
* @returns {Object} Probability distribution with confidence
* {
* distribution: { low: 0.15, medium: 0.70, high: 0.15 },
* expectedValue: 'medium',
* confidence: 0.78,
* confidenceReason: 'Based on 24 months historical data'
* }
*/
async computeConditionalProbability(decisionContext, evidence, queryVariable) {
// 1. Select appropriate micro-network for this context
const network = this._selectNetwork(decisionContext);
// 2. Load network structure and CPDs (Conditional Probability Distributions)
const { nodes, edges, cpds } = await this._loadNetworkDefinition(network);
// 3. Initialize evidence (what we know)
const internalEvidence = this._mapExternalToInternalVars(evidence, nodes);
// 4. Run inference algorithm
const result = this._runInference(nodes, edges, cpds, internalEvidence, queryVariable);
// 5. Compute confidence based on evidence quality and data coverage
const confidence = this._computeConfidenceScore(result, internalEvidence, nodes);
// 6. Annotate with reasoning
const reasoning = this._explainReasoning(result, evidence, network);
return {
distribution: result.probability,
expectedValue: result.mostLikelyValue,
confidence,
reasoning,
metadata: {
network: network,
evidenceVars: Object.keys(internalEvidence),
dataPoints: result.dataPointsUsed,
lastRetrained: result.lastRetrainDate
}
};
}
/**
* Returns probability of outcome given this specific scenario
* Used by ScenarioRobustnessService to test failure conditions
*/
async queryProbability(network, scenario) {
const evidence = scenario.parameters;
return this.computeConditionalProbability(network, evidence, 'outcome');
}
// ========== Micro-Network Definitions ==========
/**
* Network 1: Supplier Lead Time Prediction
*
* Causal structure:
* supplier_capacity → production_rate → lead_time ← order_quantity
* ↑ ↓
* supplier_reliability lead_time_buffer
*/
async _initSupplierLeadTimeNetwork() {
return {
name: 'supplier_lead_time',
nodes: {
supplier_capacity: { type: 'discrete', values: ['low', 'medium', 'high'] },
supplier_reliability: { type: 'discrete', values: ['poor', 'fair', 'good', 'excellent'] },
order_quantity: { type: 'continuous', bounds: [0, 100000] },
production_rate: { type: 'continuous', bounds: [0, 10000] },
lead_time: { type: 'discrete', values: ['<1week', '1-2weeks', '2-3weeks', '>3weeks'] }
},
edges: [
['supplier_capacity', 'production_rate'],
['supplier_reliability', 'production_rate'],
['order_quantity', 'production_rate'],
['production_rate', 'lead_time']
],
cpds: {
// CPD: P(production_rate | supplier_capacity, supplier_reliability, order_quantity)
// Learned from historical supplier data
production_rate: {
low_poor_small: { mean: 2000, std: 400 }, // low capacity, poor reliability, small order
low_poor_large: { mean: 1500, std: 500 }, // constrained by capacity
medium_fair_small: { mean: 4500, std: 800 },
medium_good_small: { mean: 5500, std: 700 },
high_excellent_large: { mean: 8500, std: 900 }
// ... more combinations
},
// CPD: P(lead_time | production_rate)
lead_time: {
'<2000units_day': { '<1week': 0.05, '1-2weeks': 0.30, '2-3weeks': 0.50, '>3weeks': 0.15 },
'2000-4000': { '<1week': 0.15, '1-2weeks': 0.55, '2-3weeks': 0.25, '>3weeks': 0.05 },
'4000+': { '<1week': 0.40, '1-2weeks': 0.45, '2-3weeks': 0.10, '>3weeks': 0.05 }
}
},
dataSource: 'supplier_performance_metrics' // Table to retrain from
};
}
/**
* Network 2: Demand Forecast Accuracy
*
* Causal structure:
* seasonal_pattern + forecast_method + market_conditions → forecast_error
*/
async _initDemandForecastNetwork() {
return {
name: 'demand_forecast_accuracy',
nodes: {
seasonal_pattern: { type: 'discrete', values: ['low', 'medium', 'high'] },
forecast_method: { type: 'discrete', values: ['statistical', 'ml', 'consensus', 'manual'] },
market_volatility: { type: 'discrete', values: ['low', 'medium', 'high'] },
promotion_intensity: { type: 'continuous', bounds: [0, 1] },
forecast_error: { type: 'continuous', bounds: [-100, 100] }, // % error
confidence_interval: { type: 'continuous', bounds: [5, 50] } // % width
},
edges: [
['seasonal_pattern', 'forecast_error'],
['forecast_method', 'forecast_error'],
['market_volatility', 'forecast_error'],
['promotion_intensity', 'forecast_error'],
['forecast_error', 'confidence_interval']
],
cpds: {
forecast_error: {
// P(error | seasonal, method, volatility, promotion)
'high_seasonal_statistical': { mean: -8.5, std: 12.5 }, // More error in high seasonality
'high_seasonal_ml': { mean: -3.2, std: 9.8 }, // ML better at capturing patterns
'high_seasonal_consensus': { mean: 2.1, std: 8.5 }, // Consensus moderates bias
'low_seasonal_statistical': { mean: 1.2, std: 4.5 },
'low_seasonal_ml': { mean: 0.8, std: 3.2 }
// ... more combinations
},
confidence_interval: {
// P(CI_width | forecast_error)
'error_<5': { mean: 10, std: 2 },
'error_5to15': { mean: 18, std: 4 },
'error_>15': { mean: 32, std: 8 }
}
},
dataSource: 'forecast_accuracy_metrics'
};
}
/**
* Network 3: Stockout Risk
*
* Causal structure:
* (inventory, lead_time, demand) → stockout
*/
async _initStockoutRiskNetwork() {
return {
name: 'stockout_risk',
nodes: {
current_inventory: { type: 'continuous' },
safety_stock_level: { type: 'continuous' },
lead_time_days: { type: 'discrete', values: ['<7', '7-14', '14-21', '>21'] },
demand_mean: { type: 'continuous' },
demand_volatility: { type: 'discrete', values: ['low', 'medium', 'high'] },
stockout_probability: { type: 'continuous', bounds: [0, 1] }
},
edges: [
['current_inventory', 'stockout_probability'],
['safety_stock_level', 'stockout_probability'],
['lead_time_days', 'stockout_probability'],
['demand_mean', 'stockout_probability'],
['demand_volatility', 'stockout_probability']
],
cpds: {
// Simplified: P(stockout | inventory_coverage_days)
stockout_probability: {
// coverage_days = (inventory + reorder_point) / daily_demand
'<7days': 0.35,
'7-14days': 0.12,
'14-21days': 0.04,
'>21days': 0.01
}
},
dataSource: 'inventory_metrics'
};
}
// ========== Inference Algorithms ==========
/**
* Variable Elimination inference for discrete networks
* (Simplified version; production should use proper inference library)
*/
_runInference(nodes, edges, cpds, evidence, queryVariable) {
// In production, use pgmpy or similar library
// For MVP, implement simplified sampling-based inference
const samples = this._importanceSampling(nodes, edges, cpds, evidence, 1000);
const result = this._aggregateSamples(samples, queryVariable);
return result;
}
/**
* Importance sampling for approximate inference
*/
_importanceSampling(nodes, edges, cpds, evidence, numSamples) {
const samples = [];
for (let i = 0; i < numSamples; i++) {
const sample = {};
const weight = 1.0; // Track likelihood weight for importance sampling
// Topological order: sample nodes in dependency order
for (const node of this._topologicalSort(edges)) {
if (node in evidence) {
sample[node] = evidence[node];
} else {
// Sample from conditional distribution
const parents = this._getParents(node, edges);
const parentValues = parents.map(p => sample[p]);
const cpd = cpds[node];
sample[node] = this._sampleFromCPD(cpd, parentValues);
}
}
samples.push({ sample, weight });
}
return samples;
}
/**
* Aggregate samples into probability distribution
*/
_aggregateSamples(samples, queryVariable) {
const distribution = {};
let total = 0;
samples.forEach(({ sample, weight }) => {
const value = sample[queryVariable];
distribution[value] = (distribution[value] || 0) + weight;
total += weight;
});
// Normalize to probabilities
Object.keys(distribution).forEach(key => {
distribution[key] /= total;
});
return {
probability: distribution,
mostLikelyValue: Object.keys(distribution).reduce((a, b) =>
distribution[a] > distribution[b] ? a : b
)
};
}
/**
* Confidence score based on evidence quality
* Higher if: more evidence provided, evidence is recent, data is abundant
*/
_computeConfidenceScore(result, evidence, nodeDefinitions) {
let score = 0.5; // Base confidence
// Factor 1: Evidence coverage (how many parent nodes have evidence?)
const parentsCovered = Object.keys(evidence).length;
const parentsTotal = Object.keys(nodeDefinitions).length;
score += 0.2 * (parentsCovered / parentsTotal);
// Factor 2: Data quality (assume we track this in the DB)
// This would come from: data_quality_metrics table
score += 0.15;
// Factor 3: Recency (more recent data = higher confidence)
// Would check: when was the network last retrained?
score += 0.15;
return Math.min(1.0, score); // Cap at 100%
}
/**
* Natural language explanation of why we got this probability
*/
_explainReasoning(result, evidence, network) {
if (network === 'supplier_lead_time') {
return `Lead time is likely ${result.mostLikelyValue} because:` +
`\n- Supplier capacity is ${evidence.supplier_capacity}` +
`\n- Order quantity of ${evidence.order_quantity} units ` +
`\n- Historical reliability: ${evidence.supplier_reliability}`;
}
// ... similar for other networks
}
// ========== Helper methods ==========
_selectNetwork(context) {
const networkMap = {
'supplier_delay': 'supplier_lead_time',
'forecast_accuracy': 'demand_forecast_accuracy',
'stockout_risk': 'stockout_risk'
};
return networkMap[context];
}
async _loadNetworkDefinition(network) {
// Load from cache or database
// In MVP: hardcoded definitions above
}
_mapExternalToInternalVars(externalEvidence, nodeDefinitions) {
// Normalize external variable names to network node names
return externalEvidence;
}
_topologicalSort(edges) {
// Standard topological sort on DAG
}
_getParents(node, edges) {
return edges.filter(([src, dst]) => dst === node).map(([src]) => src);
}
_sampleFromCPD(cpd, parentValues) {
// Sample from conditional distribution given parent values
// In MVP: return expected value or sample from Gaussian
}
}
export default new BayesianReasoningService();
Integration with Constraint Intelligence Engine:
// backend/src/services/ConstraintIntelligenceEngine.js (enhanced)
async _checkConstraintWithProbability(constraint, parameters) {
const constraintName = constraint.name?.toLowerCase();
const definition = constraint.definition || {};
const paramValue = parameters[constraintName];
if (paramValue === null) return null;
// NEW: Get probabilistic bounds for this parameter
const bayesianResult = await BayesianReasoningService.computeConditionalProbability(
constraintName,
{ /* evidence from data */ },
'forecast_value'
);
// Instead of hard check, compute probability of violation
const probabilityOfViolation = this._computeViolationProbability(
paramValue,
bayesianResult.distribution,
definition.upper_bound,
definition.lower_bound
);
// Return violation with confidence
return {
constraint: constraintName,
severity: probabilityOfViolation > 0.5 ? 'critical' : 'warning',
message: `${constraintName} violation risk is ${(probabilityOfViolation * 100).toFixed(1)}%`,
metadata: {
paramValue,
violationProbability: probabilityOfViolation,
confidence: bayesianResult.confidence,
reasoning: bayesianResult.reasoning
}
};
}
_computeViolationProbability(value, distribution, upperBound, lowerBound) {
// If distribution is continuous (Gaussian), integrate tail probability
// If distribution is discrete, sum probabilities of violating outcomes
// Simplified: treat as normal distribution
let violation_prob = 0;
Object.entries(distribution).forEach(([outcome, prob]) => {
const outcomeMagnitude = parseFloat(outcome);
if (outcomeMagnitude > upperBound || outcomeMagnitude < lowerBound) {
violation_prob += prob;
}
});
return violation_prob;
}
Database Schema Changes:
-- Network definitions (loaded into cache, retrained periodically)
CREATE TABLE bayesian_networks (
network_id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(tenant_id),
network_name VARCHAR(255), -- 'supplier_lead_time', 'demand_forecast_accuracy'
network_definition JSONB, -- Nodes, edges, CPDs
last_retrained TIMESTAMP,
training_data_points INT, -- How many samples was this trained on?
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Inference results cache (log every query for analysis)
CREATE TABLE bayesian_inferences (
inference_id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(tenant_id),
network_id UUID REFERENCES bayesian_networks(network_id),
decision_context VARCHAR(255), -- 'supplier_delay', 'demand_forecast', etc.
evidence JSONB, -- What facts did we condition on?
result JSONB, -- {distribution, confidence, reasoning}
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id, network_id, created_at)
);
-- Retraining log (for online learning)
CREATE TABLE bayesian_network_retrains (
retrain_id UUID PRIMARY KEY,
network_id UUID REFERENCES bayesian_networks(network_id),
retrain_date TIMESTAMP,
data_points_used INT,
performance_before JSONB, -- Accuracy metrics before retrain
performance_after JSONB, -- Accuracy metrics after retrain
changes_made JSONB -- Which CPDs changed significantly?
);
3.2 ScenarioRobustnessService
Purpose: Test scenarios against adversarial conditions to identify when they fail.
Implementation Strategy: For each scenario, run Monte Carlo with perturbed inputs to find failure boundaries.
// backend/src/services/ScenarioRobustnessService.js
class ScenarioRobustnessService {
/**
* Stress-tests a scenario by varying key parameters
* Returns failure modes and their probabilities
*
* @param {Object} scenario - Base scenario (production volume, inventory policy, etc.)
* @param {string} tenantId
* @returns {Object} Robustness analysis with failure conditions
*/
async analyzeRobustness(scenario, tenantId) {
const startTime = Date.now();
logger.info('ScenarioRobustnessService.analyzeRobustness() starting', {
tenantId,
scenarioId: scenario.id
});
// 1. Get baseline scenario outcome
const baseline = await this._runScenario(scenario, tenantId);
// 2. Identify key uncertainty parameters
const uncertainParams = await this._identifyUncertainParameters(scenario, tenantId);
// 3. For each uncertain parameter, run stress tests
const stressTests = [];
for (const param of uncertainParams) {
const stressResult = await this._stressTestParameter(
scenario,
baseline,
param,
tenantId
);
stressTests.push(stressResult);
}
// 4. Identify failure modes (where KPIs degrade)
const failureModes = this._extractFailureModes(stressTests, baseline);
// 5. Compute failure probabilities using Bayesian networks
const failureAnalysis = await this._analyzeFailureRisks(
failureModes,
tenantId
);
// 6. Compile robustness score (0-100: how resilient is this scenario?)
const robustnessScore = this._computeRobustnessScore(failureAnalysis);
logger.info('ScenarioRobustnessService.analyzeRobustness() completed', {
tenantId,
robustnessScore,
failureModesCount: failureModes.length,
duration: Date.now() - startTime
});
return {
scenario: {
id: scenario.id,
name: scenario.name,
parameters: scenario.parameters
},
baseline: {
revenue: baseline.revenue,
margin: baseline.margin,
serviceLevel: baseline.serviceLevel,
stockoutRisk: baseline.stockoutRisk,
workingCapital: baseline.workingCapital
},
stressTests: stressTests.map(st => ({
parameter: st.parameter,
baseline_value: st.baseline_value,
perturbed_values: st.perturbed_values,
results: st.results
})),
failureModes: failureModes,
failureAnalysis: failureAnalysis,
robustnessScore: robustnessScore,
recommendation: this._generateRecommendation(robustnessScore, failureAnalysis),
metrics: {
duration: Date.now() - startTime,
stressTestsRun: stressTests.length,
failureModesIdentified: failureModes.length
}
};
}
/**
* Runs Monte Carlo simulation for a single scenario
* Returns KPI outcomes
*/
async _runScenario(scenario, tenantId) {
// Call existing MonteCarloService
const result = await DynamicBatchedWorkerPool.submitJob({
type: 'monte_carlo',
scenario,
iterations: 1000,
tenantId
});
return {
revenue: result.revenue_distribution.p50,
margin: result.margin_distribution.p50,
serviceLevel: result.service_level_distribution.p50,
stockoutRisk: result.stockout_risk,
workingCapital: result.working_capital_distribution.p50,
// Full distributions for comparison
distributions: result
};
}
/**
* Identify which parameters have highest uncertainty
*/
async _identifyUncertainParameters(scenario, tenantId) {
// Heuristic: parameters with highest historical volatility
// In MVP: hardcode the ones we know matter
return [
{ name: 'demand', type: 'continuous', baseValue: scenario.demand },
{ name: 'supplier_lead_time', type: 'discrete', baseValue: 14 },
{ name: 'material_cost', type: 'continuous', baseValue: scenario.material_cost },
{ name: 'customer_order_volume', type: 'continuous', baseValue: scenario.customer_volume }
];
}
/**
* Stress test: vary one parameter across a range, observe impact
*/
async _stressTestParameter(scenario, baseline, param, tenantId) {
const perturbed_values = this._generatePerturbations(param);
const results = [];
for (const perturbed_value of perturbed_values) {
// Create modified scenario
const modifiedScenario = {
...scenario,
[param.name]: perturbed_value
};
// Run simulation with perturbed parameter
const outcome = await this._runScenario(modifiedScenario, tenantId);
results.push({
perturbed_value: perturbed_value,
revenue: outcome.revenue,
margin: outcome.margin,
serviceLevel: outcome.serviceLevel,
stockoutRisk: outcome.stockoutRisk,
// Compute deltas from baseline
deltaRevenue: outcome.revenue - baseline.revenue,
deltaMargin: outcome.margin - baseline.margin,
deltaServiceLevel: outcome.serviceLevel - baseline.serviceLevel,
deltaStockoutRisk: outcome.stockoutRisk - baseline.stockoutRisk
});
}
return {
parameter: param.name,
baseline_value: param.baseValue,
perturbed_values: perturbed_values,
results: results,
sensitivity: this._computeSensitivity(results) // Which KPI is most sensitive?
};
}
/**
* Generate range of perturbations for a parameter
*/
_generatePerturbations(param) {
if (param.type === 'continuous') {
// Generate P10, P50 (baseline), P90 equivalents
return [
param.baseValue * 0.7, // -30% (pessimistic)
param.baseValue * 0.85, // -15%
param.baseValue, // baseline
param.baseValue * 1.15, // +15%
param.baseValue * 1.30 // +30% (optimistic)
];
} else if (param.type === 'discrete') {
// E.g., lead time: try different values
return [7, 14, 21, 28, 35]; // days
}
}
/**
* Extract failure modes from stress tests
* A failure mode is: a specific parameter value + condition where KPI degrades critically
*/
_extractFailureModes(stressTests, baseline) {
const failureModes = [];
const CRITICAL_THRESHOLD = 0.15; // Flag if KPI degrades >15%
stressTests.forEach(stressTest => {
stressTest.results.forEach(result => {
// Check if any KPI degradation is critical
if (Math.abs(result.deltaMargin) > baseline.margin * CRITICAL_THRESHOLD) {
failureModes.push({
parameter: stressTest.parameter,
perturbedValue: result.perturbed_value,
affectedKpi: 'margin',
degradation: result.deltaMargin,
degradationPercent: (result.deltaMargin / baseline.margin) * 100,
severity: this._classifySeverity(result.deltaMargin, baseline.margin),
description: `If ${stressTest.parameter} = ${result.perturbed_value}, ` +
`margin drops to ${result.margin} (baseline: ${baseline.margin})`
});
}
if (result.deltaStockoutRisk > 0.10) { // >10 percentage point increase
failureModes.push({
parameter: stressTest.parameter,
perturbedValue: result.perturbed_value,
affectedKpi: 'stockout_risk',
degradation: result.deltaStockoutRisk,
degradationPercent: (result.deltaStockoutRisk / (1 - baseline.stockoutRisk)) * 100,
severity: this._classifySeverity(result.deltaStockoutRisk, baseline.stockoutRisk),
description: `If ${stressTest.parameter} = ${result.perturbed_value}, ` +
`stockout risk rises to ${(result.stockoutRisk * 100).toFixed(1)}%`
});
}
});
});
return failureModes.sort((a, b) => b.degradationPercent - a.degradationPercent);
}
/**
* For each failure mode, compute its probability using Bayesian networks
*/
async _analyzeFailureRisks(failureModes, tenantId) {
const failureAnalysis = [];
for (const failureMode of failureModes) {
// Use BayesianReasoningService to estimate P(this parameter takes this value)
const probabilityOfCondition = await BayesianReasoningService
.computeConditionalProbability(
failureMode.parameter,
{ /* current evidence */ },
failureMode.parameter
);
// What's the probability this failure mode occurs?
const probability = probabilityOfCondition.distribution[failureMode.perturbedValue] || 0;
failureAnalysis.push({
failureMode: failureMode,
probabilityOfCondition: probability,
severity: failureMode.severity,
riskScore: probability * this._severityToScore(failureMode.severity),
mitigation: this._suggestMitigation(failureMode)
});
}
return failureAnalysis;
}
/**
* Robustness score: 0-100
* High = scenario is resilient to perturbations
* Low = scenario is brittle and breaks easily
*/
_computeRobustnessScore(failureAnalysis) {
if (failureAnalysis.length === 0) return 95; // No failure modes = highly robust
// Average risk score across all failure modes
const avgRiskScore = failureAnalysis.reduce((sum, fa) => sum + fa.riskScore, 0) /
failureAnalysis.length;
// Convert to 0-100 scale (lower risk = higher robustness)
return Math.max(0, 100 - (avgRiskScore * 20)); // Crude conversion; tune as needed
}
/**
* Generate mitigation strategies for failure modes
*/
_suggestMitigation(failureMode) {
const suggestions = [];
if (failureMode.parameter === 'supplier_lead_time') {
suggestions.push('Increase safety stock to buffer against lead time variability');
suggestions.push('Identify secondary supplier to reduce single-source risk');
} else if (failureMode.parameter === 'demand') {
suggestions.push('Implement demand-shaping programs (promotions, pricing)');
suggestions.push('Negotiate flexible volume commitments with customer');
} else if (failureMode.parameter === 'material_cost') {
suggestions.push('Lock in supplier contracts with price caps');
suggestions.push('Explore alternative materials or sourcing regions');
}
return suggestions;
}
_classifySeverity(delta, baseline) {
const percentChange = Math.abs(delta / baseline);
if (percentChange > 0.25) return 'critical';
if (percentChange > 0.15) return 'high';
if (percentChange > 0.05) return 'medium';
return 'low';
}
_severityToScore(severity) {
return { critical: 1.0, high: 0.7, medium: 0.4, low: 0.1 }[severity];
}
_computeSensitivity(results) {
// Which KPI changes most across perturbations?
const variances = {
revenue: this._variance(results.map(r => r.deltaRevenue)),
margin: this._variance(results.map(r => r.deltaMargin)),
serviceLevel: this._variance(results.map(r => r.deltaServiceLevel)),
stockoutRisk: this._variance(results.map(r => r.deltaStockoutRisk))
};
return Object.entries(variances)
.sort(([, a], [, b]) => b - a)
.map(([kpi]) => kpi);
}
_variance(array) {
const mean = array.reduce((a, b) => a + b, 0) / array.length;
return array.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / array.length;
}
_generateRecommendation(robustnessScore, failureAnalysis) {
if (robustnessScore >= 80) {
return 'This scenario is robust. It can handle moderate disruptions.';
} else if (robustnessScore >= 60) {
return 'This scenario has some vulnerabilities. Consider mitigations before committing.';
} else {
return 'This scenario is brittle. Major risks if conditions change. Recommend alternative.';
}
}
}
export default new ScenarioRobustnessService();
Database Schema:
CREATE TABLE scenario_robustness_analyses (
analysis_id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(tenant_id),
scenario_id UUID REFERENCES scenarios(scenario_id),
baseline_revenue DECIMAL(15,2),
baseline_margin DECIMAL(5,2),
baseline_service_level DECIMAL(5,2),
robustness_score INT, -- 0-100
failure_modes_count INT,
analysis_result JSONB, -- Full result with stress tests and recommendations
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id, scenario_id)
);
4. Phase 2: Value of Information & Multi-Objective Optimization
4.1 DataSourceImpactService
Purpose: Quantify how much each data source improves decision quality through empirical backtesting.
// backend/src/services/DataSourceImpactService.js
class DataSourceImpactService {
/**
* Calculates Value of Information (VOI) for a data source
* by comparing prediction accuracy with/without that data
*
* @param {string} dataSourceName - e.g., 'realtime_wip', 'supplier_capacity_api'
* @param {Object} options - { lookbackMonths: 6, trainingPeriod: 'last_12_months' }
* @returns {Object} VOI analysis with confidence
*/
async calculateVOI(dataSourceName, options = {}) {
const { lookbackMonths = 6, trainingPeriod = 'last_12_months' } = options;
logger.info('DataSourceImpactService.calculateVOI() starting', {
dataSourceName,
lookbackMonths
});
// 1. Retrieve historical S&OP cycles with outcomes
const historicalCycles = await this._getHistoricalSOPCycles(trainingPeriod);
// 2. For each historical cycle, run predictions WITH data source
const resultsWithData = await this._runPredictionsWithDataSource(
historicalCycles,
dataSourceName
);
// 3. For each historical cycle, simulate predictions WITHOUT data source
const resultsWithoutData = await this._runPredictionsWithoutDataSource(
historicalCycles,
dataSourceName
);
// 4. Compare accuracy, acceptance rates, and business outcomes
const accuracyDelta = this._computeAccuracyDelta(resultsWithData, resultsWithoutData);
const acceptanceDelta = this._computeAcceptanceDelta(resultsWithData, resultsWithoutData);
const businessImpactDelta = await this._computeBusinessImpactDelta(
resultsWithData,
resultsWithoutData,
historicalCycles
);
// 5. Estimate realized annual value in dollars
const estimatedAnnualValue = this._estimateMonetaryValue(
accuracyDelta,
acceptanceDelta,
businessImpactDelta
);
// 6. Compute confidence in VOI estimate
const confidence = this._computeVOIConfidence(historicalCycles, resultsWithData);
logger.info('DataSourceImpactService.calculateVOI() completed', {
dataSourceName,
estimatedAnnualValue,
confidence
});
return {
dataSourceName: dataSourceName,
evaluationPeriod: trainingPeriod,
samplesAnalyzed: historicalCycles.length,
accuracy: {
improvementPercent: (accuracyDelta.improvement * 100).toFixed(2),
metricsImpacted: accuracyDelta.metrics,
baseline: accuracyDelta.baselineAccuracy,
withData: accuracyDelta.withDataAccuracy
},
acceptance: {
improvementPercent: (acceptanceDelta.improvement * 100).toFixed(2),
baselineAcceptanceRate: acceptanceDelta.baselineAcceptance,
withDataAcceptanceRate: acceptanceDelta.withDataAcceptance,
avgUserModificationReduction: acceptanceDelta.modificationReduction
},
businessImpact: {
serviceLevel: businessImpactDelta.serviceLevel,
inventoryVariance: businessImpactDelta.inventoryVariance,
stockoutReduction: businessImpactDelta.stockoutReduction,
marginProtected: businessImpactDelta.marginProtected
},
monetaryValue: {
estimatedAnnualValue: estimatedAnnualValue,
valueRange: {
conservative: estimatedAnnualValue * 0.7,
aggressive: estimatedAnnualValue * 1.3
},
assumptionsUsed: [
'Labor savings: 30 min/decision × 500 decisions/year × $150/hr = $3,750',
'Inventory carrying cost: 20% of average inventory value',
'Stockout risk reduction valued at 40% of lost revenue',
'Margin protection valued at 80% of improvement'
]
},
confidence: confidence,
recommendation: this._generateDataSourceRecommendation(
estimatedAnnualValue,
confidence
),
metrics: {
duration: 'N/A',
cyclesAnalyzed: historicalCycles.length
}
};
}
/**
* Retrieves historical S&OP cycles where we know actual outcomes
*/
async _getHistoricalSOPCycles(trainingPeriod) {
// Query: plans + actual outcomes
// WHERE plan_date BETWEEN (today - trainingPeriod) AND today
// AND actual_outcomes IS NOT NULL (completed cycles)
const query = `
SELECT
p.plan_id,
p.plan_date,
p.plan_details,
p.data_sources_available, -- JSON: which data sources were integrated?
a.service_level_actual,
a.inventory_actual,
a.stockout_incidents,
a.revenue_actual,
a.margin_actual
FROM sop_plans p
JOIN sop_actual_outcomes a ON p.plan_id = a.plan_id
WHERE p.plan_date >= (NOW() - INTERVAL '12 months')
AND a.outcomes_recorded = true
ORDER BY p.plan_date DESC
`;
// This assumes we have these tables; may need to adjust to match schema
return await db.raw(query);
}
/**
* Re-run insight/recommendation generation WITH a specific data source
*/
async _runPredictionsWithDataSource(historicalCycles, dataSourceName) {
const results = [];
for (const cycle of historicalCycles) {
// 1. Check if this cycle had the data source available
const dataSourceWasAvailable = cycle.data_sources_available.includes(dataSourceName);
// 2. If it was available, generate predictions as we normally would
const prediction = await this._generatePrediction(
cycle.plan_details,
includeDataSource: dataSourceWasAvailable
);
results.push({
cycleId: cycle.plan_id,
prediction: prediction,
actual: {
serviceLevel: cycle.service_level_actual,
inventory: cycle.inventory_actual,
stockouts: cycle.stockout_incidents,
revenue: cycle.revenue_actual,
margin: cycle.margin_actual
},
dataSourceAvailable: dataSourceWasAvailable
});
}
return results;
}
/**
* Re-run predictions WITHOUT a specific data source
* (Simulate what would have happened if we didn't have it)
*/
async _runPredictionsWithoutDataSource(historicalCycles, dataSourceName) {
const results = [];
for (const cycle of historicalCycles) {
// 1. Simulate: remove this data source from available data
const modifiedDataSources = cycle.data_sources_available.filter(
ds => ds !== dataSourceName
);
// 2. Re-generate predictions without it
const prediction = await this._generatePrediction(
cycle.plan_details,
{ excludeDataSources: [dataSourceName] }
);
results.push({
cycleId: cycle.plan_id,
prediction: prediction,
actual: cycle // Same actuals as reference
});
}
return results;
}
/**
* Generate a prediction (insight + recommendation) for a plan
* Orchestrates through ConstraintIntelligenceEngine, Monte Carlo, LLM
*/
async _generatePrediction(planDetails, options) {
// This simulates running the full evaluation pipeline
// In production: would call ScenarioImpactService or similar
// 1. Validate constraints
const constraintAnalysis = await ConstraintIntelligenceEngine.validate({
tenantId: planDetails.tenantId,
parameters: planDetails.parameters,
constraints: planDetails.constraints
});
// 2. Run Monte Carlo
const monteCarloResult = await DynamicBatchedWorkerPool.submitJob({
type: 'monte_carlo',
scenario: planDetails,
iterations: 500
});
// 3. Generate LLM narrative
const narrative = await AIManager.generateInsight(
constraintAnalysis,
monteCarloResult,
planDetails
);
return {
constraints: constraintAnalysis,
monteCarlo: monteCarloResult,
narrative: narrative,
// Extract key predictions
predictedServiceLevel: monteCarloResult.service_level_distribution.p50,
predictedStockoutRisk: monteCarloResult.stockout_risk,
predictedMargin: monteCarloResult.margin_distribution.p50
};
}
/**
* Compare prediction accuracy with vs. without data source
*/
_computeAccuracyDelta(resultsWithData, resultsWithoutData) {
const accuracyWith = resultsWithData.map(r =>
this._computePredictionAccuracy(r.prediction, r.actual)
);
const accuracyWithout = resultsWithoutData.map(r =>
this._computePredictionAccuracy(r.prediction, r.actual)
);
const avgWith = accuracyWith.reduce((a, b) => a + b, 0) / accuracyWith.length;
const avgWithout = accuracyWithout.reduce((a, b) => a + b, 0) / accuracyWithout.length;
return {
improvement: avgWith - avgWithout,
baselineAccuracy: avgWithout,
withDataAccuracy: avgWith,
metrics: [
'service_level_forecast_error',
'stockout_risk_detection',
'margin_prediction_error'
]
};
}
/**
* Compute MAE or similar for prediction accuracy
*/
_computePredictionAccuracy(prediction, actual) {
// Simple version: average absolute error across KPIs
const slError = Math.abs(prediction.predictedServiceLevel - actual.serviceLevel);
const marginError = Math.abs(prediction.predictedMargin - actual.margin);
const stockoutError = Math.abs(prediction.predictedStockoutRisk - actual.stockouts / 100);
// Normalize to 0-1 scale (lower error = higher accuracy = 1.0)
const normalizedError = (slError / 100 + marginError / 10000 + stockoutError) / 3;
return Math.max(0, 1.0 - normalizedError);
}
/**
* Compare how often users accepted recommendations
*/
_computeAcceptanceDelta(resultsWithData, resultsWithoutData) {
// Would need audit_log data: did user accept recommendation as-is, or modify?
// For now: placeholder
return {
improvement: 0.12, // +12 percentage points
baselineAcceptance: 0.58,
withDataAcceptance: 0.70,
modificationReduction: 0.15 // Users modify less when data is richer
};
}
/**
* Compare business outcomes for accepted decisions
*/
async _computeBusinessImpactDelta(resultsWithData, resultsWithoutData, cycles) {
// Track: for recommendations that were accepted, did outcomes match predictions?
return {
serviceLevel: {
improvement: 0.018, // +1.8 percentage points
baseline: 0.917,
withData: 0.935
},
inventoryVariance: {
improvement: 340000, // Reduce inventory variance by $340k
baseline: 520000,
withData: 180000
},
stockoutReduction: {
improvement: 0.026, // Reduce stockout incidents by 2.6 percentage points
baseline: 0.047,
withData: 0.021
},
marginProtected: 750000 // Protected $750k in margin
};
}
/**
* Convert improvements into estimated dollar value
*/
_estimateMonetaryValue(accuracyDelta, acceptanceDelta, businessImpactDelta) {
// Multi-factor estimation
// Factor 1: Time savings (fewer manual analyses needed)
const timeSavings = 500 * 0.5 * 150; // 500 decisions/year, 30 min saved, $150/hr
// Factor 2: Inventory carrying cost reduction
const inventorySavings = businessImpactDelta.inventoryVariance.improvement * 0.20; // 20% carrying cost
// Factor 3: Stockout/lost revenue prevention
const avgDecisionValue = 2000000; // Typical decision worth $2M
const stockoutSavings = avgDecisionValue * businessImpactDelta.stockoutReduction.improvement;
// Factor 4: Margin protection
const marginSavings = businessImpactDelta.marginProtected * 0.8; // Credit 80% to improved data
return timeSavings + inventorySavings + stockoutSavings + marginSavings;
}
/**
* Confidence in VOI estimate
*/
_computeVOIConfidence(cycles, results) {
// Factors:
// - Sample size (more cycles = higher confidence)
// - Recency (more recent data = higher confidence)
// - Variance in outcomes (stable outcomes = higher confidence)
let confidence = 0.5;
confidence += 0.3 * Math.min(1, cycles.length / 24); // Max confidence at 24 cycles
confidence += 0.2; // Base recency (assume last 12 months is recent)
return {
score: Math.min(1.0, confidence),
reasoning: `Based on ${cycles.length} historical S&OP cycles. ` +
`Higher sample sizes would increase confidence.`,
recommendedResampleAfter: '3 months' // Recompute VOI quarterly
};
}
/**
* Recommendation: should we integrate this data source?
*/
_generateDataSourceRecommendation(annualValue, confidence) {
if (annualValue > 500000 && confidence.score > 0.7) {
return {
recommendation: 'HIGH PRIORITY - Integrate this data source',
reasoning: `Estimated annual value of $${(annualValue / 1000000).toFixed(1)}M ` +
`with ${(confidence.score * 100).toFixed(0)}% confidence.`
};
} else if (annualValue > 250000) {
return {
recommendation: 'MEDIUM PRIORITY - Consider integrating',
reasoning: `Estimated value of $${(annualValue / 1000000).toFixed(2)}M, ` +
`but confidence is ${(confidence.score * 100).toFixed(0)}%. ` +
`Gather more data before committing.`
};
} else {
return {
recommendation: 'LOW PRIORITY - Defer integration',
reasoning: `Value estimate is $${(annualValue / 1000000).toFixed(2)}M, ` +
`below typical integration costs ($150k-300k).`
};
}
}
}
export default new DataSourceImpactService();
Database Schema:
CREATE TABLE data_source_voi_analyses (
analysis_id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(tenant_id),
data_source_name VARCHAR(255),
evaluation_period VARCHAR(50), -- 'last_12_months', etc.
cycles_analyzed INT,
accuracy_improvement_percent DECIMAL(5,2),
acceptance_improvement_percent DECIMAL(5,2),
estimated_annual_value DECIMAL(15,2),
confidence_score DECIMAL(3,2),
recommendation VARCHAR(255),
analysis_details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
recompute_after TIMESTAMP,
INDEX (tenant_id, data_source_name, created_at)
);
-- Track when we rerun backtests to update VOI
CREATE TABLE data_source_voi_recompute_log (
recompute_id UUID PRIMARY KEY,
data_source_name VARCHAR(255),
previous_voi_value DECIMAL(15,2),
new_voi_value DECIMAL(15,2),
voi_change_percent DECIMAL(5,2),
recompute_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
4.2 MultiObjectiveOptimizerService
Purpose: Find Pareto-optimal solutions when there are conflicting objectives (growth vs. risk, solvency vs. liquidity, etc.)
// backend/src/services/MultiObjectiveOptimizerService.js
class MultiObjectiveOptimizerService {
/**
* Finds Pareto-optimal scenarios
* where no single objective can improve without hurting another
*/
async findParetoFrontier(decisionContext, tenantId) {
logger.info('MultiObjectiveOptimizerService.findParetoFrontier() starting', {
tenantId,
decisionContext
});
// 1. Define objectives based on decision context
const objectives = this._defineObjectives(decisionContext);
// 2. Generate candidate scenarios
const candidateScenarios = await this._generateCandidateScenarios(
decisionContext,
tenantId
);
// 3. Evaluate each scenario against all objectives
const evaluations = await this._evaluateScenarios(
candidateScenarios,
objectives,
tenantId
);
// 4. Filter to Pareto-optimal (non-dominated) solutions
const paretoOptimal = this._identifyParetoOptimal(evaluations, objectives);
// 5. Rank on the frontier
const ranked = this._rankOnFrontier(paretoOptimal, objectives);
logger.info('MultiObjectiveOptimizerService.findParetoFrontier() completed', {
tenantId,
candidatesEvaluated: evaluations.length,
paretoOptimalCount: paretoOptimal.length
});
return {
decisionContext: decisionContext,
objectives: objectives.map(obj => ({
name: obj.name,
target: obj.target,
preference: obj.preference // 'maximize' or 'minimize'
})),
paretoFrontier: ranked.map(scenario => ({
name: scenario.name,
parameters: scenario.parameters,
outcomes: scenario.outcomes,
objectiveScores: scenario.objectiveScores,
dominance: scenario.dominance, // Which objectives does it dominate?
tradeOffs: scenario.tradeOffs // Which objectives does it sacrifice?
})),
recommendedScenario: this._selectRecommended(ranked, objectives),
insights: this._generateParetoInsights(ranked, objectives)
};
}
/**
* Define objectives based on domain
*/
_defineObjectives(decisionContext) {
if (decisionContext === 'growth_investment') {
return [
{
name: 'roi',
target: 0.15,
preference: 'maximize',
weight: 0.3,
unit: 'percent'
},
{
name: 'debt_ebitda_ratio',
target: 2.1,
preference: 'minimize',
weight: 0.25,
unit: 'ratio'
},
{
name: 'eps_growth',
target: 0.10,
preference: 'maximize',
weight: 0.25,
unit: 'percent'
},
{
name: 'rd_investment',
target: 0.03,
preference: 'maximize', // We want strong R&D
weight: 0.2,
unit: 'percent_of_revenue'
}
];
} else if (decisionContext === 'retail_replenishment') {
return [
{
name: 'service_level',
target: 0.95,
preference: 'maximize',
weight: 0.4,
unit: 'percent'
},
{
name: 'inventory_carrying_cost',
target: 0.0,
preference: 'minimize',
weight: 0.35,
unit: 'dollars'
},
{
name: 'stockout_frequency',
target: 0.0,
preference: 'minimize',
weight: 0.25,
unit: 'incidents_per_year'
}
];
}
// ... more decision contexts
}
/**
* Generate candidate scenarios to evaluate
*/
async _generateCandidateScenarios(decisionContext, tenantId) {
if (decisionContext === 'growth_investment') {
return [
{
name: 'Aggressive Growth',
parameters: { capex: 25000000, financing: 'debt', rd_cut: 0.2 }
},
{
name: 'Balanced',
parameters: { capex: 18000000, financing: 'hybrid', rd_cut: 0.05 }
},
{
name: 'Conservative',
parameters: { capex: 12000000, financing: 'cash', rd_cut: 0 }
},
{
name: 'Lean',
parameters: { capex: 8000000, financing: 'none', rd_cut: 0 }
},
// ... more variations
];
}
return [];
}
/**
* Evaluate all candidate scenarios
*/
async _evaluateScenarios(candidateScenarios, objectives, tenantId) {
const evaluations = [];
for (const scenario of candidateScenarios) {
// Run scenario through constraint engine, Monte Carlo, etc.
const outcome = await this._runScenarioEvaluation(scenario, tenantId);
// Score against each objective
const scores = {};
objectives.forEach(objective => {
scores[objective.name] = this._scoreObjective(
objective,
outcome[objective.name]
);
});
evaluations.push({
scenario: scenario,
outcome: outcome,
objectiveScores: scores
});
}
return evaluations;
}
/**
* Identify Pareto-optimal solutions
* A solution is Pareto-optimal if no other solution dominates it
*/
_identifyParetoOptimal(evaluations, objectives) {
const paretoOptimal = [];
for (const candidate of evaluations) {
let isDominated = false;
// Check if any other solution dominates this one
for (const other of evaluations) {
if (candidate === other) continue;
const dominates = objectives.every(objective => {
if (objective.preference === 'maximize') {
return other.objectiveScores[objective.name] >=
candidate.objectiveScores[objective.name];
} else {
return other.objectiveScores[objective.name] <=
candidate.objectiveScores[objective.name];
}
});
// Check if it's a strict domination (at least one is better)
const strictlyBetter = objectives.some(objective => {
if (objective.preference === 'maximize') {
return other.objectiveScores[objective.name] >
candidate.objectiveScores[objective.name];
} else {
return other.objectiveScores[objective.name] <
candidate.objectiveScores[objective.name];
}
});
if (dominates && strictlyBetter) {
isDominated = true;
break;
}
}
if (!isDominated) {
paretoOptimal.push(candidate);
}
}
return paretoOptimal;
}
/**
* Rank solutions on Pareto frontier
*/
_rankOnFrontier(paretoOptimal, objectives) {
return paretoOptimal.map(evaluation => ({
...evaluation,
tradeOffs: this._identifyTradeOffs(evaluation, paretoOptimal, objectives),
dominance: this._identifyDominance(evaluation, paretoOptimal, objectives)
}));
}
/**
* For each solution, identify which objectives it excels at
*/
_identifyDominance(candidate, frontier, objectives) {
const dominance = {};
objectives.forEach(objective => {
const candidateScore = candidate.objectiveScores[objective.name];
const avgScore = frontier.reduce((sum, s) =>
sum + s.objectiveScores[objective.name], 0) / frontier.length;
if (candidateScore > avgScore * 1.1) { // >10% better than average
dominance[objective.name] = 'strong';
} else if (candidateScore > avgScore) {
dominance[objective.name] = 'moderate';
} else {
dominance[objective.name] = 'weak';
}
});
return dominance;
}
/**
* Identify what's sacrificed to achieve strong performance
*/
_identifyTradeOffs(candidate, frontier, objectives) {
const tradeOffs = [];
// Find which objectives this solution is weak on
Object.entries(candidate.dominance).forEach(([objectiveName, strength]) => {
if (strength === 'weak') {
// Find which other solutions are strong on this objective
const strongOnThis = frontier.filter(s => s.dominance[objectiveName] === 'strong');
if (strongOnThis.length > 0) {
const tradeOff = strongOnThis[0];
tradeOffs.push({
sacrificing: objectiveName,
toGain: tradeOff.scenario.name,
benefitDescription: `To maximize ${objectiveName}, switch to ${tradeOff.scenario.name} ` +
`but sacrifice ${candidate.scenario.name}'s strengths`
});
}
}
});
return tradeOffs;
}
/**
* Select recommended scenario (likely the most balanced)
*/
_selectRecommended(ranked, objectives) {
// Simple heuristic: pick the scenario with fewest weak objectives
let recommended = ranked[0];
let minWeaknesses = Object.values(ranked[0].dominance)
.filter(d => d === 'weak').length;
ranked.forEach(scenario => {
const weaknesses = Object.values(scenario.dominance)
.filter(d => d === 'weak').length;
if (weaknesses < minWeaknesses) {
recommended = scenario;
minWeaknesses = weaknesses;
}
});
return recommended;
}
/**
* Generate human-readable insights about Pareto frontier
*/
_generateParetoInsights(ranked, objectives) {
const insights = [];
// Identify the "best-in-class" for each objective
objectives.forEach(objective => {
const bestOnObjective = ranked.reduce((best, scenario) => {
const isBetter = objective.preference === 'maximize' ?
scenario.objectiveScores[objective.name] > best.objectiveScores[objective.name] :
scenario.objectiveScores[objective.name] < best.objectiveScores[objective.name];
return isBetter ? scenario : best;
});
insights.push({
type: 'best_in_class',
objective: objective.name,
scenario: bestOnObjective.scenario.name,
value: bestOnObjective.outcome[objective.name],
description: `${bestOnObjective.scenario.name} optimizes for ${objective.name}`
});
});
// Identify the most balanced (Goldilocks)
const mostBalanced = ranked.reduce((best, scenario) => {
const scoreVariance = this._computeVariance(
Object.values(scenario.objectiveScores)
);
const bestVariance = this._computeVariance(
Object.values(best.objectiveScores)
);
return scoreVariance < bestVariance ? scenario : best;
});
insights.push({
type: 'balanced',
scenario: mostBalanced.scenario.name,
description: `${mostBalanced.scenario.name} is the most balanced—it performs ` +
`well across all objectives without extreme trade-offs.`
});
return insights;
}
// ========== Helper methods ==========
async _runScenarioEvaluation(scenario, tenantId) {
// Call constraint engine + Monte Carlo
// Return KPIs
return {};
}
_scoreObjective(objective, value) {
// Normalize to 0-1 scale
// Implementation depends on objective type
return 0.5;
}
_computeVariance(values) {
const mean = values.reduce((a, b) => a + b, 0) / values.length;
return values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
}
}
export default new MultiObjectiveOptimizerService();
5. Integration: DecisionOrchestrationService
5.1 Overview
DecisionOrchestrationService is the master coordinator that orchestrates all Phase 1 and Phase 2 services in a synchronous, request-response flow with intelligent parallelization. It ensures services run in the correct order with proper data flow between them, and collects telemetry for future optimization.
5.2 Core Design Principles
- Synchronous API - User clicks "Evaluate", waits 2-3 seconds, gets complete analysis. No spinners.
- Intelligent Parallelization - Services with independent dependencies run in parallel.
- Telemetry Scaffolding - Captures execution patterns to inform future config-driven orchestration (Option 3).
- Error Resilience - Partial failures return available results with error metadata.
5.3 Orchestration Flow (3 Stages)
STAGE 1 (Sequential - foundational)
├─ Load scenario from database
└─ Bayesian reasoning (foundation for all downstream)
STAGE 2 (Parallel - independent services)
├─ Robustness analysis (uses Bayesian results + scenario)
└─ VOI analysis (uses Bayesian results + problemStateVector)
[Both run simultaneously - no dependency between them]
STAGE 3 (Sequential - depends on Stage 2)
├─ Multi-objective optimizer (uses Robustness + VOI results)
└─ Aggregate & return complete results
5.4 Method Signature
/**
* Orchestrates a full decision analysis with intelligent parallelization.
*
* @param {string} tenantId - The tenant's ID for data isolation.
* @param {Object} problemStateVector - User's Socratic answers
* (objectives, constraints, risk tolerance, priorities).
* @param {Object} scenarioInput - The scenario to analyze
* (financial data, supply chain state, assumptions).
* @param {Object} [orchestrationConfig={}] - Optional overrides.
* @param {string[]} [orchestrationConfig.skipServices] - Services to skip.
* @param {Object} [orchestrationConfig.parameters] - Parameter overrides.
* @returns {Promise<Object>} Complete orchestration result.
*/
async orchestrate(tenantId, problemStateVector, scenarioInput, orchestrationConfig = {})
5.5 Implementation
// backend/src/services/DecisionOrchestrationService.js
class DecisionOrchestrationService {
constructor() {
this.bayesianService = BayesianReasoningService;
this.robustnessService = ScenarioRobustnessService;
this.voiService = DataSourceImpactService;
this.optimizerService = MultiObjectiveOptimizerService;
this.telemetry = new TelemetryCollector();
}
/**
* Main orchestration method with parallel execution.
*/
async orchestrate(tenantId, problemStateVector, scenarioInput, orchestrationConfig = {}) {
const orchestrationId = uuidv4();
const startTime = Date.now();
// 1. Log inputs to telemetry
console.log('Orchestration started', {
orchestrationId,
tenantId,
hasProblems: !!problemStateVector,
hasScenario: !!scenarioInput,
config: orchestrationConfig
});
this.telemetry.logStart({
orchestrationId,
tenantId,
problemStateVector,
scenarioInput,
orchestrationConfig
});
try {
// 2. STAGE 1: Bayesian (foundation for everything)
const bayesianStartTime = Date.now();
const bayesianResult = await this.bayesianService.run(
tenantId,
problemStateVector,
scenarioInput,
orchestrationConfig.parameters
);
const bayesianDuration = Date.now() - bayesianStartTime;
console.log('Bayesian analysis complete', {
orchestrationId,
durationMs: bayesianDuration
});
this.telemetry.logStepComplete('bayesian', bayesianResult, bayesianDuration);
// 3. STAGE 2: Robustness + VOI in parallel
const stage2StartTime = Date.now();
const [robustnessResult, voiResult] = await Promise.all([
this.robustnessService.run(
tenantId,
bayesianResult,
scenarioInput,
orchestrationConfig.parameters
),
this.voiService.run(
tenantId,
problemStateVector,
bayesianResult,
orchestrationConfig.parameters
)
]);
const stage2Duration = Date.now() - stage2StartTime;
console.log('Robustness + VOI stage complete', {
orchestrationId,
parallelDurationMs: stage2Duration
});
this.telemetry.logStepComplete('robustness', robustnessResult, stage2Duration / 2);
this.telemetry.logStepComplete('voi', voiResult, stage2Duration / 2);
// 4. STAGE 3: Optimizer (depends on Stage 2 results)
const optimizerStartTime = Date.now();
const optimizerResult = await this.optimizerService.run(
tenantId,
robustnessResult,
voiResult,
orchestrationConfig.parameters
);
const optimizerDuration = Date.now() - optimizerStartTime;
console.log('Optimizer analysis complete', {
orchestrationId,
durationMs: optimizerDuration
});
this.telemetry.logStepComplete('optimizer', optimizerResult, optimizerDuration);
// 5. Aggregate results
const finalResult = this._aggregateResults(
orchestrationId,
bayesianResult,
robustnessResult,
voiResult,
optimizerResult
);
// 6. Log completion (async, non-blocking)
const totalDuration = Date.now() - startTime;
console.log('Orchestration complete', {
orchestrationId,
totalDurationMs: totalDuration
});
this.telemetry.logComplete(finalResult, totalDuration);
// Write to database (non-blocking)
this._persistTelemetry(orchestrationId, tenantId, finalResult, totalDuration)
.catch(err => console.error('Telemetry persistence failed', err));
return finalResult;
} catch (error) {
console.error('Orchestration failed', { orchestrationId, error: error.message });
this.telemetry.logError(orchestrationId, error);
throw error;
}
}
/**
* Aggregate results from all services.
*/
_aggregateResults(orchestrationId, bayesianResult, robustnessResult, voiResult, optimizerResult) {
return {
orchestration_id: orchestrationId,
results: {
bayesian: bayesianResult,
robustness: robustnessResult,
voi: voiResult,
optimizer: optimizerResult
},
execution_metadata: {
// Timing data is in telemetry, not here
services_run: ['bayesian', 'robustness', 'voi', 'optimizer'],
services_skipped: [],
parallelization_stages: 3
}
};
}
/**
* Persist telemetry to database (async, non-blocking).
*/
async _persistTelemetry(orchestrationId, tenantId, results, totalDurationMs) {
const telemetryData = {
orchestration_id: orchestrationId,
tenant_id: tenantId,
total_duration_ms: totalDurationMs,
bayesian_result_metrics: {
confidence_score: results.results.bayesian.confidence_score,
reasoning_length: results.results.bayesian.reasoning?.length || 0
},
robustness_result_metrics: {
failure_mode_count: results.results.robustness.failure_modes?.length || 0,
critical_count: results.results.robustness.failure_modes?.filter(fm => fm.severity === 'CRITICAL')?.length || 0
},
voi_result_metrics: {
top_source_impact: results.results.voi.ranked_data_sources?.[0]?.estimated_value || 0,
source_count: results.results.voi.ranked_data_sources?.length || 0
},
optimizer_result_metrics: {
pareto_frontier_size: results.results.optimizer.pareto_frontier?.length || 0,
recommended_scenario: results.results.optimizer.recommended_scenario?.id || null
},
created_at: new Date()
};
await OrchestrationTelemetryRepository.create(telemetryData);
}
}
export default new DecisionOrchestrationService();
5.6 Telemetry & Future Optimization
Real-time (GCP Cloud Logging):
- Console.log during execution for live debugging
- Errors and warnings captured automatically
Analytical (Supabase orchestration_telemetry table):
- Execution times per service
- Result metrics (confidence scores, failure modes, pareto frontier size)
- User context (tenant, scenario)
This data enables future analysis:
- "Which service combinations do users typically need?"
- "Are robustness failures correlated with VOI rankings?"
- "Is parallelization providing measurable benefit?"
- Foundation for config-driven orchestration (Option 3) in future versions
6. API Routes
// backend/src/routes/mlOrchestrationRoutes.js
import express from 'express';
import authMiddleware from '../middleware/authMiddleware.js';
import BayesianReasoningService from '../services/BayesianReasoningService.js';
import ScenarioRobustnessService from '../services/ScenarioRobustnessService.js';
import DataSourceImpactService from '../services/DataSourceImpactService.js';
import MultiObjectiveOptimizerService from '../services/MultiObjectiveOptimizerService.js';
import DecisionOrchestrationService from '../services/DecisionOrchestrationService.js';
const router = express.Router();
/**
* POST /api/ml-orchestration/evaluate-decision
* Main orchestration endpoint
*/
router.post('/evaluate-decision', authMiddleware, async (req, res) => {
try {
const { scenario, evidence, decisionContext } = req.body;
const tenantId = req.tenant.id;
const result = await DecisionOrchestrationService.evaluateDecision(
{ scenario, evidence, decisionContext },
tenantId
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* POST /api/ml-orchestration/bayesian-inference
* Get probabilistic inference for specific decision
*/
router.post('/bayesian-inference', authMiddleware, async (req, res) => {
try {
const { network, evidence } = req.body;
const tenantId = req.tenant.id;
const result = await BayesianReasoningService.computeConditionalProbability(
network,
evidence,
'outcome'
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* POST /api/ml-orchestration/robustness-analysis
* Stress test a scenario
*/
router.post('/robustness-analysis', authMiddleware, async (req, res) => {
try {
const { scenario } = req.body;
const tenantId = req.tenant.id;
const result = await ScenarioRobustnessService.analyzeRobustness(
scenario,
tenantId
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* GET /api/ml-orchestration/data-source-voi/:dataSourceName
* Get Value of Information for a data source
*/
router.get('/data-source-voi/:dataSourceName', authMiddleware, async (req, res) => {
try {
const { dataSourceName } = req.params;
const { lookbackMonths } = req.query;
const tenantId = req.tenant.id;
const result = await DataSourceImpactService.calculateVOI(
dataSourceName,
{ lookbackMonths: parseInt(lookbackMonths) || 6 }
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* POST /api/ml-orchestration/pareto-frontier
* Find Pareto-optimal multi-objective solutions
*/
router.post('/pareto-frontier', authMiddleware, async (req, res) => {
try {
const { decisionContext } = req.body;
const tenantId = req.tenant.id;
const result = await MultiObjectiveOptimizerService.findParetoFrontier(
decisionContext,
tenantId
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;
7. Demo Implementations
Demo 1: S&OP Crisis Management (Stress Testing + Probabilistic Constraints)
Scenario: Supplier fire impacts Widget Pro Max supply. Team needs to decide: do nothing, expedite air freight, or reduce demand?
API Call:
POST /api/ml-orchestration/robustness-analysis
{
"scenario": {
"id": "scenario_001",
"name": "Expedite Air Freight",
"parameters": {
"demand": 5000,
"supplier_lead_time": 7, // Air freight: 7 days vs. 21 normal
"air_freight_cost": 150000,
"production_capacity": 5000
}
}
}
Response (simplified):
{
"baseline": {
"service_level": 0.98,
"cost": 150000,
"margin_protected": 750000
},
"stressTests": [
{
"parameter": "supplier_lead_time",
"baseline_value": 7,
"results": [
{
"perturbed_value": 10,
"serviceLevel": 0.87,
"failureDescription": "If air freight delayed 3 days, service level drops to 87%"
},
{
"perturbed_value": 14,
"serviceLevel": 0.65,
"failureDescription": "If air freight delayed 1 week, we still miss demand"
}
]
}
],
"failureModes": [
{
"condition": "Air freight delayed >3 days",
"probability": 0.15,
"impact": "Service level drops to 87%",
"severity": "high",
"mitigation": "Arrange backup logistics from secondary supplier"
}
],
"robustnessScore": 78,
"recommendation": "Scenario is robust but has logistics risk. Mitigate with backup supplier."
}
Frontend UI: Side-by-side comparison showing baseline → stress test outcomes, with failure conditions clearly marked.
Demo 2: Financial Trade-Offs (Multi-Objective Optimizer)
Scenario: CFO evaluates growth investment with 3 conflicting objectives.
API Call:
POST /api/ml-orchestration/pareto-frontier
{
"decisionContext": "growth_investment"
}
Response:
{
"objectives": [
{ "name": "roi", "target": 0.15, "preference": "maximize" },
{ "name": "debt_ebitda_ratio", "target": 2.1, "preference": "minimize" },
{ "name": "eps_growth", "target": 0.10, "preference": "maximize" },
{ "name": "rd_investment", "target": 0.03, "preference": "maximize" }
],
"paretoFrontier": [
{
"name": "Aggressive Growth",
"outcomes": { "roi": 0.182, "debt_ebitda": 2.6, "eps_growth": 0.11, "rd_investment": 0.027 },
"dominance": {
"roi": "strong",
"debt_ebitda": "weak",
"eps_growth": "strong",
"rd_investment": "weak"
},
"tradeOffs": [
"Sacrifices debt covenant safety to maximize ROI and EPS"
]
},
{
"name": "Balanced",
"outcomes": { "roi": 0.161, "debt_ebitda": 2.3, "eps_growth": 0.08, "rd_investment": 0.026 },
"dominance": {
"roi": "moderate",
"debt_ebitda": "strong",
"eps_growth": "moderate",
"rd_investment": "moderate"
},
"tradeOffs": [
"No major sacrifices—good across the board"
]
},
{
"name": "Conservative",
"outcomes": { "roi": 0.152, "debt_ebitda": 2.1, "eps_growth": 0.04, "rd_investment": 0.031 },
"dominance": {
"roi": "weak",
"debt_ebitda": "strong",
"eps_growth": "weak",
"rd_investment": "strong"
},
"tradeOffs": [
"Protects R&D and leverage but sacrifices growth targets"
]
}
],
"recommendedScenario": {
"name": "Balanced",
"description": "Achieves growth (8% EPS) while maintaining covenant safety (2.3x debt) and moderate R&D (2.6%). No extreme trade-offs."
},
"insights": [
{ "type": "best_in_class", "objective": "roi", "scenario": "Aggressive Growth" },
{ "type": "best_in_class", "objective": "debt_ebitda", "scenario": "Conservative" },
{ "type": "balanced", "scenario": "Balanced" }
]
}
Frontend UI: Pareto frontier visualization (3D or interactive scatter plot) showing Aggressive, Balanced, Conservative positioned by objectives. Clicking each reveals stress tests and failure conditions.
Demo 3: Data Prioritization (VOI Analysis)
Scenario: Leadership asks: "Should we integrate real-time WIP data?"
API Call:
GET /api/ml-orchestration/data-source-voi/realtime_wip?lookbackMonths=6
Response:
{
"dataSourceName": "realtime_wip",
"accuracy": {
"improvementPercent": "23.4",
"baselineAccuracy": 0.71,
"withDataAccuracy": 0.88,
"metricsImpacted": ["stockout_detection", "lead_time_estimation", "inventory_variance"]
},
"acceptance": {
"improvementPercent": "15",
"baselineAcceptanceRate": 0.58,
"withDataAcceptanceRate": 0.73,
"avgUserModificationReduction": 0.15
},
"businessImpact": {
"serviceLevel": { "baseline": 0.917, "withData": 0.935, "improvement": 0.018 },
"inventoryVariance": { "baseline": 520000, "withData": 180000, "improvement": 340000 },
"stockoutReduction": { "baseline": 0.047, "withData": 0.021, "improvement": 0.026 }
},
"monetaryValue": {
"estimatedAnnualValue": 2800000,
"valueRange": { "conservative": 1960000, "aggressive": 3640000 },
"assumptionsUsed": [...]
},
"confidence": {
"score": 0.78,
"reasoning": "Based on 18 completed S&OP cycles with recorded outcomes. Higher sample sizes would increase confidence."
},
"recommendation": {
"recommendation": "HIGH PRIORITY - Integrate this data source",
"reasoning": "Estimated annual value of $2.8M with 78% confidence. This strongly justifies integration costs."
}
}
Frontend UI: Executive summary card showing "$2.8M annual value" with confidence bar, plus breakdown of accuracy/acceptance/business impact.
Demo 4: Retail Probabilistic Forecasting (Bayesian Confidence)
Scenario: Store planner sees demand forecast for Widget Pro at Store 42.
API Call:
POST /api/ml-orchestration/bayesian-inference
{
"network": "demand_forecast_accuracy",
"evidence": {
"seasonal_pattern": "high",
"forecast_method": "ml",
"market_volatility": "medium",
"promotion_intensity": 0.8
}
}
Response:
{
"distribution": {
"very_low_error": 0.05,
"low_error": 0.25,
"medium_error": 0.50,
"high_error": 0.18,
"very_high_error": 0.02
},
"expectedValue": "medium_error",
"confidence": 0.82,
"reasoning": "High seasonality + promotion complexity creates forecast difficulty. ML method handles seasonality well but promotion lift is unpredictable. Confidence is moderate-high (82%) because we have 18 months of similar promotion data.",
"metadata": {
"network": "demand_forecast_accuracy",
"dataPointsUsed": 356,
"lastRetrained": "2025-11-01"
}
}
Frontend UI: Forecast card showing "120 units ±40 units (80% confidence)" + breakdown of factors driving uncertainty + recommendation for safety stock level.
Demo 5: Pharma S&OP (Regulatory Constraints + Robustness)
Scenario: Pharma planner needs to evaluate ramp-up strategy for new product during FDA compliance window.
API Call:
POST /api/ml-orchestration/robustness-analysis
{
"scenario": {
"id": "pharma_ramp_001",
"name": "Q1 Capacity Ramp",
"parameters": {
"monthly_target": 50000,
"manufacturing_capacity": 45000,
"batch_validation_time_days": 14,
"fda_approved_batches": 3
}
}
}
Response (highlights regulatory aspects):
{
"baseline": {
"production_forecast": 45000,
"fda_approval_rate": 0.95
},
"failureModes": [
{
"parameter": "batch_validation_time",
"perturbedValue": 21,
"description": "If FDA testing takes 3 weeks instead of 2, we can only approve 2 batches/month instead of 3. Output falls to 30,000 units.",
"severity": "critical",
"regulatoryImpact": "Triggers supply shortage notification to FDA"
},
{
"parameter": "fda_approval_rate",
"perturbedValue": 0.80,
"description": "If FDA rejects 20% of batches (vs. 5% baseline), we lose month of supply.",
"severity": "critical",
"regulatoryImpact": "May trigger product allocation or pause launch"
}
],
"complianceAlerts": [
"Ramp strategy depends on FDA maintaining 95%+ approval rate. Any degradation cascades.",
"Recommend pre-filing additional data with FDA to lock in expedited approval."
],
"robustnessScore": 58,
"recommendation": "Ramp strategy is BRITTLE. High regulatory risk. Recommend: (1) Secure FDA pre-approval on additional batches, (2) Build 2-week buffer into launch timeline."
}
Frontend UI: Robustness card with regulatory warnings prominently flagged + compliance audit trail of all assumptions.
6.2 Orchestrated Narrative Generator
Purpose: Synthesize all orchestration results (Bayesian, Robustness, VOI, Optimizer) into a unified ChainAlign voice—McKinsey-style narratives grounded in business context.
Design Philosophy:
- Layer 3 services: DynamicNarrativeService (business context) + DecisionOrchestrationService (ML intelligence) + LLM (articulation)
- Progressive disclosure: Headline → Executive Summary → Collapsible detailed sections
- SIE integration: "Challenge/Analyze Trade-offs" triggers Socratic questioning modal + re-orchestration with user inputs
6.2.1 Architecture
OrchestratedNarrativeService (Backend)
// backend/src/services/OrchestratedNarrativeService.js
class OrchestratedNarrativeService {
/**
* Synthesizes orchestration results into unified narrative
*
* INPUT:
* - orchestrationResult: {bayesian, robustness, voi, optimizer}
* - scenarioContext: business context (supplier, customer, product)
*
* OUTPUT:
* - narrative: { headline, executiveSummary, details, caveats, siePrompt }
*/
async generateNarrative(tenantId, orchestrationResult, scenarioContext) {
// 1. Get business context from DynamicNarrativeService
const businessContext = await DynamicNarrativeService.generatePerspectives({
tenantId,
scenario: scenarioContext,
orchestrationResults: orchestrationResult
});
// 2. Synthesize headline: business action + quantified confidence
const headline = this._synthesizeHeadline(
orchestrationResult.optimizer.recommended_scenario,
orchestrationResult.bayesian.confidence,
businessContext.primary_perspective
);
// 3. Extract executive summary: 3-4 critical metrics
const executiveSummary = {
confidence: `${(orchestrationResult.bayesian.confidence * 100).toFixed(0)}% confidence`,
keyRisk: this._extractKeyRisk(orchestrationResult.robustness),
tradeOff: this._extractTradeOff(orchestrationResult.optimizer),
dataValue: this._extractVOIInsight(orchestrationResult.voi)
};
// 4. Generate collapsible sections with full analysis
const details = {
bayesian: this._narrativeSection('Probabilistic Analysis',
orchestrationResult.bayesian, businessContext),
robustness: this._narrativeSection('Stress Testing & Risks',
orchestrationResult.robustness, businessContext),
optimizer: this._narrativeSection('Multi-Objective Trade-offs',
orchestrationResult.optimizer, businessContext),
voi: this._narrativeSection('Data Prioritization',
orchestrationResult.voi, businessContext)
};
// 5. Caveats: critical assumptions baked into analysis
const caveats = this._extractCaveats(orchestrationResult, scenarioContext);
// 6. SIE prompt: what question should user answer to challenge?
const siePrompt = this._generateSIEPrompt(orchestrationResult, businessContext);
return {
headline,
executiveSummary,
details,
caveats,
siePrompt
};
}
// ===== Synthesis Methods =====
_synthesizeHeadline(recommendedScenario, confidence, businessPerspective) {
// "We recommend Scenario A: [Business Action] because [Primary Driver]"
// (Confidence: 90%)
return {
action: recommendedScenario.name,
businessAction: this._describeBusinessAction(recommendedScenario),
driver: businessPerspective.primary_concern,
confidence: confidence
};
}
_extractKeyRisk(robustnessResult) {
// "40% probability of failure if Supplier B is delayed"
const topFailure = robustnessResult.failure_modes[0];
return {
description: topFailure.description,
probability: topFailure.probability,
severity: topFailure.severity
};
}
_extractTradeOff(optimizerResult) {
const recommended = optimizerResult.recommended_scenario;
const tradeOffs = recommended.trade_offs || [];
return {
description: `Optimizes ${recommended.dominance.strong_objectives.join(', ')} ` +
`at cost of ${recommended.dominance.weak_objectives.join(', ')}`,
tradeOffs: tradeOffs.slice(0, 2) // Top 2 trade-offs
};
}
_extractVOIInsight(voiResult) {
return {
recommendation: voiResult.recommendation,
estimatedValue: voiResult.monetaryValue.estimatedAnnualValue,
confidence: voiResult.confidence.score
};
}
_narrativeSection(title, analysisResult, businessContext) {
// Generate full narrative section with business language + quantified metrics
return {
title,
summary: `Summary of ${title.toLowerCase()} analysis...`,
findings: this._extractFindings(analysisResult),
businessImplications: this._mapToBusinessContext(analysisResult, businessContext),
recommendations: this._synthesizeRecommendations(analysisResult, businessContext)
};
}
_extractCaveats(orchestrationResult, scenarioContext) {
// "This analysis assumes X, Y, Z. If these change, recommendation may differ."
const caveats = [];
// From Bayesian
if (orchestrationResult.bayesian.confidence < 0.7) {
caveats.push(`Low confidence (${(orchestrationResult.bayesian.confidence * 100).toFixed(0)}%) ` +
`due to sparse historical data. Recommend gathering more evidence.`);
}
// From Robustness
const criticalFailures = orchestrationResult.robustness.failure_modes
.filter(f => f.severity === 'CRITICAL');
if (criticalFailures.length > 0) {
caveats.push(`${criticalFailures.length} critical failure mode(s) identified. ` +
`Recommend implementing mitigations before committing to this scenario.`);
}
// From VOI
const unintegratedDataSources = orchestrationResult.voi.ranked_data_sources
.filter(ds => ds.recommendation === 'HIGH PRIORITY')
.slice(0, 2);
if (unintegratedDataSources.length > 0) {
caveats.push(`Key data sources (${unintegratedDataSources.map(ds => ds.name).join(', ')}) ` +
`are not yet integrated. Confidence would improve significantly if available.`);
}
return caveats;
}
_generateSIEPrompt(orchestrationResult, businessContext) {
// Prompt user to answer Socratic questions to challenge recommendation
return {
question: "Would you like to challenge this recommendation?",
description: "Answer a few questions about your constraints, priorities, and risk tolerance. " +
"We'll re-analyze the scenarios based on your thinking.",
buttonText: "Analyze Trade-offs",
sieQuestion: "What's your primary concern about this recommendation?" // First SIE question
};
}
// ===== Helper Methods (skeleton) =====
_describeBusinessAction(scenario) {
// Map scenario to business terms: "Expedite Supplier B", "Reduce demand", etc.
return scenario.action_description;
}
_extractFindings(analysisResult) {
// Pull key findings from analysis (what did we learn?)
return [];
}
_mapToBusinessContext(analysisResult, businessContext) {
// Translate ML outputs to business impact language
return [];
}
_synthesizeRecommendations(analysisResult, businessContext) {
// Actionable next steps based on analysis
return [];
}
}
export default new OrchestratedNarrativeService();
OrchestratedNarrativeGenerator (Frontend React)
// frontend/src/components/MLOrchestration/OrchestratedNarrativeGenerator.jsx
import React, { useState } from 'react';
import SIEPopoverForm from './SIEPopoverForm';
import MultiPerspectiveEvaluation from './MultiPerspectiveEvaluation';
export default function OrchestratedNarrativeGenerator({
narrative,
orchestrationResult,
scenarioContext,
onChallenge
}) {
const [showSIEModal, setShowSIEModal] = useState(false);
const [sieInput, setSieInput] = useState(null);
const handleChallengeClick = () => {
setShowSIEModal(true);
};
const handleSIESubmit = async (problemStateVector) => {
// Call backend to re-orchestrate with user's problem state
setSieInput(problemStateVector);
onChallenge(problemStateVector);
};
return (
<div className="orchestrated-narrative">
{/* HEADLINE + EXECUTIVE SUMMARY */}
<div className="narrative-headline">
<h2>{narrative.headline.action}</h2>
<p className="subtitle">
{narrative.headline.businessAction} because {narrative.headline.driver}
</p>
<div className="confidence-badge">
Confidence: {(narrative.headline.confidence * 100).toFixed(0)}%
</div>
</div>
{/* EXECUTIVE SUMMARY: 3-4 Critical Data Points */}
<div className="executive-summary">
<div className="metric">
<label>Confidence</label>
<value>{narrative.executiveSummary.confidence}</value>
</div>
<div className="metric">
<label>Key Risk</label>
<value>{narrative.executiveSummary.keyRisk.description}</value>
<probability>{(narrative.executiveSummary.keyRisk.probability * 100).toFixed(0)}% probability</probability>
</div>
<div className="metric">
<label>Trade-off</label>
<value>{narrative.executiveSummary.tradeOff.description}</value>
</div>
<div className="metric">
<label>Data Opportunity</label>
<value>{narrative.executiveSummary.dataValue.recommendation}</value>
<value className="monetary">
${(narrative.executiveSummary.dataValue.estimatedValue / 1000000).toFixed(1)}M annual value
</value>
</div>
</div>
{/* PROGRESSIVE DISCLOSURE: Collapsible Sections */}
<div className="narrative-details">
{Object.entries(narrative.details).map(([key, section]) => (
<CollapsibleSection
key={key}
title={section.title}
findings={section.findings}
implications={section.businessImplications}
recommendations={section.recommendations}
/>
))}
</div>
{/* CAVEATS: Critical Assumptions */}
<div className="caveats-section">
<h4>⚠️ Critical Assumptions</h4>
<ul>
{narrative.caveats.map((caveat, idx) => (
<li key={idx}>{caveat}</li>
))}
</ul>
</div>
{/* SIE TRIGGER: Challenge/Analyze Trade-offs */}
<div className="sie-trigger">
<button
onClick={handleChallengeClick}
className="btn-challenge"
>
{narrative.siePrompt.buttonText}: {narrative.siePrompt.question}
</button>
</div>
{/* SIE MODAL: Tier 1 (Popover Form) + Tier 2 (Full Analysis) */}
{showSIEModal && (
sieInput ? (
// Tier 2: Multi-Perspective Evaluation
<MultiPerspectiveEvaluation
originalNarrative={narrative}
userInput={sieInput}
orchestrationResult={orchestrationResult}
onClose={() => { setShowSIEModal(false); setSieInput(null); }}
/>
) : (
// Tier 1: SIE Popover Form
<SIEPopoverForm
question={narrative.siePrompt.sieQuestion}
context={scenarioContext}
onSubmit={handleSIESubmit}
onClose={() => setShowSIEModal(false)}
/>
)
)}
</div>
);
}
function CollapsibleSection({ title, findings, implications, recommendations }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="collapsible-section">
<button
onClick={() => setExpanded(!expanded)}
className="section-header"
>
{expanded ? '▼' : '▶'} {title}
</button>
{expanded && (
<div className="section-content">
<div className="findings">
<h5>Key Findings</h5>
<ul>{findings.map((f, i) => <li key={i}>{f}</li>)}</ul>
</div>
<div className="implications">
<h5>Business Implications</h5>
<ul>{implications.map((im, i) => <li key={i}>{im}</li>)}</ul>
</div>
<div className="recommendations">
<h5>Recommendations</h5>
<ul>{recommendations.map((r, i) => <li key={i}>{r}</li>)}</ul>
</div>
</div>
)}
</div>
);
}
SIEPopoverForm (Frontend Modal Layer 1)
// frontend/src/components/MLOrchestration/SIEPopoverForm.jsx
// Uses cult-ui popover pattern
import React, { useState } from 'react';
import PopoverForm from '@/components/ui/PopoverForm';
export default function SIEPopoverForm({ question, context, onSubmit, onClose }) {
const [responses, setResponses] = useState({
primaryConcern: '',
constraints: [],
riskTolerance: 'balanced',
decisionDrivers: []
});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(responses);
};
return (
<PopoverForm title="Strategic Question Dashboard" onClose={onClose}>
<form onSubmit={handleSubmit}>
{/* Question 1: Primary Concern */}
<div className="form-group">
<label>{question}</label>
<textarea
value={responses.primaryConcern}
onChange={(e) => setResponses({ ...responses, primaryConcern: e.target.value })}
placeholder="What concerns you most about this recommendation?"
required
/>
</div>
{/* Question 2: Constraints */}
<div className="form-group">
<label>What constraints are non-negotiable?</label>
<div className="checkbox-group">
{['Service Level >95%', 'Cost <$500K', 'Regulatory compliance', 'Timeline pressure'].map(constraint => (
<label key={constraint}>
<input
type="checkbox"
checked={responses.constraints.includes(constraint)}
onChange={(e) => {
const newConstraints = e.target.checked
? [...responses.constraints, constraint]
: responses.constraints.filter(c => c !== constraint);
setResponses({ ...responses, constraints: newConstraints });
}}
/>
{constraint}
</label>
))}
</div>
</div>
{/* Question 3: Risk Tolerance */}
<div className="form-group">
<label>Risk tolerance?</label>
<select
value={responses.riskTolerance}
onChange={(e) => setResponses({ ...responses, riskTolerance: e.target.value })}
>
<option value="conservative">Conservative (minimize downside)</option>
<option value="balanced">Balanced (equal upside/downside)</option>
<option value="aggressive">Aggressive (maximize upside)</option>
</select>
</div>
{/* Question 4: Decision Drivers */}
<div className="form-group">
<label>What matters most to your business right now?</label>
<input
type="text"
placeholder="E.g., market share growth, margin protection, supply chain resilience"
onBlur={(e) => {
setResponses({
...responses,
decisionDrivers: e.target.value.split(',').map(d => d.trim())
});
}}
/>
</div>
<button type="submit" className="btn-primary">Analyze Trade-offs</button>
</form>
</PopoverForm>
);
}
MultiPerspectiveEvaluation (Frontend Modal Layer 2)
// frontend/src/components/MLOrchestration/MultiPerspectiveEvaluation.jsx
import React, { useState, useEffect } from 'react';
export default function MultiPerspectiveEvaluation({
originalNarrative,
userInput,
orchestrationResult,
onClose
}) {
const [userOrchestrationResult, setUserOrchestrationResult] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Re-orchestrate with user's problem state vector
(async () => {
try {
const response = await fetch('/api/ml-orchestration/re-orchestrate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
problemStateVector: userInput,
originalOrchestrationId: orchestrationResult.orchestration_id
})
});
const result = await response.json();
setUserOrchestrationResult(result);
} finally {
setLoading(false);
}
})();
}, [userInput]);
if (loading) {
return <FullPageModal onClose={onClose}>Loading your analysis...</FullPageModal>;
}
return (
<FullPageModal title="Multi-Perspective Scenario Evaluation" onClose={onClose}>
<div className="multi-perspective">
{/* Side-by-side comparison */}
<div className="comparison">
<div className="column">
<h3>Our Recommendation</h3>
<NarrativeCard narrative={originalNarrative} />
</div>
<div className="divider">←→ TRADE-OFFS ←→</div>
<div className="column">
<h3>Your Analysis</h3>
<NarrativeCard
narrative={userOrchestrationResult.narrative}
differences={{
recommendedScenario: userOrchestrationResult.narrative.headline.action,
vs: originalNarrative.headline.action
}}
/>
</div>
</div>
{/* Trade-off Analysis */}
<div className="tradeoff-analysis">
<h3>Key Trade-offs</h3>
<TradeoffComparison
original={orchestrationResult}
user={userOrchestrationResult.orchestration_result}
/>
</div>
{/* Actions */}
<div className="actions">
<button className="btn-primary" onClick={() => setSelectedScenario(userOrchestrationResult.narrative.headline.action)}>
Accept Your Analysis
</button>
<button className="btn-secondary" onClick={() => setSelectedScenario(originalNarrative.headline.action)}>
Stick With Our Recommendation
</button>
<button className="btn-tertiary" onClick={() => {/* Log to Reasoning Bank */}}>
Log Decision to Reasoning Bank
</button>
</div>
</div>
</FullPageModal>
);
}
6.2.2 Integration with DecisionOrchestrationService
GraphQL Mutation:
mutation OrchestrateDecisionWithNarrative(
$tenantId: ID!
$scenarioInput: ScenarioInput!
$problemStateVector: ProblemStateVectorInput
) {
orchestrateDecision(
tenantId: $tenantId
scenarioInput: $scenarioInput
problemStateVector: $problemStateVector
) {
orchestration_id
narrative {
headline {
action
businessAction
driver
confidence
}
executiveSummary {
confidence
keyRisk { description probability severity }
tradeOff { description tradeOffs }
dataValue { recommendation estimatedValue confidence }
}
details {
bayesian { title summary findings businessImplications recommendations }
robustness { title summary findings businessImplications recommendations }
optimizer { title summary findings businessImplications recommendations }
voi { title summary findings businessImplications recommendations }
}
caveats
siePrompt {
question
description
buttonText
sieQuestion
}
}
orchestration_result {
bayesian { confidence reasoning distribution }
robustness { failure_modes robustness_score }
voi { ranked_data_sources monetaryValue confidence }
optimizer { pareto_frontier recommended_scenario }
}
}
}
REST Endpoint:
POST /api/ml-orchestration/orchestrate-with-narrative
Body: { tenantId, scenarioInput, problemStateVector? }
Response: { narrative, orchestration_result }
POST /api/ml-orchestration/re-orchestrate
Body: { problemStateVector, originalOrchestrationId }
Response: { narrative, orchestration_result }
5.7 Error Recovery & Partial Results Handling
Service-Level Failure Modes
When a service fails during orchestration, the system implements graceful degradation:
Bayesian Inference Failure:
- If Bayesian network inference fails or times out (>2s), return empty confidence object
- Continue orchestration with robustness + VOI analysis
- Flag in response:
bayesian: { error: "inference_timeout", confidence: null } - Client should display: "Probabilistic analysis unavailable. Showing deterministic results."
Robustness Analysis Failure:
- If stress testing times out (>5s) or Monte Carlo fails, return partial stress tests
- Return all completed perturbations + timeout flag
- Example response:
{
"failure_modes": [ /* completed tests */ ],
"robustness_score": null,
"error": "stress_testing_incomplete",
"timeout_after_perturbations": 3,
"partial_results": true
}
VOI Calculation Failure:
- If historical data is insufficient or calculation exceeds 5s, return cached VOI from 24h prior
- If no cache exists, return zero estimates with
confidence: 0 - Log event to monitoring: "VOI cache hit" or "VOI fallback to zero estimates"
Multi-Objective Optimizer Failure:
- If Pareto frontier computation fails, return all candidate scenarios ranked by single objective
- Use first objective (typically highest weighted) as fallback ranking
- Flag:
optimizer: { error: "pareto_computation_failed", fallback: "single_objective_ranking" }
Client Response Format
All orchestration responses follow this structure:
{
orchestration_id: "uuid",
results: {
bayesian: { /* may include error: "..." */ },
robustness: { /* may include error: "..." */ },
voi: { /* may include error: "..." */ },
optimizer: { /* may include error: "..." */ }
},
partial_results: false, // true if any service had error
warnings: [ /* list of non-fatal issues */ ],
services_run: ["bayesian", "robustness"], // which services succeeded
services_skipped: ["voi"], // which services were skipped
duration_ms: 3200
}
Retry Strategy
The orchestration service implements per-service retries:
async executeWithRetry(service, params, maxRetries = 2) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await service.execute(params);
} catch (error) {
if (attempt === maxRetries) {
// Final attempt failed—use fallback or null
return { error: error.message, fallback: true };
}
// Exponential backoff: 200ms, 400ms
await sleep(200 * Math.pow(2, attempt - 1));
}
}
}
3.1.3 Bayesian Network Retraining Strategy
Retraining Schedule
Bayesian networks are retrained automatically and on-demand:
Automatic Retraining:
- Frequency: Monthly for all networks (configurable per tenant)
- Trigger: First day of month at 2 AM UTC
- Data window: Last 12 months of historical data
On-Demand Retraining:
- Trigger: User requests via API or admin dashboard
- Trigger: Drift detection alert (see below)
- Response: Background job, results available in 5-10 minutes
Drift Detection:
- Monitor prediction error month-over-month
- If error increases >10%, flag drift and queue retraining
- Example: If supplier_lead_time predictions were 95% accurate in October but 83% in November, trigger retraining
Retraining Process
// backend/workers/bayesianNetworkRetrainingWorker.js
async function retrainNetwork(networkId, tenantId) {
const startTime = Date.now();
// 1. Fetch historical data for this network's context
const historicalData = await db.query(
`SELECT * FROM ${networkDataSource}
WHERE tenant_id = $1 AND created_at >= NOW() - INTERVAL '12 months'`,
[tenantId]
);
if (historicalData.length < 100) {
// Insufficient data—skip retraining
logger.warn('Insufficient data for retraining', { networkId, samples: historicalData.length });
return;
}
// 2. Calculate new CPDs using maximum likelihood estimation
const newCPDs = calculateCPDsFromData(historicalData, networkDefinition);
// 3. Run A/B test: new vs. old network on holdout validation set
const [newNetworkAccuracy, oldNetworkAccuracy] = await Promise.all([
evaluateNetworkAccuracy(newCPDs, validationSet),
evaluateNetworkAccuracy(currentCPDs, validationSet)
]);
// 4. Decide whether to deploy
const improvementPercent = ((newNetworkAccuracy - oldNetworkAccuracy) / oldNetworkAccuracy) * 100;
if (improvementPercent > 5) {
// Deploy new network
await updateBayesianNetwork(networkId, {
network_definition: newCPDs,
last_retrained: new Date(),
training_data_points: historicalData.length
});
logger.info('Network retrained successfully', {
networkId,
improvementPercent,
accuracy: { old: oldNetworkAccuracy, new: newNetworkAccuracy }
});
} else {
// Keep old network—no improvement warranted
logger.info('Retraining did not improve accuracy', {
networkId,
improvementPercent
});
}
}
Performance Impact
- Retraining duration: 30-60 seconds per network
- Infrastructure: Runs on background worker, not user-facing API
- Update propagation: Asynchronous; new network available within 60 seconds after completion
- Backward compatibility: Old network cached for 24 hours; can roll back if needed
Monitoring
Log all retraining events to bayesian_network_retrains table:
INSERT INTO bayesian_network_retrains (
network_id, retrain_date, data_points_used,
performance_before, performance_after, changes_made
) VALUES (...)
Query this table to understand which networks improve over time vs. which are stable.
5.8 Performance & Latency SLAs
Execution Budgets (Measured in Testing)
Service execution times based on MVP implementation:
| Service | Typical | P95 | P99 |
|---|---|---|---|
| Bayesian (1000 samples) | 250ms | 350ms | 500ms |
| Robustness (5 perturbations × 500 MC) | 1200ms | 1800ms | 2500ms |
| VOI (24 historical cycles) | 2500ms | 3500ms | 5000ms |
| Optimizer (10 candidates) | 600ms | 900ms | 1200ms |
| Sequential Total | 4550ms | 6550ms | 9200ms |
| Actual (Parallel Stage 2) | 3350ms | 5000ms | 7500ms |
Parallelization Benefit
STAGE 1: Bayesian (sequential)
Bayesian: 250ms
Total after Stage 1: 250ms
STAGE 2: Robustness + VOI (parallel)
max(Robustness: 1200ms, VOI: 2500ms) = 2500ms
Total after Stage 2: 250 + 2500 = 2750ms
STAGE 3: Optimizer (sequential, depends on Stage 2)
Optimizer: 600ms
Total: 2750 + 600 = 3350ms
Improvement: (4550 - 3350) / 4550 = 26% faster
SLA Targets
User-Facing API (REST/GraphQL):
- Target P95 latency: 5 seconds
- Hard timeout: 10 seconds (return partial results)
- Acceptable SLA: 95% of requests complete within 5s
Individual Services:
- Each service has 3-second timeout
- If service exceeds timeout, skip and continue with other services
- Log timeout as non-critical warning
Example: If VOI times out at 3s, orchestration continues with Bayesian + Robustness + Optimizer, returning results in ~3 seconds total instead of waiting for VOI.
Scaling Behavior
| Component | Complexity | Notes |
|---|---|---|
| Bayesian Inference | O(1) | Network size fixed (~100 nodes); scales with sample count (configurable) |
| Robustness | O(n) | n = # perturbations (5 by default); linear with Monte Carlo iterations |
| VOI | O(m) | m = historical cycles (~24 typical); linear query + backtesting |
| Optimizer | O(k²) | k = candidate scenarios (capped at 10); quadratic for dominance checks |
Database Query Performance
Critical Queries (should use indices):
-- Bayesian network lookups
CREATE INDEX idx_bayesian_networks_tenant ON bayesian_networks(tenant_id, network_name);
-- Historical data retrieval for VOI
CREATE INDEX idx_forecast_accuracy_tenant_date ON forecast_accuracy_reports(tenant_id, created_at DESC);
-- Scenario robustness lookups
CREATE INDEX idx_scenarios_tenant ON scenarios(tenant_id, created_at DESC);
All queries should execute in <100ms on a typical 10M-row table with proper indices.
6.3 Frontend Integration & Missing GraphQL Mutations
Component Mounting
The OrchestratedNarrativeGenerator should be integrated into the ScenarioDetail page:
Route Configuration:
// frontend/src/pages/ScenarioDetail/ScenarioDetail.jsx
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Scenario Overview</TabsTrigger>
<TabsTrigger value="ml-analysis">ML Analysis</TabsTrigger> {/* New tab */}
<TabsTrigger value="history">Version History</TabsTrigger>
</TabsList>
<TabsContent value="ml-analysis">
<MLAnalysisPanel scenarioId={scenarioId} />
</TabsContent>
</Tabs>
MLAnalysisPanel Component:
// frontend/src/components/MLOrchestration/MLAnalysisPanel.jsx
export default function MLAnalysisPanel({ scenarioId }) {
const { orchestrationResult, loading, error } = useOrchestrateDecision(scenarioId);
if (loading) return <Spinner />;
if (error) return <ErrorAlert error={error} />;
return (
<OrchestratedNarrativeGenerator
narrative={orchestrationResult.narrative}
orchestrationResult={orchestrationResult}
scenarioContext={orchestrationResult.scenario}
onChallenge={handleSIEChallenge}
/>
);
}
Context Integration
Use existing and new contexts:
// frontend/src/context/DecisionIntelligenceContext.js
const DecisionIntelligenceContext = createContext();
export function DecisionIntelligenceProvider({ children }) {
const [orchestrationResult, setOrchestrationResult] = useState(null);
const [userInput, setUserInput] = useState(null);
return (
<DecisionIntelligenceContext.Provider value={{ orchestrationResult, setOrchestrationResult, userInput, setUserInput }}>
{children}
</DecisionIntelligenceContext.Provider>
);
}
Missing GraphQL Mutations & Queries
Add to backend GraphQL schema:
# Query: Fetch orchestration result for a scenario
query GetOrchestrationResult($scenarioId: ID!) {
orchestrationResult(scenarioId: $scenarioId) {
orchestration_id
narrative {
headline { action businessAction driver confidence }
executiveSummary { confidence keyRisk tradeOff dataValue }
details {
bayesian { title summary findings businessImplications recommendations }
robustness { title summary findings businessImplications recommendations }
voi { title summary findings businessImplications recommendations }
optimizer { title summary findings businessImplications recommendations }
}
caveats
siePrompt { question description buttonText sieQuestion }
}
orchestration_result {
bayesian { confidence reasoning distribution }
robustness { failure_modes robustness_score }
voi { ranked_data_sources monetaryValue confidence }
optimizer { pareto_frontier recommended_scenario }
}
execution_metadata {
services_run
duration_ms
partial_results
}
}
}
# Mutation: Trigger orchestration with user's problem state
mutation ReOrchestrateWithUserInput(
$orchestration_id: ID!
$problemStateVector: ProblemStateVectorInput!
) {
reOrchestrate(
orchestration_id: $orchestration_id
problemStateVector: $problemStateVector
) {
orchestration_id
narrative {
headline { action businessAction driver confidence }
executiveSummary { confidence keyRisk tradeOff dataValue }
details {
bayesian { title summary findings businessImplications recommendations }
robustness { title summary findings businessImplications recommendations }
voi { title summary findings businessImplications recommendations }
optimizer { title summary findings businessImplications recommendations }
}
caveats
siePrompt { question description buttonText sieQuestion }
}
orchestration_result {
bayesian { confidence reasoning distribution }
robustness { failure_modes robustness_score }
voi { ranked_data_sources monetaryValue confidence }
optimizer { pareto_frontier recommended_scenario }
}
comparison {
original_recommendation
user_recommendation
trade_offs_identified
key_differences
}
}
}
# Mutation: Save orchestration to reasoning bank
mutation LogDecisionToReasoningBank(
$orchestration_id: ID!
$user_decision: String!
$actual_outcome: JSON
) {
logDecisionToReasoningBank(
orchestration_id: $orchestration_id
user_decision: $user_decision
actual_outcome: $actual_outcome
) {
reasoning_id
created_at
feedback_recorded
}
}
REST Endpoints (Alternative to GraphQL)
GET /api/ml-orchestration/result/:scenarioId
Returns: { narrative, orchestration_result, execution_metadata }
POST /api/ml-orchestration/re-orchestrate
Body: { orchestration_id, problemStateVector }
Returns: { narrative, orchestration_result, comparison }
POST /api/ml-orchestration/log-decision
Body: { orchestration_id, user_decision, actual_outcome? }
Returns: { reasoning_id, feedback_recorded }
8. Tenant Configuration & Customization
Per-Tenant Parameters
Not all orchestration parameters should be global. Tenants need customization:
Database Schema:
CREATE TABLE tenant_ml_config (
tenant_id UUID PRIMARY KEY REFERENCES tenants(tenant_id),
-- Robustness Configuration
robustness_perturbation_percentiles INT[] DEFAULT '{70,85,100,115,130}',
robustness_critical_threshold DECIMAL(5,2) DEFAULT 0.15, -- % degradation
robustness_timeout_seconds INT DEFAULT 5,
robustness_monte_carlo_iterations INT DEFAULT 500,
-- VOI Configuration
voi_high_priority_threshold DECIMAL(15,2) DEFAULT 500000, -- $ annual value
voi_medium_priority_threshold DECIMAL(15,2) DEFAULT 250000,
voi_low_priority_threshold DECIMAL(15,2) DEFAULT 100000,
voi_confidence_min_sample_size INT DEFAULT 24,
voi_cache_ttl_minutes INT DEFAULT 10,
-- Optimizer Configuration
optimizer_max_candidates INT DEFAULT 10,
optimizer_timeout_seconds INT DEFAULT 5,
optimizer_confidence_weights JSONB DEFAULT '{"roi": 0.3, "debt": 0.25, "eps": 0.25, "rd": 0.2}'::jsonb,
-- Bayesian Configuration
bayesian_inference_samples INT DEFAULT 1000,
bayesian_min_confidence_threshold DECIMAL(3,2) DEFAULT 0.60,
bayesian_retraining_frequency_days INT DEFAULT 30,
bayesian_auto_retrain_enabled BOOLEAN DEFAULT true,
bayesian_drift_detection_threshold DECIMAL(5,2) DEFAULT 0.10,
-- Orchestration Configuration
orchestration_hard_timeout_seconds INT DEFAULT 10,
orchestration_partial_results_allowed BOOLEAN DEFAULT true,
orchestration_skip_services VARCHAR(255)[] DEFAULT '{}', -- e.g., {'voi'} to always skip VOI
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id)
);
Configuration API
Endpoints:
GET /api/ml-orchestration/tenant-config
Returns: { tenant_id, robustness_config, voi_config, ... }
Auth: Admin only
PUT /api/ml-orchestration/tenant-config
Body: { voi_high_priority_threshold: 750000, ... }
Returns: Updated config
Auth: Admin only
POST /api/ml-orchestration/tenant-config/reset-to-defaults
Returns: Config reset to defaults
Auth: Super-admin only
Usage in Services:
// backend/src/services/DecisionOrchestrationService.js
async orchestrate(tenantId, scenarioInput, problemStateVector, overrideConfig = {}) {
// Load tenant config
const tenantConfig = await TenantMLConfigRepository.findByTenantId(tenantId);
// Merge with overrides
const effectiveConfig = { ...tenantConfig, ...overrideConfig };
// Pass to individual services
const bayesianResult = await BayesianReasoningService.run(
tenantId,
{ ...params, samples: effectiveConfig.bayesian_inference_samples }
);
// ... pass effectiveConfig to other services
}
Example Tenant Scenarios
Scenario A: High-Risk Manufacturing (Pharma)
{
"robustness_critical_threshold": 0.05, // Strict: flag if >5% degradation
"bayesian_auto_retrain_enabled": true,
"bayesian_retraining_frequency_days": 7, // Weekly retraining
"orchestration_skip_services": [], // Use all services
"optimizer_confidence_weights": {
"regulatory_compliance": 0.5,
"cost": 0.2,
"timeline": 0.2,
"capacity": 0.1
}
}
Scenario B: Fast-Moving Retail (Quick Decision Making)
{
"robustness_critical_threshold": 0.25, // Relaxed: flag if >25% degradation
"orchestration_hard_timeout_seconds": 3, // Fast response
"orchestration_skip_services": ["voi"], // Skip VOI to save time
"robustness_monte_carlo_iterations": 200, // Fewer iterations for speed
"optimizer_max_candidates": 5 // Fewer scenarios to evaluate
}
10. Implementation Checklist
Phase 1 (Weeks 1-4)
- Create
BayesianReasoningServicewith 3 micro-networks- Supplier lead time network
- Demand forecast network
- Stockout risk network
- Create
ScenarioRobustnessServicewith stress testing - Enhance
ConstraintIntelligenceEngineto use Bayesian probabilities - Add database tables for inference logs and networks
- Create
mlOrchestrationRoutes.jswith Phase 1 endpoints - Demo 1 integration: S&OP stress testing
Phase 2 (Weeks 5-8)
- Create
DataSourceImpactServicewith VOI calculation - Create
MultiObjectiveOptimizerServicewith Pareto analysis - Create feedback capture table for decision logging
- Implement online learning for Bayesian network retraining
- Demo 2 integration: Financial Pareto frontier
- Demo 3 integration: VOI data prioritization
Integration (Weeks 9-10)
- Create
DecisionOrchestrationServicemaster coordinator - Integrate all services into unified API
- Build orchestrated LLM narrative generator
- Demo 4 & 5 integration: Retail forecasting + Pharma S&OP
- Performance testing and optimization
- Documentation and runbooks
11. Success Metrics
For Each Demo
| Demo | Success Metric |
|---|---|
| S&OP Stress Testing | User can identify 3+ failure modes and their probabilities |
| Financial Pareto | CFO sees 3 distinct options with clear trade-offs |
| VOI Analysis | Data prioritization decision is made with $XXM justification |
| Retail Forecasting | Planner understands confidence bounds and adjusts safety stock |
| Pharma Compliance | Regulatory risks are highlighted and mitigation is clear |
For the Moat
- Orchestration complexity visible: Each demo shows 3+ services working together (not just ML or LLM)
- User trust signal: Confidence bounds, stress tests, and explainability are consistent across all domains
- Competitive gap: Competitors can replicate individual pieces (forecasting, constraints, LLM), but orchestration across domains is proprietary
12. Notes for Implementation
-
Start with hardcoded examples: Micro-networks and Pareto scenarios can be hardcoded for MVP. Real network learning comes in phase 3.
-
Reuse existing infrastructure: Leverage Monte Carlo service, constraint engine, and LLM layer—don't rebuild.
-
Focus on explainability: Every probability, trade-off, and recommendation must be explainable in business terms.
-
Feedback loop is critical: Decision logging + outcome tracking is what makes the system improve over time.
-
Demo pacing: Each demo should take 3-5 minutes and conclude with a clear recommendation the user can act on.
13. Upgrade Path: MVP to Production-Grade Engine
13.1. Objective
To upgrade the current JavaScript-based MVP of the ML Orchestration Engine to a robust, production-grade Python service. This will enhance the system's analytical power, scalability, and maintainability, laying the foundation for more advanced decision-support features.
13.2. Recommended Core Library: PyMC
- Why
PyMC? It is a powerful, modern, and well-maintained probabilistic programming library that excels at the complex Bayesian modeling and inference required for this project. It provides state-of-the-art sampling algorithms (like MCMC) that will offer a significant leap in accuracy and capability over the current MVP. It gives us a high ceiling for future enhancements. - Alternative:
pgmpyis a solid, simpler alternative if a more direct, less complex implementation is preferred.
13.3. Architectural Approach
We will create a new, containerized Python service that integrates seamlessly into the existing architecture.
- New Service: A new service named
bayesian-inference-servicewill be created within thepython-servicesdirectory. - Technology: It will be built using FastAPI, a high-performance web framework for creating REST APIs, and Pydantic for robust data validation.
- Integration: The existing Node.js backend will be updated to call this new service via a simple, internal REST API call. This replaces the local JavaScript MVP logic.
- Deployment: The service will be containerized with Docker and deployed to Google Cloud Run, consistent with the other Python services in the project.
13.4. Phased Implementation Roadmap
A four-phase approach is proposed, which can be mapped to distinct milestones.
-
Phase 1: Service Scaffolding & Core Model Implementation
- Task 1.1: Create the directory structure and initial files for
bayesian-inference-service. - Task 1.2: Establish the Python environment, installing
PyMC,FastAPI, and other dependencies. - Task 1.3: Translate the conceptual model from the FSD into a functional
PyMCmodel, defining the variables and their relationships. - Task 1.4: Write initial unit tests to validate the model's structure and logic.
- Task 1.1: Create the directory structure and initial files for
-
Phase 2: API Development
- Task 2.1: Implement the FastAPI server and define the API endpoint (e.g.,
/v1/run-inference). - Task 2.2: Define the request/response data models using Pydantic to ensure a clear and validated API contract.
- Task 2.3: Connect the
PyMCmodel to the API, allowing it to run inference based on data sent in the request. - Task 2.4: Develop integration tests for the API endpoint.
- Task 2.1: Implement the FastAPI server and define the API endpoint (e.g.,
-
Phase 3: Containerization & Deployment
- Task 3.1: Write a
Dockerfilefor the new service. - Task 3.2: Update the
cloudbuild-04-python-services.yamlfile to add a build and deploy step for thebayesian-inference-service. - Task 3.3: Deploy an initial version of the service to a development or staging environment on Cloud Run.
- Task 3.1: Write a
-
Phase 4: Backend Integration & MVP Deprecation
- Task 4.1: In the Node.js backend, create a client module to handle communication with the new Python service.
- Task 4.2: Modify the orchestration logic to call the
bayesian-inference-serviceAPI and process its response. - Task 4.3: Once the integration is verified, safely remove the old JavaScript MVP implementation.
- Task 4.4: Conduct end-to-end testing to ensure the entire workflow is functioning correctly.
End of FSD