""" Event data structures for the Trixy application. This module defines the data classes that encapsulate information passed with events throughout the Trixy system. All event data classes inherit from TrixyEventData and provide structured, type-safe event information. """ from abc import ABC from dataclasses import dataclass, field from datetime import datetime from typing import Any, Dict, List, Optional, Union from enum import Enum class EventType(Enum): """Enumeration of all supported Trixy event types.""" # Satellite Management Events SATELLITE_CONNECTED = "satellite_connected" SATELLITE_DISCONNECTED = "satellite_disconnected" SATELLITE_REGISTERED = "satellite_registered" # Wakeword & Audio Events WAKEWORD_RECEIVED = "wakeword_received" RAW_AUDIO_INPUT_RECEIVED = "raw_audio_input_received" # Processing Events TEXT_RECEIVED = "text_received" INTENT_RECEIVED = "intent_received" TTS_RECEIVED = "tts_received" # Conversation Events CONVERSATION_STARTED = "conversation_started" CONVERSATION_ENDED = "conversation_ended" CONVERSATION_STATE_CHANGED = "conversation_state_changed" CONVERSATION_TURN_ADDED = "conversation_turn_added" CONVERSATION_CONTEXT_UPDATED = "conversation_context_updated" # Arbitration Events ARBITRATION_STARTED = "arbitration_started" ARBITRATION_COMPLETED = "arbitration_completed" ARBITRATION_CANCELLED = "arbitration_cancelled" ARBITRATION_TIMED_OUT = "arbitration_timed_out" ARBITRATION_REPORT_ADDED = "arbitration_report_added" SATELLITE_SELECTED = "satellite_selected" SATELLITE_IGNORED = "satellite_ignored" # System Events SYSTEM_STARTUP = "system_startup" SYSTEM_SHUTDOWN = "system_shutdown" PLUGIN_LOADED = "plugin_loaded" PLUGIN_UNLOADED = "plugin_unloaded" TRAINING_STARTED = "training_started" TRAINING_COMPLETED = "training_completed" SCHEDULE_TRIGGERED = "schedule_triggered" @dataclass class TrixyEventData(ABC): """ Base class for all Trixy event data. All event data classes should inherit from this class to provide consistent timestamp tracking and metadata support. """ timestamp: datetime = field(default_factory=datetime.now) metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convert event data to dictionary representation.""" result = {} for key, value in self.__dict__.items(): if isinstance(value, datetime): result[key] = value.isoformat() elif hasattr(value, 'to_dict'): result[key] = value.to_dict() else: result[key] = value return result @dataclass class SatelliteInfo: """Information about a satellite device.""" satellite_id: str mac_address: str room_id: str alias: str version: str audio_ports: Dict[str, int] = field(default_factory=dict) capabilities: List[str] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """Convert satellite info to dictionary.""" return { 'satellite_id': self.satellite_id, 'mac_address': self.mac_address, 'room_id': self.room_id, 'alias': self.alias, 'version': self.version, 'audio_ports': self.audio_ports, 'capabilities': self.capabilities } @dataclass class SpeakerInfo: """Information about a detected speaker.""" speaker_id: str speaker_name: str confidence: float = 0.0 embedding: Optional[List[float]] = None def to_dict(self) -> Dict[str, Any]: """Convert speaker info to dictionary.""" return { 'speaker_id': self.speaker_id, 'speaker_name': self.speaker_name, 'confidence': self.confidence, 'embedding': self.embedding } # Satellite Management Event Data Classes @dataclass class SatelliteConnectedEventData(TrixyEventData): """Data for satellite_connected event.""" satellite_info: SatelliteInfo = field(default=None) connection_time: datetime = field(default_factory=datetime.now) server_assigned_ports: Dict[str, int] = field(default_factory=dict) @dataclass class SatelliteDisconnectedEventData(TrixyEventData): """Data for satellite_disconnected event.""" satellite_id: str = field(default="") satellite_info: Optional[SatelliteInfo] = None disconnection_reason: str = "unknown" was_clean_disconnect: bool = False @dataclass class SatelliteRegisteredEventData(TrixyEventData): """Data for satellite_registered event.""" satellite_info: SatelliteInfo = field(default=None) registration_file_path: str = field(default="") registration_mode_timeout: int = 60 # Wakeword & Audio Event Data Classes @dataclass class WakewordReceivedEventData(TrixyEventData): """Data for wakeword_received event.""" wakeword_id: str = field(default="") wakeword_type: str = field(default="") # "custom", "system_command" speaker_info: SpeakerInfo = field(default=None) satellite_info: SatelliteInfo = field(default=None) volume: float = field(default=0.0) confidence: float = 0.0 raw_scores: Dict[str, float] = field(default_factory=dict) processing_time_ms: float = 0.0 audio_buffer_length: float = 0.0 # seconds chunk_id: int = 0 features_shape: List[int] = field(default_factory=list) model_name: str = field(default="") temporal_filtered: bool = False @dataclass class RawAudioInputReceivedEventData(TrixyEventData): """Data for raw_audio_input_received event.""" conversation_id: str = field(default="") audio_data: bytes = field(default=b"") speaker_info: SpeakerInfo = field(default=None) satellite_info: SatelliteInfo = field(default=None) sample_rate: int = 16000 channels: int = 1 bit_depth: int = 16 duration_seconds: float = 0.0 # Processing Event Data Classes @dataclass class TextReceivedEventData(TrixyEventData): """Data for text_received event.""" conversation_id: str = field(default="") text: str = field(default="") confidence: float = field(default=0.0) speaker_info: SpeakerInfo = field(default=None) satellite_info: Optional[SatelliteInfo] = None language_code: str = "en-US" processing_time_ms: float = 0.0 @dataclass class IntentReceivedEventData(TrixyEventData): """Data for intent_received event.""" conversation_id: str = field(default="") intent: str = field(default="") entities: Dict[str, Any] = field(default_factory=dict) confidence: float = field(default=0.0) original_text: str = field(default="") speaker_info: SpeakerInfo = field(default=None) satellite_info: Optional[SatelliteInfo] = None processing_time_ms: float = 0.0 @dataclass class TTSReceivedEventData(TrixyEventData): """Data for tts_received event.""" conversation_id: str = field(default="") audio_data: bytes = field(default=b"") text: str = field(default="") voice_settings: Dict[str, Any] = field(default_factory=dict) sample_rate: int = 16000 channels: int = 1 bit_depth: int = 16 duration_seconds: float = 0.0 target_satellite_id: Optional[str] = None # System Event Data Classes @dataclass class SystemStartupEventData(TrixyEventData): """Data for system_startup event.""" mode: str = field(default="") # "server", "client", "standalone" version: str = field(default="") config_file: str = field(default="") debug_mode: bool = False startup_time_ms: float = 0.0 @dataclass class SystemShutdownEventData(TrixyEventData): """Data for system_shutdown event.""" reason: str = "user_requested" graceful_shutdown: bool = True uptime_seconds: float = 0.0 @dataclass class PluginEventData(TrixyEventData): """Base data for plugin-related events.""" plugin_name: str = field(default="") plugin_path: str = field(default="") plugin_version: str = "unknown" config_data: Dict[str, Any] = field(default_factory=dict) @dataclass class PluginLoadedEventData(PluginEventData): """Data for plugin_loaded event.""" load_time_ms: float = 0.0 enabled: bool = True @dataclass class PluginUnloadedEventData(PluginEventData): """Data for plugin_unloaded event.""" unload_reason: str = "user_requested" was_enabled: bool = False @dataclass class TrainingStartedEventData(TrixyEventData): """Data for training_started event.""" training_type: str = field(default="") # "wakeword", "voice_recognition" model_name: str = field(default="") dataset_size: int = field(default=0) estimated_duration_minutes: float = 0.0 training_parameters: Dict[str, Any] = field(default_factory=dict) @dataclass class TrainingCompletedEventData(TrixyEventData): """Data for training_completed event.""" training_type: str = field(default="") model_name: str = field(default="") success: bool = field(default=False) final_accuracy: float = 0.0 training_duration_minutes: float = 0.0 model_save_path: str = "" error_message: str = "" @dataclass class ScheduleTriggeredEventData(TrixyEventData): """Data for schedule_triggered event.""" schedule_id: str = field(default="") schedule_name: str = field(default="") trigger_type: str = field(default="") # "date", "time", "event", "weekday" trigger_value: str = field(default="") action_type: str = field(default="") # "trigger_event", "start_training", "call_function" action_data: Dict[str, Any] = field(default_factory=dict) next_trigger_time: Optional[datetime] = None # Conversation Event Data Classes @dataclass class ConversationStartedEventData(TrixyEventData): """Data for conversation_started event.""" conversation_id: str = field(default="") trigger_event: str = field(default="") speaker_info: Optional[SpeakerInfo] = None satellite_info: Optional[SatelliteInfo] = None timeout_seconds: float = 300.0 max_turns: int = 50 @dataclass class ConversationEndedEventData(TrixyEventData): """Data for conversation_ended event.""" conversation_id: str = field(default="") end_reason: str = field(default="") # "completed", "timeout", "cancelled", "error" final_state: str = field(default="") duration_seconds: float = 0.0 turn_count: int = 0 speaker_info: Optional[SpeakerInfo] = None satellite_info: Optional[SatelliteInfo] = None final_response: Optional[str] = None error_message: Optional[str] = None @dataclass class ConversationStateChangedEventData(TrixyEventData): """Data for conversation_state_changed event.""" conversation_id: str = field(default="") from_state: str = field(default="") to_state: str = field(default="") trigger: str = field(default="") source: str = field(default="") duration_ms: float = 0.0 metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class ConversationTurnAddedEventData(TrixyEventData): """Data for conversation_turn_added event.""" conversation_id: str = field(default="") turn_id: str = field(default="") turn_number: int = 0 role: str = field(default="") # "user", "assistant", "system", "plugin" input_type: str = field(default="") input_text: Optional[str] = None input_confidence: float = 0.0 speaker_info: Optional[SpeakerInfo] = None satellite_info: Optional[SatelliteInfo] = None plugin_name: Optional[str] = None @dataclass class ConversationContextUpdatedEventData(TrixyEventData): """Data for conversation_context_updated event.""" conversation_id: str = field(default="") context_key: str = field(default="") context_scope: str = field(default="") # "session", "speaker", "satellite", "room", "global", "plugin" operation: str = field(default="") # "set", "update", "delete", "clear" source: str = field(default="") metadata: Dict[str, Any] = field(default_factory=dict) # Arbitration Event Data Classes @dataclass class ArbitrationStartedEventData(TrixyEventData): """Data for arbitration_started event.""" arbitration_session_id: str = field(default="") trigger_satellite_id: str = field(default="") trigger_wakeword_id: str = field(default="") collection_window_seconds: float = 1.0 algorithm: str = field(default="volume_based") expected_participants: int = 1 @dataclass class ArbitrationCompletedEventData(TrixyEventData): """Data for arbitration_completed event.""" arbitration_session_id: str = field(default="") selected_satellite_id: str = field(default="") selection_algorithm: str = field(default="") selection_score: float = 0.0 selection_confidence: float = 0.0 total_participants: int = 0 collection_duration_ms: float = 0.0 arbitration_duration_ms: float = 0.0 conversation_id: Optional[str] = None selection_reason: str = field(default="") alternative_selections: Dict[str, float] = field(default_factory=dict) @dataclass class ArbitrationCancelledEventData(TrixyEventData): """Data for arbitration_cancelled event.""" arbitration_session_id: str = field(default="") cancellation_reason: str = field(default="") participants_count: int = 0 duration_ms: float = 0.0 cancelled_by: str = field(default="system") @dataclass class ArbitrationTimedOutEventData(TrixyEventData): """Data for arbitration_timed_out event.""" arbitration_session_id: str = field(default="") timeout_duration_ms: float = 0.0 participants_count: int = 0 reports_received: int = 0 timeout_phase: str = field(default="") # "collection", "arbitration", "session" @dataclass class ArbitrationReportAddedEventData(TrixyEventData): """Data for arbitration_report_added event.""" arbitration_session_id: str = field(default="") satellite_id: str = field(default="") wakeword_id: str = field(default="") volume: float = 0.0 confidence: float = 0.0 speaker_info: Optional[SpeakerInfo] = None satellite_info: Optional[SatelliteInfo] = None report_timestamp: datetime = field(default_factory=datetime.now) estimated_distance: Optional[float] = None room_id: str = field(default="") total_reports_in_session: int = 1 @dataclass class SatelliteSelectedEventData(TrixyEventData): """Data for satellite_selected event.""" arbitration_session_id: str = field(default="") selected_satellite_id: str = field(default="") satellite_info: Optional[SatelliteInfo] = None selection_algorithm: str = field(default="") selection_score: float = 0.0 selection_confidence: float = 0.0 conversation_id: str = field(default="") selection_metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class SatelliteIgnoredEventData(TrixyEventData): """Data for satellite_ignored event.""" arbitration_session_id: str = field(default="") ignored_satellite_id: str = field(default="") satellite_info: Optional[SatelliteInfo] = None selected_satellite_id: str = field(default="") ignore_reason: str = field(default="not_selected") original_report_volume: float = 0.0 original_report_confidence: float = 0.0 # Event Data Factory class EventDataFactory: """Factory class for creating event data instances.""" _event_data_classes = { EventType.SATELLITE_CONNECTED: SatelliteConnectedEventData, EventType.SATELLITE_DISCONNECTED: SatelliteDisconnectedEventData, EventType.SATELLITE_REGISTERED: SatelliteRegisteredEventData, EventType.WAKEWORD_RECEIVED: WakewordReceivedEventData, EventType.RAW_AUDIO_INPUT_RECEIVED: RawAudioInputReceivedEventData, EventType.TEXT_RECEIVED: TextReceivedEventData, EventType.INTENT_RECEIVED: IntentReceivedEventData, EventType.TTS_RECEIVED: TTSReceivedEventData, EventType.CONVERSATION_STARTED: ConversationStartedEventData, EventType.CONVERSATION_ENDED: ConversationEndedEventData, EventType.CONVERSATION_STATE_CHANGED: ConversationStateChangedEventData, EventType.CONVERSATION_TURN_ADDED: ConversationTurnAddedEventData, EventType.CONVERSATION_CONTEXT_UPDATED: ConversationContextUpdatedEventData, EventType.ARBITRATION_STARTED: ArbitrationStartedEventData, EventType.ARBITRATION_COMPLETED: ArbitrationCompletedEventData, EventType.ARBITRATION_CANCELLED: ArbitrationCancelledEventData, EventType.ARBITRATION_TIMED_OUT: ArbitrationTimedOutEventData, EventType.ARBITRATION_REPORT_ADDED: ArbitrationReportAddedEventData, EventType.SATELLITE_SELECTED: SatelliteSelectedEventData, EventType.SATELLITE_IGNORED: SatelliteIgnoredEventData, EventType.SYSTEM_STARTUP: SystemStartupEventData, EventType.SYSTEM_SHUTDOWN: SystemShutdownEventData, EventType.PLUGIN_LOADED: PluginLoadedEventData, EventType.PLUGIN_UNLOADED: PluginUnloadedEventData, EventType.TRAINING_STARTED: TrainingStartedEventData, EventType.TRAINING_COMPLETED: TrainingCompletedEventData, EventType.SCHEDULE_TRIGGERED: ScheduleTriggeredEventData, } @classmethod def create_event_data(cls, event_type: Union[EventType, str], **kwargs) -> TrixyEventData: """ Create an event data instance for the specified event type. Args: event_type: The type of event (EventType enum or string) **kwargs: Arguments to pass to the event data constructor Returns: TrixyEventData: An instance of the appropriate event data class Raises: ValueError: If the event type is not supported """ if isinstance(event_type, str): try: event_type = EventType(event_type) except ValueError: raise ValueError(f"Unknown event type: {event_type}") if event_type not in cls._event_data_classes: raise ValueError(f"No event data class registered for event type: {event_type}") event_data_class = cls._event_data_classes[event_type] return event_data_class(**kwargs) @classmethod def get_supported_events(cls) -> List[EventType]: """Get a list of all supported event types.""" return list(cls._event_data_classes.keys()) @classmethod def is_supported_event(cls, event_type: Union[EventType, str]) -> bool: """Check if an event type is supported.""" if isinstance(event_type, str): try: event_type = EventType(event_type) except ValueError: return False return event_type in cls._event_data_classes