Improve AI logic
This commit is contained in:
391
shared/clients/ai_insights_client.py
Normal file
391
shared/clients/ai_insights_client.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user