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:
2025-09-29 16:29:18 -04:00
parent faa23d596e
commit d9c526fa5c
26 changed files with 3624 additions and 39 deletions

587
lyra/discord/bot.py Normal file
View 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