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