- Added DATABASE_SETUP.md with comprehensive guide for PostgreSQL and Redis installation on Windows - Created .claude/settings.local.json with permission settings for pytest and database fix scripts - Updated .gitignore to exclude .env.backup file - Included database connection test utilities in lyra/database_setup.py - Added environment variable configuration examples for local development
587 lines
20 KiB
Python
587 lines
20 KiB
Python
"""
|
|
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 |