""" 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}"