""" AI Insights Service HTTP Client Shared client for all services to post and retrieve AI insights """ import httpx from typing import Dict, List, Any, Optional from uuid import UUID import structlog from datetime import datetime logger = structlog.get_logger() class AIInsightsClient: """ HTTP client for AI Insights Service. Allows services to post insights, retrieve orchestration-ready insights, and record feedback. """ def __init__(self, base_url: str, timeout: int = 30): """ Initialize AI Insights client. Args: base_url: Base URL of AI Insights Service (e.g., http://ai-insights-service:8000) timeout: Request timeout in seconds """ self.base_url = base_url.rstrip('/') self.timeout = timeout self.client = httpx.AsyncClient(timeout=self.timeout) async def close(self): """Close the HTTP client.""" await self.client.aclose() async def create_insight( self, tenant_id: UUID, insight_data: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """ Create a new insight in AI Insights Service. Args: tenant_id: Tenant UUID insight_data: Insight data dictionary with fields: - type: str (optimization, alert, prediction, recommendation, insight, anomaly) - priority: str (low, medium, high, critical) - category: str (forecasting, procurement, production, inventory, etc.) - title: str - description: str - impact_type: str - impact_value: float - impact_unit: str - confidence: int (0-100) - metrics_json: dict - actionable: bool - recommendation_actions: list (optional) - source_service: str - source_model: str (optional) Returns: Created insight dict or None if failed """ url = f"{self.base_url}/api/v1/tenants/{tenant_id}/insights" try: # Ensure tenant_id is in the data insight_data['tenant_id'] = str(tenant_id) response = await self.client.post(url, json=insight_data) if response.status_code == 201: logger.info( "Insight created successfully", tenant_id=str(tenant_id), insight_title=insight_data.get('title') ) return response.json() else: logger.error( "Failed to create insight", status_code=response.status_code, response=response.text, insight_title=insight_data.get('title') ) return None except Exception as e: logger.error( "Error creating insight", error=str(e), tenant_id=str(tenant_id) ) return None async def create_insights_bulk( self, tenant_id: UUID, insights: List[Dict[str, Any]] ) -> Dict[str, Any]: """ Create multiple insights in bulk. Args: tenant_id: Tenant UUID insights: List of insight data dictionaries Returns: Dictionary with success/failure counts """ results = { 'total': len(insights), 'successful': 0, 'failed': 0, 'created_insights': [] } for insight_data in insights: result = await self.create_insight(tenant_id, insight_data) if result: results['successful'] += 1 results['created_insights'].append(result) else: results['failed'] += 1 logger.info( "Bulk insight creation complete", total=results['total'], successful=results['successful'], failed=results['failed'] ) return results async def get_insights( self, tenant_id: UUID, filters: Optional[Dict[str, Any]] = None ) -> Optional[Dict[str, Any]]: """ Get insights for a tenant. Args: tenant_id: Tenant UUID filters: Optional filters: - category: str - priority: str - actionable_only: bool - min_confidence: int - page: int - page_size: int Returns: Paginated insights response or None if failed """ url = f"{self.base_url}/api/v1/tenants/{tenant_id}/insights" try: response = await self.client.get(url, params=filters or {}) if response.status_code == 200: return response.json() else: logger.error( "Failed to get insights", status_code=response.status_code ) return None except Exception as e: logger.error("Error getting insights", error=str(e)) return None async def get_orchestration_ready_insights( self, tenant_id: UUID, target_date: datetime, min_confidence: int = 70 ) -> Optional[Dict[str, List[Dict[str, Any]]]]: """ Get insights ready for orchestration workflow. Args: tenant_id: Tenant UUID target_date: Target date for orchestration min_confidence: Minimum confidence threshold Returns: Categorized insights or None if failed: { "forecast_adjustments": [...], "procurement_recommendations": [...], "production_adjustments": [...], "inventory_optimization": [...], "risk_alerts": [...] } """ url = f"{self.base_url}/api/v1/tenants/{tenant_id}/insights/orchestration-ready" params = { 'target_date': target_date.isoformat(), 'min_confidence': min_confidence } try: response = await self.client.get(url, params=params) if response.status_code == 200: return response.json() else: logger.error( "Failed to get orchestration insights", status_code=response.status_code ) return None except Exception as e: logger.error("Error getting orchestration insights", error=str(e)) return None async def record_feedback( self, tenant_id: UUID, insight_id: UUID, feedback_data: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """ Record feedback for an applied insight. Args: tenant_id: Tenant UUID insight_id: Insight UUID feedback_data: Feedback data with fields: - success: bool - applied_at: datetime (optional) - actual_impact_value: float (optional) - actual_impact_unit: str (optional) - notes: str (optional) Returns: Feedback response or None if failed """ url = f"{self.base_url}/api/v1/tenants/{tenant_id}/insights/{insight_id}/feedback" try: feedback_data['insight_id'] = str(insight_id) response = await self.client.post(url, json=feedback_data) if response.status_code in [200, 201]: logger.info( "Feedback recorded", insight_id=str(insight_id), success=feedback_data.get('success') ) return response.json() else: logger.error( "Failed to record feedback", status_code=response.status_code ) return None except Exception as e: logger.error("Error recording feedback", error=str(e)) return None async def get_insights_summary( self, tenant_id: UUID, time_period_days: int = 30 ) -> Optional[Dict[str, Any]]: """ Get aggregate metrics summary for insights. Args: tenant_id: Tenant UUID time_period_days: Time period for metrics (default 30 days) Returns: Summary metrics or None if failed """ url = f"{self.base_url}/api/v1/tenants/{tenant_id}/insights/metrics/summary" params = {'time_period_days': time_period_days} try: response = await self.client.get(url, params=params) if response.status_code == 200: return response.json() else: logger.error( "Failed to get insights summary", status_code=response.status_code ) return None except Exception as e: logger.error("Error getting insights summary", error=str(e)) return None async def post_accuracy_metrics( self, tenant_id: UUID, validation_date: datetime, metrics: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """ Post forecast accuracy metrics to AI Insights Service. Creates an insight with accuracy validation results. Args: tenant_id: Tenant UUID validation_date: Date the forecasts were validated for metrics: Dictionary with accuracy metrics: - overall_mape: Mean Absolute Percentage Error - overall_rmse: Root Mean Squared Error - overall_mae: Mean Absolute Error - products_validated: Number of products validated - poor_accuracy_products: List of products with MAPE > 30% Returns: Created insight or None if failed """ mape = metrics.get('overall_mape', 0) products_validated = metrics.get('products_validated', 0) poor_count = len(metrics.get('poor_accuracy_products', [])) # Determine priority based on MAPE if mape > 40: priority = 'critical' elif mape > 30: priority = 'high' elif mape > 20: priority = 'medium' else: priority = 'low' # Create insight insight_data = { 'type': 'insight', 'priority': priority, 'category': 'forecasting', 'title': f'Forecast Accuracy Validation - {validation_date.strftime("%Y-%m-%d")}', 'description': ( f'Validated {products_validated} product forecasts against actual sales. ' f'Overall MAPE: {mape:.2f}%. ' f'{poor_count} products require retraining (MAPE > 30%).' ), 'impact_type': 'accuracy', 'impact_value': mape, 'impact_unit': 'mape_percentage', 'confidence': 100, # Validation is based on actual data 'metrics_json': { 'validation_date': validation_date.isoformat() if hasattr(validation_date, 'isoformat') else str(validation_date), 'overall_mape': mape, 'overall_rmse': metrics.get('overall_rmse', 0), 'overall_mae': metrics.get('overall_mae', 0), 'products_validated': products_validated, 'poor_accuracy_count': poor_count, 'poor_accuracy_products': metrics.get('poor_accuracy_products', []) }, 'actionable': poor_count > 0, 'recommendation_actions': [ f'Retrain models for {poor_count} products with poor accuracy' ] if poor_count > 0 else [], 'source_service': 'forecasting', 'source_model': 'forecast_validation' } return await self.create_insight(tenant_id, insight_data) async def health_check(self) -> bool: """ Check if AI Insights Service is healthy. Returns: True if healthy, False otherwise """ url = f"{self.base_url}/health" try: response = await self.client.get(url) return response.status_code == 200 except Exception as e: logger.error("AI Insights Service health check failed", error=str(e)) return False