| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958 |
- """
- Conversation State Management and Transitions
- This module implements conversation state tracking and state transition management
- for the Trixy voice assistant system. It provides a finite state machine approach
- to managing conversation phases, transitions, and validation.
- Key Features:
- - Finite state machine for conversation phases
- - State transition validation and rules
- - State history tracking and analytics
- - Thread-safe state operations
- - Integration with conversation sessions
- - Support for plugin-driven state changes
- - State persistence and restoration
- Classes:
- - ConversationState: Core state enumeration
- - ConversationStateManager: State management and transitions
- - StateTransition: Individual state transition record
- - StateTransitionRules: Validation rules for transitions
- - ConversationPhase: High-level conversation phases
- """
- import threading
- import time
- from datetime import datetime, timezone, timedelta
- from dataclasses import dataclass, field
- from enum import Enum
- from typing import Any, Dict, List, Optional, Set, Callable, Tuple
- import json
- from pathlib import Path
- import weakref
- def pprint(message: str) -> None:
- """Conversation state logging function."""
- print(f"[CONVERSATION_STATE] {message}")
- class ConversationState(Enum):
- """
- Core conversation states representing the current phase of a conversation.
-
- These states map to the conversation workflow specified in CLAUDE.md:
- 1. Wakeword detection starts conversation session
- 2. Raw audio input with conversation_id
- 3. Text processing with same conversation_id
- 4. Intent handling with conversation tracking
- 5. Multi-turn conversations where plugins ask questions and wait for responses
- """
- # Initialization states
- INITIALIZING = "initializing" # Session being created
- READY = "ready" # Ready for input
-
- # Input processing states
- LISTENING = "listening" # Waiting for or receiving audio input
- TRANSCRIBING = "transcribing" # Converting audio to text
- PROCESSING_INTENT = "processing_intent" # Extracting intent from text
-
- # Response generation states
- GENERATING_RESPONSE = "generating_response" # Creating response
- SYNTHESIZING_SPEECH = "synthesizing_speech" # Converting text to speech
- PLAYING_RESPONSE = "playing_response" # Playing audio response
-
- # Multi-turn states
- WAITING_FOR_USER = "waiting_for_user" # Waiting for user input
- WAITING_FOR_PLUGIN = "waiting_for_plugin" # Plugin processing
- PLUGIN_INTERACTION = "plugin_interaction" # Plugin asking questions
-
- # Terminal states
- COMPLETED = "completed" # Conversation completed successfully
- TIMED_OUT = "timed_out" # Conversation timed out
- ERROR = "error" # Error occurred
- CANCELLED = "cancelled" # Conversation cancelled
- ABORTED = "aborted" # Conversation aborted due to error
- class ConversationPhase(Enum):
- """
- High-level conversation phases that group related states.
- """
- STARTUP = "startup" # Initialization and setup
- INPUT = "input" # Receiving and processing input
- PROCESSING = "processing" # Intent processing and analysis
- OUTPUT = "output" # Response generation and delivery
- INTERACTION = "interaction" # Multi-turn interaction
- TERMINAL = "terminal" # End states
- # State to phase mapping
- STATE_TO_PHASE = {
- ConversationState.INITIALIZING: ConversationPhase.STARTUP,
- ConversationState.READY: ConversationPhase.STARTUP,
-
- ConversationState.LISTENING: ConversationPhase.INPUT,
- ConversationState.TRANSCRIBING: ConversationPhase.INPUT,
-
- ConversationState.PROCESSING_INTENT: ConversationPhase.PROCESSING,
- ConversationState.WAITING_FOR_PLUGIN: ConversationPhase.PROCESSING,
- ConversationState.PLUGIN_INTERACTION: ConversationPhase.PROCESSING,
-
- ConversationState.GENERATING_RESPONSE: ConversationPhase.OUTPUT,
- ConversationState.SYNTHESIZING_SPEECH: ConversationPhase.OUTPUT,
- ConversationState.PLAYING_RESPONSE: ConversationPhase.OUTPUT,
-
- ConversationState.WAITING_FOR_USER: ConversationPhase.INTERACTION,
-
- ConversationState.COMPLETED: ConversationPhase.TERMINAL,
- ConversationState.TIMED_OUT: ConversationPhase.TERMINAL,
- ConversationState.ERROR: ConversationPhase.TERMINAL,
- ConversationState.CANCELLED: ConversationPhase.TERMINAL,
- ConversationState.ABORTED: ConversationPhase.TERMINAL,
- }
- @dataclass
- class StateTransition:
- """
- Represents a single state transition in a conversation.
-
- Tracks the complete context of state changes for analytics and debugging.
- """
- transition_id: str
- from_state: ConversationState
- to_state: ConversationState
- timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
-
- # Transition context
- trigger: str = "system" # What triggered the transition
- source: str = "state_manager" # Component that initiated transition
- duration_ms: float = 0.0 # Time spent in previous state
-
- # Additional data
- metadata: Dict[str, Any] = field(default_factory=dict)
- error_message: Optional[str] = None
-
- def to_dict(self) -> Dict[str, Any]:
- """Convert transition to dictionary representation."""
- return {
- "transition_id": self.transition_id,
- "from_state": self.from_state.value,
- "to_state": self.to_state.value,
- "timestamp": self.timestamp.isoformat(),
- "trigger": self.trigger,
- "source": self.source,
- "duration_ms": self.duration_ms,
- "metadata": self.metadata,
- "error_message": self.error_message
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'StateTransition':
- """Create StateTransition from dictionary."""
- return cls(
- transition_id=data["transition_id"],
- from_state=ConversationState(data["from_state"]),
- to_state=ConversationState(data["to_state"]),
- timestamp=datetime.fromisoformat(data["timestamp"]),
- trigger=data.get("trigger", "system"),
- source=data.get("source", "state_manager"),
- duration_ms=data.get("duration_ms", 0.0),
- metadata=data.get("metadata", {}),
- error_message=data.get("error_message")
- )
- class StateTransitionRules:
- """
- Defines and validates state transition rules for conversations.
-
- This class implements a finite state machine with validation rules
- to ensure conversations follow valid state sequences.
- """
-
- # Valid state transitions (from_state -> set of valid to_states)
- VALID_TRANSITIONS = {
- ConversationState.INITIALIZING: {
- ConversationState.READY,
- ConversationState.ERROR
- },
-
- ConversationState.READY: {
- ConversationState.LISTENING,
- ConversationState.WAITING_FOR_USER,
- ConversationState.COMPLETED,
- ConversationState.CANCELLED,
- ConversationState.TIMED_OUT,
- ConversationState.ERROR
- },
-
- ConversationState.LISTENING: {
- ConversationState.TRANSCRIBING,
- ConversationState.READY,
- ConversationState.TIMED_OUT,
- ConversationState.CANCELLED,
- ConversationState.ERROR
- },
-
- ConversationState.TRANSCRIBING: {
- ConversationState.PROCESSING_INTENT,
- ConversationState.LISTENING,
- ConversationState.ERROR
- },
-
- ConversationState.PROCESSING_INTENT: {
- ConversationState.GENERATING_RESPONSE,
- ConversationState.WAITING_FOR_PLUGIN,
- ConversationState.PLUGIN_INTERACTION,
- ConversationState.ERROR
- },
-
- ConversationState.WAITING_FOR_PLUGIN: {
- ConversationState.PLUGIN_INTERACTION,
- ConversationState.GENERATING_RESPONSE,
- ConversationState.TIMED_OUT,
- ConversationState.ERROR
- },
-
- ConversationState.PLUGIN_INTERACTION: {
- ConversationState.WAITING_FOR_USER,
- ConversationState.GENERATING_RESPONSE,
- ConversationState.LISTENING,
- ConversationState.COMPLETED,
- ConversationState.ERROR
- },
-
- ConversationState.GENERATING_RESPONSE: {
- ConversationState.SYNTHESIZING_SPEECH,
- ConversationState.COMPLETED,
- ConversationState.ERROR
- },
-
- ConversationState.SYNTHESIZING_SPEECH: {
- ConversationState.PLAYING_RESPONSE,
- ConversationState.ERROR
- },
-
- ConversationState.PLAYING_RESPONSE: {
- ConversationState.WAITING_FOR_USER,
- ConversationState.COMPLETED,
- ConversationState.ERROR
- },
-
- ConversationState.WAITING_FOR_USER: {
- ConversationState.LISTENING,
- ConversationState.PLUGIN_INTERACTION,
- ConversationState.COMPLETED,
- ConversationState.TIMED_OUT,
- ConversationState.CANCELLED,
- ConversationState.ERROR
- },
-
- # Terminal states (no transitions allowed except to error)
- ConversationState.COMPLETED: {ConversationState.ERROR},
- ConversationState.TIMED_OUT: {ConversationState.ERROR},
- ConversationState.CANCELLED: {ConversationState.ERROR},
- ConversationState.ABORTED: {ConversationState.ERROR},
- ConversationState.ERROR: set() # No transitions from error
- }
-
- @classmethod
- def is_valid_transition(
- cls,
- from_state: ConversationState,
- to_state: ConversationState
- ) -> bool:
- """
- Check if a state transition is valid.
-
- Args:
- from_state: Current state
- to_state: Desired next state
-
- Returns:
- True if transition is valid
- """
- if from_state not in cls.VALID_TRANSITIONS:
- return False
-
- return to_state in cls.VALID_TRANSITIONS[from_state]
-
- @classmethod
- def get_valid_next_states(
- cls,
- current_state: ConversationState
- ) -> Set[ConversationState]:
- """
- Get all valid next states for a given current state.
-
- Args:
- current_state: Current conversation state
-
- Returns:
- Set of valid next states
- """
- return cls.VALID_TRANSITIONS.get(current_state, set())
-
- @classmethod
- def is_terminal_state(cls, state: ConversationState) -> bool:
- """
- Check if a state is terminal (conversation ending).
-
- Args:
- state: State to check
-
- Returns:
- True if state is terminal
- """
- return STATE_TO_PHASE.get(state) == ConversationPhase.TERMINAL
-
- @classmethod
- def get_phase(cls, state: ConversationState) -> ConversationPhase:
- """
- Get the conversation phase for a given state.
-
- Args:
- state: Conversation state
-
- Returns:
- Conversation phase
- """
- return STATE_TO_PHASE.get(state, ConversationPhase.TERMINAL)
- class ConversationStateError(Exception):
- """Base exception for conversation state errors."""
- pass
- class InvalidStateTransitionError(ConversationStateError):
- """Raised when an invalid state transition is attempted."""
- pass
- class ConversationStateManager:
- """
- Manages conversation state transitions and validation.
-
- This class implements a finite state machine for conversation management,
- ensuring valid state transitions and providing comprehensive state tracking
- and analytics.
-
- Features:
- - Finite state machine with validation
- - State history tracking
- - Thread-safe state operations
- - State change notifications
- - Integration with conversation sessions
- - State persistence and restoration
- - Performance analytics
- """
-
- def __init__(
- self,
- conversation_id: str,
- initial_state: ConversationState = ConversationState.INITIALIZING,
- enable_validation: bool = True,
- max_history_size: int = 100,
- state_timeout_seconds: float = 300.0,
- application_ref: Optional[weakref.ref] = None
- ):
- """
- Initialize conversation state manager.
-
- Args:
- conversation_id: Conversation identifier
- initial_state: Initial conversation state
- enable_validation: Enable state transition validation
- max_history_size: Maximum state history entries
- state_timeout_seconds: Timeout for state changes
- application_ref: Weak reference to application container
- """
- # Core properties
- self._conversation_id = conversation_id
- self._current_state = initial_state
- self._previous_state: Optional[ConversationState] = None
- self._enable_validation = enable_validation
- self._state_timeout = state_timeout_seconds
-
- # Timing
- self._state_start_time = datetime.now(timezone.utc)
- self._last_transition_time = self._state_start_time
- self._creation_time = self._state_start_time
-
- # History and tracking
- self._transition_history: List[StateTransition] = []
- self._max_history_size = max_history_size
-
- # State change listeners
- self._state_listeners: List[Callable[[ConversationState, ConversationState], None]] = []
-
- # Thread safety
- self._lock = threading.RLock()
-
- # Application integration
- self._application_ref = application_ref
-
- # Transition counter for unique IDs
- self._transition_counter = 0
-
- # Performance tracking
- self._total_transitions = 0
- self._error_count = 0
- self._timeout_count = 0
-
- pprint(f"State manager initialized for conversation {conversation_id} in state {initial_state.value}")
-
- # Properties
-
- @property
- def conversation_id(self) -> str:
- """Get the conversation ID."""
- return self._conversation_id
-
- @property
- def current_state(self) -> ConversationState:
- """Get the current conversation state."""
- with self._lock:
- return self._current_state
-
- @property
- def previous_state(self) -> Optional[ConversationState]:
- """Get the previous conversation state."""
- with self._lock:
- return self._previous_state
-
- @property
- def current_phase(self) -> ConversationPhase:
- """Get the current conversation phase."""
- return StateTransitionRules.get_phase(self.current_state)
-
- @property
- def is_terminal(self) -> bool:
- """Check if current state is terminal."""
- return StateTransitionRules.is_terminal_state(self.current_state)
-
- @property
- def state_duration_seconds(self) -> float:
- """Get duration in current state in seconds."""
- with self._lock:
- return (datetime.now(timezone.utc) - self._state_start_time).total_seconds()
-
- @property
- def total_duration_seconds(self) -> float:
- """Get total conversation duration in seconds."""
- with self._lock:
- return (datetime.now(timezone.utc) - self._creation_time).total_seconds()
-
- @property
- def transition_count(self) -> int:
- """Get number of state transitions."""
- with self._lock:
- return len(self._transition_history)
-
- # State Management
-
- def transition_to(
- self,
- new_state: ConversationState,
- trigger: str = "system",
- source: str = "state_manager",
- metadata: Optional[Dict[str, Any]] = None,
- force: bool = False
- ) -> bool:
- """
- Transition to a new conversation state.
-
- Args:
- new_state: Target state
- trigger: What triggered the transition
- source: Component that initiated the transition
- metadata: Additional transition metadata
- force: Force transition even if validation fails
-
- Returns:
- True if transition was successful
-
- Raises:
- InvalidStateTransitionError: If transition is not valid and not forced
- """
- with self._lock:
- current_state = self._current_state
-
- # Check if already in target state
- if current_state == new_state:
- return True
-
- # Validate transition
- if self._enable_validation and not force:
- if not StateTransitionRules.is_valid_transition(current_state, new_state):
- error_msg = f"Invalid transition from {current_state.value} to {new_state.value}"
- raise InvalidStateTransitionError(error_msg)
-
- # Calculate duration in previous state
- now = datetime.now(timezone.utc)
- duration_ms = (now - self._state_start_time).total_seconds() * 1000
-
- # Create transition record
- self._transition_counter += 1
- transition = StateTransition(
- transition_id=f"{self._conversation_id}_{self._transition_counter:06d}",
- from_state=current_state,
- to_state=new_state,
- timestamp=now,
- trigger=trigger,
- source=source,
- duration_ms=duration_ms,
- metadata=metadata or {}
- )
-
- # Update state
- self._previous_state = current_state
- self._current_state = new_state
- self._state_start_time = now
- self._last_transition_time = now
- self._total_transitions += 1
-
- # Add to history
- self._transition_history.append(transition)
-
- # Trim history if needed
- if len(self._transition_history) > self._max_history_size:
- self._transition_history.pop(0)
-
- # Notify listeners
- self._notify_state_change(current_state, new_state)
-
- pprint(f"Conversation {self._conversation_id} transitioned: {current_state.value} -> {new_state.value}")
-
- # Trigger event if application is available
- self._trigger_state_change_event(transition)
-
- return True
-
- def can_transition_to(self, target_state: ConversationState) -> bool:
- """
- Check if transition to target state is valid.
-
- Args:
- target_state: Target state to check
-
- Returns:
- True if transition is valid
- """
- with self._lock:
- if not self._enable_validation:
- return True
-
- return StateTransitionRules.is_valid_transition(self._current_state, target_state)
-
- def get_valid_next_states(self) -> Set[ConversationState]:
- """
- Get all valid next states from current state.
-
- Returns:
- Set of valid next states
- """
- with self._lock:
- return StateTransitionRules.get_valid_next_states(self._current_state)
-
- def reset_to_state(
- self,
- state: ConversationState,
- reason: str = "reset"
- ) -> None:
- """
- Reset state manager to a specific state (bypass validation).
-
- Args:
- state: State to reset to
- reason: Reason for reset
- """
- self.transition_to(
- state,
- trigger=f"reset: {reason}",
- source="state_manager",
- force=True
- )
-
- # State Monitoring
-
- def is_state_timed_out(self) -> bool:
- """
- Check if current state has timed out.
-
- Returns:
- True if state has exceeded timeout
- """
- return self.state_duration_seconds > self._state_timeout
-
- def check_timeout(self) -> bool:
- """
- Check for timeout and transition to TIMED_OUT state if needed.
-
- Returns:
- True if timeout occurred and state was changed
- """
- if self.is_state_timed_out() and not self.is_terminal:
- self._timeout_count += 1
- self.transition_to(
- ConversationState.TIMED_OUT,
- trigger="timeout",
- source="state_manager",
- metadata={"timeout_seconds": self._state_timeout}
- )
- return True
- return False
-
- def handle_error(self, error_message: str, source: str = "unknown") -> None:
- """
- Handle an error and transition to ERROR state.
-
- Args:
- error_message: Error description
- source: Source of the error
- """
- self._error_count += 1
- self.transition_to(
- ConversationState.ERROR,
- trigger="error",
- source=source,
- metadata={"error_message": error_message},
- force=True # Errors can transition from any state
- )
-
- # State Listeners
-
- def add_state_listener(
- self,
- listener: Callable[[ConversationState, ConversationState], None]
- ) -> None:
- """
- Add a state change listener.
-
- Args:
- listener: Function called on state changes (from_state, to_state)
- """
- with self._lock:
- if listener not in self._state_listeners:
- self._state_listeners.append(listener)
-
- def remove_state_listener(
- self,
- listener: Callable[[ConversationState, ConversationState], None]
- ) -> None:
- """
- Remove a state change listener.
-
- Args:
- listener: Listener function to remove
- """
- with self._lock:
- if listener in self._state_listeners:
- self._state_listeners.remove(listener)
-
- def _notify_state_change(
- self,
- from_state: ConversationState,
- to_state: ConversationState
- ) -> None:
- """Notify all state change listeners."""
- for listener in self._state_listeners:
- try:
- listener(from_state, to_state)
- except Exception as e:
- pprint(f"Error in state change listener: {e}")
-
- # History and Analytics
-
- def get_transition_history(
- self,
- limit: Optional[int] = None,
- since: Optional[datetime] = None
- ) -> List[StateTransition]:
- """
- Get state transition history.
-
- Args:
- limit: Maximum number of transitions to return
- since: Only return transitions after this time
-
- Returns:
- List of state transitions
- """
- with self._lock:
- history = self._transition_history
-
- # Filter by time
- if since:
- history = [t for t in history if t.timestamp >= since]
-
- # Apply limit
- if limit:
- history = history[-limit:]
-
- return history.copy()
-
- def get_state_durations(self) -> Dict[ConversationState, float]:
- """
- Get total time spent in each state.
-
- Returns:
- Dictionary mapping states to total duration in seconds
- """
- with self._lock:
- durations = {}
-
- for transition in self._transition_history:
- state = transition.from_state
- duration_seconds = transition.duration_ms / 1000.0
- durations[state] = durations.get(state, 0.0) + duration_seconds
-
- # Add current state duration
- current_duration = self.state_duration_seconds
- durations[self._current_state] = durations.get(self._current_state, 0.0) + current_duration
-
- return durations
-
- def get_analytics(self) -> Dict[str, Any]:
- """
- Get comprehensive state analytics.
-
- Returns:
- Dictionary containing state analytics
- """
- with self._lock:
- durations = self.get_state_durations()
- total_duration = self.total_duration_seconds
-
- # Calculate state statistics
- state_stats = {}
- for state, duration in durations.items():
- percentage = (duration / max(total_duration, 1.0)) * 100
- transition_count = len([t for t in self._transition_history if t.from_state == state])
-
- state_stats[state.value] = {
- "total_duration_seconds": duration,
- "percentage_of_total": round(percentage, 2),
- "transition_count": transition_count,
- "average_duration_seconds": duration / max(transition_count, 1)
- }
-
- # Calculate phase statistics
- phase_stats = {}
- for phase in ConversationPhase:
- phase_states = [s for s in ConversationState if STATE_TO_PHASE.get(s) == phase]
- phase_duration = sum(durations.get(s, 0.0) for s in phase_states)
- phase_percentage = (phase_duration / max(total_duration, 1.0)) * 100
-
- phase_stats[phase.value] = {
- "total_duration_seconds": phase_duration,
- "percentage_of_total": round(phase_percentage, 2),
- "states": [s.value for s in phase_states if s in durations]
- }
-
- return {
- "conversation_id": self._conversation_id,
- "current_state": self._current_state.value,
- "current_phase": self.current_phase.value,
- "is_terminal": self.is_terminal,
- "total_duration_seconds": total_duration,
- "current_state_duration_seconds": self.state_duration_seconds,
- "total_transitions": self._total_transitions,
- "error_count": self._error_count,
- "timeout_count": self._timeout_count,
- "state_statistics": state_stats,
- "phase_statistics": phase_stats,
- "creation_time": self._creation_time.isoformat(),
- "last_transition_time": self._last_transition_time.isoformat()
- }
-
- # Integration
-
- def _trigger_state_change_event(self, transition: StateTransition) -> None:
- """Trigger state change event if application is available."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- # Create state change event data
- from ..events import EventDataFactory
-
- state_data = EventDataFactory.create_event_data(
- "conversation_state_changed",
- conversation_id=self._conversation_id,
- from_state=transition.from_state.value,
- to_state=transition.to_state.value,
- trigger=transition.trigger,
- source=transition.source,
- duration_ms=transition.duration_ms,
- metadata=transition.metadata
- )
-
- event_handler.trigger_event("conversation_state_changed", state_data)
-
- except Exception as e:
- pprint(f"Error triggering state change event: {e}")
-
- # Persistence
-
- def to_dict(self) -> Dict[str, Any]:
- """
- Convert state manager to dictionary representation.
-
- Returns:
- Dictionary containing state manager data
- """
- with self._lock:
- return {
- "conversation_id": self._conversation_id,
- "current_state": self._current_state.value,
- "previous_state": self._previous_state.value if self._previous_state else None,
- "enable_validation": self._enable_validation,
- "state_timeout": self._state_timeout,
- "creation_time": self._creation_time.isoformat(),
- "state_start_time": self._state_start_time.isoformat(),
- "last_transition_time": self._last_transition_time.isoformat(),
- "transition_history": [t.to_dict() for t in self._transition_history],
- "total_transitions": self._total_transitions,
- "error_count": self._error_count,
- "timeout_count": self._timeout_count,
- "analytics": self.get_analytics()
- }
-
- @classmethod
- def from_dict(
- cls,
- data: Dict[str, Any],
- application_ref: Optional[weakref.ref] = None
- ) -> 'ConversationStateManager':
- """
- Create ConversationStateManager from dictionary.
-
- Args:
- data: Dictionary data
- application_ref: Application reference
-
- Returns:
- ConversationStateManager: Restored state manager
- """
- # Create state manager
- state_manager = cls(
- conversation_id=data["conversation_id"],
- initial_state=ConversationState(data["current_state"]),
- enable_validation=data.get("enable_validation", True),
- state_timeout_seconds=data.get("state_timeout", 300.0),
- application_ref=application_ref
- )
-
- # Restore state
- if data.get("previous_state"):
- state_manager._previous_state = ConversationState(data["previous_state"])
- state_manager._creation_time = datetime.fromisoformat(data["creation_time"])
- state_manager._state_start_time = datetime.fromisoformat(data["state_start_time"])
- state_manager._last_transition_time = datetime.fromisoformat(data["last_transition_time"])
- state_manager._total_transitions = data.get("total_transitions", 0)
- state_manager._error_count = data.get("error_count", 0)
- state_manager._timeout_count = data.get("timeout_count", 0)
-
- # Restore transition history
- for transition_data in data.get("transition_history", []):
- transition = StateTransition.from_dict(transition_data)
- state_manager._transition_history.append(transition)
-
- return state_manager
-
- def save_to_file(self, file_path: Path) -> None:
- """
- Save state manager to JSON file.
-
- Args:
- file_path: Path to save the file
- """
- try:
- with open(file_path, 'w', encoding='utf-8') as f:
- json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
- pprint(f"State manager saved to {file_path}")
- except Exception as e:
- raise ConversationStateError(f"Failed to save state manager: {e}")
-
- @classmethod
- def load_from_file(
- cls,
- file_path: Path,
- application_ref: Optional[weakref.ref] = None
- ) -> 'ConversationStateManager':
- """
- Load state manager from JSON file.
-
- Args:
- file_path: Path to load from
- application_ref: Application reference
-
- Returns:
- ConversationStateManager: Loaded state manager
- """
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- data = json.load(f)
-
- state_manager = cls.from_dict(data, application_ref)
- pprint(f"State manager loaded from {file_path}")
- return state_manager
-
- except Exception as e:
- raise ConversationStateError(f"Failed to load state manager: {e}")
-
- def __str__(self) -> str:
- """String representation of the state manager."""
- return (f"ConversationStateManager(conversation={self._conversation_id}, "
- f"state={self._current_state.value}, transitions={self.transition_count})")
-
- def __repr__(self) -> str:
- """Detailed representation of the state manager."""
- return (f"ConversationStateManager(conversation_id='{self._conversation_id}', "
- f"current_state={self._current_state.value}, "
- f"previous_state={self._previous_state.value if self._previous_state else None}, "
- f"transitions={self.transition_count}, "
- f"duration={self.total_duration_seconds:.1f}s)")
- # Factory function
- def create_state_manager(
- conversation_id: str,
- initial_state: ConversationState = ConversationState.INITIALIZING,
- **kwargs
- ) -> ConversationStateManager:
- """
- Create a ConversationStateManager with default configuration.
-
- Args:
- conversation_id: Conversation identifier
- initial_state: Initial state
- **kwargs: Additional state manager parameters
-
- Returns:
- ConversationStateManager: Configured state manager
- """
- return ConversationStateManager(
- conversation_id=conversation_id,
- initial_state=initial_state,
- **kwargs
- )
- # Module exports
- __all__ = [
- "ConversationState",
- "ConversationStateManager",
- "ConversationStateError",
- "InvalidStateTransitionError",
- "StateTransition",
- "StateTransitionRules",
- "ConversationPhase",
- "create_state_manager"
- ]
|