New alert system and panel de control page

This commit is contained in:
Urtzi Alfaro
2025-11-27 15:52:40 +01:00
parent 1a2f4602f3
commit e902419b6e
178 changed files with 20982 additions and 6944 deletions

View File

@@ -0,0 +1,276 @@
"""
Alert Types for Next-Generation Alert System
Defines enriched alert types that transform passive notifications into actionable guidance.
This replaces simple severity-based alerts with context-rich, prioritized, intelligent alerts.
"""
from enum import Enum
from typing import Dict, Any, Optional, List
from pydantic import BaseModel, Field
from datetime import datetime
# ============================================================
# Alert Type Classifications
# ============================================================
class AlertTypeClass(str, Enum):
"""High-level alert type classifications"""
ACTION_NEEDED = "action_needed" # Requires user decision
PREVENTED_ISSUE = "prevented_issue" # AI already handled, FYI
TREND_WARNING = "trend_warning" # Proactive insight
ESCALATION = "escalation" # Time-sensitive with auto-action countdown
INFORMATION = "information" # Pure informational
class PriorityLevel(str, Enum):
"""Priority levels based on multi-factor scoring"""
CRITICAL = "critical" # 90-100: Needs decision in next 2 hours
IMPORTANT = "important" # 70-89: Needs decision today
STANDARD = "standard" # 50-69: Review when convenient
INFO = "info" # 0-49: For awareness
class PlacementHint(str, Enum):
"""UI placement hints for where alert should appear"""
TOAST = "toast" # Immediate popup notification
ACTION_QUEUE = "action_queue" # Dashboard action queue section
DASHBOARD_INLINE = "dashboard_inline" # Embedded in relevant dashboard section
NOTIFICATION_PANEL = "notification_panel" # Bell icon notification panel
EMAIL_DIGEST = "email_digest" # End-of-day email summary
# ============================================================
# Smart Action Definitions
# ============================================================
class SmartActionType(str, Enum):
"""Types of smart actions users can take"""
APPROVE_PO = "approve_po"
REJECT_PO = "reject_po"
MODIFY_PO = "modify_po"
CALL_SUPPLIER = "call_supplier"
NAVIGATE = "navigate"
ADJUST_PRODUCTION = "adjust_production"
START_PRODUCTION_BATCH = "start_production_batch"
NOTIFY_CUSTOMER = "notify_customer"
CANCEL_AUTO_ACTION = "cancel_auto_action"
MARK_DELIVERY_RECEIVED = "mark_delivery_received"
COMPLETE_STOCK_RECEIPT = "complete_stock_receipt"
OPEN_REASONING = "open_reasoning"
SNOOZE = "snooze"
DISMISS = "dismiss"
MARK_READ = "mark_read"
class SmartAction(BaseModel):
"""Smart action button definition"""
label: str = Field(..., description="User-facing button label")
type: SmartActionType = Field(..., description="Action type for handler routing")
variant: str = Field(default="primary", description="UI variant: primary, secondary, tertiary, danger")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Action-specific data")
disabled: bool = Field(default=False, description="Whether action is disabled")
disabled_reason: Optional[str] = Field(None, description="Reason why action is disabled")
estimated_time_minutes: Optional[int] = Field(None, description="Estimated time to complete action")
consequence: Optional[str] = Field(None, description="What happens if this action is taken")
# ============================================================
# Context & Enrichment Models
# ============================================================
class OrchestratorContext(BaseModel):
"""Context from Daily Orchestrator about recent actions"""
already_addressed: bool = Field(..., description="Has AI already addressed this issue?")
action_type: Optional[str] = Field(None, description="Type of action taken: PO, batch, adjustment")
action_id: Optional[str] = Field(None, description="ID of the PO/batch created")
action_status: Optional[str] = Field(None, description="Status: created, pending_approval, completed")
delivery_date: Optional[datetime] = Field(None, description="When will solution arrive")
reasoning: Optional[Dict[str, Any]] = Field(None, description="Structured reasoning data")
estimated_resolution_time: Optional[datetime] = Field(None, description="When issue will be resolved")
estimated_savings_eur: Optional[float] = Field(None, description="Estimated savings from preventing this issue")
class BusinessImpact(BaseModel):
"""Business impact assessment"""
financial_impact_eur: Optional[float] = Field(None, description="Estimated € impact")
affected_orders: Optional[int] = Field(None, description="Number of orders affected")
affected_customers: Optional[List[str]] = Field(None, description="Customer names affected")
production_batches_at_risk: Optional[List[str]] = Field(None, description="Batch IDs at risk")
stockout_risk_hours: Optional[float] = Field(None, description="Hours until stockout")
waste_risk_kg: Optional[float] = Field(None, description="Kg of waste risk")
customer_satisfaction_impact: Optional[str] = Field(None, description="Impact level: high, medium, low")
class UrgencyContext(BaseModel):
"""Urgency and timing context"""
deadline: Optional[datetime] = Field(None, description="Hard deadline for decision")
time_until_consequence_hours: Optional[float] = Field(None, description="Hours until consequence occurs")
can_wait_until_tomorrow: bool = Field(default=True, description="Can this wait until tomorrow?")
peak_hour_relevant: bool = Field(default=False, description="Is this relevant during peak hours?")
auto_action_countdown_seconds: Optional[int] = Field(None, description="Seconds until auto-action triggers")
class UserAgency(BaseModel):
"""User's ability to act on this alert"""
can_user_fix: bool = Field(..., description="Can the user actually fix this?")
requires_external_party: bool = Field(default=False, description="Requires supplier/customer action?")
external_party_name: Optional[str] = Field(None, description="Name of external party")
external_party_contact: Optional[str] = Field(None, description="Phone/email of external party")
blockers: Optional[List[str]] = Field(None, description="Things blocking user from acting")
suggested_workaround: Optional[str] = Field(None, description="Alternative solution if blocked")
class TrendContext(BaseModel):
"""Trend analysis context"""
metric_name: str = Field(..., description="Name of metric trending")
current_value: float = Field(..., description="Current value")
baseline_value: float = Field(..., description="Baseline/expected value")
change_percentage: float = Field(..., description="Percentage change")
direction: str = Field(..., description="Direction: increasing, decreasing")
significance: str = Field(..., description="Significance: high, medium, low")
period_days: int = Field(..., description="Number of days in trend period")
possible_causes: Optional[List[str]] = Field(None, description="Potential root causes")
# ============================================================
# Enriched Alert Model
# ============================================================
class EnrichedAlert(BaseModel):
"""
Next-generation enriched alert with full context and guidance.
This is what gets sent to the frontend after intelligence processing.
"""
# Original Alert Data
id: str = Field(..., description="Alert UUID")
tenant_id: str = Field(..., description="Tenant UUID")
service: str = Field(..., description="Originating service")
alert_type: str = Field(..., description="Specific alert type code")
title: str = Field(..., description="User-facing title")
message: str = Field(..., description="Detailed message")
# Classification
type_class: AlertTypeClass = Field(..., description="High-level classification")
priority_level: PriorityLevel = Field(..., description="Priority level")
priority_score: int = Field(..., description="Numeric priority score 0-100")
# Context Enrichment
orchestrator_context: Optional[OrchestratorContext] = Field(None, description="AI system context")
business_impact: Optional[BusinessImpact] = Field(None, description="Business impact assessment")
urgency_context: Optional[UrgencyContext] = Field(None, description="Urgency and timing")
user_agency: Optional[UserAgency] = Field(None, description="User's ability to act")
trend_context: Optional[TrendContext] = Field(None, description="Trend analysis (if trend warning)")
# AI Reasoning
ai_reasoning_summary: Optional[str] = Field(None, description="Plain language AI reasoning")
reasoning_data: Optional[Dict[str, Any]] = Field(None, description="Structured reasoning from orchestrator")
confidence_score: Optional[float] = Field(None, description="AI confidence 0-1")
# Actions
actions: List[SmartAction] = Field(default_factory=list, description="Smart action buttons")
primary_action: Optional[SmartAction] = Field(None, description="Primary recommended action")
# UI Placement
placement: List[PlacementHint] = Field(default_factory=list, description="Where to show this alert")
# Grouping
group_id: Optional[str] = Field(None, description="Group ID if part of grouped alerts")
is_group_summary: bool = Field(default=False, description="Is this a group summary?")
grouped_alert_count: Optional[int] = Field(None, description="Number of alerts in group")
grouped_alert_ids: Optional[List[str]] = Field(None, description="IDs of grouped alerts")
# Metadata
created_at: datetime = Field(..., description="When alert was created")
enriched_at: datetime = Field(..., description="When alert was enriched")
alert_metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
# Status
status: str = Field(default="active", description="Status: active, resolved, acknowledged, snoozed")
# ============================================================
# Raw Alert Input Model
# ============================================================
class RawAlert(BaseModel):
"""
Raw alert from originating services (inventory, production, etc.)
This is what services send before enrichment.
"""
tenant_id: str
alert_type: str
title: str
message: str
service: str
actions: Optional[List[str]] = None # Simple action labels
alert_metadata: Dict[str, Any] = Field(default_factory=dict)
item_type: str = Field(default="alert") # alert or recommendation
# ============================================================
# Alert Group Model
# ============================================================
class AlertGroup(BaseModel):
"""Grouped alerts for better UX"""
group_id: str = Field(..., description="Group UUID")
tenant_id: str = Field(..., description="Tenant UUID")
group_type: str = Field(..., description="Type of grouping: supplier, service, type")
title: str = Field(..., description="Group title")
summary: str = Field(..., description="Group summary message")
alert_count: int = Field(..., description="Number of alerts in group")
alert_ids: List[str] = Field(..., description="Alert UUIDs in group")
highest_priority_score: int = Field(..., description="Highest priority in group")
created_at: datetime = Field(..., description="When group was created")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Group metadata")
# ============================================================
# Priority Scoring Components
# ============================================================
class PriorityScoreComponents(BaseModel):
"""Breakdown of priority score calculation"""
business_impact_score: float = Field(..., description="Business impact component 0-100")
urgency_score: float = Field(..., description="Urgency component 0-100")
user_agency_score: float = Field(..., description="User agency component 0-100")
confidence_score: float = Field(..., description="Confidence component 0-100")
final_score: int = Field(..., description="Final weighted score 0-100")
weights: Dict[str, float] = Field(..., description="Weights used in calculation")
# ============================================================
# Standard Alert Type Constants
# ============================================================
class AlertTypeConstants:
"""Standard alert type string constants"""
# Inventory alerts
LOW_STOCK_WARNING = "low_stock_warning"
CRITICAL_STOCK_SHORTAGE = "critical_stock_shortage"
EXPIRING_SOON = "expiring_soon"
EXPIRED_STOCK = "expired_stock"
# Production alerts
PRODUCTION_DELAY = "production_delay"
PRODUCTION_STALLED = "production_stalled"
BATCH_AT_RISK = "batch_at_risk"
PRODUCTION_BATCH_START = "production_batch_start"
# Purchase Order alerts
PO_APPROVAL_NEEDED = "po_approval_needed"
PO_APPROVAL_ESCALATION = "po_approval_escalation"
# Delivery lifecycle alerts (NEW)
DELIVERY_SCHEDULED = "delivery_scheduled"
DELIVERY_ARRIVING_SOON = "delivery_arriving_soon"
DELIVERY_OVERDUE = "delivery_overdue"
STOCK_RECEIPT_INCOMPLETE = "stock_receipt_incomplete"
# Forecasting alerts
DEMAND_SURGE_PREDICTED = "demand_surge_predicted"
DEMAND_DROP_PREDICTED = "demand_drop_predicted"

