conversation_workflow.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. """
  2. Complete Conversation Workflow Integration for Trixy ML System.
  3. This module implements the complete conversation workflow from wakeword detection
  4. to response generation, coordinating all ML components and integrating with the
  5. main application to provide seamless voice interaction capabilities.
  6. Workflow Steps:
  7. 1. Continuous wakeword listening
  8. 2. Wakeword detection and speaker identification
  9. 3. Audio recording and voice activity detection
  10. 4. Voice recognition and speaker verification
  11. 5. Audio processing and network transmission
  12. 6. Response handling and audio playback
  13. 7. Return to listening state
  14. Key Features:
  15. - Complete end-to-end conversation flow
  16. - State machine-based workflow management
  17. - Audio buffer management and optimization
  18. - Real-time processing and low-latency response
  19. - Error handling and recovery mechanisms
  20. - Performance monitoring and analytics
  21. - Integration with application container
  22. - Configurable workflow parameters
  23. Usage:
  24. from trixy_core.ml import ConversationWorkflow, WorkflowConfig
  25. # Create workflow configuration
  26. config = WorkflowConfig(
  27. wakeword_sensitivity=0.7,
  28. max_recording_duration=30.0,
  29. response_timeout=10.0
  30. )
  31. # Initialize workflow
  32. workflow = ConversationWorkflow(config, ml_manager, app_container)
  33. # Start conversation processing
  34. await workflow.start()
  35. # Feed audio data
  36. await workflow.process_audio(audio_data)
  37. """
  38. import asyncio
  39. import logging
  40. import time
  41. import uuid
  42. from dataclasses import dataclass, field
  43. from datetime import datetime, timedelta
  44. from enum import Enum
  45. from typing import Any, Dict, List, Optional, Callable, Union, Tuple
  46. import numpy as np
  47. from .performance_monitor import PerformanceMonitor, MetricType
  48. class ConversationState(Enum):
  49. """Conversation workflow states."""
  50. IDLE = "idle"
  51. LISTENING = "listening"
  52. WAKEWORD_DETECTED = "wakeword_detected"
  53. RECORDING = "recording"
  54. PROCESSING = "processing"
  55. WAITING_RESPONSE = "waiting_response"
  56. PLAYING_RESPONSE = "playing_response"
  57. ERROR = "error"
  58. TIMEOUT = "timeout"
  59. class WorkflowEvent(Enum):
  60. """Workflow events."""
  61. START_LISTENING = "start_listening"
  62. WAKEWORD_DETECTED = "wakeword_detected"
  63. START_RECORDING = "start_recording"
  64. VOICE_ACTIVITY_DETECTED = "voice_activity_detected"
  65. SILENCE_DETECTED = "silence_detected"
  66. RECORDING_COMPLETE = "recording_complete"
  67. PROCESSING_COMPLETE = "processing_complete"
  68. RESPONSE_RECEIVED = "response_received"
  69. RESPONSE_COMPLETE = "response_complete"
  70. TIMEOUT_OCCURRED = "timeout_occurred"
  71. ERROR_OCCURRED = "error_occurred"
  72. RESET = "reset"
  73. @dataclass
  74. class WorkflowConfig:
  75. """Configuration for conversation workflow."""
  76. # Wakeword detection settings
  77. wakeword_sensitivity: float = 0.7
  78. wakeword_cooldown_seconds: float = 2.0
  79. continuous_listening: bool = True
  80. # Recording settings
  81. max_recording_duration: float = 30.0
  82. silence_timeout: float = 2.0
  83. min_recording_duration: float = 0.5
  84. voice_activity_threshold: float = 0.01
  85. # Processing settings
  86. processing_timeout: float = 15.0
  87. response_timeout: float = 10.0
  88. enable_speaker_verification: bool = True
  89. enable_noise_reduction: bool = True
  90. # Audio settings
  91. sample_rate: int = 16000
  92. chunk_size: int = 1024
  93. audio_format: str = "int16"
  94. # Network settings
  95. server_timeout: float = 30.0
  96. max_retries: int = 3
  97. retry_delay: float = 1.0
  98. # Performance settings
  99. enable_performance_monitoring: bool = True
  100. log_conversation_metrics: bool = True
  101. enable_audio_caching: bool = False
  102. def validate(self) -> bool:
  103. """Validate configuration."""
  104. if not (0.0 <= self.wakeword_sensitivity <= 1.0):
  105. return False
  106. if self.max_recording_duration <= 0:
  107. return False
  108. if self.silence_timeout <= 0:
  109. return False
  110. if self.processing_timeout <= 0:
  111. return False
  112. return True
  113. @dataclass
  114. class ConversationSession:
  115. """Represents an active conversation session."""
  116. session_id: str
  117. start_time: datetime
  118. speaker_id: Optional[str] = None
  119. speaker_name: Optional[str] = None
  120. wakeword_confidence: float = 0.0
  121. audio_buffer: List[np.ndarray] = field(default_factory=list)
  122. processing_start_time: Optional[datetime] = None
  123. response_start_time: Optional[datetime] = None
  124. state: ConversationState = ConversationState.IDLE
  125. metadata: Dict[str, Any] = field(default_factory=dict)
  126. @property
  127. def duration(self) -> float:
  128. """Get session duration in seconds."""
  129. return (datetime.now() - self.start_time).total_seconds()
  130. @property
  131. def audio_duration(self) -> float:
  132. """Get total audio duration in seconds."""
  133. if not self.audio_buffer:
  134. return 0.0
  135. total_samples = sum(len(chunk) for chunk in self.audio_buffer)
  136. return total_samples / 16000 # Assuming 16kHz sample rate
  137. class ConversationWorkflowError(Exception):
  138. """Base exception for conversation workflow errors."""
  139. pass
  140. class ConversationWorkflow:
  141. """
  142. Complete conversation workflow manager.
  143. Coordinates the entire conversation flow from wakeword detection to response,
  144. managing state transitions, audio processing, and integration with ML components.
  145. """
  146. def __init__(
  147. self,
  148. config: WorkflowConfig,
  149. ml_manager: Any,
  150. app_container: Optional[Any] = None,
  151. performance_monitor: Optional[PerformanceMonitor] = None,
  152. logger: Optional[logging.Logger] = None
  153. ):
  154. """
  155. Initialize conversation workflow.
  156. Args:
  157. config: Workflow configuration
  158. ml_manager: ML manager instance
  159. app_container: Application container
  160. performance_monitor: Performance monitor instance
  161. logger: Logger instance
  162. """
  163. self.config = config
  164. self.ml_manager = ml_manager
  165. self.app_container = app_container
  166. self.performance_monitor = performance_monitor
  167. self.logger = logger or logging.getLogger(__name__)
  168. if not self.config.validate():
  169. raise ConversationWorkflowError("Invalid workflow configuration")
  170. # State management
  171. self._current_state = ConversationState.IDLE
  172. self._previous_state = ConversationState.IDLE
  173. self._state_start_time = time.time()
  174. self._running = False
  175. # Session management
  176. self._current_session: Optional[ConversationSession] = None
  177. self._session_history: List[ConversationSession] = []
  178. self._max_history_size = 100
  179. # Audio processing
  180. self._audio_buffer = []
  181. self._recording_start_time = 0.0
  182. self._last_voice_activity = 0.0
  183. self._silence_start_time = 0.0
  184. # Performance tracking
  185. self._workflow_stats = {
  186. "total_conversations": 0,
  187. "successful_conversations": 0,
  188. "failed_conversations": 0,
  189. "average_response_time": 0.0,
  190. "total_processing_time": 0.0,
  191. "wakeword_detections": 0,
  192. "false_wakewords": 0
  193. }
  194. # Event handlers
  195. self._event_handlers: Dict[WorkflowEvent, List[Callable]] = {}
  196. self._state_transition_handlers: Dict[Tuple[ConversationState, ConversationState], List[Callable]] = {}
  197. # Setup state machine
  198. self._setup_state_machine()
  199. self.logger.info("Conversation workflow initialized")
  200. def _setup_state_machine(self) -> None:
  201. """Setup state machine transitions and handlers."""
  202. # Define valid state transitions
  203. self._valid_transitions = {
  204. ConversationState.IDLE: [ConversationState.LISTENING, ConversationState.ERROR],
  205. ConversationState.LISTENING: [ConversationState.WAKEWORD_DETECTED, ConversationState.IDLE, ConversationState.ERROR],
  206. ConversationState.WAKEWORD_DETECTED: [ConversationState.RECORDING, ConversationState.LISTENING, ConversationState.ERROR],
  207. ConversationState.RECORDING: [ConversationState.PROCESSING, ConversationState.LISTENING, ConversationState.TIMEOUT, ConversationState.ERROR],
  208. ConversationState.PROCESSING: [ConversationState.WAITING_RESPONSE, ConversationState.LISTENING, ConversationState.TIMEOUT, ConversationState.ERROR],
  209. ConversationState.WAITING_RESPONSE: [ConversationState.PLAYING_RESPONSE, ConversationState.LISTENING, ConversationState.TIMEOUT, ConversationState.ERROR],
  210. ConversationState.PLAYING_RESPONSE: [ConversationState.LISTENING, ConversationState.IDLE, ConversationState.ERROR],
  211. ConversationState.ERROR: [ConversationState.IDLE, ConversationState.LISTENING],
  212. ConversationState.TIMEOUT: [ConversationState.IDLE, ConversationState.LISTENING]
  213. }
  214. # Register default event handlers
  215. self._register_default_handlers()
  216. def _register_default_handlers(self) -> None:
  217. """Register default event handlers."""
  218. # Wakeword detection handler
  219. async def handle_wakeword_detected(event_data: Dict[str, Any]) -> None:
  220. await self._transition_to_state(ConversationState.WAKEWORD_DETECTED)
  221. await self._start_new_session(event_data)
  222. self.register_event_handler(WorkflowEvent.WAKEWORD_DETECTED, handle_wakeword_detected)
  223. # Recording complete handler
  224. async def handle_recording_complete(event_data: Dict[str, Any]) -> None:
  225. await self._transition_to_state(ConversationState.PROCESSING)
  226. await self._process_recorded_audio()
  227. self.register_event_handler(WorkflowEvent.RECORDING_COMPLETE, handle_recording_complete)
  228. # Response received handler
  229. async def handle_response_received(event_data: Dict[str, Any]) -> None:
  230. await self._transition_to_state(ConversationState.PLAYING_RESPONSE)
  231. await self._play_response(event_data)
  232. self.register_event_handler(WorkflowEvent.RESPONSE_RECEIVED, handle_response_received)
  233. # Error handler
  234. async def handle_error(event_data: Dict[str, Any]) -> None:
  235. await self._handle_workflow_error(event_data)
  236. self.register_event_handler(WorkflowEvent.ERROR_OCCURRED, handle_error)
  237. # Timeout handler
  238. async def handle_timeout(event_data: Dict[str, Any]) -> None:
  239. await self._handle_workflow_timeout(event_data)
  240. self.register_event_handler(WorkflowEvent.TIMEOUT_OCCURRED, handle_timeout)
  241. async def start(self) -> None:
  242. """Start the conversation workflow."""
  243. if self._running:
  244. self.logger.warning("Conversation workflow is already running")
  245. return
  246. try:
  247. self.logger.info("Starting conversation workflow...")
  248. # Initialize ML manager if not already done
  249. if not self.ml_manager.is_running():
  250. await self.ml_manager.start()
  251. # Start performance monitoring
  252. if self.performance_monitor and self.config.enable_performance_monitoring:
  253. await self.performance_monitor.start_monitoring()
  254. # Set initial state
  255. self._running = True
  256. await self._transition_to_state(ConversationState.LISTENING)
  257. # Start continuous processing
  258. asyncio.create_task(self._workflow_loop())
  259. self.logger.info("Conversation workflow started successfully")
  260. except Exception as e:
  261. self.logger.error(f"Error starting conversation workflow: {e}")
  262. await self.stop()
  263. raise ConversationWorkflowError(f"Failed to start workflow: {e}")
  264. async def stop(self) -> None:
  265. """Stop the conversation workflow."""
  266. if not self._running:
  267. return
  268. try:
  269. self.logger.info("Stopping conversation workflow...")
  270. # Complete current session if active
  271. if self._current_session:
  272. await self._complete_session(success=False, reason="workflow_stopped")
  273. # Stop performance monitoring
  274. if self.performance_monitor and self.config.enable_performance_monitoring:
  275. await self.performance_monitor.stop_monitoring()
  276. # Set final state
  277. self._running = False
  278. await self._transition_to_state(ConversationState.IDLE)
  279. self.logger.info("Conversation workflow stopped successfully")
  280. except Exception as e:
  281. self.logger.error(f"Error stopping conversation workflow: {e}")
  282. async def process_audio(self, audio_data: np.ndarray) -> None:
  283. """
  284. Process audio data through the conversation workflow.
  285. Args:
  286. audio_data: Audio data to process
  287. """
  288. if not self._running:
  289. return
  290. # Record performance metrics
  291. if self.performance_monitor:
  292. timer_key = self.performance_monitor.start_timer("workflow", "process_audio")
  293. try:
  294. # Process based on current state
  295. if self._current_state == ConversationState.LISTENING:
  296. await self._process_listening_audio(audio_data)
  297. elif self._current_state == ConversationState.RECORDING:
  298. await self._process_recording_audio(audio_data)
  299. # Check for timeouts
  300. await self._check_timeouts()
  301. except Exception as e:
  302. self.logger.error(f"Error processing audio: {e}")
  303. await self._emit_event(WorkflowEvent.ERROR_OCCURRED, {"error": e})
  304. finally:
  305. if self.performance_monitor:
  306. self.performance_monitor.end_timer(timer_key)
  307. async def _process_listening_audio(self, audio_data: np.ndarray) -> None:
  308. """Process audio in listening state (wakeword detection)."""
  309. # Feed audio to ML manager for wakeword detection
  310. await self.ml_manager.process_audio_chunk(audio_data)
  311. # Note: Wakeword detection results would be handled via ML manager callbacks
  312. # This is a simplified version - actual implementation would integrate with
  313. # wakeword detection callbacks from the ML manager
  314. async def _process_recording_audio(self, audio_data: np.ndarray) -> None:
  315. """Process audio in recording state."""
  316. if not self._current_session:
  317. return
  318. # Add audio to session buffer
  319. self._current_session.audio_buffer.append(audio_data.copy())
  320. # Detect voice activity
  321. has_voice = self._detect_voice_activity(audio_data)
  322. current_time = time.time()
  323. if has_voice:
  324. self._last_voice_activity = current_time
  325. self._silence_start_time = 0.0
  326. await self._emit_event(WorkflowEvent.VOICE_ACTIVITY_DETECTED, {
  327. "session_id": self._current_session.session_id,
  328. "timestamp": current_time
  329. })
  330. else:
  331. if self._last_voice_activity > 0 and self._silence_start_time == 0.0:
  332. self._silence_start_time = current_time
  333. await self._emit_event(WorkflowEvent.SILENCE_DETECTED, {
  334. "session_id": self._current_session.session_id,
  335. "timestamp": current_time
  336. })
  337. # Check recording completion conditions
  338. recording_duration = current_time - self._recording_start_time
  339. silence_duration = (current_time - self._silence_start_time) if self._silence_start_time > 0 else 0
  340. if (recording_duration >= self.config.max_recording_duration or
  341. (silence_duration >= self.config.silence_timeout and
  342. recording_duration >= self.config.min_recording_duration)):
  343. await self._emit_event(WorkflowEvent.RECORDING_COMPLETE, {
  344. "session_id": self._current_session.session_id,
  345. "duration": recording_duration,
  346. "silence_duration": silence_duration
  347. })
  348. def _detect_voice_activity(self, audio_data: np.ndarray) -> bool:
  349. """Simple voice activity detection based on energy."""
  350. if len(audio_data) == 0:
  351. return False
  352. # Calculate RMS energy
  353. rms = np.sqrt(np.mean(audio_data.astype(np.float64) ** 2))
  354. return rms > self.config.voice_activity_threshold
  355. async def _workflow_loop(self) -> None:
  356. """Main workflow processing loop."""
  357. while self._running:
  358. try:
  359. # Check for timeouts
  360. await self._check_timeouts()
  361. # Update performance metrics
  362. if self.performance_monitor and self.config.log_conversation_metrics:
  363. await self._update_performance_metrics()
  364. # Small delay to prevent excessive CPU usage
  365. await asyncio.sleep(0.1)
  366. except Exception as e:
  367. self.logger.error(f"Error in workflow loop: {e}")
  368. await asyncio.sleep(1.0)
  369. async def _check_timeouts(self) -> None:
  370. """Check for workflow timeouts."""
  371. current_time = time.time()
  372. state_duration = current_time - self._state_start_time
  373. # Check state-specific timeouts
  374. if (self._current_state == ConversationState.PROCESSING and
  375. state_duration > self.config.processing_timeout):
  376. await self._emit_event(WorkflowEvent.TIMEOUT_OCCURRED, {
  377. "state": self._current_state.value,
  378. "duration": state_duration,
  379. "timeout_type": "processing"
  380. })
  381. elif (self._current_state == ConversationState.WAITING_RESPONSE and
  382. state_duration > self.config.response_timeout):
  383. await self._emit_event(WorkflowEvent.TIMEOUT_OCCURRED, {
  384. "state": self._current_state.value,
  385. "duration": state_duration,
  386. "timeout_type": "response"
  387. })
  388. async def _start_new_session(self, event_data: Dict[str, Any]) -> None:
  389. """Start a new conversation session."""
  390. # Complete previous session if exists
  391. if self._current_session:
  392. await self._complete_session(success=False, reason="new_session_started")
  393. # Create new session
  394. session_id = str(uuid.uuid4())
  395. self._current_session = ConversationSession(
  396. session_id=session_id,
  397. start_time=datetime.now(),
  398. speaker_id=event_data.get("speaker_id"),
  399. speaker_name=event_data.get("speaker_name"),
  400. wakeword_confidence=event_data.get("confidence", 0.0),
  401. metadata=event_data.copy()
  402. )
  403. # Start recording
  404. await self._transition_to_state(ConversationState.RECORDING)
  405. self._recording_start_time = time.time()
  406. self._last_voice_activity = 0.0
  407. self._silence_start_time = 0.0
  408. # Update stats
  409. self._workflow_stats["total_conversations"] += 1
  410. self._workflow_stats["wakeword_detections"] += 1
  411. self.logger.info(f"Started new conversation session: {session_id}")
  412. async def _process_recorded_audio(self) -> None:
  413. """Process recorded audio from current session."""
  414. if not self._current_session:
  415. return
  416. try:
  417. self._current_session.processing_start_time = datetime.now()
  418. # Combine audio chunks
  419. if not self._current_session.audio_buffer:
  420. await self._complete_session(success=False, reason="no_audio_recorded")
  421. return
  422. combined_audio = np.concatenate(self._current_session.audio_buffer)
  423. # Perform voice recognition and speaker identification
  424. # This would integrate with the voice recognition system
  425. recognition_result = await self._perform_voice_recognition(combined_audio)
  426. if recognition_result.get("success", False):
  427. # Send audio to server for processing
  428. await self._send_audio_to_server(combined_audio, recognition_result)
  429. else:
  430. await self._complete_session(success=False, reason="voice_recognition_failed")
  431. except Exception as e:
  432. self.logger.error(f"Error processing recorded audio: {e}")
  433. await self._complete_session(success=False, reason=f"processing_error: {e}")
  434. async def _perform_voice_recognition(self, audio_data: np.ndarray) -> Dict[str, Any]:
  435. """Perform voice recognition on recorded audio."""
  436. # This would integrate with the voice recognition detector
  437. # For now, return a mock result
  438. return {
  439. "success": True,
  440. "speaker_id": self._current_session.speaker_id or "unknown",
  441. "confidence": 0.8,
  442. "quality_score": 0.9
  443. }
  444. async def _send_audio_to_server(self, audio_data: np.ndarray,
  445. recognition_result: Dict[str, Any]) -> None:
  446. """Send processed audio to server."""
  447. try:
  448. await self._transition_to_state(ConversationState.WAITING_RESPONSE)
  449. self._current_session.response_start_time = datetime.now()
  450. # This would integrate with network client to send audio to server
  451. # For now, simulate server communication
  452. await asyncio.sleep(1.0) # Simulate network delay
  453. # Mock response
  454. response_data = {
  455. "session_id": self._current_session.session_id,
  456. "response_text": "Mock response from server",
  457. "response_audio": None, # Would contain audio data
  458. "confidence": 0.9
  459. }
  460. await self._emit_event(WorkflowEvent.RESPONSE_RECEIVED, response_data)
  461. except Exception as e:
  462. self.logger.error(f"Error sending audio to server: {e}")
  463. await self._complete_session(success=False, reason=f"server_error: {e}")
  464. async def _play_response(self, response_data: Dict[str, Any]) -> None:
  465. """Play response audio."""
  466. try:
  467. # This would integrate with audio playback system
  468. # For now, simulate response playback
  469. await asyncio.sleep(2.0) # Simulate response duration
  470. await self._emit_event(WorkflowEvent.RESPONSE_COMPLETE, response_data)
  471. await self._complete_session(success=True, reason="response_completed")
  472. except Exception as e:
  473. self.logger.error(f"Error playing response: {e}")
  474. await self._complete_session(success=False, reason=f"playback_error: {e}")
  475. async def _complete_session(self, success: bool, reason: str) -> None:
  476. """Complete the current conversation session."""
  477. if not self._current_session:
  478. return
  479. try:
  480. # Update session metadata
  481. self._current_session.metadata.update({
  482. "success": success,
  483. "completion_reason": reason,
  484. "end_time": datetime.now(),
  485. "total_duration": self._current_session.duration,
  486. "audio_duration": self._current_session.audio_duration
  487. })
  488. # Update statistics
  489. if success:
  490. self._workflow_stats["successful_conversations"] += 1
  491. else:
  492. self._workflow_stats["failed_conversations"] += 1
  493. # Record performance metrics
  494. if self.performance_monitor:
  495. self.performance_monitor.record_metric(
  496. "workflow", MetricType.PROCESSING_TIME,
  497. self._current_session.duration * 1000, "milliseconds"
  498. )
  499. # Store in history
  500. self._session_history.append(self._current_session)
  501. if len(self._session_history) > self._max_history_size:
  502. self._session_history.pop(0)
  503. self.logger.info(f"Completed conversation session: {self._current_session.session_id} "
  504. f"(success={success}, reason={reason})")
  505. # Return to listening state
  506. self._current_session = None
  507. if self.config.continuous_listening:
  508. await self._transition_to_state(ConversationState.LISTENING)
  509. else:
  510. await self._transition_to_state(ConversationState.IDLE)
  511. except Exception as e:
  512. self.logger.error(f"Error completing session: {e}")
  513. async def _handle_workflow_error(self, event_data: Dict[str, Any]) -> None:
  514. """Handle workflow errors."""
  515. error = event_data.get("error")
  516. self.logger.error(f"Workflow error: {error}")
  517. # Complete current session if active
  518. if self._current_session:
  519. await self._complete_session(success=False, reason=f"error: {error}")
  520. # Return to appropriate state
  521. if self.config.continuous_listening:
  522. await self._transition_to_state(ConversationState.LISTENING)
  523. else:
  524. await self._transition_to_state(ConversationState.IDLE)
  525. async def _handle_workflow_timeout(self, event_data: Dict[str, Any]) -> None:
  526. """Handle workflow timeouts."""
  527. timeout_type = event_data.get("timeout_type", "unknown")
  528. self.logger.warning(f"Workflow timeout: {timeout_type}")
  529. # Complete current session if active
  530. if self._current_session:
  531. await self._complete_session(success=False, reason=f"timeout: {timeout_type}")
  532. # Return to listening state
  533. if self.config.continuous_listening:
  534. await self._transition_to_state(ConversationState.LISTENING)
  535. else:
  536. await self._transition_to_state(ConversationState.IDLE)
  537. async def _transition_to_state(self, new_state: ConversationState) -> None:
  538. """Transition to a new state."""
  539. if new_state == self._current_state:
  540. return
  541. # Validate transition
  542. if new_state not in self._valid_transitions.get(self._current_state, []):
  543. self.logger.warning(f"Invalid state transition from {self._current_state.value} to {new_state.value}")
  544. return
  545. old_state = self._current_state
  546. self._previous_state = old_state
  547. self._current_state = new_state
  548. self._state_start_time = time.time()
  549. self.logger.debug(f"State transition: {old_state.value} -> {new_state.value}")
  550. # Call state transition handlers
  551. transition_key = (old_state, new_state)
  552. if transition_key in self._state_transition_handlers:
  553. for handler in self._state_transition_handlers[transition_key]:
  554. try:
  555. await handler(old_state, new_state)
  556. except Exception as e:
  557. self.logger.error(f"Error in state transition handler: {e}")
  558. # Record state change metrics
  559. if self.performance_monitor:
  560. self.performance_monitor.record_metric(
  561. "workflow", MetricType.PROCESSING_TIME,
  562. time.time() - self._state_start_time, "seconds",
  563. {"state_transition": f"{old_state.value}_to_{new_state.value}"}
  564. )
  565. async def _emit_event(self, event: WorkflowEvent, data: Dict[str, Any]) -> None:
  566. """Emit a workflow event."""
  567. try:
  568. # Add common event data
  569. event_data = {
  570. "event": event.value,
  571. "timestamp": time.time(),
  572. "current_state": self._current_state.value,
  573. "session_id": self._current_session.session_id if self._current_session else None,
  574. **data
  575. }
  576. # Call event handlers
  577. if event in self._event_handlers:
  578. for handler in self._event_handlers[event]:
  579. try:
  580. await handler(event_data)
  581. except Exception as e:
  582. self.logger.error(f"Error in event handler for {event.value}: {e}")
  583. self.logger.debug(f"Emitted event: {event.value}")
  584. except Exception as e:
  585. self.logger.error(f"Error emitting event {event.value}: {e}")
  586. async def _update_performance_metrics(self) -> None:
  587. """Update performance metrics."""
  588. if not self.performance_monitor:
  589. return
  590. try:
  591. # Record current state duration
  592. state_duration = (time.time() - self._state_start_time) * 1000
  593. self.performance_monitor.record_metric(
  594. "workflow", MetricType.PROCESSING_TIME,
  595. state_duration, "milliseconds",
  596. {"state": self._current_state.value}
  597. )
  598. # Record session metrics if active
  599. if self._current_session:
  600. session_duration = self._current_session.duration * 1000
  601. self.performance_monitor.record_metric(
  602. "workflow", MetricType.PROCESSING_TIME,
  603. session_duration, "milliseconds",
  604. {"metric_type": "session_duration"}
  605. )
  606. except Exception as e:
  607. self.logger.error(f"Error updating performance metrics: {e}")
  608. def register_event_handler(self, event: WorkflowEvent, handler: Callable) -> None:
  609. """Register an event handler."""
  610. if event not in self._event_handlers:
  611. self._event_handlers[event] = []
  612. self._event_handlers[event].append(handler)
  613. def register_state_transition_handler(self, from_state: ConversationState,
  614. to_state: ConversationState,
  615. handler: Callable) -> None:
  616. """Register a state transition handler."""
  617. transition_key = (from_state, to_state)
  618. if transition_key not in self._state_transition_handlers:
  619. self._state_transition_handlers[transition_key] = []
  620. self._state_transition_handlers[transition_key].append(handler)
  621. def get_current_state(self) -> ConversationState:
  622. """Get current workflow state."""
  623. return self._current_state
  624. def get_current_session(self) -> Optional[ConversationSession]:
  625. """Get current conversation session."""
  626. return self._current_session
  627. def get_session_history(self, limit: Optional[int] = None) -> List[ConversationSession]:
  628. """Get conversation session history."""
  629. if limit:
  630. return self._session_history[-limit:]
  631. return self._session_history.copy()
  632. def get_workflow_statistics(self) -> Dict[str, Any]:
  633. """Get workflow statistics."""
  634. stats = self._workflow_stats.copy()
  635. stats.update({
  636. "current_state": self._current_state.value,
  637. "state_duration": time.time() - self._state_start_time,
  638. "active_session": self._current_session.session_id if self._current_session else None,
  639. "session_history_count": len(self._session_history),
  640. "is_running": self._running
  641. })
  642. # Calculate success rate
  643. total_conversations = stats["total_conversations"]
  644. if total_conversations > 0:
  645. stats["success_rate"] = stats["successful_conversations"] / total_conversations
  646. else:
  647. stats["success_rate"] = 0.0
  648. return stats
  649. async def simulate_wakeword_detection(self, speaker_id: str = "test_speaker",
  650. confidence: float = 0.8) -> None:
  651. """Simulate wakeword detection for testing."""
  652. if self._current_state != ConversationState.LISTENING:
  653. self.logger.warning("Cannot simulate wakeword - not in listening state")
  654. return
  655. await self._emit_event(WorkflowEvent.WAKEWORD_DETECTED, {
  656. "speaker_id": speaker_id,
  657. "speaker_name": f"Speaker {speaker_id}",
  658. "confidence": confidence,
  659. "wakeword_type": "hey_trixy"
  660. })
  661. async def force_state_transition(self, new_state: ConversationState) -> None:
  662. """Force a state transition (for testing/debugging)."""
  663. self.logger.warning(f"Forcing state transition to: {new_state.value}")
  664. await self._transition_to_state(new_state)
  665. # Factory functions
  666. def create_default_workflow_config() -> WorkflowConfig:
  667. """Create default workflow configuration."""
  668. return WorkflowConfig()
  669. def create_optimized_workflow_config() -> WorkflowConfig:
  670. """Create optimized workflow configuration for production."""
  671. config = WorkflowConfig()
  672. config.wakeword_sensitivity = 0.8
  673. config.max_recording_duration = 20.0
  674. config.silence_timeout = 1.5
  675. config.processing_timeout = 10.0
  676. config.response_timeout = 8.0
  677. config.enable_performance_monitoring = True
  678. config.log_conversation_metrics = True
  679. return config
  680. async def create_conversation_workflow(
  681. config: Optional[WorkflowConfig] = None,
  682. ml_manager: Optional[Any] = None,
  683. app_container: Optional[Any] = None,
  684. performance_monitor: Optional[PerformanceMonitor] = None,
  685. auto_start: bool = True
  686. ) -> ConversationWorkflow:
  687. """
  688. Create and initialize conversation workflow.
  689. Args:
  690. config: Workflow configuration
  691. ml_manager: ML manager instance
  692. app_container: Application container
  693. performance_monitor: Performance monitor
  694. auto_start: Whether to auto-start the workflow
  695. Returns:
  696. Initialized conversation workflow
  697. """
  698. if not config:
  699. config = create_default_workflow_config()
  700. workflow = ConversationWorkflow(
  701. config=config,
  702. ml_manager=ml_manager,
  703. app_container=app_container,
  704. performance_monitor=performance_monitor
  705. )
  706. if auto_start:
  707. await workflow.start()
  708. return workflow