""" Supplier Insights Orchestrator Coordinates supplier performance analysis and insight posting """ import pandas as pd from typing import Dict, List, Any, Optional import structlog from datetime import datetime from uuid import UUID import sys import os # Add shared clients to path sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) from shared.clients.ai_insights_client import AIInsightsClient from app.ml.supplier_performance_predictor import SupplierPerformancePredictor logger = structlog.get_logger() class SupplierInsightsOrchestrator: """ Orchestrates supplier performance analysis and insight generation workflow. Workflow: 1. Analyze supplier performance from historical orders 2. Generate insights for procurement risk management 3. Post insights to AI Insights Service 4. Provide supplier comparison and recommendations 5. Track supplier reliability scores """ def __init__( self, ai_insights_base_url: str = "http://ai-insights-service:8000" ): self.predictor = SupplierPerformancePredictor() self.ai_insights_client = AIInsightsClient(ai_insights_base_url) async def analyze_and_post_supplier_insights( self, tenant_id: str, supplier_id: str, order_history: pd.DataFrame, min_orders: int = 10 ) -> Dict[str, Any]: """ Complete workflow: Analyze supplier and post insights. Args: tenant_id: Tenant identifier supplier_id: Supplier identifier order_history: Historical order data min_orders: Minimum orders for analysis Returns: Workflow results with analysis and posted insights """ logger.info( "Starting supplier performance analysis workflow", tenant_id=tenant_id, supplier_id=supplier_id, orders=len(order_history) ) # Step 1: Analyze supplier performance analysis_results = await self.predictor.analyze_supplier_performance( tenant_id=tenant_id, supplier_id=supplier_id, order_history=order_history, min_orders=min_orders ) logger.info( "Supplier analysis complete", supplier_id=supplier_id, reliability_score=analysis_results.get('reliability_score'), insights_generated=len(analysis_results.get('insights', [])) ) # Step 2: Enrich insights with tenant_id and supplier context enriched_insights = self._enrich_insights( analysis_results.get('insights', []), tenant_id, supplier_id ) # Step 3: Post insights to AI Insights Service if enriched_insights: post_results = await self.ai_insights_client.create_insights_bulk( tenant_id=UUID(tenant_id), insights=enriched_insights ) logger.info( "Supplier insights posted to AI Insights Service", supplier_id=supplier_id, total=post_results['total'], successful=post_results['successful'], failed=post_results['failed'] ) else: post_results = {'total': 0, 'successful': 0, 'failed': 0} logger.info("No insights to post for supplier", supplier_id=supplier_id) # Step 4: Return comprehensive results return { 'tenant_id': tenant_id, 'supplier_id': supplier_id, 'analyzed_at': analysis_results['analyzed_at'], 'orders_analyzed': analysis_results['orders_analyzed'], 'reliability_score': analysis_results.get('reliability_score'), 'risk_assessment': analysis_results.get('risk_assessment', {}), 'predictions': analysis_results.get('predictions', {}), 'insights_generated': len(enriched_insights), 'insights_posted': post_results['successful'], 'insights_failed': post_results['failed'], 'created_insights': post_results.get('created_insights', []) } def _enrich_insights( self, insights: List[Dict[str, Any]], tenant_id: str, supplier_id: str ) -> List[Dict[str, Any]]: """ Enrich insights with required fields for AI Insights Service. Args: insights: Raw insights from predictor tenant_id: Tenant identifier supplier_id: Supplier identifier Returns: Enriched insights ready for posting """ enriched = [] for insight in insights: # Add required tenant_id enriched_insight = insight.copy() enriched_insight['tenant_id'] = tenant_id # Add supplier context to metrics if 'metrics_json' not in enriched_insight: enriched_insight['metrics_json'] = {} enriched_insight['metrics_json']['supplier_id'] = supplier_id # Add source metadata enriched_insight['source_service'] = 'procurement' enriched_insight['source_model'] = 'supplier_performance_predictor' enriched_insight['detected_at'] = datetime.utcnow().isoformat() enriched.append(enriched_insight) return enriched async def analyze_all_suppliers( self, tenant_id: str, suppliers_data: Dict[str, pd.DataFrame], min_orders: int = 10 ) -> Dict[str, Any]: """ Analyze all suppliers for a tenant and generate comparative insights. Args: tenant_id: Tenant identifier suppliers_data: Dict of {supplier_id: order_history DataFrame} min_orders: Minimum orders for analysis Returns: Comprehensive analysis with supplier comparison """ logger.info( "Analyzing all suppliers for tenant", tenant_id=tenant_id, suppliers=len(suppliers_data) ) all_results = [] total_insights_posted = 0 # Analyze each supplier for supplier_id, order_history in suppliers_data.items(): try: results = await self.analyze_and_post_supplier_insights( tenant_id=tenant_id, supplier_id=supplier_id, order_history=order_history, min_orders=min_orders ) all_results.append(results) total_insights_posted += results['insights_posted'] except Exception as e: logger.error( "Error analyzing supplier", supplier_id=supplier_id, error=str(e) ) # Compare suppliers comparison = self.predictor.compare_suppliers( [r for r in all_results if r.get('reliability_score') is not None] ) # Generate comparative insights if needed comparative_insights = self._generate_comparative_insights( tenant_id, comparison ) if comparative_insights: enriched_comparative = self._enrich_insights( comparative_insights, tenant_id, 'all_suppliers' ) post_results = await self.ai_insights_client.create_insights_bulk( tenant_id=UUID(tenant_id), insights=enriched_comparative ) total_insights_posted += post_results['successful'] logger.info( "All suppliers analysis complete", tenant_id=tenant_id, suppliers_analyzed=len(all_results), total_insights_posted=total_insights_posted ) return { 'tenant_id': tenant_id, 'analyzed_at': datetime.utcnow().isoformat(), 'suppliers_analyzed': len(all_results), 'supplier_results': all_results, 'comparison': comparison, 'total_insights_posted': total_insights_posted } def _generate_comparative_insights( self, tenant_id: str, comparison: Dict[str, Any] ) -> List[Dict[str, Any]]: """ Generate insights from supplier comparison. Args: tenant_id: Tenant identifier comparison: Supplier comparison results Returns: List of comparative insights """ insights = [] if 'recommendations' in comparison and comparison['recommendations']: for rec in comparison['recommendations']: if 'URGENT' in rec['recommendation']: priority = 'critical' elif 'high-risk' in rec.get('reason', '').lower(): priority = 'high' else: priority = 'medium' insights.append({ 'type': 'recommendation', 'priority': priority, 'category': 'procurement', 'title': 'Supplier Comparison: Action Required', 'description': rec['recommendation'], 'impact_type': 'cost_optimization', 'impact_value': 0, 'impact_unit': 'recommendation', 'confidence': 85, 'metrics_json': { 'comparison_type': 'multi_supplier', 'suppliers_compared': comparison['suppliers_compared'], 'top_supplier': comparison.get('top_supplier'), 'top_score': comparison.get('top_supplier_score'), 'reason': rec.get('reason', '') }, 'actionable': True, 'recommendation_actions': [ { 'label': 'Review Supplier Portfolio', 'action': 'review_supplier_portfolio', 'params': {'tenant_id': tenant_id} } ], 'source_service': 'procurement', 'source_model': 'supplier_performance_predictor' }) return insights async def get_supplier_risk_score( self, supplier_id: str ) -> Optional[int]: """ Get cached reliability score for a supplier. Args: supplier_id: Supplier identifier Returns: Reliability score (0-100) or None if not analyzed """ return self.predictor.get_supplier_reliability_score(supplier_id) async def close(self): """Close HTTP client connections.""" await self.ai_insights_client.close()