Add AI insights feature

This commit is contained in:
Urtzi Alfaro
2025-12-15 21:14:22 +01:00
parent 5642b5a0c0
commit c566967bea
39 changed files with 17729 additions and 404 deletions

View File

@@ -7,7 +7,7 @@ Provides endpoints to trigger ML insight generation for:
- Demand pattern analysis
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from typing import Optional, List
from uuid import UUID
@@ -71,6 +71,7 @@ class SafetyStockOptimizationResponse(BaseModel):
async def trigger_safety_stock_optimization(
tenant_id: str,
request_data: SafetyStockOptimizationRequest,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
@@ -81,10 +82,12 @@ async def trigger_safety_stock_optimization(
2. Runs the SafetyStockInsightsOrchestrator to optimize levels
3. Generates insights about safety stock recommendations
4. Posts insights to AI Insights Service
5. Publishes recommendation events to RabbitMQ
Args:
tenant_id: Tenant UUID
request_data: Optimization parameters
request: FastAPI request (for app state access)
db: Database session
Returns:
@@ -103,8 +106,13 @@ async def trigger_safety_stock_optimization(
from app.models.inventory import Ingredient
from sqlalchemy import select
# Get event publisher from app state (if available)
event_publisher = getattr(request.app.state, 'event_publisher', None) if hasattr(request, 'app') else None
# Initialize orchestrator
orchestrator = SafetyStockInsightsOrchestrator()
orchestrator = SafetyStockInsightsOrchestrator(
event_publisher=event_publisher
)
# Get products to optimize
if request_data.product_ids:
@@ -378,6 +386,7 @@ async def generate_safety_stock_insights_internal(
result = await trigger_safety_stock_optimization(
tenant_id=tenant_id,
request_data=request_data,
request=request,
db=db
)

View File

@@ -126,6 +126,7 @@ class InventoryService(StandardFastAPIService):
# Store services in app state
app.state.alert_service = alert_service
app.state.inventory_scheduler = inventory_scheduler # Store scheduler for manual triggering
app.state.event_publisher = self.event_publisher # Store event publisher for ML insights
else:
self.logger.error("Event publisher not initialized, alert service unavailable")

View File

@@ -14,6 +14,7 @@ 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 shared.messaging import UnifiedEventPublisher
from app.ml.safety_stock_optimizer import SafetyStockOptimizer
@@ -28,15 +29,18 @@ class SafetyStockInsightsOrchestrator:
1. Optimize safety stock from demand history and cost parameters
2. Generate insights comparing optimal vs hardcoded approach
3. Post insights to AI Insights Service
4. Provide optimized safety stock levels for inventory management
4. Publish recommendation events to RabbitMQ
5. Provide optimized safety stock levels for inventory management
"""
def __init__(
self,
ai_insights_base_url: str = "http://ai-insights-service:8000"
ai_insights_base_url: str = "http://ai-insights-service:8000",
event_publisher: Optional[UnifiedEventPublisher] = None
):
self.optimizer = SafetyStockOptimizer()
self.ai_insights_client = AIInsightsClient(ai_insights_base_url)
self.event_publisher = event_publisher
async def optimize_and_post_insights(
self,
@@ -109,6 +113,17 @@ class SafetyStockInsightsOrchestrator:
successful=post_results['successful'],
failed=post_results['failed']
)
# Step 4: Publish recommendation events to RabbitMQ
created_insights = post_results.get('created_insights', [])
if created_insights:
product_context = product_characteristics.copy() if product_characteristics else {}
product_context['inventory_product_id'] = inventory_product_id
await self._publish_insight_events(
tenant_id=tenant_id,
insights=created_insights,
product_context=product_context
)
else:
post_results = {'total': 0, 'successful': 0, 'failed': 0}
logger.info("No insights to post for product", inventory_product_id=inventory_product_id)
@@ -167,6 +182,84 @@ class SafetyStockInsightsOrchestrator:
return enriched
async def _publish_insight_events(
self,
tenant_id: str,
insights: List[Dict[str, Any]],
product_context: Optional[Dict[str, Any]] = None
) -> None:
"""
Publish recommendation events to RabbitMQ for each insight.
Args:
tenant_id: Tenant identifier
insights: List of created insights (with insight_id from AI Insights Service)
product_context: Optional product context (name, id, etc.)
"""
if not self.event_publisher:
logger.warning("Event publisher not configured, skipping event publication")
return
for insight in insights:
try:
# Determine severity based on confidence and priority
confidence = insight.get('confidence', 0)
priority = insight.get('priority', 'medium')
if priority == 'urgent' or confidence >= 90:
severity = 'urgent'
elif priority == 'high' or confidence >= 70:
severity = 'high'
elif priority == 'medium' or confidence >= 50:
severity = 'medium'
else:
severity = 'low'
# Build event metadata
event_metadata = {
'insight_id': insight.get('id'),
'insight_type': insight.get('insight_type'),
'inventory_product_id': insight.get('metrics_json', {}).get('inventory_product_id'),
'ingredient_name': product_context.get('ingredient_name') if product_context else None,
'suggested_safety_stock': insight.get('metrics_json', {}).get('suggested_safety_stock'),
'current_safety_stock': insight.get('metrics_json', {}).get('current_safety_stock'),
'estimated_savings': insight.get('impact_value'),
'confidence': confidence,
'recommendation': insight.get('recommendation'),
'impact_type': insight.get('impact_type'),
'source_service': 'inventory',
'source_model': 'safety_stock_optimizer'
}
# Remove None values
event_metadata = {k: v for k, v in event_metadata.items() if v is not None}
# Publish recommendation event
await self.event_publisher.publish_recommendation(
event_type='ai_safety_stock_optimization',
tenant_id=tenant_id,
severity=severity,
data=event_metadata
)
logger.info(
"Published safety stock insight recommendation event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
insight_type=insight.get('insight_type'),
severity=severity
)
except Exception as e:
logger.error(
"Failed to publish insight event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
error=str(e),
exc_info=True
)
# Don't raise - we don't want to fail the whole workflow if event publishing fails
async def optimize_all_products(
self,
tenant_id: str,