Initial commit - production deployment
This commit is contained in:
0
services/orchestrator/app/ml/__init__.py
Normal file
0
services/orchestrator/app/ml/__init__.py
Normal file
894
services/orchestrator/app/ml/ai_enhanced_orchestrator.py
Normal file
894
services/orchestrator/app/ml/ai_enhanced_orchestrator.py
Normal 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()
|
||||
Reference in New Issue
Block a user