Files
bakery-ia/services/alert_processor/scripts/demo/seed_demo_alerts.py
2025-11-27 15:52:40 +01:00

322 lines
13 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Alert Seeding Script for Alert Processor Service
ONLY seeds prevented-issue alerts (AI interventions with financial impact).
Action-needed alerts are system-generated and should not be seeded.
All alerts reference real seed data:
- Real ingredient IDs from inventory seed
- Real supplier IDs from supplier seed
- Real product names from recipes seed
- Historical data over past 7 days for trend analysis
"""
import asyncio
import uuid
import sys
import os
import random
from datetime import datetime, timezone, timedelta
from pathlib import Path
from decimal import Decimal
from typing import List, Dict, Any
# Add app to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select, delete
import structlog
from app.models.events import Alert, AlertStatus, PriorityLevel, AlertTypeClass
from app.config import AlertProcessorConfig
# Add shared utilities to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from shared.utils.demo_dates import BASE_REFERENCE_DATE, adjust_date_for_demo
# Configure logging
logger = structlog.get_logger()
# Demo tenant IDs (match those from other services)
DEMO_TENANT_IDS = [
uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"), # San Pablo
uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # La Espiga
]
# System user ID for AI actions
SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004")
# ============================================================================
# REAL SEED DATA IDs (from demo seed scripts)
# ============================================================================
# Real ingredient IDs from inventory seed
HARINA_T55_ID = "10000000-0000-0000-0000-000000000001"
MANTEQUILLA_ID = "10000000-0000-0000-0000-000000000007"
HUEVOS_ID = "10000000-0000-0000-0000-000000000008"
AZUCAR_BLANCO_ID = "10000000-0000-0000-0000-000000000005"
NUECES_ID = "10000000-0000-0000-0000-000000000018"
PASAS_ID = "10000000-0000-0000-0000-000000000019"
# Real supplier IDs from supplier seed
MOLINOS_SAN_JOSE_ID = "40000000-0000-0000-0000-000000000001"
LACTEOS_DEL_VALLE_ID = "40000000-0000-0000-0000-000000000002"
# Real product names from recipes seed
PAN_DE_PUEBLO = "Pan de Pueblo"
BAGUETTE_FRANCESA = "Baguette Francesa Tradicional"
PAN_RUSTICO_CEREALES = "Pan Rústico de Cereales"
def create_prevented_issue_alerts(tenant_id: uuid.UUID, reference_time: datetime) -> List[Alert]:
"""Create prevented issue alerts showing AI interventions with financial impact"""
alerts = []
# Historical prevented issues over past 7 days
prevented_scenarios = [
{
"days_ago": 1,
"title": "Problema Evitado: Exceso de Stock de Harina",
"message": "Detecté que la orden automática iba a crear sobrestockexceso. Reduje la cantidad de 150kg a 100kg.",
"alert_type": "prevented_overstock",
"service": "inventory",
"priority_score": 55,
"priority_level": "standard",
"financial_impact": 87.50,
"ai_reasoning": "El análisis de demanda mostró una disminución del 15% en productos con harina. Ajusté la orden para evitar desperdicio.",
"confidence": 0.88,
"metadata": {
"ingredient_id": HARINA_T55_ID,
"ingredient": "Harina de Trigo T55",
"original_quantity_kg": 150,
"adjusted_quantity_kg": 100,
"savings_eur": 87.50,
"waste_prevented_kg": 50,
"supplier_id": MOLINOS_SAN_JOSE_ID,
"supplier": "Molinos San José S.L."
}
},
{
"days_ago": 2,
"title": "Problema Evitado: Conflicto de Equipamiento",
"message": "Evité un conflicto en el horno principal reprogramando el lote de baguettes 30 minutos antes.",
"alert_type": "prevented_equipment_conflict",
"service": "production",
"priority_score": 70,
"priority_level": "important",
"financial_impact": 0,
"ai_reasoning": "Dos lotes estaban programados para el mismo horno. Reprogramé automáticamente para optimizar el uso.",
"confidence": 0.94,
"metadata": {
"equipment": "Horno Principal",
"product_name": BAGUETTE_FRANCESA,
"batch_rescheduled": BAGUETTE_FRANCESA,
"time_adjustment_minutes": 30,
"downtime_prevented_minutes": 45
}
},
{
"days_ago": 3,
"title": "Problema Evitado: Compra Duplicada",
"message": "Detecté dos órdenes de compra casi idénticas para mantequilla. Cancelé la duplicada automáticamente.",
"alert_type": "prevented_duplicate_po",
"service": "procurement",
"priority_score": 62,
"priority_level": "standard",
"financial_impact": 245.80,
"ai_reasoning": "Dos servicios crearon órdenes similares con 10 minutos de diferencia. Cancelé la segunda para evitar sobrepedido.",
"confidence": 0.96,
"metadata": {
"ingredient_id": MANTEQUILLA_ID,
"ingredient": "Mantequilla sin Sal 82% MG",
"duplicate_po_amount": 245.80,
"time_difference_minutes": 10,
"supplier_id": LACTEOS_DEL_VALLE_ID,
"supplier": "Lácteos del Valle S.A."
}
},
{
"days_ago": 4,
"title": "Problema Evitado: Caducidad Inminente",
"message": "Prioricé automáticamente el uso de huevos que caducan en 2 días en lugar de stock nuevo.",
"alert_type": "prevented_expiration_waste",
"service": "inventory",
"priority_score": 58,
"priority_level": "standard",
"financial_impact": 34.50,
"ai_reasoning": "Detecté stock próximo a caducar. Ajusté el plan de producción para usar primero los ingredientes más antiguos.",
"confidence": 0.90,
"metadata": {
"ingredient_id": HUEVOS_ID,
"ingredient": "Huevos Frescos Categoría A",
"quantity_prioritized": 120,
"days_until_expiration": 2,
"waste_prevented_eur": 34.50
}
},
{
"days_ago": 5,
"title": "Problema Evitado: Sobrepago a Proveedor",
"message": "Detecté una discrepancia de precio en la orden de azúcar blanco. Precio cotizado: €2.20/kg, precio esperado: €1.85/kg.",
"alert_type": "prevented_price_discrepancy",
"service": "procurement",
"priority_score": 68,
"priority_level": "standard",
"financial_impact": 17.50,
"ai_reasoning": "El precio era 18.9% mayor que el histórico. Rechacé la orden automáticamente y notifiqué al proveedor.",
"confidence": 0.85,
"metadata": {
"ingredient_id": AZUCAR_BLANCO_ID,
"ingredient": "Azúcar Blanco Refinado",
"quoted_price_per_kg": 2.20,
"expected_price_per_kg": 1.85,
"quantity_kg": 50,
"savings_eur": 17.50,
"supplier": "Varios Distribuidores"
}
},
{
"days_ago": 6,
"title": "Problema Evitado: Pedido Sin Ingredientes",
"message": f"Un pedido de cliente incluía {PAN_RUSTICO_CEREALES}, pero no había suficiente stock. Sugerí sustitución con {PAN_DE_PUEBLO}.",
"alert_type": "prevented_unfulfillable_order",
"service": "orders",
"priority_score": 75,
"priority_level": "important",
"financial_impact": 0,
"ai_reasoning": "Detecté que el pedido no podía cumplirse con el stock actual. Ofrecí automáticamente una alternativa antes de confirmar.",
"confidence": 0.92,
"metadata": {
"original_product": PAN_RUSTICO_CEREALES,
"missing_ingredients": ["Semillas de girasol", "Semillas de sésamo"],
"suggested_alternative": PAN_DE_PUEBLO,
"customer_satisfaction_preserved": True
}
},
]
for scenario in prevented_scenarios:
created_at = reference_time - timedelta(days=scenario["days_ago"])
resolved_at = created_at + timedelta(seconds=1) # Instantly resolved by AI
alert = Alert(
id=uuid.uuid4(),
tenant_id=tenant_id,
item_type="alert",
alert_type=scenario["alert_type"],
service=scenario["service"],
title=scenario["title"],
message=scenario["message"],
status=AlertStatus.RESOLVED, # Already resolved by AI
priority_score=scenario["priority_score"],
priority_level=scenario["priority_level"],
type_class="prevented_issue", # KEY: This classifies as prevented
orchestrator_context={
"created_by": "ai_intervention_system",
"auto_resolved": True,
"resolution_method": "automatic"
},
business_impact={
"financial_impact": scenario["financial_impact"],
"currency": "EUR",
"orders_affected": scenario["metadata"].get("orders_affected", 0),
"impact_description": f"Ahorro estimado: €{scenario['financial_impact']:.2f}" if scenario["financial_impact"] > 0 else "Operación mejorada"
},
urgency_context={
"time_until_consequence": "0 segundos",
"consequence": "Problema resuelto automáticamente",
"resolution_time_ms": random.randint(100, 500)
},
user_agency={
"user_can_fix": False, # AI already fixed it
"requires_supplier": False,
"requires_external_party": False,
"estimated_resolution_time": "Automático"
},
trend_context=None,
smart_actions=[], # No actions needed - already resolved
ai_reasoning_summary=scenario["ai_reasoning"],
confidence_score=scenario["confidence"],
timing_decision="send_now",
scheduled_send_time=None,
placement=["dashboard"], # Only dashboard - not urgent since already resolved
action_created_at=None,
superseded_by_action_id=None,
hidden_from_ui=False,
alert_metadata=scenario["metadata"],
created_at=created_at,
updated_at=resolved_at,
resolved_at=resolved_at
)
alerts.append(alert)
return alerts
async def seed_demo_alerts():
"""Main function to seed demo alerts"""
logger.info("Starting demo alert seeding")
# Initialize database
config = AlertProcessorConfig()
engine = create_async_engine(config.DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
try:
# Delete existing alerts for demo tenants
for tenant_id in DEMO_TENANT_IDS:
logger.info("Deleting existing alerts", tenant_id=str(tenant_id))
await session.execute(
delete(Alert).where(Alert.tenant_id == tenant_id)
)
await session.commit()
logger.info("Existing alerts deleted")
# Create alerts for each tenant
reference_time = datetime.now(timezone.utc)
total_alerts_created = 0
for tenant_id in DEMO_TENANT_IDS:
logger.info("Creating prevented-issue alerts for tenant", tenant_id=str(tenant_id))
# Create prevented-issue alerts (historical AI interventions)
# NOTE: Action-needed alerts are NOT seeded - they are system-generated
prevented_alerts = create_prevented_issue_alerts(tenant_id, reference_time)
for alert in prevented_alerts:
session.add(alert)
logger.info(f"Created {len(prevented_alerts)} prevented-issue alerts")
total_alerts_created += len(prevented_alerts)
# Commit all alerts
await session.commit()
logger.info(
"Demo alert seeding completed",
total_alerts=total_alerts_created,
tenants=len(DEMO_TENANT_IDS)
)
print(f"\n✅ Successfully seeded {total_alerts_created} demo alerts")
print(f" - Prevented-issue alerts (AI interventions): {len(prevented_alerts) * len(DEMO_TENANT_IDS)}")
print(f" - Action-needed alerts: 0 (system-generated, not seeded)")
print(f" - Tenants: {len(DEMO_TENANT_IDS)}")
print(f"\n📝 Note: All alerts reference real seed data (ingredients, suppliers, products)")
except Exception as e:
logger.error("Error seeding demo alerts", error=str(e), exc_info=True)
await session.rollback()
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_demo_alerts())