Initial commit - production deployment
This commit is contained in:
440
services/demo_session/scripts/README.md
Normal file
440
services/demo_session/scripts/README.md
Normal file
@@ -0,0 +1,440 @@
|
||||
# Dashboard Demo Seed Scripts
|
||||
|
||||
Comprehensive demo data seeding scripts for the JTBD-aligned dashboard.
|
||||
|
||||
## 🎯 Purpose
|
||||
|
||||
These scripts create realistic demo data to showcase all dashboard features and user flows:
|
||||
|
||||
- **Time-based Action Queue** (URGENT/TODAY/WEEK grouping)
|
||||
- **AI Prevented Issues** (showcasing AI value)
|
||||
- **Execution Progress Tracking** (production/deliveries/approvals)
|
||||
- **Stock Receipt Modal** workflows
|
||||
- **Health Status** tri-state checklist
|
||||
- **All Alert Types** with full enrichment
|
||||
|
||||
## 📋 Available Scripts
|
||||
|
||||
### 1. `seed_dashboard_comprehensive.py` ⭐ **RECOMMENDED**
|
||||
|
||||
**Comprehensive dashboard demo covering ALL scenarios**
|
||||
|
||||
**What it seeds:**
|
||||
- 🔴 **URGENT** actions (<6h deadline): 3 alerts
|
||||
- PO approval escalation (72h aged, 2h deadline)
|
||||
- Delivery overdue (4h late, supplier contact needed)
|
||||
- Batch at risk (missing ingredients, 5h window)
|
||||
|
||||
- 🟡 **TODAY** actions (<24h deadline): 3 alerts
|
||||
- PO approval needed (dairy products, 20h deadline)
|
||||
- Delivery arriving soon (8h, prep required)
|
||||
- Low stock warning (yeast, order today recommended)
|
||||
|
||||
- 🟢 **THIS WEEK** actions (<7d deadline): 2 alerts
|
||||
- Weekend demand surge prediction
|
||||
- Stock receipt incomplete (2 days old)
|
||||
|
||||
- ✅ **AI PREVENTED ISSUES**: 3 alerts
|
||||
- Prevented stockout (PO created, €250 saved)
|
||||
- Prevented waste (production adjusted, €120 saved)
|
||||
- Prevented delay (batches rescheduled, €85 saved)
|
||||
|
||||
**Expected Dashboard State:**
|
||||
```
|
||||
Health Status: YELLOW (actions needed)
|
||||
├─ ⚡ AI Handled: 3 issues (€455 saved)
|
||||
└─ ⚠️ Needs You: 8 actions
|
||||
|
||||
Action Queue: 8 total actions
|
||||
├─ 🔴 URGENT: 3
|
||||
├─ 🟡 TODAY: 3
|
||||
└─ 🟢 WEEK: 2
|
||||
|
||||
AI Impact: €455 in prevented costs
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Quick start
|
||||
python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
|
||||
# With custom tenant
|
||||
DEMO_TENANT_ID=your-tenant-id python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
|
||||
# With custom RabbitMQ
|
||||
RABBITMQ_URL=amqp://user:pass@host:5672/ python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `seed_enriched_alert_demo.py`
|
||||
|
||||
**Legacy enriched alert demo** (basic scenarios)
|
||||
|
||||
Seeds 5 basic alert types with automatic enrichment:
|
||||
- Low stock (AI handled)
|
||||
- Supplier delay (critical)
|
||||
- Waste trend (standard)
|
||||
- Forecast anomaly (info)
|
||||
- Equipment maintenance (medium)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python services/demo_session/scripts/seed_enriched_alert_demo.py
|
||||
```
|
||||
|
||||
**Note:** For full dashboard testing, use `seed_dashboard_comprehensive.py` instead.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **RabbitMQ running:**
|
||||
```bash
|
||||
kubectl get pods | grep rabbitmq
|
||||
# Should show: rabbitmq-0 1/1 Running
|
||||
```
|
||||
|
||||
2. **Alert Processor service running:**
|
||||
```bash
|
||||
kubectl get pods -l app.kubernetes.io/name=alert-processor-service
|
||||
# Should show: alert-processor-service-xxx 1/1 Running
|
||||
```
|
||||
|
||||
3. **Python dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Running the Demo
|
||||
|
||||
```bash
|
||||
# 1. Navigate to project root
|
||||
cd /path/to/bakery-ia
|
||||
|
||||
# 2. Load environment variables (if needed)
|
||||
source .env
|
||||
|
||||
# 3. Run comprehensive dashboard seeder
|
||||
python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
================================================================================
|
||||
🚀 SEEDING COMPREHENSIVE DASHBOARD DEMO DATA
|
||||
================================================================================
|
||||
|
||||
📋 Configuration:
|
||||
Tenant ID: demo-tenant-bakery-ia
|
||||
RabbitMQ: amqp://guest:guest@localhost:5672/
|
||||
|
||||
📊 Dashboard Scenarios to Seed:
|
||||
🔴 URGENT actions (<6h): 3
|
||||
🟡 TODAY actions (<24h): 3
|
||||
🟢 WEEK actions (<7d): 2
|
||||
✅ AI Prevented Issues: 3
|
||||
📦 Total Alerts: 11
|
||||
|
||||
📤 Publishing Alerts:
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
1. ✅ [🔴 URGENT] URGENT: PO Approval Needed - Yeast Supplier
|
||||
2. ✅ [🔴 URGENT] Delivery Overdue: Flour Delivery
|
||||
3. ✅ [🔴 URGENT] Batch At Risk: Missing Ingredients
|
||||
4. ✅ [🟡 TODAY] PO Approval: Butter & Dairy Products
|
||||
5. ✅ [🟡 TODAY] Delivery Arriving in 8 Hours: Sugar & Ingredients
|
||||
6. ✅ [🟡 TODAY] Low Stock: Fresh Yeast
|
||||
7. ✅ [🟢 WEEK] Weekend Demand Surge Predicted
|
||||
8. ✅ [🟢 WEEK] Stock Receipt Pending: Flour Delivery
|
||||
9. ✅ [✅ PREVENTED] ✅ AI Prevented Stockout: Flour
|
||||
10. ✅ [✅ PREVENTED] ✅ AI Prevented Waste: Reduced Monday Production
|
||||
11. ✅ [✅ PREVENTED] ✅ AI Prevented Delay: Rescheduled Conflicting Batches
|
||||
|
||||
✅ Published 11/11 alerts successfully
|
||||
|
||||
================================================================================
|
||||
🎉 DASHBOARD DEMO SEEDED SUCCESSFULLY!
|
||||
================================================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verification
|
||||
|
||||
### 1. Check Alert Processing
|
||||
|
||||
```bash
|
||||
# View alert-processor logs (real-time)
|
||||
kubectl logs -f deployment/alert-processor-service | grep 'enriched_alert'
|
||||
|
||||
# Should see:
|
||||
# alert_enriched alert_id=xxx type_class=action_needed priority_score=92
|
||||
# alert_enriched alert_id=xxx type_class=prevented_issue priority_score=35
|
||||
```
|
||||
|
||||
### 2. Access Dashboard
|
||||
|
||||
```bash
|
||||
# Port forward if needed
|
||||
kubectl port-forward svc/frontend-service 3000:3000
|
||||
|
||||
# Open browser
|
||||
open http://localhost:3000/dashboard
|
||||
```
|
||||
|
||||
### 3. Verify Dashboard Sections
|
||||
|
||||
**✅ Health Status Card:**
|
||||
- Should show YELLOW status
|
||||
- Tri-state checklist items visible
|
||||
- AI prevented issues badge showing "3 issues prevented"
|
||||
|
||||
**✅ Action Queue Card:**
|
||||
- 🔴 URGENT section with 3 items (2h countdown visible)
|
||||
- 🟡 TODAY section with 3 items
|
||||
- 🟢 THIS WEEK section with 2 items
|
||||
|
||||
**✅ Orchestration Summary:**
|
||||
- "User Needed: 8" in yellow (clickable)
|
||||
- "AI Prevented: 3 issues" in green badge
|
||||
|
||||
**✅ AI Impact Card:**
|
||||
- Shows €455 total savings
|
||||
- Lists 3 prevented issues
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Scenarios
|
||||
|
||||
### Scenario 1: Urgent Action with Countdown
|
||||
|
||||
**Test:** PO Approval Escalation (2h deadline)
|
||||
|
||||
1. Navigate to dashboard
|
||||
2. Find "URGENT: PO Approval Needed - Yeast Supplier" in 🔴 URGENT section
|
||||
3. Verify countdown timer shows ~2 hours
|
||||
4. Click "Approve" button
|
||||
5. Verify alert moves to resolved/archived
|
||||
|
||||
**Expected Smart Actions:**
|
||||
- ✅ Approve PO (primary, green)
|
||||
- ⚙️ Modify PO (secondary)
|
||||
- ❌ Reject PO (danger, red)
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Stock Receipt Modal
|
||||
|
||||
**Test:** Mark Delivery as Received
|
||||
|
||||
1. Find "Delivery Arriving in 8 Hours" in 🟡 TODAY section
|
||||
2. Click "Mark as Received" button
|
||||
3. **Stock Receipt Modal should open:**
|
||||
- Shows PO details (supplier, items)
|
||||
- Lot input fields for each line item
|
||||
- Quantity validation (lots must sum to actual)
|
||||
- Mandatory expiration dates
|
||||
4. Fill in lot details:
|
||||
- Lot number (e.g., "LOT-2024-089")
|
||||
- Quantity per lot
|
||||
- Expiration date (required)
|
||||
- Warehouse location
|
||||
5. Click "Confirm Receipt"
|
||||
6. Verify inventory is updated
|
||||
|
||||
**Expected Validation:**
|
||||
- ❌ Error if lot quantities don't sum to actual quantity
|
||||
- ❌ Error if expiration date missing
|
||||
- ✅ Success toast on confirmation
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: AI Prevented Issue Showcase
|
||||
|
||||
**Test:** View AI Value Proposition
|
||||
|
||||
1. Find "✅ AI Prevented Stockout: Flour" in alert list
|
||||
2. Verify prevented issue badge (⚡ lightning bolt)
|
||||
3. Click to expand reasoning
|
||||
4. **Should show:**
|
||||
- AI action taken: "Purchase order created automatically"
|
||||
- Savings: €250
|
||||
- Reasoning: "Detected stock would run out in 1.8 days..."
|
||||
- Business impact: "Secured 4 production batches"
|
||||
|
||||
5. Navigate to Orchestration Summary Card
|
||||
6. Verify "AI Prevented: 3 issues" badge shows €455 total
|
||||
|
||||
---
|
||||
|
||||
### Scenario 4: Call Supplier (External Action)
|
||||
|
||||
**Test:** Supplier contact integration
|
||||
|
||||
1. Find "Delivery Overdue: Flour Delivery" in 🔴 URGENT section
|
||||
2. Click "Call Supplier" button
|
||||
3. **Expected behavior:**
|
||||
- Phone dialer opens with +34-555-5678
|
||||
- OR clipboard copies phone number
|
||||
- Toast notification confirms action
|
||||
|
||||
**Metadata displayed:**
|
||||
- Supplier: Harinera San José
|
||||
- Phone: +34-555-5678
|
||||
- Email: pedidos@harinerasj.es
|
||||
- Hours overdue: 4
|
||||
|
||||
---
|
||||
|
||||
### Scenario 5: Navigation to Linked Pages
|
||||
|
||||
**Test:** Smart action navigation
|
||||
|
||||
1. Find "Batch At Risk: Missing Ingredients" in 🔴 URGENT
|
||||
2. Click "View Production" button
|
||||
3. **Should navigate to:** `/production?batch_id=batch-chocolate-cake-evening`
|
||||
4. Production page shows batch details
|
||||
5. Missing ingredients highlighted
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Issue: Alerts not appearing in dashboard
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
# 1. Verify RabbitMQ is running
|
||||
kubectl get pods | grep rabbitmq
|
||||
|
||||
# 2. Check alert-processor logs
|
||||
kubectl logs deployment/alert-processor-service --tail=100
|
||||
|
||||
# 3. Verify alerts.exchange exists
|
||||
# (Check RabbitMQ management UI: localhost:15672)
|
||||
|
||||
# 4. Check for errors in seeder output
|
||||
python services/demo_session/scripts/seed_dashboard_comprehensive.py 2>&1 | grep ERROR
|
||||
```
|
||||
|
||||
**Common Fixes:**
|
||||
- Restart alert-processor: `kubectl rollout restart deployment/alert-processor-service`
|
||||
- Re-run seeder with debug: `python -u services/demo_session/scripts/seed_dashboard_comprehensive.py`
|
||||
- Check RabbitMQ queue: `raw_alerts_queue` should have consumers
|
||||
|
||||
---
|
||||
|
||||
### Issue: Countdown timer not working
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
# Verify urgency_context.auto_action_countdown_seconds is set
|
||||
# Should be in alert metadata
|
||||
```
|
||||
|
||||
**Fix:** Re-run seeder to ensure urgency_context is populated
|
||||
|
||||
---
|
||||
|
||||
### Issue: Stock Receipt Modal not opening
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
# 1. Verify modal component is imported in DashboardPage
|
||||
grep -r "StockReceiptModal" frontend/src/pages/app/DashboardPage.tsx
|
||||
|
||||
# 2. Check browser console for errors
|
||||
# Look for: "delivery:mark-received event not handled"
|
||||
|
||||
# 3. Verify smartActionHandlers.ts is loaded
|
||||
```
|
||||
|
||||
**Fix:** Ensure event listener is registered in DashboardPage.tsx
|
||||
|
||||
---
|
||||
|
||||
## 📊 Data Reference
|
||||
|
||||
### Alert Type Classes
|
||||
|
||||
- `action_needed` - Requires user decision (yellow)
|
||||
- `prevented_issue` - AI already handled (blue/green)
|
||||
- `trend_warning` - Proactive insight (info)
|
||||
- `escalation` - Time-sensitive with countdown (red)
|
||||
- `information` - Pure informational (gray)
|
||||
|
||||
### Priority Levels
|
||||
|
||||
- `critical` (90-100) - Needs decision in 2 hours
|
||||
- `important` (70-89) - Needs decision today
|
||||
- `standard` (50-69) - Review when convenient
|
||||
- `info` (0-49) - For awareness
|
||||
|
||||
### Time Groups
|
||||
|
||||
- 🔴 **URGENT** - Deadline <6 hours
|
||||
- 🟡 **TODAY** - Deadline <24 hours
|
||||
- 🟢 **THIS WEEK** - Deadline <7 days
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Resetting Demo Data
|
||||
|
||||
To clear all demo alerts and start fresh:
|
||||
|
||||
```bash
|
||||
# 1. Delete all alerts for demo tenant
|
||||
# (This requires admin access to alert-processor DB)
|
||||
|
||||
# 2. Or restart alert-processor (clears in-memory cache)
|
||||
kubectl rollout restart deployment/alert-processor-service
|
||||
|
||||
# 3. Re-run seeder
|
||||
python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **Automatic Enrichment:** All alerts are automatically enriched by alert-processor service
|
||||
- **Priority Scoring:** Multi-factor algorithm considers urgency, impact, user agency
|
||||
- **Smart Actions:** Dynamically generated based on alert type and context
|
||||
- **Real-time Updates:** Dashboard subscribes to SSE for live alert updates
|
||||
- **i18n Support:** All alerts support EN/ES/EU languages
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
After seeding:
|
||||
|
||||
1. **Test all smart actions** (approve, reject, call, navigate, etc.)
|
||||
2. **Verify performance** (<500ms dashboard load time)
|
||||
3. **Test responsive design** (mobile, tablet, desktop)
|
||||
4. **Check translations** (switch language in UI)
|
||||
5. **Test SSE updates** (create new alert, see real-time update)
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
To add new demo scenarios:
|
||||
|
||||
1. Edit `seed_dashboard_comprehensive.py`
|
||||
2. Add new alert to appropriate function (`create_urgent_actions()`, etc.)
|
||||
3. Include full metadata for enrichment
|
||||
4. Test enrichment output
|
||||
5. Update this README with new scenario
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Alert Type Schemas](../../../shared/schemas/alert_types.py)
|
||||
- [Dashboard Service API](../../../services/orchestrator/app/api/dashboard.py)
|
||||
- [Smart Action Handlers](../../../frontend/src/utils/smartActionHandlers.ts)
|
||||
- [JTBD Implementation Status](../../../docs/JTBD-IMPLEMENTATION-STATUS.md)
|
||||
721
services/demo_session/scripts/seed_dashboard_comprehensive.py
Executable file
721
services/demo_session/scripts/seed_dashboard_comprehensive.py
Executable file
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Dashboard Demo Seed Script
|
||||
|
||||
Seeds ALL dashboard scenarios to showcase the complete JTBD-aligned dashboard:
|
||||
|
||||
1. Health Status (green/yellow/red with tri-state checklist)
|
||||
2. Unified Action Queue (URGENT/TODAY/WEEK time-based grouping)
|
||||
3. Execution Progress (production/deliveries/approvals tracking)
|
||||
4. Orchestration Summary (AI automated vs user needed)
|
||||
5. Stock Receipt Modal scenarios
|
||||
6. AI Prevented Issues showcase
|
||||
7. Alert Hub with all alert types
|
||||
|
||||
This creates a realistic dashboard state with:
|
||||
- Actions in all time groups (urgent <6h, today <24h, week <7d)
|
||||
- Execution progress with on_track/at_risk states
|
||||
- AI prevented issues with savings
|
||||
- Deliveries ready for stock receipt
|
||||
- Production batches in various states
|
||||
- Purchase orders needing approval
|
||||
|
||||
Usage:
|
||||
python services/demo_session/scripts/seed_dashboard_comprehensive.py
|
||||
|
||||
Environment Variables:
|
||||
RABBITMQ_URL: RabbitMQ connection URL (default: amqp://guest:guest@localhost:5672/)
|
||||
DEMO_TENANT_ID: Tenant ID to seed data for (default: demo-tenant-bakery-ia)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from shared.messaging import RabbitMQClient, AlertTypeConstants
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Configuration
|
||||
DEMO_TENANT_ID = os.getenv('DEMO_TENANT_ID', 'demo-tenant-bakery-ia')
|
||||
|
||||
# Build RabbitMQ URL from individual components or use direct URL
|
||||
RABBITMQ_URL = os.getenv('RABBITMQ_URL')
|
||||
if not RABBITMQ_URL:
|
||||
rabbitmq_host = os.getenv('RABBITMQ_HOST', 'localhost')
|
||||
rabbitmq_port = os.getenv('RABBITMQ_PORT', '5672')
|
||||
rabbitmq_user = os.getenv('RABBITMQ_USER', 'guest')
|
||||
rabbitmq_password = os.getenv('RABBITMQ_PASSWORD', 'guest')
|
||||
RABBITMQ_URL = f'amqp://{rabbitmq_user}:{rabbitmq_password}@{rabbitmq_host}:{rabbitmq_port}/'
|
||||
|
||||
# Demo entity IDs
|
||||
FLOUR_ID = "flour-tipo-55"
|
||||
YEAST_ID = "yeast-fresh"
|
||||
BUTTER_ID = "butter-french"
|
||||
SUGAR_ID = "sugar-white"
|
||||
CHOCOLATE_ID = "chocolate-dark"
|
||||
|
||||
CROISSANT_PRODUCT = "croissant-mantequilla"
|
||||
BAGUETTE_PRODUCT = "baguette-traditional"
|
||||
CHOCOLATE_CAKE_PRODUCT = "chocolate-cake"
|
||||
|
||||
SUPPLIER_FLOUR = "supplier-harinera"
|
||||
SUPPLIER_YEAST = "supplier-levadura"
|
||||
SUPPLIER_DAIRY = "supplier-lacteos"
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
"""Get current UTC time"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1. UNIFIED ACTION QUEUE - Time-Based Grouping
|
||||
# ============================================================
|
||||
|
||||
def create_urgent_actions() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create URGENT actions (<6 hours deadline)
|
||||
These appear in the 🔴 URGENT section
|
||||
"""
|
||||
now = utc_now()
|
||||
|
||||
return [
|
||||
# PO Approval Escalation - 2h deadline
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'procurement',
|
||||
'alert_type': AlertTypeConstants.PO_APPROVAL_ESCALATION,
|
||||
'title': 'URGENT: PO Approval Needed - Yeast Supplier',
|
||||
'message': 'Purchase order #PO-2024-089 has been pending for 72 hours. Production batch depends on this delivery tomorrow morning.',
|
||||
'alert_metadata': {
|
||||
'po_id': 'po-2024-089',
|
||||
'po_number': 'PO-2024-089',
|
||||
'supplier_id': SUPPLIER_YEAST,
|
||||
'supplier_name': 'Levadura Fresh S.L.',
|
||||
'supplier_phone': '+34-555-1234',
|
||||
'total_amount': 450.00,
|
||||
'currency': 'EUR',
|
||||
'item_categories': ['Yeast', 'Leavening Agents'],
|
||||
'delivery_date': (now + timedelta(hours=10)).isoformat(),
|
||||
'batch_id': 'batch-croissants-tomorrow',
|
||||
'batch_name': 'Croissants Butter - Morning Batch',
|
||||
'financial_impact_eur': 1200,
|
||||
'orders_affected': 8,
|
||||
'escalation': {
|
||||
'aged_hours': 72,
|
||||
'priority_boost': 20,
|
||||
'reason': 'pending_over_72h'
|
||||
},
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(hours=2)).isoformat(),
|
||||
'time_until_consequence_hours': 2,
|
||||
'auto_action_countdown_seconds': 7200, # 2h countdown
|
||||
'can_wait_until_tomorrow': False
|
||||
}
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=72)).isoformat()
|
||||
},
|
||||
|
||||
# Delivery Overdue - Needs immediate action
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'procurement',
|
||||
'alert_type': AlertTypeConstants.DELIVERY_OVERDUE,
|
||||
'title': 'Delivery Overdue: Flour Delivery',
|
||||
'message': 'Expected delivery from Harinera San José is 4 hours overdue. Contact supplier immediately.',
|
||||
'alert_metadata': {
|
||||
'po_id': 'po-2024-085',
|
||||
'po_number': 'PO-2024-085',
|
||||
'supplier_id': SUPPLIER_FLOUR,
|
||||
'supplier_name': 'Harinera San José',
|
||||
'supplier_phone': '+34-555-5678',
|
||||
'supplier_email': 'pedidos@harinerasj.es',
|
||||
'ingredient_id': FLOUR_ID,
|
||||
'ingredient_name': 'Harina Tipo 55',
|
||||
'quantity_expected': 500,
|
||||
'unit': 'kg',
|
||||
'expected_delivery': (now - timedelta(hours=4)).isoformat(),
|
||||
'hours_overdue': 4,
|
||||
'stock_runout_hours': 18,
|
||||
'batches_at_risk': ['batch-baguettes-001', 'batch-croissants-002'],
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(hours=3)).isoformat(),
|
||||
'time_until_consequence_hours': 3,
|
||||
'can_wait_until_tomorrow': False
|
||||
}
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=4)).isoformat()
|
||||
},
|
||||
|
||||
# Production Batch At Risk - 5h window
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'production',
|
||||
'alert_type': AlertTypeConstants.BATCH_AT_RISK,
|
||||
'title': 'Batch At Risk: Missing Ingredients',
|
||||
'message': 'Batch "Chocolate Cakes Evening" scheduled in 5 hours but missing 3kg dark chocolate.',
|
||||
'alert_metadata': {
|
||||
'batch_id': 'batch-chocolate-cake-evening',
|
||||
'batch_name': 'Chocolate Cakes Evening',
|
||||
'product_id': CHOCOLATE_CAKE_PRODUCT,
|
||||
'product_name': 'Chocolate Cake Premium',
|
||||
'planned_start': (now + timedelta(hours=5)).isoformat(),
|
||||
'missing_ingredients': [
|
||||
{'ingredient_id': CHOCOLATE_ID, 'name': 'Dark Chocolate 70%', 'needed': 3, 'available': 0, 'unit': 'kg'}
|
||||
],
|
||||
'orders_dependent': 5,
|
||||
'financial_impact_eur': 380,
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(hours=5)).isoformat(),
|
||||
'time_until_consequence_hours': 5,
|
||||
'can_wait_until_tomorrow': False
|
||||
}
|
||||
},
|
||||
'timestamp': now.isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def create_today_actions() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create TODAY actions (<24 hours deadline)
|
||||
These appear in the 🟡 TODAY section
|
||||
"""
|
||||
now = utc_now()
|
||||
|
||||
return [
|
||||
# PO Approval Needed - Standard priority
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'procurement',
|
||||
'alert_type': AlertTypeConstants.PO_APPROVAL_NEEDED,
|
||||
'title': 'PO Approval: Butter & Dairy Products',
|
||||
'message': 'Purchase order for weekly dairy delivery needs your approval. Delivery scheduled for Friday.',
|
||||
'alert_metadata': {
|
||||
'po_id': 'po-2024-090',
|
||||
'po_number': 'PO-2024-090',
|
||||
'supplier_id': SUPPLIER_DAIRY,
|
||||
'supplier_name': 'Lácteos Frescos Madrid',
|
||||
'supplier_phone': '+34-555-9012',
|
||||
'total_amount': 890.50,
|
||||
'currency': 'EUR',
|
||||
'item_categories': ['Butter', 'Cream', 'Milk'],
|
||||
'delivery_date': (now + timedelta(days=2)).isoformat(),
|
||||
'line_items': [
|
||||
{'ingredient': 'French Butter', 'quantity': 20, 'unit': 'kg', 'price': 12.50},
|
||||
{'ingredient': 'Heavy Cream', 'quantity': 15, 'unit': 'L', 'price': 4.20},
|
||||
{'ingredient': 'Whole Milk', 'quantity': 30, 'unit': 'L', 'price': 1.80}
|
||||
],
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(hours=20)).isoformat(),
|
||||
'time_until_consequence_hours': 20,
|
||||
'can_wait_until_tomorrow': True
|
||||
}
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=6)).isoformat()
|
||||
},
|
||||
|
||||
# Delivery Arriving Soon - Needs preparation
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'procurement',
|
||||
'alert_type': AlertTypeConstants.DELIVERY_ARRIVING_SOON,
|
||||
'title': 'Delivery Arriving in 8 Hours: Sugar & Ingredients',
|
||||
'message': 'Prepare for incoming delivery. Stock receipt will be required.',
|
||||
'alert_metadata': {
|
||||
'po_id': 'po-2024-088',
|
||||
'po_number': 'PO-2024-088',
|
||||
'supplier_id': 'supplier-ingredients',
|
||||
'supplier_name': 'Ingredientes Premium',
|
||||
'supplier_phone': '+34-555-3456',
|
||||
'delivery_id': 'delivery-2024-088',
|
||||
'expected_arrival': (now + timedelta(hours=8)).isoformat(),
|
||||
'item_count': 5,
|
||||
'total_weight_kg': 250,
|
||||
'requires_stock_receipt': True,
|
||||
'warehouse_location': 'Warehouse A - Section 3',
|
||||
'line_items': [
|
||||
{'ingredient': 'White Sugar', 'quantity': 100, 'unit': 'kg'},
|
||||
{'ingredient': 'Brown Sugar', 'quantity': 50, 'unit': 'kg'},
|
||||
{'ingredient': 'Vanilla Extract', 'quantity': 2, 'unit': 'L'}
|
||||
],
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(hours=8)).isoformat(),
|
||||
'time_until_consequence_hours': 8,
|
||||
'can_wait_until_tomorrow': False
|
||||
}
|
||||
},
|
||||
'timestamp': now.isoformat()
|
||||
},
|
||||
|
||||
# Low Stock Warning - Today action recommended
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'inventory',
|
||||
'alert_type': AlertTypeConstants.LOW_STOCK_WARNING,
|
||||
'title': 'Low Stock: Fresh Yeast',
|
||||
'message': 'Fresh yeast stock is low. Current: 2.5kg, Minimum: 10kg. Recommend ordering today.',
|
||||
'alert_metadata': {
|
||||
'ingredient_id': YEAST_ID,
|
||||
'ingredient_name': 'Fresh Yeast',
|
||||
'current_stock': 2.5,
|
||||
'minimum_stock': 10,
|
||||
'maximum_stock': 25,
|
||||
'unit': 'kg',
|
||||
'daily_consumption_avg': 3.5,
|
||||
'days_remaining': 0.7,
|
||||
'supplier_name': 'Levadura Fresh S.L.',
|
||||
'last_order_date': (now - timedelta(days=8)).isoformat(),
|
||||
'recommended_order_quantity': 20,
|
||||
'urgency_context': {
|
||||
'stockout_risk_hours': 17,
|
||||
'can_wait_until_tomorrow': True
|
||||
}
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=3)).isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def create_week_actions() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create WEEK actions (<7 days deadline)
|
||||
These appear in the 🟢 THIS WEEK section
|
||||
"""
|
||||
now = utc_now()
|
||||
|
||||
return [
|
||||
# Demand Surge Predicted - Plan ahead
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'forecasting',
|
||||
'alert_type': AlertTypeConstants.DEMAND_SURGE_PREDICTED,
|
||||
'title': 'Weekend Demand Surge Predicted',
|
||||
'message': 'Sunny weather forecast for Saturday-Sunday. Expect 25% demand increase for croissants and pastries.',
|
||||
'alert_metadata': {
|
||||
'forecast_type': 'weather_based',
|
||||
'weather_condition': 'sunny',
|
||||
'days_affected': [
|
||||
(now + timedelta(days=3)).date().isoformat(),
|
||||
(now + timedelta(days=4)).date().isoformat()
|
||||
],
|
||||
'expected_demand_increase_pct': 25,
|
||||
'products_affected': [
|
||||
{'product_id': CROISSANT_PRODUCT, 'product_name': 'Croissant Butter', 'increase_pct': 30},
|
||||
{'product_id': BAGUETTE_PRODUCT, 'product_name': 'Baguette Traditional', 'increase_pct': 20}
|
||||
],
|
||||
'confidence': 0.85,
|
||||
'recommended_action': 'Increase production by 25% and ensure adequate stock',
|
||||
'estimated_revenue_opportunity_eur': 450,
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(days=2)).isoformat(),
|
||||
'can_wait_until_tomorrow': True
|
||||
}
|
||||
},
|
||||
'timestamp': now.isoformat()
|
||||
},
|
||||
|
||||
# Stock Receipt Incomplete - Can wait
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'inventory',
|
||||
'alert_type': AlertTypeConstants.STOCK_RECEIPT_INCOMPLETE,
|
||||
'title': 'Stock Receipt Pending: Flour Delivery',
|
||||
'message': 'Delivery received 2 days ago but stock receipt not completed. Please confirm lot details and expiration dates.',
|
||||
'alert_metadata': {
|
||||
'receipt_id': 'receipt-2024-012',
|
||||
'po_id': 'po-2024-083',
|
||||
'po_number': 'PO-2024-083',
|
||||
'supplier_name': 'Harinera San José',
|
||||
'delivery_date': (now - timedelta(days=2)).isoformat(),
|
||||
'days_since_delivery': 2,
|
||||
'line_items_pending': 3,
|
||||
'total_items': 3,
|
||||
'requires_lot_tracking': True,
|
||||
'requires_expiration': True,
|
||||
'urgency_context': {
|
||||
'deadline': (now + timedelta(days=5)).isoformat(),
|
||||
'can_wait_until_tomorrow': True
|
||||
}
|
||||
},
|
||||
'timestamp': (now - timedelta(days=2)).isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. AI PREVENTED ISSUES - Showcase AI Value
|
||||
# ============================================================
|
||||
|
||||
def create_prevented_issues() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create alerts showing AI prevented issues (type_class: prevented_issue)
|
||||
These show in the Health Status Card and Prevented Issues Card
|
||||
"""
|
||||
now = utc_now()
|
||||
|
||||
return [
|
||||
# Prevented Stockout - PO created automatically
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'inventory',
|
||||
'alert_type': 'ai_prevented_stockout',
|
||||
'title': '✅ AI Prevented Stockout: Flour',
|
||||
'message': 'AI detected upcoming stockout and created purchase order automatically. Delivery arriving Friday.',
|
||||
'alert_metadata': {
|
||||
'type_class': 'prevented_issue',
|
||||
'priority_level': 'info',
|
||||
'ingredient_id': FLOUR_ID,
|
||||
'ingredient_name': 'Harina Tipo 55',
|
||||
'prevented_risk': 'stockout',
|
||||
'ai_action_taken': 'purchase_order_created',
|
||||
'po_id': 'po-2024-091',
|
||||
'po_number': 'PO-2024-091',
|
||||
'quantity_ordered': 500,
|
||||
'unit': 'kg',
|
||||
'supplier_name': 'Harinera San José',
|
||||
'delivery_date': (now + timedelta(days=2)).isoformat(),
|
||||
'estimated_savings_eur': 250,
|
||||
'orchestrator_context': {
|
||||
'already_addressed': True,
|
||||
'action_type': 'purchase_order',
|
||||
'action_id': 'po-2024-091',
|
||||
'action_status': 'pending_approval',
|
||||
'reasoning': 'Detected stock would run out in 1.8 days based on historical consumption patterns'
|
||||
},
|
||||
'business_impact': {
|
||||
'prevented_stockout_hours': 43,
|
||||
'affected_orders_prevented': 12,
|
||||
'production_batches_secured': 4
|
||||
},
|
||||
'ai_reasoning_summary': 'Analyzed consumption patterns and detected stockout in 43 hours. Created PO for 500kg to arrive Friday, preventing disruption to 4 production batches.'
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=12)).isoformat()
|
||||
},
|
||||
|
||||
# Prevented Waste - Production adjusted
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'production',
|
||||
'alert_type': 'ai_prevented_waste',
|
||||
'title': '✅ AI Prevented Waste: Reduced Monday Production',
|
||||
'message': 'AI detected post-weekend demand pattern and reduced Monday production by 15%, preventing €120 in waste.',
|
||||
'alert_metadata': {
|
||||
'type_class': 'prevented_issue',
|
||||
'priority_level': 'info',
|
||||
'product_id': BAGUETTE_PRODUCT,
|
||||
'product_name': 'Baguette Traditional',
|
||||
'prevented_risk': 'waste',
|
||||
'ai_action_taken': 'production_adjusted',
|
||||
'reduction_pct': 15,
|
||||
'units_reduced': 45,
|
||||
'day_of_week': 'Monday',
|
||||
'historical_pattern': 'post_weekend_low_demand',
|
||||
'estimated_savings_eur': 120,
|
||||
'orchestrator_context': {
|
||||
'already_addressed': True,
|
||||
'action_type': 'production_batch',
|
||||
'action_id': 'batch-baguettes-monday-adjusted',
|
||||
'action_status': 'completed',
|
||||
'reasoning': 'Historical data shows 18% demand drop on Mondays following sunny weekends'
|
||||
},
|
||||
'business_impact': {
|
||||
'waste_prevented_kg': 12,
|
||||
'waste_reduction_pct': 15
|
||||
},
|
||||
'ai_reasoning_summary': 'Detected consistent Monday demand drop (18% avg) in post-weekend data. Adjusted production to prevent overproduction and waste.'
|
||||
},
|
||||
'timestamp': (now - timedelta(days=1)).isoformat()
|
||||
},
|
||||
|
||||
# Prevented Production Delay - Batch rescheduled
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'production',
|
||||
'alert_type': 'ai_prevented_delay',
|
||||
'title': '✅ AI Prevented Delay: Rescheduled Conflicting Batches',
|
||||
'message': 'AI detected oven capacity conflict and rescheduled batches to optimize throughput.',
|
||||
'alert_metadata': {
|
||||
'type_class': 'prevented_issue',
|
||||
'priority_level': 'info',
|
||||
'prevented_risk': 'production_delay',
|
||||
'ai_action_taken': 'batch_rescheduled',
|
||||
'batches_affected': 3,
|
||||
'equipment_id': 'oven-001',
|
||||
'equipment_name': 'Industrial Oven #1',
|
||||
'capacity_utilization_before': 110,
|
||||
'capacity_utilization_after': 95,
|
||||
'time_saved_minutes': 45,
|
||||
'estimated_savings_eur': 85,
|
||||
'orchestrator_context': {
|
||||
'already_addressed': True,
|
||||
'action_type': 'batch_optimization',
|
||||
'action_status': 'completed',
|
||||
'reasoning': 'Detected overlapping batch schedules would exceed oven capacity by 10%'
|
||||
},
|
||||
'business_impact': {
|
||||
'orders_on_time': 8,
|
||||
'customer_satisfaction_impact': 'high'
|
||||
},
|
||||
'ai_reasoning_summary': 'Identified capacity conflict in oven schedule. Rescheduled 3 batches to maintain on-time delivery for 8 orders.'
|
||||
},
|
||||
'timestamp': (now - timedelta(hours=18)).isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. EXECUTION PROGRESS - Production/Deliveries/Approvals
|
||||
# ============================================================
|
||||
|
||||
def create_production_batches() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Create production batch data for execution progress tracking
|
||||
"""
|
||||
now = utc_now()
|
||||
|
||||
return [
|
||||
# Completed batch
|
||||
{
|
||||
'batch_id': 'batch-morning-croissants',
|
||||
'product_name': 'Croissant Butter',
|
||||
'quantity': 120,
|
||||
'status': 'completed',
|
||||
'planned_start': (now - timedelta(hours=4)).isoformat(),
|
||||
'actual_start': (now - timedelta(hours=4, minutes=5)).isoformat(),
|
||||
'actual_end': (now - timedelta(hours=1)).isoformat()
|
||||
},
|
||||
# In progress batch
|
||||
{
|
||||
'batch_id': 'batch-baguettes-lunch',
|
||||
'product_name': 'Baguette Traditional',
|
||||
'quantity': 80,
|
||||
'status': 'in_progress',
|
||||
'planned_start': (now - timedelta(hours=2)).isoformat(),
|
||||
'actual_start': (now - timedelta(hours=2, minutes=10)).isoformat(),
|
||||
'progress_pct': 65
|
||||
},
|
||||
# Pending batches
|
||||
{
|
||||
'batch_id': 'batch-afternoon-pastries',
|
||||
'product_name': 'Mixed Pastries',
|
||||
'quantity': 50,
|
||||
'status': 'pending',
|
||||
'planned_start': (now + timedelta(hours=1)).isoformat()
|
||||
},
|
||||
{
|
||||
'batch_id': 'batch-evening-bread',
|
||||
'product_name': 'Artisan Bread Assortment',
|
||||
'quantity': 40,
|
||||
'status': 'pending',
|
||||
'planned_start': (now + timedelta(hours=3)).isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN SEEDING FUNCTION
|
||||
# ============================================================
|
||||
|
||||
async def seed_comprehensive_dashboard():
|
||||
"""
|
||||
Seed comprehensive dashboard demo data
|
||||
|
||||
Creates realistic dashboard state showcasing:
|
||||
- All time-based action groups (urgent/today/week)
|
||||
- AI prevented issues with savings
|
||||
- Execution progress tracking
|
||||
- Stock receipt scenarios
|
||||
- Full alert type coverage
|
||||
"""
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("🚀 SEEDING COMPREHENSIVE DASHBOARD DEMO DATA")
|
||||
print("="*80 + "\n")
|
||||
|
||||
print(f"📋 Configuration:")
|
||||
print(f" Tenant ID: {DEMO_TENANT_ID}")
|
||||
print(f" RabbitMQ: {RABBITMQ_URL}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Initialize RabbitMQ
|
||||
logger.info("Connecting to RabbitMQ", url=RABBITMQ_URL)
|
||||
rabbitmq_client = RabbitMQClient(RABBITMQ_URL, 'dashboard-demo-seeder')
|
||||
await rabbitmq_client.connect()
|
||||
|
||||
# Collect all alerts
|
||||
all_alerts = []
|
||||
all_alerts.extend(create_urgent_actions())
|
||||
all_alerts.extend(create_today_actions())
|
||||
all_alerts.extend(create_week_actions())
|
||||
all_alerts.extend(create_prevented_issues())
|
||||
|
||||
print(f"📊 Dashboard Scenarios to Seed:")
|
||||
print(f" 🔴 URGENT actions (<6h): {len(create_urgent_actions())}")
|
||||
print(f" 🟡 TODAY actions (<24h): {len(create_today_actions())}")
|
||||
print(f" 🟢 WEEK actions (<7d): {len(create_week_actions())}")
|
||||
print(f" ✅ AI Prevented Issues: {len(create_prevented_issues())}")
|
||||
print(f" 📦 Total Alerts: {len(all_alerts)}")
|
||||
print()
|
||||
|
||||
# Publish alerts
|
||||
print("📤 Publishing Alerts:")
|
||||
print("-" * 80)
|
||||
|
||||
success_count = 0
|
||||
for i, alert in enumerate(all_alerts, 1):
|
||||
routing_key = f"{alert['item_type']}.{alert['service']}.{alert['alert_type']}"
|
||||
|
||||
success = await rabbitmq_client.publish_event(
|
||||
exchange_name='alerts.exchange',
|
||||
routing_key=routing_key,
|
||||
event_data=alert,
|
||||
persistent=True
|
||||
)
|
||||
|
||||
if success:
|
||||
success_count += 1
|
||||
status = "✅"
|
||||
else:
|
||||
status = "❌"
|
||||
logger.warning("Failed to publish alert", alert_id=alert['id'])
|
||||
|
||||
# Determine time group emoji
|
||||
if 'escalation' in alert.get('alert_metadata', {}) or \
|
||||
alert.get('alert_metadata', {}).get('urgency_context', {}).get('time_until_consequence_hours', 999) < 6:
|
||||
group = "🔴 URGENT"
|
||||
elif alert.get('alert_metadata', {}).get('urgency_context', {}).get('time_until_consequence_hours', 999) < 24:
|
||||
group = "🟡 TODAY"
|
||||
elif alert.get('alert_metadata', {}).get('type_class') == 'prevented_issue':
|
||||
group = "✅ PREVENTED"
|
||||
else:
|
||||
group = "🟢 WEEK"
|
||||
|
||||
print(f" {i:2d}. {status} [{group}] {alert['title']}")
|
||||
|
||||
print()
|
||||
print(f"✅ Published {success_count}/{len(all_alerts)} alerts successfully")
|
||||
print()
|
||||
|
||||
await rabbitmq_client.disconnect()
|
||||
|
||||
# Print dashboard preview
|
||||
print("="*80)
|
||||
print("📊 EXPECTED DASHBOARD STATE")
|
||||
print("="*80)
|
||||
print()
|
||||
|
||||
print("🏥 HEALTH STATUS:")
|
||||
print(" Status: YELLOW (actions needed)")
|
||||
print(" Checklist:")
|
||||
print(" ⚡ AI Handled: 3 issues prevented (€455 saved)")
|
||||
print(" ⚠️ Needs You: 3 urgent actions, 3 today actions")
|
||||
print()
|
||||
|
||||
print("📋 ACTION QUEUE:")
|
||||
print(" 🔴 URGENT (<6h): 3 actions")
|
||||
print(" - PO Approval Escalation (2h deadline)")
|
||||
print(" - Delivery Overdue (4h late)")
|
||||
print(" - Batch At Risk (5h until start)")
|
||||
print()
|
||||
print(" 🟡 TODAY (<24h): 3 actions")
|
||||
print(" - PO Approval: Dairy Products")
|
||||
print(" - Delivery Arriving Soon (8h)")
|
||||
print(" - Low Stock: Fresh Yeast")
|
||||
print()
|
||||
print(" 🟢 THIS WEEK (<7d): 2 actions")
|
||||
print(" - Weekend Demand Surge")
|
||||
print(" - Stock Receipt Incomplete")
|
||||
print()
|
||||
|
||||
print("📊 EXECUTION PROGRESS:")
|
||||
print(" Production: 2/4 batches (on_track)")
|
||||
print(" ✅ Completed: 1")
|
||||
print(" 🔄 In Progress: 1")
|
||||
print(" ⏳ Pending: 2")
|
||||
print()
|
||||
print(" Deliveries: Status depends on real PO data")
|
||||
print(" Approvals: 2 pending")
|
||||
print()
|
||||
|
||||
print("✅ AI PREVENTED ISSUES:")
|
||||
print(" Total Prevented: 3 issues")
|
||||
print(" Total Savings: €455")
|
||||
print(" Details:")
|
||||
print(" - Prevented stockout (€250 saved)")
|
||||
print(" - Prevented waste (€120 saved)")
|
||||
print(" - Prevented delay (€85 saved)")
|
||||
print()
|
||||
|
||||
print("="*80)
|
||||
print("🎉 DASHBOARD DEMO SEEDED SUCCESSFULLY!")
|
||||
print("="*80)
|
||||
print()
|
||||
|
||||
print("Next Steps:")
|
||||
print(" 1. Verify alert-processor is running:")
|
||||
print(" kubectl get pods -l app.kubernetes.io/name=alert-processor-service")
|
||||
print()
|
||||
print(" 2. Check alert enrichment logs:")
|
||||
print(" kubectl logs -f deployment/alert-processor-service | grep 'enriched_alert'")
|
||||
print()
|
||||
print(" 3. View dashboard:")
|
||||
print(" http://localhost:3000/dashboard")
|
||||
print()
|
||||
print(" 4. Test smart actions:")
|
||||
print(" - Approve/Reject PO from action queue")
|
||||
print(" - Mark delivery as received (opens stock receipt modal)")
|
||||
print(" - Call supplier (initiates phone call)")
|
||||
print(" - Adjust production (navigates to production page)")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to seed dashboard demo", error=str(e), exc_info=True)
|
||||
print(f"\n❌ ERROR: {str(e)}")
|
||||
print("\nTroubleshooting:")
|
||||
print(" • Verify RabbitMQ is running: kubectl get pods | grep rabbitmq")
|
||||
print(" • Check RABBITMQ_URL environment variable")
|
||||
print(" • Ensure alerts.exchange exists")
|
||||
print(" • Check alert-processor service logs for errors")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Load environment variables
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Run seeder
|
||||
asyncio.run(seed_comprehensive_dashboard())
|
||||
426
services/demo_session/scripts/seed_enriched_alert_demo.py
Normal file
426
services/demo_session/scripts/seed_enriched_alert_demo.py
Normal file
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Seed demo data for Unified Alert Service
|
||||
|
||||
This script creates demo alerts that showcase the enrichment capabilities:
|
||||
- Low stock (AI already handled - prevented issue)
|
||||
- Supplier delay (action needed - critical)
|
||||
- Waste trend (trend warning - standard)
|
||||
- Orchestrator actions (for context enrichment)
|
||||
|
||||
The unified alert-processor service automatically enriches all alerts with:
|
||||
- Multi-factor priority scoring
|
||||
- Orchestrator context (AI actions)
|
||||
- Business impact analysis
|
||||
- Smart actions with deep links
|
||||
- Timing intelligence
|
||||
|
||||
Usage:
|
||||
python services/demo_session/scripts/seed_enriched_alert_demo.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from shared.messaging import RabbitMQClient
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Demo tenant ID (should match existing demo tenant)
|
||||
DEMO_TENANT_ID = "demo-tenant-bakery-ia"
|
||||
|
||||
# Demo entity IDs (should match existing demo data)
|
||||
FLOUR_INGREDIENT_ID = "flour-tipo-55"
|
||||
YEAST_INGREDIENT_ID = "yeast-fresh"
|
||||
CROISSANT_PRODUCT_ID = "croissant-mantequilla"
|
||||
CROISSANT_BATCH_ID = "batch-croissants-001"
|
||||
YEAST_SUPPLIER_ID = "supplier-levadura-fresh"
|
||||
FLOUR_PO_ID = "po-flour-demo-001"
|
||||
|
||||
# Demo alerts for unified alert-processor (automatically enriched)
|
||||
DEMO_ALERTS = [
|
||||
# Alert 1: Low Stock - AI Already Handled (Prevented Issue)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'inventory',
|
||||
'type': 'low_stock',
|
||||
'severity': 'warning', # Will be enriched with priority score ~71
|
||||
'title': 'Bajo Stock: Harina Tipo 55',
|
||||
'message': 'Stock: 45kg, Mínimo: 200kg',
|
||||
'actions': [], # Will be enhanced with smart actions
|
||||
'metadata': {
|
||||
'ingredient_id': FLOUR_INGREDIENT_ID,
|
||||
'ingredient_name': 'Harina Tipo 55',
|
||||
'current_stock': 45,
|
||||
'minimum_stock': 200,
|
||||
'unit': 'kg',
|
||||
'supplier_name': 'Harinera San José',
|
||||
'last_order_date': (datetime.utcnow() - timedelta(days=7)).isoformat(),
|
||||
'i18n': {
|
||||
'title_key': 'alerts.low_stock_warning.title',
|
||||
'message_key': 'alerts.low_stock_warning.message_generic',
|
||||
'title_params': {'ingredient_name': 'Harina Tipo 55'},
|
||||
'message_params': {
|
||||
'ingredient_name': 'Harina Tipo 55',
|
||||
'current_stock': 45,
|
||||
'minimum_stock': 200
|
||||
}
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
},
|
||||
|
||||
# Alert 2: Supplier Delay - Action Needed (Critical)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'procurement',
|
||||
'type': 'supplier_delay',
|
||||
'severity': 'critical', # Will be enriched with priority score ~92
|
||||
'actions': [], # Will be enhanced with smart actions
|
||||
'title': 'Retraso de Proveedor: Levadura Fresh',
|
||||
'message': 'Entrega retrasada 24 horas. Levadura necesaria para producción de mañana.',
|
||||
'metadata': {
|
||||
'supplier_id': YEAST_SUPPLIER_ID,
|
||||
'supplier_name': 'Levadura Fresh',
|
||||
'supplier_phone': '+34-555-1234',
|
||||
'supplier_email': 'pedidos@levadura-fresh.es',
|
||||
'ingredient_id': YEAST_INGREDIENT_ID,
|
||||
'ingredient_name': 'Levadura Fresca',
|
||||
'batch_id': CROISSANT_BATCH_ID,
|
||||
'batch_name': 'Croissants Mantequilla Mañana',
|
||||
'orders_affected': 3,
|
||||
'financial_impact_eur': 450,
|
||||
'deadline': (datetime.utcnow() + timedelta(hours=6)).isoformat(),
|
||||
'quantity_needed': 15,
|
||||
'unit': 'kg',
|
||||
'i18n': {
|
||||
'title_key': 'alerts.supplier_delay.title',
|
||||
'message_key': 'alerts.supplier_delay.message',
|
||||
'title_params': {'supplier_name': 'Levadura Fresh'},
|
||||
'message_params': {
|
||||
'supplier_name': 'Levadura Fresh',
|
||||
'ingredient_name': 'Levadura Fresca',
|
||||
'po_id': 'PO-DEMO-123',
|
||||
'new_delivery_date': (datetime.utcnow() + timedelta(hours=24)).strftime('%Y-%m-%d'),
|
||||
'original_delivery_date': (datetime.utcnow() - timedelta(hours=24)).strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
},
|
||||
|
||||
# Alert 3: Waste Trend - Trend Warning (Standard)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'production',
|
||||
'type': 'waste_trend',
|
||||
'severity': 'medium', # Will be enriched with priority score ~58
|
||||
'actions': [], # Will be enhanced with smart actions
|
||||
'title': 'Tendencia de Desperdicio: Croissants',
|
||||
'message': 'Desperdicio aumentó 15% en 3 días. Patrón detectado: sobreproducción miércoles.',
|
||||
'metadata': {
|
||||
'product_id': CROISSANT_PRODUCT_ID,
|
||||
'product_name': 'Croissant Mantequilla',
|
||||
'waste_percentage': 23,
|
||||
'baseline_percentage': 8,
|
||||
'trend_days': 3,
|
||||
'pattern': 'wednesday_overproduction',
|
||||
'pattern_description': 'Desperdicio consistentemente alto los miércoles (18%, 21%, 23%)',
|
||||
'financial_impact_eur': 180,
|
||||
'recommendation': 'Reducir producción miércoles en 25 unidades',
|
||||
'confidence': 0.85,
|
||||
'historical_data': [
|
||||
{'day': 'Mon', 'waste_pct': 7},
|
||||
{'day': 'Tue', 'waste_pct': 9},
|
||||
{'day': 'Wed', 'waste_pct': 23},
|
||||
{'day': 'Thu', 'waste_pct': 8},
|
||||
{'day': 'Fri', 'waste_pct': 6}
|
||||
],
|
||||
'i18n': {
|
||||
'title_key': 'alerts.waste_trend.title',
|
||||
'message_key': 'alerts.waste_trend.message',
|
||||
'title_params': {'product_name': 'Croissant Mantequilla'},
|
||||
'message_params': {
|
||||
'product_name': 'Croissant Mantequilla',
|
||||
'spike_percent': 15,
|
||||
'trend_days': 3,
|
||||
'pattern': 'wednesday_overproduction'
|
||||
}
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
},
|
||||
|
||||
# Alert 4: Forecast Anomaly - Information (Low Priority)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'forecasting',
|
||||
'type': 'forecast_updated',
|
||||
'severity': 'low', # Will be enriched with priority score ~35
|
||||
'actions': [], # Will be enhanced with smart actions
|
||||
'title': 'Previsión Actualizada: Fin de Semana Soleado',
|
||||
'message': 'Pronóstico meteorológico actualizado: soleado sábado y domingo. Aumento de demanda esperado.',
|
||||
'metadata': {
|
||||
'forecast_type': 'weather_based',
|
||||
'weather_condition': 'sunny',
|
||||
'days_affected': ['2024-11-23', '2024-11-24'],
|
||||
'expected_demand_increase_pct': 15,
|
||||
'confidence': 0.78,
|
||||
'recommended_action': 'Aumentar producción croissants y pan rústico 15%',
|
||||
'i18n': {
|
||||
'title_key': 'alerts.demand_surge_weekend.title',
|
||||
'message_key': 'alerts.demand_surge_weekend.message',
|
||||
'title_params': {'weekend_date': (datetime.utcnow() + timedelta(days=1)).strftime('%Y-%m-%d')},
|
||||
'message_params': {
|
||||
'surge_percent': 15,
|
||||
'date': (datetime.utcnow() + timedelta(days=1)).strftime('%Y-%m-%d'),
|
||||
'products': ['croissants', 'pan rustico']
|
||||
}
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
},
|
||||
|
||||
# Alert 5: Equipment Maintenance - Action Needed (Medium)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'item_type': 'alert',
|
||||
'service': 'production',
|
||||
'type': 'equipment_maintenance',
|
||||
'severity': 'medium', # Will be enriched with priority score ~65
|
||||
'actions': [], # Will be enhanced with smart actions
|
||||
'title': 'Mantenimiento Programado: Horno Industrial',
|
||||
'message': 'Horno principal requiere mantenimiento en 48 horas según calendario.',
|
||||
'metadata': {
|
||||
'equipment_id': 'oven-001',
|
||||
'equipment_name': 'Horno Industrial Principal',
|
||||
'equipment_type': 'oven',
|
||||
'maintenance_type': 'preventive',
|
||||
'scheduled_date': (datetime.utcnow() + timedelta(hours=48)).isoformat(),
|
||||
'estimated_duration_hours': 3,
|
||||
'last_maintenance': (datetime.utcnow() - timedelta(days=90)).isoformat(),
|
||||
'maintenance_interval_days': 90,
|
||||
'supplier_contact': 'TecnoHornos Madrid',
|
||||
'supplier_phone': '+34-555-6789',
|
||||
'i18n': {
|
||||
'title_key': 'alerts.maintenance_required.title',
|
||||
'message_key': 'alerts.maintenance_required.message_with_hours',
|
||||
'title_params': {'equipment_name': 'Horno Industrial Principal'},
|
||||
'message_params': {
|
||||
'equipment_name': 'Horno Industrial Principal',
|
||||
'hours_until': 48,
|
||||
'maintenance_date': (datetime.utcnow() + timedelta(hours=48)).strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# Demo orchestrator actions (for context enrichment)
|
||||
DEMO_ORCHESTRATOR_ACTIONS = [
|
||||
# Action 1: PO Created for Flour (provides context for Alert 1)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'action_type': 'purchase_order_created',
|
||||
'reasoning_type': 'preventive',
|
||||
'entity_type': 'purchase_order',
|
||||
'entity_id': FLOUR_PO_ID,
|
||||
'summary': 'Pedido de compra #12345 creado para 500kg harina',
|
||||
'reasoning_summary': 'Detecté que el stock se agotará en 2.3 días basándome en patrones históricos de demanda. Creé pedido de compra para llegar el viernes.',
|
||||
'metadata': {
|
||||
'ingredient_id': FLOUR_INGREDIENT_ID,
|
||||
'ingredient_name': 'Harina Tipo 55',
|
||||
'quantity': 500,
|
||||
'unit': 'kg',
|
||||
'supplier_name': 'Harinera San José',
|
||||
'po_number': 'PO-12345',
|
||||
'estimated_delivery': (datetime.utcnow() + timedelta(days=2, hours=10)).isoformat(),
|
||||
'estimated_savings_eur': 200,
|
||||
'prevented_issue': 'stockout',
|
||||
'confidence': 0.92,
|
||||
'demand_forecast_method': 'historical_patterns',
|
||||
'stock_runout_days': 2.3
|
||||
},
|
||||
'created_at': (datetime.utcnow() - timedelta(hours=2)).isoformat()
|
||||
},
|
||||
|
||||
# Action 2: Batch Scheduled for Weekend (weather-based)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'action_type': 'batch_scheduled',
|
||||
'reasoning_type': 'weather_based',
|
||||
'entity_type': 'production_batch',
|
||||
'entity_id': CROISSANT_BATCH_ID,
|
||||
'summary': 'Aumentada producción croissants 20% para sábado',
|
||||
'reasoning_summary': 'Pronóstico meteorológico muestra fin de semana soleado. Datos históricos muestran aumento de demanda 15% en sábados soleados.',
|
||||
'metadata': {
|
||||
'product_id': CROISSANT_PRODUCT_ID,
|
||||
'product_name': 'Croissant Mantequilla',
|
||||
'quantity_increase': 25,
|
||||
'quantity_increase_pct': 20,
|
||||
'date': (datetime.utcnow() + timedelta(days=2)).date().isoformat(),
|
||||
'weather_condition': 'sunny',
|
||||
'confidence': 0.85,
|
||||
'historical_sunny_saturday_demand_increase': 15,
|
||||
'estimated_additional_revenue_eur': 180
|
||||
},
|
||||
'created_at': (datetime.utcnow() - timedelta(hours=4)).isoformat()
|
||||
},
|
||||
|
||||
# Action 3: Waste Reduction Adjustment (for trend context)
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': DEMO_TENANT_ID,
|
||||
'action_type': 'production_adjusted',
|
||||
'reasoning_type': 'waste_reduction',
|
||||
'entity_type': 'production_plan',
|
||||
'entity_id': str(uuid.uuid4()),
|
||||
'summary': 'Reducida producción lunes 10% basado en patrón post-fin de semana',
|
||||
'reasoning_summary': 'Detecté patrón de baja demanda post-fin de semana. Histórico muestra reducción 12% demanda lunes. Ajusté producción para prevenir desperdicio.',
|
||||
'metadata': {
|
||||
'day_of_week': 'monday',
|
||||
'reduction_pct': 10,
|
||||
'historical_demand_drop_pct': 12,
|
||||
'products_affected': ['Croissant Mantequilla', 'Pan Rústico'],
|
||||
'estimated_waste_prevented_eur': 85,
|
||||
'confidence': 0.78
|
||||
},
|
||||
'created_at': (datetime.utcnow() - timedelta(hours=20)).isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def seed_enriched_alert_demo():
|
||||
"""
|
||||
Seed demo data for unified alert service
|
||||
|
||||
This creates alerts that will be automatically enriched by alert-processor with:
|
||||
- Multi-factor priority scoring
|
||||
- Orchestrator context (AI actions)
|
||||
- Business impact analysis
|
||||
- Smart actions
|
||||
- Timing intelligence
|
||||
"""
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("🚀 SEEDING DEMO DATA FOR UNIFIED ALERT SERVICE")
|
||||
print("="*70 + "\n")
|
||||
|
||||
rabbitmq_url = os.getenv('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672/')
|
||||
|
||||
try:
|
||||
# Initialize RabbitMQ client
|
||||
logger.info("Connecting to RabbitMQ", url=rabbitmq_url)
|
||||
rabbitmq_client = RabbitMQClient(rabbitmq_url, 'demo-seeder')
|
||||
await rabbitmq_client.connect()
|
||||
|
||||
# Publish demo alerts (will be automatically enriched)
|
||||
print("\n📤 Publishing Alerts (Automatic Enrichment):")
|
||||
print("-" * 70)
|
||||
|
||||
for i, alert in enumerate(DEMO_ALERTS, 1):
|
||||
routing_key = f"{alert['item_type']}.{alert['service']}.{alert['type']}"
|
||||
|
||||
# Use publish_event method (correct API)
|
||||
success = await rabbitmq_client.publish_event(
|
||||
exchange_name='alerts.exchange',
|
||||
routing_key=routing_key,
|
||||
event_data=alert,
|
||||
persistent=True
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.warning("Failed to publish alert", alert_id=alert['id'])
|
||||
continue
|
||||
|
||||
print(f" {i}. ✅ {alert['title']}")
|
||||
print(f" Service: {alert['service']}")
|
||||
print(f" Severity: {alert['severity']} (will be enriched)")
|
||||
print(f" Routing Key: {routing_key}")
|
||||
print()
|
||||
|
||||
# Note about orchestrator actions
|
||||
print("\n📊 Orchestrator Actions (for Context Enrichment):")
|
||||
print("-" * 70)
|
||||
print("NOTE: Orchestrator actions should be seeded via orchestrator service.")
|
||||
print("These provide context for 'AI already handled' enrichment.\n")
|
||||
|
||||
for i, action in enumerate(DEMO_ORCHESTRATOR_ACTIONS, 1):
|
||||
print(f" {i}. {action['summary']}")
|
||||
print(f" Type: {action['reasoning_type']}")
|
||||
print(f" Created: {action['created_at']}")
|
||||
print()
|
||||
|
||||
print("💡 TIP: To seed orchestrator actions, run:")
|
||||
print(" python services/orchestrator/scripts/seed_demo_actions.py")
|
||||
print()
|
||||
|
||||
await rabbitmq_client.disconnect()
|
||||
|
||||
print("="*70)
|
||||
print("✅ DEMO ALERTS SEEDED SUCCESSFULLY!")
|
||||
print("="*70)
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Verify alert-processor service is running:")
|
||||
print(" kubectl get pods -l app.kubernetes.io/name=alert-processor-service")
|
||||
print()
|
||||
print(" 2. Check logs for automatic enrichment:")
|
||||
print(" kubectl logs -f deployment/alert-processor-service")
|
||||
print()
|
||||
print(" 3. View enriched alerts in dashboard:")
|
||||
print(" http://localhost:3000/dashboard")
|
||||
print()
|
||||
print("Expected automatic enrichment:")
|
||||
print(" • Low Stock Alert → Important (71) - Prevented Issue + Smart Actions")
|
||||
print(" • Supplier Delay → Critical (92) - Action Needed + Deep Links")
|
||||
print(" • Waste Trend → Standard (58) - Trend Warning + AI Reasoning")
|
||||
print(" • Forecast Update → Info (35) - Information")
|
||||
print(" • Equipment Maint → Standard (65) - Action Needed + Timing")
|
||||
print()
|
||||
print("All alerts enriched with:")
|
||||
print(" ✓ Multi-factor priority scores")
|
||||
print(" ✓ Orchestrator context (AI actions)")
|
||||
print(" ✓ Business impact analysis")
|
||||
print(" ✓ Smart actions with deep links")
|
||||
print(" ✓ Timing intelligence")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to seed demo data", error=str(e))
|
||||
print(f"\n❌ ERROR: {str(e)}")
|
||||
print("\nTroubleshooting:")
|
||||
print(" • Verify RabbitMQ is running: kubectl get pods | grep rabbitmq")
|
||||
print(" • Check RABBITMQ_URL environment variable")
|
||||
print(" • Ensure raw_alerts exchange can be created")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Load environment variables
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Run seeder
|
||||
asyncio.run(seed_enriched_alert_demo())
|
||||
Reference in New Issue
Block a user