View File

@@ -0,0 +1,343 @@
"""
Event Classification Schema
This module defines the three-tier event model that separates:
- ALERTS: Actionable events requiring user decision
- NOTIFICATIONS: Informational state changes (FYI only)
- RECOMMENDATIONS: Advisory suggestions from AI
This replaces the old conflated "alert" system with semantic clarity.
"""
from enum import Enum
from typing import Any, Dict, List, Optional
from datetime import datetime
from pydantic import BaseModel, Field
class EventClass(str, Enum):
"""
Top-level event classification.
- ALERT: Actionable, requires user decision, has smart actions
- NOTIFICATION: Informational state change, no action needed
- RECOMMENDATION: Advisory suggestion, optional action
"""
ALERT = "alert"
NOTIFICATION = "notification"
RECOMMENDATION = "recommendation"
class EventDomain(str, Enum):
"""
Business domain classification for events.
Enables domain-specific dashboards and selective subscription.
"""
INVENTORY = "inventory"
PRODUCTION = "production"
SUPPLY_CHAIN = "supply_chain"
DEMAND = "demand"
OPERATIONS = "operations"
class PriorityLevel(str, Enum):
"""Priority levels for alerts and recommendations."""
CRITICAL = "critical" # 90-100: Immediate action required
IMPORTANT = "important" # 70-89: Action needed soon
STANDARD = "standard" # 50-69: Normal priority
INFO = "info" # 0-49: Low priority, informational
class AlertTypeClass(str, Enum):
"""
Alert-specific classification (only applies to EventClass.ALERT).
"""
ACTION_NEEDED = "action_needed" # User must decide
PREVENTED_ISSUE = "prevented_issue" # AI already handled, FYI
TREND_WARNING = "trend_warning" # Pattern detected
ESCALATION = "escalation" # Time-sensitive with auto-action countdown
INFORMATION = "information" # Pure informational alert
class NotificationType(str, Enum):
"""
Notification-specific types for state changes.
"""
STATE_CHANGE = "state_change" # Entity state transition
COMPLETION = "completion" # Process/task completed
ARRIVAL = "arrival" # Entity arrived/received
DEPARTURE = "departure" # Entity left/shipped
UPDATE = "update" # General update
SYSTEM_EVENT = "system_event" # System operation
class RecommendationType(str, Enum):
"""
Recommendation-specific types.
"""
OPTIMIZATION = "optimization" # Efficiency improvement
COST_REDUCTION = "cost_reduction" # Save money
RISK_MITIGATION = "risk_mitigation" # Prevent future issues
TREND_INSIGHT = "trend_insight" # Pattern analysis
BEST_PRACTICE = "best_practice" # Suggested approach
class RawEvent(BaseModel):
"""
Base event emitted by domain services.
This is the unified schema replacing the old RawAlert.
All domain services emit RawEvents which are then conditionally enriched.
"""
tenant_id: str = Field(..., description="Tenant identifier")
# Event classification
event_class: EventClass = Field(..., description="Alert, Notification, or Recommendation")
event_domain: EventDomain = Field(..., description="Business domain (inventory, production, etc.)")
event_type: str = Field(..., description="Specific event type (e.g., 'critical_stock_shortage')")
# Core content
title: str = Field(..., description="Event title")
message: str = Field(..., description="Event message")
# Source
service: str = Field(..., description="Originating service name")
# Actions (optional, mainly for alerts)
actions: Optional[List[str]] = Field(default=None, description="Available action types")
# Metadata (domain-specific data)
event_metadata: Dict[str, Any] = Field(default_factory=dict, description="Domain-specific metadata")
# Timestamp
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Event creation time")
# Deduplication (optional)
deduplication_key: Optional[str] = Field(default=None, description="Key for deduplication")
class Config:
use_enum_values = True
class EnrichedAlert(BaseModel):
"""
Fully enriched alert with priority scoring, smart actions, and context.
Only used for EventClass.ALERT.
"""
# From RawEvent
id: str
tenant_id: str
event_domain: EventDomain
event_type: str
title: str
message: str
service: str
timestamp: datetime
# Alert-specific
type_class: AlertTypeClass
status: str # active, acknowledged, resolved, dismissed
# Priority
priority_score: int = Field(..., ge=0, le=100, description="0-100 priority score")
priority_level: PriorityLevel
# Enrichment context
orchestrator_context: Optional[Dict[str, Any]] = Field(default=None)
business_impact: Optional[Dict[str, Any]] = Field(default=None)
urgency_context: Optional[Dict[str, Any]] = Field(default=None)
user_agency: Optional[Dict[str, Any]] = Field(default=None)
# Smart actions
smart_actions: Optional[List[Dict[str, Any]]] = Field(default=None)
# AI reasoning
ai_reasoning_summary: Optional[str] = Field(default=None)
confidence_score: Optional[float] = Field(default=None, ge=0.0, le=1.0)
# Timing
timing_decision: Optional[str] = Field(default=None)
scheduled_send_time: Optional[datetime] = Field(default=None)
placement: Optional[List[str]] = Field(default=None)
# Metadata
alert_metadata: Dict[str, Any] = Field(default_factory=dict)
class Config:
use_enum_values = True
class Notification(BaseModel):
"""
Lightweight notification for state changes.
Only used for EventClass.NOTIFICATION.
"""
# From RawEvent
id: str
tenant_id: str
event_domain: EventDomain
event_type: str
notification_type: NotificationType
title: str
message: str
service: str
timestamp: datetime
# Lightweight context
entity_type: Optional[str] = Field(default=None, description="Type of entity (batch, delivery, etc.)")
entity_id: Optional[str] = Field(default=None, description="ID of entity")
old_state: Optional[str] = Field(default=None, description="Previous state")
new_state: Optional[str] = Field(default=None, description="New state")
# Display metadata
notification_metadata: Dict[str, Any] = Field(default_factory=dict)
# Placement (lightweight, typically just toast + panel)
placement: List[str] = Field(default_factory=lambda: ["notification_panel"])
# TTL tracking
expires_at: Optional[datetime] = Field(default=None, description="Auto-delete after this time")
class Config:
use_enum_values = True
class Recommendation(BaseModel):
"""
AI-generated recommendation with moderate enrichment.
Only used for EventClass.RECOMMENDATION.
"""
# From RawEvent
id: str
tenant_id: str
event_domain: EventDomain
event_type: str
recommendation_type: RecommendationType
title: str
message: str
service: str
timestamp: datetime
# Recommendation-specific
priority_level: PriorityLevel = Field(default=PriorityLevel.INFO)
# Context (lighter than alerts, no orchestrator queries)
estimated_impact: Optional[Dict[str, Any]] = Field(default=None, description="Estimated benefit")
suggested_actions: Optional[List[Dict[str, Any]]] = Field(default=None)
# AI reasoning
ai_reasoning_summary: Optional[str] = Field(default=None)
confidence_score: Optional[float] = Field(default=None, ge=0.0, le=1.0)
# Dismissal tracking
dismissed_at: Optional[datetime] = Field(default=None)
dismissed_by: Optional[str] = Field(default=None)
# Metadata
recommendation_metadata: Dict[str, Any] = Field(default_factory=dict)
class Config:
use_enum_values = True
# Event type mappings for easy classification
EVENT_TYPE_TO_CLASS_MAP = {
# Alerts (actionable)
"critical_stock_shortage": (EventClass.ALERT, EventDomain.INVENTORY),
"production_delay": (EventClass.ALERT, EventDomain.PRODUCTION),
"equipment_failure": (EventClass.ALERT, EventDomain.PRODUCTION),
"po_approval_needed": (EventClass.ALERT, EventDomain.SUPPLY_CHAIN),
"delivery_overdue": (EventClass.ALERT, EventDomain.SUPPLY_CHAIN),
"temperature_breach": (EventClass.ALERT, EventDomain.INVENTORY),
"expired_products": (EventClass.ALERT, EventDomain.INVENTORY),
"low_stock_warning": (EventClass.ALERT, EventDomain.INVENTORY),
"production_ingredient_shortage": (EventClass.ALERT, EventDomain.INVENTORY),
"order_overload": (EventClass.ALERT, EventDomain.PRODUCTION),
# Notifications (informational)
"stock_received": (EventClass.NOTIFICATION, EventDomain.INVENTORY),
"stock_movement": (EventClass.NOTIFICATION, EventDomain.INVENTORY),
"batch_state_changed": (EventClass.NOTIFICATION, EventDomain.PRODUCTION),
"batch_completed": (EventClass.NOTIFICATION, EventDomain.PRODUCTION),
"orchestration_run_started": (EventClass.NOTIFICATION, EventDomain.OPERATIONS),
"orchestration_run_completed": (EventClass.NOTIFICATION, EventDomain.OPERATIONS),
"po_approved": (EventClass.NOTIFICATION, EventDomain.SUPPLY_CHAIN),
"po_sent_to_supplier": (EventClass.NOTIFICATION, EventDomain.SUPPLY_CHAIN),
"delivery_scheduled": (EventClass.NOTIFICATION, EventDomain.SUPPLY_CHAIN),
"delivery_arriving_soon": (EventClass.NOTIFICATION, EventDomain.SUPPLY_CHAIN),
"delivery_received": (EventClass.NOTIFICATION, EventDomain.SUPPLY_CHAIN),
# Recommendations (advisory)
"demand_surge_predicted": (EventClass.RECOMMENDATION, EventDomain.DEMAND),
"weather_impact_forecast": (EventClass.RECOMMENDATION, EventDomain.DEMAND),
"holiday_preparation": (EventClass.RECOMMENDATION, EventDomain.DEMAND),
"inventory_optimization_opportunity": (EventClass.RECOMMENDATION, EventDomain.INVENTORY),
"cost_reduction_suggestion": (EventClass.RECOMMENDATION, EventDomain.SUPPLY_CHAIN),
"efficiency_improvement": (EventClass.RECOMMENDATION, EventDomain.PRODUCTION),
}
def get_event_classification(event_type: str) -> tuple[EventClass, EventDomain]:
"""
Get the event_class and event_domain for a given event_type.
Args:
event_type: The specific event type string
Returns:
Tuple of (EventClass, EventDomain)
Raises:
ValueError: If event_type is not recognized
"""
if event_type in EVENT_TYPE_TO_CLASS_MAP:
return EVENT_TYPE_TO_CLASS_MAP[event_type]
# Default: treat unknown types as notifications in operations domain
return (EventClass.NOTIFICATION, EventDomain.OPERATIONS)
def get_redis_channel(tenant_id: str, event_domain: EventDomain, event_class: EventClass) -> str:
"""
Get the Redis pub/sub channel name for an event.
Pattern: tenant:{tenant_id}:{domain}.{class}
Examples:
- tenant:uuid:inventory.alerts
- tenant:uuid:production.notifications
- tenant:uuid:recommendations (recommendations not domain-specific)
Args:
tenant_id: Tenant identifier
event_domain: Event domain
event_class: Event class
Returns:
Redis channel name
"""
if event_class == EventClass.RECOMMENDATION:
# Recommendations go to a tenant-wide channel
return f"tenant:{tenant_id}:recommendations"
return f"tenant:{tenant_id}:{event_domain.value}.{event_class.value}s"
def get_rabbitmq_routing_key(event_class: EventClass, event_domain: EventDomain, severity: str) -> str:
"""
Get the RabbitMQ routing key for an event.
Pattern: {event_class}.{event_domain}.{severity}
Examples:
- alert.inventory.urgent
- notification.production.info
- recommendation.demand.medium
Args:
event_class: Event class
event_domain: Event domain
severity: Severity level (urgent, high, medium, low)
Returns:
RabbitMQ routing key
"""
return f"{event_class.value}.{event_domain.value}.{severity}"