New alert system and panel de control page
This commit is contained in:
343
shared/schemas/event_classification.py
Normal file
343
shared/schemas/event_classification.py
Normal 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}"
|
||||
Reference in New Issue
Block a user