392 lines
12 KiB
Python
392 lines
12 KiB
Python
"""
|
|
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
|