New alert system and panel de control page
This commit is contained in:
@@ -12,12 +12,31 @@ from shared.database.base import Base
|
||||
AuditLog = create_audit_log_model(Base)
|
||||
|
||||
# Import all models to register them with the Base metadata
|
||||
from .alerts import Alert, AlertStatus, AlertSeverity
|
||||
from .events import (
|
||||
Alert,
|
||||
Notification,
|
||||
Recommendation,
|
||||
EventInteraction,
|
||||
AlertStatus,
|
||||
PriorityLevel,
|
||||
AlertTypeClass,
|
||||
NotificationType,
|
||||
RecommendationType,
|
||||
)
|
||||
|
||||
# List all models for easier access
|
||||
__all__ = [
|
||||
# New event models
|
||||
"Alert",
|
||||
"Notification",
|
||||
"Recommendation",
|
||||
"EventInteraction",
|
||||
# Enums
|
||||
"AlertStatus",
|
||||
"AlertSeverity",
|
||||
"PriorityLevel",
|
||||
"AlertTypeClass",
|
||||
"NotificationType",
|
||||
"RecommendationType",
|
||||
# System
|
||||
"AuditLog",
|
||||
]
|
||||
@@ -1,90 +0,0 @@
|
||||
# services/alert_processor/app/models/alerts.py
|
||||
"""
|
||||
Alert models for the alert processor service
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Text, DateTime, JSON, Enum, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime, timezone
|
||||
import uuid
|
||||
import enum
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
|
||||
def utc_now():
|
||||
"""Return current UTC time as timezone-aware datetime"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class AlertStatus(enum.Enum):
|
||||
"""Alert status values"""
|
||||
ACTIVE = "active"
|
||||
RESOLVED = "resolved"
|
||||
ACKNOWLEDGED = "acknowledged"
|
||||
IGNORED = "ignored"
|
||||
|
||||
|
||||
class AlertSeverity(enum.Enum):
|
||||
"""Alert severity levels"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class InteractionType(enum.Enum):
|
||||
"""Alert interaction types"""
|
||||
ACKNOWLEDGED = "acknowledged"
|
||||
RESOLVED = "resolved"
|
||||
SNOOZED = "snoozed"
|
||||
DISMISSED = "dismissed"
|
||||
|
||||
|
||||
class Alert(Base):
|
||||
"""Alert records for the alert processor service"""
|
||||
__tablename__ = "alerts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Alert classification
|
||||
item_type = Column(String(50), nullable=False) # 'alert' or 'recommendation'
|
||||
alert_type = Column(String(100), nullable=False) # e.g., 'overstock_warning'
|
||||
severity = Column(Enum(AlertSeverity, values_callable=lambda obj: [e.value for e in obj]), nullable=False, index=True)
|
||||
status = Column(Enum(AlertStatus, values_callable=lambda obj: [e.value for e in obj]), default=AlertStatus.ACTIVE, index=True)
|
||||
|
||||
# Source and content
|
||||
service = Column(String(100), nullable=False) # originating service
|
||||
title = Column(String(255), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Actions and metadata
|
||||
actions = Column(JSON, nullable=True) # List of available actions
|
||||
alert_metadata = Column(JSON, nullable=True) # Additional alert-specific data
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now, index=True)
|
||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class AlertInteraction(Base):
|
||||
"""Alert interaction tracking for analytics"""
|
||||
__tablename__ = "alert_interactions"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
alert_id = Column(UUID(as_uuid=True), ForeignKey('alerts.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Interaction details
|
||||
interaction_type = Column(String(50), nullable=False, index=True)
|
||||
interacted_at = Column(DateTime(timezone=True), nullable=False, default=utc_now, index=True)
|
||||
response_time_seconds = Column(Integer, nullable=True)
|
||||
|
||||
# Context
|
||||
interaction_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=utc_now)
|
||||
402
services/alert_processor/app/models/events.py
Normal file
402
services/alert_processor/app/models/events.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
Unified Event Storage Models
|
||||
|
||||
This module defines separate storage models for:
|
||||
- Alerts: Full enrichment, lifecycle tracking
|
||||
- Notifications: Lightweight, ephemeral (7-day TTL)
|
||||
- Recommendations: Medium weight, dismissible
|
||||
|
||||
Replaces the old single Alert model with semantic clarity.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Text, DateTime, Integer, ForeignKey, Float, CheckConstraint, Index, Boolean, Enum
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
import uuid
|
||||
import enum
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
|
||||
def utc_now():
|
||||
"""Return current UTC time as timezone-aware datetime"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ENUMS
|
||||
# ============================================================
|
||||
|
||||
class AlertStatus(enum.Enum):
|
||||
"""Alert lifecycle status"""
|
||||
ACTIVE = "active"
|
||||
RESOLVED = "resolved"
|
||||
ACKNOWLEDGED = "acknowledged"
|
||||
IN_PROGRESS = "in_progress"
|
||||
DISMISSED = "dismissed"
|
||||
IGNORED = "ignored"
|
||||
|
||||
|
||||
class PriorityLevel(enum.Enum):
|
||||
"""Priority levels based on multi-factor scoring"""
|
||||
CRITICAL = "critical" # 90-100
|
||||
IMPORTANT = "important" # 70-89
|
||||
STANDARD = "standard" # 50-69
|
||||
INFO = "info" # 0-49
|
||||
|
||||
|
||||
class AlertTypeClass(enum.Enum):
|
||||
"""Alert type classification (for alerts only)"""
|
||||
ACTION_NEEDED = "action_needed" # Requires user action
|
||||
PREVENTED_ISSUE = "prevented_issue" # AI already handled
|
||||
TREND_WARNING = "trend_warning" # Pattern detected
|
||||
ESCALATION = "escalation" # Time-sensitive with countdown
|
||||
INFORMATION = "information" # FYI only
|
||||
|
||||
|
||||
class NotificationType(enum.Enum):
|
||||
"""Notification type classification"""
|
||||
STATE_CHANGE = "state_change"
|
||||
COMPLETION = "completion"
|
||||
ARRIVAL = "arrival"
|
||||
DEPARTURE = "departure"
|
||||
UPDATE = "update"
|
||||
SYSTEM_EVENT = "system_event"
|
||||
|
||||
|
||||
class RecommendationType(enum.Enum):
|
||||
"""Recommendation type classification"""
|
||||
OPTIMIZATION = "optimization"
|
||||
COST_REDUCTION = "cost_reduction"
|
||||
RISK_MITIGATION = "risk_mitigation"
|
||||
TREND_INSIGHT = "trend_insight"
|
||||
BEST_PRACTICE = "best_practice"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ALERT MODEL (Full Enrichment)
|
||||
# ============================================================
|
||||
|
||||
class Alert(Base):
|
||||
"""
|
||||
Alert model with full enrichment capabilities.
|
||||
|
||||
Used for EventClass.ALERT only.
|
||||
Full priority scoring, context enrichment, smart actions, lifecycle tracking.
|
||||
"""
|
||||
__tablename__ = "alerts"
|
||||
|
||||
# Primary key
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Event classification
|
||||
item_type = Column(String(50), nullable=False) # 'alert' or 'recommendation' - from old schema
|
||||
event_domain = Column(String(50), nullable=True, index=True) # inventory, production, etc. - new field, make nullable for now
|
||||
alert_type = Column(String(100), nullable=False) # specific type of alert (e.g., 'low_stock', 'supplier_delay') - from old schema
|
||||
service = Column(String(100), nullable=False)
|
||||
|
||||
# Content
|
||||
title = Column(String(500), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Alert-specific classification
|
||||
type_class = Column(
|
||||
Enum(AlertTypeClass, name='alerttypeclass', create_type=False, native_enum=True, values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Status
|
||||
status = Column(
|
||||
Enum(AlertStatus, name='alertstatus', create_type=False, native_enum=True, values_callable=lambda x: [e.value for e in x]),
|
||||
default=AlertStatus.ACTIVE,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Priority (multi-factor scored)
|
||||
priority_score = Column(Integer, nullable=False) # 0-100
|
||||
priority_level = Column(
|
||||
Enum(PriorityLevel, name='prioritylevel', create_type=False, native_enum=True, values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Enrichment context (JSONB)
|
||||
orchestrator_context = Column(JSONB, nullable=True)
|
||||
business_impact = Column(JSONB, nullable=True)
|
||||
urgency_context = Column(JSONB, nullable=True)
|
||||
user_agency = Column(JSONB, nullable=True)
|
||||
trend_context = Column(JSONB, nullable=True)
|
||||
|
||||
# Smart actions
|
||||
smart_actions = Column(JSONB, nullable=False)
|
||||
|
||||
# AI reasoning
|
||||
ai_reasoning_summary = Column(Text, nullable=True)
|
||||
confidence_score = Column(Float, nullable=False, default=0.8)
|
||||
|
||||
# Timing intelligence
|
||||
timing_decision = Column(String(50), nullable=False, default='send_now')
|
||||
scheduled_send_time = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Placement hints
|
||||
placement = Column(JSONB, nullable=False)
|
||||
|
||||
# Escalation & chaining
|
||||
action_created_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
superseded_by_action_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
hidden_from_ui = Column(Boolean, default=False, nullable=False, index=True)
|
||||
|
||||
# Metadata
|
||||
alert_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False, index=True)
|
||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_alerts_tenant_status', 'tenant_id', 'status'),
|
||||
Index('idx_alerts_priority_score', 'tenant_id', 'priority_score', 'created_at'),
|
||||
Index('idx_alerts_type_class', 'tenant_id', 'type_class', 'status'),
|
||||
Index('idx_alerts_domain', 'tenant_id', 'event_domain', 'status'),
|
||||
Index('idx_alerts_timing', 'timing_decision', 'scheduled_send_time'),
|
||||
CheckConstraint('priority_score >= 0 AND priority_score <= 100', name='chk_alert_priority_range'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for API/SSE"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'tenant_id': str(self.tenant_id),
|
||||
'event_class': 'alert',
|
||||
'event_domain': self.event_domain,
|
||||
'event_type': self.alert_type,
|
||||
'alert_type': self.alert_type, # Frontend expects this field name
|
||||
'service': self.service,
|
||||
'title': self.title,
|
||||
'message': self.message,
|
||||
'type_class': self.type_class.value if isinstance(self.type_class, AlertTypeClass) else self.type_class,
|
||||
'status': self.status.value if isinstance(self.status, AlertStatus) else self.status,
|
||||
'priority_level': self.priority_level.value if isinstance(self.priority_level, PriorityLevel) else self.priority_level,
|
||||
'priority_score': self.priority_score,
|
||||
'orchestrator_context': self.orchestrator_context,
|
||||
'business_impact': self.business_impact,
|
||||
'urgency_context': self.urgency_context,
|
||||
'user_agency': self.user_agency,
|
||||
'trend_context': self.trend_context,
|
||||
'actions': self.smart_actions,
|
||||
'ai_reasoning_summary': self.ai_reasoning_summary,
|
||||
'confidence_score': self.confidence_score,
|
||||
'timing_decision': self.timing_decision,
|
||||
'scheduled_send_time': self.scheduled_send_time.isoformat() if self.scheduled_send_time else None,
|
||||
'placement': self.placement,
|
||||
'action_created_at': self.action_created_at.isoformat() if self.action_created_at else None,
|
||||
'superseded_by_action_id': str(self.superseded_by_action_id) if self.superseded_by_action_id else None,
|
||||
'hidden_from_ui': self.hidden_from_ui,
|
||||
'alert_metadata': self.alert_metadata, # Frontend expects alert_metadata
|
||||
'metadata': self.alert_metadata, # Keep legacy field for backwards compat
|
||||
'timestamp': self.created_at.isoformat() if self.created_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'resolved_at': self.resolved_at.isoformat() if self.resolved_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# NOTIFICATION MODEL (Lightweight, Ephemeral)
|
||||
# ============================================================
|
||||
|
||||
class Notification(Base):
|
||||
"""
|
||||
Notification model for informational state changes.
|
||||
|
||||
Used for EventClass.NOTIFICATION only.
|
||||
Lightweight schema, no priority scoring, no lifecycle, 7-day TTL.
|
||||
"""
|
||||
__tablename__ = "notifications"
|
||||
|
||||
# Primary key
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Event classification
|
||||
event_domain = Column(String(50), nullable=False, index=True)
|
||||
event_type = Column(String(100), nullable=False)
|
||||
notification_type = Column(String(50), nullable=False) # NotificationType
|
||||
service = Column(String(100), nullable=False)
|
||||
|
||||
# Content
|
||||
title = Column(String(500), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Entity context (optional)
|
||||
entity_type = Column(String(100), nullable=True) # 'batch', 'delivery', 'po', etc.
|
||||
entity_id = Column(String(100), nullable=True, index=True)
|
||||
old_state = Column(String(100), nullable=True)
|
||||
new_state = Column(String(100), nullable=True)
|
||||
|
||||
# Display metadata
|
||||
notification_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Placement hints (lightweight)
|
||||
placement = Column(JSONB, nullable=False, default=['notification_panel'])
|
||||
|
||||
# TTL tracking
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False, index=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_notifications_tenant_domain', 'tenant_id', 'event_domain', 'created_at'),
|
||||
Index('idx_notifications_entity', 'tenant_id', 'entity_type', 'entity_id'),
|
||||
Index('idx_notifications_expiry', 'expires_at'),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Set default expiry to 7 days from now"""
|
||||
if 'expires_at' not in kwargs:
|
||||
kwargs['expires_at'] = utc_now() + timedelta(days=7)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for API/SSE"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'tenant_id': str(self.tenant_id),
|
||||
'event_class': 'notification',
|
||||
'event_domain': self.event_domain,
|
||||
'event_type': self.event_type,
|
||||
'notification_type': self.notification_type,
|
||||
'service': self.service,
|
||||
'title': self.title,
|
||||
'message': self.message,
|
||||
'entity_type': self.entity_type,
|
||||
'entity_id': self.entity_id,
|
||||
'old_state': self.old_state,
|
||||
'new_state': self.new_state,
|
||||
'metadata': self.notification_metadata,
|
||||
'placement': self.placement,
|
||||
'timestamp': self.created_at.isoformat() if self.created_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# RECOMMENDATION MODEL (Medium Weight, Dismissible)
|
||||
# ============================================================
|
||||
|
||||
class Recommendation(Base):
|
||||
"""
|
||||
Recommendation model for AI-generated suggestions.
|
||||
|
||||
Used for EventClass.RECOMMENDATION only.
|
||||
Medium weight schema, light priority, no orchestrator queries, dismissible.
|
||||
"""
|
||||
__tablename__ = "recommendations"
|
||||
|
||||
# Primary key
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Event classification
|
||||
event_domain = Column(String(50), nullable=False, index=True)
|
||||
event_type = Column(String(100), nullable=False)
|
||||
recommendation_type = Column(String(50), nullable=False) # RecommendationType
|
||||
service = Column(String(100), nullable=False)
|
||||
|
||||
# Content
|
||||
title = Column(String(500), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Light priority (info by default)
|
||||
priority_level = Column(String(50), nullable=False, default='info')
|
||||
|
||||
# Context (lighter than alerts)
|
||||
estimated_impact = Column(JSONB, nullable=True)
|
||||
suggested_actions = Column(JSONB, nullable=True)
|
||||
|
||||
# AI reasoning
|
||||
ai_reasoning_summary = Column(Text, nullable=True)
|
||||
confidence_score = Column(Float, nullable=True)
|
||||
|
||||
# Dismissal tracking
|
||||
dismissed_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
dismissed_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Metadata
|
||||
recommendation_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False, index=True)
|
||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_recommendations_tenant_domain', 'tenant_id', 'event_domain', 'created_at'),
|
||||
Index('idx_recommendations_dismissed', 'tenant_id', 'dismissed_at'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for API/SSE"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'tenant_id': str(self.tenant_id),
|
||||
'event_class': 'recommendation',
|
||||
'event_domain': self.event_domain,
|
||||
'event_type': self.event_type,
|
||||
'recommendation_type': self.recommendation_type,
|
||||
'service': self.service,
|
||||
'title': self.title,
|
||||
'message': self.message,
|
||||
'priority_level': self.priority_level,
|
||||
'estimated_impact': self.estimated_impact,
|
||||
'suggested_actions': self.suggested_actions,
|
||||
'ai_reasoning_summary': self.ai_reasoning_summary,
|
||||
'confidence_score': self.confidence_score,
|
||||
'dismissed_at': self.dismissed_at.isoformat() if self.dismissed_at else None,
|
||||
'dismissed_by': str(self.dismissed_by) if self.dismissed_by else None,
|
||||
'metadata': self.recommendation_metadata,
|
||||
'timestamp': self.created_at.isoformat() if self.created_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# INTERACTION TRACKING (Shared across all event types)
|
||||
# ============================================================
|
||||
|
||||
class EventInteraction(Base):
|
||||
"""Event interaction tracking for analytics"""
|
||||
__tablename__ = "event_interactions"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Event reference (polymorphic)
|
||||
event_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
event_class = Column(String(50), nullable=False, index=True) # 'alert', 'notification', 'recommendation'
|
||||
|
||||
# User
|
||||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Interaction details
|
||||
interaction_type = Column(String(50), nullable=False, index=True) # acknowledged, resolved, dismissed, clicked, etc.
|
||||
interacted_at = Column(DateTime(timezone=True), nullable=False, default=utc_now, index=True)
|
||||
response_time_seconds = Column(Integer, nullable=True)
|
||||
|
||||
# Context
|
||||
interaction_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=utc_now)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_event_interactions_event', 'event_id', 'event_class'),
|
||||
Index('idx_event_interactions_user', 'tenant_id', 'user_id', 'interacted_at'),
|
||||
)
|
||||
Reference in New Issue
Block a user