feat: Add database setup guide and local configuration files
- 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
This commit is contained in:
587
lyra/discord/bot.py
Normal file
587
lyra/discord/bot.py
Normal file
@@ -0,0 +1,587 @@
|
||||
"""
|
||||
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
|
Reference in New Issue
Block a user