conversation_state.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  1. """
  2. Conversation State Management and Transitions
  3. This module implements conversation state tracking and state transition management
  4. for the Trixy voice assistant system. It provides a finite state machine approach
  5. to managing conversation phases, transitions, and validation.
  6. Key Features:
  7. - Finite state machine for conversation phases
  8. - State transition validation and rules
  9. - State history tracking and analytics
  10. - Thread-safe state operations
  11. - Integration with conversation sessions
  12. - Support for plugin-driven state changes
  13. - State persistence and restoration
  14. Classes:
  15. - ConversationState: Core state enumeration
  16. - ConversationStateManager: State management and transitions
  17. - StateTransition: Individual state transition record
  18. - StateTransitionRules: Validation rules for transitions
  19. - ConversationPhase: High-level conversation phases
  20. """
  21. import threading
  22. import time
  23. from datetime import datetime, timezone, timedelta
  24. from dataclasses import dataclass, field
  25. from enum import Enum
  26. from typing import Any, Dict, List, Optional, Set, Callable, Tuple
  27. import json
  28. from pathlib import Path
  29. import weakref
  30. def pprint(message: str) -> None:
  31. """Conversation state logging function."""
  32. print(f"[CONVERSATION_STATE] {message}")
  33. class ConversationState(Enum):
  34. """
  35. Core conversation states representing the current phase of a conversation.
  36. These states map to the conversation workflow specified in CLAUDE.md:
  37. 1. Wakeword detection starts conversation session
  38. 2. Raw audio input with conversation_id
  39. 3. Text processing with same conversation_id
  40. 4. Intent handling with conversation tracking
  41. 5. Multi-turn conversations where plugins ask questions and wait for responses
  42. """
  43. # Initialization states
  44. INITIALIZING = "initializing" # Session being created
  45. READY = "ready" # Ready for input
  46. # Input processing states
  47. LISTENING = "listening" # Waiting for or receiving audio input
  48. TRANSCRIBING = "transcribing" # Converting audio to text
  49. PROCESSING_INTENT = "processing_intent" # Extracting intent from text
  50. # Response generation states
  51. GENERATING_RESPONSE = "generating_response" # Creating response
  52. SYNTHESIZING_SPEECH = "synthesizing_speech" # Converting text to speech
  53. PLAYING_RESPONSE = "playing_response" # Playing audio response
  54. # Multi-turn states
  55. WAITING_FOR_USER = "waiting_for_user" # Waiting for user input
  56. WAITING_FOR_PLUGIN = "waiting_for_plugin" # Plugin processing
  57. PLUGIN_INTERACTION = "plugin_interaction" # Plugin asking questions
  58. # Terminal states
  59. COMPLETED = "completed" # Conversation completed successfully
  60. TIMED_OUT = "timed_out" # Conversation timed out
  61. ERROR = "error" # Error occurred
  62. CANCELLED = "cancelled" # Conversation cancelled
  63. ABORTED = "aborted" # Conversation aborted due to error
  64. class ConversationPhase(Enum):
  65. """
  66. High-level conversation phases that group related states.
  67. """
  68. STARTUP = "startup" # Initialization and setup
  69. INPUT = "input" # Receiving and processing input
  70. PROCESSING = "processing" # Intent processing and analysis
  71. OUTPUT = "output" # Response generation and delivery
  72. INTERACTION = "interaction" # Multi-turn interaction
  73. TERMINAL = "terminal" # End states
  74. # State to phase mapping
  75. STATE_TO_PHASE = {
  76. ConversationState.INITIALIZING: ConversationPhase.STARTUP,
  77. ConversationState.READY: ConversationPhase.STARTUP,
  78. ConversationState.LISTENING: ConversationPhase.INPUT,
  79. ConversationState.TRANSCRIBING: ConversationPhase.INPUT,
  80. ConversationState.PROCESSING_INTENT: ConversationPhase.PROCESSING,
  81. ConversationState.WAITING_FOR_PLUGIN: ConversationPhase.PROCESSING,
  82. ConversationState.PLUGIN_INTERACTION: ConversationPhase.PROCESSING,
  83. ConversationState.GENERATING_RESPONSE: ConversationPhase.OUTPUT,
  84. ConversationState.SYNTHESIZING_SPEECH: ConversationPhase.OUTPUT,
  85. ConversationState.PLAYING_RESPONSE: ConversationPhase.OUTPUT,
  86. ConversationState.WAITING_FOR_USER: ConversationPhase.INTERACTION,
  87. ConversationState.COMPLETED: ConversationPhase.TERMINAL,
  88. ConversationState.TIMED_OUT: ConversationPhase.TERMINAL,
  89. ConversationState.ERROR: ConversationPhase.TERMINAL,
  90. ConversationState.CANCELLED: ConversationPhase.TERMINAL,
  91. ConversationState.ABORTED: ConversationPhase.TERMINAL,
  92. }
  93. @dataclass
  94. class StateTransition:
  95. """
  96. Represents a single state transition in a conversation.
  97. Tracks the complete context of state changes for analytics and debugging.
  98. """
  99. transition_id: str
  100. from_state: ConversationState
  101. to_state: ConversationState
  102. timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
  103. # Transition context
  104. trigger: str = "system" # What triggered the transition
  105. source: str = "state_manager" # Component that initiated transition
  106. duration_ms: float = 0.0 # Time spent in previous state
  107. # Additional data
  108. metadata: Dict[str, Any] = field(default_factory=dict)
  109. error_message: Optional[str] = None
  110. def to_dict(self) -> Dict[str, Any]:
  111. """Convert transition to dictionary representation."""
  112. return {
  113. "transition_id": self.transition_id,
  114. "from_state": self.from_state.value,
  115. "to_state": self.to_state.value,
  116. "timestamp": self.timestamp.isoformat(),
  117. "trigger": self.trigger,
  118. "source": self.source,
  119. "duration_ms": self.duration_ms,
  120. "metadata": self.metadata,
  121. "error_message": self.error_message
  122. }
  123. @classmethod
  124. def from_dict(cls, data: Dict[str, Any]) -> 'StateTransition':
  125. """Create StateTransition from dictionary."""
  126. return cls(
  127. transition_id=data["transition_id"],
  128. from_state=ConversationState(data["from_state"]),
  129. to_state=ConversationState(data["to_state"]),
  130. timestamp=datetime.fromisoformat(data["timestamp"]),
  131. trigger=data.get("trigger", "system"),
  132. source=data.get("source", "state_manager"),
  133. duration_ms=data.get("duration_ms", 0.0),
  134. metadata=data.get("metadata", {}),
  135. error_message=data.get("error_message")
  136. )
  137. class StateTransitionRules:
  138. """
  139. Defines and validates state transition rules for conversations.
  140. This class implements a finite state machine with validation rules
  141. to ensure conversations follow valid state sequences.
  142. """
  143. # Valid state transitions (from_state -> set of valid to_states)
  144. VALID_TRANSITIONS = {
  145. ConversationState.INITIALIZING: {
  146. ConversationState.READY,
  147. ConversationState.ERROR
  148. },
  149. ConversationState.READY: {
  150. ConversationState.LISTENING,
  151. ConversationState.WAITING_FOR_USER,
  152. ConversationState.COMPLETED,
  153. ConversationState.CANCELLED,
  154. ConversationState.TIMED_OUT,
  155. ConversationState.ERROR
  156. },
  157. ConversationState.LISTENING: {
  158. ConversationState.TRANSCRIBING,
  159. ConversationState.READY,
  160. ConversationState.TIMED_OUT,
  161. ConversationState.CANCELLED,
  162. ConversationState.ERROR
  163. },
  164. ConversationState.TRANSCRIBING: {
  165. ConversationState.PROCESSING_INTENT,
  166. ConversationState.LISTENING,
  167. ConversationState.ERROR
  168. },
  169. ConversationState.PROCESSING_INTENT: {
  170. ConversationState.GENERATING_RESPONSE,
  171. ConversationState.WAITING_FOR_PLUGIN,
  172. ConversationState.PLUGIN_INTERACTION,
  173. ConversationState.ERROR
  174. },
  175. ConversationState.WAITING_FOR_PLUGIN: {
  176. ConversationState.PLUGIN_INTERACTION,
  177. ConversationState.GENERATING_RESPONSE,
  178. ConversationState.TIMED_OUT,
  179. ConversationState.ERROR
  180. },
  181. ConversationState.PLUGIN_INTERACTION: {
  182. ConversationState.WAITING_FOR_USER,
  183. ConversationState.GENERATING_RESPONSE,
  184. ConversationState.LISTENING,
  185. ConversationState.COMPLETED,
  186. ConversationState.ERROR
  187. },
  188. ConversationState.GENERATING_RESPONSE: {
  189. ConversationState.SYNTHESIZING_SPEECH,
  190. ConversationState.COMPLETED,
  191. ConversationState.ERROR
  192. },
  193. ConversationState.SYNTHESIZING_SPEECH: {
  194. ConversationState.PLAYING_RESPONSE,
  195. ConversationState.ERROR
  196. },
  197. ConversationState.PLAYING_RESPONSE: {
  198. ConversationState.WAITING_FOR_USER,
  199. ConversationState.COMPLETED,
  200. ConversationState.ERROR
  201. },
  202. ConversationState.WAITING_FOR_USER: {
  203. ConversationState.LISTENING,
  204. ConversationState.PLUGIN_INTERACTION,
  205. ConversationState.COMPLETED,
  206. ConversationState.TIMED_OUT,
  207. ConversationState.CANCELLED,
  208. ConversationState.ERROR
  209. },
  210. # Terminal states (no transitions allowed except to error)
  211. ConversationState.COMPLETED: {ConversationState.ERROR},
  212. ConversationState.TIMED_OUT: {ConversationState.ERROR},
  213. ConversationState.CANCELLED: {ConversationState.ERROR},
  214. ConversationState.ABORTED: {ConversationState.ERROR},
  215. ConversationState.ERROR: set() # No transitions from error
  216. }
  217. @classmethod
  218. def is_valid_transition(
  219. cls,
  220. from_state: ConversationState,
  221. to_state: ConversationState
  222. ) -> bool:
  223. """
  224. Check if a state transition is valid.
  225. Args:
  226. from_state: Current state
  227. to_state: Desired next state
  228. Returns:
  229. True if transition is valid
  230. """
  231. if from_state not in cls.VALID_TRANSITIONS:
  232. return False
  233. return to_state in cls.VALID_TRANSITIONS[from_state]
  234. @classmethod
  235. def get_valid_next_states(
  236. cls,
  237. current_state: ConversationState
  238. ) -> Set[ConversationState]:
  239. """
  240. Get all valid next states for a given current state.
  241. Args:
  242. current_state: Current conversation state
  243. Returns:
  244. Set of valid next states
  245. """
  246. return cls.VALID_TRANSITIONS.get(current_state, set())
  247. @classmethod
  248. def is_terminal_state(cls, state: ConversationState) -> bool:
  249. """
  250. Check if a state is terminal (conversation ending).
  251. Args:
  252. state: State to check
  253. Returns:
  254. True if state is terminal
  255. """
  256. return STATE_TO_PHASE.get(state) == ConversationPhase.TERMINAL
  257. @classmethod
  258. def get_phase(cls, state: ConversationState) -> ConversationPhase:
  259. """
  260. Get the conversation phase for a given state.
  261. Args:
  262. state: Conversation state
  263. Returns:
  264. Conversation phase
  265. """
  266. return STATE_TO_PHASE.get(state, ConversationPhase.TERMINAL)
  267. class ConversationStateError(Exception):
  268. """Base exception for conversation state errors."""
  269. pass
  270. class InvalidStateTransitionError(ConversationStateError):
  271. """Raised when an invalid state transition is attempted."""
  272. pass
  273. class ConversationStateManager:
  274. """
  275. Manages conversation state transitions and validation.
  276. This class implements a finite state machine for conversation management,
  277. ensuring valid state transitions and providing comprehensive state tracking
  278. and analytics.
  279. Features:
  280. - Finite state machine with validation
  281. - State history tracking
  282. - Thread-safe state operations
  283. - State change notifications
  284. - Integration with conversation sessions
  285. - State persistence and restoration
  286. - Performance analytics
  287. """
  288. def __init__(
  289. self,
  290. conversation_id: str,
  291. initial_state: ConversationState = ConversationState.INITIALIZING,
  292. enable_validation: bool = True,
  293. max_history_size: int = 100,
  294. state_timeout_seconds: float = 300.0,
  295. application_ref: Optional[weakref.ref] = None
  296. ):
  297. """
  298. Initialize conversation state manager.
  299. Args:
  300. conversation_id: Conversation identifier
  301. initial_state: Initial conversation state
  302. enable_validation: Enable state transition validation
  303. max_history_size: Maximum state history entries
  304. state_timeout_seconds: Timeout for state changes
  305. application_ref: Weak reference to application container
  306. """
  307. # Core properties
  308. self._conversation_id = conversation_id
  309. self._current_state = initial_state
  310. self._previous_state: Optional[ConversationState] = None
  311. self._enable_validation = enable_validation
  312. self._state_timeout = state_timeout_seconds
  313. # Timing
  314. self._state_start_time = datetime.now(timezone.utc)
  315. self._last_transition_time = self._state_start_time
  316. self._creation_time = self._state_start_time
  317. # History and tracking
  318. self._transition_history: List[StateTransition] = []
  319. self._max_history_size = max_history_size
  320. # State change listeners
  321. self._state_listeners: List[Callable[[ConversationState, ConversationState], None]] = []
  322. # Thread safety
  323. self._lock = threading.RLock()
  324. # Application integration
  325. self._application_ref = application_ref
  326. # Transition counter for unique IDs
  327. self._transition_counter = 0
  328. # Performance tracking
  329. self._total_transitions = 0
  330. self._error_count = 0
  331. self._timeout_count = 0
  332. pprint(f"State manager initialized for conversation {conversation_id} in state {initial_state.value}")
  333. # Properties
  334. @property
  335. def conversation_id(self) -> str:
  336. """Get the conversation ID."""
  337. return self._conversation_id
  338. @property
  339. def current_state(self) -> ConversationState:
  340. """Get the current conversation state."""
  341. with self._lock:
  342. return self._current_state
  343. @property
  344. def previous_state(self) -> Optional[ConversationState]:
  345. """Get the previous conversation state."""
  346. with self._lock:
  347. return self._previous_state
  348. @property
  349. def current_phase(self) -> ConversationPhase:
  350. """Get the current conversation phase."""
  351. return StateTransitionRules.get_phase(self.current_state)
  352. @property
  353. def is_terminal(self) -> bool:
  354. """Check if current state is terminal."""
  355. return StateTransitionRules.is_terminal_state(self.current_state)
  356. @property
  357. def state_duration_seconds(self) -> float:
  358. """Get duration in current state in seconds."""
  359. with self._lock:
  360. return (datetime.now(timezone.utc) - self._state_start_time).total_seconds()
  361. @property
  362. def total_duration_seconds(self) -> float:
  363. """Get total conversation duration in seconds."""
  364. with self._lock:
  365. return (datetime.now(timezone.utc) - self._creation_time).total_seconds()
  366. @property
  367. def transition_count(self) -> int:
  368. """Get number of state transitions."""
  369. with self._lock:
  370. return len(self._transition_history)
  371. # State Management
  372. def transition_to(
  373. self,
  374. new_state: ConversationState,
  375. trigger: str = "system",
  376. source: str = "state_manager",
  377. metadata: Optional[Dict[str, Any]] = None,
  378. force: bool = False
  379. ) -> bool:
  380. """
  381. Transition to a new conversation state.
  382. Args:
  383. new_state: Target state
  384. trigger: What triggered the transition
  385. source: Component that initiated the transition
  386. metadata: Additional transition metadata
  387. force: Force transition even if validation fails
  388. Returns:
  389. True if transition was successful
  390. Raises:
  391. InvalidStateTransitionError: If transition is not valid and not forced
  392. """
  393. with self._lock:
  394. current_state = self._current_state
  395. # Check if already in target state
  396. if current_state == new_state:
  397. return True
  398. # Validate transition
  399. if self._enable_validation and not force:
  400. if not StateTransitionRules.is_valid_transition(current_state, new_state):
  401. error_msg = f"Invalid transition from {current_state.value} to {new_state.value}"
  402. raise InvalidStateTransitionError(error_msg)
  403. # Calculate duration in previous state
  404. now = datetime.now(timezone.utc)
  405. duration_ms = (now - self._state_start_time).total_seconds() * 1000
  406. # Create transition record
  407. self._transition_counter += 1
  408. transition = StateTransition(
  409. transition_id=f"{self._conversation_id}_{self._transition_counter:06d}",
  410. from_state=current_state,
  411. to_state=new_state,
  412. timestamp=now,
  413. trigger=trigger,
  414. source=source,
  415. duration_ms=duration_ms,
  416. metadata=metadata or {}
  417. )
  418. # Update state
  419. self._previous_state = current_state
  420. self._current_state = new_state
  421. self._state_start_time = now
  422. self._last_transition_time = now
  423. self._total_transitions += 1
  424. # Add to history
  425. self._transition_history.append(transition)
  426. # Trim history if needed
  427. if len(self._transition_history) > self._max_history_size:
  428. self._transition_history.pop(0)
  429. # Notify listeners
  430. self._notify_state_change(current_state, new_state)
  431. pprint(f"Conversation {self._conversation_id} transitioned: {current_state.value} -> {new_state.value}")
  432. # Trigger event if application is available
  433. self._trigger_state_change_event(transition)
  434. return True
  435. def can_transition_to(self, target_state: ConversationState) -> bool:
  436. """
  437. Check if transition to target state is valid.
  438. Args:
  439. target_state: Target state to check
  440. Returns:
  441. True if transition is valid
  442. """
  443. with self._lock:
  444. if not self._enable_validation:
  445. return True
  446. return StateTransitionRules.is_valid_transition(self._current_state, target_state)
  447. def get_valid_next_states(self) -> Set[ConversationState]:
  448. """
  449. Get all valid next states from current state.
  450. Returns:
  451. Set of valid next states
  452. """
  453. with self._lock:
  454. return StateTransitionRules.get_valid_next_states(self._current_state)
  455. def reset_to_state(
  456. self,
  457. state: ConversationState,
  458. reason: str = "reset"
  459. ) -> None:
  460. """
  461. Reset state manager to a specific state (bypass validation).
  462. Args:
  463. state: State to reset to
  464. reason: Reason for reset
  465. """
  466. self.transition_to(
  467. state,
  468. trigger=f"reset: {reason}",
  469. source="state_manager",
  470. force=True
  471. )
  472. # State Monitoring
  473. def is_state_timed_out(self) -> bool:
  474. """
  475. Check if current state has timed out.
  476. Returns:
  477. True if state has exceeded timeout
  478. """
  479. return self.state_duration_seconds > self._state_timeout
  480. def check_timeout(self) -> bool:
  481. """
  482. Check for timeout and transition to TIMED_OUT state if needed.
  483. Returns:
  484. True if timeout occurred and state was changed
  485. """
  486. if self.is_state_timed_out() and not self.is_terminal:
  487. self._timeout_count += 1
  488. self.transition_to(
  489. ConversationState.TIMED_OUT,
  490. trigger="timeout",
  491. source="state_manager",
  492. metadata={"timeout_seconds": self._state_timeout}
  493. )
  494. return True
  495. return False
  496. def handle_error(self, error_message: str, source: str = "unknown") -> None:
  497. """
  498. Handle an error and transition to ERROR state.
  499. Args:
  500. error_message: Error description
  501. source: Source of the error
  502. """
  503. self._error_count += 1
  504. self.transition_to(
  505. ConversationState.ERROR,
  506. trigger="error",
  507. source=source,
  508. metadata={"error_message": error_message},
  509. force=True # Errors can transition from any state
  510. )
  511. # State Listeners
  512. def add_state_listener(
  513. self,
  514. listener: Callable[[ConversationState, ConversationState], None]
  515. ) -> None:
  516. """
  517. Add a state change listener.
  518. Args:
  519. listener: Function called on state changes (from_state, to_state)
  520. """
  521. with self._lock:
  522. if listener not in self._state_listeners:
  523. self._state_listeners.append(listener)
  524. def remove_state_listener(
  525. self,
  526. listener: Callable[[ConversationState, ConversationState], None]
  527. ) -> None:
  528. """
  529. Remove a state change listener.
  530. Args:
  531. listener: Listener function to remove
  532. """
  533. with self._lock:
  534. if listener in self._state_listeners:
  535. self._state_listeners.remove(listener)
  536. def _notify_state_change(
  537. self,
  538. from_state: ConversationState,
  539. to_state: ConversationState
  540. ) -> None:
  541. """Notify all state change listeners."""
  542. for listener in self._state_listeners:
  543. try:
  544. listener(from_state, to_state)
  545. except Exception as e:
  546. pprint(f"Error in state change listener: {e}")
  547. # History and Analytics
  548. def get_transition_history(
  549. self,
  550. limit: Optional[int] = None,
  551. since: Optional[datetime] = None
  552. ) -> List[StateTransition]:
  553. """
  554. Get state transition history.
  555. Args:
  556. limit: Maximum number of transitions to return
  557. since: Only return transitions after this time
  558. Returns:
  559. List of state transitions
  560. """
  561. with self._lock:
  562. history = self._transition_history
  563. # Filter by time
  564. if since:
  565. history = [t for t in history if t.timestamp >= since]
  566. # Apply limit
  567. if limit:
  568. history = history[-limit:]
  569. return history.copy()
  570. def get_state_durations(self) -> Dict[ConversationState, float]:
  571. """
  572. Get total time spent in each state.
  573. Returns:
  574. Dictionary mapping states to total duration in seconds
  575. """
  576. with self._lock:
  577. durations = {}
  578. for transition in self._transition_history:
  579. state = transition.from_state
  580. duration_seconds = transition.duration_ms / 1000.0
  581. durations[state] = durations.get(state, 0.0) + duration_seconds
  582. # Add current state duration
  583. current_duration = self.state_duration_seconds
  584. durations[self._current_state] = durations.get(self._current_state, 0.0) + current_duration
  585. return durations
  586. def get_analytics(self) -> Dict[str, Any]:
  587. """
  588. Get comprehensive state analytics.
  589. Returns:
  590. Dictionary containing state analytics
  591. """
  592. with self._lock:
  593. durations = self.get_state_durations()
  594. total_duration = self.total_duration_seconds
  595. # Calculate state statistics
  596. state_stats = {}
  597. for state, duration in durations.items():
  598. percentage = (duration / max(total_duration, 1.0)) * 100
  599. transition_count = len([t for t in self._transition_history if t.from_state == state])
  600. state_stats[state.value] = {
  601. "total_duration_seconds": duration,
  602. "percentage_of_total": round(percentage, 2),
  603. "transition_count": transition_count,
  604. "average_duration_seconds": duration / max(transition_count, 1)
  605. }
  606. # Calculate phase statistics
  607. phase_stats = {}
  608. for phase in ConversationPhase:
  609. phase_states = [s for s in ConversationState if STATE_TO_PHASE.get(s) == phase]
  610. phase_duration = sum(durations.get(s, 0.0) for s in phase_states)
  611. phase_percentage = (phase_duration / max(total_duration, 1.0)) * 100
  612. phase_stats[phase.value] = {
  613. "total_duration_seconds": phase_duration,
  614. "percentage_of_total": round(phase_percentage, 2),
  615. "states": [s.value for s in phase_states if s in durations]
  616. }
  617. return {
  618. "conversation_id": self._conversation_id,
  619. "current_state": self._current_state.value,
  620. "current_phase": self.current_phase.value,
  621. "is_terminal": self.is_terminal,
  622. "total_duration_seconds": total_duration,
  623. "current_state_duration_seconds": self.state_duration_seconds,
  624. "total_transitions": self._total_transitions,
  625. "error_count": self._error_count,
  626. "timeout_count": self._timeout_count,
  627. "state_statistics": state_stats,
  628. "phase_statistics": phase_stats,
  629. "creation_time": self._creation_time.isoformat(),
  630. "last_transition_time": self._last_transition_time.isoformat()
  631. }
  632. # Integration
  633. def _trigger_state_change_event(self, transition: StateTransition) -> None:
  634. """Trigger state change event if application is available."""
  635. try:
  636. if self._application_ref and self._application_ref():
  637. app = self._application_ref()
  638. event_handler = app.get_event_handler()
  639. # Create state change event data
  640. from ..events import EventDataFactory
  641. state_data = EventDataFactory.create_event_data(
  642. "conversation_state_changed",
  643. conversation_id=self._conversation_id,
  644. from_state=transition.from_state.value,
  645. to_state=transition.to_state.value,
  646. trigger=transition.trigger,
  647. source=transition.source,
  648. duration_ms=transition.duration_ms,
  649. metadata=transition.metadata
  650. )
  651. event_handler.trigger_event("conversation_state_changed", state_data)
  652. except Exception as e:
  653. pprint(f"Error triggering state change event: {e}")
  654. # Persistence
  655. def to_dict(self) -> Dict[str, Any]:
  656. """
  657. Convert state manager to dictionary representation.
  658. Returns:
  659. Dictionary containing state manager data
  660. """
  661. with self._lock:
  662. return {
  663. "conversation_id": self._conversation_id,
  664. "current_state": self._current_state.value,
  665. "previous_state": self._previous_state.value if self._previous_state else None,
  666. "enable_validation": self._enable_validation,
  667. "state_timeout": self._state_timeout,
  668. "creation_time": self._creation_time.isoformat(),
  669. "state_start_time": self._state_start_time.isoformat(),
  670. "last_transition_time": self._last_transition_time.isoformat(),
  671. "transition_history": [t.to_dict() for t in self._transition_history],
  672. "total_transitions": self._total_transitions,
  673. "error_count": self._error_count,
  674. "timeout_count": self._timeout_count,
  675. "analytics": self.get_analytics()
  676. }
  677. @classmethod
  678. def from_dict(
  679. cls,
  680. data: Dict[str, Any],
  681. application_ref: Optional[weakref.ref] = None
  682. ) -> 'ConversationStateManager':
  683. """
  684. Create ConversationStateManager from dictionary.
  685. Args:
  686. data: Dictionary data
  687. application_ref: Application reference
  688. Returns:
  689. ConversationStateManager: Restored state manager
  690. """
  691. # Create state manager
  692. state_manager = cls(
  693. conversation_id=data["conversation_id"],
  694. initial_state=ConversationState(data["current_state"]),
  695. enable_validation=data.get("enable_validation", True),
  696. state_timeout_seconds=data.get("state_timeout", 300.0),
  697. application_ref=application_ref
  698. )
  699. # Restore state
  700. if data.get("previous_state"):
  701. state_manager._previous_state = ConversationState(data["previous_state"])
  702. state_manager._creation_time = datetime.fromisoformat(data["creation_time"])
  703. state_manager._state_start_time = datetime.fromisoformat(data["state_start_time"])
  704. state_manager._last_transition_time = datetime.fromisoformat(data["last_transition_time"])
  705. state_manager._total_transitions = data.get("total_transitions", 0)
  706. state_manager._error_count = data.get("error_count", 0)
  707. state_manager._timeout_count = data.get("timeout_count", 0)
  708. # Restore transition history
  709. for transition_data in data.get("transition_history", []):
  710. transition = StateTransition.from_dict(transition_data)
  711. state_manager._transition_history.append(transition)
  712. return state_manager
  713. def save_to_file(self, file_path: Path) -> None:
  714. """
  715. Save state manager to JSON file.
  716. Args:
  717. file_path: Path to save the file
  718. """
  719. try:
  720. with open(file_path, 'w', encoding='utf-8') as f:
  721. json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
  722. pprint(f"State manager saved to {file_path}")
  723. except Exception as e:
  724. raise ConversationStateError(f"Failed to save state manager: {e}")
  725. @classmethod
  726. def load_from_file(
  727. cls,
  728. file_path: Path,
  729. application_ref: Optional[weakref.ref] = None
  730. ) -> 'ConversationStateManager':
  731. """
  732. Load state manager from JSON file.
  733. Args:
  734. file_path: Path to load from
  735. application_ref: Application reference
  736. Returns:
  737. ConversationStateManager: Loaded state manager
  738. """
  739. try:
  740. with open(file_path, 'r', encoding='utf-8') as f:
  741. data = json.load(f)
  742. state_manager = cls.from_dict(data, application_ref)
  743. pprint(f"State manager loaded from {file_path}")
  744. return state_manager
  745. except Exception as e:
  746. raise ConversationStateError(f"Failed to load state manager: {e}")
  747. def __str__(self) -> str:
  748. """String representation of the state manager."""
  749. return (f"ConversationStateManager(conversation={self._conversation_id}, "
  750. f"state={self._current_state.value}, transitions={self.transition_count})")
  751. def __repr__(self) -> str:
  752. """Detailed representation of the state manager."""
  753. return (f"ConversationStateManager(conversation_id='{self._conversation_id}', "
  754. f"current_state={self._current_state.value}, "
  755. f"previous_state={self._previous_state.value if self._previous_state else None}, "
  756. f"transitions={self.transition_count}, "
  757. f"duration={self.total_duration_seconds:.1f}s)")
  758. # Factory function
  759. def create_state_manager(
  760. conversation_id: str,
  761. initial_state: ConversationState = ConversationState.INITIALIZING,
  762. **kwargs
  763. ) -> ConversationStateManager:
  764. """
  765. Create a ConversationStateManager with default configuration.
  766. Args:
  767. conversation_id: Conversation identifier
  768. initial_state: Initial state
  769. **kwargs: Additional state manager parameters
  770. Returns:
  771. ConversationStateManager: Configured state manager
  772. """
  773. return ConversationStateManager(
  774. conversation_id=conversation_id,
  775. initial_state=initial_state,
  776. **kwargs
  777. )
  778. # Module exports
  779. __all__ = [
  780. "ConversationState",
  781. "ConversationStateManager",
  782. "ConversationStateError",
  783. "InvalidStateTransitionError",
  784. "StateTransition",
  785. "StateTransitionRules",
  786. "ConversationPhase",
  787. "create_state_manager"
  788. ]