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

@@ -6,7 +6,7 @@ Provides endpoints to trigger ML insight generation for:
- Price forecasting and timing recommendations
"""
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
@@ -108,6 +108,7 @@ class PriceForecastResponse(BaseModel):
async def trigger_supplier_analysis(
tenant_id: str,
request_data: SupplierAnalysisRequest,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
@@ -142,8 +143,11 @@ async def trigger_supplier_analysis(
from app.core.config import settings
from sqlalchemy import select
# Get event publisher from app state
event_publisher = getattr(request.app.state, 'event_publisher', None)
# Initialize orchestrator and clients
orchestrator = SupplierInsightsOrchestrator()
orchestrator = SupplierInsightsOrchestrator(event_publisher=event_publisher)
suppliers_client = SuppliersServiceClient(settings)
# Get suppliers to analyze from suppliers service via API
@@ -319,6 +323,7 @@ async def trigger_supplier_analysis(
async def trigger_price_forecasting(
tenant_id: str,
request_data: PriceForecastRequest,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
@@ -353,8 +358,11 @@ async def trigger_price_forecasting(
from app.core.config import settings
from sqlalchemy import select
# Get event publisher from app state
event_publisher = getattr(request.app.state, 'event_publisher', None)
# Initialize orchestrator and inventory client
orchestrator = PriceInsightsOrchestrator()
orchestrator = PriceInsightsOrchestrator(event_publisher=event_publisher)
inventory_client = InventoryServiceClient(settings)
# Get ingredients to forecast from inventory service via API
@@ -594,6 +602,7 @@ async def generate_price_insights_internal(
result = await trigger_price_forecasting(
tenant_id=tenant_id,
request_data=request_data,
request=request,
db=db
)

View File

@@ -107,6 +107,7 @@ class ProcurementService(StandardFastAPIService):
# Store in app state for internal API access
app.state.delivery_tracking_service = self.delivery_tracking_service
app.state.event_publisher = self.event_publisher
# Start overdue PO scheduler
if self.rabbitmq_client and self.rabbitmq_client.connected:

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.price_forecaster import PriceForecaster
@@ -33,10 +34,12 @@ class PriceInsightsOrchestrator:
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.forecaster = PriceForecaster()
self.ai_insights_client = AIInsightsClient(ai_insights_base_url)
self.event_publisher = event_publisher
async def forecast_and_post_insights(
self,
@@ -107,7 +110,17 @@ class PriceInsightsOrchestrator:
post_results = {'total': 0, 'successful': 0, 'failed': 0}
logger.info("No insights to post for ingredient", ingredient_id=ingredient_id)
# Step 4: Return comprehensive results
# Step 4: Publish insight events to RabbitMQ
created_insights = post_results.get('created_insights', [])
if created_insights:
ingredient_context = {'ingredient_id': ingredient_id}
await self._publish_insight_events(
tenant_id=tenant_id,
insights=created_insights,
ingredient_context=ingredient_context
)
# Step 5: Return comprehensive results
return {
'tenant_id': tenant_id,
'ingredient_id': ingredient_id,
@@ -261,6 +274,71 @@ class PriceInsightsOrchestrator:
'bulk_opportunity_count': bulk_opportunity_count
}
async def _publish_insight_events(self, tenant_id, insights, ingredient_context=None):
"""
Publish insight events to RabbitMQ for alert processing.
Args:
tenant_id: Tenant identifier
insights: List of created insights
ingredient_context: Additional context about the ingredient
"""
if not self.event_publisher:
logger.warning("No event publisher available for price insights")
return
for insight in insights:
# Determine severity based on confidence and priority
confidence = insight.get('confidence', 0)
priority = insight.get('priority', 'medium')
# Map priority to severity, with confidence as tiebreaker
if priority == 'critical' or (priority == 'high' and confidence >= 70):
severity = 'high'
elif priority == 'high' or (priority == 'medium' and confidence >= 80):
severity = 'medium'
else:
severity = 'low'
# Prepare the event data
event_data = {
'insight_id': insight.get('id'),
'type': insight.get('type'),
'title': insight.get('title'),
'description': insight.get('description'),
'category': insight.get('category'),
'priority': insight.get('priority'),
'confidence': confidence,
'recommendation': insight.get('recommendation_actions', []),
'impact_type': insight.get('impact_type'),
'impact_value': insight.get('impact_value'),
'ingredient_id': ingredient_context.get('ingredient_id') if ingredient_context else None,
'timestamp': insight.get('detected_at', datetime.utcnow().isoformat()),
'source_service': 'procurement',
'source_model': 'price_forecaster'
}
try:
await self.event_publisher.publish_recommendation(
event_type='ai_price_forecast',
tenant_id=tenant_id,
severity=severity,
data=event_data
)
logger.info(
"Published price insight event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
severity=severity
)
except Exception as e:
logger.error(
"Failed to publish price insight event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
error=str(e)
)
def _generate_portfolio_summary_insight(
self,
tenant_id: str,

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.supplier_performance_predictor import SupplierPerformancePredictor
@@ -28,16 +29,19 @@ class SupplierInsightsOrchestrator:
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
4. Publish recommendation events to RabbitMQ
5. Provide supplier comparison and recommendations
6. Track supplier reliability scores
"""
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.predictor = SupplierPerformancePredictor()
self.ai_insights_client = AIInsightsClient(ai_insights_base_url)
self.event_publisher = event_publisher
async def analyze_and_post_supplier_insights(
self,
@@ -105,7 +109,17 @@ class SupplierInsightsOrchestrator:
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
# Step 4: Publish insight events to RabbitMQ
created_insights = post_results.get('created_insights', [])
if created_insights:
supplier_context = {'supplier_id': supplier_id}
await self._publish_insight_events(
tenant_id=tenant_id,
insights=created_insights,
supplier_context=supplier_context
)
# Step 5: Return comprehensive results
return {
'tenant_id': tenant_id,
'supplier_id': supplier_id,
@@ -159,6 +173,71 @@ class SupplierInsightsOrchestrator:
return enriched
async def _publish_insight_events(self, tenant_id, insights, supplier_context=None):
"""
Publish insight events to RabbitMQ for alert processing.
Args:
tenant_id: Tenant identifier
insights: List of created insights
supplier_context: Additional context about the supplier
"""
if not self.event_publisher:
logger.warning("No event publisher available for supplier insights")
return
for insight in insights:
# Determine severity based on confidence and priority
confidence = insight.get('confidence', 0)
priority = insight.get('priority', 'medium')
# Map priority to severity, with confidence as tiebreaker
if priority == 'critical' or (priority == 'high' and confidence >= 70):
severity = 'high'
elif priority == 'high' or (priority == 'medium' and confidence >= 80):
severity = 'medium'
else:
severity = 'low'
# Prepare the event data
event_data = {
'insight_id': insight.get('id'),
'type': insight.get('type'),
'title': insight.get('title'),
'description': insight.get('description'),
'category': insight.get('category'),
'priority': insight.get('priority'),
'confidence': confidence,
'recommendation': insight.get('recommendation_actions', []),
'impact_type': insight.get('impact_type'),
'impact_value': insight.get('impact_value'),
'supplier_id': supplier_context.get('supplier_id') if supplier_context else None,
'timestamp': insight.get('detected_at', datetime.utcnow().isoformat()),
'source_service': 'procurement',
'source_model': 'supplier_performance_predictor'
}
try:
await self.event_publisher.publish_recommendation(
event_type='ai_supplier_recommendation',
tenant_id=tenant_id,
severity=severity,
data=event_data
)
logger.info(
"Published supplier insight event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
severity=severity
)
except Exception as e:
logger.error(
"Failed to publish supplier insight event",
tenant_id=tenant_id,
insight_id=insight.get('id'),
error=str(e)
)
async def analyze_all_suppliers(
self,
tenant_id: str,