322 lines
13 KiB
Python
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"), # Professional
|
|
uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7")
|
|
]
|
|
|
|
# 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())
|