| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594 |
- """
- Main Conversation Manager Implementation
- This module implements the ConversationManager class that serves as the central
- management system for all conversations in the Trixy voice assistant system.
- It coordinates conversation sessions, state management, context preservation,
- and multi-turn conversation workflows as specified in CLAUDE.md.
- Key Features:
- - Session management for multi-turn conversations
- - Conversation ID tracking across events
- - Context preservation and state management
- - Plugin integration for multi-turn workflows
- - Thread-safe operations for multi-satellite support
- - Conversation timeout handling and cleanup
- - Comprehensive logging and analytics
- - Integration with event system and application container
- The ConversationManager implements 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
- Example workflow:
- - User: "Trixy, Please order a pizza"
- - Trixy: "Ok, what kind of pizza do you want?"
- - User: "A spicy one"
- - Trixy: "Ok, i will send an order to your local pizza service"
- """
- import asyncio
- import json
- import threading
- import time
- import uuid
- import weakref
- from datetime import datetime, timezone, timedelta
- from dataclasses import dataclass, field
- from pathlib import Path
- from typing import Any, Dict, List, Optional, Set, Callable, Tuple, Union
- from collections import defaultdict, OrderedDict
- from concurrent.futures import ThreadPoolExecutor
- from .conversation_session import (
- ConversationSession, ConversationSessionState, ConversationTurn,
- ConversationRole, create_conversation_session
- )
- from .conversation_context import (
- ConversationContext, ContextScope, ContextRetentionPolicy,
- create_conversation_context
- )
- from .conversation_state import (
- ConversationState, ConversationStateManager, ConversationPhase,
- create_state_manager
- )
- from ..events.event_data import SatelliteInfo, SpeakerInfo
- def pprint(message: str) -> None:
- """Conversation manager logging function."""
- print(f"[CONVERSATION_MANAGER] {message}")
- @dataclass
- class ConversationStats:
- """Statistics for conversation management."""
- total_conversations: int = 0
- active_conversations: int = 0
- completed_conversations: int = 0
- timed_out_conversations: int = 0
- error_conversations: int = 0
- total_turns: int = 0
- average_turns_per_conversation: float = 0.0
- average_conversation_duration: float = 0.0
- plugin_interactions: int = 0
- context_entries: int = 0
- uptime_seconds: float = 0.0
- last_updated: Optional[datetime] = None
-
- def to_dict(self) -> Dict[str, Any]:
- """Convert to dictionary representation."""
- return {
- "total_conversations": self.total_conversations,
- "active_conversations": self.active_conversations,
- "completed_conversations": self.completed_conversations,
- "timed_out_conversations": self.timed_out_conversations,
- "error_conversations": self.error_conversations,
- "total_turns": self.total_turns,
- "average_turns_per_conversation": self.average_turns_per_conversation,
- "average_conversation_duration": self.average_conversation_duration,
- "plugin_interactions": self.plugin_interactions,
- "context_entries": self.context_entries,
- "uptime_seconds": self.uptime_seconds,
- "last_updated": self.last_updated.isoformat() if self.last_updated else None,
- }
- class ConversationManagerError(Exception):
- """Base exception for conversation manager errors."""
- pass
- class ConversationNotFoundError(ConversationManagerError):
- """Raised when a requested conversation is not found."""
- pass
- class ConversationTimeoutError(ConversationManagerError):
- """Raised when a conversation operation times out."""
- pass
- class ConversationStateError(ConversationManagerError):
- """Raised when a conversation is in an invalid state for the requested operation."""
- pass
- class ConversationManager:
- """
- Central management system for all conversations in the Trixy voice assistant.
-
- This class implements the conversation management system as specified in CLAUDE.md,
- providing comprehensive session management, context preservation, and multi-turn
- conversation support for the Trixy voice assistant system.
-
- Key Features:
- - Session management for multi-turn conversations
- - Conversation ID tracking across events
- - Context preservation across turns and sessions
- - Plugin integration for multi-turn workflows
- - Thread-safe operations for multi-satellite support
- - Automatic timeout handling and cleanup
- - Comprehensive analytics and monitoring
- - Integration with event system and application container
-
- The manager supports 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
-
- Usage:
- # Create conversation manager
- manager = ConversationManager(application)
-
- # Start a conversation
- session = manager.start_conversation(
- satellite_info=satellite_info,
- speaker_info=speaker_info,
- trigger_event="wakeword_received"
- )
-
- # Process conversation workflow
- manager.process_audio_input(session.conversation_id, audio_data)
- manager.process_text_input(session.conversation_id, "order pizza")
- manager.process_intent(session.conversation_id, intent_data)
-
- # Plugin interaction
- manager.ask_question(session.conversation_id, "What kind of pizza?")
- manager.process_text_input(session.conversation_id, "spicy one")
-
- # Complete conversation
- manager.complete_conversation(session.conversation_id, "Order placed!")
- """
-
- def __init__(
- self,
- application,
- max_concurrent_conversations: int = 100,
- default_conversation_timeout: float = 300.0, # 5 minutes
- plugin_question_timeout: float = 30.0, # 30 seconds
- cleanup_interval_seconds: float = 60.0, # 1 minute
- max_conversation_history: int = 1000,
- enable_context_persistence: bool = False,
- context_storage_path: Optional[str] = None,
- enable_analytics: bool = True
- ):
- """
- Initialize the conversation manager.
-
- Args:
- application: Application container reference
- max_concurrent_conversations: Maximum concurrent conversations
- default_conversation_timeout: Default timeout for conversations
- plugin_question_timeout: Timeout for plugin questions
- cleanup_interval_seconds: Interval for cleanup tasks
- max_conversation_history: Maximum conversations to keep in history
- enable_context_persistence: Enable context persistence to disk
- context_storage_path: Path for storing context data
- enable_analytics: Enable analytics collection
- """
- # Core dependencies
- self._application = application
- self._application_ref = weakref.ref(application) if application else None
-
- # Configuration
- self._max_concurrent_conversations = max_concurrent_conversations
- self._default_timeout = default_conversation_timeout
- self._plugin_timeout = plugin_question_timeout
- self._cleanup_interval = cleanup_interval_seconds
- self._max_history = max_conversation_history
- self._enable_context_persistence = enable_context_persistence
- self._context_storage_path = Path(context_storage_path or "./conversation_context")
- self._enable_analytics = enable_analytics
-
- # Active conversation storage
- self._active_conversations: Dict[str, ConversationSession] = OrderedDict()
- self._conversation_contexts: Dict[str, ConversationContext] = {}
- self._conversation_states: Dict[str, ConversationStateManager] = {}
-
- # Conversation history
- self._completed_conversations: Dict[str, ConversationSession] = OrderedDict()
-
- # Context management across conversations
- self._global_context: ConversationContext = create_conversation_context(
- conversation_id="global",
- enable_persistence=enable_context_persistence,
- persistence_path=self._context_storage_path / "global"
- )
-
- # Speaker and satellite context tracking
- self._speaker_contexts: Dict[str, ConversationContext] = {}
- self._satellite_contexts: Dict[str, ConversationContext] = {}
- self._room_contexts: Dict[str, ConversationContext] = {}
-
- # Plugin interaction tracking
- self._plugin_questions: Dict[str, Dict[str, Any]] = {} # conversation_id -> plugin data
- self._pending_callbacks: Dict[str, Callable] = {}
-
- # Thread safety
- self._lock = threading.RLock()
-
- # Background tasks
- self._cleanup_timer: Optional[threading.Timer] = None
- self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="ConversationManager")
-
- # Statistics and monitoring
- self._stats = ConversationStats()
- self._start_time = datetime.now(timezone.utc)
-
- # Event integration
- self._event_handler = None
- self._satellite_manager = None
-
- # Create storage directories
- if self._enable_context_persistence:
- self._context_storage_path.mkdir(parents=True, exist_ok=True)
-
- pprint(f"ConversationManager initialized (max_concurrent: {max_concurrent_conversations})")
-
- # Start background cleanup
- self._start_cleanup_timer()
-
- # Load persisted contexts
- if self._enable_context_persistence:
- self._load_persisted_contexts()
-
- # Core Conversation Management
-
- def start_conversation(
- self,
- satellite_info: SatelliteInfo,
- speaker_info: Optional[SpeakerInfo] = None,
- trigger_event: str = "wakeword_received",
- conversation_id: Optional[str] = None,
- timeout_seconds: Optional[float] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> ConversationSession:
- """
- Start a new conversation session.
-
- Args:
- satellite_info: Information about the satellite device
- speaker_info: Information about the detected speaker
- trigger_event: Event that triggered the conversation
- conversation_id: Optional specific conversation ID
- timeout_seconds: Conversation timeout (uses default if None)
- metadata: Additional metadata for the conversation
-
- Returns:
- ConversationSession: The created conversation session
-
- Raises:
- ConversationManagerError: If conversation cannot be started
- """
- with self._lock:
- # Check capacity
- if len(self._active_conversations) >= self._max_concurrent_conversations:
- # Try to clean up completed conversations
- self._cleanup_completed_conversations()
-
- if len(self._active_conversations) >= self._max_concurrent_conversations:
- raise ConversationManagerError(
- f"Maximum concurrent conversations ({self._max_concurrent_conversations}) reached"
- )
-
- # Generate conversation ID if not provided
- if conversation_id is None:
- conversation_id = self._generate_conversation_id()
-
- # Check for duplicate conversation ID
- if conversation_id in self._active_conversations:
- raise ConversationManagerError(f"Conversation {conversation_id} already exists")
-
- # Create conversation session
- session = create_conversation_session(
- conversation_id=conversation_id,
- satellite_info=satellite_info,
- speaker_info=speaker_info,
- trigger_event=trigger_event,
- timeout_seconds=timeout_seconds or self._default_timeout,
- application_ref=self._application_ref
- )
-
- # Create conversation context
- context = create_conversation_context(
- conversation_id=conversation_id,
- speaker_id=speaker_info.speaker_id if speaker_info else None,
- satellite_id=satellite_info.satellite_id,
- room_id=satellite_info.room_id,
- enable_persistence=self._enable_context_persistence,
- persistence_path=self._context_storage_path / conversation_id
- )
-
- # Create state manager
- state_manager = create_state_manager(
- conversation_id=conversation_id,
- initial_state=ConversationState.INITIALIZING,
- application_ref=self._application_ref
- )
-
- # Add metadata to session
- if metadata:
- session._metadata.update(metadata)
-
- # Store in active conversations
- self._active_conversations[conversation_id] = session
- self._conversation_contexts[conversation_id] = context
- self._conversation_states[conversation_id] = state_manager
-
- # Set up speaker context if needed
- if speaker_info and speaker_info.speaker_id not in self._speaker_contexts:
- self._speaker_contexts[speaker_info.speaker_id] = create_conversation_context(
- conversation_id=f"speaker_{speaker_info.speaker_id}",
- speaker_id=speaker_info.speaker_id,
- enable_persistence=self._enable_context_persistence,
- persistence_path=self._context_storage_path / "speakers" / speaker_info.speaker_id
- )
-
- # Set up satellite context if needed
- if satellite_info.satellite_id not in self._satellite_contexts:
- self._satellite_contexts[satellite_info.satellite_id] = create_conversation_context(
- conversation_id=f"satellite_{satellite_info.satellite_id}",
- satellite_id=satellite_info.satellite_id,
- enable_persistence=self._enable_context_persistence,
- persistence_path=self._context_storage_path / "satellites" / satellite_info.satellite_id
- )
-
- # Set up room context if needed
- if satellite_info.room_id not in self._room_contexts:
- self._room_contexts[satellite_info.room_id] = create_conversation_context(
- conversation_id=f"room_{satellite_info.room_id}",
- room_id=satellite_info.room_id,
- enable_persistence=self._enable_context_persistence,
- persistence_path=self._context_storage_path / "rooms" / satellite_info.room_id
- )
-
- # Transition to ready state
- state_manager.transition_to(
- ConversationState.READY,
- trigger="conversation_started",
- source="conversation_manager"
- )
-
- # Update statistics
- self._update_stats()
-
- # Trigger conversation started event
- self._trigger_conversation_event("conversation_started", session)
-
- pprint(f"Started conversation {conversation_id} for {satellite_info.alias}")
- return session
-
- def get_conversation(self, conversation_id: str) -> Optional[ConversationSession]:
- """
- Get an active conversation session.
-
- Args:
- conversation_id: Conversation identifier
-
- Returns:
- ConversationSession or None if not found
- """
- with self._lock:
- return self._active_conversations.get(conversation_id)
-
- def get_conversation_context(self, conversation_id: str) -> Optional[ConversationContext]:
- """
- Get conversation context.
-
- Args:
- conversation_id: Conversation identifier
-
- Returns:
- ConversationContext or None if not found
- """
- with self._lock:
- return self._conversation_contexts.get(conversation_id)
-
- def get_conversation_state(self, conversation_id: str) -> Optional[ConversationStateManager]:
- """
- Get conversation state manager.
-
- Args:
- conversation_id: Conversation identifier
-
- Returns:
- ConversationStateManager or None if not found
- """
- with self._lock:
- return self._conversation_states.get(conversation_id)
-
- def end_conversation(
- self,
- conversation_id: str,
- reason: str = "completed",
- final_response: Optional[str] = None
- ) -> bool:
- """
- End a conversation session.
-
- Args:
- conversation_id: Conversation identifier
- reason: Reason for ending the conversation
- final_response: Optional final response to the user
-
- Returns:
- True if conversation was ended successfully
- """
- with self._lock:
- session = self._active_conversations.get(conversation_id)
- if not session:
- return False
-
- # Complete the session
- if reason == "completed":
- session.complete(final_response)
- elif reason == "timeout":
- session.timeout()
- elif reason == "cancelled":
- session.cancel()
- else:
- session.error(f"Ended: {reason}")
-
- # Get context and state manager
- context = self._conversation_contexts.get(conversation_id)
- state_manager = self._conversation_states.get(conversation_id)
-
- # End session context
- if context:
- context.end_session()
-
- # Transition to appropriate terminal state
- if state_manager and not state_manager.is_terminal:
- if reason == "completed":
- state_manager.transition_to(ConversationState.COMPLETED)
- elif reason == "timeout":
- state_manager.transition_to(ConversationState.TIMED_OUT)
- elif reason == "cancelled":
- state_manager.transition_to(ConversationState.CANCELLED)
- else:
- state_manager.transition_to(ConversationState.ERROR)
-
- # Move to completed conversations
- self._completed_conversations[conversation_id] = session
-
- # Clean up active conversation
- del self._active_conversations[conversation_id]
- if conversation_id in self._conversation_contexts:
- del self._conversation_contexts[conversation_id]
- if conversation_id in self._conversation_states:
- del self._conversation_states[conversation_id]
- if conversation_id in self._plugin_questions:
- del self._plugin_questions[conversation_id]
- if conversation_id in self._pending_callbacks:
- del self._pending_callbacks[conversation_id]
-
- # Trim completed conversations if needed
- if len(self._completed_conversations) > self._max_history:
- # Remove oldest completed conversations
- oldest_key = next(iter(self._completed_conversations))
- del self._completed_conversations[oldest_key]
-
- # Update statistics
- self._update_stats()
-
- # Trigger conversation ended event
- self._trigger_conversation_event("conversation_ended", session, {"reason": reason})
-
- pprint(f"Ended conversation {conversation_id}: {reason}")
- return True
-
- # Audio and Input Processing
-
- def process_audio_input(
- self,
- conversation_id: str,
- audio_data: bytes,
- speaker_info: Optional[SpeakerInfo] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> Optional[ConversationTurn]:
- """
- Process raw audio input for a conversation.
-
- Args:
- conversation_id: Conversation identifier
- audio_data: Raw audio data
- speaker_info: Speaker information
- metadata: Additional metadata
-
- Returns:
- ConversationTurn or None if conversation not found
- """
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- pprint(f"Conversation {conversation_id} not found for audio input")
- return None
-
- # Transition to listening state
- state_manager.transition_to(
- ConversationState.LISTENING,
- trigger="audio_input_received",
- source="conversation_manager"
- )
-
- # Add audio turn
- turn = session.add_turn(
- role=ConversationRole.USER,
- input_type="audio",
- input_audio_data=audio_data,
- speaker_info=speaker_info,
- metadata=metadata or {}
- )
-
- # Update context with audio metadata
- context.set("last_audio_input", {
- "turn_id": turn.turn_id,
- "audio_length": len(audio_data),
- "timestamp": turn.timestamp.isoformat()
- }, scope=ContextScope.SESSION)
-
- # Trigger audio input event
- self._trigger_audio_input_event(conversation_id, audio_data, speaker_info, session.satellite_info)
-
- pprint(f"Processed audio input for conversation {conversation_id}")
- return turn
-
- def process_text_input(
- self,
- conversation_id: str,
- text: str,
- confidence: float = 1.0,
- speaker_info: Optional[SpeakerInfo] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> Optional[ConversationTurn]:
- """
- Process text input for a conversation.
-
- Args:
- conversation_id: Conversation identifier
- text: Input text
- confidence: Confidence score
- speaker_info: Speaker information
- metadata: Additional metadata
-
- Returns:
- ConversationTurn or None if conversation not found
- """
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- pprint(f"Conversation {conversation_id} not found for text input")
- return None
-
- # Check if this is an answer to a plugin question
- if conversation_id in self._plugin_questions:
- return self._handle_plugin_answer(conversation_id, text, confidence, speaker_info)
-
- # Transition to processing state
- state_manager.transition_to(
- ConversationState.TRANSCRIBING,
- trigger="text_input_received",
- source="conversation_manager"
- )
-
- # Add text turn
- turn = session.add_turn(
- role=ConversationRole.USER,
- input_type="text",
- input_text=text,
- input_confidence=confidence,
- speaker_info=speaker_info,
- metadata=metadata or {}
- )
-
- # Update context with text
- context.set("last_text_input", {
- "turn_id": turn.turn_id,
- "text": text,
- "confidence": confidence,
- "timestamp": turn.timestamp.isoformat()
- }, scope=ContextScope.SESSION)
-
- # Store in speaker context if available
- if speaker_info:
- speaker_context = self._speaker_contexts.get(speaker_info.speaker_id)
- if speaker_context:
- speaker_context.set("last_utterance", text, scope=ContextScope.SPEAKER)
-
- # Trigger text received event
- self._trigger_text_received_event(conversation_id, text, confidence, speaker_info, session.satellite_info)
-
- pprint(f"Processed text input for conversation {conversation_id}: {text}")
- return turn
-
- def process_intent(
- self,
- conversation_id: str,
- intent: str,
- entities: Optional[Dict[str, Any]] = None,
- confidence: float = 1.0,
- metadata: Optional[Dict[str, Any]] = None
- ) -> bool:
- """
- Process intent for a conversation.
-
- Args:
- conversation_id: Conversation identifier
- intent: Detected intent
- entities: Extracted entities
- confidence: Intent confidence
- metadata: Additional metadata
-
- Returns:
- True if intent was processed successfully
- """
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- pprint(f"Conversation {conversation_id} not found for intent processing")
- return False
-
- # Transition to intent processing state
- state_manager.transition_to(
- ConversationState.PROCESSING_INTENT,
- trigger="intent_received",
- source="conversation_manager"
- )
-
- # Update current turn with intent information
- current_turn = session.get_current_turn()
- if current_turn:
- session.update_turn_intent(
- current_turn.turn_id,
- intent=intent,
- entities=entities or {},
- confidence=confidence
- )
-
- # Update context with intent
- context.set("current_intent", {
- "intent": intent,
- "entities": entities or {},
- "confidence": confidence,
- "timestamp": datetime.now(timezone.utc).isoformat()
- }, scope=ContextScope.SESSION)
-
- # Add to conversation history in context
- conversation_history = context.get("conversation_history", [], scope=ContextScope.SESSION)
- conversation_history.append({
- "type": "intent",
- "intent": intent,
- "entities": entities or {},
- "timestamp": datetime.now(timezone.utc).isoformat()
- })
- context.set("conversation_history", conversation_history, scope=ContextScope.SESSION)
-
- # Trigger intent received event
- self._trigger_intent_received_event(
- conversation_id, intent, entities or {}, confidence,
- current_turn.input_text if current_turn else "",
- session.speaker_info, session.satellite_info
- )
-
- pprint(f"Processed intent for conversation {conversation_id}: {intent}")
- return True
-
- # Response Generation
-
- def generate_response(
- self,
- conversation_id: str,
- response_text: str,
- voice_settings: Optional[Dict[str, Any]] = None,
- plugin_name: Optional[str] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> bool:
- """
- Generate a response for a conversation.
-
- Args:
- conversation_id: Conversation identifier
- response_text: Response text
- voice_settings: Voice synthesis settings
- plugin_name: Plugin that generated the response
- metadata: Additional metadata
-
- Returns:
- True if response was generated successfully
- """
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- pprint(f"Conversation {conversation_id} not found for response generation")
- return False
-
- # Transition to response generation state
- state_manager.transition_to(
- ConversationState.GENERATING_RESPONSE,
- trigger="generate_response",
- source=plugin_name or "conversation_manager"
- )
-
- # Update current turn or create assistant turn
- current_turn = session.get_current_turn()
-
- if current_turn and current_turn.role == ConversationRole.USER:
- # Update existing user turn with response
- session.update_turn_response(
- current_turn.turn_id,
- response_text=response_text,
- response_voice_settings=voice_settings or {},
- plugin_name=plugin_name
- )
- else:
- # Create new assistant turn
- turn = session.add_turn(
- role=ConversationRole.ASSISTANT,
- input_type="response",
- input_text=response_text,
- metadata=metadata or {}
- )
- session.update_turn_response(
- turn.turn_id,
- response_text=response_text,
- response_voice_settings=voice_settings or {},
- plugin_name=plugin_name
- )
-
- # Update context with response
- context.set("last_response", {
- "text": response_text,
- "plugin": plugin_name,
- "timestamp": datetime.now(timezone.utc).isoformat()
- }, scope=ContextScope.SESSION)
-
- # Add to conversation history
- conversation_history = context.get("conversation_history", [], scope=ContextScope.SESSION)
- conversation_history.append({
- "type": "response",
- "text": response_text,
- "plugin": plugin_name,
- "timestamp": datetime.now(timezone.utc).isoformat()
- })
- context.set("conversation_history", conversation_history, scope=ContextScope.SESSION)
-
- pprint(f"Generated response for conversation {conversation_id}: {response_text[:50]}...")
- return True
-
- def synthesize_speech(
- self,
- conversation_id: str,
- audio_data: bytes,
- text: str,
- voice_settings: Optional[Dict[str, Any]] = None
- ) -> bool:
- """
- Process synthesized speech for a conversation.
-
- Args:
- conversation_id: Conversation identifier
- audio_data: Synthesized audio data
- text: Original text
- voice_settings: Voice settings used
-
- Returns:
- True if speech was processed successfully
- """
- session = self.get_conversation(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not state_manager:
- return False
-
- # Transition to speech synthesis state
- state_manager.transition_to(
- ConversationState.SYNTHESIZING_SPEECH,
- trigger="speech_synthesized",
- source="conversation_manager"
- )
-
- # Update current turn with audio data
- current_turn = session.get_current_turn()
- if current_turn:
- session.update_turn_response(
- current_turn.turn_id,
- response_audio_data=audio_data,
- response_voice_settings=voice_settings or {}
- )
-
- # Trigger TTS received event
- self._trigger_tts_received_event(conversation_id, audio_data, text, voice_settings or {})
-
- # Transition to playing response
- state_manager.transition_to(
- ConversationState.PLAYING_RESPONSE,
- trigger="playing_response",
- source="conversation_manager"
- )
-
- pprint(f"Synthesized speech for conversation {conversation_id}")
- return True
-
- # Plugin Interaction Support
-
- def ask_question(
- self,
- conversation_id: str,
- question: str,
- plugin_name: str,
- callback: Optional[Callable] = None,
- timeout_seconds: Optional[float] = None,
- voice_settings: Optional[Dict[str, Any]] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> bool:
- """
- Ask a question and wait for user response (plugin interaction).
-
- Args:
- conversation_id: Conversation identifier
- question: Question to ask the user
- plugin_name: Name of the plugin asking the question
- callback: Optional callback function for the response
- timeout_seconds: Timeout for the response
- voice_settings: Voice settings for TTS
- metadata: Additional metadata
-
- Returns:
- True if question was asked successfully
- """
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- pprint(f"Conversation {conversation_id} not found for plugin question")
- return False
-
- # Transition to plugin interaction state
- state_manager.transition_to(
- ConversationState.PLUGIN_INTERACTION,
- trigger="plugin_question",
- source=plugin_name
- )
-
- # Ask question through session
- turn = session.ask_question(
- question=question,
- plugin_name=plugin_name,
- callback=callback,
- timeout_seconds=timeout_seconds or self._plugin_timeout,
- voice_settings=voice_settings
- )
-
- # Store plugin question info
- self._plugin_questions[conversation_id] = {
- "plugin_name": plugin_name,
- "question": question,
- "turn_id": turn.turn_id,
- "callback": callback,
- "timeout": datetime.now(timezone.utc) + timedelta(
- seconds=timeout_seconds or self._plugin_timeout
- ),
- "metadata": metadata or {}
- }
-
- if callback:
- self._pending_callbacks[conversation_id] = callback
-
- # Update context
- context.set("plugin_question", {
- "plugin": plugin_name,
- "question": question,
- "turn_id": turn.turn_id,
- "timestamp": datetime.now(timezone.utc).isoformat()
- }, scope=ContextScope.SESSION)
-
- # Transition to waiting for user
- state_manager.transition_to(
- ConversationState.WAITING_FOR_USER,
- trigger="waiting_for_answer",
- source=plugin_name
- )
-
- # Generate TTS response for the question
- self.generate_response(
- conversation_id=conversation_id,
- response_text=question,
- voice_settings=voice_settings,
- plugin_name=plugin_name,
- metadata={"is_question": True}
- )
-
- pprint(f"Plugin {plugin_name} asked question in conversation {conversation_id}: {question}")
- return True
-
- def _handle_plugin_answer(
- self,
- conversation_id: str,
- answer: str,
- confidence: float,
- speaker_info: Optional[SpeakerInfo]
- ) -> Optional[ConversationTurn]:
- """Handle an answer to a plugin question."""
- session = self.get_conversation(conversation_id)
- context = self.get_conversation_context(conversation_id)
- state_manager = self.get_conversation_state(conversation_id)
-
- if not session or not context or not state_manager:
- return None
-
- plugin_data = self._plugin_questions.get(conversation_id)
- if not plugin_data:
- return None
-
- # Answer the question through session
- turn = session.answer_question(answer, confidence, speaker_info)
-
- # Update statistics
- if self._enable_analytics:
- self._stats.plugin_interactions += 1
-
- # Clean up plugin question
- if conversation_id in self._plugin_questions:
- del self._plugin_questions[conversation_id]
- if conversation_id in self._pending_callbacks:
- del self._pending_callbacks[conversation_id]
-
- # Clear plugin question context
- context.delete("plugin_question", scope=ContextScope.SESSION)
-
- # Update context with answer
- context.set("plugin_answer", {
- "plugin": plugin_data["plugin_name"],
- "question": plugin_data["question"],
- "answer": answer,
- "confidence": confidence,
- "timestamp": datetime.now(timezone.utc).isoformat()
- }, scope=ContextScope.SESSION)
-
- pprint(f"Received plugin answer in conversation {conversation_id}: {answer}")
- return turn
-
- def complete_conversation(
- self,
- conversation_id: str,
- final_response: Optional[str] = None,
- metadata: Optional[Dict[str, Any]] = None
- ) -> bool:
- """
- Complete a conversation with optional final response.
-
- Args:
- conversation_id: Conversation identifier
- final_response: Optional final response
- metadata: Additional metadata
-
- Returns:
- True if conversation was completed successfully
- """
- if final_response:
- self.generate_response(
- conversation_id=conversation_id,
- response_text=final_response,
- metadata=metadata
- )
-
- return self.end_conversation(conversation_id, "completed", final_response)
-
- # Context Management
-
- def get_global_context(self) -> ConversationContext:
- """Get the global context manager."""
- return self._global_context
-
- def get_speaker_context(self, speaker_id: str) -> Optional[ConversationContext]:
- """Get context for a specific speaker."""
- return self._speaker_contexts.get(speaker_id)
-
- def get_satellite_context(self, satellite_id: str) -> Optional[ConversationContext]:
- """Get context for a specific satellite."""
- return self._satellite_contexts.get(satellite_id)
-
- def get_room_context(self, room_id: str) -> Optional[ConversationContext]:
- """Get context for a specific room."""
- return self._room_contexts.get(room_id)
-
- def set_global_context(self, key: str, value: Any, **kwargs) -> None:
- """Set a global context value."""
- self._global_context.set(key, value, scope=ContextScope.GLOBAL, **kwargs)
-
- def get_context_value(
- self,
- conversation_id: str,
- key: str,
- scopes: Optional[List[ContextScope]] = None,
- default: Any = None
- ) -> Any:
- """
- Get a context value searching multiple scopes.
-
- Args:
- conversation_id: Conversation identifier
- key: Context key
- scopes: Scopes to search (defaults to all relevant scopes)
- default: Default value if not found
-
- Returns:
- Context value or default
- """
- context = self.get_conversation_context(conversation_id)
- if not context:
- return default
-
- if scopes is None:
- scopes = [ContextScope.SESSION, ContextScope.SPEAKER, ContextScope.SATELLITE,
- ContextScope.ROOM, ContextScope.GLOBAL]
-
- return context.get_multi_scope(key, scopes, default)
-
- # Monitoring and Analytics
-
- def get_active_conversations(self) -> List[ConversationSession]:
- """Get all active conversation sessions."""
- with self._lock:
- return list(self._active_conversations.values())
-
- def get_completed_conversations(self) -> List[ConversationSession]:
- """Get completed conversation sessions."""
- with self._lock:
- return list(self._completed_conversations.values())
-
- def get_conversation_by_speaker(self, speaker_id: str) -> List[ConversationSession]:
- """Get conversations for a specific speaker."""
- with self._lock:
- conversations = []
- for session in self._active_conversations.values():
- if session.speaker_info and session.speaker_info.speaker_id == speaker_id:
- conversations.append(session)
- return conversations
-
- def get_conversation_by_satellite(self, satellite_id: str) -> List[ConversationSession]:
- """Get conversations for a specific satellite."""
- with self._lock:
- conversations = []
- for session in self._active_conversations.values():
- if session.satellite_info.satellite_id == satellite_id:
- conversations.append(session)
- return conversations
-
- def _update_stats(self) -> None:
- """Update conversation statistics."""
- if not self._enable_analytics:
- return
-
- with self._lock:
- now = datetime.now(timezone.utc)
-
- # Count conversations by state
- active_count = len(self._active_conversations)
- completed_count = len(self._completed_conversations)
-
- # Count conversations by final state
- timed_out_count = 0
- error_count = 0
- for session in self._completed_conversations.values():
- if session.state == ConversationSessionState.TIMED_OUT:
- timed_out_count += 1
- elif session.state == ConversationSessionState.ERROR:
- error_count += 1
-
- # Calculate turn statistics
- total_turns = 0
- total_duration = 0.0
- for session in list(self._active_conversations.values()) + list(self._completed_conversations.values()):
- total_turns += session.turn_count
- total_duration += session.duration_seconds
-
- total_conversations = active_count + completed_count
- avg_turns = total_turns / max(total_conversations, 1)
- avg_duration = total_duration / max(total_conversations, 1)
-
- # Count context entries
- context_entries = 0
- for context in self._conversation_contexts.values():
- for scope_entries in context._contexts.values():
- context_entries += len(scope_entries)
-
- # Update statistics
- self._stats = ConversationStats(
- total_conversations=total_conversations,
- active_conversations=active_count,
- completed_conversations=completed_count,
- timed_out_conversations=timed_out_count,
- error_conversations=error_count,
- total_turns=total_turns,
- average_turns_per_conversation=avg_turns,
- average_conversation_duration=avg_duration,
- plugin_interactions=self._stats.plugin_interactions,
- context_entries=context_entries,
- uptime_seconds=(now - self._start_time).total_seconds(),
- last_updated=now
- )
-
- def get_stats(self) -> ConversationStats:
- """Get conversation statistics."""
- self._update_stats()
- return self._stats
-
- def get_status(self) -> Dict[str, Any]:
- """Get comprehensive status information."""
- with self._lock:
- stats = self.get_stats()
-
- return {
- "statistics": stats.to_dict(),
- "configuration": {
- "max_concurrent_conversations": self._max_concurrent_conversations,
- "default_timeout_seconds": self._default_timeout,
- "plugin_timeout_seconds": self._plugin_timeout,
- "cleanup_interval_seconds": self._cleanup_interval,
- "max_history": self._max_history,
- "enable_context_persistence": self._enable_context_persistence,
- "enable_analytics": self._enable_analytics,
- },
- "active_conversations": [
- {
- "conversation_id": session.conversation_id,
- "state": session.state.value,
- "duration_seconds": session.duration_seconds,
- "turn_count": session.turn_count,
- "speaker_id": session.speaker_info.speaker_id if session.speaker_info else None,
- "satellite_id": session.satellite_info.satellite_id,
- "room_id": session.satellite_info.room_id
- }
- for session in self._active_conversations.values()
- ],
- "plugin_questions": len(self._plugin_questions),
- "pending_callbacks": len(self._pending_callbacks),
- "context_managers": {
- "conversation_contexts": len(self._conversation_contexts),
- "speaker_contexts": len(self._speaker_contexts),
- "satellite_contexts": len(self._satellite_contexts),
- "room_contexts": len(self._room_contexts),
- },
- "timestamp": datetime.now(timezone.utc).isoformat()
- }
-
- # Cleanup and Maintenance
-
- def _start_cleanup_timer(self) -> None:
- """Start the background cleanup timer."""
- if self._cleanup_timer:
- self._cleanup_timer.cancel()
-
- self._cleanup_timer = threading.Timer(self._cleanup_interval, self._periodic_cleanup)
- self._cleanup_timer.daemon = True
- self._cleanup_timer.start()
-
- def _periodic_cleanup(self) -> None:
- """Perform periodic cleanup tasks."""
- try:
- # Check for timed out conversations
- self._check_conversation_timeouts()
-
- # Check for timed out plugin questions
- self._check_plugin_timeouts()
-
- # Clean up expired contexts
- self._cleanup_expired_contexts()
-
- # Clean up completed conversations
- self._cleanup_completed_conversations()
-
- # Update statistics
- self._update_stats()
-
- except Exception as e:
- pprint(f"Error during periodic cleanup: {e}")
- finally:
- # Restart timer
- self._start_cleanup_timer()
-
- def _check_conversation_timeouts(self) -> None:
- """Check for and handle conversation timeouts."""
- with self._lock:
- timed_out = []
-
- for conversation_id, session in self._active_conversations.items():
- if session.is_timed_out:
- timed_out.append(conversation_id)
-
- for conversation_id in timed_out:
- pprint(f"Conversation {conversation_id} timed out")
- self.end_conversation(conversation_id, "timeout")
-
- def _check_plugin_timeouts(self) -> None:
- """Check for and handle plugin question timeouts."""
- with self._lock:
- now = datetime.now(timezone.utc)
- timed_out = []
-
- for conversation_id, plugin_data in self._plugin_questions.items():
- if now > plugin_data["timeout"]:
- timed_out.append(conversation_id)
-
- for conversation_id in timed_out:
- session = self._active_conversations.get(conversation_id)
- if session:
- session.cancel_plugin_question("timeout")
- pprint(f"Plugin question timed out for conversation {conversation_id}")
-
- # Clean up
- if conversation_id in self._plugin_questions:
- del self._plugin_questions[conversation_id]
- if conversation_id in self._pending_callbacks:
- del self._pending_callbacks[conversation_id]
-
- def _cleanup_expired_contexts(self) -> None:
- """Clean up expired context entries."""
- # Clean up conversation contexts
- for context in self._conversation_contexts.values():
- context.cleanup_expired()
-
- # Clean up speaker contexts
- for context in self._speaker_contexts.values():
- context.cleanup_expired()
-
- # Clean up satellite contexts
- for context in self._satellite_contexts.values():
- context.cleanup_expired()
-
- # Clean up room contexts
- for context in self._room_contexts.values():
- context.cleanup_expired()
-
- # Clean up global context
- self._global_context.cleanup_expired()
-
- def _cleanup_completed_conversations(self) -> None:
- """Clean up old completed conversations."""
- with self._lock:
- if len(self._completed_conversations) <= self._max_history:
- return
-
- # Remove oldest conversations
- excess_count = len(self._completed_conversations) - self._max_history
- for _ in range(excess_count):
- if self._completed_conversations:
- oldest_key = next(iter(self._completed_conversations))
- del self._completed_conversations[oldest_key]
-
- # Event Integration
-
- def set_event_handler(self, event_handler) -> None:
- """Set the event handler for triggering conversation events."""
- self._event_handler = event_handler
-
- def set_satellite_manager(self, satellite_manager) -> None:
- """Set the satellite manager for satellite integration."""
- self._satellite_manager = satellite_manager
-
- def _trigger_conversation_event(
- self,
- event_type: str,
- session: ConversationSession,
- additional_data: Optional[Dict[str, Any]] = None
- ) -> None:
- """Trigger a conversation-related event."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- from ..events import EventDataFactory
-
- event_data = EventDataFactory.create_event_data(
- event_type,
- conversation_id=session.conversation_id,
- speaker_info=session.speaker_info,
- satellite_info=session.satellite_info,
- state=session.state.value,
- turn_count=session.turn_count,
- duration_seconds=session.duration_seconds,
- **(additional_data or {})
- )
-
- event_handler.trigger_event(event_type, event_data)
-
- except Exception as e:
- pprint(f"Error triggering conversation event: {e}")
-
- def _trigger_audio_input_event(
- self,
- conversation_id: str,
- audio_data: bytes,
- speaker_info: Optional[SpeakerInfo],
- satellite_info: SatelliteInfo
- ) -> None:
- """Trigger raw audio input received event."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- from ..events import EventDataFactory
-
- audio_data = EventDataFactory.create_event_data(
- "raw_audio_input_received",
- conversation_id=conversation_id,
- audio_data=audio_data,
- speaker_info=speaker_info,
- satellite_info=satellite_info,
- sample_rate=16000,
- channels=1,
- bit_depth=16,
- duration_seconds=len(audio_data) / (16000 * 2) # Assuming 16kHz 16-bit
- )
-
- event_handler.trigger_event("raw_audio_input_received", audio_data)
-
- except Exception as e:
- pprint(f"Error triggering audio input event: {e}")
-
- def _trigger_text_received_event(
- self,
- conversation_id: str,
- text: str,
- confidence: float,
- speaker_info: Optional[SpeakerInfo],
- satellite_info: SatelliteInfo
- ) -> None:
- """Trigger text received event."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- from ..events import EventDataFactory
-
- text_data = EventDataFactory.create_event_data(
- "text_received",
- conversation_id=conversation_id,
- text=text,
- confidence=confidence,
- speaker_info=speaker_info,
- satellite_info=satellite_info
- )
-
- event_handler.trigger_event("text_received", text_data)
-
- except Exception as e:
- pprint(f"Error triggering text received event: {e}")
-
- def _trigger_intent_received_event(
- self,
- conversation_id: str,
- intent: str,
- entities: Dict[str, Any],
- confidence: float,
- original_text: str,
- speaker_info: Optional[SpeakerInfo],
- satellite_info: SatelliteInfo
- ) -> None:
- """Trigger intent received event."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- from ..events import EventDataFactory
-
- intent_data = EventDataFactory.create_event_data(
- "intent_received",
- conversation_id=conversation_id,
- intent=intent,
- entities=entities,
- confidence=confidence,
- original_text=original_text,
- speaker_info=speaker_info,
- satellite_info=satellite_info
- )
-
- event_handler.trigger_event("intent_received", intent_data)
-
- except Exception as e:
- pprint(f"Error triggering intent received event: {e}")
-
- def _trigger_tts_received_event(
- self,
- conversation_id: str,
- audio_data: bytes,
- text: str,
- voice_settings: Dict[str, Any]
- ) -> None:
- """Trigger TTS received event."""
- try:
- if self._application_ref and self._application_ref():
- app = self._application_ref()
- event_handler = app.get_event_handler()
-
- from ..events import EventDataFactory
-
- tts_data = EventDataFactory.create_event_data(
- "tts_received",
- conversation_id=conversation_id,
- audio_data=audio_data,
- text=text,
- voice_settings=voice_settings,
- sample_rate=16000,
- channels=1,
- bit_depth=16,
- duration_seconds=len(audio_data) / (16000 * 2)
- )
-
- event_handler.trigger_event("tts_received", tts_data)
-
- except Exception as e:
- pprint(f"Error triggering TTS received event: {e}")
-
- # Persistence Operations
-
- def _load_persisted_contexts(self) -> None:
- """Load persisted context data."""
- if not self._enable_context_persistence or not self._context_storage_path.exists():
- return
-
- try:
- # Load global context
- global_path = self._context_storage_path / "global"
- if global_path.exists():
- self._global_context._load_persistent_context()
-
- # Load speaker contexts
- speakers_path = self._context_storage_path / "speakers"
- if speakers_path.exists():
- for speaker_dir in speakers_path.iterdir():
- if speaker_dir.is_dir():
- speaker_id = speaker_dir.name
- self._speaker_contexts[speaker_id] = create_conversation_context(
- conversation_id=f"speaker_{speaker_id}",
- speaker_id=speaker_id,
- enable_persistence=True,
- persistence_path=speaker_dir
- )
-
- # Load satellite contexts
- satellites_path = self._context_storage_path / "satellites"
- if satellites_path.exists():
- for satellite_dir in satellites_path.iterdir():
- if satellite_dir.is_dir():
- satellite_id = satellite_dir.name
- self._satellite_contexts[satellite_id] = create_conversation_context(
- conversation_id=f"satellite_{satellite_id}",
- satellite_id=satellite_id,
- enable_persistence=True,
- persistence_path=satellite_dir
- )
-
- # Load room contexts
- rooms_path = self._context_storage_path / "rooms"
- if rooms_path.exists():
- for room_dir in rooms_path.iterdir():
- if room_dir.is_dir():
- room_id = room_dir.name
- self._room_contexts[room_id] = create_conversation_context(
- conversation_id=f"room_{room_id}",
- room_id=room_id,
- enable_persistence=True,
- persistence_path=room_dir
- )
-
- pprint("Loaded persisted conversation contexts")
-
- except Exception as e:
- pprint(f"Error loading persisted contexts: {e}")
-
- # Utility Methods
-
- def _generate_conversation_id(self) -> str:
- """Generate a unique conversation ID."""
- timestamp = int(time.time() * 1000)
- return f"conv_{timestamp}_{uuid.uuid4().hex[:8]}"
-
- def shutdown(self) -> None:
- """Shutdown the conversation manager gracefully."""
- with self._lock:
- pprint("Shutting down conversation manager...")
-
- # Cancel cleanup timer
- if self._cleanup_timer:
- self._cleanup_timer.cancel()
- self._cleanup_timer = None
-
- # End all active conversations
- active_ids = list(self._active_conversations.keys())
- for conversation_id in active_ids:
- self.end_conversation(conversation_id, "shutdown")
-
- # Shutdown executor
- self._executor.shutdown(wait=True)
-
- # Clear all data
- self._active_conversations.clear()
- self._conversation_contexts.clear()
- self._conversation_states.clear()
- self._plugin_questions.clear()
- self._pending_callbacks.clear()
-
- pprint("Conversation manager shutdown complete")
-
- def __str__(self) -> str:
- """String representation of the conversation manager."""
- stats = self.get_stats()
- return (f"ConversationManager(active={stats.active_conversations}, "
- f"completed={stats.completed_conversations})")
-
- def __repr__(self) -> str:
- """Detailed representation of the conversation manager."""
- return (f"ConversationManager(max_concurrent={self._max_concurrent_conversations}, "
- f"active={len(self._active_conversations)}, "
- f"completed={len(self._completed_conversations)})")
- # Factory function
- def create_conversation_manager(
- application,
- **kwargs
- ) -> ConversationManager:
- """
- Create a ConversationManager with default configuration.
-
- Args:
- application: Application container reference
- **kwargs: Additional configuration parameters
-
- Returns:
- ConversationManager: Configured conversation manager
- """
- return ConversationManager(application, **kwargs)
- # Module exports
- __all__ = [
- "ConversationManager",
- "ConversationManagerError",
- "ConversationNotFoundError",
- "ConversationTimeoutError",
- "ConversationStateError",
- "ConversationStats",
- "create_conversation_manager"
- ]
|