""" Discord bot integration for Lyra with human-like behavior patterns. Implements sophisticated behavioral patterns including: - Natural response timing based on message complexity - Typing indicators and delays - Emotional response to user interactions - Memory of past conversations - Personality-driven responses """ import discord from discord.ext import commands import asyncio import logging import random import time from typing import Dict, List, Optional, Any from datetime import datetime, timedelta from dataclasses import dataclass from ..config import config from ..core.lyra_model import LyraModel from ..database.manager import DatabaseManager from ..emotions.system import EmotionalState from ..training.pipeline import LyraTrainingPipeline logger = logging.getLogger(__name__) @dataclass class UserInteraction: """Tracks user interaction history.""" user_id: str username: str last_interaction: datetime interaction_count: int emotional_history: List[str] conversation_context: List[Dict[str, Any]] relationship_level: float # 0.0 to 1.0 @dataclass class ResponseTiming: """Calculates human-like response timing.""" base_delay: float typing_speed: float # Characters per second thinking_time: float emotional_modifier: float class HumanBehaviorEngine: """Simulates human-like behavior patterns for responses.""" def __init__(self): # Typing speed parameters (realistic human ranges) self.typing_speeds = { 'excited': 4.5, # Fast typing when excited 'normal': 3.2, # Average typing speed 'thoughtful': 2.1, # Slower when thinking deeply 'tired': 1.8, # Slower when tired 'emotional': 2.8 # Variable when emotional } # Response delay patterns self.delay_patterns = { 'instant': (0.5, 1.5), # Quick reactions 'normal': (1.5, 4.0), # Normal thinking 'complex': (3.0, 8.0), # Complex responses 'emotional': (2.0, 6.0), # Emotional processing 'distracted': (5.0, 15.0) # When "distracted" } def calculate_response_timing( self, message_content: str, emotional_state: EmotionalState, relationship_level: float, message_complexity: float ) -> ResponseTiming: """Calculate human-like response timing.""" # Base delay based on relationship (closer = faster response) base_delay = max(1.0, 8.0 - (relationship_level * 6.0)) # Adjust for message complexity complexity_factor = 1.0 + (message_complexity * 2.0) thinking_time = base_delay * complexity_factor # Emotional adjustments dominant_emotion, intensity = emotional_state.get_dominant_emotion() emotional_modifier = 1.0 if dominant_emotion == 'excitement': emotional_modifier = 0.6 # Respond faster when excited typing_speed = self.typing_speeds['excited'] elif dominant_emotion == 'sadness': emotional_modifier = 1.4 # Respond slower when sad typing_speed = self.typing_speeds['thoughtful'] elif dominant_emotion == 'anger': emotional_modifier = 0.8 # Quick but not too quick when angry typing_speed = self.typing_speeds['emotional'] elif dominant_emotion == 'curiosity': emotional_modifier = 0.9 # Eager to respond when curious typing_speed = self.typing_speeds['normal'] else: typing_speed = self.typing_speeds['normal'] # Add randomness for realism randomness = random.uniform(0.8, 1.2) thinking_time *= emotional_modifier * randomness return ResponseTiming( base_delay=base_delay, typing_speed=typing_speed, thinking_time=max(thinking_time, 0.5), # Minimum delay emotional_modifier=emotional_modifier ) def should_show_typing( self, message_length: int, emotional_state: EmotionalState ) -> bool: """Determine if typing indicator should be shown.""" # Always show typing for longer messages if message_length > 50: return True # Show typing based on emotional state dominant_emotion, intensity = emotional_state.get_dominant_emotion() if dominant_emotion in ['excitement', 'curiosity'] and intensity > 0.7: return random.random() < 0.9 # Usually show when excited if dominant_emotion == 'thoughtfulness': return random.random() < 0.8 # Often show when thinking # Random chance for shorter messages return random.random() < 0.3 def calculate_typing_duration( self, message_length: int, typing_speed: float ) -> float: """Calculate realistic typing duration.""" base_time = message_length / typing_speed # Add pauses for punctuation and thinking pause_count = message_length // 25 # Pause every 25 characters pause_time = pause_count * random.uniform(0.3, 1.2) # Add natural variation variation = base_time * random.uniform(0.8, 1.3) return max(base_time + pause_time + variation, 1.0) class LyraDiscordBot(commands.Bot): """Main Discord bot class with integrated Lyra AI.""" def __init__( self, lyra_model: LyraModel, training_pipeline: LyraTrainingPipeline, database_manager: DatabaseManager ): intents = discord.Intents.default() intents.message_content = True intents.guilds = True intents.guild_messages = True super().__init__( command_prefix='!lyra ', intents=intents, description="Lyra AI - Your emotionally intelligent companion" ) # Core components self.lyra_model = lyra_model self.training_pipeline = training_pipeline self.database_manager = database_manager # Behavior systems self.behavior_engine = HumanBehaviorEngine() self.user_interactions: Dict[str, UserInteraction] = {} # State tracking self.active_conversations: Dict[str, List[Dict]] = {} self.processing_messages: set = set() # Performance tracking self.response_count = 0 self.start_time = datetime.now() async def on_ready(self): """Called when bot is ready.""" logger.info(f'{self.user} has connected to Discord!') logger.info(f'Connected to {len(self.guilds)} servers') # Load user interaction history await self._load_user_interactions() # Set presence await self.change_presence( activity=discord.Activity( type=discord.ActivityType.listening, name="conversations and learning 🎭" ) ) async def on_message(self, message: discord.Message): """Handle incoming messages with human-like behavior.""" # Skip own messages if message.author == self.user: return # Skip system messages if message.type != discord.MessageType.default: return # Check if message mentions Lyra or is DM should_respond = ( isinstance(message.channel, discord.DMChannel) or self.user in message.mentions or 'lyra' in message.content.lower() ) if not should_respond: # Still process commands await self.process_commands(message) return # Prevent duplicate processing message_key = f"{message.channel.id}:{message.id}" if message_key in self.processing_messages: return self.processing_messages.add(message_key) try: await self._handle_conversation(message) except Exception as e: logger.error(f"Error handling message: {e}") await message.channel.send( "I'm having trouble processing that right now. " "Could you try again in a moment? 😅" ) finally: self.processing_messages.discard(message_key) async def _handle_conversation(self, message: discord.Message): """Handle conversation with human-like behavior.""" user_id = str(message.author.id) channel_id = str(message.channel.id) # Update user interaction await self._update_user_interaction(message) user_interaction = self.user_interactions.get(user_id) # Get conversation context conversation_context = self.active_conversations.get(channel_id, []) # Add user message to context conversation_context.append({ 'role': 'user', 'content': message.content, 'timestamp': datetime.now(), 'author': message.author.display_name }) # Keep context manageable (sliding window) if len(conversation_context) > 20: conversation_context = conversation_context[-20:] self.active_conversations[channel_id] = conversation_context # Generate Lyra's response response_text, response_info = await self.lyra_model.generate_response( user_message=message.content, user_id=user_id, max_new_tokens=150, temperature=0.9, top_p=0.95 ) # Get emotional state for timing calculation emotional_state = response_info['emotional_state'] # Calculate response timing message_complexity = self._calculate_message_complexity(message.content) relationship_level = user_interaction.relationship_level if user_interaction else 0.1 # Create EmotionalState object for timing calculation emotions_tensor = torch.rand(19) # Placeholder emotion_state = EmotionalState.from_tensor(emotions_tensor, self.lyra_model.device) timing = self.behavior_engine.calculate_response_timing( message.content, emotion_state, relationship_level, message_complexity ) # Human-like response behavior await self._deliver_response_naturally( message.channel, response_text, timing, emotion_state ) # Add Lyra's response to context conversation_context.append({ 'role': 'assistant', 'content': response_text, 'timestamp': datetime.now(), 'emotional_state': response_info['emotional_state'], 'thoughts': response_info.get('thoughts', []) }) # Store conversation for training await self._store_conversation_turn( user_id, channel_id, message.content, response_text, response_info ) self.response_count += 1 async def _deliver_response_naturally( self, channel: discord.TextChannel, response_text: str, timing: ResponseTiming, emotional_state: EmotionalState ): """Deliver response with natural human-like timing.""" # Initial thinking delay await asyncio.sleep(timing.thinking_time) # Show typing indicator if appropriate if self.behavior_engine.should_show_typing(len(response_text), emotional_state): typing_duration = self.behavior_engine.calculate_typing_duration( len(response_text), timing.typing_speed ) # Start typing and wait async with channel.typing(): await asyncio.sleep(min(typing_duration, 8.0)) # Max 8 seconds typing # Small pause before sending (like human hesitation) await asyncio.sleep(random.uniform(0.3, 1.0)) # Send the message await channel.send(response_text) def _calculate_message_complexity(self, message: str) -> float: """Calculate message complexity for timing.""" # Simple complexity scoring word_count = len(message.split()) question_marks = message.count('?') exclamation_marks = message.count('!') # Base complexity on length complexity = min(word_count / 50.0, 1.0) # Increase for questions (require more thought) if question_marks > 0: complexity += 0.3 # Increase for emotional content if exclamation_marks > 0: complexity += 0.2 return min(complexity, 1.0) async def _update_user_interaction(self, message: discord.Message): """Update user interaction tracking.""" user_id = str(message.author.id) if user_id not in self.user_interactions: self.user_interactions[user_id] = UserInteraction( user_id=user_id, username=message.author.display_name, last_interaction=datetime.now(), interaction_count=1, emotional_history=[], conversation_context=[], relationship_level=0.1 ) else: interaction = self.user_interactions[user_id] interaction.last_interaction = datetime.now() interaction.interaction_count += 1 # Gradually build relationship interaction.relationship_level = min( interaction.relationship_level + 0.01, 1.0 ) async def _store_conversation_turn( self, user_id: str, channel_id: str, user_message: str, lyra_response: str, response_info: Dict[str, Any] ): """Store conversation turn for training.""" try: conversation_data = { 'user_id': user_id, 'channel_id': channel_id, 'user_message': user_message, 'lyra_response': lyra_response, 'emotional_state': response_info.get('emotional_state'), 'thoughts': response_info.get('thoughts', []), 'timestamp': datetime.now(), 'response_method': response_info.get('response_generation_method') } # Store in database if available if self.database_manager: await self.database_manager.store_conversation_turn(conversation_data) except Exception as e: logger.error(f"Error storing conversation: {e}") async def _load_user_interactions(self): """Load user interaction history from database.""" try: if self.database_manager: interactions = await self.database_manager.get_user_interactions() for interaction_data in interactions: user_id = interaction_data['user_id'] self.user_interactions[user_id] = UserInteraction( user_id=user_id, username=interaction_data.get('username', 'Unknown'), last_interaction=interaction_data.get('last_interaction', datetime.now()), interaction_count=interaction_data.get('interaction_count', 0), emotional_history=interaction_data.get('emotional_history', []), conversation_context=interaction_data.get('conversation_context', []), relationship_level=interaction_data.get('relationship_level', 0.1) ) except Exception as e: logger.error(f"Error loading user interactions: {e}") @commands.command(name='status') async def status_command(self, ctx): """Show Lyra's current status.""" uptime = datetime.now() - self.start_time lyra_status = self.lyra_model.get_lyra_status() embed = discord.Embed( title="🎭 Lyra Status", color=discord.Color.purple(), timestamp=datetime.now() ) embed.add_field( name="⏱️ Uptime", value=f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds%3600)//60}m", inline=True ) embed.add_field( name="💬 Responses", value=str(self.response_count), inline=True ) embed.add_field( name="👥 Active Users", value=str(len(self.user_interactions)), inline=True ) # Emotional state if 'emotions' in lyra_status: emotion_info = lyra_status['emotions'] embed.add_field( name="😊 Current Mood", value=f"{emotion_info.get('dominant_emotion', 'neutral').title()}", inline=True ) await ctx.send(embed=embed) @commands.command(name='personality') async def personality_command(self, ctx): """Show Lyra's current personality.""" lyra_status = self.lyra_model.get_lyra_status() embed = discord.Embed( title="🧠 Lyra's Personality", color=discord.Color.blue(), timestamp=datetime.now() ) if 'personality' in lyra_status: personality = lyra_status['personality'] # Myers-Briggs type if 'myers_briggs_type' in personality: embed.add_field( name="🏷️ Type", value=personality['myers_briggs_type'], inline=True ) # OCEAN traits if 'ocean_traits' in personality: ocean = personality['ocean_traits'] trait_text = "\n".join([ f"**{trait.title()}**: {value:.1f}/5.0" for trait, value in ocean.items() ]) embed.add_field( name="🌊 OCEAN Traits", value=trait_text, inline=False ) await ctx.send(embed=embed) @commands.command(name='learn') async def manual_learning(self, ctx, feedback: float = None): """Provide manual learning feedback.""" if feedback is None: await ctx.send( "Please provide feedback between 0.0 and 1.0\n" "Example: `!lyra learn 0.8` (for good response)" ) return if not 0.0 <= feedback <= 1.0: await ctx.send("Feedback must be between 0.0 and 1.0") return # Apply feedback to Lyra's systems user_id = str(ctx.author.id) self.lyra_model.evolve_from_feedback( user_feedback=feedback, conversation_success=feedback, user_id=user_id ) # Emotional response to feedback if feedback >= 0.8: response = "Thank you! That positive feedback makes me really happy! 😊" elif feedback >= 0.6: response = "Thanks for the feedback! I'll keep that in mind. 😌" elif feedback >= 0.4: response = "I appreciate the feedback. I'll try to do better. 🤔" else: response = "I understand. I'll work on improving my responses. 😔" await ctx.send(response) async def close(self): """Cleanup when shutting down.""" logger.info("Shutting down Lyra Discord Bot...") # Save user interactions try: if self.database_manager: for user_id, interaction in self.user_interactions.items(): await self.database_manager.update_user_interaction(user_id, interaction) except Exception as e: logger.error(f"Error saving user interactions: {e}") await super().close() async def create_discord_bot( lyra_model: LyraModel, training_pipeline: LyraTrainingPipeline, database_manager: DatabaseManager ) -> LyraDiscordBot: """Create and configure the Discord bot.""" bot = LyraDiscordBot(lyra_model, training_pipeline, database_manager) # Add additional setup here if needed return bot