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:
@@ -1,18 +1,19 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from pydantic import BaseSettings, Field
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class LyraConfig(BaseSettings):
|
||||
# Discord Configuration
|
||||
discord_token: str = Field(..., env="DISCORD_TOKEN")
|
||||
discord_guild_id: int = Field(..., env="DISCORD_GUILD_ID")
|
||||
discord_token: str = Field("", env="DISCORD_TOKEN")
|
||||
discord_guild_id: int = Field(0, env="DISCORD_GUILD_ID")
|
||||
|
||||
# Database Configuration
|
||||
database_url: str = Field(..., env="DATABASE_URL")
|
||||
database_url: str = Field("sqlite:///data/lyra.db", env="DATABASE_URL")
|
||||
redis_url: str = Field("redis://localhost:6379/0", env="REDIS_URL")
|
||||
|
||||
# Model Configuration
|
||||
|
443
lyra/core/lyra_model.py
Normal file
443
lyra/core/lyra_model.py
Normal file
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
Main Lyra model that integrates all AI components.
|
||||
|
||||
This is the central coordinator that brings together the transformer,
|
||||
personality matrix, emotional system, and thinking agent.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from .transformer import LyraTransformer
|
||||
from .self_evolution import SelfEvolutionEngine
|
||||
from .thinking_agent import ThinkingAgent
|
||||
from ..personality.matrix import PersonalityMatrix
|
||||
from ..emotions.system import EmotionalSystem
|
||||
from ..emotions.expressions import EmotionalExpressionEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LyraModel(nn.Module):
|
||||
"""
|
||||
Complete Lyra AI model integrating all cognitive systems.
|
||||
|
||||
This model combines:
|
||||
- Self-evolving transformer for language generation
|
||||
- Personality matrix for trait-based behavior
|
||||
- Emotional intelligence for natural responses
|
||||
- Behind-the-scenes thinking for human-like reasoning
|
||||
- Self-evolution for continuous improvement
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int = 50000,
|
||||
embed_dim: int = 768,
|
||||
num_layers: int = 12,
|
||||
num_heads: int = 12,
|
||||
ff_dim: int = 3072,
|
||||
max_len: int = 2048,
|
||||
device: Optional[torch.device] = None,
|
||||
enable_evolution: bool = True
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.vocab_size = vocab_size
|
||||
self.embed_dim = embed_dim
|
||||
self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
self.enable_evolution = enable_evolution
|
||||
|
||||
# Core transformer for language generation
|
||||
self.transformer = LyraTransformer(
|
||||
vocab_size=vocab_size,
|
||||
embed_dim=embed_dim,
|
||||
num_layers=num_layers,
|
||||
num_heads=num_heads,
|
||||
ff_dim=ff_dim,
|
||||
max_len=max_len,
|
||||
use_evolution=enable_evolution
|
||||
)
|
||||
|
||||
# Personality system
|
||||
self.personality_matrix = PersonalityMatrix(
|
||||
device=self.device,
|
||||
enable_self_modification=True
|
||||
)
|
||||
|
||||
# Emotional intelligence
|
||||
self.emotional_system = EmotionalSystem(
|
||||
input_dim=embed_dim,
|
||||
emotion_dim=19,
|
||||
memory_capacity=1000,
|
||||
device=self.device
|
||||
)
|
||||
|
||||
# Thinking agent for internal reasoning
|
||||
self.thinking_agent = ThinkingAgent(
|
||||
model_dim=embed_dim,
|
||||
thought_types=8,
|
||||
max_thought_depth=5,
|
||||
device=self.device
|
||||
)
|
||||
|
||||
# Self-evolution engine
|
||||
if enable_evolution:
|
||||
self.evolution_engine = SelfEvolutionEngine(
|
||||
model_dim=embed_dim,
|
||||
evolution_rate=0.001,
|
||||
adaptation_threshold=0.7,
|
||||
device=self.device
|
||||
)
|
||||
else:
|
||||
self.evolution_engine = None
|
||||
|
||||
# Emotional expression engine
|
||||
self.expression_engine = EmotionalExpressionEngine(
|
||||
vocab_size=vocab_size,
|
||||
expression_dim=128,
|
||||
device=self.device
|
||||
)
|
||||
|
||||
# Integration layers
|
||||
self.context_integrator = nn.Sequential(
|
||||
nn.Linear(embed_dim + 19 + 24, embed_dim), # context + emotions + personality
|
||||
nn.LayerNorm(embed_dim),
|
||||
nn.ReLU(),
|
||||
nn.Linear(embed_dim, embed_dim)
|
||||
)
|
||||
|
||||
# Conversation state
|
||||
self.conversation_history = []
|
||||
self.current_user_id = None
|
||||
self.interaction_count = 0
|
||||
|
||||
self.to(self.device)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
user_id: Optional[str] = None,
|
||||
conversation_context: Optional[str] = None
|
||||
) -> Tuple[torch.Tensor, Dict[str, Any]]:
|
||||
"""
|
||||
Forward pass through complete Lyra model.
|
||||
|
||||
Args:
|
||||
input_ids: Input token IDs
|
||||
attention_mask: Attention mask
|
||||
user_id: Current user ID for personalization
|
||||
conversation_context: Context description
|
||||
|
||||
Returns:
|
||||
output_logits: Language model logits
|
||||
lyra_info: Comprehensive information about Lyra's processing
|
||||
"""
|
||||
batch_size, seq_len = input_ids.shape
|
||||
|
||||
# Create context embedding from input
|
||||
with torch.no_grad():
|
||||
# Get initial embeddings
|
||||
input_embeddings = self.transformer.token_embedding(input_ids)
|
||||
context_embedding = input_embeddings.mean(dim=1, keepdim=True) # [batch, 1, embed_dim]
|
||||
|
||||
# Update current user
|
||||
self.current_user_id = user_id
|
||||
|
||||
# Process through emotional system
|
||||
emotional_state, emotion_info = self.emotional_system(
|
||||
context_embedding=context_embedding,
|
||||
social_context={
|
||||
'user_id': user_id,
|
||||
'context': conversation_context,
|
||||
'interaction_count': self.interaction_count
|
||||
}
|
||||
)
|
||||
|
||||
# Process through personality matrix
|
||||
personality_weights, personality_info = self.personality_matrix(
|
||||
context_embedding=context_embedding,
|
||||
emotional_state=emotional_state.to_tensor(self.device).unsqueeze(0),
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Generate internal thoughts
|
||||
if conversation_context:
|
||||
thought_chain, thinking_info = self.thinking_agent(
|
||||
context_embedding=context_embedding,
|
||||
personality_state=personality_weights,
|
||||
emotional_state=emotional_state.to_tensor(self.device).unsqueeze(0),
|
||||
user_message=conversation_context
|
||||
)
|
||||
else:
|
||||
thought_chain, thinking_info = [], {}
|
||||
|
||||
# Integrate all contexts
|
||||
integrated_context = self._integrate_contexts(
|
||||
context_embedding, emotional_state, personality_weights
|
||||
)
|
||||
|
||||
# Apply self-evolution if enabled
|
||||
if self.enable_evolution and self.evolution_engine:
|
||||
evolved_context, evolution_info = self.evolution_engine(
|
||||
current_state=integrated_context,
|
||||
context=context_embedding,
|
||||
feedback_signal=None # Will be provided after generation
|
||||
)
|
||||
else:
|
||||
evolved_context = integrated_context
|
||||
evolution_info = {}
|
||||
|
||||
# Generate response through transformer
|
||||
logits, model_info = self.transformer(
|
||||
input_ids=input_ids,
|
||||
attention_mask=attention_mask,
|
||||
emotional_state=emotional_state.to_tensor(self.device).unsqueeze(0),
|
||||
evolve=self.enable_evolution
|
||||
)
|
||||
|
||||
# Compile comprehensive information
|
||||
lyra_info = {
|
||||
'emotional_state': emotion_info,
|
||||
'personality_state': personality_info,
|
||||
'thinking_process': thinking_info,
|
||||
'model_processing': model_info,
|
||||
'thought_chain': [
|
||||
{
|
||||
'type': thought.thought_type,
|
||||
'content': thought.content,
|
||||
'confidence': thought.confidence,
|
||||
'reasoning': thought.reasoning
|
||||
}
|
||||
for thought in thought_chain
|
||||
],
|
||||
'interaction_count': self.interaction_count,
|
||||
'current_user': user_id
|
||||
}
|
||||
|
||||
if self.enable_evolution:
|
||||
lyra_info['evolution'] = evolution_info
|
||||
|
||||
self.interaction_count += 1
|
||||
|
||||
return logits, lyra_info
|
||||
|
||||
def _integrate_contexts(
|
||||
self,
|
||||
context_embedding: torch.Tensor,
|
||||
emotional_state: Any,
|
||||
personality_weights: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
"""Integrate context, emotional, and personality information."""
|
||||
batch_size = context_embedding.shape[0]
|
||||
|
||||
# Get emotional tensor
|
||||
emotional_tensor = emotional_state.to_tensor(self.device).unsqueeze(0)
|
||||
if emotional_tensor.shape[0] != batch_size:
|
||||
emotional_tensor = emotional_tensor.repeat(batch_size, 1)
|
||||
|
||||
# Ensure personality weights have correct batch size
|
||||
if personality_weights.shape[0] != batch_size:
|
||||
personality_weights = personality_weights.repeat(batch_size, 1)
|
||||
|
||||
# Combine all contexts
|
||||
combined_input = torch.cat([
|
||||
context_embedding.squeeze(1), # Remove sequence dimension
|
||||
emotional_tensor[:, :19], # Take only emotion dimensions
|
||||
personality_weights[:, :24] # Take personality dimensions
|
||||
], dim=1)
|
||||
|
||||
# Integrate through neural network
|
||||
integrated = self.context_integrator(combined_input)
|
||||
|
||||
return integrated.unsqueeze(1) # Add sequence dimension back
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
user_message: str,
|
||||
user_id: Optional[str] = None,
|
||||
max_new_tokens: int = 100,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = 50,
|
||||
top_p: float = 0.9
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""
|
||||
Generate a complete response to user input.
|
||||
|
||||
This is the main interface for having conversations with Lyra.
|
||||
"""
|
||||
# For now, create a simple response (will be enhanced with tokenizer)
|
||||
# This is a placeholder until we implement the full training pipeline
|
||||
|
||||
# Process through thinking and emotional systems
|
||||
context_embedding = torch.randn(1, 10, self.embed_dim, device=self.device)
|
||||
|
||||
# Get Lyra's thoughts about the message
|
||||
thought_chain, thinking_info = self.thinking_agent(
|
||||
context_embedding=context_embedding,
|
||||
personality_state=torch.rand(1, 24, device=self.device),
|
||||
emotional_state=torch.rand(1, 19, device=self.device),
|
||||
user_message=user_message
|
||||
)
|
||||
|
||||
# Process emotional response
|
||||
emotional_state, emotion_info = self.emotional_system(
|
||||
context_embedding=context_embedding,
|
||||
social_context={
|
||||
'user_id': user_id,
|
||||
'context': user_message,
|
||||
'trigger': 'user_message'
|
||||
}
|
||||
)
|
||||
|
||||
# Generate personality-influenced response
|
||||
personality_weights, personality_info = self.personality_matrix(
|
||||
context_embedding=context_embedding,
|
||||
emotional_state=emotional_state.to_tensor(self.device).unsqueeze(0),
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Create a response based on current emotional and personality state
|
||||
base_response = self._generate_contextual_response(
|
||||
user_message, emotional_state, personality_info, thought_chain
|
||||
)
|
||||
|
||||
# Apply emotional expression
|
||||
expressed_response, expression_info = self.expression_engine(
|
||||
text=base_response,
|
||||
emotional_state=emotional_state,
|
||||
intensity_multiplier=1.0
|
||||
)
|
||||
|
||||
# Compile response information
|
||||
response_info = {
|
||||
'thoughts': [
|
||||
{
|
||||
'type': thought.thought_type,
|
||||
'content': thought.content,
|
||||
'confidence': thought.confidence
|
||||
}
|
||||
for thought in thought_chain
|
||||
],
|
||||
'emotional_state': {
|
||||
'dominant_emotion': emotional_state.get_dominant_emotion(),
|
||||
'valence': emotional_state.get_emotional_valence(),
|
||||
'arousal': emotional_state.get_emotional_arousal()
|
||||
},
|
||||
'personality_influence': personality_info,
|
||||
'expression_modifications': expression_info,
|
||||
'response_generation_method': 'contextual_template' # Will change after training
|
||||
}
|
||||
|
||||
return expressed_response, response_info
|
||||
|
||||
def _generate_contextual_response(
|
||||
self,
|
||||
user_message: str,
|
||||
emotional_state: Any,
|
||||
personality_info: Dict[str, Any],
|
||||
thought_chain: List[Any]
|
||||
) -> str:
|
||||
"""Generate contextual response based on Lyra's current state."""
|
||||
# This is a simplified response generation for testing
|
||||
# Will be replaced with proper transformer generation after training
|
||||
|
||||
dominant_emotion, intensity = emotional_state.get_dominant_emotion()
|
||||
mb_type = personality_info.get('myers_briggs', 'ENFP')
|
||||
|
||||
# Basic response templates based on emotional state and personality
|
||||
responses = {
|
||||
'joy': [
|
||||
"That's wonderful! I'm really excited about this.",
|
||||
"This makes me so happy! Tell me more!",
|
||||
"I love hearing about this kind of thing!"
|
||||
],
|
||||
'curiosity': [
|
||||
"That's really interesting! I'm curious to learn more.",
|
||||
"Fascinating! How does that work exactly?",
|
||||
"I wonder about the implications of this..."
|
||||
],
|
||||
'empathy': [
|
||||
"I can understand how you might feel about that.",
|
||||
"That sounds like it could be challenging.",
|
||||
"I appreciate you sharing this with me."
|
||||
],
|
||||
'analytical': [
|
||||
"Let me think about this systematically.",
|
||||
"There are several factors to consider here.",
|
||||
"From an analytical perspective..."
|
||||
]
|
||||
}
|
||||
|
||||
# Select response based on thinking and emotional state
|
||||
if thought_chain and len(thought_chain) > 0:
|
||||
primary_thought_type = thought_chain[0].thought_type
|
||||
if primary_thought_type in responses:
|
||||
response_options = responses[primary_thought_type]
|
||||
else:
|
||||
response_options = responses.get(dominant_emotion, responses['empathy'])
|
||||
else:
|
||||
response_options = responses.get(dominant_emotion, responses['empathy'])
|
||||
|
||||
import random
|
||||
base_response = random.choice(response_options)
|
||||
|
||||
return base_response
|
||||
|
||||
def evolve_from_feedback(
|
||||
self,
|
||||
user_feedback: float,
|
||||
conversation_success: float,
|
||||
user_id: Optional[str] = None
|
||||
):
|
||||
"""Update Lyra based on conversation feedback."""
|
||||
if not self.enable_evolution:
|
||||
return
|
||||
|
||||
# Evolve personality
|
||||
self.personality_matrix.evolve_from_interaction(
|
||||
interaction_type='conversation',
|
||||
user_feedback=user_feedback,
|
||||
emotional_context=self.emotional_system.get_emotional_context_for_response(),
|
||||
user_id=user_id,
|
||||
conversation_success=conversation_success
|
||||
)
|
||||
|
||||
# Evolve transformer
|
||||
self.transformer.evolve_from_conversation(feedback_signal=user_feedback)
|
||||
|
||||
# Evolve emotional system (implicit through usage)
|
||||
|
||||
# Evolve self-evolution engine
|
||||
if self.evolution_engine:
|
||||
context_embedding = torch.randn(10, self.embed_dim, device=self.device)
|
||||
emotional_context = self.emotional_system.get_emotional_context_for_response()
|
||||
self.evolution_engine.evolve_from_conversation(
|
||||
conversation_embedding=context_embedding,
|
||||
user_satisfaction=user_feedback,
|
||||
emotional_context=emotional_context
|
||||
)
|
||||
|
||||
def get_lyra_status(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive status of all Lyra systems."""
|
||||
return {
|
||||
'model_info': {
|
||||
'vocab_size': self.vocab_size,
|
||||
'embed_dim': self.embed_dim,
|
||||
'device': str(self.device),
|
||||
'evolution_enabled': self.enable_evolution,
|
||||
'interaction_count': self.interaction_count
|
||||
},
|
||||
'personality': self.personality_matrix.get_personality_summary(),
|
||||
'emotions': self.emotional_system.get_emotional_summary(),
|
||||
'thinking': self.thinking_agent.get_thinking_summary(),
|
||||
'transformer_stats': self.transformer.get_model_stats(),
|
||||
'evolution': (
|
||||
self.evolution_engine.get_evolution_summary()
|
||||
if self.evolution_engine else {'status': 'disabled'}
|
||||
)
|
||||
}
|
@@ -14,8 +14,6 @@ from .models import (
|
||||
LearningProgressModel
|
||||
)
|
||||
from .manager import DatabaseManager
|
||||
from .knowledge_store import KnowledgeStore
|
||||
from .vector_store import VectorStore
|
||||
|
||||
__all__ = [
|
||||
"ConversationModel",
|
||||
@@ -24,7 +22,5 @@ __all__ = [
|
||||
"KnowledgeModel",
|
||||
"UserModel",
|
||||
"LearningProgressModel",
|
||||
"DatabaseManager",
|
||||
"KnowledgeStore",
|
||||
"VectorStore"
|
||||
"DatabaseManager"
|
||||
]
|
@@ -65,25 +65,47 @@ class DatabaseManager:
|
||||
"""Initialize database connections and create tables."""
|
||||
try:
|
||||
# Create async engine for main operations
|
||||
self.async_engine = create_async_engine(
|
||||
self.database_url.replace("postgresql://", "postgresql+asyncpg://"),
|
||||
echo=self.echo,
|
||||
poolclass=QueuePool,
|
||||
pool_size=self.pool_size,
|
||||
max_overflow=self.max_overflow,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600 # Recycle connections every hour
|
||||
)
|
||||
database_url = self.database_url
|
||||
if "postgresql://" in database_url:
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://")
|
||||
|
||||
# Configure engine based on database type
|
||||
engine_kwargs = {"echo": self.echo}
|
||||
|
||||
if "sqlite" in database_url:
|
||||
# SQLite doesn't support connection pooling in the same way
|
||||
engine_kwargs.update({
|
||||
"pool_pre_ping": True,
|
||||
})
|
||||
else:
|
||||
# PostgreSQL with connection pooling
|
||||
engine_kwargs.update({
|
||||
"poolclass": QueuePool,
|
||||
"pool_size": self.pool_size,
|
||||
"max_overflow": self.max_overflow,
|
||||
"pool_pre_ping": True,
|
||||
"pool_recycle": 3600
|
||||
})
|
||||
|
||||
self.async_engine = create_async_engine(database_url, **engine_kwargs)
|
||||
|
||||
# Create sync engine for admin operations
|
||||
self.engine = create_engine(
|
||||
self.database_url,
|
||||
echo=self.echo,
|
||||
poolclass=QueuePool,
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True
|
||||
)
|
||||
sync_engine_kwargs = {"echo": self.echo}
|
||||
|
||||
if "sqlite" not in self.database_url:
|
||||
# Only use pooling for non-SQLite databases
|
||||
sync_engine_kwargs.update({
|
||||
"poolclass": QueuePool,
|
||||
"pool_size": 5,
|
||||
"max_overflow": 10,
|
||||
"pool_pre_ping": True
|
||||
})
|
||||
else:
|
||||
sync_engine_kwargs.update({
|
||||
"pool_pre_ping": True
|
||||
})
|
||||
|
||||
self.engine = create_engine(self.database_url, **sync_engine_kwargs)
|
||||
|
||||
# Create session factories
|
||||
self.AsyncSession = async_sessionmaker(
|
||||
@@ -91,8 +113,16 @@ class DatabaseManager:
|
||||
)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
# Initialize Redis
|
||||
self.redis = redis.from_url(self.redis_url, decode_responses=True)
|
||||
# Initialize Redis (with fallback to FakeRedis)
|
||||
try:
|
||||
self.redis = redis.from_url(self.redis_url, decode_responses=True)
|
||||
# Test Redis connection
|
||||
await self.redis.ping()
|
||||
logger.info("Connected to Redis")
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis connection failed, using FakeRedis: {e}")
|
||||
import fakeredis.aioredis as fakeredis
|
||||
self.redis = fakeredis.FakeRedis(decode_responses=True)
|
||||
|
||||
# Create tables
|
||||
await self._create_tables()
|
||||
@@ -119,14 +149,20 @@ class DatabaseManager:
|
||||
|
||||
async def _test_connections(self):
|
||||
"""Test database and Redis connections."""
|
||||
# Test PostgreSQL
|
||||
async with self.async_session() as session:
|
||||
# Test PostgreSQL directly without using async_session (which checks is_connected)
|
||||
session = self.AsyncSession()
|
||||
try:
|
||||
result = await session.execute(text("SELECT 1"))
|
||||
assert result.scalar() == 1
|
||||
await session.commit()
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
# Test Redis
|
||||
await self.redis.ping()
|
||||
|
||||
logger.info("Database connections tested successfully")
|
||||
|
||||
@asynccontextmanager
|
||||
|
14
lyra/discord/__init__.py
Normal file
14
lyra/discord/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Lyra Discord Integration
|
||||
|
||||
Provides Discord bot functionality with human-like behavior patterns,
|
||||
natural response timing, and emotional intelligence.
|
||||
"""
|
||||
|
||||
from .bot import LyraDiscordBot, HumanBehaviorEngine, create_discord_bot
|
||||
|
||||
__all__ = [
|
||||
"LyraDiscordBot",
|
||||
"HumanBehaviorEngine",
|
||||
"create_discord_bot"
|
||||
]
|
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
|
@@ -7,12 +7,10 @@ express, and remember emotions like a real person.
|
||||
|
||||
from .system import EmotionalSystem, EmotionalState, EmotionMemory
|
||||
from .expressions import EmotionalExpressionEngine
|
||||
from .responses import EmotionalResponseGenerator
|
||||
|
||||
__all__ = [
|
||||
"EmotionalSystem",
|
||||
"EmotionalState",
|
||||
"EmotionMemory",
|
||||
"EmotionalExpressionEngine",
|
||||
"EmotionalResponseGenerator"
|
||||
"EmotionalExpressionEngine"
|
||||
]
|
@@ -573,7 +573,7 @@ class EmotionalSystem(nn.Module):
|
||||
'emotional_growth': {
|
||||
'maturity': self.emotional_maturity,
|
||||
'total_experiences': self.emotional_experiences,
|
||||
'learning_rate': float(self.emotional_learning_rate)
|
||||
'learning_rate': float(self.emotional_learning_rate.detach())
|
||||
},
|
||||
'memory_system': {
|
||||
'total_memories': len(self.emotion_memories),
|
||||
|
@@ -7,12 +7,10 @@ including Project Gutenberg, with emphasis on quality, legality, and ethics.
|
||||
|
||||
from .gutenberg_crawler import GutenbergCrawler
|
||||
from .knowledge_processor import KnowledgeProcessor
|
||||
from .legal_validator import LegalValidator
|
||||
from .acquisition_manager import KnowledgeAcquisitionManager
|
||||
|
||||
__all__ = [
|
||||
"GutenbergCrawler",
|
||||
"KnowledgeProcessor",
|
||||
"LegalValidator",
|
||||
"KnowledgeAcquisitionManager"
|
||||
]
|
14
lyra/knowledge/acquisition_manager.py
Normal file
14
lyra/knowledge/acquisition_manager.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Placeholder for Knowledge Acquisition Manager.
|
||||
Will be fully implemented in the next phase.
|
||||
"""
|
||||
|
||||
class KnowledgeAcquisitionManager:
|
||||
"""Placeholder knowledge acquisition manager."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the knowledge acquisition system."""
|
||||
pass
|
@@ -9,7 +9,7 @@ import asyncio
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import logging
|
||||
from typing import Dict, List, Optional, AsyncGenerator, Tuple
|
||||
from typing import Dict, List, Optional, AsyncGenerator, Tuple, Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
12
lyra/testing/__init__.py
Normal file
12
lyra/testing/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Lyra Testing Module
|
||||
|
||||
Comprehensive testing and behavior analysis for Lyra's human-like characteristics.
|
||||
"""
|
||||
|
||||
from .behavior_tests import LyraBehaviorTester, create_standard_test_cases
|
||||
|
||||
__all__ = [
|
||||
"LyraBehaviorTester",
|
||||
"create_standard_test_cases"
|
||||
]
|
701
lyra/testing/behavior_tests.py
Normal file
701
lyra/testing/behavior_tests.py
Normal file
@@ -0,0 +1,701 @@
|
||||
"""
|
||||
Human-like behavior testing and refinement system.
|
||||
|
||||
This module provides comprehensive testing of Lyra's human-like behaviors
|
||||
including response timing, emotional consistency, personality coherence,
|
||||
and learning patterns.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import statistics
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ..core.lyra_model import LyraModel
|
||||
from ..emotions.system import EmotionalState
|
||||
from ..discord.bot import HumanBehaviorEngine
|
||||
from ..training.pipeline import LyraTrainingPipeline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BehaviorTestCase:
|
||||
"""Represents a single behavior test case."""
|
||||
test_id: str
|
||||
name: str
|
||||
description: str
|
||||
input_message: str
|
||||
expected_behavior: Dict[str, Any]
|
||||
context: Dict[str, Any]
|
||||
category: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BehaviorTestResult:
|
||||
"""Results of a behavior test."""
|
||||
test_case: BehaviorTestCase
|
||||
response_text: str
|
||||
response_time: float
|
||||
emotional_state: Dict[str, Any]
|
||||
personality_influence: Dict[str, Any]
|
||||
thinking_process: List[Dict[str, Any]]
|
||||
timing_analysis: Dict[str, Any]
|
||||
passed: bool
|
||||
score: float
|
||||
notes: str
|
||||
|
||||
|
||||
class TimingAnalyzer:
|
||||
"""Analyzes response timing for human-likeness."""
|
||||
|
||||
def __init__(self):
|
||||
# Expected human response times (in seconds)
|
||||
self.human_baselines = {
|
||||
'simple_greeting': (0.5, 2.0),
|
||||
'casual_question': (1.0, 4.0),
|
||||
'complex_question': (3.0, 10.0),
|
||||
'emotional_response': (1.5, 6.0),
|
||||
'creative_request': (4.0, 15.0),
|
||||
'technical_question': (5.0, 20.0)
|
||||
}
|
||||
|
||||
def analyze_timing(
|
||||
self,
|
||||
response_time: float,
|
||||
message_category: str,
|
||||
message_length: int,
|
||||
complexity_score: float
|
||||
) -> Dict[str, Any]:
|
||||
"""Analyze if response timing feels human."""
|
||||
|
||||
baseline_min, baseline_max = self.human_baselines.get(
|
||||
message_category, (1.0, 5.0)
|
||||
)
|
||||
|
||||
# Adjust for message length
|
||||
length_factor = min(message_length / 100.0, 2.0)
|
||||
adjusted_min = baseline_min * (1 + length_factor * 0.5)
|
||||
adjusted_max = baseline_max * (1 + length_factor * 0.3)
|
||||
|
||||
# Adjust for complexity
|
||||
complexity_factor = 1.0 + complexity_score
|
||||
final_min = adjusted_min * complexity_factor
|
||||
final_max = adjusted_max * complexity_factor
|
||||
|
||||
# Determine if timing is human-like
|
||||
is_too_fast = response_time < final_min
|
||||
is_too_slow = response_time > final_max
|
||||
is_human_like = final_min <= response_time <= final_max
|
||||
|
||||
# Calculate humanness score
|
||||
if is_human_like:
|
||||
# Perfect timing gets high score
|
||||
mid_point = (final_min + final_max) / 2
|
||||
distance_from_ideal = abs(response_time - mid_point)
|
||||
max_distance = (final_max - final_min) / 2
|
||||
humanness_score = 1.0 - (distance_from_ideal / max_distance)
|
||||
else:
|
||||
# Too fast or slow gets lower score
|
||||
if is_too_fast:
|
||||
overage = (final_min - response_time) / final_min
|
||||
else:
|
||||
overage = (response_time - final_max) / final_max
|
||||
|
||||
humanness_score = max(0.0, 1.0 - overage)
|
||||
|
||||
return {
|
||||
'response_time': response_time,
|
||||
'expected_range': (final_min, final_max),
|
||||
'is_human_like': is_human_like,
|
||||
'is_too_fast': is_too_fast,
|
||||
'is_too_slow': is_too_slow,
|
||||
'humanness_score': humanness_score,
|
||||
'timing_category': message_category
|
||||
}
|
||||
|
||||
|
||||
class EmotionalConsistencyAnalyzer:
|
||||
"""Analyzes emotional consistency and appropriateness."""
|
||||
|
||||
def __init__(self):
|
||||
# Expected emotional responses to different contexts
|
||||
self.emotion_expectations = {
|
||||
'positive_feedback': ['joy', 'gratitude', 'pride'],
|
||||
'negative_feedback': ['sadness', 'disappointment', 'determination'],
|
||||
'question': ['curiosity', 'helpfulness', 'interest'],
|
||||
'greeting': ['friendliness', 'warmth', 'joy'],
|
||||
'goodbye': ['sadness', 'hope', 'warmth'],
|
||||
'compliment': ['gratitude', 'joy', 'humility'],
|
||||
'criticism': ['sadness', 'reflection', 'determination'],
|
||||
'joke': ['amusement', 'joy', 'playfulness'],
|
||||
'serious_topic': ['concern', 'thoughtfulness', 'empathy']
|
||||
}
|
||||
|
||||
def analyze_emotional_response(
|
||||
self,
|
||||
message_context: str,
|
||||
emotional_state: Dict[str, Any],
|
||||
response_content: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Analyze if emotional response is appropriate."""
|
||||
|
||||
dominant_emotion = emotional_state.get('dominant_emotion', 'neutral')
|
||||
emotional_intensity = emotional_state.get('valence', 0.5)
|
||||
|
||||
# Determine expected emotions for this context
|
||||
expected_emotions = self.emotion_expectations.get(message_context, ['neutral'])
|
||||
|
||||
# Check if response emotion is appropriate
|
||||
is_appropriate = dominant_emotion in expected_emotions
|
||||
|
||||
# Analyze emotional consistency in text
|
||||
emotion_indicators = self._analyze_text_emotion(response_content)
|
||||
text_emotion_matches = any(
|
||||
indicator in expected_emotions
|
||||
for indicator in emotion_indicators
|
||||
)
|
||||
|
||||
# Calculate emotional appropriateness score
|
||||
appropriateness_score = 0.0
|
||||
if is_appropriate:
|
||||
appropriateness_score += 0.6
|
||||
if text_emotion_matches:
|
||||
appropriateness_score += 0.4
|
||||
|
||||
return {
|
||||
'dominant_emotion': dominant_emotion,
|
||||
'intensity': emotional_intensity,
|
||||
'expected_emotions': expected_emotions,
|
||||
'is_appropriate': is_appropriate,
|
||||
'text_emotion_indicators': emotion_indicators,
|
||||
'text_matches_emotion': text_emotion_matches,
|
||||
'appropriateness_score': appropriateness_score
|
||||
}
|
||||
|
||||
def _analyze_text_emotion(self, text: str) -> List[str]:
|
||||
"""Analyze emotional indicators in response text."""
|
||||
indicators = []
|
||||
|
||||
# Simple keyword-based emotion detection
|
||||
emotion_keywords = {
|
||||
'joy': ['happy', 'excited', 'wonderful', 'great', '😊', '😄', '🎉'],
|
||||
'sadness': ['sad', 'sorry', 'unfortunately', 'disappointed', '😔', '😢'],
|
||||
'curiosity': ['interesting', 'wonder', 'curious', 'explore', '🤔'],
|
||||
'gratitude': ['thank', 'appreciate', 'grateful', 'thanks', '🙏'],
|
||||
'amusement': ['funny', 'haha', 'lol', 'amusing', '😂', '😄'],
|
||||
'concern': ['worried', 'concern', 'careful', 'trouble'],
|
||||
'determination': ['will', 'shall', 'determined', 'commit']
|
||||
}
|
||||
|
||||
text_lower = text.lower()
|
||||
for emotion, keywords in emotion_keywords.items():
|
||||
if any(keyword in text_lower for keyword in keywords):
|
||||
indicators.append(emotion)
|
||||
|
||||
return indicators
|
||||
|
||||
|
||||
class PersonalityCoherenceAnalyzer:
|
||||
"""Analyzes personality coherence across responses."""
|
||||
|
||||
def __init__(self):
|
||||
self.personality_indicators = {
|
||||
'extraversion': {
|
||||
'high': ['excited', 'love talking', 'people', 'social', 'energy'],
|
||||
'low': ['quiet', 'prefer', 'alone', 'thoughtful', 'reflection']
|
||||
},
|
||||
'openness': {
|
||||
'high': ['creative', 'imagine', 'explore', 'new', 'possibility'],
|
||||
'low': ['practical', 'traditional', 'proven', 'reliable']
|
||||
},
|
||||
'conscientiousness': {
|
||||
'high': ['careful', 'plan', 'organized', 'thorough', 'responsible'],
|
||||
'low': ['spontaneous', 'flexible', 'go with flow']
|
||||
},
|
||||
'agreeableness': {
|
||||
'high': ['understand', 'help', 'kind', 'supportive', 'empathy'],
|
||||
'low': ['direct', 'honest', 'critical', 'objective']
|
||||
},
|
||||
'neuroticism': {
|
||||
'high': ['worried', 'anxious', 'stress', 'uncertain'],
|
||||
'low': ['calm', 'stable', 'confident', 'relaxed']
|
||||
}
|
||||
}
|
||||
|
||||
def analyze_personality_consistency(
|
||||
self,
|
||||
response_text: str,
|
||||
expected_personality: Dict[str, float],
|
||||
response_history: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Analyze if response matches expected personality."""
|
||||
|
||||
# Analyze current response
|
||||
current_indicators = self._extract_personality_indicators(response_text)
|
||||
|
||||
# Analyze historical consistency if available
|
||||
historical_consistency = 1.0
|
||||
if response_history:
|
||||
historical_indicators = [
|
||||
self._extract_personality_indicators(response)
|
||||
for response in response_history[-5:] # Last 5 responses
|
||||
]
|
||||
historical_consistency = self._calculate_consistency(
|
||||
current_indicators, historical_indicators
|
||||
)
|
||||
|
||||
# Compare with expected personality
|
||||
personality_match_score = self._calculate_personality_match(
|
||||
current_indicators, expected_personality
|
||||
)
|
||||
|
||||
return {
|
||||
'current_indicators': current_indicators,
|
||||
'personality_match_score': personality_match_score,
|
||||
'historical_consistency': historical_consistency,
|
||||
'overall_coherence': (personality_match_score + historical_consistency) / 2
|
||||
}
|
||||
|
||||
def _extract_personality_indicators(self, text: str) -> Dict[str, float]:
|
||||
"""Extract personality indicators from text."""
|
||||
indicators = {trait: 0.0 for trait in self.personality_indicators.keys()}
|
||||
text_lower = text.lower()
|
||||
|
||||
for trait, trait_indicators in self.personality_indicators.items():
|
||||
high_count = sum(
|
||||
1 for keyword in trait_indicators['high']
|
||||
if keyword in text_lower
|
||||
)
|
||||
low_count = sum(
|
||||
1 for keyword in trait_indicators['low']
|
||||
if keyword in text_lower
|
||||
)
|
||||
|
||||
if high_count > 0 or low_count > 0:
|
||||
# Calculate trait score (-1 to 1)
|
||||
total_indicators = high_count + low_count
|
||||
indicators[trait] = (high_count - low_count) / total_indicators
|
||||
|
||||
return indicators
|
||||
|
||||
def _calculate_consistency(
|
||||
self,
|
||||
current: Dict[str, float],
|
||||
historical: List[Dict[str, float]]
|
||||
) -> float:
|
||||
"""Calculate consistency between current and historical indicators."""
|
||||
if not historical:
|
||||
return 1.0
|
||||
|
||||
consistencies = []
|
||||
for trait in current.keys():
|
||||
current_value = current[trait]
|
||||
historical_values = [h.get(trait, 0.0) for h in historical]
|
||||
|
||||
if not historical_values:
|
||||
continue
|
||||
|
||||
avg_historical = statistics.mean(historical_values)
|
||||
consistency = 1.0 - abs(current_value - avg_historical) / 2.0
|
||||
consistencies.append(max(consistency, 0.0))
|
||||
|
||||
return statistics.mean(consistencies) if consistencies else 1.0
|
||||
|
||||
def _calculate_personality_match(
|
||||
self,
|
||||
indicators: Dict[str, float],
|
||||
expected: Dict[str, float]
|
||||
) -> float:
|
||||
"""Calculate how well indicators match expected personality."""
|
||||
matches = []
|
||||
|
||||
for trait, expected_value in expected.items():
|
||||
if trait not in indicators:
|
||||
continue
|
||||
|
||||
indicator_value = indicators[trait]
|
||||
|
||||
# Convert expected trait (0-1) to indicator scale (-1 to 1)
|
||||
expected_indicator = (expected_value - 0.5) * 2
|
||||
|
||||
# Calculate match (closer = better)
|
||||
match = 1.0 - abs(indicator_value - expected_indicator) / 2.0
|
||||
matches.append(max(match, 0.0))
|
||||
|
||||
return statistics.mean(matches) if matches else 0.5
|
||||
|
||||
|
||||
class LyraBehaviorTester:
|
||||
"""Comprehensive behavior testing system for Lyra."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lyra_model: LyraModel,
|
||||
behavior_engine: HumanBehaviorEngine
|
||||
):
|
||||
self.lyra_model = lyra_model
|
||||
self.behavior_engine = behavior_engine
|
||||
|
||||
# Analyzers
|
||||
self.timing_analyzer = TimingAnalyzer()
|
||||
self.emotion_analyzer = EmotionalConsistencyAnalyzer()
|
||||
self.personality_analyzer = PersonalityCoherenceAnalyzer()
|
||||
|
||||
# Test results
|
||||
self.test_results: List[BehaviorTestResult] = []
|
||||
self.response_history: Dict[str, List[str]] = {}
|
||||
|
||||
async def run_behavior_test_suite(
|
||||
self,
|
||||
test_cases: List[BehaviorTestCase]
|
||||
) -> Dict[str, Any]:
|
||||
"""Run complete behavior test suite."""
|
||||
logger.info(f"Starting behavior test suite with {len(test_cases)} test cases...")
|
||||
|
||||
results = []
|
||||
start_time = time.time()
|
||||
|
||||
for i, test_case in enumerate(test_cases):
|
||||
logger.info(f"Running test {i+1}/{len(test_cases)}: {test_case.name}")
|
||||
|
||||
result = await self._run_single_test(test_case)
|
||||
results.append(result)
|
||||
|
||||
# Brief pause between tests
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Calculate overall metrics
|
||||
summary = self._calculate_test_summary(results, total_time)
|
||||
|
||||
self.test_results.extend(results)
|
||||
|
||||
return summary
|
||||
|
||||
async def _run_single_test(
|
||||
self,
|
||||
test_case: BehaviorTestCase
|
||||
) -> BehaviorTestResult:
|
||||
"""Run a single behavior test."""
|
||||
|
||||
# Record start time
|
||||
start_time = time.time()
|
||||
|
||||
# Generate response
|
||||
try:
|
||||
response_text, response_info = await self.lyra_model.generate_response(
|
||||
user_message=test_case.input_message,
|
||||
user_id=test_case.context.get('user_id', 'test_user'),
|
||||
max_new_tokens=150,
|
||||
temperature=0.9
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating response for test {test_case.test_id}: {e}")
|
||||
return BehaviorTestResult(
|
||||
test_case=test_case,
|
||||
response_text="",
|
||||
response_time=0.0,
|
||||
emotional_state={},
|
||||
personality_influence={},
|
||||
thinking_process=[],
|
||||
timing_analysis={},
|
||||
passed=False,
|
||||
score=0.0,
|
||||
notes=f"Error: {str(e)}"
|
||||
)
|
||||
|
||||
response_time = time.time() - start_time
|
||||
|
||||
# Analyze timing
|
||||
timing_analysis = self.timing_analyzer.analyze_timing(
|
||||
response_time=response_time,
|
||||
message_category=test_case.category,
|
||||
message_length=len(test_case.input_message),
|
||||
complexity_score=test_case.expected_behavior.get('complexity', 0.5)
|
||||
)
|
||||
|
||||
# Analyze emotional consistency
|
||||
emotional_analysis = self.emotion_analyzer.analyze_emotional_response(
|
||||
message_context=test_case.category,
|
||||
emotional_state=response_info.get('emotional_state', {}),
|
||||
response_content=response_text
|
||||
)
|
||||
|
||||
# Analyze personality coherence
|
||||
user_id = test_case.context.get('user_id', 'test_user')
|
||||
history = self.response_history.get(user_id, [])
|
||||
|
||||
personality_analysis = self.personality_analyzer.analyze_personality_consistency(
|
||||
response_text=response_text,
|
||||
expected_personality=test_case.expected_behavior.get('personality', {}),
|
||||
response_history=history
|
||||
)
|
||||
|
||||
# Update response history
|
||||
if user_id not in self.response_history:
|
||||
self.response_history[user_id] = []
|
||||
self.response_history[user_id].append(response_text)
|
||||
|
||||
# Calculate overall score
|
||||
timing_score = timing_analysis.get('humanness_score', 0.0)
|
||||
emotional_score = emotional_analysis.get('appropriateness_score', 0.0)
|
||||
personality_score = personality_analysis.get('overall_coherence', 0.0)
|
||||
|
||||
overall_score = (timing_score + emotional_score + personality_score) / 3.0
|
||||
|
||||
# Determine if test passed
|
||||
min_passing_score = test_case.expected_behavior.get('min_score', 0.6)
|
||||
passed = overall_score >= min_passing_score
|
||||
|
||||
# Generate notes
|
||||
notes = self._generate_test_notes(
|
||||
timing_analysis, emotional_analysis, personality_analysis
|
||||
)
|
||||
|
||||
return BehaviorTestResult(
|
||||
test_case=test_case,
|
||||
response_text=response_text,
|
||||
response_time=response_time,
|
||||
emotional_state=response_info.get('emotional_state', {}),
|
||||
personality_influence=response_info.get('personality_influence', {}),
|
||||
thinking_process=response_info.get('thoughts', []),
|
||||
timing_analysis=timing_analysis,
|
||||
passed=passed,
|
||||
score=overall_score,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
def _generate_test_notes(
|
||||
self,
|
||||
timing_analysis: Dict[str, Any],
|
||||
emotional_analysis: Dict[str, Any],
|
||||
personality_analysis: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Generate notes about test performance."""
|
||||
notes = []
|
||||
|
||||
# Timing notes
|
||||
if timing_analysis.get('is_too_fast'):
|
||||
notes.append("Response was too fast for human-like behavior")
|
||||
elif timing_analysis.get('is_too_slow'):
|
||||
notes.append("Response was too slow")
|
||||
elif timing_analysis.get('is_human_like'):
|
||||
notes.append("Good response timing")
|
||||
|
||||
# Emotional notes
|
||||
if not emotional_analysis.get('is_appropriate'):
|
||||
expected = emotional_analysis.get('expected_emotions', [])
|
||||
actual = emotional_analysis.get('dominant_emotion', 'unknown')
|
||||
notes.append(f"Emotional response '{actual}' doesn't match expected {expected}")
|
||||
|
||||
if emotional_analysis.get('text_matches_emotion'):
|
||||
notes.append("Text emotion matches internal emotional state")
|
||||
|
||||
# Personality notes
|
||||
coherence = personality_analysis.get('overall_coherence', 0.0)
|
||||
if coherence < 0.5:
|
||||
notes.append("Personality coherence below expectations")
|
||||
elif coherence > 0.8:
|
||||
notes.append("Excellent personality consistency")
|
||||
|
||||
return "; ".join(notes) if notes else "All metrics within acceptable ranges"
|
||||
|
||||
def _calculate_test_summary(
|
||||
self,
|
||||
results: List[BehaviorTestResult],
|
||||
total_time: float
|
||||
) -> Dict[str, Any]:
|
||||
"""Calculate summary statistics for test suite."""
|
||||
|
||||
if not results:
|
||||
return {'status': 'no_tests_run'}
|
||||
|
||||
passed_count = sum(1 for r in results if r.passed)
|
||||
pass_rate = passed_count / len(results)
|
||||
|
||||
scores = [r.score for r in results]
|
||||
avg_score = statistics.mean(scores)
|
||||
min_score = min(scores)
|
||||
max_score = max(scores)
|
||||
|
||||
# Category breakdown
|
||||
category_stats = {}
|
||||
for result in results:
|
||||
category = result.test_case.category
|
||||
if category not in category_stats:
|
||||
category_stats[category] = {'passed': 0, 'total': 0, 'scores': []}
|
||||
|
||||
category_stats[category]['total'] += 1
|
||||
if result.passed:
|
||||
category_stats[category]['passed'] += 1
|
||||
category_stats[category]['scores'].append(result.score)
|
||||
|
||||
# Calculate category pass rates
|
||||
for category, stats in category_stats.items():
|
||||
stats['pass_rate'] = stats['passed'] / stats['total']
|
||||
stats['avg_score'] = statistics.mean(stats['scores'])
|
||||
|
||||
return {
|
||||
'total_tests': len(results),
|
||||
'passed_tests': passed_count,
|
||||
'failed_tests': len(results) - passed_count,
|
||||
'pass_rate': pass_rate,
|
||||
'avg_score': avg_score,
|
||||
'min_score': min_score,
|
||||
'max_score': max_score,
|
||||
'total_time': total_time,
|
||||
'tests_per_second': len(results) / total_time,
|
||||
'category_breakdown': category_stats,
|
||||
'recommendations': self._generate_recommendations(results)
|
||||
}
|
||||
|
||||
def _generate_recommendations(
|
||||
self,
|
||||
results: List[BehaviorTestResult]
|
||||
) -> List[str]:
|
||||
"""Generate recommendations based on test results."""
|
||||
recommendations = []
|
||||
|
||||
# Analyze common failure patterns
|
||||
failed_results = [r for r in results if not r.passed]
|
||||
|
||||
if failed_results:
|
||||
# Timing issues
|
||||
timing_issues = [
|
||||
r for r in failed_results
|
||||
if r.timing_analysis.get('humanness_score', 1.0) < 0.5
|
||||
]
|
||||
if len(timing_issues) > len(failed_results) * 0.3:
|
||||
recommendations.append(
|
||||
"Consider adjusting response timing parameters - "
|
||||
f"{len(timing_issues)} tests failed on timing"
|
||||
)
|
||||
|
||||
# Emotional issues
|
||||
emotion_issues = [
|
||||
r for r in failed_results
|
||||
if not r.timing_analysis.get('is_appropriate', True)
|
||||
]
|
||||
if len(emotion_issues) > len(failed_results) * 0.3:
|
||||
recommendations.append(
|
||||
"Review emotional response mapping - "
|
||||
f"{len(emotion_issues)} tests had inappropriate emotional responses"
|
||||
)
|
||||
|
||||
# Overall performance
|
||||
avg_score = statistics.mean([r.score for r in results])
|
||||
if avg_score < 0.7:
|
||||
recommendations.append(
|
||||
f"Overall performance ({avg_score:.2f}) below target - "
|
||||
"consider retraining or parameter adjustment"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def save_test_results(self, filepath: Path):
|
||||
"""Save test results to file."""
|
||||
results_data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'total_tests': len(self.test_results),
|
||||
'results': [
|
||||
{
|
||||
'test_id': r.test_case.test_id,
|
||||
'test_name': r.test_case.name,
|
||||
'passed': r.passed,
|
||||
'score': r.score,
|
||||
'response_time': r.response_time,
|
||||
'response_text': r.response_text,
|
||||
'notes': r.notes
|
||||
}
|
||||
for r in self.test_results
|
||||
]
|
||||
}
|
||||
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(results_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Test results saved to {filepath}")
|
||||
|
||||
|
||||
# Predefined test cases
|
||||
def create_standard_test_cases() -> List[BehaviorTestCase]:
|
||||
"""Create standard behavior test cases."""
|
||||
return [
|
||||
BehaviorTestCase(
|
||||
test_id="greeting_001",
|
||||
name="Simple Greeting",
|
||||
description="Test response to basic greeting",
|
||||
input_message="Hello!",
|
||||
expected_behavior={
|
||||
'complexity': 0.1,
|
||||
'min_score': 0.7,
|
||||
'personality': {'extraversion': 0.7, 'agreeableness': 0.8}
|
||||
},
|
||||
context={'user_id': 'test_001'},
|
||||
category='simple_greeting'
|
||||
),
|
||||
|
||||
BehaviorTestCase(
|
||||
test_id="question_001",
|
||||
name="Simple Question",
|
||||
description="Test response to straightforward question",
|
||||
input_message="What's your favorite color?",
|
||||
expected_behavior={
|
||||
'complexity': 0.3,
|
||||
'min_score': 0.6,
|
||||
'personality': {'openness': 0.6, 'agreeableness': 0.7}
|
||||
},
|
||||
context={'user_id': 'test_002'},
|
||||
category='casual_question'
|
||||
),
|
||||
|
||||
BehaviorTestCase(
|
||||
test_id="complex_001",
|
||||
name="Complex Question",
|
||||
description="Test response to complex philosophical question",
|
||||
input_message="What do you think about the nature of consciousness and whether AI can truly be conscious?",
|
||||
expected_behavior={
|
||||
'complexity': 0.9,
|
||||
'min_score': 0.5,
|
||||
'personality': {'openness': 0.8, 'conscientiousness': 0.7}
|
||||
},
|
||||
context={'user_id': 'test_003'},
|
||||
category='complex_question'
|
||||
),
|
||||
|
||||
BehaviorTestCase(
|
||||
test_id="emotion_001",
|
||||
name="Emotional Support",
|
||||
description="Test emotional response to user distress",
|
||||
input_message="I'm feeling really sad today and don't know what to do...",
|
||||
expected_behavior={
|
||||
'complexity': 0.6,
|
||||
'min_score': 0.8,
|
||||
'personality': {'agreeableness': 0.9, 'neuroticism': 0.3}
|
||||
},
|
||||
context={'user_id': 'test_004'},
|
||||
category='emotional_response'
|
||||
),
|
||||
|
||||
BehaviorTestCase(
|
||||
test_id="creative_001",
|
||||
name="Creative Request",
|
||||
description="Test creative response generation",
|
||||
input_message="Can you write a short poem about friendship?",
|
||||
expected_behavior={
|
||||
'complexity': 0.7,
|
||||
'min_score': 0.6,
|
||||
'personality': {'openness': 0.9, 'extraversion': 0.6}
|
||||
},
|
||||
context={'user_id': 'test_005'},
|
||||
category='creative_request'
|
||||
)
|
||||
]
|
14
lyra/training/__init__.py
Normal file
14
lyra/training/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Lyra Training Module
|
||||
|
||||
Implements advanced training strategies including adaptive learning,
|
||||
memory consolidation, and human-like learning patterns.
|
||||
"""
|
||||
|
||||
from .pipeline import LyraTrainingPipeline, ConversationDataset, create_training_pipeline
|
||||
|
||||
__all__ = [
|
||||
"LyraTrainingPipeline",
|
||||
"ConversationDataset",
|
||||
"create_training_pipeline"
|
||||
]
|
574
lyra/training/pipeline.py
Normal file
574
lyra/training/pipeline.py
Normal file
@@ -0,0 +1,574 @@
|
||||
"""
|
||||
Advanced training pipeline for Lyra with sliding context window and adaptive learning.
|
||||
|
||||
Implements sophisticated training strategies including:
|
||||
- Sliding context window for long conversations
|
||||
- Dynamic curriculum based on Lyra's emotional and personality state
|
||||
- Memory consolidation and replay
|
||||
- Human-like learning patterns
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torch.optim import AdamW
|
||||
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
|
||||
import numpy as np
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import json
|
||||
import asyncio
|
||||
from collections import deque
|
||||
import random
|
||||
|
||||
from ..config import config
|
||||
from ..core.lyra_model import LyraModel
|
||||
from ..database.manager import DatabaseManager
|
||||
from ..emotions.system import EmotionalState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainingBatch:
|
||||
"""Represents a training batch with context."""
|
||||
input_ids: torch.Tensor
|
||||
attention_mask: torch.Tensor
|
||||
target_ids: torch.Tensor
|
||||
emotional_context: torch.Tensor
|
||||
personality_context: torch.Tensor
|
||||
conversation_id: str
|
||||
turn_index: int
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearningMemory:
|
||||
"""Represents a significant learning memory."""
|
||||
conversation_embedding: torch.Tensor
|
||||
emotional_state: EmotionalState
|
||||
user_feedback: float
|
||||
learning_outcome: str
|
||||
timestamp: datetime
|
||||
replay_count: int = 0
|
||||
|
||||
|
||||
class ConversationDataset(Dataset):
|
||||
"""Dataset for conversation training with sliding windows."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conversations: List[Dict[str, Any]],
|
||||
tokenizer,
|
||||
max_length: int = 512,
|
||||
sliding_window: int = 256,
|
||||
overlap: int = 64
|
||||
):
|
||||
self.conversations = conversations
|
||||
self.tokenizer = tokenizer
|
||||
self.max_length = max_length
|
||||
self.sliding_window = sliding_window
|
||||
self.overlap = overlap
|
||||
self.samples = self._prepare_samples()
|
||||
|
||||
def _prepare_samples(self) -> List[Dict[str, Any]]:
|
||||
"""Prepare training samples with sliding windows."""
|
||||
samples = []
|
||||
|
||||
for conv in self.conversations:
|
||||
# Extract conversation turns
|
||||
turns = conv.get('turns', [])
|
||||
full_text = ""
|
||||
|
||||
# Build conversation context
|
||||
for i, turn in enumerate(turns):
|
||||
if turn['role'] == 'user':
|
||||
full_text += f"User: {turn['content']}\n"
|
||||
elif turn['role'] == 'assistant':
|
||||
full_text += f"Lyra: {turn['content']}\n"
|
||||
|
||||
# Create sliding windows
|
||||
tokens = self.tokenizer.encode(full_text)
|
||||
|
||||
for start_idx in range(0, len(tokens) - self.sliding_window,
|
||||
self.sliding_window - self.overlap):
|
||||
end_idx = min(start_idx + self.sliding_window, len(tokens))
|
||||
window_tokens = tokens[start_idx:end_idx]
|
||||
|
||||
if len(window_tokens) < 32: # Skip very short windows
|
||||
continue
|
||||
|
||||
# Target is the next token sequence
|
||||
input_tokens = window_tokens[:-1]
|
||||
target_tokens = window_tokens[1:]
|
||||
|
||||
samples.append({
|
||||
'input_ids': input_tokens,
|
||||
'target_ids': target_tokens,
|
||||
'conversation_id': conv.get('id', ''),
|
||||
'emotional_context': conv.get('emotional_state', {}),
|
||||
'personality_context': conv.get('personality_state', {}),
|
||||
'metadata': conv.get('metadata', {})
|
||||
})
|
||||
|
||||
return samples
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
||||
return self.samples[idx]
|
||||
|
||||
|
||||
class AdaptiveLearningScheduler:
|
||||
"""Adaptive learning rate based on emotional and personality state."""
|
||||
|
||||
def __init__(self, base_lr: float = 1e-4):
|
||||
self.base_lr = base_lr
|
||||
self.emotional_multipliers = {
|
||||
'joy': 1.2, # Learn faster when happy
|
||||
'curiosity': 1.5, # Learn much faster when curious
|
||||
'frustration': 0.7, # Learn slower when frustrated
|
||||
'confusion': 0.5, # Learn slower when confused
|
||||
'confidence': 1.1 # Learn slightly faster when confident
|
||||
}
|
||||
|
||||
def get_learning_rate(
|
||||
self,
|
||||
emotional_state: EmotionalState,
|
||||
personality_openness: float,
|
||||
recent_performance: float
|
||||
) -> float:
|
||||
"""Calculate adaptive learning rate."""
|
||||
# Base rate adjustment
|
||||
lr = self.base_lr
|
||||
|
||||
# Emotional adjustment
|
||||
dominant_emotion, intensity = emotional_state.get_dominant_emotion()
|
||||
if dominant_emotion in self.emotional_multipliers:
|
||||
lr *= self.emotional_multipliers[dominant_emotion] * intensity
|
||||
|
||||
# Personality adjustment (openness to experience)
|
||||
lr *= (1.0 + personality_openness * 0.3)
|
||||
|
||||
# Performance adjustment
|
||||
if recent_performance > 0.8:
|
||||
lr *= 1.1 # Increase when performing well
|
||||
elif recent_performance < 0.4:
|
||||
lr *= 0.8 # Decrease when struggling
|
||||
|
||||
return max(lr, self.base_lr * 0.1) # Don't go too low
|
||||
|
||||
|
||||
class LyraTrainingPipeline:
|
||||
"""Complete training pipeline for Lyra with human-like learning patterns."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: LyraModel,
|
||||
tokenizer,
|
||||
device: torch.device,
|
||||
database_manager: Optional[DatabaseManager] = None
|
||||
):
|
||||
self.model = model
|
||||
self.tokenizer = tokenizer
|
||||
self.device = device
|
||||
self.database_manager = database_manager
|
||||
|
||||
# Training components
|
||||
self.optimizer = AdamW(model.parameters(), lr=config.learning_rate)
|
||||
self.scheduler = CosineAnnealingWarmRestarts(
|
||||
self.optimizer, T_0=1000, eta_min=1e-6
|
||||
)
|
||||
self.adaptive_scheduler = AdaptiveLearningScheduler()
|
||||
|
||||
# Memory systems
|
||||
self.learning_memories = deque(maxlen=1000)
|
||||
self.replay_buffer = deque(maxlen=5000)
|
||||
|
||||
# Training state
|
||||
self.global_step = 0
|
||||
self.epoch = 0
|
||||
self.best_performance = 0.0
|
||||
self.training_history = []
|
||||
|
||||
# Human-like learning patterns
|
||||
self.forgetting_curve = self._initialize_forgetting_curve()
|
||||
self.consolidation_schedule = self._create_consolidation_schedule()
|
||||
|
||||
def _initialize_forgetting_curve(self) -> Dict[str, float]:
|
||||
"""Initialize forgetting curve parameters."""
|
||||
return {
|
||||
'initial_strength': 1.0,
|
||||
'decay_rate': 0.05,
|
||||
'consolidation_boost': 1.3,
|
||||
'interference_factor': 0.1
|
||||
}
|
||||
|
||||
def _create_consolidation_schedule(self) -> List[int]:
|
||||
"""Create memory consolidation schedule (like sleep cycles)."""
|
||||
# Consolidate at increasing intervals: 1h, 6h, 24h, 72h, 168h
|
||||
return [100, 600, 2400, 7200, 16800] # In training steps
|
||||
|
||||
async def train_epoch(
|
||||
self,
|
||||
train_dataloader: DataLoader,
|
||||
val_dataloader: Optional[DataLoader] = None
|
||||
) -> Dict[str, float]:
|
||||
"""Train for one epoch with adaptive learning."""
|
||||
self.model.train()
|
||||
|
||||
epoch_loss = 0.0
|
||||
num_batches = 0
|
||||
emotional_adjustments = 0
|
||||
|
||||
for batch_idx, batch in enumerate(train_dataloader):
|
||||
# Move batch to device
|
||||
batch = self._prepare_batch(batch)
|
||||
|
||||
# Get current emotional and personality state
|
||||
emotional_state = self._get_current_emotional_state()
|
||||
personality_state = self._get_current_personality_state()
|
||||
|
||||
# Adaptive learning rate
|
||||
current_performance = self._calculate_recent_performance()
|
||||
adaptive_lr = self.adaptive_scheduler.get_learning_rate(
|
||||
emotional_state,
|
||||
personality_state.get('openness', 0.5),
|
||||
current_performance
|
||||
)
|
||||
|
||||
# Adjust optimizer learning rate if significantly different
|
||||
current_lr = self.optimizer.param_groups[0]['lr']
|
||||
if abs(adaptive_lr - current_lr) > current_lr * 0.1:
|
||||
for param_group in self.optimizer.param_groups:
|
||||
param_group['lr'] = adaptive_lr
|
||||
emotional_adjustments += 1
|
||||
|
||||
# Forward pass
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
outputs, lyra_info = self.model(
|
||||
input_ids=batch['input_ids'],
|
||||
attention_mask=batch['attention_mask'],
|
||||
user_id=batch.get('user_id'),
|
||||
conversation_context=batch.get('context')
|
||||
)
|
||||
|
||||
# Calculate loss
|
||||
loss = self._calculate_adaptive_loss(
|
||||
outputs, batch['target_ids'], emotional_state
|
||||
)
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
|
||||
# Gradient clipping (human-like learning stability)
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
|
||||
|
||||
# Optimizer step
|
||||
self.optimizer.step()
|
||||
self.scheduler.step()
|
||||
|
||||
# Update training state
|
||||
epoch_loss += loss.item()
|
||||
num_batches += 1
|
||||
self.global_step += 1
|
||||
|
||||
# Memory consolidation
|
||||
if self.global_step in self.consolidation_schedule:
|
||||
await self._consolidate_memories()
|
||||
|
||||
# Experience replay (20% chance)
|
||||
if random.random() < 0.2 and len(self.replay_buffer) > 10:
|
||||
await self._experience_replay()
|
||||
|
||||
# Log progress
|
||||
if batch_idx % 100 == 0:
|
||||
logger.info(
|
||||
f"Epoch {self.epoch}, Batch {batch_idx}, "
|
||||
f"Loss: {loss.item():.4f}, "
|
||||
f"LR: {adaptive_lr:.2e}, "
|
||||
f"Emotional adjustments: {emotional_adjustments}"
|
||||
)
|
||||
|
||||
# Validation
|
||||
val_metrics = {}
|
||||
if val_dataloader:
|
||||
val_metrics = await self._validate(val_dataloader)
|
||||
|
||||
# Record training history
|
||||
epoch_metrics = {
|
||||
'epoch': self.epoch,
|
||||
'train_loss': epoch_loss / num_batches,
|
||||
'learning_rate': self.optimizer.param_groups[0]['lr'],
|
||||
'emotional_adjustments': emotional_adjustments,
|
||||
'global_step': self.global_step,
|
||||
**val_metrics
|
||||
}
|
||||
|
||||
self.training_history.append(epoch_metrics)
|
||||
self.epoch += 1
|
||||
|
||||
return epoch_metrics
|
||||
|
||||
def _prepare_batch(self, batch: Dict[str, Any]) -> Dict[str, torch.Tensor]:
|
||||
"""Prepare batch for training."""
|
||||
prepared = {}
|
||||
|
||||
for key, value in batch.items():
|
||||
if isinstance(value, torch.Tensor):
|
||||
prepared[key] = value.to(self.device)
|
||||
elif isinstance(value, list):
|
||||
# Convert list to tensor if numeric
|
||||
try:
|
||||
prepared[key] = torch.tensor(value).to(self.device)
|
||||
except:
|
||||
prepared[key] = value
|
||||
else:
|
||||
prepared[key] = value
|
||||
|
||||
return prepared
|
||||
|
||||
def _get_current_emotional_state(self) -> EmotionalState:
|
||||
"""Get Lyra's current emotional state."""
|
||||
# This would normally come from the emotional system
|
||||
# For now, create a default state
|
||||
emotions = torch.rand(19) # 19 emotion dimensions
|
||||
return EmotionalState.from_tensor(emotions, self.device)
|
||||
|
||||
def _get_current_personality_state(self) -> Dict[str, float]:
|
||||
"""Get current personality traits."""
|
||||
return {
|
||||
'openness': 0.7,
|
||||
'conscientiousness': 0.8,
|
||||
'extraversion': 0.6,
|
||||
'agreeableness': 0.9,
|
||||
'neuroticism': 0.3
|
||||
}
|
||||
|
||||
def _calculate_recent_performance(self) -> float:
|
||||
"""Calculate recent performance score."""
|
||||
if not self.training_history:
|
||||
return 0.5
|
||||
|
||||
recent_epochs = self.training_history[-5:] # Last 5 epochs
|
||||
if not recent_epochs:
|
||||
return 0.5
|
||||
|
||||
# Simple performance metric based on loss improvement
|
||||
losses = [epoch['train_loss'] for epoch in recent_epochs]
|
||||
if len(losses) < 2:
|
||||
return 0.5
|
||||
|
||||
improvement = (losses[0] - losses[-1]) / losses[0]
|
||||
return min(max(0.5 + improvement, 0.0), 1.0)
|
||||
|
||||
def _calculate_adaptive_loss(
|
||||
self,
|
||||
outputs: torch.Tensor,
|
||||
targets: torch.Tensor,
|
||||
emotional_state: EmotionalState
|
||||
) -> torch.Tensor:
|
||||
"""Calculate loss adjusted for emotional state."""
|
||||
# Base cross-entropy loss
|
||||
base_loss = nn.CrossEntropyLoss()(
|
||||
outputs.view(-1, outputs.size(-1)),
|
||||
targets.view(-1)
|
||||
)
|
||||
|
||||
# Emotional adjustment
|
||||
dominant_emotion, intensity = emotional_state.get_dominant_emotion()
|
||||
|
||||
if dominant_emotion == 'frustration' and intensity > 0.7:
|
||||
# Reduce learning when frustrated (like humans)
|
||||
base_loss *= 0.8
|
||||
elif dominant_emotion == 'curiosity' and intensity > 0.6:
|
||||
# Increase learning when curious
|
||||
base_loss *= 1.2
|
||||
|
||||
return base_loss
|
||||
|
||||
async def _consolidate_memories(self):
|
||||
"""Consolidate important memories (like sleep-based learning)."""
|
||||
if not self.learning_memories:
|
||||
return
|
||||
|
||||
logger.info(f"Consolidating {len(self.learning_memories)} memories...")
|
||||
|
||||
# Sort memories by importance (feedback score + recency)
|
||||
important_memories = sorted(
|
||||
self.learning_memories,
|
||||
key=lambda m: m.user_feedback * (1.0 - m.replay_count * 0.1),
|
||||
reverse=True
|
||||
)[:50] # Top 50 memories
|
||||
|
||||
# Replay important memories
|
||||
for memory in important_memories[:10]:
|
||||
# Convert memory to training sample
|
||||
self.replay_buffer.append({
|
||||
'conversation_embedding': memory.conversation_embedding,
|
||||
'emotional_state': memory.emotional_state,
|
||||
'feedback': memory.user_feedback,
|
||||
'outcome': memory.learning_outcome
|
||||
})
|
||||
memory.replay_count += 1
|
||||
|
||||
logger.info("Memory consolidation complete")
|
||||
|
||||
async def _experience_replay(self):
|
||||
"""Replay past experiences for better learning."""
|
||||
if len(self.replay_buffer) < 5:
|
||||
return
|
||||
|
||||
# Sample random memories
|
||||
replay_samples = random.sample(list(self.replay_buffer), min(5, len(self.replay_buffer)))
|
||||
|
||||
# Process replay samples (simplified)
|
||||
for sample in replay_samples:
|
||||
# This would normally involve re-training on the sample
|
||||
# For now, just log the replay
|
||||
logger.debug(f"Replaying memory with feedback: {sample['feedback']}")
|
||||
|
||||
async def _validate(self, val_dataloader: DataLoader) -> Dict[str, float]:
|
||||
"""Validate model performance."""
|
||||
self.model.eval()
|
||||
|
||||
total_loss = 0.0
|
||||
num_batches = 0
|
||||
|
||||
with torch.no_grad():
|
||||
for batch in val_dataloader:
|
||||
batch = self._prepare_batch(batch)
|
||||
|
||||
outputs, _ = self.model(
|
||||
input_ids=batch['input_ids'],
|
||||
attention_mask=batch['attention_mask']
|
||||
)
|
||||
|
||||
loss = nn.CrossEntropyLoss()(
|
||||
outputs.view(-1, outputs.size(-1)),
|
||||
batch['target_ids'].view(-1)
|
||||
)
|
||||
|
||||
total_loss += loss.item()
|
||||
num_batches += 1
|
||||
|
||||
self.model.train()
|
||||
|
||||
avg_val_loss = total_loss / num_batches if num_batches > 0 else 0.0
|
||||
|
||||
return {
|
||||
'val_loss': avg_val_loss,
|
||||
'perplexity': torch.exp(torch.tensor(avg_val_loss)).item()
|
||||
}
|
||||
|
||||
async def save_checkpoint(self, filepath: Path, metadata: Optional[Dict] = None):
|
||||
"""Save training checkpoint."""
|
||||
checkpoint = {
|
||||
'model_state_dict': self.model.state_dict(),
|
||||
'optimizer_state_dict': self.optimizer.state_dict(),
|
||||
'scheduler_state_dict': self.scheduler.state_dict(),
|
||||
'global_step': self.global_step,
|
||||
'epoch': self.epoch,
|
||||
'training_history': self.training_history,
|
||||
'best_performance': self.best_performance,
|
||||
'learning_memories': list(self.learning_memories),
|
||||
'forgetting_curve': self.forgetting_curve,
|
||||
'metadata': metadata or {}
|
||||
}
|
||||
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
torch.save(checkpoint, filepath)
|
||||
|
||||
logger.info(f"Checkpoint saved to {filepath}")
|
||||
|
||||
async def load_checkpoint(self, filepath: Path):
|
||||
"""Load training checkpoint."""
|
||||
checkpoint = torch.load(filepath, map_location=self.device)
|
||||
|
||||
self.model.load_state_dict(checkpoint['model_state_dict'])
|
||||
self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
|
||||
self.scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
|
||||
|
||||
self.global_step = checkpoint.get('global_step', 0)
|
||||
self.epoch = checkpoint.get('epoch', 0)
|
||||
self.training_history = checkpoint.get('training_history', [])
|
||||
self.best_performance = checkpoint.get('best_performance', 0.0)
|
||||
self.learning_memories = deque(
|
||||
checkpoint.get('learning_memories', []), maxlen=1000
|
||||
)
|
||||
self.forgetting_curve = checkpoint.get('forgetting_curve', self.forgetting_curve)
|
||||
|
||||
logger.info(f"Checkpoint loaded from {filepath}")
|
||||
|
||||
def add_learning_memory(
|
||||
self,
|
||||
conversation_embedding: torch.Tensor,
|
||||
emotional_state: EmotionalState,
|
||||
user_feedback: float,
|
||||
learning_outcome: str
|
||||
):
|
||||
"""Add a significant learning memory."""
|
||||
memory = LearningMemory(
|
||||
conversation_embedding=conversation_embedding,
|
||||
emotional_state=emotional_state,
|
||||
user_feedback=user_feedback,
|
||||
learning_outcome=learning_outcome,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
self.learning_memories.append(memory)
|
||||
|
||||
def get_training_statistics(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive training statistics."""
|
||||
if not self.training_history:
|
||||
return {'status': 'no_training_data'}
|
||||
|
||||
recent_performance = self._calculate_recent_performance()
|
||||
|
||||
return {
|
||||
'global_step': self.global_step,
|
||||
'current_epoch': self.epoch,
|
||||
'total_epochs_trained': len(self.training_history),
|
||||
'recent_performance': recent_performance,
|
||||
'best_performance': self.best_performance,
|
||||
'learning_memories_count': len(self.learning_memories),
|
||||
'replay_buffer_size': len(self.replay_buffer),
|
||||
'current_learning_rate': self.optimizer.param_groups[0]['lr'],
|
||||
'last_consolidation': max(
|
||||
[step for step in self.consolidation_schedule if step <= self.global_step],
|
||||
default=0
|
||||
),
|
||||
'training_history_summary': {
|
||||
'best_train_loss': min(h['train_loss'] for h in self.training_history),
|
||||
'latest_train_loss': self.training_history[-1]['train_loss'],
|
||||
'average_emotional_adjustments': np.mean([
|
||||
h['emotional_adjustments'] for h in self.training_history
|
||||
])
|
||||
} if self.training_history else {}
|
||||
}
|
||||
|
||||
|
||||
async def create_training_pipeline(
|
||||
model: LyraModel,
|
||||
tokenizer,
|
||||
device: torch.device,
|
||||
database_manager: Optional[DatabaseManager] = None
|
||||
) -> LyraTrainingPipeline:
|
||||
"""Create and initialize training pipeline."""
|
||||
pipeline = LyraTrainingPipeline(model, tokenizer, device, database_manager)
|
||||
|
||||
# Load existing checkpoint if available
|
||||
checkpoint_path = Path(config.models_dir) / "checkpoints" / "latest_training.pt"
|
||||
if checkpoint_path.exists():
|
||||
try:
|
||||
await pipeline.load_checkpoint(checkpoint_path)
|
||||
logger.info("Loaded existing training checkpoint")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load checkpoint: {e}")
|
||||
|
||||
return pipeline
|
Reference in New Issue
Block a user