Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

View File

@@ -0,0 +1,894 @@
"""
AI-Enhanced Orchestration Saga
Integrates ML insights into daily workflow orchestration
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Any, Optional, Tuple
from datetime import datetime, timedelta
from uuid import UUID
import structlog
from shared.clients.ai_insights_client import AIInsightsClient
logger = structlog.get_logger()
class AIEnhancedOrchestrator:
"""
Enhanced orchestration engine that integrates ML insights into daily workflow.
Workflow:
1. Pre-Orchestration: Gather all relevant insights for target date
2. Intelligent Planning: Modify orchestration plan based on insights
3. Execution: Apply insights with confidence-based decision making
4. Feedback Tracking: Record outcomes for continuous learning
Replaces hardcoded logic with learned intelligence from:
- Demand Forecasting
- Supplier Performance
- Safety Stock Optimization
- Price Forecasting
- Production Yield Prediction
- Dynamic Business Rules
"""
def __init__(
self,
ai_insights_base_url: str = "http://ai-insights-service:8000",
min_confidence_threshold: int = 70
):
self.ai_insights_client = AIInsightsClient(ai_insights_base_url)
self.min_confidence_threshold = min_confidence_threshold
self.applied_insights = [] # Track applied insights for feedback
async def orchestrate_with_ai(
self,
tenant_id: str,
target_date: datetime,
base_orchestration_plan: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Run AI-enhanced orchestration for a target date.
Args:
tenant_id: Tenant identifier
target_date: Date to orchestrate for
base_orchestration_plan: Optional base plan to enhance (if None, creates new)
Returns:
Enhanced orchestration plan with applied insights and metadata
"""
logger.info(
"Starting AI-enhanced orchestration",
tenant_id=tenant_id,
target_date=target_date.isoformat()
)
# Step 1: Gather insights for target date
insights = await self._gather_insights(tenant_id, target_date)
logger.info(
"Insights gathered",
demand_forecasts=len(insights['demand_forecasts']),
supplier_alerts=len(insights['supplier_alerts']),
inventory_optimizations=len(insights['inventory_optimizations']),
price_opportunities=len(insights['price_opportunities']),
yield_predictions=len(insights['yield_predictions']),
business_rules=len(insights['business_rules'])
)
# Step 2: Initialize or load base plan
if base_orchestration_plan is None:
orchestration_plan = self._create_base_plan(target_date)
else:
orchestration_plan = base_orchestration_plan.copy()
# Step 3: Apply insights to plan
enhanced_plan = await self._apply_insights_to_plan(
orchestration_plan, insights, tenant_id
)
# Step 4: Generate execution summary
execution_summary = self._generate_execution_summary(
enhanced_plan, insights
)
logger.info(
"AI-enhanced orchestration complete",
tenant_id=tenant_id,
insights_applied=execution_summary['total_insights_applied'],
modifications=execution_summary['total_modifications']
)
return {
'tenant_id': tenant_id,
'target_date': target_date.isoformat(),
'orchestrated_at': datetime.utcnow().isoformat(),
'plan': enhanced_plan,
'insights_used': insights,
'execution_summary': execution_summary,
'applied_insights': self.applied_insights
}
async def _gather_insights(
self,
tenant_id: str,
target_date: datetime
) -> Dict[str, List[Dict[str, Any]]]:
"""
Gather all relevant insights for target date from AI Insights Service.
Returns insights categorized by type:
- demand_forecasts
- supplier_alerts
- inventory_optimizations
- price_opportunities
- yield_predictions
- business_rules
"""
# Get orchestration-ready insights
insights = await self.ai_insights_client.get_orchestration_ready_insights(
tenant_id=UUID(tenant_id),
target_date=target_date,
min_confidence=self.min_confidence_threshold
)
# Categorize insights by source
categorized = {
'demand_forecasts': [],
'supplier_alerts': [],
'inventory_optimizations': [],
'price_opportunities': [],
'yield_predictions': [],
'business_rules': [],
'other': []
}
for insight in insights:
source_model = insight.get('source_model', '')
category = insight.get('category', '')
if source_model == 'hybrid_forecaster' or category == 'demand':
categorized['demand_forecasts'].append(insight)
elif source_model == 'supplier_performance_predictor':
categorized['supplier_alerts'].append(insight)
elif source_model == 'safety_stock_optimizer':
categorized['inventory_optimizations'].append(insight)
elif source_model == 'price_forecaster':
categorized['price_opportunities'].append(insight)
elif source_model == 'yield_predictor':
categorized['yield_predictions'].append(insight)
elif source_model == 'business_rules_engine':
categorized['business_rules'].append(insight)
else:
categorized['other'].append(insight)
return categorized
def _create_base_plan(self, target_date: datetime) -> Dict[str, Any]:
"""Create base orchestration plan with default hardcoded values."""
return {
'target_date': target_date.isoformat(),
'procurement': {
'orders': [],
'supplier_selections': {},
'order_quantities': {}
},
'inventory': {
'safety_stock_levels': {},
'reorder_points': {},
'transfers': []
},
'production': {
'production_runs': [],
'recipe_quantities': {},
'worker_assignments': {}
},
'sales': {
'forecasted_demand': {},
'pricing_adjustments': {}
},
'modifications': [],
'ai_enhancements': []
}
async def _apply_insights_to_plan(
self,
plan: Dict[str, Any],
insights: Dict[str, List[Dict[str, Any]]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply categorized insights to orchestration plan.
Each insight type modifies specific parts of the plan:
- Demand forecasts → sales forecasts, production quantities
- Supplier alerts → supplier selection, procurement timing
- Inventory optimizations → safety stock levels, reorder points
- Price opportunities → procurement timing, order quantities
- Yield predictions → production quantities, worker assignments
- Business rules → cross-cutting modifications
"""
enhanced_plan = plan.copy()
# Apply demand forecasts
if insights['demand_forecasts']:
enhanced_plan = await self._apply_demand_forecasts(
enhanced_plan, insights['demand_forecasts'], tenant_id
)
# Apply supplier alerts
if insights['supplier_alerts']:
enhanced_plan = await self._apply_supplier_alerts(
enhanced_plan, insights['supplier_alerts'], tenant_id
)
# Apply inventory optimizations
if insights['inventory_optimizations']:
enhanced_plan = await self._apply_inventory_optimizations(
enhanced_plan, insights['inventory_optimizations'], tenant_id
)
# Apply price opportunities
if insights['price_opportunities']:
enhanced_plan = await self._apply_price_opportunities(
enhanced_plan, insights['price_opportunities'], tenant_id
)
# Apply yield predictions
if insights['yield_predictions']:
enhanced_plan = await self._apply_yield_predictions(
enhanced_plan, insights['yield_predictions'], tenant_id
)
# Apply business rules (highest priority, can override)
if insights['business_rules']:
enhanced_plan = await self._apply_business_rules(
enhanced_plan, insights['business_rules'], tenant_id
)
return enhanced_plan
async def _apply_demand_forecasts(
self,
plan: Dict[str, Any],
forecasts: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply demand forecasts to sales and production planning.
Modifications:
- Update sales forecasted_demand
- Adjust production recipe_quantities
- Record insight application
"""
for forecast in forecasts:
if forecast['confidence'] < self.min_confidence_threshold:
continue
metrics = forecast.get('metrics_json', {})
product_id = metrics.get('product_id')
predicted_demand = metrics.get('predicted_demand')
forecast_date = metrics.get('forecast_date')
if not product_id or predicted_demand is None:
continue
# Update sales forecast
plan['sales']['forecasted_demand'][product_id] = {
'quantity': predicted_demand,
'confidence': forecast['confidence'],
'source': 'ai_forecast',
'insight_id': forecast.get('id')
}
# Adjust production quantities (demand + buffer)
buffer_pct = 1.10 # 10% buffer for uncertainty
production_quantity = int(predicted_demand * buffer_pct)
plan['production']['recipe_quantities'][product_id] = {
'quantity': production_quantity,
'demand_forecast': predicted_demand,
'buffer_applied': buffer_pct,
'source': 'ai_forecast',
'insight_id': forecast.get('id')
}
# Record modification
plan['modifications'].append({
'type': 'demand_forecast_applied',
'insight_id': forecast.get('id'),
'product_id': product_id,
'predicted_demand': predicted_demand,
'production_quantity': production_quantity,
'confidence': forecast['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': forecast.get('id'),
'type': 'demand_forecast',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {
'product_id': product_id,
'predicted_demand': predicted_demand,
'production_quantity': production_quantity
}
})
logger.info(
"Applied demand forecast",
product_id=product_id,
predicted_demand=predicted_demand,
production_quantity=production_quantity
)
return plan
async def _apply_supplier_alerts(
self,
plan: Dict[str, Any],
alerts: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply supplier performance alerts to procurement decisions.
Modifications:
- Switch suppliers for low reliability
- Adjust lead times for delays
- Increase order quantities for short deliveries
"""
for alert in alerts:
if alert['confidence'] < self.min_confidence_threshold:
continue
metrics = alert.get('metrics_json', {})
supplier_id = metrics.get('supplier_id')
reliability_score = metrics.get('reliability_score')
predicted_delay = metrics.get('predicted_delivery_delay_days')
if not supplier_id:
continue
# Low reliability: recommend supplier switch
if reliability_score and reliability_score < 70:
plan['procurement']['supplier_selections'][supplier_id] = {
'action': 'avoid',
'reason': f'Low reliability score: {reliability_score}',
'alternative_required': True,
'source': 'supplier_alert',
'insight_id': alert.get('id')
}
plan['modifications'].append({
'type': 'supplier_switch_recommended',
'insight_id': alert.get('id'),
'supplier_id': supplier_id,
'reliability_score': reliability_score,
'confidence': alert['confidence']
})
# Delay predicted: adjust lead time
if predicted_delay and predicted_delay > 1:
plan['procurement']['supplier_selections'][supplier_id] = {
'action': 'adjust_lead_time',
'additional_lead_days': int(predicted_delay),
'reason': f'Predicted delay: {predicted_delay} days',
'source': 'supplier_alert',
'insight_id': alert.get('id')
}
plan['modifications'].append({
'type': 'lead_time_adjusted',
'insight_id': alert.get('id'),
'supplier_id': supplier_id,
'additional_days': int(predicted_delay),
'confidence': alert['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': alert.get('id'),
'type': 'supplier_alert',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {
'supplier_id': supplier_id,
'reliability_score': reliability_score,
'predicted_delay': predicted_delay
}
})
logger.info(
"Applied supplier alert",
supplier_id=supplier_id,
reliability_score=reliability_score,
predicted_delay=predicted_delay
)
return plan
async def _apply_inventory_optimizations(
self,
plan: Dict[str, Any],
optimizations: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply safety stock optimizations to inventory management.
Modifications:
- Update safety stock levels (from hardcoded 95% to learned optimal)
- Adjust reorder points accordingly
"""
for optimization in optimizations:
if optimization['confidence'] < self.min_confidence_threshold:
continue
metrics = optimization.get('metrics_json', {})
product_id = metrics.get('inventory_product_id')
optimal_safety_stock = metrics.get('optimal_safety_stock')
optimal_service_level = metrics.get('optimal_service_level')
if not product_id or optimal_safety_stock is None:
continue
# Update safety stock level
plan['inventory']['safety_stock_levels'][product_id] = {
'quantity': optimal_safety_stock,
'service_level': optimal_service_level,
'source': 'ai_optimization',
'insight_id': optimization.get('id'),
'replaced_hardcoded': True
}
# Adjust reorder point (lead time demand + safety stock)
# This would use demand forecast if available
lead_time_demand = metrics.get('lead_time_demand', optimal_safety_stock * 2)
reorder_point = lead_time_demand + optimal_safety_stock
plan['inventory']['reorder_points'][product_id] = {
'quantity': reorder_point,
'lead_time_demand': lead_time_demand,
'safety_stock': optimal_safety_stock,
'source': 'ai_optimization',
'insight_id': optimization.get('id')
}
plan['modifications'].append({
'type': 'safety_stock_optimized',
'insight_id': optimization.get('id'),
'product_id': product_id,
'optimal_safety_stock': optimal_safety_stock,
'optimal_service_level': optimal_service_level,
'confidence': optimization['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': optimization.get('id'),
'type': 'inventory_optimization',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {
'product_id': product_id,
'optimal_safety_stock': optimal_safety_stock,
'reorder_point': reorder_point
}
})
logger.info(
"Applied safety stock optimization",
product_id=product_id,
optimal_safety_stock=optimal_safety_stock,
reorder_point=reorder_point
)
return plan
async def _apply_price_opportunities(
self,
plan: Dict[str, Any],
opportunities: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply price forecasting opportunities to procurement timing.
Modifications:
- Advance orders for predicted price increases
- Delay orders for predicted price decreases
- Increase quantities for bulk opportunities
"""
for opportunity in opportunities:
if opportunity['confidence'] < self.min_confidence_threshold:
continue
metrics = opportunity.get('metrics_json', {})
ingredient_id = metrics.get('ingredient_id')
recommendation = metrics.get('recommendation')
expected_price_change = metrics.get('expected_price_change_pct')
if not ingredient_id or not recommendation:
continue
# Buy now: price increasing
if recommendation == 'buy_now' and expected_price_change and expected_price_change > 5:
plan['procurement']['order_quantities'][ingredient_id] = {
'action': 'increase',
'multiplier': 1.5, # Buy 50% more
'reason': f'Price expected to increase {expected_price_change:.1f}%',
'source': 'price_forecast',
'insight_id': opportunity.get('id')
}
plan['modifications'].append({
'type': 'bulk_purchase_opportunity',
'insight_id': opportunity.get('id'),
'ingredient_id': ingredient_id,
'expected_price_change': expected_price_change,
'quantity_multiplier': 1.5,
'confidence': opportunity['confidence']
})
# Wait: price decreasing
elif recommendation == 'wait' and expected_price_change and expected_price_change < -5:
plan['procurement']['order_quantities'][ingredient_id] = {
'action': 'delay',
'delay_days': 7,
'reason': f'Price expected to decrease {abs(expected_price_change):.1f}%',
'source': 'price_forecast',
'insight_id': opportunity.get('id')
}
plan['modifications'].append({
'type': 'procurement_delayed',
'insight_id': opportunity.get('id'),
'ingredient_id': ingredient_id,
'expected_price_change': expected_price_change,
'delay_days': 7,
'confidence': opportunity['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': opportunity.get('id'),
'type': 'price_opportunity',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {
'ingredient_id': ingredient_id,
'recommendation': recommendation,
'expected_price_change': expected_price_change
}
})
logger.info(
"Applied price opportunity",
ingredient_id=ingredient_id,
recommendation=recommendation,
expected_price_change=expected_price_change
)
return plan
async def _apply_yield_predictions(
self,
plan: Dict[str, Any],
predictions: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply production yield predictions to production planning.
Modifications:
- Increase production quantities for low predicted yield
- Optimize worker assignments
- Adjust production timing
"""
for prediction in predictions:
if prediction['confidence'] < self.min_confidence_threshold:
continue
metrics = prediction.get('metrics_json', {})
recipe_id = metrics.get('recipe_id')
predicted_yield = metrics.get('predicted_yield')
expected_waste = metrics.get('expected_waste')
if not recipe_id or predicted_yield is None:
continue
# Low yield: increase production quantity to compensate
if predicted_yield < 90:
current_quantity = plan['production']['recipe_quantities'].get(
recipe_id, {}
).get('quantity', 100)
# Adjust quantity to account for predicted waste
adjusted_quantity = int(current_quantity * (100 / predicted_yield))
plan['production']['recipe_quantities'][recipe_id] = {
'quantity': adjusted_quantity,
'predicted_yield': predicted_yield,
'waste_compensation': adjusted_quantity - current_quantity,
'source': 'yield_prediction',
'insight_id': prediction.get('id')
}
plan['modifications'].append({
'type': 'yield_compensation_applied',
'insight_id': prediction.get('id'),
'recipe_id': recipe_id,
'predicted_yield': predicted_yield,
'original_quantity': current_quantity,
'adjusted_quantity': adjusted_quantity,
'confidence': prediction['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': prediction.get('id'),
'type': 'yield_prediction',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {
'recipe_id': recipe_id,
'predicted_yield': predicted_yield,
'expected_waste': expected_waste
}
})
logger.info(
"Applied yield prediction",
recipe_id=recipe_id,
predicted_yield=predicted_yield
)
return plan
async def _apply_business_rules(
self,
plan: Dict[str, Any],
rules: List[Dict[str, Any]],
tenant_id: str
) -> Dict[str, Any]:
"""
Apply dynamic business rules to orchestration plan.
Business rules can override other insights based on business logic.
"""
for rule in rules:
if rule['confidence'] < self.min_confidence_threshold:
continue
# Business rules are flexible and defined in JSONB
# Parse recommendation_actions to understand what to apply
actions = rule.get('recommendation_actions', [])
for action in actions:
action_type = action.get('action')
params = action.get('params', {})
# Example: Force supplier switch
if action_type == 'force_supplier_switch':
supplier_id = params.get('from_supplier_id')
alternate_id = params.get('to_supplier_id')
if supplier_id and alternate_id:
plan['procurement']['supplier_selections'][supplier_id] = {
'action': 'replace',
'alternate_supplier': alternate_id,
'reason': rule.get('description'),
'source': 'business_rule',
'insight_id': rule.get('id'),
'override': True
}
# Example: Halt production
elif action_type == 'halt_production':
recipe_id = params.get('recipe_id')
if recipe_id:
plan['production']['recipe_quantities'][recipe_id] = {
'quantity': 0,
'halted': True,
'reason': rule.get('description'),
'source': 'business_rule',
'insight_id': rule.get('id')
}
plan['modifications'].append({
'type': 'business_rule_applied',
'insight_id': rule.get('id'),
'rule_description': rule.get('description'),
'confidence': rule['confidence']
})
# Track for feedback
self.applied_insights.append({
'insight_id': rule.get('id'),
'type': 'business_rule',
'applied_at': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'metrics': {'actions': len(actions)}
})
logger.info(
"Applied business rule",
rule_description=rule.get('title')
)
return plan
def _generate_execution_summary(
self,
plan: Dict[str, Any],
insights: Dict[str, List[Dict[str, Any]]]
) -> Dict[str, Any]:
"""Generate summary of AI-enhanced orchestration execution."""
total_insights_available = sum(len(v) for v in insights.values())
total_insights_applied = len(self.applied_insights)
total_modifications = len(plan.get('modifications', []))
# Count by type
insights_by_type = {}
for category, category_insights in insights.items():
insights_by_type[category] = {
'available': len(category_insights),
'applied': len([
i for i in self.applied_insights
if i['type'] == category.rstrip('s') # Remove plural
])
}
return {
'total_insights_available': total_insights_available,
'total_insights_applied': total_insights_applied,
'total_modifications': total_modifications,
'application_rate': round(
(total_insights_applied / total_insights_available * 100)
if total_insights_available > 0 else 0,
2
),
'insights_by_type': insights_by_type,
'modifications_summary': self._summarize_modifications(plan)
}
def _summarize_modifications(self, plan: Dict[str, Any]) -> Dict[str, int]:
"""Summarize modifications by type."""
modifications = plan.get('modifications', [])
summary = {}
for mod in modifications:
mod_type = mod.get('type', 'unknown')
summary[mod_type] = summary.get(mod_type, 0) + 1
return summary
async def record_orchestration_feedback(
self,
tenant_id: str,
target_date: datetime,
actual_outcomes: Dict[str, Any]
) -> Dict[str, Any]:
"""
Record feedback for applied insights to enable continuous learning.
Args:
tenant_id: Tenant identifier
target_date: Orchestration target date
actual_outcomes: Actual results:
- actual_demand: {product_id: actual_quantity}
- actual_yields: {recipe_id: actual_yield_pct}
- actual_costs: {ingredient_id: actual_price}
- supplier_performance: {supplier_id: on_time_delivery}
Returns:
Feedback recording results
"""
logger.info(
"Recording orchestration feedback",
tenant_id=tenant_id,
target_date=target_date.isoformat(),
applied_insights=len(self.applied_insights)
)
feedback_results = []
for applied in self.applied_insights:
insight_id = applied.get('insight_id')
insight_type = applied.get('type')
metrics = applied.get('metrics', {})
# Prepare feedback based on type
feedback_data = {
'applied': True,
'applied_at': applied.get('applied_at'),
'outcome_date': target_date.isoformat()
}
# Demand forecast feedback
if insight_type == 'demand_forecast':
product_id = metrics.get('product_id')
predicted_demand = metrics.get('predicted_demand')
actual_demand = actual_outcomes.get('actual_demand', {}).get(product_id)
if actual_demand is not None:
error = abs(actual_demand - predicted_demand)
error_pct = (error / actual_demand * 100) if actual_demand > 0 else 0
feedback_data['outcome_metrics'] = {
'predicted_demand': predicted_demand,
'actual_demand': actual_demand,
'error': error,
'error_pct': round(error_pct, 2),
'accuracy': round(100 - error_pct, 2)
}
# Yield prediction feedback
elif insight_type == 'yield_prediction':
recipe_id = metrics.get('recipe_id')
predicted_yield = metrics.get('predicted_yield')
actual_yield = actual_outcomes.get('actual_yields', {}).get(recipe_id)
if actual_yield is not None:
error = abs(actual_yield - predicted_yield)
feedback_data['outcome_metrics'] = {
'predicted_yield': predicted_yield,
'actual_yield': actual_yield,
'error': round(error, 2),
'accuracy': round(100 - (error / actual_yield * 100), 2) if actual_yield > 0 else 0
}
# Record feedback via AI Insights Client
try:
await self.ai_insights_client.record_feedback(
tenant_id=UUID(tenant_id),
insight_id=UUID(insight_id) if insight_id else None,
feedback_data=feedback_data
)
feedback_results.append({
'insight_id': insight_id,
'insight_type': insight_type,
'status': 'recorded',
'feedback': feedback_data
})
except Exception as e:
logger.error(
"Error recording feedback",
insight_id=insight_id,
error=str(e)
)
feedback_results.append({
'insight_id': insight_id,
'insight_type': insight_type,
'status': 'failed',
'error': str(e)
})
logger.info(
"Feedback recording complete",
total=len(feedback_results),
successful=len([r for r in feedback_results if r['status'] == 'recorded'])
)
return {
'tenant_id': tenant_id,
'target_date': target_date.isoformat(),
'feedback_recorded_at': datetime.utcnow().isoformat(),
'total_insights': len(self.applied_insights),
'feedback_results': feedback_results,
'successful': len([r for r in feedback_results if r['status'] == 'recorded']),
'failed': len([r for r in feedback_results if r['status'] == 'failed'])
}
async def close(self):
"""Close HTTP client connections."""
await self.ai_insights_client.close()