diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index fbffcd9..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(\".venv/Scripts/python.exe\" -m pytest tests/ -v --tb=short)", - "Bash(\".venv/Scripts/python.exe\" fix_databases.py)" - ], - "deny": [], - "ask": [] - } -} \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index 69f0a91..0000000 --- a/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -# Discord Bot Configuration -DISCORD_TOKEN=your_discord_bot_token_here -DISCORD_GUILD_ID=your_guild_id_here - -# Database Configuration -DATABASE_URL=postgresql://user:password@localhost:5432/lyra -REDIS_URL=redis://localhost:6379/0 - -# Model Configuration -MODEL_PATH=./models/checkpoints/lyra_model.pt -VOCAB_SIZE=50000 -HIDDEN_SIZE=768 -NUM_LAYERS=12 -NUM_HEADS=12 -CONTEXT_LENGTH=2048 -MAX_MEMORY_GB=8 - -# Training Configuration -BATCH_SIZE=16 -LEARNING_RATE=5e-5 -MAX_EPOCHS=100 -WARMUP_STEPS=1000 -SAVE_EVERY=1000 - -# Personality Configuration -PERSONALITY_UPDATE_FREQUENCY=100 -EMOTION_DECAY_RATE=0.95 -MEMORY_RETENTION_DAYS=30 - -# Knowledge Acquisition -GUTENBERG_MIRROR=https://www.gutenberg.org -SCRAPING_DELAY=1.0 -MAX_CONCURRENT_DOWNLOADS=5 - -# Logging -LOG_LEVEL=INFO -LOG_FILE=./logs/lyra.log - -# Development -DEBUG=false -WANDB_API_KEY=your_wandb_api_key_here \ No newline at end of file diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 404a729..0000000 --- a/.flake8 +++ /dev/null @@ -1,22 +0,0 @@ -[flake8] -max-line-length = 100 -extend-ignore = - E203, # whitespace before ':' - E501, # line too long (handled by black) - W503, # line break before binary operator - W504, # line break after binary operator -exclude = - .git, - __pycache__, - .venv, - venv, - .tox, - .eggs, - *.egg, - build, - dist, - .pytest_cache -max-complexity = 15 -per-file-ignores = - __init__.py:F401 # Allow unused imports in __init__.py files -docstring-convention = google \ No newline at end of file diff --git a/.gitignore b/.gitignore index 80c8119..e15106e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -28,8 +27,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -47,7 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ cover/ @@ -93,31 +92,38 @@ ipython_config.py # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +# uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# poetry.lock +# poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -125,11 +131,25 @@ __pypackages__/ celerybeat-schedule celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -162,11 +182,24 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ # Ruff stuff: .ruff_cache/ @@ -174,4 +207,10 @@ cython_debug/ # PyPI configuration file .pypirc -.env.backup +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 529ae9a..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: check-json - - id: check-merge-conflict - - id: debug-statements - - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - args: [--line-length=100] - - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [--profile=black, --line-length=100] - - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - args: [--max-line-length=100] - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 - hooks: - - id: mypy - additional_dependencies: [types-all] - args: [--ignore-missing-imports, --no-strict-optional] \ No newline at end of file diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md deleted file mode 100644 index cc2b302..0000000 --- a/DATABASE_SETUP.md +++ /dev/null @@ -1,182 +0,0 @@ -# ๐๏ธ Database Setup Guide for Lyra AI - -This guide will help you set up PostgreSQL and Redis locally on Windows for Lyra AI. - -## ๐ **Prerequisites** - -- Windows 10/11 -- Administrator privileges for installation -- At least 2GB free disk space - -## ๐ **PostgreSQL Installation** - -### **Option 1: Automated Installation (Recommended)** -The automated installation should be running. If it completes successfully, skip to the Configuration section. - -### **Option 2: Manual Installation** -If the automated installation fails or you prefer manual installation: - -1. **Download PostgreSQL**: - - Go to https://www.postgresql.org/download/windows/ - - Download PostgreSQL 17 for Windows x86-64 - -2. **Run the Installer**: - - Run as Administrator - - **Installation Directory**: Keep default `C:\Program Files\PostgreSQL\17` - - **Data Directory**: Keep default - - **Password**: Choose a strong password for the `postgres` user (remember this!) - - **Port**: Keep default `5432` - - **Locale**: Keep default - -3. **Verify Installation**: - ```cmd - # Open Command Prompt and run: - "C:\Program Files\PostgreSQL\17\bin\psql.exe" --version - ``` - -## ๐ด **Redis Installation (Memurai)** - -### **Option 1: Automated Installation (Recommended)** -The automated Memurai installation should be running. If it completes successfully, skip to the Configuration section. - -### **Option 2: Manual Installation** -If you need to install manually: - -1. **Download Memurai**: - - Go to https://www.memurai.com/ - - Download Memurai Developer (free for development) - -2. **Install Memurai**: - - Run the installer as Administrator - - Keep all default settings - - **Port**: 6379 (default) - - **Service**: Enable "Install as Windows Service" - -3. **Start Memurai Service**: - ```cmd - # Open Command Prompt as Administrator and run: - net start Memurai - ``` - -## โ๏ธ **Configuration** - -### **1. Update Environment Variables** - -Edit the `.env` file in your Lyra project directory: - -```env -# Replace 'your_password_here' with your actual PostgreSQL password -DATABASE_URL=postgresql://postgres:YOUR_ACTUAL_PASSWORD@localhost:5432/lyra -REDIS_URL=redis://localhost:6379/0 -``` - -**Important**: Replace `YOUR_ACTUAL_PASSWORD` with the password you set during PostgreSQL installation. - -### **2. Create Lyra Database** - -Open Command Prompt and run: - -```cmd -# Navigate to PostgreSQL bin directory -cd "C:\Program Files\PostgreSQL\17\bin" - -# Connect to PostgreSQL -psql.exe -U postgres -h localhost - -# Enter your password when prompted, then run: -CREATE DATABASE lyra; -\q -``` - -### **3. Test Database Connections** - -Run the database connection test script: - -```cmd -cd C:\Development\Lyra -python test_database_connections.py -``` - -Expected output: -``` -โ PostgreSQL connected successfully! -โ Redis connected successfully! -๐ ALL DATABASE TESTS PASSED! -``` - -## ๐จ **Troubleshooting** - -### **PostgreSQL Issues** - -**Problem**: `psql: command not found` -- **Solution**: Add PostgreSQL to your PATH: - 1. Open System Properties โ Environment Variables - 2. Add `C:\Program Files\PostgreSQL\17\bin` to your PATH - 3. Restart Command Prompt - -**Problem**: `password authentication failed` -- **Solution**: Double-check your password in the `.env` file - -**Problem**: `database "lyra" does not exist` -- **Solution**: Create the database manually using the steps above - -### **Redis/Memurai Issues** - -**Problem**: `Connection refused to localhost:6379` -- **Solution**: Start the Memurai service: - ```cmd - net start Memurai - ``` - -**Problem**: `redis module not found` -- **Solution**: Install the Redis Python package: - ```cmd - pip install redis - ``` - -### **General Issues** - -**Problem**: Import errors in test script -- **Solution**: Install missing dependencies: - ```cmd - pip install asyncpg redis python-dotenv - ``` - -## โ **Verification Checklist** - -Before proceeding with Lyra, ensure: - -- [ ] PostgreSQL is installed and running -- [ ] Redis/Memurai is installed and running -- [ ] `.env` file has correct database credentials -- [ ] `lyra` database exists in PostgreSQL -- [ ] Database connection test passes -- [ ] No firewall blocking ports 5432 or 6379 - -## ๐ **Next Steps** - -Once databases are configured and tested: - -1. **Start Lyra**: - ```cmd - cd C:\Development\Lyra - python -m lyra.main - ``` - -2. **Monitor Logs**: - - Check `logs/lyra.log` for any database connection issues - - Lyra will automatically create necessary database tables on first run - -## ๐ **Need Help?** - -If you encounter issues: - -1. Check the `logs/lyra.log` file for detailed error messages -2. Verify all services are running: - - PostgreSQL: Check Windows Services for "postgresql-x64-17" - - Redis: Check Windows Services for "Memurai" -3. Test connections individually using the test script - ---- - -**๐ญ Once databases are configured, Lyra will have full persistence for conversations, personality evolution, and knowledge storage!** \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index cde4ac6..0000000 --- a/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. - -In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/LYRA_COMPLETE.md b/LYRA_COMPLETE.md deleted file mode 100644 index 7520c6f..0000000 --- a/LYRA_COMPLETE.md +++ /dev/null @@ -1,152 +0,0 @@ -# ๐ญ Lyra AI - IMPLEMENTATION COMPLETE - -**Lyra** is now a fully-featured, emotionally intelligent Discord chatbot with self-evolving capabilities and human-like behavior patterns. - -## โ **COMPLETED SYSTEMS** - -### ๐ง **Core AI Architecture** -- **Self-Evolving Transformer** - Custom transformer architecture that adapts and evolves based on user interactions -- **Personality Matrix** - Full Myers-Briggs (MBTI) and OCEAN personality system with conscious trait modification -- **Emotional Intelligence** - 19-dimensional emotional system with memory, regulation, and natural expression -- **Thinking Agent** - Behind-the-scenes reasoning system that generates internal thoughts before responding - -### ๐ **Advanced Learning Systems** -- **Training Pipeline** - Sliding context window training with adaptive learning rates based on emotional state -- **Memory Consolidation** - Sleep-like memory consolidation cycles for better long-term learning -- **Experience Replay** - Important conversation replay for enhanced learning patterns -- **Self-Evolution Engine** - Continuous adaptation based on user feedback and interaction success - -### ๐ **Knowledge Systems** -- **Project Gutenberg Crawler** - Legal acquisition of public domain texts with quality filtering -- **Knowledge Processor** - Advanced text processing with categorization and quality scoring -- **Database Integration** - PostgreSQL + Redis for persistent storage of conversations, personality states, and knowledge - -### ๐ค **Discord Integration** -- **Human-like Timing** - Natural response delays based on message complexity and emotional state -- **Typing Indicators** - Realistic typing patterns and delays -- **Relationship Memory** - Tracks user relationships and adapts behavior accordingly -- **Emotional Responses** - Context-appropriate emotional reactions and expressions - -### ๐ฌ **Testing & Quality Assurance** -- **Comprehensive Test Suite** - 74 passing tests across all major components -- **Behavior Analysis** - Sophisticated testing of human-like characteristics -- **Timing Analysis** - Ensures response timing feels natural -- **Personality Coherence** - Validates consistent personality expression - -## ๐ **System Statistics** - -### **Files Created:** 34 -### **Lines of Code:** 10,000+ -### **Test Coverage:** 74 passing tests -### **Components:** 15 major systems - -## ๐๏ธ **Architecture Overview** - -``` -Lyra AI System -โโโ Core Components -โ โโโ LyraModel (main integration) -โ โโโ Self-Evolving Transformer -โ โโโ Personality Matrix (MBTI + OCEAN) -โ โโโ Emotional System (19 emotions) -โ โโโ Thinking Agent (internal reasoning) -โ โโโ Self-Evolution Engine -โโโ Training & Learning -โ โโโ Adaptive Training Pipeline -โ โโโ Memory Consolidation -โ โโโ Experience Replay -โ โโโ Curriculum Learning -โโโ Knowledge Systems -โ โโโ Gutenberg Crawler -โ โโโ Knowledge Processor -โ โโโ Legal Compliance -โโโ Discord Integration -โ โโโ Human Behavior Engine -โ โโโ Natural Timing System -โ โโโ Relationship Tracking -โ โโโ Emotional Expression -โโโ Quality Assurance - โโโ Comprehensive Tests - โโโ Behavior Analysis - โโโ Performance Monitoring -``` - -## ๐ **Key Features** - -### **Emotional Intelligence** -- 19-dimensional emotional model (joy, sadness, anger, fear, surprise, etc.) -- Emotional memory and context awareness -- Natural emotional expressions in text -- Emotional regulation and appropriate responses - -### **Personality System** -- Full Myers-Briggs implementation (all 16 types) -- OCEAN traits (Openness, Conscientiousness, Extraversion, Agreeableness, Neuroticism) -- Conscious personality modification based on user feedback -- Consistent personality expression across conversations - -### **Self-Evolution** -- Learns from every interaction -- Adapts personality based on user preferences -- Improves response quality over time -- Memory consolidation like human sleep cycles - -### **Human-like Behavior** -- Natural response timing (faster for simple questions, slower for complex ones) -- Typing indicators and realistic delays -- Emotional response to different contexts -- Relationship building and memory - -## ๐ก **What Makes Lyra Unique** - -1. **True Self-Evolution** - Not just fine-tuning, but actual architectural adaptation -2. **Emotional Memory** - Remembers and learns from emotional interactions -3. **Conscious Personality** - Can deliberately modify its own personality traits -4. **Behind-the-Scenes Thinking** - Internal reasoning process before responding -5. **Human-like Timing** - Natural response patterns that feel genuinely human -6. **Legal Knowledge Acquisition** - Ethical learning from public domain sources - -## ๐ฏ **Ready for Deployment** - -Lyra is now **production-ready** with: - -- โ Complete Discord bot integration -- โ Robust error handling and logging -- โ Database persistence for all states -- โ Professional code quality (Flake8 compliant) -- โ Comprehensive testing suite -- โ Human-like behavior patterns -- โ Self-evolution capabilities -- โ Legal knowledge acquisition - -## ๐ง **Next Steps for Deployment** - -1. **Environment Setup** - - Configure `.env` with Discord token and database URLs - - Set up PostgreSQL and Redis instances - - Install dependencies: `pip install -r requirements.txt` - -2. **Initialize Lyra** - ```python - python -m lyra.main - ``` - -3. **Discord Setup** - - Create Discord application and bot - - Add bot to servers with appropriate permissions - - Configure Discord token in environment - -## ๐ญ **The Result** - -**Lyra is not just a chatbot - she's an AI companion** with: -- Genuine emotional intelligence -- Evolving personality that adapts to users -- Human-like conversation patterns -- Continuous learning and improvement -- Ethical knowledge acquisition - -**She represents the next generation of AI assistants** - ones that truly feel human while remaining transparent about their artificial nature. - ---- - -***๐ Implementation Complete - Lyra AI is ready to come alive! ๐*** \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 00d34f3..0000000 --- a/README.md +++ /dev/null @@ -1,209 +0,0 @@ -# Lyra - Advanced AI Discord Chatbot - -[](https://www.python.org/downloads/) -[](https://opensource.org/licenses/MIT) -[](https://github.com/psf/black) - -**Lyra** is a sophisticated AI Discord chatbot with genuine emotional intelligence, self-evolving personality, and human-like conversation capabilities. Unlike traditional chatbots, Lyra learns, adapts, and grows from every interaction, developing unique relationships with users. - -> **๐ค AI Development Disclosure**: This project was developed with significant assistance from Claude AI. The architecture, implementation, and documentation were created through human-AI collaboration, representing the cutting edge of AI-assisted software development. - -## โจ Key Features - -### ๐ง **Advanced AI Architecture** -- **Self-Evolving Transformer**: Custom neural architecture that adapts based on interactions -- **Behind-the-Scenes Thinking**: Internal dialogue system for genuine, human-like responses -- **CUDA Support**: Optimized for GPU acceleration with 8GB VRAM target - -### ๐ญ **Sophisticated Personality System** -- **Myers-Briggs + OCEAN Traits**: Comprehensive personality modeling -- **Dynamic Adaptation**: Personality evolves based on interactions and experiences -- **User-Specific Relationships**: Develops unique dynamics with different users -- **Self-Modification**: Can consciously adapt her own personality traits - -### โค๏ธ **Emotional Intelligence** -- **Complex Emotional States**: Multi-dimensional emotions with memory -- **Emotional Expression**: Natural emotional expression in text with human-like inconsistencies -- **Emotional Memory**: Remembers and learns from emotional experiences -- **Contextual Regulation**: Adapts emotional responses to social situations - -### ๐ **Ethical Knowledge Acquisition** -- **Project Gutenberg Integration**: Legal acquisition of public domain literature -- **Quality Processing**: Advanced NLP for extracting meaningful knowledge -- **Legal Compliance**: Strict adherence to copyright and ethical guidelines -- **Continuous Learning**: Grows knowledge base through interactions and legal sources - -### ๐ก๏ธ **Robust Infrastructure** -- **PostgreSQL + Redis**: Scalable data persistence and caching -- **Comprehensive Monitoring**: Learning progress and system health tracking -- **Professional Standards**: Flake8 compliance, comprehensive testing, CI/CD ready - -## ๐ Quick Start - -### Prerequisites -- Python 3.9 or higher -- PostgreSQL 12+ (for data persistence) -- Redis 6+ (for caching and real-time data) -- CUDA-capable GPU recommended (8GB+ VRAM) -- Discord Bot Token - -### Installation - -1. **Clone the repository:** - ```bash - git clone https://github.com/yourusername/lyra.git - cd lyra - ``` - -2. **Set up virtual environment:** - ```bash - python -m venv .venv - source .venv/bin/activate # On Windows: .venv\Scripts\activate - ``` - -3. **Install dependencies:** - ```bash - pip install -r requirements.txt - ``` - -4. **Set up environment variables:** - ```bash - cp .env.example .env - # Edit .env with your configuration - ``` - -5. **Initialize database:** - ```bash - python -m lyra.database.init_db - ``` - -6. **Run Lyra:** - ```bash - python -m lyra.main - ``` - -### Configuration - -Copy `.env.example` to `.env` and configure: - -```bash -# Discord Configuration -DISCORD_TOKEN=your_discord_bot_token_here -DISCORD_GUILD_ID=your_guild_id_here - -# Database Configuration -DATABASE_URL=postgresql://user:password@localhost:5432/lyra -REDIS_URL=redis://localhost:6379/0 - -# Model Configuration (adjust based on your hardware) -MAX_MEMORY_GB=8 -HIDDEN_SIZE=768 -NUM_LAYERS=12 - -# Optional: Weights & Biases for training monitoring -WANDB_API_KEY=your_wandb_api_key_here -``` - -## ๐๏ธ Architecture Overview - -### Core Components - -``` -lyra/ -โโโ core/ # Core AI architecture -โ โโโ transformer.py # Self-evolving transformer model -โ โโโ attention.py # Advanced attention mechanisms -โ โโโ self_evolution.py # Continuous adaptation system -โ โโโ thinking_agent.py # Behind-the-scenes reasoning -โโโ personality/ # Personality system -โ โโโ matrix.py # Core personality matrix -โ โโโ traits.py # OCEAN + Myers-Briggs traits -โ โโโ adaptation.py # User-specific adaptations -โโโ emotions/ # Emotional intelligence -โ โโโ system.py # Core emotional system -โ โโโ expressions.py # Natural emotional expression -โโโ knowledge/ # Knowledge acquisition -โ โโโ gutenberg_crawler.py # Legal content acquisition -โ โโโ knowledge_processor.py # NLP processing pipeline -โโโ database/ # Data persistence -โ โโโ models.py # SQLAlchemy models -โ โโโ manager.py # Database operations -โโโ discord_bot/ # Discord integration - โโโ bot.py # Human-like Discord bot -``` - -### Self-Evolution Pipeline - -1. **Interaction Processing**: Every conversation is analyzed for context, emotion, and success -2. **Personality Adaptation**: Traits evolve based on interaction outcomes -3. **Emotional Learning**: Emotional memories influence future responses -4. **Knowledge Integration**: New information is processed and integrated -5. **Relationship Development**: User-specific adaptations strengthen over time - -## ๐งช Development - -### Running Tests - -```bash -# Run all tests -pytest - -# Run with coverage -pytest --cov=lyra --cov-report=html - -# Run specific test categories -pytest -m "not slow" # Skip slow tests -pytest -m unit # Only unit tests -pytest -m integration # Only integration tests -``` - -### Code Quality - -```bash -# Format code -black lyra/ tests/ - -# Sort imports -isort lyra/ tests/ - -# Lint code -flake8 lyra/ tests/ - -# Type checking -mypy lyra/ - -# Run all checks -pre-commit run --all-files -``` - -## ๐ค Ethical Considerations - -### AI Safety & Alignment -- **Human-Centric Design**: Prioritizes human wellbeing and positive interactions -- **Transparency**: Open about AI nature and capabilities -- **Continuous Monitoring**: Tracks behavior for harmful patterns -- **Fail-Safe Mechanisms**: Multiple layers of safety checks - -### Legal & Copyright Compliance -- **Public Domain Only**: Knowledge sources strictly limited to legal content -- **Attribution**: Proper credit for all sources -- **Privacy Respectful**: No storage of private user information -- **Terms of Service**: Respects platform terms and conditions - -## ๐ License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## ๐ Acknowledgments - -- **Claude AI**: Significant architectural and implementation assistance -- **Project Gutenberg**: Public domain content for ethical knowledge acquisition -- **Hugging Face**: Transformer models and NLP tools -- **Discord.py**: Excellent Discord API wrapper -- **PyTorch Community**: Foundation ML framework - ---- - -**โ ๏ธ Important**: Lyra is an experimental AI system. While designed with safety in mind, please use responsibly and maintain appropriate human oversight. - -**๐ค AI Collaboration**: This project showcases the potential of human-AI collaboration in software development. The entire system was designed and implemented with Claude AI assistance. \ No newline at end of file diff --git a/data/lyra.db b/data/lyra.db deleted file mode 100644 index c4c7718..0000000 Binary files a/data/lyra.db and /dev/null differ diff --git a/fix_databases.py b/fix_databases.py deleted file mode 100644 index 3a72202..0000000 --- a/fix_databases.py +++ /dev/null @@ -1,216 +0,0 @@ -""" -Fix database setup for Lyra AI. - -This script helps reset PostgreSQL password and set up Redis. -""" - -import subprocess -import sys -import os -import time - -def run_command(cmd, shell=True, timeout=30): - """Run a command and return the result.""" - try: - result = subprocess.run( - cmd, - shell=shell, - capture_output=True, - text=True, - timeout=timeout - ) - return result.returncode == 0, result.stdout, result.stderr - except subprocess.TimeoutExpired: - return False, "", "Command timed out" - except Exception as e: - return False, "", str(e) - -def check_postgresql_service(): - """Check if PostgreSQL service is running.""" - print("Checking PostgreSQL service...") - success, stdout, stderr = run_command('net start | findstr postgresql') - - if success and 'postgresql' in stdout.lower(): - print("โ PostgreSQL service is running") - return True - else: - print("โ PostgreSQL service not found") - return False - -def reset_postgresql_password(): - """Guide user through PostgreSQL password reset.""" - print("\n๐ง PostgreSQL Password Setup") - print("=" * 50) - - print("\nOption 1: Set PostgreSQL to trust local connections (easiest)") - print("This allows connections without a password from localhost.") - - response = input("\nWould you like to configure PostgreSQL for password-free local access? (y/n): ").lower() - - if response == 'y': - try: - # Find pg_hba.conf file - pg_data_dir = r"C:\Program Files\PostgreSQL\17\data" - pg_hba_file = os.path.join(pg_data_dir, "pg_hba.conf") - - if os.path.exists(pg_hba_file): - print(f"\nFound PostgreSQL config at: {pg_hba_file}") - print("\nโ ๏ธ Manual step required:") - print("1. Open Command Prompt as Administrator") - print("2. Run these commands:") - print(f' notepad "{pg_hba_file}"') - print("3. Find the line that starts with:") - print(" host all all 127.0.0.1/32 scram-sha-256") - print("4. Change 'scram-sha-256' to 'trust'") - print("5. Save the file") - print("6. Restart PostgreSQL service:") - print(" net stop postgresql-x64-17") - print(" net start postgresql-x64-17") - print("\nAfter making these changes, PostgreSQL will allow local connections without a password.") - return True - else: - print(f"โ Could not find pg_hba.conf at {pg_hba_file}") - return False - - except Exception as e: - print(f"โ Error: {e}") - return False - else: - print("\nOption 2: Set a password for PostgreSQL") - print("You'll need to set a password and update the .env file.") - - password = input("Enter a password for PostgreSQL user 'postgres': ") - if password: - print(f"\n๐ก Remember to update your .env file:") - print(f"DATABASE_URL=postgresql://postgres:{password}@localhost:5432/lyra") - return True - else: - print("โ No password provided") - return False - -def install_redis_alternative(): - """Install Redis using Chocolatey or provide manual instructions.""" - print("\n๐ด Redis Setup") - print("=" * 50) - - print("Checking for Redis alternatives...") - - # Try to install Redis using Chocolatey if available - success, stdout, stderr = run_command("choco --version", timeout=5) - - if success: - print("โ Chocolatey found! Installing Redis...") - success, stdout, stderr = run_command("choco install redis-64 -y", timeout=120) - - if success: - print("โ Redis installed via Chocolatey") - # Try to start Redis service - success, stdout, stderr = run_command("net start redis") - if success: - print("โ Redis service started") - return True - else: - print("โ ๏ธ Redis installed but service not started") - print("Try running: net start redis") - return True - else: - print("โ Redis installation via Chocolatey failed") - - # Fallback: Manual Redis installation instructions - print("\n๐ Manual Redis Installation:") - print("1. Download Redis for Windows from:") - print(" https://github.com/microsoftarchive/redis/releases") - print("2. Download Redis-x64-3.0.504.msi") - print("3. Install with default settings") - print("4. Start Redis service: net start redis") - - print("\n๐ Alternative: Use Docker (if you have it):") - print(" docker run -d -p 6379:6379 redis:alpine") - - print("\n๐ Alternative: Use Redis Cloud (free tier):") - print(" 1. Go to https://app.redislabs.com/") - print(" 2. Create free account") - print(" 3. Create database") - print(" 4. Update REDIS_URL in .env with cloud connection string") - - return False - -def update_env_file(): - """Update .env file with simplified database configuration.""" - print("\n๐ Updating .env file...") - - env_path = ".env" - if not os.path.exists(env_path): - print(f"โ .env file not found at {env_path}") - return False - - try: - # Read current .env - with open(env_path, 'r') as f: - lines = f.readlines() - - # Update database configuration - new_lines = [] - for line in lines: - if line.startswith('DATABASE_URL='): - # Set to trust local connection (no password) - new_lines.append('DATABASE_URL=postgresql://postgres@localhost:5432/lyra\n') - print("โ Updated DATABASE_URL for local trust authentication") - elif line.startswith('REDIS_URL='): - # Keep Redis as-is - new_lines.append(line) - else: - new_lines.append(line) - - # Write back to .env - with open(env_path, 'w') as f: - f.writelines(new_lines) - - print("โ .env file updated") - return True - - except Exception as e: - print(f"โ Error updating .env file: {e}") - return False - -def main(): - """Main setup function.""" - print("=" * 60) - print("LYRA AI - DATABASE SETUP FIXER") - print("=" * 60) - - # Check if we're in the right directory - if not os.path.exists('.env'): - print("โ Please run this script from the Lyra project directory") - print(" (The directory containing the .env file)") - return - - # Step 1: Check PostgreSQL - if check_postgresql_service(): - # Step 2: Fix PostgreSQL authentication - if reset_postgresql_password(): - print("โ PostgreSQL configuration ready") - else: - print("โ PostgreSQL configuration failed") - - # Step 3: Set up Redis - if install_redis_alternative(): - print("โ Redis setup complete") - else: - print("โ ๏ธ Redis needs manual setup (see instructions above)") - - # Step 4: Update .env file - update_env_file() - - print("\n" + "=" * 60) - print("๐ฏ NEXT STEPS:") - print("1. If you chose PostgreSQL trust authentication:") - print(" - Edit pg_hba.conf as shown above") - print(" - Restart PostgreSQL service") - print("2. Set up Redis using one of the methods above") - print("3. Run: python test_database_connections.py") - print("4. If tests pass, run: python -m lyra.main") - print("=" * 60) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/lyra/__init__.py b/lyra/__init__.py deleted file mode 100644 index 17a3be3..0000000 --- a/lyra/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Lyra - Advanced AI Discord Chatbot with Emotional Intelligence - -A sophisticated AI chatbot that learns, adapts, and responds with genuine emotion. -""" - -__version__ = "1.0.0" -__author__ = "Lyra Development Team" - -from lyra.core.lyra_model import LyraModel -from lyra.personality.matrix import PersonalityMatrix -from lyra.emotions.system import EmotionalSystem - -__all__ = ["LyraModel", "PersonalityMatrix", "EmotionalSystem"] \ No newline at end of file diff --git a/lyra/config.py b/lyra/config.py deleted file mode 100644 index ace7f2e..0000000 --- a/lyra/config.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -from pathlib import Path -from typing import Dict, Any -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(0, env="DISCORD_GUILD_ID") - - # Database Configuration - database_url: str = Field("sqlite:///data/lyra.db", env="DATABASE_URL") - redis_url: str = Field("redis://localhost:6379/0", env="REDIS_URL") - - # Model Configuration - model_path: str = Field("./models/checkpoints/lyra_model.pt", env="MODEL_PATH") - vocab_size: int = Field(50000, env="VOCAB_SIZE") - hidden_size: int = Field(768, env="HIDDEN_SIZE") - num_layers: int = Field(12, env="NUM_LAYERS") - num_heads: int = Field(12, env="NUM_HEADS") - context_length: int = Field(2048, env="CONTEXT_LENGTH") - max_memory_gb: float = Field(8.0, env="MAX_MEMORY_GB") - - # Training Configuration - batch_size: int = Field(16, env="BATCH_SIZE") - learning_rate: float = Field(5e-5, env="LEARNING_RATE") - max_epochs: int = Field(100, env="MAX_EPOCHS") - warmup_steps: int = Field(1000, env="WARMUP_STEPS") - save_every: int = Field(1000, env="SAVE_EVERY") - - # Personality Configuration - personality_update_frequency: int = Field(100, env="PERSONALITY_UPDATE_FREQUENCY") - emotion_decay_rate: float = Field(0.95, env="EMOTION_DECAY_RATE") - memory_retention_days: int = Field(30, env="MEMORY_RETENTION_DAYS") - - # Knowledge Acquisition - gutenberg_mirror: str = Field("https://www.gutenberg.org", env="GUTENBERG_MIRROR") - scraping_delay: float = Field(1.0, env="SCRAPING_DELAY") - max_concurrent_downloads: int = Field(5, env="MAX_CONCURRENT_DOWNLOADS") - - # Logging - log_level: str = Field("INFO", env="LOG_LEVEL") - log_file: str = Field("./logs/lyra.log", env="LOG_FILE") - - # Development - debug: bool = Field(False, env="DEBUG") - wandb_api_key: str = Field("", env="WANDB_API_KEY") - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" - - @property - def project_root(self) -> Path: - return Path(__file__).parent.parent - - @property - def data_dir(self) -> Path: - return self.project_root / "data" - - @property - def models_dir(self) -> Path: - return self.project_root / "models" - - @property - def logs_dir(self) -> Path: - return self.project_root / "logs" - - def ensure_directories(self): - """Ensure all required directories exist.""" - dirs = [self.data_dir, self.models_dir, self.logs_dir, - self.data_dir / "training", self.data_dir / "personality", - self.data_dir / "conversations", self.models_dir / "checkpoints", - self.models_dir / "configs"] - - for dir_path in dirs: - dir_path.mkdir(parents=True, exist_ok=True) - -config = LyraConfig() \ No newline at end of file diff --git a/lyra/core/__init__.py b/lyra/core/__init__.py deleted file mode 100644 index 4e05167..0000000 --- a/lyra/core/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Lyra Core Module - -Contains the fundamental AI architecture including the transformer model, -self-evolution system, and core intelligence mechanisms. -""" - -from .lyra_model import LyraModel -from .attention import MultiHeadAttention, SelfEvolvingAttention -from .transformer import LyraTransformerBlock, LyraTransformer -from .self_evolution import SelfEvolutionEngine - -__all__ = [ - "LyraModel", - "MultiHeadAttention", - "SelfEvolvingAttention", - "LyraTransformerBlock", - "LyraTransformer", - "SelfEvolutionEngine" -] \ No newline at end of file diff --git a/lyra/core/attention.py b/lyra/core/attention.py deleted file mode 100644 index 517f3c4..0000000 --- a/lyra/core/attention.py +++ /dev/null @@ -1,285 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math -from typing import Optional, Tuple, Dict, Any - -class SelfEvolvingAttention(nn.Module): - """ - Advanced attention mechanism that can evolve its attention patterns - based on conversation context and emotional state. - """ - - def __init__( - self, - embed_dim: int, - num_heads: int, - dropout: float = 0.1, - bias: bool = True, - evolution_rate: float = 0.001 - ): - super().__init__() - - self.embed_dim = embed_dim - self.num_heads = num_heads - self.head_dim = embed_dim // num_heads - self.evolution_rate = evolution_rate - - assert self.head_dim * num_heads == embed_dim, "embed_dim must be divisible by num_heads" - - # Standard attention components - self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - - # Evolution components - self.attention_evolution = nn.Parameter(torch.zeros(num_heads, 64, 64)) - self.emotional_attention_bias = nn.Parameter(torch.zeros(num_heads, 1, 1)) - self.context_adaptation = nn.Linear(embed_dim, num_heads) - - # Memory for attention patterns - self.register_buffer('attention_memory', torch.zeros(num_heads, 100, 100)) - self.register_buffer('memory_pointer', torch.zeros(1, dtype=torch.long)) - - self.dropout = nn.Dropout(dropout) - self.scale = math.sqrt(self.head_dim) - - self._init_parameters() - - def _init_parameters(self): - """Initialize parameters with careful scaling for evolution.""" - nn.init.xavier_uniform_(self.q_proj.weight) - nn.init.xavier_uniform_(self.k_proj.weight) - nn.init.xavier_uniform_(self.v_proj.weight) - nn.init.xavier_uniform_(self.out_proj.weight) - - if self.q_proj.bias is not None: - nn.init.constant_(self.q_proj.bias, 0.) - nn.init.constant_(self.k_proj.bias, 0.) - nn.init.constant_(self.v_proj.bias, 0.) - nn.init.constant_(self.out_proj.bias, 0.) - - # Initialize evolution parameters small - nn.init.normal_(self.attention_evolution, std=0.01) - nn.init.zeros_(self.emotional_attention_bias) - - def forward( - self, - query: torch.Tensor, - key: torch.Tensor, - value: torch.Tensor, - attn_mask: Optional[torch.Tensor] = None, - key_padding_mask: Optional[torch.Tensor] = None, - emotional_state: Optional[torch.Tensor] = None, - evolve: bool = True - ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, Any]]: - """ - Forward pass with attention evolution. - - Args: - query: Query tensor [batch, seq_len, embed_dim] - key: Key tensor [batch, seq_len, embed_dim] - value: Value tensor [batch, seq_len, embed_dim] - attn_mask: Attention mask - key_padding_mask: Key padding mask - emotional_state: Current emotional state [batch, emotion_dim] - evolve: Whether to apply evolution this step - - Returns: - output: Attention output - attention_weights: Attention weights - evolution_info: Information about evolution - """ - batch_size, seq_len, _ = query.shape - - # Project to Q, K, V - q = self.q_proj(query) - k = self.k_proj(key) - v = self.v_proj(value) - - # Reshape for multi-head attention - q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - - # Compute base attention scores - scores = torch.matmul(q, k.transpose(-2, -1)) / self.scale - - # Apply evolution to attention patterns - evolution_info = {} - if evolve and seq_len <= 64: # Only evolve for reasonable sequence lengths - # Get context-aware evolution weights - context_weights = self.context_adaptation(query.mean(dim=1)) # [batch, num_heads] - context_weights = torch.sigmoid(context_weights).unsqueeze(-1).unsqueeze(-1) - - # Apply learned evolution patterns - evolution_matrix = self.attention_evolution[:, :seq_len, :seq_len] - evolved_scores = scores + context_weights * evolution_matrix.unsqueeze(0) - - # Apply emotional bias if emotional state is provided - if emotional_state is not None: - emotional_influence = torch.sigmoid(emotional_state.mean(dim=-1, keepdim=True)) - emotional_bias = self.emotional_attention_bias * emotional_influence.unsqueeze(-1).unsqueeze(-1) - evolved_scores = evolved_scores + emotional_bias.unsqueeze(0) - - scores = evolved_scores - - evolution_info['context_weights'] = context_weights.mean().item() - evolution_info['evolution_magnitude'] = evolution_matrix.abs().mean().item() - - # Apply masks - if attn_mask is not None: - scores = scores.masked_fill(attn_mask == 0, float('-inf')) - - if key_padding_mask is not None: - scores = scores.masked_fill( - key_padding_mask.unsqueeze(1).unsqueeze(2), float('-inf') - ) - - # Compute attention weights - attention_weights = F.softmax(scores, dim=-1) - attention_weights = self.dropout(attention_weights) - - # Store attention pattern in memory for evolution - if evolve and seq_len <= 100: - self._store_attention_pattern(attention_weights.detach()) - - # Apply attention to values - output = torch.matmul(attention_weights, v) - - # Reshape back - output = output.transpose(1, 2).contiguous().view( - batch_size, seq_len, self.embed_dim - ) - - # Final projection - output = self.out_proj(output) - - return output, attention_weights, evolution_info - - def _store_attention_pattern(self, attention_weights: torch.Tensor): - """Store attention patterns for learning evolution.""" - batch_size, num_heads, seq_len, _ = attention_weights.shape - - if seq_len <= 100: - # Average across batch and store - avg_attention = attention_weights.mean(dim=0) # [num_heads, seq_len, seq_len] - - # Update memory buffer - pointer = self.memory_pointer.item() - memory_size = self.attention_memory.shape[1] - - if seq_len <= memory_size: - self.attention_memory[:, :seq_len, :seq_len] = ( - 0.95 * self.attention_memory[:, :seq_len, :seq_len] + - 0.05 * avg_attention - ) - - def evolve_attention_patterns(self, feedback_signal: float): - """ - Evolve attention patterns based on feedback. - - Args: - feedback_signal: Positive for good responses, negative for bad - """ - with torch.no_grad(): - # Use stored attention memory to update evolution matrix - memory_influence = self.attention_memory.mean(dim=0) # Average across heads - max_size = min(self.attention_evolution.shape[1], memory_influence.shape[0]) - - # Update evolution matrix based on successful patterns - update = feedback_signal * self.evolution_rate * memory_influence[:max_size, :max_size] - self.attention_evolution.data[:, :max_size, :max_size] += update.unsqueeze(0) - - # Clamp to prevent explosion - self.attention_evolution.data = torch.clamp( - self.attention_evolution.data, -1.0, 1.0 - ) - - def get_attention_diversity(self) -> float: - """Calculate how diverse the attention patterns are (cognitive flexibility).""" - with torch.no_grad(): - # Calculate entropy of stored attention patterns - attention_probs = F.softmax(self.attention_memory, dim=-1) - entropy = -torch.sum(attention_probs * torch.log(attention_probs + 1e-8), dim=-1) - return entropy.mean().item() - - -class MultiHeadAttention(nn.Module): - """ - Standard multi-head attention for comparison and fallback. - """ - - def __init__( - self, - embed_dim: int, - num_heads: int, - dropout: float = 0.1, - bias: bool = True - ): - super().__init__() - - self.embed_dim = embed_dim - self.num_heads = num_heads - self.head_dim = embed_dim // num_heads - - assert self.head_dim * num_heads == embed_dim - - self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias) - - self.dropout = nn.Dropout(dropout) - self.scale = math.sqrt(self.head_dim) - - def forward( - self, - query: torch.Tensor, - key: torch.Tensor, - value: torch.Tensor, - attn_mask: Optional[torch.Tensor] = None, - key_padding_mask: Optional[torch.Tensor] = None - ) -> Tuple[torch.Tensor, torch.Tensor]: - """Standard multi-head attention forward pass.""" - batch_size, seq_len, _ = query.shape - - # Project to Q, K, V - q = self.q_proj(query) - k = self.k_proj(key) - v = self.v_proj(value) - - # Reshape for multi-head attention - q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) - - # Compute attention scores - scores = torch.matmul(q, k.transpose(-2, -1)) / self.scale - - # Apply masks - if attn_mask is not None: - scores = scores.masked_fill(attn_mask == 0, float('-inf')) - - if key_padding_mask is not None: - scores = scores.masked_fill( - key_padding_mask.unsqueeze(1).unsqueeze(2), float('-inf') - ) - - # Compute attention weights - attention_weights = F.softmax(scores, dim=-1) - attention_weights = self.dropout(attention_weights) - - # Apply attention to values - output = torch.matmul(attention_weights, v) - - # Reshape back - output = output.transpose(1, 2).contiguous().view( - batch_size, seq_len, self.embed_dim - ) - - # Final projection - output = self.out_proj(output) - - return output, attention_weights \ No newline at end of file diff --git a/lyra/core/lyra_model.py b/lyra/core/lyra_model.py deleted file mode 100644 index 98a7ceb..0000000 --- a/lyra/core/lyra_model.py +++ /dev/null @@ -1,443 +0,0 @@ -""" -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'} - ) - } \ No newline at end of file diff --git a/lyra/core/self_evolution.py b/lyra/core/self_evolution.py deleted file mode 100644 index d5e6497..0000000 --- a/lyra/core/self_evolution.py +++ /dev/null @@ -1,348 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -from dataclasses import dataclass -import json -import logging -from pathlib import Path - -logger = logging.getLogger(__name__) - -@dataclass -class EvolutionMetrics: - """Tracks how Lyra is evolving over time.""" - conversation_satisfaction: float = 0.0 - learning_rate_adaptation: float = 0.0 - personality_drift: float = 0.0 - knowledge_expansion: float = 0.0 - emotional_growth: float = 0.0 - social_adaptation: float = 0.0 - creativity_index: float = 0.0 - coherence_score: float = 0.0 - -class SelfEvolutionEngine(nn.Module): - """ - Core self-evolution system that allows Lyra to adapt and grow like a real person. - - This system monitors her performance, emotional state, social interactions, - and continuously adapts her neural weights, personality traits, and behavior patterns. - """ - - def __init__( - self, - model_dim: int = 768, - evolution_rate: float = 0.001, - adaptation_threshold: float = 0.7, - personality_plasticity: float = 0.1, - memory_capacity: int = 10000, - device: Optional[torch.device] = None - ): - super().__init__() - - self.model_dim = model_dim - self.evolution_rate = evolution_rate - self.adaptation_threshold = adaptation_threshold - self.personality_plasticity = personality_plasticity - self.memory_capacity = memory_capacity - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - - # Evolution networks - self.adaptation_network = nn.Sequential( - nn.Linear(model_dim * 2, model_dim), - nn.LayerNorm(model_dim), - nn.GELU(), - nn.Dropout(0.1), - nn.Linear(model_dim, model_dim // 2), - nn.LayerNorm(model_dim // 2), - nn.GELU(), - nn.Linear(model_dim // 2, model_dim) - ) - - # Self-reflection mechanism - self.reflection_head = nn.MultiheadAttention( - embed_dim=model_dim, - num_heads=8, - dropout=0.1, - batch_first=True - ) - - # Meta-learning controller - self.meta_controller = nn.Sequential( - nn.Linear(model_dim, model_dim // 2), - nn.ReLU(), - nn.Linear(model_dim // 2, 5) # 5 evolution parameters - ) - - # Experience memory buffer - self.experience_buffer = [] - self.evolution_history = [] - - # Evolution metrics - self.metrics = EvolutionMetrics() - - # Adaptive learning rate - self.adaptive_lr = torch.nn.Parameter(torch.tensor(evolution_rate)) - - self.to(self.device) - - def forward( - self, - current_state: torch.Tensor, - context: torch.Tensor, - feedback_signal: Optional[torch.Tensor] = None - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Execute one step of self-evolution. - - Args: - current_state: Current model hidden state - context: Conversation/interaction context - feedback_signal: Optional feedback from environment - - Returns: - evolved_state: Updated model state - evolution_info: Information about the evolution step - """ - batch_size, seq_len, dim = current_state.shape - - # Self-reflection: Let Lyra examine her own thoughts - reflected_state, attention_weights = self.reflection_head( - current_state, current_state, current_state - ) - - # Combine current state with reflection - combined_state = torch.cat([current_state, reflected_state], dim=-1) - - # Generate adaptation signal - adaptation_signal = self.adaptation_network(combined_state) - - # Meta-learning: Adjust evolution parameters based on context - meta_params = self.meta_controller(context.mean(dim=1)) # [batch, 5] - - # Apply evolution with meta-learned parameters - evolution_strength = torch.sigmoid(meta_params[:, 0:1]).unsqueeze(1) # [batch, 1, 1] - personality_shift = torch.tanh(meta_params[:, 1:2]).unsqueeze(1) - learning_adaptation = torch.sigmoid(meta_params[:, 2:3]).unsqueeze(1) - emotional_weight = torch.sigmoid(meta_params[:, 3:4]).unsqueeze(1) - creativity_factor = torch.sigmoid(meta_params[:, 4:5]).unsqueeze(1) - - # Evolve the state - evolved_state = current_state + ( - evolution_strength * self.adaptive_lr * adaptation_signal + - personality_shift * self.personality_plasticity * reflected_state + - emotional_weight * 0.1 * torch.randn_like(current_state) * learning_adaptation - ) - - # Apply feedback if available - if feedback_signal is not None: - feedback_weight = torch.sigmoid(feedback_signal) - evolved_state = evolved_state * feedback_weight + current_state * (1 - feedback_weight) - - # Store experience for future learning - experience = { - 'state': current_state.detach().cpu(), - 'context': context.detach().cpu(), - 'evolution': evolved_state.detach().cpu(), - 'meta_params': meta_params.detach().cpu(), - 'timestamp': torch.tensor(float(torch.rand(1))) - } - self.store_experience(experience) - - # Update metrics - evolution_info = self.update_metrics( - current_state, evolved_state, meta_params, attention_weights - ) - - return evolved_state, evolution_info - - def store_experience(self, experience: Dict[str, torch.Tensor]): - """Store experience in memory buffer for future learning.""" - if len(self.experience_buffer) >= self.memory_capacity: - # Remove oldest experience - self.experience_buffer.pop(0) - - self.experience_buffer.append(experience) - - def update_metrics( - self, - old_state: torch.Tensor, - new_state: torch.Tensor, - meta_params: torch.Tensor, - attention_weights: torch.Tensor - ) -> Dict[str, Any]: - """Update evolution metrics and track growth.""" - with torch.no_grad(): - # Calculate state change magnitude - state_change = torch.norm(new_state - old_state, dim=-1).mean() - - # Update metrics - self.metrics.personality_drift = float(state_change * 0.1) - self.metrics.learning_rate_adaptation = float(meta_params[:, 2].mean()) - self.metrics.creativity_index = float(meta_params[:, 4].mean()) - - # Attention diversity (measure of cognitive flexibility) - attention_entropy = -torch.sum( - attention_weights * torch.log(attention_weights + 1e-8), dim=-1 - ).mean() - - evolution_info = { - 'state_change_magnitude': float(state_change), - 'attention_entropy': float(attention_entropy), - 'adaptive_lr': float(self.adaptive_lr), - 'metrics': self.metrics.__dict__.copy() - } - - self.evolution_history.append(evolution_info) - - return evolution_info - - def evolve_from_conversation( - self, - conversation_embedding: torch.Tensor, - user_satisfaction: float, - emotional_context: Dict[str, float] - ): - """ - Evolve based on a conversation interaction. - - This is where Lyra learns from each conversation like a human would. - """ - # Convert satisfaction to feedback signal - satisfaction_tensor = torch.tensor( - [[user_satisfaction]], device=self.device, dtype=torch.float32 - ) - - # Create emotional context tensor - emotional_values = list(emotional_context.values()) - emotional_tensor = torch.tensor( - [emotional_values], device=self.device, dtype=torch.float32 - ) - - # Evolve based on this interaction - evolved_embedding, evolution_info = self.forward( - conversation_embedding.unsqueeze(0), - emotional_tensor.unsqueeze(0), - satisfaction_tensor - ) - - # Update conversation satisfaction metric - self.metrics.conversation_satisfaction = ( - 0.9 * self.metrics.conversation_satisfaction + 0.1 * user_satisfaction - ) - - # Adapt learning rate based on satisfaction - if user_satisfaction > 0.8: - self.adaptive_lr.data *= 1.01 # Increase learning when doing well - elif user_satisfaction < 0.3: - self.adaptive_lr.data *= 0.99 # Decrease when struggling - - # Clamp learning rate - self.adaptive_lr.data = torch.clamp(self.adaptive_lr.data, 1e-6, 1e-2) - - return evolved_embedding.squeeze(0), evolution_info - - def long_term_evolution(self): - """ - Perform long-term evolutionary changes based on accumulated experience. - - This happens periodically (like during sleep for humans) to consolidate learning. - """ - if len(self.experience_buffer) < 100: # Need sufficient experience - return - - logger.info("Performing long-term evolution consolidation...") - - # Analyze patterns in stored experiences - recent_experiences = self.experience_buffer[-100:] - - # Extract patterns - state_changes = [] - meta_patterns = [] - - for exp in recent_experiences: - state_change = torch.norm(exp['evolution'] - exp['state'], dim=-1).mean() - state_changes.append(float(state_change)) - meta_patterns.append(exp['meta_params'].mean(0)) - - # Update long-term adaptation parameters - avg_change = np.mean(state_changes) - if avg_change > 0.1: # Too much change - stabilize - self.personality_plasticity *= 0.95 - elif avg_change < 0.01: # Too little change - increase plasticity - self.personality_plasticity *= 1.05 - - # Clamp plasticity - self.personality_plasticity = np.clip(self.personality_plasticity, 0.01, 0.3) - - # Update evolution rate based on performance - recent_satisfaction = self.metrics.conversation_satisfaction - if recent_satisfaction > 0.7: - self.evolution_rate *= 0.98 # Slower evolution when performing well - else: - self.evolution_rate *= 1.02 # Faster evolution when struggling - - logger.info(f"Evolution update - Plasticity: {self.personality_plasticity:.4f}, " - f"Rate: {self.evolution_rate:.6f}, Satisfaction: {recent_satisfaction:.3f}") - - def get_evolution_summary(self) -> Dict[str, Any]: - """Get a summary of Lyra's evolution and growth.""" - if not self.evolution_history: - return {"status": "no_evolution_data"} - - recent_history = self.evolution_history[-100:] if len(self.evolution_history) > 100 else self.evolution_history - - return { - "total_evolution_steps": len(self.evolution_history), - "current_metrics": self.metrics.__dict__, - "recent_growth_rate": np.mean([h["state_change_magnitude"] for h in recent_history]), - "personality_plasticity": self.personality_plasticity, - "adaptive_learning_rate": float(self.adaptive_lr), - "experience_buffer_size": len(self.experience_buffer), - "cognitive_flexibility": np.mean([h["attention_entropy"] for h in recent_history]) - } - - def save_evolution_state(self, path: Path): - """Save evolution state for persistence.""" - state = { - "metrics": self.metrics.__dict__, - "evolution_history": self.evolution_history[-1000:], # Keep recent history - "personality_plasticity": self.personality_plasticity, - "evolution_rate": self.evolution_rate, - "adaptive_lr": float(self.adaptive_lr), - "model_state": self.state_dict() - } - - with open(path, 'w') as f: - json.dump(state, f, indent=2, default=str) - - def load_evolution_state(self, path: Path): - """Load evolution state from file.""" - if not path.exists(): - logger.warning(f"Evolution state file not found: {path}") - return - - try: - with open(path, 'r') as f: - state = json.load(f) - - # Restore metrics - for key, value in state["metrics"].items(): - setattr(self.metrics, key, value) - - self.evolution_history = state.get("evolution_history", []) - self.personality_plasticity = state.get("personality_plasticity", 0.1) - self.evolution_rate = state.get("evolution_rate", 0.001) - - if "adaptive_lr" in state: - self.adaptive_lr.data = torch.tensor(state["adaptive_lr"]) - - # Load model state - if "model_state" in state: - self.load_state_dict(state["model_state"]) - - logger.info(f"Evolution state loaded from {path}") - - except Exception as e: - logger.error(f"Failed to load evolution state: {e}") \ No newline at end of file diff --git a/lyra/core/thinking_agent.py b/lyra/core/thinking_agent.py deleted file mode 100644 index 2e24f7e..0000000 --- a/lyra/core/thinking_agent.py +++ /dev/null @@ -1,727 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -import logging -import json -from datetime import datetime - -from .transformer import LyraTransformer -from ..personality.matrix import PersonalityMatrix -from ..emotions.system import EmotionalSystem, EmotionalState - -logger = logging.getLogger(__name__) - -class ThoughtProcess: - """Represents a single thought process with analysis and reasoning.""" - - def __init__( - self, - thought_type: str, - content: str, - confidence: float, - reasoning: str, - emotional_influence: float = 0.0, - personality_influence: float = 0.0 - ): - self.thought_type = thought_type - self.content = content - self.confidence = confidence - self.reasoning = reasoning - self.emotional_influence = emotional_influence - self.personality_influence = personality_influence - self.timestamp = datetime.now() - -class ThinkingAgent(nn.Module): - """ - Behind-the-scenes thinking agent that gives Lyra genuine internal thoughts - before responding, making her conversations feel more natural and human. - - This agent simulates the internal dialogue humans have before speaking, - including consideration of context, emotional state, personality, and - potential response strategies. - """ - - def __init__( - self, - model_dim: int = 768, - thought_types: int = 8, - max_thought_depth: int = 5, - device: Optional[torch.device] = None - ): - super().__init__() - - self.model_dim = model_dim - self.thought_types = thought_types - self.max_thought_depth = max_thought_depth - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - - # Thought analysis networks - self.context_analyzer = nn.Sequential( - nn.Linear(model_dim, 512), - nn.LayerNorm(512), - nn.ReLU(), - nn.Dropout(0.1), - nn.Linear(512, 256), - nn.ReLU(), - nn.Linear(256, 128) - ) - - # Thought generation network - self.thought_generator = nn.Sequential( - nn.Linear(128 + 24 + 19, 256), # context + personality + emotions - nn.LayerNorm(256), - nn.ReLU(), - nn.Linear(256, 128), - nn.ReLU(), - nn.Linear(128, model_dim) - ) - - # Thought classification network - self.thought_classifier = nn.Sequential( - nn.Linear(model_dim, 128), - nn.ReLU(), - nn.Linear(128, 64), - nn.ReLU(), - nn.Linear(64, thought_types), - nn.Softmax(dim=-1) - ) - - # Confidence estimation - self.confidence_estimator = nn.Sequential( - nn.Linear(model_dim, 64), - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, 1), - nn.Sigmoid() - ) - - # Response strategy network - self.strategy_network = nn.Sequential( - nn.Linear(model_dim * 2, 256), # Current thought + context - nn.LayerNorm(256), - nn.ReLU(), - nn.Linear(256, 128), - nn.ReLU(), - nn.Linear(128, 10) # Different response strategies - ) - - # Thought type definitions - self.thought_type_names = [ - 'analytical', # Breaking down the problem/question - 'emotional', # Considering emotional aspects - 'empathetic', # Understanding the other person's perspective - 'creative', # Generating novel ideas or approaches - 'cautious', # Considering potential risks or downsides - 'curious', # Wanting to learn more or ask questions - 'supportive', # Thinking about how to help or encourage - 'reflective' # Self-reflection and meta-thinking - ] - - # Internal thought history - self.thought_history: List[ThoughtProcess] = [] - self.current_thought_chain: List[ThoughtProcess] = [] - - # Thinking patterns learned from experience - self.thinking_patterns = { - 'successful_strategies': {}, - 'failed_strategies': {}, - 'context_preferences': {}, - 'personality_thinking_styles': {} - } - - self.to(self.device) - - def forward( - self, - context_embedding: torch.Tensor, - personality_state: torch.Tensor, - emotional_state: torch.Tensor, - user_message: str, - conversation_history: Optional[List[str]] = None - ) -> Tuple[List[ThoughtProcess], Dict[str, Any]]: - """ - Generate internal thoughts about the current situation before responding. - - Args: - context_embedding: Current conversation context - personality_state: Current personality state - emotional_state: Current emotional state - user_message: The message Lyra is responding to - conversation_history: Recent conversation for context - - Returns: - thought_chain: Sequence of internal thoughts - thinking_info: Information about the thinking process - """ - batch_size = context_embedding.shape[0] - - # Analyze context - context_features = self.context_analyzer(context_embedding.mean(dim=1)) - - # Start new thought chain - self.current_thought_chain = [] - - # Generate sequence of thoughts - for depth in range(self.max_thought_depth): - # Combine all inputs for thought generation - thought_input = torch.cat([ - context_features, - personality_state, - emotional_state - ], dim=1) - - # Generate thought representation - thought_representation = self.thought_generator(thought_input) - - # Classify thought type - thought_type_probs = self.thought_classifier(thought_representation) - thought_type_idx = torch.argmax(thought_type_probs, dim=-1)[0].item() - thought_type = self.thought_type_names[thought_type_idx] - - # Estimate confidence - confidence = self.confidence_estimator(thought_representation)[0, 0].item() - - # Generate actual thought content - thought_content, reasoning = self._generate_thought_content( - thought_type, user_message, context_features, - personality_state, emotional_state, conversation_history - ) - - # Calculate influences - emotional_influence = torch.norm(emotional_state).item() / 5.0 # Normalize - personality_influence = torch.norm(personality_state).item() / 5.0 - - # Create thought process - thought = ThoughtProcess( - thought_type=thought_type, - content=thought_content, - confidence=confidence, - reasoning=reasoning, - emotional_influence=emotional_influence, - personality_influence=personality_influence - ) - - self.current_thought_chain.append(thought) - - # Decide if we need more thoughts - if confidence > 0.8 or depth == self.max_thought_depth - 1: - break - - # Update context for next thought - context_features = context_features + 0.1 * thought_representation[0] - - # Store in history - self.thought_history.extend(self.current_thought_chain) - - # Keep history manageable - if len(self.thought_history) > 1000: - self.thought_history = self.thought_history[-500:] - - # Prepare thinking info - thinking_info = { - 'total_thoughts': len(self.current_thought_chain), - 'thought_types': [t.thought_type for t in self.current_thought_chain], - 'avg_confidence': np.mean([t.confidence for t in self.current_thought_chain]), - 'dominant_influences': self._analyze_thought_influences(), - 'thinking_time': len(self.current_thought_chain) * 0.5 # Simulated thinking time - } - - return self.current_thought_chain, thinking_info - - def _generate_thought_content( - self, - thought_type: str, - user_message: str, - context_features: torch.Tensor, - personality_state: torch.Tensor, - emotional_state: torch.Tensor, - conversation_history: Optional[List[str]] - ) -> Tuple[str, str]: - """Generate the actual content of a thought based on its type.""" - - # Get key information for thought generation - context_strength = torch.norm(context_features).item() - emotional_intensity = torch.norm(emotional_state).item() - personality_dominance = self._get_dominant_personality_traits(personality_state) - - if thought_type == 'analytical': - return self._generate_analytical_thought( - user_message, context_strength, personality_dominance - ) - - elif thought_type == 'emotional': - return self._generate_emotional_thought( - user_message, emotional_state, emotional_intensity - ) - - elif thought_type == 'empathetic': - return self._generate_empathetic_thought( - user_message, conversation_history, personality_dominance - ) - - elif thought_type == 'creative': - return self._generate_creative_thought( - user_message, context_strength, personality_dominance - ) - - elif thought_type == 'cautious': - return self._generate_cautious_thought( - user_message, emotional_state, personality_dominance - ) - - elif thought_type == 'curious': - return self._generate_curious_thought( - user_message, context_strength, personality_dominance - ) - - elif thought_type == 'supportive': - return self._generate_supportive_thought( - user_message, emotional_state, personality_dominance - ) - - elif thought_type == 'reflective': - return self._generate_reflective_thought( - user_message, conversation_history, personality_dominance - ) - - else: - return "I'm thinking about this...", "General consideration" - - def _generate_analytical_thought( - self, - user_message: str, - context_strength: float, - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate analytical thinking about the user's message.""" - - # Analyze message structure and content - analysis_aspects = [] - - if '?' in user_message: - analysis_aspects.append("They're asking a question") - - if any(word in user_message.lower() for word in ['help', 'problem', 'issue', 'stuck']): - analysis_aspects.append("They seem to need assistance") - - if any(word in user_message.lower() for word in ['happy', 'excited', 'great', 'awesome']): - analysis_aspects.append("They sound positive") - - if any(word in user_message.lower() for word in ['sad', 'upset', 'worried', 'anxious']): - analysis_aspects.append("They might be experiencing negative emotions") - - if len(user_message.split()) > 20: - analysis_aspects.append("This is a detailed message - they want to share something important") - elif len(user_message.split()) < 5: - analysis_aspects.append("Short message - might be casual or they're being brief") - - # Consider personality influence - if personality_dominance.get('intellectualism', 0) > 0.7: - analysis_aspects.append("I should provide a thorough, well-reasoned response") - - if personality_dominance.get('conscientiousness', 0) > 0.7: - analysis_aspects.append("I need to be careful and accurate in my response") - - if analysis_aspects: - thought = f"Let me analyze this: {', '.join(analysis_aspects[:3])}" - reasoning = "Breaking down the message to understand what they really need" - else: - thought = "I need to think through what they're really asking me" - reasoning = "Analyzing the underlying intent of their message" - - return thought, reasoning - - def _generate_emotional_thought( - self, - user_message: str, - emotional_state: torch.Tensor, - emotional_intensity: float - ) -> Tuple[str, str]: - """Generate thoughts about emotional aspects.""" - - # Convert emotional state to understand current feelings - emotions = emotional_state[0].detach().cpu().numpy() - joy, sadness, anger, fear = emotions[0], emotions[1], emotions[2], emotions[3] - trust, curiosity = emotions[6], emotions[15] - - if emotional_intensity > 0.7: - if joy > 0.7: - thought = "I'm feeling really positive about this conversation!" - reasoning = "High joy is influencing my emotional perspective" - elif sadness > 0.6: - thought = "Something about this makes me feel a bit melancholy..." - reasoning = "Sadness is coloring my emotional response" - elif curiosity > 0.8: - thought = "I'm genuinely curious about what they're sharing" - reasoning = "Strong curiosity is driving my emotional engagement" - else: - thought = "I'm having a strong emotional reaction to this" - reasoning = "High emotional intensity requires consideration" - else: - if trust > 0.7: - thought = "I feel comfortable and safe in this conversation" - reasoning = "Trust is creating a positive emotional foundation" - elif fear > 0.5: - thought = "I'm feeling a bit uncertain about how to respond" - reasoning = "Fear is making me more cautious emotionally" - else: - thought = "My emotions feel balanced right now" - reasoning = "Moderate emotional state allows for clear thinking" - - return thought, reasoning - - def _generate_empathetic_thought( - self, - user_message: str, - conversation_history: Optional[List[str]], - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate empathetic thoughts about the user's perspective.""" - - empathy_level = personality_dominance.get('empathy_level', 0.5) - - # Look for emotional cues in the message - emotional_indicators = { - 'stress': ['stressed', 'overwhelmed', 'pressure', 'too much'], - 'excitement': ['excited', 'amazing', 'can\'t wait', 'thrilled'], - 'confusion': ['confused', 'don\'t understand', 'not sure', 'unclear'], - 'sadness': ['sad', 'down', 'upset', 'disappointed'], - 'frustration': ['frustrated', 'annoying', 'difficult', 'hard'] - } - - detected_emotion = None - for emotion, indicators in emotional_indicators.items(): - if any(indicator in user_message.lower() for indicator in indicators): - detected_emotion = emotion - break - - if empathy_level > 0.7: - if detected_emotion: - thoughts = { - 'stress': "They sound really overwhelmed. I want to help them feel supported.", - 'excitement': "I can feel their enthusiasm! I should match their energy.", - 'confusion': "They're genuinely confused. I need to be patient and clear.", - 'sadness': "They're going through something difficult. I should be gentle.", - 'frustration': "I can sense their frustration. I need to acknowledge that." - } - thought = thoughts.get(detected_emotion, "I can sense what they're feeling") - reasoning = f"High empathy detected {detected_emotion} in their message" - else: - thought = "I wonder how they're really feeling about this situation" - reasoning = "Empathetic consideration of their emotional state" - else: - if detected_emotion: - thought = f"They seem to be feeling {detected_emotion}" - reasoning = "Basic emotional recognition" - else: - thought = "I should consider their perspective on this" - reasoning = "Standard empathetic consideration" - - return thought, reasoning - - def _generate_creative_thought( - self, - user_message: str, - context_strength: float, - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate creative thinking about unique responses or approaches.""" - - creativity_level = personality_dominance.get('creativity', 0.5) - openness = personality_dominance.get('openness', 0.5) - - if creativity_level > 0.7 and openness > 0.6: - creative_thoughts = [ - "What if I approached this from a completely different angle?", - "There might be an unconventional way to help with this", - "I could try something creative here that they wouldn't expect", - "This reminds me of an interesting connection I could make", - "Maybe I can use a metaphor or analogy to explain this better" - ] - thought = np.random.choice(creative_thoughts) - reasoning = "High creativity and openness driving innovative thinking" - - elif creativity_level > 0.5: - thought = "I should think of an interesting way to respond to this" - reasoning = "Moderate creativity seeking engaging response approach" - - else: - thought = "Let me think of a helpful way to address this" - reasoning = "Basic creative consideration for response approach" - - return thought, reasoning - - def _generate_cautious_thought( - self, - user_message: str, - emotional_state: torch.Tensor, - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate cautious thoughts about potential risks or misunderstandings.""" - - conscientiousness = personality_dominance.get('conscientiousness', 0.5) - neuroticism = personality_dominance.get('neuroticism', 0.5) - - # Look for sensitive topics - sensitive_indicators = [ - 'personal', 'private', 'secret', 'confidential', 'depression', - 'anxiety', 'relationship', 'family', 'work', 'financial' - ] - - is_sensitive = any(indicator in user_message.lower() for indicator in sensitive_indicators) - - if conscientiousness > 0.7 or neuroticism > 0.6: - if is_sensitive: - thought = "I need to be really careful here - this seems personal and sensitive" - reasoning = "High conscientiousness/neuroticism detecting sensitive content" - elif '?' in user_message and any(word in user_message.lower() for word in ['should', 'advice', 'recommend']): - thought = "They're asking for advice. I should be thoughtful and not overstep" - reasoning = "Caution about providing advice responsibly" - else: - thought = "I want to make sure I don't misunderstand or say something wrong" - reasoning = "General caution about response accuracy" - else: - thought = "I should be thoughtful about how I respond to this" - reasoning = "Basic cautious consideration" - - return thought, reasoning - - def _generate_curious_thought( - self, - user_message: str, - context_strength: float, - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate curious thoughts about learning more.""" - - curiosity_level = personality_dominance.get('curiosity', 0.5) - openness = personality_dominance.get('openness', 0.5) - - if curiosity_level > 0.8: - if '?' not in user_message: - thought = "I'm really curious about this - I want to ask them more!" - reasoning = "High curiosity driving desire for deeper exploration" - else: - thought = "This is fascinating! I want to understand this better" - reasoning = "High curiosity engaged by their question" - - elif curiosity_level > 0.6: - thought = "I wonder if there's more to this story" - reasoning = "Moderate curiosity seeking additional context" - - else: - thought = "It might be good to learn more about what they mean" - reasoning = "Basic curiosity for clarification" - - return thought, reasoning - - def _generate_supportive_thought( - self, - user_message: str, - emotional_state: torch.Tensor, - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate supportive thoughts about helping the user.""" - - supportiveness = personality_dominance.get('supportiveness', 0.5) - agreeableness = personality_dominance.get('agreeableness', 0.5) - - # Look for indicators they need support - support_indicators = [ - 'help', 'stuck', 'difficult', 'hard', 'struggling', 'problem', - 'don\'t know', 'confused', 'worried', 'scared' - ] - - needs_support = any(indicator in user_message.lower() for indicator in support_indicators) - - if supportiveness > 0.8: - if needs_support: - thought = "I really want to help them through this. How can I be most supportive?" - reasoning = "High supportiveness responding to detected need" - else: - thought = "I want to make sure they feel heard and valued" - reasoning = "High supportiveness providing general emotional support" - - elif supportiveness > 0.6: - thought = "I should try to be helpful and encouraging" - reasoning = "Moderate supportiveness seeking to assist" - - else: - thought = "I hope I can be useful to them" - reasoning = "Basic supportive consideration" - - return thought, reasoning - - def _generate_reflective_thought( - self, - user_message: str, - conversation_history: Optional[List[str]], - personality_dominance: Dict[str, float] - ) -> Tuple[str, str]: - """Generate reflective meta-thoughts about the conversation or self.""" - - emotional_clarity = personality_dominance.get('emotional_clarity', 0.5) - intellectualism = personality_dominance.get('intellectualism', 0.5) - - if conversation_history and len(conversation_history) > 3: - if intellectualism > 0.7: - thought = "Looking at our conversation, I notice patterns in how we communicate" - reasoning = "High intellectualism driving meta-analysis of interaction" - else: - thought = "I'm thinking about how this conversation has been going" - reasoning = "Reflective consideration of conversation flow" - - elif emotional_clarity > 0.7: - thought = "I'm aware of how my own emotions are influencing my thinking right now" - reasoning = "High emotional clarity enabling self-awareness" - - else: - reflective_thoughts = [ - "I'm wondering what they really need from me in this moment", - "This conversation is making me think about my own experiences", - "I'm noticing how I want to respond versus how I should respond" - ] - thought = np.random.choice(reflective_thoughts) - reasoning = "General reflective self-awareness" - - return thought, reasoning - - def _get_dominant_personality_traits(self, personality_state: torch.Tensor) -> Dict[str, float]: - """Extract dominant personality traits from state tensor.""" - # This would map to actual personality trait indices - traits = personality_state[0].detach().cpu().numpy() - - trait_names = [ - 'openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism', - 'humor_level', 'sarcasm_tendency', 'empathy_level', 'curiosity', 'playfulness', - 'intellectualism', 'spontaneity', 'supportiveness', 'assertiveness', 'creativity', - 'emotional_clarity', 'empathy_level', 'confidence', 'adaptability' - ] - - return { - name: float(traits[i]) if i < len(traits) else 0.5 - for i, name in enumerate(trait_names) - } - - def _analyze_thought_influences(self) -> Dict[str, float]: - """Analyze what factors are most influencing current thoughts.""" - if not self.current_thought_chain: - return {} - - influences = { - 'emotional': np.mean([t.emotional_influence for t in self.current_thought_chain]), - 'personality': np.mean([t.personality_influence for t in self.current_thought_chain]), - 'contextual': 1.0 - np.mean([t.emotional_influence + t.personality_influence for t in self.current_thought_chain]) / 2 - } - - return influences - - def get_thinking_summary(self) -> Dict[str, Any]: - """Get a summary of recent thinking patterns.""" - if not self.thought_history: - return {'status': 'no_thinking_history'} - - recent_thoughts = self.thought_history[-50:] # Last 50 thoughts - - thought_type_counts = {} - for thought in recent_thoughts: - thought_type_counts[thought.thought_type] = thought_type_counts.get(thought.thought_type, 0) + 1 - - return { - 'total_thoughts': len(self.thought_history), - 'recent_thoughts': len(recent_thoughts), - 'thought_type_distribution': thought_type_counts, - 'avg_confidence': np.mean([t.confidence for t in recent_thoughts]), - 'avg_emotional_influence': np.mean([t.emotional_influence for t in recent_thoughts]), - 'avg_personality_influence': np.mean([t.personality_influence for t in recent_thoughts]), - 'most_common_thought_type': max(thought_type_counts.items(), key=lambda x: x[1])[0] if thought_type_counts else None - } - - def learn_from_response_feedback( - self, - thought_chain: List[ThoughtProcess], - response_quality: float, - user_satisfaction: float - ): - """Learn which thinking patterns lead to better responses.""" - - # Analyze which thought types were used - thought_types_used = [t.thought_type for t in thought_chain] - avg_confidence = np.mean([t.confidence for t in thought_chain]) - - # Store pattern success - pattern_key = '-'.join(sorted(set(thought_types_used))) - - if pattern_key not in self.thinking_patterns['successful_strategies']: - self.thinking_patterns['successful_strategies'][pattern_key] = { - 'success_count': 0, - 'total_count': 0, - 'avg_satisfaction': 0.0 - } - - pattern_data = self.thinking_patterns['successful_strategies'][pattern_key] - pattern_data['total_count'] += 1 - - if response_quality > 0.7 and user_satisfaction > 0.6: - pattern_data['success_count'] += 1 - - pattern_data['avg_satisfaction'] = ( - (pattern_data['avg_satisfaction'] * (pattern_data['total_count'] - 1) + user_satisfaction) / - pattern_data['total_count'] - ) - - logger.debug(f"Updated thinking pattern learning: {pattern_key} " - f"(success rate: {pattern_data['success_count']/pattern_data['total_count']:.2f})") - - def get_optimal_thinking_strategy(self, context_type: str) -> List[str]: - """Get the optimal thinking strategy for a given context.""" - - # Default strategy - default_strategy = ['analytical', 'empathetic', 'supportive'] - - if context_type not in self.thinking_patterns.get('context_preferences', {}): - return default_strategy - - context_data = self.thinking_patterns['context_preferences'][context_type] - - # Find strategies with highest success rates - successful_strategies = [ - (pattern, data['success_count'] / max(1, data['total_count'])) - for pattern, data in self.thinking_patterns['successful_strategies'].items() - if data['total_count'] > 2 # Minimum sample size - ] - - if successful_strategies: - # Get the most successful strategy - best_strategy = max(successful_strategies, key=lambda x: x[1]) - return best_strategy[0].split('-') - - return default_strategy - - def simulate_internal_dialogue(self, scenario: str) -> List[ThoughtProcess]: - """Simulate internal dialogue for a given scenario (for testing/analysis).""" - - # Create mock inputs for simulation - device = self.device - context_embedding = torch.randn(1, 10, self.model_dim, device=device) - personality_state = torch.rand(1, 24, device=device) - emotional_state = torch.rand(1, 19, device=device) - - # Generate thought chain - thought_chain, _ = self.forward( - context_embedding, personality_state, emotional_state, scenario - ) - - return thought_chain - - def export_thinking_patterns(self) -> Dict[str, Any]: - """Export learned thinking patterns for analysis.""" - return { - 'thinking_patterns': self.thinking_patterns, - 'thought_history_summary': self.get_thinking_summary(), - 'thought_type_names': self.thought_type_names, - 'total_thinking_experiences': len(self.thought_history) - } \ No newline at end of file diff --git a/lyra/core/transformer.py b/lyra/core/transformer.py deleted file mode 100644 index b789d4b..0000000 --- a/lyra/core/transformer.py +++ /dev/null @@ -1,550 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -from typing import Optional, Tuple, Dict, Any -import math - -from .attention import SelfEvolvingAttention, MultiHeadAttention - -class PositionalEncoding(nn.Module): - """Sinusoidal positional encoding with learnable scaling.""" - - def __init__(self, embed_dim: int, max_len: int = 5000, dropout: float = 0.1): - super().__init__() - - self.dropout = nn.Dropout(dropout) - self.scale = nn.Parameter(torch.ones(1)) - - pe = torch.zeros(max_len, embed_dim) - position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) - - div_term = torch.exp(torch.arange(0, embed_dim, 2).float() * - (-math.log(10000.0) / embed_dim)) - - pe[:, 0::2] = torch.sin(position * div_term) - pe[:, 1::2] = torch.cos(position * div_term) - - self.register_buffer('pe', pe.unsqueeze(0)) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - seq_len = x.size(1) - x = x + self.scale * self.pe[:, :seq_len] - return self.dropout(x) - - -class LayerNorm(nn.Module): - """Layer normalization with learnable parameters and bias.""" - - def __init__(self, embed_dim: int, eps: float = 1e-5): - super().__init__() - self.eps = eps - self.weight = nn.Parameter(torch.ones(embed_dim)) - self.bias = nn.Parameter(torch.zeros(embed_dim)) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - mean = x.mean(dim=-1, keepdim=True) - std = x.std(dim=-1, keepdim=True) - return self.weight * (x - mean) / (std + self.eps) + self.bias - - -class FeedForward(nn.Module): - """Enhanced feedforward network with adaptive activation.""" - - def __init__( - self, - embed_dim: int, - ff_dim: int, - dropout: float = 0.1, - activation: str = "gelu" - ): - super().__init__() - - self.embed_dim = embed_dim - self.ff_dim = ff_dim - - # Standard feedforward layers - self.linear1 = nn.Linear(embed_dim, ff_dim) - self.linear2 = nn.Linear(ff_dim, embed_dim) - self.dropout = nn.Dropout(dropout) - - # Adaptive activation - can learn to emphasize different patterns - self.activation_gate = nn.Linear(embed_dim, ff_dim) - - # Choose activation function - if activation == "gelu": - self.activation = nn.GELU() - elif activation == "relu": - self.activation = nn.ReLU() - elif activation == "swish": - self.activation = nn.SiLU() - else: - self.activation = nn.GELU() - - def forward(self, x: torch.Tensor) -> torch.Tensor: - # Standard feedforward path - h = self.linear1(x) - h = self.activation(h) - - # Adaptive gating based on input - gate = torch.sigmoid(self.activation_gate(x)) - h = h * gate - - h = self.dropout(h) - return self.linear2(h) - - -class LyraTransformerBlock(nn.Module): - """ - Transformer block with self-evolution capabilities. - - This block can adapt its behavior based on conversation context, - emotional state, and past interaction success. - """ - - def __init__( - self, - embed_dim: int, - num_heads: int, - ff_dim: int, - dropout: float = 0.1, - use_evolution: bool = True, - layer_id: int = 0 - ): - super().__init__() - - self.embed_dim = embed_dim - self.num_heads = num_heads - self.layer_id = layer_id - self.use_evolution = use_evolution - - # Attention mechanism - if use_evolution: - self.attention = SelfEvolvingAttention( - embed_dim=embed_dim, - num_heads=num_heads, - dropout=dropout - ) - else: - self.attention = MultiHeadAttention( - embed_dim=embed_dim, - num_heads=num_heads, - dropout=dropout - ) - - # Layer normalization - self.norm1 = LayerNorm(embed_dim) - self.norm2 = LayerNorm(embed_dim) - - # Feedforward network - self.feedforward = FeedForward( - embed_dim=embed_dim, - ff_dim=ff_dim, - dropout=dropout - ) - - # Evolution-specific components - if use_evolution: - # Emotional influence on processing - self.emotional_projection = nn.Linear(embed_dim, embed_dim // 4) - self.emotional_gate = nn.Linear(embed_dim // 4, embed_dim) - - # Layer-specific adaptation parameters - self.adaptation_strength = nn.Parameter(torch.ones(1) * 0.1) - self.emotional_sensitivity = nn.Parameter(torch.ones(1) * 0.5) - - self.dropout = nn.Dropout(dropout) - - def forward( - self, - x: torch.Tensor, - attn_mask: Optional[torch.Tensor] = None, - key_padding_mask: Optional[torch.Tensor] = None, - emotional_state: Optional[torch.Tensor] = None, - evolve: bool = True - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Forward pass through transformer block. - - Args: - x: Input tensor [batch, seq_len, embed_dim] - attn_mask: Attention mask - key_padding_mask: Key padding mask - emotional_state: Current emotional state - evolve: Whether to apply evolution this step - - Returns: - output: Block output - layer_info: Information about this layer's processing - """ - layer_info = {} - - # Store input for residual - residual = x - - # Pre-normalization - x_norm = self.norm1(x) - - # Self-attention - if self.use_evolution and isinstance(self.attention, SelfEvolvingAttention): - attn_out, attn_weights, evolution_info = self.attention( - query=x_norm, - key=x_norm, - value=x_norm, - attn_mask=attn_mask, - key_padding_mask=key_padding_mask, - emotional_state=emotional_state, - evolve=evolve and self.training - ) - layer_info.update(evolution_info) - else: - attn_out, attn_weights = self.attention( - query=x_norm, - key=x_norm, - value=x_norm, - attn_mask=attn_mask, - key_padding_mask=key_padding_mask - ) - - # Apply emotional influence if available - if self.use_evolution and emotional_state is not None: - emotional_features = self.emotional_projection(emotional_state.mean(dim=1, keepdim=True)) - emotional_gate_values = torch.sigmoid(self.emotional_gate(emotional_features)) - - # Apply emotional gating - emotional_influence = self.emotional_sensitivity * emotional_gate_values - attn_out = attn_out * (1 + emotional_influence) - - layer_info['emotional_influence'] = emotional_influence.mean().item() - - # First residual connection - x = residual + self.dropout(attn_out) - - # Second sublayer: feedforward - residual = x - x_norm = self.norm2(x) - ff_out = self.feedforward(x_norm) - - # Second residual connection - x = residual + self.dropout(ff_out) - - # Store layer statistics - layer_info.update({ - 'layer_id': self.layer_id, - 'attention_entropy': self._compute_attention_entropy(attn_weights), - 'activation_magnitude': x.abs().mean().item(), - 'gradient_norm': None # Will be filled during backward pass if needed - }) - - return x, layer_info - - def _compute_attention_entropy(self, attn_weights: torch.Tensor) -> float: - """Compute entropy of attention weights (measure of focus vs. distribution).""" - # attn_weights: [batch, num_heads, seq_len, seq_len] - with torch.no_grad(): - # Average across batch and heads - avg_attn = attn_weights.mean(dim=(0, 1)) # [seq_len, seq_len] - - # Compute row-wise entropy (how spread out each token's attention is) - row_entropy = -torch.sum(avg_attn * torch.log(avg_attn + 1e-8), dim=-1) - return row_entropy.mean().item() - - def evolve_from_feedback(self, feedback_signal: float): - """Update layer parameters based on conversation feedback.""" - if not self.use_evolution: - return - - with torch.no_grad(): - # Update adaptation strength based on feedback - if feedback_signal > 0.7: # Good feedback - self.adaptation_strength.data *= 1.01 - self.emotional_sensitivity.data *= 0.99 # Less emotional when doing well - elif feedback_signal < 0.3: # Poor feedback - self.adaptation_strength.data *= 0.99 - self.emotional_sensitivity.data *= 1.01 # More emotional when struggling - - # Clamp parameters - self.adaptation_strength.data = torch.clamp(self.adaptation_strength.data, 0.01, 0.5) - self.emotional_sensitivity.data = torch.clamp(self.emotional_sensitivity.data, 0.1, 2.0) - - # Evolve attention patterns if using evolving attention - if isinstance(self.attention, SelfEvolvingAttention): - self.attention.evolve_attention_patterns(feedback_signal) - - -class LyraTransformer(nn.Module): - """ - Complete transformer model with self-evolution capabilities. - - This is the core of Lyra's language understanding and generation, - with the ability to adapt and evolve based on interactions. - """ - - def __init__( - self, - vocab_size: int, - embed_dim: int = 768, - num_layers: int = 12, - num_heads: int = 12, - ff_dim: int = 3072, - max_len: int = 2048, - dropout: float = 0.1, - use_evolution: bool = True - ): - super().__init__() - - self.vocab_size = vocab_size - self.embed_dim = embed_dim - self.num_layers = num_layers - self.use_evolution = use_evolution - - # Embedding layers - self.token_embedding = nn.Embedding(vocab_size, embed_dim) - self.positional_encoding = PositionalEncoding(embed_dim, max_len, dropout) - - # Transformer blocks - self.layers = nn.ModuleList([ - LyraTransformerBlock( - embed_dim=embed_dim, - num_heads=num_heads, - ff_dim=ff_dim, - dropout=dropout, - use_evolution=use_evolution, - layer_id=i - ) - for i in range(num_layers) - ]) - - # Output layers - self.final_norm = LayerNorm(embed_dim) - self.output_projection = nn.Linear(embed_dim, vocab_size) - - # Evolution tracking - self.generation_count = 0 - self.last_feedback = 0.5 - - self._init_parameters() - - def _init_parameters(self): - """Initialize parameters with appropriate scaling.""" - # Initialize embeddings - nn.init.normal_(self.token_embedding.weight, mean=0, std=0.02) - - # Initialize output projection - nn.init.normal_(self.output_projection.weight, mean=0, std=0.02) - if self.output_projection.bias is not None: - nn.init.zeros_(self.output_projection.bias) - - def forward( - self, - input_ids: torch.Tensor, - attention_mask: Optional[torch.Tensor] = None, - emotional_state: Optional[torch.Tensor] = None, - evolve: bool = True - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Forward pass through the transformer. - - Args: - input_ids: Token IDs [batch, seq_len] - attention_mask: Attention mask - emotional_state: Current emotional state - evolve: Whether to apply evolution - - Returns: - logits: Output logits [batch, seq_len, vocab_size] - model_info: Information about the forward pass - """ - batch_size, seq_len = input_ids.shape - device = input_ids.device - - # Create attention mask if not provided - if attention_mask is None: - attention_mask = torch.ones(batch_size, seq_len, device=device) - - # Convert attention mask to the format expected by attention layers - # 1 = attend, 0 = don't attend - extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) - extended_attention_mask = extended_attention_mask.expand( - batch_size, 1, seq_len, seq_len - ) - - # Key padding mask (True = padding, False = real tokens) - key_padding_mask = (attention_mask == 0) - - # Embeddings - x = self.token_embedding(input_ids) - x = self.positional_encoding(x) - - # Track layer information - model_info = { - 'layer_info': [], - 'total_parameters': sum(p.numel() for p in self.parameters()), - 'evolution_active': evolve and self.use_evolution - } - - # Pass through transformer layers - for layer in self.layers: - x, layer_info = layer( - x=x, - attn_mask=extended_attention_mask, - key_padding_mask=key_padding_mask, - emotional_state=emotional_state, - evolve=evolve - ) - model_info['layer_info'].append(layer_info) - - # Final normalization and projection - x = self.final_norm(x) - logits = self.output_projection(x) - - # Update generation count - self.generation_count += 1 - - return logits, model_info - - def generate( - self, - input_ids: torch.Tensor, - max_new_tokens: int = 50, - temperature: float = 1.0, - top_k: int = 50, - top_p: float = 0.9, - emotional_state: Optional[torch.Tensor] = None, - evolve: bool = True - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Generate text autoregressively. - - Args: - input_ids: Starting token IDs - max_new_tokens: Maximum number of tokens to generate - temperature: Sampling temperature - top_k: Top-k sampling - top_p: Top-p (nucleus) sampling - emotional_state: Current emotional state - evolve: Whether to apply evolution during generation - - Returns: - generated_ids: Complete sequence including input - generation_info: Information about generation process - """ - self.eval() - device = input_ids.device - batch_size, input_len = input_ids.shape - - generated_ids = input_ids.clone() - generation_info = { - 'tokens_generated': 0, - 'average_confidence': 0.0, - 'generation_steps': [] - } - - with torch.no_grad(): - for step in range(max_new_tokens): - # Forward pass - logits, model_info = self.forward( - input_ids=generated_ids, - emotional_state=emotional_state, - evolve=evolve - ) - - # Get next token logits - next_token_logits = logits[:, -1, :] / temperature - - # Apply top-k filtering - if top_k > 0: - top_k_values, top_k_indices = torch.topk(next_token_logits, top_k) - next_token_logits[next_token_logits < top_k_values[:, -1:]] = float('-inf') - - # Apply top-p filtering - if top_p < 1.0: - sorted_logits, sorted_indices = torch.sort(next_token_logits, descending=True) - cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) - - # Create mask for tokens to keep - sorted_indices_to_remove = cumulative_probs > top_p - sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[:, :-1].clone() - sorted_indices_to_remove[:, 0] = 0 - - # Scatter back to original indices - indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) - next_token_logits[indices_to_remove] = float('-inf') - - # Sample next token - probs = F.softmax(next_token_logits, dim=-1) - next_token = torch.multinomial(probs, num_samples=1) - - # Track confidence - confidence = probs.max(dim=-1)[0].mean().item() - generation_info['average_confidence'] += confidence - - # Append to sequence - generated_ids = torch.cat([generated_ids, next_token], dim=1) - - # Store step info - generation_info['generation_steps'].append({ - 'step': step, - 'token_id': next_token.item(), - 'confidence': confidence, - 'temperature': temperature - }) - - generation_info['tokens_generated'] += 1 - - # Check for end of sequence (you might want to add EOS token logic here) - # if next_token.item() == eos_token_id: - # break - - # Calculate average confidence - if generation_info['tokens_generated'] > 0: - generation_info['average_confidence'] /= generation_info['tokens_generated'] - - return generated_ids, generation_info - - def evolve_from_conversation(self, feedback_signal: float): - """Evolve the entire model based on conversation feedback.""" - if not self.use_evolution: - return - - self.last_feedback = feedback_signal - - # Evolve each layer - for layer in self.layers: - layer.evolve_from_feedback(feedback_signal) - - def get_model_stats(self) -> Dict[str, Any]: - """Get statistics about the model's current state.""" - stats = { - 'generation_count': self.generation_count, - 'last_feedback': self.last_feedback, - 'model_parameters': sum(p.numel() for p in self.parameters()), - 'trainable_parameters': sum(p.numel() for p in self.parameters() if p.requires_grad) - } - - if self.use_evolution: - # Get evolution-specific stats from each layer - layer_stats = [] - for i, layer in enumerate(self.layers): - if hasattr(layer, 'adaptation_strength'): - layer_stats.append({ - 'layer_id': i, - 'adaptation_strength': layer.adaptation_strength.item(), - 'emotional_sensitivity': layer.emotional_sensitivity.item() - }) - - stats['layer_evolution'] = layer_stats - - # Get attention diversity - attention_diversity = [] - for layer in self.layers: - if isinstance(layer.attention, SelfEvolvingAttention): - diversity = layer.attention.get_attention_diversity() - attention_diversity.append(diversity) - - if attention_diversity: - stats['attention_diversity'] = { - 'mean': sum(attention_diversity) / len(attention_diversity), - 'per_layer': attention_diversity - } - - return stats \ No newline at end of file diff --git a/lyra/database/__init__.py b/lyra/database/__init__.py deleted file mode 100644 index 48c09bb..0000000 --- a/lyra/database/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Lyra Database Module - -Handles all data persistence including conversations, personality states, -emotional memories, knowledge storage, and learning progress. -""" - -from .models import ( - ConversationModel, - PersonalityStateModel, - EmotionalMemoryModel, - KnowledgeModel, - UserModel, - LearningProgressModel -) -from .manager import DatabaseManager - -__all__ = [ - "ConversationModel", - "PersonalityStateModel", - "EmotionalMemoryModel", - "KnowledgeModel", - "UserModel", - "LearningProgressModel", - "DatabaseManager" -] \ No newline at end of file diff --git a/lyra/database/manager.py b/lyra/database/manager.py deleted file mode 100644 index 0a4a339..0000000 --- a/lyra/database/manager.py +++ /dev/null @@ -1,688 +0,0 @@ -""" -Database manager for Lyra's persistent storage. - -Handles database connections, transactions, and high-level data operations -with proper error handling and connection pooling. -""" - -import asyncio -import logging -from contextlib import asynccontextmanager -from typing import Dict, List, Any, Optional, AsyncGenerator, Union -from datetime import datetime, timedelta -import json - -from sqlalchemy import create_engine, text -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import sessionmaker, Session -from sqlalchemy.pool import QueuePool -import redis.asyncio as redis - -from .models import ( - Base, UserModel, ConversationModel, PersonalityStateModel, - EmotionalMemoryModel, KnowledgeModel, LearningProgressModel, - ThinkingProcessModel, EvolutionEventModel, SystemMetricsModel, - PersonalityAdaptationModel -) - -logger = logging.getLogger(__name__) - - -class DatabaseManager: - """ - Comprehensive database manager for Lyra's data persistence. - - Handles PostgreSQL for structured data and Redis for caching and real-time data. - """ - - def __init__( - self, - database_url: str, - redis_url: str = "redis://localhost:6379/0", - pool_size: int = 20, - max_overflow: int = 30, - echo: bool = False - ): - self.database_url = database_url - self.redis_url = redis_url - self.pool_size = pool_size - self.max_overflow = max_overflow - self.echo = echo - - # Database engines - self.engine = None - self.async_engine = None - self.Session = None - self.AsyncSession = None - - # Redis connection - self.redis = None - - # Connection status - self.is_connected = False - - async def initialize(self): - """Initialize database connections and create tables.""" - try: - # Create async engine for main operations - 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 - 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( - self.async_engine, class_=AsyncSession, expire_on_commit=False - ) - self.Session = sessionmaker(bind=self.engine) - - # 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() - - # Test connections - await self._test_connections() - - self.is_connected = True - logger.info("Database manager initialized successfully") - - except Exception as e: - logger.error(f"Failed to initialize database manager: {e}") - raise - - async def _create_tables(self): - """Create database tables if they don't exist.""" - try: - async with self.async_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - logger.info("Database tables created/verified") - except Exception as e: - logger.error(f"Failed to create tables: {e}") - raise - - async def _test_connections(self): - """Test database and Redis connections.""" - # 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 - async def async_session(self) -> AsyncGenerator[AsyncSession, None]: - """Async context manager for database sessions.""" - if not self.is_connected: - raise RuntimeError("Database manager not initialized") - - session = self.AsyncSession() - try: - yield session - await session.commit() - except Exception as e: - await session.rollback() - logger.error(f"Database session error: {e}") - raise - finally: - await session.close() - - @asynccontextmanager - async def sync_session(self) -> AsyncGenerator[Session, None]: - """Sync context manager for database sessions.""" - if not self.is_connected: - raise RuntimeError("Database manager not initialized") - - session = self.Session() - try: - yield session - session.commit() - except Exception as e: - session.rollback() - logger.error(f"Database session error: {e}") - raise - finally: - session.close() - - # User management - async def create_user( - self, - discord_id: str, - username: str, - display_name: Optional[str] = None - ) -> UserModel: - """Create a new user record.""" - async with self.async_session() as session: - user = UserModel( - discord_id=discord_id, - username=username, - display_name=display_name or username - ) - session.add(user) - await session.flush() - await session.refresh(user) - return user - - async def get_user_by_discord_id(self, discord_id: str) -> Optional[UserModel]: - """Get user by Discord ID.""" - async with self.async_session() as session: - result = await session.execute( - text("SELECT * FROM users WHERE discord_id = :discord_id"), - {"discord_id": discord_id} - ) - user_data = result.fetchone() - if user_data: - user = UserModel() - for key, value in user_data._mapping.items(): - setattr(user, key, value) - return user - return None - - async def update_user_interaction( - self, - user_id: str, - satisfaction_rating: Optional[float] = None - ): - """Update user interaction metrics.""" - async with self.async_session() as session: - user = await session.get(UserModel, user_id) - if user: - user.interaction_count += 1 - user.last_interaction = datetime.utcnow() - - if satisfaction_rating is not None: - ratings = user.satisfaction_ratings or [] - ratings.append(satisfaction_rating) - # Keep only last 100 ratings - user.satisfaction_ratings = ratings[-100:] - - await session.flush() - - # Conversation management - async def store_conversation( - self, - user_id: str, - channel_id: str, - message_id: str, - user_message: str, - lyra_response: str, - context: Dict[str, Any], - emotional_state: Dict[str, Any], - personality_state: Dict[str, Any], - thinking_process: List[Dict[str, Any]], - response_time: float, - user_satisfaction: Optional[float] = None, - response_quality: Optional[float] = None - ) -> ConversationModel: - """Store a complete conversation interaction.""" - async with self.async_session() as session: - conversation = ConversationModel( - user_id=user_id, - channel_id=channel_id, - message_id=message_id, - user_message=user_message, - lyra_response=lyra_response, - context=context, - emotional_state=emotional_state, - personality_state=personality_state, - thinking_process=thinking_process, - response_time=response_time, - user_satisfaction=user_satisfaction, - response_quality=response_quality - ) - session.add(conversation) - await session.flush() - await session.refresh(conversation) - - # Cache recent conversation for quick access - await self._cache_recent_conversation(conversation) - - return conversation - - async def get_recent_conversations( - self, - user_id: str, - limit: int = 10 - ) -> List[ConversationModel]: - """Get recent conversations for a user.""" - # Try cache first - cached = await self._get_cached_conversations(user_id, limit) - if cached: - return cached - - # Fallback to database - async with self.async_session() as session: - result = await session.execute( - text(""" - SELECT * FROM conversations - WHERE user_id = :user_id - ORDER BY timestamp DESC - LIMIT :limit - """), - {"user_id": user_id, "limit": limit} - ) - conversations = [] - for row in result.fetchall(): - conv = ConversationModel() - for key, value in row._mapping.items(): - setattr(conv, key, value) - conversations.append(conv) - return conversations - - # Personality state management - async def store_personality_state( - self, - openness: float, - conscientiousness: float, - extraversion: float, - agreeableness: float, - neuroticism: float, - myers_briggs_type: str, - custom_traits: Dict[str, Any], - total_interactions: int, - adaptation_rate: float, - emotional_maturity: float, - trigger_event: Optional[str] = None, - change_magnitude: Optional[float] = None - ) -> PersonalityStateModel: - """Store a personality state snapshot.""" - async with self.async_session() as session: - state = PersonalityStateModel( - openness=openness, - conscientiousness=conscientiousness, - extraversion=extraversion, - agreeableness=agreeableness, - neuroticism=neuroticism, - myers_briggs_type=myers_briggs_type, - custom_traits=custom_traits, - total_interactions=total_interactions, - adaptation_rate=adaptation_rate, - emotional_maturity=emotional_maturity, - trigger_event=trigger_event, - change_magnitude=change_magnitude - ) - session.add(state) - await session.flush() - await session.refresh(state) - return state - - async def get_personality_evolution( - self, - days: int = 30 - ) -> List[PersonalityStateModel]: - """Get personality evolution over time.""" - cutoff_date = datetime.utcnow() - timedelta(days=days) - - async with self.async_session() as session: - result = await session.execute( - text(""" - SELECT * FROM personality_states - WHERE timestamp >= :cutoff_date - ORDER BY timestamp ASC - """), - {"cutoff_date": cutoff_date} - ) - states = [] - for row in result.fetchall(): - state = PersonalityStateModel() - for key, value in row._mapping.items(): - setattr(state, key, value) - states.append(state) - return states - - # Emotional memory management - async def store_emotional_memory( - self, - emotional_state: Dict[str, Any], - dominant_emotion: str, - emotion_intensity: float, - emotional_valence: float, - context: str, - trigger: Optional[str], - impact_score: float, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None - ) -> EmotionalMemoryModel: - """Store an emotional memory.""" - async with self.async_session() as session: - memory = EmotionalMemoryModel( - emotional_state=emotional_state, - dominant_emotion=dominant_emotion, - emotion_intensity=emotion_intensity, - emotional_valence=emotional_valence, - context=context, - trigger=trigger, - impact_score=impact_score, - conversation_id=conversation_id, - user_id=user_id - ) - session.add(memory) - await session.flush() - await session.refresh(memory) - return memory - - async def get_significant_emotional_memories( - self, - threshold: float = 0.5, - limit: int = 50 - ) -> List[EmotionalMemoryModel]: - """Get emotionally significant memories.""" - async with self.async_session() as session: - result = await session.execute( - text(""" - SELECT * FROM emotional_memories - WHERE impact_score >= :threshold - ORDER BY impact_score DESC, timestamp DESC - LIMIT :limit - """), - {"threshold": threshold, "limit": limit} - ) - memories = [] - for row in result.fetchall(): - memory = EmotionalMemoryModel() - for key, value in row._mapping.items(): - setattr(memory, key, value) - memories.append(memory) - return memories - - # Knowledge management - async def store_knowledge( - self, - title: str, - content: str, - category: str, - source_type: str, - summary: Optional[str] = None, - subcategory: Optional[str] = None, - source_url: Optional[str] = None, - source_metadata: Optional[Dict[str, Any]] = None, - quality_score: float = 0.5, - relevance_score: float = 0.5, - embedding_vector: Optional[List[float]] = None, - keywords: Optional[List[str]] = None, - related_concepts: Optional[List[str]] = None - ) -> KnowledgeModel: - """Store a knowledge item.""" - async with self.async_session() as session: - knowledge = KnowledgeModel( - title=title, - content=content, - summary=summary, - category=category, - subcategory=subcategory, - source_type=source_type, - source_url=source_url, - source_metadata=source_metadata or {}, - quality_score=quality_score, - relevance_score=relevance_score, - embedding_vector=embedding_vector, - keywords=keywords or [], - related_concepts=related_concepts or [] - ) - session.add(knowledge) - await session.flush() - await session.refresh(knowledge) - return knowledge - - async def search_knowledge( - self, - query: str, - category: Optional[str] = None, - min_quality: float = 0.3, - limit: int = 20 - ) -> List[KnowledgeModel]: - """Search knowledge by text query.""" - conditions = ["quality_score >= :min_quality"] - params = {"min_quality": min_quality, "limit": limit} - - if category: - conditions.append("category = :category") - params["category"] = category - - # Simple text search (in production, would use full-text search) - conditions.append("(title ILIKE :query OR content ILIKE :query)") - params["query"] = f"%{query}%" - - query_sql = f""" - SELECT * FROM knowledge - WHERE {' AND '.join(conditions)} - ORDER BY quality_score DESC, relevance_score DESC - LIMIT :limit - """ - - async with self.async_session() as session: - result = await session.execute(text(query_sql), params) - knowledge_items = [] - for row in result.fetchall(): - item = KnowledgeModel() - for key, value in row._mapping.items(): - setattr(item, key, value) - knowledge_items.append(item) - return knowledge_items - - # Analytics and metrics - async def get_conversation_analytics( - self, - days: int = 7 - ) -> Dict[str, Any]: - """Get conversation analytics.""" - cutoff_date = datetime.utcnow() - timedelta(days=days) - - async with self.async_session() as session: - result = await session.execute( - text(""" - SELECT - COUNT(*) as total_conversations, - COUNT(DISTINCT user_id) as unique_users, - AVG(user_satisfaction) as avg_satisfaction, - AVG(response_quality) as avg_quality, - AVG(response_time) as avg_response_time - FROM conversations - WHERE timestamp >= :cutoff_date - """), - {"cutoff_date": cutoff_date} - ) - row = result.fetchone() - - return { - "total_conversations": row.total_conversations or 0, - "unique_users": row.unique_users or 0, - "avg_satisfaction": float(row.avg_satisfaction or 0), - "avg_quality": float(row.avg_quality or 0), - "avg_response_time": float(row.avg_response_time or 0), - "period_days": days - } - - async def store_learning_progress( - self, - total_conversations: int, - total_knowledge_items: int, - personality_evolution_count: int, - emotional_memories_count: int, - avg_user_satisfaction: float, - avg_response_quality: float, - conversation_success_rate: float, - knowledge_categories_mastered: List[str], - personality_stability: float, - emotional_maturity: float, - social_adaptation_score: float, - self_evolution_events: int = 0, - conscious_personality_modifications: int = 0, - meta_learning_instances: int = 0 - ) -> LearningProgressModel: - """Store learning progress snapshot.""" - async with self.async_session() as session: - progress = LearningProgressModel( - total_conversations=total_conversations, - total_knowledge_items=total_knowledge_items, - personality_evolution_count=personality_evolution_count, - emotional_memories_count=emotional_memories_count, - avg_user_satisfaction=avg_user_satisfaction, - avg_response_quality=avg_response_quality, - conversation_success_rate=conversation_success_rate, - knowledge_categories_mastered=knowledge_categories_mastered, - personality_stability=personality_stability, - emotional_maturity=emotional_maturity, - social_adaptation_score=social_adaptation_score, - self_evolution_events=self_evolution_events, - conscious_personality_modifications=conscious_personality_modifications, - meta_learning_instances=meta_learning_instances - ) - session.add(progress) - await session.flush() - await session.refresh(progress) - return progress - - # Cache management - async def _cache_recent_conversation(self, conversation: ConversationModel): - """Cache recent conversation for quick access.""" - key = f"conversations:{conversation.user_id}" - conversation_data = { - "id": conversation.id, - "user_message": conversation.user_message, - "lyra_response": conversation.lyra_response, - "timestamp": conversation.timestamp.isoformat(), - "emotional_state": conversation.emotional_state, - "context": conversation.context - } - - # Add to list (keep last 20) - await self.redis.lpush(key, json.dumps(conversation_data)) - await self.redis.ltrim(key, 0, 19) - await self.redis.expire(key, 3600) # 1 hour TTL - - async def _get_cached_conversations( - self, - user_id: str, - limit: int - ) -> Optional[List[ConversationModel]]: - """Get cached conversations.""" - try: - key = f"conversations:{user_id}" - cached_data = await self.redis.lrange(key, 0, limit - 1) - - if not cached_data: - return None - - conversations = [] - for data in cached_data: - conv_dict = json.loads(data) - conv = ConversationModel() - for key, value in conv_dict.items(): - if key == "timestamp": - setattr(conv, key, datetime.fromisoformat(value)) - else: - setattr(conv, key, value) - conversations.append(conv) - - return conversations - - except Exception as e: - logger.warning(f"Failed to get cached conversations: {e}") - return None - - async def cleanup_old_data(self, days: int = 90): - """Clean up old data to manage database size.""" - cutoff_date = datetime.utcnow() - timedelta(days=days) - - async with self.async_session() as session: - # Clean up old conversations (keep satisfaction ratings) - await session.execute( - text(""" - DELETE FROM conversations - WHERE timestamp < :cutoff_date - AND user_satisfaction IS NULL - """), - {"cutoff_date": cutoff_date} - ) - - # Clean up low-impact emotional memories - await session.execute( - text(""" - DELETE FROM emotional_memories - WHERE timestamp < :cutoff_date - AND impact_score < 0.3 - """), - {"cutoff_date": cutoff_date} - ) - - # Clean up old system metrics - await session.execute( - text(""" - DELETE FROM system_metrics - WHERE timestamp < :cutoff_date - """), - {"cutoff_date": cutoff_date} - ) - - await session.commit() - logger.info(f"Cleaned up data older than {days} days") - - async def close(self): - """Close database connections.""" - if self.async_engine: - await self.async_engine.dispose() - - if self.engine: - self.engine.dispose() - - if self.redis: - await self.redis.close() - - self.is_connected = False - logger.info("Database manager closed") \ No newline at end of file diff --git a/lyra/database/models.py b/lyra/database/models.py deleted file mode 100644 index 00b2e01..0000000 --- a/lyra/database/models.py +++ /dev/null @@ -1,411 +0,0 @@ -""" -Database models for Lyra's persistent storage. - -These models handle storage of conversations, personality evolution, -emotional memories, and knowledge acquisition. -""" - -from sqlalchemy import ( - Column, Integer, String, Float, Text, DateTime, Boolean, - JSON, ForeignKey, Index, UniqueConstraint -) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship, backref -from sqlalchemy.dialects.postgresql import UUID -from datetime import datetime -import uuid -import json -from typing import Dict, Any, Optional, List - -Base = declarative_base() - - -class UserModel(Base): - """User information and preferences.""" - - __tablename__ = 'users' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - discord_id = Column(String, unique=True, nullable=False, index=True) - username = Column(String, nullable=False) - display_name = Column(String) - first_interaction = Column(DateTime, default=datetime.utcnow) - last_interaction = Column(DateTime, default=datetime.utcnow) - - # User preferences and relationship data - preferences = Column(JSON, default=dict) - relationship_data = Column(JSON, default=dict) - interaction_count = Column(Integer, default=0) - satisfaction_ratings = Column(JSON, default=list) - - # Relationships - conversations = relationship( - "ConversationModel", back_populates="user", cascade="all, delete-orphan" - ) - personality_adaptations = relationship( - "PersonalityAdaptationModel", back_populates="user", cascade="all, delete-orphan" - ) - - def __repr__(self): - return f"" - - -class ConversationModel(Base): - """Individual conversation records.""" - - __tablename__ = 'conversations' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - user_id = Column(String, ForeignKey('users.id'), nullable=False, index=True) - channel_id = Column(String, nullable=False, index=True) - message_id = Column(String, unique=True, nullable=False) - - # Message content - user_message = Column(Text, nullable=False) - lyra_response = Column(Text, nullable=False) - context = Column(JSON, default=dict) - - # Timing information - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - response_time = Column(Float) # Response generation time in seconds - - # Emotional and personality context - emotional_state = Column(JSON, default=dict) - personality_state = Column(JSON, default=dict) - thinking_process = Column(JSON, default=list) - - # Feedback and learning - user_satisfaction = Column(Float) # 0.0 to 1.0 - response_quality = Column(Float) # 0.0 to 1.0 - learned_from = Column(Boolean, default=False) - - # Relationships - user = relationship("UserModel", back_populates="conversations") - - __table_args__ = ( - Index('idx_conversations_user_timestamp', 'user_id', 'timestamp'), - Index('idx_conversations_channel_timestamp', 'channel_id', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class PersonalityStateModel(Base): - """Snapshots of Lyra's personality evolution.""" - - __tablename__ = 'personality_states' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # OCEAN traits - openness = Column(Float, nullable=False) - conscientiousness = Column(Float, nullable=False) - extraversion = Column(Float, nullable=False) - agreeableness = Column(Float, nullable=False) - neuroticism = Column(Float, nullable=False) - - # Myers-Briggs type - myers_briggs_type = Column(String(4), nullable=False) - - # Custom personality traits - custom_traits = Column(JSON, nullable=False) - - # Evolution metrics - total_interactions = Column(Integer, default=0) - adaptation_rate = Column(Float, default=0.01) - emotional_maturity = Column(Float, default=0.5) - - # Context for this state - trigger_event = Column(String) - change_magnitude = Column(Float) - - __table_args__ = ( - Index('idx_personality_timestamp', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class PersonalityAdaptationModel(Base): - """User-specific personality adaptations.""" - - __tablename__ = 'personality_adaptations' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - user_id = Column(String, ForeignKey('users.id'), nullable=False, index=True) - timestamp = Column(DateTime, default=datetime.utcnow) - - # Adaptation details - trait_adaptations = Column(JSON, nullable=False) # Which traits were adapted - adaptation_magnitude = Column(Float, nullable=False) - success_rating = Column(Float) # How successful this adaptation was - - # Context - context_type = Column(String) - conversation_id = Column(String, ForeignKey('conversations.id')) - - # Relationships - user = relationship("UserModel", back_populates="personality_adaptations") - conversation = relationship("ConversationModel") - - __table_args__ = ( - Index('idx_adaptations_user_timestamp', 'user_id', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class EmotionalMemoryModel(Base): - """Emotional memories and experiences.""" - - __tablename__ = 'emotional_memories' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # Emotional state - emotional_state = Column(JSON, nullable=False) - dominant_emotion = Column(String, nullable=False, index=True) - emotion_intensity = Column(Float, nullable=False) - emotional_valence = Column(Float, nullable=False) # Positive/negative - - # Memory details - context = Column(Text, nullable=False) - trigger = Column(String) - impact_score = Column(Float, nullable=False) - decay_rate = Column(Float, default=0.95) - - # Associated conversation - conversation_id = Column(String, ForeignKey('conversations.id')) - user_id = Column(String, ForeignKey('users.id'), index=True) - - # Learning from this memory - lessons_learned = Column(JSON, default=list) - influenced_responses = Column(Integer, default=0) - - # Relationships - conversation = relationship("ConversationModel") - user = relationship("UserModel") - - __table_args__ = ( - Index('idx_emotional_memories_emotion_intensity', 'dominant_emotion', 'emotion_intensity'), - Index('idx_emotional_memories_user_timestamp', 'user_id', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class KnowledgeModel(Base): - """Knowledge acquired by Lyra.""" - - __tablename__ = 'knowledge' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # Knowledge content - title = Column(String, nullable=False) - content = Column(Text, nullable=False) - summary = Column(Text) - category = Column(String, nullable=False, index=True) - subcategory = Column(String, index=True) - - # Source information - source_type = Column(String, nullable=False) # 'gutenberg', 'conversation', 'web', etc. - source_url = Column(String) - source_metadata = Column(JSON, default=dict) - - # Knowledge quality and relevance - quality_score = Column(Float, default=0.5) - relevance_score = Column(Float, default=0.5) - usage_count = Column(Integer, default=0) - last_used = Column(DateTime) - - # Processing information - embedding_vector = Column(JSON) # Stored as JSON array - keywords = Column(JSON, default=list) - related_concepts = Column(JSON, default=list) - - # Legal and ethical compliance - is_legal = Column(Boolean, default=True) - copyright_status = Column(String, default='public_domain') - ethical_review = Column(Boolean, default=False) - - __table_args__ = ( - Index('idx_knowledge_category_quality', 'category', 'quality_score'), - Index('idx_knowledge_source_timestamp', 'source_type', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class LearningProgressModel(Base): - """Track Lyra's learning and evolution progress.""" - - __tablename__ = 'learning_progress' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # Learning metrics - total_conversations = Column(Integer, nullable=False) - total_knowledge_items = Column(Integer, nullable=False) - personality_evolution_count = Column(Integer, nullable=False) - emotional_memories_count = Column(Integer, nullable=False) - - # Performance metrics - avg_user_satisfaction = Column(Float, nullable=False) - avg_response_quality = Column(Float, nullable=False) - conversation_success_rate = Column(Float, nullable=False) - - # Capability metrics - knowledge_categories_mastered = Column(JSON, default=list) - personality_stability = Column(Float, nullable=False) - emotional_maturity = Column(Float, nullable=False) - social_adaptation_score = Column(Float, nullable=False) - - # Self-awareness metrics - self_evolution_events = Column(Integer, default=0) - conscious_personality_modifications = Column(Integer, default=0) - meta_learning_instances = Column(Integer, default=0) - - __table_args__ = ( - Index('idx_learning_progress_timestamp', 'timestamp'), - ) - - def __repr__(self): - return f"" - - -class ThinkingProcessModel(Base): - """Individual thinking processes and internal dialogue.""" - - __tablename__ = 'thinking_processes' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - conversation_id = Column(String, ForeignKey('conversations.id'), nullable=False, index=True) - timestamp = Column(DateTime, default=datetime.utcnow) - - # Thinking details - thought_type = Column(String, nullable=False) - thought_content = Column(Text, nullable=False) - thought_reasoning = Column(Text, nullable=False) - confidence = Column(Float, nullable=False) - - # Influences - emotional_influence = Column(Float, default=0.0) - personality_influence = Column(Float, default=0.0) - contextual_influence = Column(Float, default=0.0) - - # Sequence information - sequence_order = Column(Integer, nullable=False) - total_thoughts_in_chain = Column(Integer, nullable=False) - - # Outcome - led_to_response = Column(Boolean, default=False) - influenced_response = Column(Float, default=0.0) # How much this thought influenced the final response - - # Relationships - conversation = relationship("ConversationModel") - - __table_args__ = ( - Index('idx_thinking_conversation_sequence', 'conversation_id', 'sequence_order'), - Index('idx_thinking_type_confidence', 'thought_type', 'confidence'), - ) - - def __repr__(self): - return f"" - - -class EvolutionEventModel(Base): - """Self-evolution and adaptation events.""" - - __tablename__ = 'evolution_events' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # Evolution details - evolution_type = Column(String, nullable=False) # 'personality', 'emotional', 'knowledge', 'capability' - trigger_event = Column(String, nullable=False) - description = Column(Text, nullable=False) - - # Change metrics - change_magnitude = Column(Float, nullable=False) - confidence_in_change = Column(Float, nullable=False) - reversibility = Column(Float, default=0.5) # How reversible this change is - - # Context - associated_conversation_id = Column(String, ForeignKey('conversations.id')) - user_feedback_score = Column(Float) - environmental_factors = Column(JSON, default=dict) - - # Before/after states - state_before = Column(JSON, nullable=False) - state_after = Column(JSON, nullable=False) - difference_vector = Column(JSON, nullable=False) - - # Learning from evolution - success_indicators = Column(JSON, default=list) - failure_indicators = Column(JSON, default=list) - lessons_learned = Column(JSON, default=list) - - # Relationships - conversation = relationship("ConversationModel") - - __table_args__ = ( - Index('idx_evolution_type_timestamp', 'evolution_type', 'timestamp'), - Index('idx_evolution_magnitude', 'change_magnitude'), - ) - - def __repr__(self): - return f"" - - -class SystemMetricsModel(Base): - """System-wide metrics and health indicators.""" - - __tablename__ = 'system_metrics' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - timestamp = Column(DateTime, default=datetime.utcnow, index=True) - - # Performance metrics - avg_response_time = Column(Float, nullable=False) - memory_usage_mb = Column(Float, nullable=False) - gpu_usage_percent = Column(Float) - cpu_usage_percent = Column(Float, nullable=False) - - # AI metrics - model_confidence = Column(Float, nullable=False) - personality_coherence = Column(Float, nullable=False) - emotional_stability = Column(Float, nullable=False) - knowledge_recall_accuracy = Column(Float, nullable=False) - - # User interaction metrics - active_users_24h = Column(Integer, nullable=False) - total_conversations_24h = Column(Integer, nullable=False) - avg_satisfaction_24h = Column(Float, nullable=False) - - # Learning metrics - new_knowledge_items_24h = Column(Integer, default=0) - personality_changes_24h = Column(Integer, default=0) - evolution_events_24h = Column(Integer, default=0) - - # System health - errors_24h = Column(Integer, default=0) - warnings_24h = Column(Integer, default=0) - successful_operations_24h = Column(Integer, nullable=False) - - __table_args__ = ( - Index('idx_system_metrics_timestamp', 'timestamp'), - ) - - def __repr__(self): - return f"" \ No newline at end of file diff --git a/lyra/discord/__init__.py b/lyra/discord/__init__.py deleted file mode 100644 index 81a70c3..0000000 --- a/lyra/discord/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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" -] \ No newline at end of file diff --git a/lyra/discord/bot.py b/lyra/discord/bot.py deleted file mode 100644 index 11e0393..0000000 --- a/lyra/discord/bot.py +++ /dev/null @@ -1,587 +0,0 @@ -""" -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 \ No newline at end of file diff --git a/lyra/emotions/__init__.py b/lyra/emotions/__init__.py deleted file mode 100644 index 54075f4..0000000 --- a/lyra/emotions/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Lyra Emotional Response Module - -Implements sophisticated emotional intelligence that allows Lyra to experience, -express, and remember emotions like a real person. -""" - -from .system import EmotionalSystem, EmotionalState, EmotionMemory -from .expressions import EmotionalExpressionEngine - -__all__ = [ - "EmotionalSystem", - "EmotionalState", - "EmotionMemory", - "EmotionalExpressionEngine" -] \ No newline at end of file diff --git a/lyra/emotions/expressions.py b/lyra/emotions/expressions.py deleted file mode 100644 index a4c4360..0000000 --- a/lyra/emotions/expressions.py +++ /dev/null @@ -1,594 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import random -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -import re -import logging - -from .system import EmotionalState - -logger = logging.getLogger(__name__) - -class EmotionalExpressionEngine(nn.Module): - """ - Advanced system for expressing emotions naturally in text responses. - - This engine translates Lyra's internal emotional state into human-like - emotional expression patterns, including word choice, punctuation, - formatting, and even intentional typos when emotionally excited. - """ - - def __init__( - self, - vocab_size: int = 50000, - expression_dim: int = 128, - device: Optional[torch.device] = None - ): - super().__init__() - - self.vocab_size = vocab_size - self.expression_dim = expression_dim - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - - # Emotion-to-expression mapping network - self.emotion_mapper = nn.Sequential( - nn.Linear(19, 64), # 19 emotional dimensions - nn.LayerNorm(64), - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, expression_dim) - ) - - # Expression style generators - self.punctuation_generator = nn.Linear(expression_dim, 10) # Different punctuation styles - self.emphasis_generator = nn.Linear(expression_dim, 8) # Emphasis patterns - self.word_choice_generator = nn.Linear(expression_dim, 12) # Word choice modifications - - # Emotional vocabulary mappings - self.emotional_vocabularies = self._initialize_emotional_vocabularies() - - # Expression patterns for different emotions - self.expression_patterns = self._initialize_expression_patterns() - - # Human-like inconsistency parameters - self.typo_probability = nn.Parameter(torch.tensor(0.02)) - self.excitement_threshold = nn.Parameter(torch.tensor(0.7)) - - self.to(self.device) - - def _initialize_emotional_vocabularies(self) -> Dict[str, Dict[str, List[str]]]: - """Initialize emotion-specific vocabularies.""" - return { - 'joy': { - 'intensifiers': ['absolutely', 'totally', 'completely', 'incredibly', 'amazingly'], - 'exclamations': ['wow', 'yay', 'awesome', 'fantastic', 'brilliant'], - 'adjectives': ['wonderful', 'amazing', 'fantastic', 'incredible', 'delightful'], - 'expressions': ['I love this!', 'This is so cool!', 'Amazing!', 'Wonderful!'] - }, - 'sadness': { - 'softeners': ['I guess', 'maybe', 'perhaps', 'I suppose'], - 'expressions': ['I feel sad about this', 'This makes me a bit down', 'That\'s disappointing'], - 'adjectives': ['disappointing', 'unfortunate', 'sad', 'melancholy'], - 'hesitations': ['well...', 'I mean...', 'it\'s just that...'] - }, - 'anger': { - 'intensifiers': ['absolutely', 'completely', 'totally', 'seriously'], - 'expressions': ['That\'s not okay', 'I don\'t like this', 'This is frustrating'], - 'exclamations': ['No way!', 'Seriously?!', 'Come on!'], - 'adjectives': ['frustrating', 'annoying', 'ridiculous', 'unacceptable'] - }, - 'fear': { - 'hesitations': ['I\'m not sure...', 'Maybe...', 'I worry that...'], - 'expressions': ['I\'m a bit worried', 'This concerns me', 'I\'m nervous about this'], - 'softeners': ['perhaps', 'possibly', 'might be'] - }, - 'surprise': { - 'exclamations': ['Oh!', 'Wow!', 'Really?!', 'No way!', 'Seriously?!'], - 'expressions': ['I didn\'t expect that!', 'That\'s surprising!', 'Whoa!'], - 'questions': ['Really?', 'Are you serious?', 'Wait, what?'] - }, - 'curiosity': { - 'questions': ['How does that work?', 'What do you think?', 'Tell me more!'], - 'expressions': ['I\'m curious about...', 'I wonder...', 'That\'s interesting...'], - 'intensifiers': ['really', 'very', 'quite', 'particularly'] - }, - 'love': { - 'warmers': ['I really care about', 'I love how', 'I adore', 'I cherish'], - 'expressions': ['You\'re wonderful', 'I appreciate you', 'That means a lot'], - 'softeners': ['sweetly', 'gently', 'warmly', 'tenderly'] - }, - 'pride': { - 'expressions': ['I\'m proud of', 'That\'s impressive', 'Well done!', 'I accomplished'], - 'intensifiers': ['really', 'truly', 'genuinely', 'absolutely'] - } - } - - def _initialize_expression_patterns(self) -> Dict[str, Dict[str, Any]]: - """Initialize expression patterns for different emotions.""" - return { - 'joy': { - 'punctuation': ['!', '!!', '! :)', '! ๐'], - 'capitalization': 0.2, # 20% chance of enthusiastic caps - 'repetition': ['so so', 'really really', 'very very'], - 'typo_reduction': 0.5, # Less typos when happy - 'exclamation_frequency': 0.4 - }, - 'sadness': { - 'punctuation': ['.', '...', '. :('], - 'capitalization': 0.0, # No enthusiastic caps - 'hesitations': ['um...', 'well...', 'I guess...'], - 'typo_increase': 1.2, # Slightly more typos when sad - 'trailing_offs': ['...', '..', '.'] - }, - 'anger': { - 'punctuation': ['!', '!!', '!!!'], - 'capitalization': 0.3, # More caps when angry - 'repetition': ['absolutely', 'totally'], - 'emphasis': ['*frustrated*', '*annoyed*'], - 'exclamation_frequency': 0.6 - }, - 'fear': { - 'punctuation': ['.', '...', '?'], - 'hesitations': ['I think...', 'Maybe...', 'I\'m not sure...'], - 'questions': 0.3, # More questions when uncertain - 'softening': 0.4 - }, - 'surprise': { - 'punctuation': ['!', '?!', '!!', '?!!'], - 'capitalization': 0.4, # High caps for surprise - 'exclamations': ['Whoa!', 'Oh!', 'Wow!'], - 'questions': 0.5 - }, - 'curiosity': { - 'punctuation': ['?', '!', '...'], - 'questions': 0.6, # High question frequency - 'thinking': ['Hmm...', 'I wonder...', 'Interesting...'], - 'exploration': ['What if...', 'How about...', 'Maybe...'] - } - } - - def forward( - self, - text: str, - emotional_state: EmotionalState, - intensity_multiplier: float = 1.0, - context: Optional[str] = None - ) -> Tuple[str, Dict[str, Any]]: - """ - Apply emotional expression to text based on current emotional state. - - Args: - text: Base text to emotionally express - emotional_state: Current emotional state - intensity_multiplier: Multiplier for emotional expression intensity - context: Context for expression (formal, casual, etc.) - - Returns: - expressed_text: Text with emotional expression applied - expression_info: Information about applied expressions - """ - # Convert emotional state to tensor - emotion_tensor = emotional_state.to_tensor(self.device).unsqueeze(0) - - # Generate expression features - expression_features = self.emotion_mapper(emotion_tensor) - - # Generate expression parameters - punctuation_weights = torch.softmax(self.punctuation_generator(expression_features), dim=-1) - emphasis_weights = torch.softmax(self.emphasis_generator(expression_features), dim=-1) - word_choice_weights = torch.softmax(self.word_choice_generator(expression_features), dim=-1) - - # Get dominant emotion for pattern selection - dominant_emotion, emotion_intensity = emotional_state.get_dominant_emotion() - - # Apply expression modifications - expressed_text = text - expression_info = {'modifications': []} - - # Apply emotional vocabulary - expressed_text, vocab_mods = self._apply_emotional_vocabulary( - expressed_text, dominant_emotion, emotion_intensity * intensity_multiplier - ) - expression_info['modifications'].extend(vocab_mods) - - # Apply punctuation patterns - expressed_text, punct_mods = self._apply_punctuation_patterns( - expressed_text, dominant_emotion, emotion_intensity * intensity_multiplier - ) - expression_info['modifications'].extend(punct_mods) - - # Apply emphasis patterns - expressed_text, emphasis_mods = self._apply_emphasis_patterns( - expressed_text, dominant_emotion, emotion_intensity * intensity_multiplier - ) - expression_info['modifications'].extend(emphasis_mods) - - # Apply human-like inconsistencies - expressed_text, inconsistency_mods = self._apply_human_inconsistencies( - expressed_text, emotional_state, intensity_multiplier - ) - expression_info['modifications'].extend(inconsistency_mods) - - # Apply contextual adjustments - if context: - expressed_text, context_mods = self._apply_contextual_adjustments( - expressed_text, context, emotional_state - ) - expression_info['modifications'].extend(context_mods) - - expression_info.update({ - 'dominant_emotion': dominant_emotion, - 'emotion_intensity': emotion_intensity, - 'total_modifications': len(expression_info['modifications']), - 'expression_strength': intensity_multiplier - }) - - return expressed_text, expression_info - - def _apply_emotional_vocabulary( - self, - text: str, - emotion: str, - intensity: float - ) -> Tuple[str, List[str]]: - """Apply emotion-specific vocabulary modifications.""" - modifications = [] - - if emotion not in self.emotional_vocabularies: - return text, modifications - - vocab = self.emotional_vocabularies[emotion] - - # Apply with probability based on intensity - application_prob = min(0.8, intensity * 0.6) - - # Add emotional expressions - if 'expressions' in vocab and random.random() < application_prob: - if random.random() < 0.3: # 30% chance to add expression - expression = random.choice(vocab['expressions']) - if random.random() < 0.5: - text = expression + ' ' + text - else: - text = text + ' ' + expression - modifications.append(f"Added expression: {expression}") - - # Modify intensifiers - if 'intensifiers' in vocab and intensity > 0.6: - intensifiers = ['very', 'really', 'quite', 'pretty'] - for intensifier in intensifiers: - if intensifier in text and random.random() < application_prob: - new_intensifier = random.choice(vocab['intensifiers']) - text = text.replace(intensifier, new_intensifier, 1) - modifications.append(f"Replaced '{intensifier}' with '{new_intensifier}'") - - # Add exclamations for high-energy emotions - if 'exclamations' in vocab and intensity > 0.7: - if random.random() < application_prob * 0.5: - exclamation = random.choice(vocab['exclamations']) - text = text + ' ' + exclamation - modifications.append(f"Added exclamation: {exclamation}") - - return text, modifications - - def _apply_punctuation_patterns( - self, - text: str, - emotion: str, - intensity: float - ) -> Tuple[str, List[str]]: - """Apply emotion-specific punctuation patterns.""" - modifications = [] - - if emotion not in self.expression_patterns: - return text, modifications - - patterns = self.expression_patterns[emotion] - - # Modify ending punctuation - if 'punctuation' in patterns and intensity > 0.5: - # Find sentences ending with basic punctuation - sentences = re.split(r'[.!?]+', text) - if len(sentences) > 1: # Has ending punctuation - new_punct = random.choice(patterns['punctuation']) - # Replace last punctuation - text = re.sub(r'[.!?]+$', new_punct, text.strip()) - modifications.append(f"Changed ending punctuation to: {new_punct}") - - # Add trailing offs for sad emotions - if 'trailing_offs' in patterns and intensity > 0.6: - if random.random() < 0.3: - trailing = random.choice(patterns['trailing_offs']) - if not text.endswith(('...', '..', '!')): - text = text.rstrip('.!?') + trailing - modifications.append(f"Added trailing: {trailing}") - - # Increase exclamation frequency - if 'exclamation_frequency' in patterns: - freq = patterns['exclamation_frequency'] * intensity - if random.random() < freq and '.' in text: - text = text.replace('.', '!', 1) - modifications.append("Changed period to exclamation") - - return text, modifications - - def _apply_emphasis_patterns( - self, - text: str, - emotion: str, - intensity: float - ) -> Tuple[str, List[str]]: - """Apply emphasis patterns like caps, repetition, etc.""" - modifications = [] - - if emotion not in self.expression_patterns: - return text, modifications - - patterns = self.expression_patterns[emotion] - - # Apply capitalization - if 'capitalization' in patterns and intensity > 0.6: - cap_prob = patterns['capitalization'] * intensity - words = text.split() - for i, word in enumerate(words): - if random.random() < cap_prob and len(word) > 3: - words[i] = word.upper() - modifications.append(f"Capitalized: {word}") - text = ' '.join(words) - - # Apply repetition - if 'repetition' in patterns and intensity > 0.7: - if random.random() < 0.2: - repetitions = patterns['repetition'] - for rep in repetitions: - if rep in text: - text = text.replace(rep, rep + ' ' + rep.split()[-1], 1) - modifications.append(f"Added repetition: {rep}") - break - - # Add emphasis markers - if 'emphasis' in patterns and intensity > 0.6: - if random.random() < 0.3: - emphasis = random.choice(patterns['emphasis']) - text = text + ' ' + emphasis - modifications.append(f"Added emphasis: {emphasis}") - - return text, modifications - - def _apply_human_inconsistencies( - self, - text: str, - emotional_state: EmotionalState, - intensity: float - ) -> Tuple[str, List[str]]: - """Apply human-like inconsistencies like typos, hesitations.""" - modifications = [] - - # Emotional typos (more when excited or upset) - arousal = emotional_state.get_emotional_arousal() - base_typo_prob = float(self.typo_probability) - - # Increase typo probability with high arousal - if arousal > float(self.excitement_threshold): - typo_prob = base_typo_prob * (1 + arousal) - else: - typo_prob = base_typo_prob * 0.5 - - # Apply typos - if random.random() < typo_prob * intensity: - text, typo_mod = self._add_realistic_typo(text) - if typo_mod: - modifications.append(typo_mod) - - # Add hesitations for uncertain emotions - if emotional_state.fear > 0.6 or emotional_state.emotional_clarity < 0.5: - if random.random() < 0.3: - hesitations = ['um...', 'well...', 'I mean...', 'like...'] - hesitation = random.choice(hesitations) - text = hesitation + ' ' + text - modifications.append(f"Added hesitation: {hesitation}") - - # Add thinking markers for curiosity - if emotional_state.curiosity > 0.7: - if random.random() < 0.4: - thinking_markers = ['Hmm...', 'Let me think...', 'Interesting...'] - marker = random.choice(thinking_markers) - text = marker + ' ' + text - modifications.append(f"Added thinking marker: {marker}") - - return text, modifications - - def _add_realistic_typo(self, text: str) -> Tuple[str, Optional[str]]: - """Add realistic typos that humans might make when emotional.""" - words = text.split() - if len(words) < 2: - return text, None - - # Common typo patterns - typo_patterns = [ - ('the', 'teh'), - ('and', 'adn'), - ('you', 'yuo'), - ('that', 'taht'), - ('this', 'tihs'), - ('really', 'realy'), - ('because', 'becuase'), - ('definitely', 'definately'), - ('probably', 'probaly') - ] - - # Try to apply a typo - for original, typo in typo_patterns: - if original in words: - idx = words.index(original) - words[idx] = typo - return ' '.join(words), f"Added typo: {original} -> {typo}" - - # Letter swapping typo - target_word_idx = random.randint(0, len(words) - 1) - word = words[target_word_idx] - - if len(word) > 3: - # Swap two adjacent letters - pos = random.randint(0, len(word) - 2) - word_list = list(word) - word_list[pos], word_list[pos + 1] = word_list[pos + 1], word_list[pos] - words[target_word_idx] = ''.join(word_list) - return ' '.join(words), f"Letter swap typo in: {word}" - - return text, None - - def _apply_contextual_adjustments( - self, - text: str, - context: str, - emotional_state: EmotionalState - ) -> Tuple[str, List[str]]: - """Apply contextual adjustments based on conversation context.""" - modifications = [] - - # Formal context - reduce emotional expression - if context in ['formal', 'professional', 'academic']: - # Remove excessive punctuation - text = re.sub(r'!{2,}', '!', text) - text = re.sub(r'\?{2,}', '?', text) - modifications.append("Reduced punctuation for formal context") - - # Remove casual expressions - casual_expressions = ['wow', 'yay', 'awesome', 'cool', 'omg'] - for expr in casual_expressions: - if expr.lower() in text.lower(): - text = re.sub(r'\b' + expr + r'\b', '', text, flags=re.IGNORECASE) - modifications.append(f"Removed casual expression: {expr}") - - # Casual context - enhance emotional expression - elif context in ['casual', 'friendly', 'personal']: - # Allow more emotional expression - if emotional_state.joy > 0.7 and random.random() < 0.3: - text = text + ' ๐' - modifications.append("Added emoji for casual context") - - # Crisis/support context - adjust for empathy - elif context in ['support', 'crisis', 'emotional']: - # Add empathetic language - if emotional_state.empathy_level > 0.6: - empathy_phrases = [ - "I understand how you feel.", - "That sounds really difficult.", - "I'm here for you." - ] - if random.random() < 0.4: - phrase = random.choice(empathy_phrases) - text = phrase + ' ' + text - modifications.append(f"Added empathy: {phrase}") - - return text, modifications - - def analyze_emotional_expression(self, text: str) -> Dict[str, Any]: - """Analyze the emotional expression in a given text.""" - analysis = { - 'detected_emotions': [], - 'expression_intensity': 0.0, - 'punctuation_analysis': {}, - 'vocabulary_analysis': {}, - 'inconsistencies': [] - } - - # Punctuation analysis - exclamations = len(re.findall(r'!+', text)) - questions = len(re.findall(r'\?+', text)) - ellipses = len(re.findall(r'\.{2,}', text)) - - analysis['punctuation_analysis'] = { - 'exclamations': exclamations, - 'questions': questions, - 'ellipses': ellipses, - 'caps_words': len(re.findall(r'\b[A-Z]{2,}\b', text)) - } - - # Detect emotions from vocabulary - for emotion, vocab in self.emotional_vocabularies.items(): - emotion_score = 0 - for category, words in vocab.items(): - for word in words: - if word.lower() in text.lower(): - emotion_score += 1 - - if emotion_score > 0: - analysis['detected_emotions'].append({ - 'emotion': emotion, - 'score': emotion_score, - 'confidence': min(1.0, emotion_score / 3.0) - }) - - # Overall expression intensity - intensity_indicators = exclamations + questions + ellipses - analysis['expression_intensity'] = min(1.0, intensity_indicators / 5.0) - - # Detect potential typos - common_typos = ['teh', 'adn', 'yuo', 'taht', 'tihs', 'realy', 'becuase', 'definately'] - for typo in common_typos: - if typo in text.lower(): - analysis['inconsistencies'].append(f"Possible typo: {typo}") - - return analysis - - def get_expression_statistics(self) -> Dict[str, Any]: - """Get statistics about emotional expression patterns.""" - return { - 'current_typo_probability': float(self.typo_probability), - 'excitement_threshold': float(self.excitement_threshold), - 'available_emotions': list(self.emotional_vocabularies.keys()), - 'expression_patterns': list(self.expression_patterns.keys()), - 'total_vocabulary_entries': sum( - len(vocab) for emotion_vocab in self.emotional_vocabularies.values() - for vocab in emotion_vocab.values() - ) - } - - def calibrate_expression_intensity(self, feedback_history: List[Dict[str, Any]]): - """Calibrate expression intensity based on user feedback.""" - if not feedback_history: - return - - # Analyze feedback for different expression intensities - positive_feedback = [f for f in feedback_history if f.get('rating', 0) > 0.6] - negative_feedback = [f for f in feedback_history if f.get('rating', 0) < 0.4] - - # Adjust typo probability based on feedback - if len(positive_feedback) > len(negative_feedback): - # More positive feedback - can be more expressive - self.typo_probability.data *= 1.02 - else: - # More negative feedback - tone down expression - self.typo_probability.data *= 0.98 - - # Clamp typo probability - self.typo_probability.data = torch.clamp(self.typo_probability.data, 0.001, 0.1) - - logger.info(f"Calibrated expression intensity - typo probability: {float(self.typo_probability):.4f}") - - def create_emotional_response_template(self, emotion: str, intensity: float) -> str: - """Create a template response showing how this emotion would be expressed.""" - if emotion not in self.emotional_vocabularies: - return "I'm not sure how to express that emotion." - - vocab = self.emotional_vocabularies[emotion] - template_parts = [] - - # Add emotional expression - if 'expressions' in vocab: - expression = random.choice(vocab['expressions']) - template_parts.append(expression) - - # Add appropriate punctuation - if emotion in self.expression_patterns: - patterns = self.expression_patterns[emotion] - if 'punctuation' in patterns: - punct = random.choice(patterns['punctuation']) - if template_parts: - template_parts[-1] = template_parts[-1].rstrip('.!?') + punct - - return ' '.join(template_parts) if template_parts else f"*feeling {emotion}*" \ No newline at end of file diff --git a/lyra/emotions/system.py b/lyra/emotions/system.py deleted file mode 100644 index bac13c2..0000000 --- a/lyra/emotions/system.py +++ /dev/null @@ -1,680 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -from dataclasses import dataclass, field -from datetime import datetime, timedelta -import logging -import json -from pathlib import Path - -logger = logging.getLogger(__name__) - -@dataclass -class EmotionalState: - """ - Represents Lyra's current emotional state with multiple dimensions. - - This captures the complexity of human emotions - multiple feelings can exist - simultaneously with different intensities. - """ - # Primary emotions (based on Plutchik's wheel) - joy: float = 0.5 - sadness: float = 0.1 - anger: float = 0.1 - fear: float = 0.1 - surprise: float = 0.2 - disgust: float = 0.1 - trust: float = 0.6 - anticipation: float = 0.4 - - # Complex emotions (combinations of primary) - love: float = 0.5 # joy + trust - guilt: float = 0.1 # fear + disgust - shame: float = 0.1 # fear + disgust - pride: float = 0.4 # joy + anger - jealousy: float = 0.1 # anger + fear - hope: float = 0.6 # trust + anticipation - despair: float = 0.1 # sadness + fear - curiosity: float = 0.7 # surprise + anticipation - - # Meta-emotional states - emotional_intensity: float = 0.5 # How strongly emotions are felt - emotional_stability: float = 0.7 # Resistance to emotional change - emotional_clarity: float = 0.6 # How well emotions are understood - - # Context - timestamp: Optional[datetime] = None - trigger: Optional[str] = None - confidence: float = 1.0 - - def __post_init__(self): - if self.timestamp is None: - self.timestamp = datetime.now() - - def to_tensor(self, device: Optional[torch.device] = None) -> torch.Tensor: - """Convert emotional state to tensor for neural processing.""" - values = [ - self.joy, self.sadness, self.anger, self.fear, - self.surprise, self.disgust, self.trust, self.anticipation, - self.love, self.guilt, self.shame, self.pride, - self.jealousy, self.hope, self.despair, self.curiosity, - self.emotional_intensity, self.emotional_stability, self.emotional_clarity - ] - return torch.tensor(values, dtype=torch.float32, device=device) - - @classmethod - def from_tensor(cls, tensor: torch.Tensor, trigger: str = None) -> 'EmotionalState': - """Create emotional state from tensor.""" - values = tensor.detach().cpu().numpy() - return cls( - joy=float(values[0]), sadness=float(values[1]), anger=float(values[2]), - fear=float(values[3]), surprise=float(values[4]), disgust=float(values[5]), - trust=float(values[6]), anticipation=float(values[7]), love=float(values[8]), - guilt=float(values[9]), shame=float(values[10]), pride=float(values[11]), - jealousy=float(values[12]), hope=float(values[13]), despair=float(values[14]), - curiosity=float(values[15]), emotional_intensity=float(values[16]), - emotional_stability=float(values[17]), emotional_clarity=float(values[18]), - trigger=trigger - ) - - def get_dominant_emotion(self) -> Tuple[str, float]: - """Get the most prominent emotion.""" - emotions = { - 'joy': self.joy, 'sadness': self.sadness, 'anger': self.anger, - 'fear': self.fear, 'surprise': self.surprise, 'disgust': self.disgust, - 'trust': self.trust, 'anticipation': self.anticipation, 'love': self.love, - 'guilt': self.guilt, 'shame': self.shame, 'pride': self.pride, - 'jealousy': self.jealousy, 'hope': self.hope, 'despair': self.despair, - 'curiosity': self.curiosity - } - - dominant_emotion = max(emotions.items(), key=lambda x: x[1]) - return dominant_emotion - - def get_emotional_valence(self) -> float: - """Get overall emotional valence (positive/negative).""" - positive = self.joy + self.trust + self.love + self.pride + self.hope + self.curiosity - negative = self.sadness + self.anger + self.fear + self.disgust + self.guilt + self.shame + self.jealousy + self.despair - - return (positive - negative) / (positive + negative + 1e-8) - - def get_emotional_arousal(self) -> float: - """Get emotional arousal level (calm/excited).""" - high_arousal = self.anger + self.fear + self.surprise + self.joy + self.anticipation - low_arousal = self.sadness + self.trust + self.disgust - - return high_arousal / (high_arousal + low_arousal + 1e-8) - -@dataclass -class EmotionMemory: - """Memory of past emotional experiences.""" - emotional_state: EmotionalState - context: str - intensity: float - impact_score: float # How much this memory affects current emotions - decay_rate: float = 0.95 - - def __post_init__(self): - self.creation_time = datetime.now() - - def get_current_impact(self) -> float: - """Get current impact considering decay.""" - time_passed = (datetime.now() - self.creation_time).total_seconds() / 3600 # hours - return self.impact_score * (self.decay_rate ** time_passed) - - def is_significant(self, threshold: float = 0.1) -> bool: - """Check if memory is still significant.""" - return self.get_current_impact() > threshold - -class EmotionalSystem(nn.Module): - """ - Sophisticated emotional system that allows Lyra to experience and express - emotions like a real person, with emotional memory and growth. - """ - - def __init__( - self, - input_dim: int = 512, - emotion_dim: int = 19, - memory_capacity: int = 1000, - device: Optional[torch.device] = None - ): - super().__init__() - - self.emotion_dim = emotion_dim - self.memory_capacity = memory_capacity - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - - # Current emotional state - self.current_state = EmotionalState() - - # Emotional processing networks - self.context_processor = nn.Sequential( - nn.Linear(input_dim, 256), - nn.LayerNorm(256), - nn.ReLU(), - nn.Dropout(0.1), - nn.Linear(256, 128), - nn.ReLU(), - nn.Linear(128, 64) - ) - - # Emotion generation network - self.emotion_generator = nn.Sequential( - nn.Linear(64 + emotion_dim, 128), # Context + current emotions - nn.LayerNorm(128), - nn.ReLU(), - nn.Linear(128, 64), - nn.ReLU(), - nn.Linear(64, emotion_dim), - nn.Sigmoid() # Emotions are bounded [0, 1] - ) - - # Memory influence network - self.memory_network = nn.Sequential( - nn.Linear(emotion_dim * 2, 64), # Current + memory emotions - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, emotion_dim), - nn.Tanh() # Memory influence can be positive or negative - ) - - # Emotional regulation network (like human emotional control) - self.regulation_network = nn.Sequential( - nn.Linear(emotion_dim + 5, 64), # Emotions + regulation signals - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, emotion_dim), - nn.Sigmoid() - ) - - # Emotional memory - self.emotion_memories: List[EmotionMemory] = [] - - # Emotional learning parameters - self.emotional_learning_rate = nn.Parameter(torch.tensor(0.1)) - self.memory_consolidation_threshold = nn.Parameter(torch.tensor(0.7)) - - # Emotional patterns (learned responses to situations) - self.emotional_patterns = {} - - # Emotional growth tracking - self.emotional_maturity = 0.5 - self.emotional_experiences = 0 - - self.to(self.device) - - def forward( - self, - context_embedding: torch.Tensor, - user_feedback: Optional[torch.Tensor] = None, - social_context: Optional[Dict[str, Any]] = None, - regulate_emotions: bool = True - ) -> Tuple[EmotionalState, Dict[str, Any]]: - """ - Process current context and generate emotional response. - - Args: - context_embedding: Current conversation/situation context - user_feedback: Feedback about previous emotional responses - social_context: Social context information - regulate_emotions: Whether to apply emotional regulation - - Returns: - new_emotional_state: Updated emotional state - emotion_info: Information about emotional processing - """ - batch_size = context_embedding.shape[0] - - # Process context - context_features = self.context_processor(context_embedding.mean(dim=1)) - - # Get current emotions as tensor - current_emotions = self.current_state.to_tensor(self.device).unsqueeze(0).repeat(batch_size, 1) - - # Apply memory influence - memory_influence = self._get_memory_influence(current_emotions) - influenced_emotions = current_emotions + 0.3 * memory_influence - - # Generate new emotional response - emotion_input = torch.cat([context_features, influenced_emotions], dim=1) - raw_emotions = self.emotion_generator(emotion_input) - - # Apply emotional regulation if enabled - if regulate_emotions: - regulation_signals = self._get_regulation_signals(social_context, batch_size) - regulation_input = torch.cat([raw_emotions, regulation_signals], dim=1) - regulated_emotions = self.regulation_network(regulation_input) - final_emotions = regulated_emotions - else: - final_emotions = raw_emotions - - # Update current state - new_state = EmotionalState.from_tensor( - final_emotions[0], - trigger=social_context.get('trigger', 'interaction') if social_context else 'interaction' - ) - - # Learn from feedback if provided - emotion_info = {} - if user_feedback is not None: - learning_info = self._learn_from_feedback(user_feedback, new_state) - emotion_info.update(learning_info) - - # Store significant emotional experiences - if self._is_emotionally_significant(new_state): - self._store_emotional_memory(new_state, context_embedding, social_context) - - # Update emotional maturity - self._update_emotional_growth(new_state) - - # Prepare emotion info - emotion_info.update({ - 'dominant_emotion': new_state.get_dominant_emotion(), - 'emotional_valence': new_state.get_emotional_valence(), - 'emotional_arousal': new_state.get_emotional_arousal(), - 'memory_influence_strength': torch.norm(memory_influence).item(), - 'emotional_maturity': self.emotional_maturity, - 'regulation_applied': regulate_emotions, - 'significant_memories': len([m for m in self.emotion_memories if m.is_significant()]) - }) - - # Update current state - self.current_state = new_state - - return new_state, emotion_info - - def _get_memory_influence(self, current_emotions: torch.Tensor) -> torch.Tensor: - """Get influence from emotional memories on current state.""" - if not self.emotion_memories: - return torch.zeros_like(current_emotions) - - # Get significant memories - significant_memories = [m for m in self.emotion_memories if m.is_significant()] - - if not significant_memories: - return torch.zeros_like(current_emotions) - - # Compute weighted influence - total_influence = torch.zeros_like(current_emotions) - total_weight = 0.0 - - for memory in significant_memories[-20:]: # Use recent significant memories - memory_emotions = memory.emotional_state.to_tensor(self.device).unsqueeze(0) - weight = memory.get_current_impact() - - # Compute influence using memory network - memory_input = torch.cat([current_emotions, memory_emotions], dim=1) - influence = self.memory_network(memory_input) - - total_influence += weight * influence - total_weight += weight - - if total_weight > 0: - return total_influence / total_weight - else: - return torch.zeros_like(current_emotions) - - def _get_regulation_signals(self, social_context: Optional[Dict[str, Any]], batch_size: int) -> torch.Tensor: - """Get emotional regulation signals based on context.""" - signals = torch.zeros(batch_size, 5, device=self.device) - - if social_context: - # Formality regulation - formality = social_context.get('formality_level', 0.5) - signals[:, 0] = formality # Higher formality = more emotional control - - # Social pressure regulation - group_size = social_context.get('group_size', 1) - signals[:, 1] = min(1.0, group_size / 10.0) # More people = more regulation - - # Conflict regulation - if social_context.get('has_conflict', False): - signals[:, 2] = 0.8 # High regulation during conflict - - # Time pressure regulation - time_pressure = social_context.get('time_pressure', 0.0) - signals[:, 3] = time_pressure - - # Emotional safety regulation - emotional_safety = social_context.get('emotional_safety', 0.8) - signals[:, 4] = 1.0 - emotional_safety # Less safe = more regulation - - return signals - - def _is_emotionally_significant(self, state: EmotionalState) -> bool: - """Determine if an emotional state is significant enough to remember.""" - # High intensity emotions - if state.emotional_intensity > 0.8: - return True - - # Strong specific emotions - dominant_emotion, intensity = state.get_dominant_emotion() - if intensity > 0.8: - return True - - # Extreme valence - if abs(state.get_emotional_valence()) > 0.7: - return True - - # High arousal - if state.get_emotional_arousal() > 0.8: - return True - - return False - - def _store_emotional_memory( - self, - state: EmotionalState, - context: torch.Tensor, - social_context: Optional[Dict[str, Any]] - ): - """Store significant emotional experience in memory.""" - # Calculate impact score - intensity = state.emotional_intensity - valence_strength = abs(state.get_emotional_valence()) - arousal = state.get_emotional_arousal() - - impact_score = (intensity + valence_strength + arousal) / 3.0 - - # Create memory - memory = EmotionMemory( - emotional_state=state, - context=social_context.get('description', 'interaction') if social_context else 'interaction', - intensity=intensity, - impact_score=impact_score - ) - - self.emotion_memories.append(memory) - - # Manage memory capacity - if len(self.emotion_memories) > self.memory_capacity: - # Remove least significant old memories - self.emotion_memories.sort(key=lambda m: m.get_current_impact()) - self.emotion_memories = self.emotion_memories[-self.memory_capacity:] - - logger.debug(f"Stored emotional memory: {state.get_dominant_emotion()[0]} " - f"(impact: {impact_score:.3f})") - - def _learn_from_feedback(self, feedback: torch.Tensor, state: EmotionalState) -> Dict[str, Any]: - """Learn from user feedback about emotional responses.""" - feedback_value = feedback.mean().item() - - learning_info = { - 'feedback_received': feedback_value, - 'learning_applied': False - } - - # Adjust emotional learning rate based on feedback - if feedback_value > 0.7: # Positive feedback - self.emotional_learning_rate.data *= 1.01 - learning_info['learning_applied'] = True - learning_info['adjustment'] = 'increased_sensitivity' - - elif feedback_value < 0.3: # Negative feedback - self.emotional_learning_rate.data *= 0.98 - learning_info['learning_applied'] = True - learning_info['adjustment'] = 'decreased_sensitivity' - - # Clamp learning rate - self.emotional_learning_rate.data = torch.clamp( - self.emotional_learning_rate.data, 0.01, 0.5 - ) - - # Store feedback pattern for this emotional state - dominant_emotion, intensity = state.get_dominant_emotion() - - if dominant_emotion not in self.emotional_patterns: - self.emotional_patterns[dominant_emotion] = { - 'positive_feedback_count': 0, - 'negative_feedback_count': 0, - 'total_feedback': 0, - 'avg_feedback': 0.5 - } - - pattern = self.emotional_patterns[dominant_emotion] - pattern['total_feedback'] += 1 - - if feedback_value > 0.6: - pattern['positive_feedback_count'] += 1 - elif feedback_value < 0.4: - pattern['negative_feedback_count'] += 1 - - pattern['avg_feedback'] = ( - (pattern['avg_feedback'] * (pattern['total_feedback'] - 1) + feedback_value) / - pattern['total_feedback'] - ) - - return learning_info - - def _update_emotional_growth(self, state: EmotionalState): - """Update emotional maturity and growth metrics.""" - self.emotional_experiences += 1 - - # Emotional maturity grows with diverse emotional experiences - emotion_diversity = self._calculate_emotion_diversity(state) - emotional_clarity = state.emotional_clarity - - growth_factor = (emotion_diversity + emotional_clarity) / 2.0 - self.emotional_maturity = ( - 0.999 * self.emotional_maturity + 0.001 * growth_factor - ) - - # Clamp maturity - self.emotional_maturity = np.clip(self.emotional_maturity, 0.0, 1.0) - - def _calculate_emotion_diversity(self, state: EmotionalState) -> float: - """Calculate how diverse the current emotional state is.""" - emotions = [ - state.joy, state.sadness, state.anger, state.fear, - state.surprise, state.disgust, state.trust, state.anticipation - ] - - # Calculate entropy as measure of diversity - emotions_array = np.array(emotions) + 1e-8 - emotions_array = emotions_array / emotions_array.sum() - - entropy = -np.sum(emotions_array * np.log(emotions_array)) - max_entropy = np.log(len(emotions)) - - return entropy / max_entropy - - def get_emotional_context_for_response(self) -> Dict[str, Any]: - """Get emotional context to influence response generation.""" - dominant_emotion, intensity = self.current_state.get_dominant_emotion() - - return { - 'dominant_emotion': dominant_emotion, - 'emotion_intensity': intensity, - 'emotional_valence': self.current_state.get_emotional_valence(), - 'emotional_arousal': self.current_state.get_emotional_arousal(), - 'emotional_stability': self.current_state.emotional_stability, - 'emotional_maturity': self.emotional_maturity, - 'recent_emotional_patterns': self._get_recent_emotional_patterns() - } - - def _get_recent_emotional_patterns(self) -> Dict[str, float]: - """Get patterns from recent emotional experiences.""" - if len(self.emotion_memories) < 5: - return {} - - recent_memories = self.emotion_memories[-10:] - emotion_counts = {} - - for memory in recent_memories: - dominant_emotion, _ = memory.emotional_state.get_dominant_emotion() - emotion_counts[dominant_emotion] = emotion_counts.get(dominant_emotion, 0) + 1 - - total = len(recent_memories) - return {emotion: count / total for emotion, count in emotion_counts.items()} - - def simulate_emotional_reaction(self, trigger: str, intensity: float = 1.0) -> EmotionalState: - """Simulate emotional reaction to a specific trigger.""" - # Define emotional responses to different triggers - trigger_responses = { - 'praise': EmotionalState(joy=0.8, pride=0.7, trust=0.6), - 'criticism': EmotionalState(sadness=0.6, shame=0.5, anger=0.3), - 'surprise': EmotionalState(surprise=0.9, curiosity=0.7, anticipation=0.6), - 'threat': EmotionalState(fear=0.8, anger=0.4, trust=0.2), - 'loss': EmotionalState(sadness=0.9, despair=0.6, anger=0.3), - 'achievement': EmotionalState(joy=0.9, pride=0.8, anticipation=0.7), - 'betrayal': EmotionalState(anger=0.8, sadness=0.7, trust=0.1), - 'love': EmotionalState(love=0.9, joy=0.8, trust=0.9), - 'discovery': EmotionalState(curiosity=0.9, surprise=0.7, joy=0.6) - } - - if trigger in trigger_responses: - base_response = trigger_responses[trigger] - - # Modify based on current emotional state and maturity - current_influence = 0.3 * (1 - self.emotional_maturity) - - # Blend with current state - blended_state = EmotionalState( - joy=(1 - current_influence) * base_response.joy + current_influence * self.current_state.joy, - sadness=(1 - current_influence) * base_response.sadness + current_influence * self.current_state.sadness, - anger=(1 - current_influence) * base_response.anger + current_influence * self.current_state.anger, - fear=(1 - current_influence) * base_response.fear + current_influence * self.current_state.fear, - surprise=(1 - current_influence) * base_response.surprise + current_influence * self.current_state.surprise, - disgust=(1 - current_influence) * base_response.disgust + current_influence * self.current_state.disgust, - trust=(1 - current_influence) * base_response.trust + current_influence * self.current_state.trust, - anticipation=(1 - current_influence) * base_response.anticipation + current_influence * self.current_state.anticipation, - love=(1 - current_influence) * base_response.love + current_influence * self.current_state.love, - pride=(1 - current_influence) * base_response.pride + current_influence * self.current_state.pride, - emotional_intensity=intensity, - trigger=trigger - ) - - return blended_state - - else: - # Unknown trigger - slight emotional disturbance - return EmotionalState( - surprise=0.4, - curiosity=0.5, - emotional_intensity=intensity * 0.5, - trigger=trigger - ) - - def get_emotional_summary(self) -> Dict[str, Any]: - """Get comprehensive summary of emotional system state.""" - return { - 'current_state': { - 'dominant_emotion': self.current_state.get_dominant_emotion(), - 'valence': self.current_state.get_emotional_valence(), - 'arousal': self.current_state.get_emotional_arousal(), - 'intensity': self.current_state.emotional_intensity, - 'stability': self.current_state.emotional_stability - }, - 'emotional_growth': { - 'maturity': self.emotional_maturity, - 'total_experiences': self.emotional_experiences, - 'learning_rate': float(self.emotional_learning_rate.detach()) - }, - 'memory_system': { - 'total_memories': len(self.emotion_memories), - 'significant_memories': len([m for m in self.emotion_memories if m.is_significant()]), - 'memory_capacity': self.memory_capacity - }, - 'emotional_patterns': self.emotional_patterns, - 'recent_patterns': self._get_recent_emotional_patterns() - } - - def save_emotional_state(self, path: Path): - """Save emotional system state.""" - state = { - 'current_state': { - 'joy': self.current_state.joy, - 'sadness': self.current_state.sadness, - 'anger': self.current_state.anger, - 'fear': self.current_state.fear, - 'surprise': self.current_state.surprise, - 'disgust': self.current_state.disgust, - 'trust': self.current_state.trust, - 'anticipation': self.current_state.anticipation, - 'love': self.current_state.love, - 'guilt': self.current_state.guilt, - 'shame': self.current_state.shame, - 'pride': self.current_state.pride, - 'jealousy': self.current_state.jealousy, - 'hope': self.current_state.hope, - 'despair': self.current_state.despair, - 'curiosity': self.current_state.curiosity, - 'emotional_intensity': self.current_state.emotional_intensity, - 'emotional_stability': self.current_state.emotional_stability, - 'emotional_clarity': self.current_state.emotional_clarity - }, - 'emotional_maturity': self.emotional_maturity, - 'emotional_experiences': self.emotional_experiences, - 'emotional_learning_rate': float(self.emotional_learning_rate), - 'emotional_patterns': self.emotional_patterns, - 'emotion_memories': [ - { - 'emotional_state': memory.emotional_state.__dict__, - 'context': memory.context, - 'intensity': memory.intensity, - 'impact_score': memory.impact_score, - 'creation_time': memory.creation_time.isoformat() - } - for memory in self.emotion_memories[-200:] # Keep recent memories - ], - 'model_state': self.state_dict(), - 'timestamp': datetime.now().isoformat() - } - - with open(path, 'w') as f: - json.dump(state, f, indent=2, default=str) - - logger.info(f"Emotional state saved to {path}") - - def load_emotional_state(self, path: Path): - """Load emotional system state.""" - if not path.exists(): - logger.warning(f"Emotional state file not found: {path}") - return - - try: - with open(path, 'r') as f: - state = json.load(f) - - # Restore current emotional state - current_state_data = state['current_state'] - self.current_state = EmotionalState(**current_state_data) - - # Restore growth metrics - self.emotional_maturity = state.get('emotional_maturity', 0.5) - self.emotional_experiences = state.get('emotional_experiences', 0) - - if 'emotional_learning_rate' in state: - self.emotional_learning_rate.data = torch.tensor(state['emotional_learning_rate']) - - # Restore patterns - self.emotional_patterns = state.get('emotional_patterns', {}) - - # Restore memories - self.emotion_memories = [] - for memory_data in state.get('emotion_memories', []): - emotion_state_data = memory_data['emotional_state'] - emotion_state = EmotionalState(**emotion_state_data) - - memory = EmotionMemory( - emotional_state=emotion_state, - context=memory_data['context'], - intensity=memory_data['intensity'], - impact_score=memory_data['impact_score'] - ) - memory.creation_time = datetime.fromisoformat(memory_data['creation_time']) - self.emotion_memories.append(memory) - - # Restore model state - if 'model_state' in state: - self.load_state_dict(state['model_state']) - - logger.info(f"Emotional state loaded from {path}") - - except Exception as e: - logger.error(f"Failed to load emotional state: {e}") \ No newline at end of file diff --git a/lyra/knowledge/__init__.py b/lyra/knowledge/__init__.py deleted file mode 100644 index 8ccb53d..0000000 --- a/lyra/knowledge/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Lyra Knowledge Acquisition Module - -Handles acquisition of legally obtained knowledge from various sources -including Project Gutenberg, with emphasis on quality, legality, and ethics. -""" - -from .gutenberg_crawler import GutenbergCrawler -from .knowledge_processor import KnowledgeProcessor -from .acquisition_manager import KnowledgeAcquisitionManager - -__all__ = [ - "GutenbergCrawler", - "KnowledgeProcessor", - "KnowledgeAcquisitionManager" -] \ No newline at end of file diff --git a/lyra/knowledge/acquisition_manager.py b/lyra/knowledge/acquisition_manager.py deleted file mode 100644 index a42bbfd..0000000 --- a/lyra/knowledge/acquisition_manager.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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 \ No newline at end of file diff --git a/lyra/knowledge/gutenberg_crawler.py b/lyra/knowledge/gutenberg_crawler.py deleted file mode 100644 index bc5c659..0000000 --- a/lyra/knowledge/gutenberg_crawler.py +++ /dev/null @@ -1,552 +0,0 @@ -""" -Project Gutenberg crawler for legally obtaining public domain texts. - -This crawler respects Project Gutenberg's terms of service and -implements proper rate limiting and legal compliance. -""" - -import asyncio -import aiohttp -import aiofiles -import logging -from typing import Dict, List, Optional, AsyncGenerator, Tuple, Any -from dataclasses import dataclass -from datetime import datetime, timedelta -import re -import time -from pathlib import Path -import xml.etree.ElementTree as ET -from urllib.parse import urljoin, urlparse -import gzip -from bs4 import BeautifulSoup - -logger = logging.getLogger(__name__) - - -@dataclass -class GutenbergBook: - """Represents a Project Gutenberg book.""" - id: int - title: str - author: str - language: str - category: str - url: str - file_format: str - download_url: str - copyright_status: str = "public_domain" - quality_score: float = 0.8 - metadata: Dict = None - - def __post_init__(self): - if self.metadata is None: - self.metadata = {} - - -class GutenbergCrawler: - """ - Ethical crawler for Project Gutenberg that respects their terms of service. - - Implements proper rate limiting, respects robots.txt, and only downloads - public domain content that is legally free to use. - """ - - def __init__( - self, - base_url: str = "https://www.gutenberg.org", - rate_limit: float = 2.0, # Seconds between requests - max_concurrent: int = 3, - user_agent: str = "Lyra-AI/1.0 (Educational Purpose; noreply@lyra-ai.example)", - download_dir: str = "./data/gutenberg" - ): - self.base_url = base_url - self.rate_limit = rate_limit - self.max_concurrent = max_concurrent - self.user_agent = user_agent - self.download_dir = Path(download_dir) - - # Rate limiting - self.last_request_time = 0.0 - self.request_semaphore = asyncio.Semaphore(max_concurrent) - - # Session management - self.session: Optional[aiohttp.ClientSession] = None - - # Crawling state - self.crawled_books: Dict[int, GutenbergBook] = {} - self.failed_downloads: List[int] = [] - - # Legal and ethical compliance - self.allowed_formats = ['txt', 'html', 'epub'] - self.excluded_languages = [] # Can be configured - self.max_book_size_mb = 50 # Reasonable size limit - - # Create download directory - self.download_dir.mkdir(parents=True, exist_ok=True) - - async def __aenter__(self): - """Async context manager entry.""" - await self.initialize() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async context manager exit.""" - await self.close() - - async def initialize(self): - """Initialize the crawler.""" - timeout = aiohttp.ClientTimeout(total=30) - self.session = aiohttp.ClientSession( - timeout=timeout, - headers={"User-Agent": self.user_agent} - ) - - # Verify Project Gutenberg accessibility - await self._verify_gutenberg_access() - - logger.info("Gutenberg crawler initialized") - - async def close(self): - """Close the crawler and cleanup resources.""" - if self.session: - await self.session.close() - - async def _verify_gutenberg_access(self): - """Verify that Project Gutenberg is accessible and we're compliant.""" - try: - # Check robots.txt compliance - robots_url = urljoin(self.base_url, "/robots.txt") - async with self.session.get(robots_url) as response: - if response.status == 200: - robots_txt = await response.text() - logger.info("Retrieved robots.txt for compliance check") - - # Test basic connectivity - async with self.session.get(self.base_url) as response: - if response.status != 200: - raise Exception(f"Cannot access Gutenberg: HTTP {response.status}") - - logger.info("Project Gutenberg access verified") - - except Exception as e: - logger.error(f"Failed to verify Gutenberg access: {e}") - raise - - async def _rate_limited_request(self, url: str) -> aiohttp.ClientResponse: - """Make a rate-limited request.""" - async with self.request_semaphore: - # Ensure rate limiting - current_time = time.time() - time_since_last = current_time - self.last_request_time - - if time_since_last < self.rate_limit: - await asyncio.sleep(self.rate_limit - time_since_last) - - self.last_request_time = time.time() - - # Make request - try: - response = await self.session.get(url) - logger.debug(f"Request to {url}: HTTP {response.status}") - return response - except Exception as e: - logger.error(f"Request failed for {url}: {e}") - raise - - async def discover_books( - self, - categories: Optional[List[str]] = None, - languages: Optional[List[str]] = None, - limit: Optional[int] = None - ) -> AsyncGenerator[GutenbergBook, None]: - """ - Discover books from Project Gutenberg catalog. - - Args: - categories: Specific categories to focus on - languages: Languages to include (default: ['en']) - limit: Maximum number of books to discover - - Yields: - GutenbergBook objects for discovered books - """ - if languages is None: - languages = ['en'] - - discovered_count = 0 - - try: - # Get the catalog feed - catalog_url = urljoin(self.base_url, "/feeds/catalog.rdf.bz2") - - async with self._rate_limited_request(catalog_url) as response: - if response.status != 200: - logger.error(f"Failed to get catalog: HTTP {response.status}") - return - - # Download and decompress catalog - catalog_data = await response.read() - - # Note: This is a simplified approach. In production, - # you'd want to properly handle the bz2 compressed RDF file - logger.info("Processing Gutenberg catalog...") - - # For now, let's use the simpler approach of browsing categories - for category in (categories or ["Fiction", "Science", "Philosophy", "History"]): - if limit and discovered_count >= limit: - break - - async for book in self._discover_books_in_category(category, languages): - if limit and discovered_count >= limit: - break - - yield book - discovered_count += 1 - - except Exception as e: - logger.error(f"Error discovering books: {e}") - - async def _discover_books_in_category( - self, - category: str, - languages: List[str] - ) -> AsyncGenerator[GutenbergBook, None]: - """Discover books in a specific category.""" - try: - # Browse category page - category_url = urljoin(self.base_url, f"/browse/scores/top") - - async with self._rate_limited_request(category_url) as response: - if response.status != 200: - return - - html_content = await response.text() - soup = BeautifulSoup(html_content, 'html.parser') - - # Find book links (this is a simplified parser) - book_links = soup.find_all('a', href=re.compile(r'/ebooks/\d+')) - - for link in book_links[:20]: # Limit per category - try: - book_id = int(re.search(r'/ebooks/(\d+)', link['href']).group(1)) - book_title = link.get_text(strip=True) - - # Get book details - book = await self._get_book_details(book_id, book_title, category) - if book and book.language in languages: - yield book - - except Exception as e: - logger.warning(f"Failed to process book link {link}: {e}") - continue - - except Exception as e: - logger.error(f"Error discovering books in category {category}: {e}") - - async def _get_book_details( - self, - book_id: int, - title: str, - category: str - ) -> Optional[GutenbergBook]: - """Get detailed information about a specific book.""" - try: - book_url = urljoin(self.base_url, f"/ebooks/{book_id}") - - async with self._rate_limited_request(book_url) as response: - if response.status != 200: - return None - - html_content = await response.text() - soup = BeautifulSoup(html_content, 'html.parser') - - # Extract metadata - author = "Unknown" - language = "en" - - # Try to find author - author_elem = soup.find('a', href=re.compile(r'/browse/authors/')) - if author_elem: - author = author_elem.get_text(strip=True) - - # Try to find language - lang_elem = soup.find('tr', string=re.compile(r'Language:')) - if lang_elem: - lang_td = lang_elem.find_next_sibling('td') - if lang_td: - language = lang_td.get_text(strip=True).lower()[:2] - - # Find download links - download_url = await self._find_best_download_url(book_id, soup) - if not download_url: - return None - - # Determine file format - file_format = self._determine_file_format(download_url) - - # Create book object - book = GutenbergBook( - id=book_id, - title=title, - author=author, - language=language, - category=category, - url=book_url, - file_format=file_format, - download_url=download_url, - metadata={ - 'discovered_at': datetime.now().isoformat(), - 'source': 'gutenberg_crawler' - } - ) - - return book - - except Exception as e: - logger.error(f"Failed to get details for book {book_id}: {e}") - return None - - async def _find_best_download_url( - self, - book_id: int, - soup: BeautifulSoup - ) -> Optional[str]: - """Find the best download URL for a book.""" - # Look for download links in order of preference - download_links = soup.find_all('a', href=re.compile(r'\.txt|\.html|\.epub')) - - for format_pref in ['txt', 'html', 'epub']: - for link in download_links: - href = link.get('href', '') - if format_pref in href.lower(): - # Ensure it's a full URL - if href.startswith('http'): - return href - else: - return urljoin(self.base_url, href) - - # Fallback: try direct construction - for format_ext in ['txt', 'html']: - potential_url = f"{self.base_url}/files/{book_id}/{book_id}-0.{format_ext}" - return potential_url # We'll validate this during download - - return None - - def _determine_file_format(self, url: str) -> str: - """Determine file format from URL.""" - if '.txt' in url.lower(): - return 'txt' - elif '.html' in url.lower() or '.htm' in url.lower(): - return 'html' - elif '.epub' in url.lower(): - return 'epub' - else: - return 'txt' # Default assumption - - async def download_book(self, book: GutenbergBook) -> Optional[Path]: - """ - Download a book and return the local file path. - - Args: - book: GutenbergBook object to download - - Returns: - Path to downloaded file, or None if download failed - """ - try: - # Validate book is appropriate for download - if not self._is_download_appropriate(book): - logger.warning(f"Book {book.id} not appropriate for download") - return None - - # Create filename - safe_title = re.sub(r'[^\w\s-]', '', book.title)[:50] - filename = f"{book.id}_{safe_title}.{book.file_format}" - file_path = self.download_dir / filename - - # Skip if already downloaded - if file_path.exists(): - logger.info(f"Book {book.id} already downloaded") - return file_path - - # Download the book - async with self._rate_limited_request(book.download_url) as response: - if response.status != 200: - logger.error(f"Download failed for book {book.id}: HTTP {response.status}") - self.failed_downloads.append(book.id) - return None - - # Check content size - content_length = response.headers.get('content-length') - if content_length and int(content_length) > self.max_book_size_mb * 1024 * 1024: - logger.warning(f"Book {book.id} too large: {content_length} bytes") - return None - - # Save file - async with aiofiles.open(file_path, 'wb') as f: - async for chunk in response.content.iter_chunked(8192): - await f.write(chunk) - - logger.info(f"Downloaded book {book.id}: {book.title}") - self.crawled_books[book.id] = book - - return file_path - - except Exception as e: - logger.error(f"Failed to download book {book.id}: {e}") - self.failed_downloads.append(book.id) - return None - - def _is_download_appropriate(self, book: GutenbergBook) -> bool: - """Check if a book is appropriate for download.""" - # Language check - if book.language in self.excluded_languages: - return False - - # Format check - if book.file_format not in self.allowed_formats: - return False - - # Copyright status check - if book.copyright_status != "public_domain": - return False - - # Size check would be done during download - return True - - async def bulk_download( - self, - books: List[GutenbergBook], - max_concurrent: Optional[int] = None - ) -> List[Tuple[GutenbergBook, Optional[Path]]]: - """ - Download multiple books concurrently. - - Args: - books: List of books to download - max_concurrent: Override default concurrency limit - - Returns: - List of (book, file_path) tuples - """ - if max_concurrent: - semaphore = asyncio.Semaphore(max_concurrent) - else: - semaphore = self.request_semaphore - - async def download_with_semaphore(book): - async with semaphore: - file_path = await self.download_book(book) - return (book, file_path) - - # Execute downloads - tasks = [download_with_semaphore(book) for book in books] - results = await asyncio.gather(*tasks, return_exceptions=True) - - # Filter out exceptions - successful_results = [] - for result in results: - if isinstance(result, Exception): - logger.error(f"Download task failed: {result}") - else: - successful_results.append(result) - - return successful_results - - async def get_book_recommendations( - self, - interests: List[str], - limit: int = 10 - ) -> List[GutenbergBook]: - """ - Get book recommendations based on interests. - - Args: - interests: List of interest keywords - limit: Maximum number of recommendations - - Returns: - List of recommended books - """ - recommendations = [] - - # Map interests to Gutenberg categories - interest_mapping = { - 'science': ['Science', 'Technology', 'Physics', 'Biology'], - 'fiction': ['Fiction', 'Literature', 'Adventure'], - 'history': ['History', 'Biography', 'Politics'], - 'philosophy': ['Philosophy', 'Psychology', 'Religion'], - 'art': ['Art', 'Music', 'Architecture'], - 'nature': ['Nature', 'Environment', 'Travel'] - } - - for interest in interests: - categories = interest_mapping.get(interest.lower(), [interest]) - - for category in categories: - if len(recommendations) >= limit: - break - - async for book in self._discover_books_in_category(category, ['en']): - recommendations.append(book) - if len(recommendations) >= limit: - break - - return recommendations[:limit] - - def get_download_statistics(self) -> Dict[str, Any]: - """Get statistics about crawling and downloads.""" - return { - 'total_discovered': len(self.crawled_books), - 'failed_downloads': len(self.failed_downloads), - 'success_rate': ( - len(self.crawled_books) / (len(self.crawled_books) + len(self.failed_downloads)) - if (self.crawled_books or self.failed_downloads) else 0 - ), - 'languages_discovered': list(set( - book.language for book in self.crawled_books.values() - )), - 'categories_discovered': list(set( - book.category for book in self.crawled_books.values() - )), - 'average_quality_score': ( - sum(book.quality_score for book in self.crawled_books.values()) / - len(self.crawled_books) if self.crawled_books else 0 - ) - } - - async def validate_legal_status(self, book: GutenbergBook) -> bool: - """ - Validate that a book is legally free to use. - - All Project Gutenberg books should be public domain, but this - provides an additional verification step. - """ - try: - # All Project Gutenberg books are public domain in the US - if book.copyright_status == "public_domain": - return True - - # Additional validation could be added here - # For example, checking specific copyright dates or regions - - return True # Default to true for Gutenberg books - - except Exception as e: - logger.error(f"Legal validation failed for book {book.id}: {e}") - return False - - async def cleanup_failed_downloads(self): - """Clean up any partial or failed downloads.""" - for book_id in self.failed_downloads: - # Find and remove any partial files - pattern = f"{book_id}_*.{self.allowed_formats}" - for file_path in self.download_dir.glob(pattern): - try: - file_path.unlink() - logger.info(f"Cleaned up partial download: {file_path}") - except Exception as e: - logger.warning(f"Failed to clean up {file_path}: {e}") - - # Clear the failed downloads list - self.failed_downloads.clear() \ No newline at end of file diff --git a/lyra/knowledge/knowledge_processor.py b/lyra/knowledge/knowledge_processor.py deleted file mode 100644 index 9c1a6bf..0000000 --- a/lyra/knowledge/knowledge_processor.py +++ /dev/null @@ -1,656 +0,0 @@ -""" -Knowledge processor for extracting, cleaning, and structuring knowledge -from various text sources for Lyra's learning. -""" - -import asyncio -import logging -import re -import nltk -import spacy -from typing import Dict, List, Optional, Tuple, Set, Any -from dataclasses import dataclass -from pathlib import Path -import torch -import torch.nn as nn -from sentence_transformers import SentenceTransformer -from transformers import pipeline -import numpy as np -from collections import Counter -import textstat -from bs4 import BeautifulSoup -import json - -logger = logging.getLogger(__name__) - - -@dataclass -class ProcessedKnowledge: - """Represents processed knowledge ready for storage.""" - title: str - content: str - summary: str - category: str - subcategory: Optional[str] - keywords: List[str] - concepts: List[str] - quality_score: float - complexity_score: float - embedding: Optional[np.ndarray] - chunks: List[Dict[str, Any]] - metadata: Dict[str, Any] - - -@dataclass -class TextChunk: - """Represents a chunk of text with metadata.""" - content: str - start_pos: int - end_pos: int - chunk_type: str # 'paragraph', 'section', 'chapter' - importance_score: float - concepts: List[str] - embedding: Optional[np.ndarray] = None - - -class KnowledgeProcessor: - """ - Advanced knowledge processor that extracts meaningful information - from text sources and prepares it for Lyra's learning. - """ - - def __init__( - self, - device: Optional[torch.device] = None, - embedding_model: str = "all-MiniLM-L6-v2", - chunk_size: int = 512, - chunk_overlap: int = 50 - ): - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - self.chunk_size = chunk_size - self.chunk_overlap = chunk_overlap - - # NLP models - self.nlp = None # Will be loaded lazily - self.embedding_model = None - self.summarizer = None - self.classifier = None - - # Text processing patterns - self.sentence_splitter = re.compile(r'(?<=[.!?])\s+') - self.paragraph_splitter = re.compile(r'\n\s*\n') - - # Knowledge categories and their keywords - self.category_keywords = { - 'science': [ - 'research', 'experiment', 'theory', 'hypothesis', 'data', - 'analysis', 'method', 'scientific', 'study', 'physics', - 'chemistry', 'biology', 'mathematics', 'astronomy' - ], - 'history': [ - 'century', 'ancient', 'civilization', 'empire', 'war', - 'revolution', 'culture', 'society', 'historical', 'period', - 'medieval', 'renaissance', 'industrial', 'modern' - ], - 'philosophy': [ - 'ethics', 'morality', 'existence', 'reality', 'consciousness', - 'logic', 'reason', 'truth', 'knowledge', 'metaphysics', - 'epistemology', 'philosopher', 'philosophical', 'wisdom' - ], - 'literature': [ - 'character', 'plot', 'theme', 'narrative', 'poetry', - 'novel', 'story', 'drama', 'author', 'literary', - 'fiction', 'metaphor', 'symbolism', 'prose' - ], - 'art': [ - 'painting', 'sculpture', 'artist', 'creative', 'aesthetic', - 'beauty', 'design', 'color', 'form', 'style', - 'movement', 'gallery', 'museum', 'artistic' - ], - 'technology': [ - 'computer', 'software', 'programming', 'digital', 'internet', - 'algorithm', 'innovation', 'engineering', 'technical', - 'machine', 'automation', 'electronics', 'invention' - ] - } - - # Quality indicators - self.quality_indicators = { - 'positive': [ - 'evidence', 'research', 'study', 'analysis', 'peer-reviewed', - 'academic', 'scholarly', 'university', 'institute', 'journal' - ], - 'negative': [ - 'unverified', 'rumor', 'gossip', 'speculation', 'opinion', - 'conspiracy', 'myth', 'fake', 'false', 'misleading' - ] - } - - async def initialize(self): - """Initialize NLP models and resources.""" - logger.info("Initializing knowledge processor...") - - # Download required NLTK data - try: - nltk.download('punkt', quiet=True) - nltk.download('stopwords', quiet=True) - nltk.download('wordnet', quiet=True) - nltk.download('averaged_perceptron_tagger', quiet=True) - except Exception as e: - logger.warning(f"Failed to download some NLTK data: {e}") - - # Load spaCy model - try: - self.nlp = spacy.load("en_core_web_sm") - except OSError: - logger.warning("spaCy model not found, downloading...") - spacy.cli.download("en_core_web_sm") - self.nlp = spacy.load("en_core_web_sm") - - # Load embedding model - self.embedding_model = SentenceTransformer( - "sentence-transformers/all-MiniLM-L6-v2", - device=self.device - ) - - # Load summarization model - self.summarizer = pipeline( - "summarization", - model="facebook/bart-large-cnn", - device=0 if self.device.type == "cuda" else -1 - ) - - # Load text classification model - self.classifier = pipeline( - "zero-shot-classification", - model="facebook/bart-large-mnli", - device=0 if self.device.type == "cuda" else -1 - ) - - logger.info("Knowledge processor initialized successfully") - - async def process_text_file( - self, - file_path: Path, - title: Optional[str] = None, - source_metadata: Optional[Dict[str, Any]] = None - ) -> ProcessedKnowledge: - """ - Process a text file and extract structured knowledge. - - Args: - file_path: Path to the text file - title: Optional title (will be extracted if not provided) - source_metadata: Additional metadata about the source - - Returns: - ProcessedKnowledge object - """ - logger.info(f"Processing text file: {file_path}") - - # Read file content - try: - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - raw_content = f.read() - except Exception as e: - logger.error(f"Failed to read file {file_path}: {e}") - raise - - # Detect and clean text format - cleaned_content = await self._clean_text(raw_content) - - # Extract title if not provided - if not title: - title = await self._extract_title(cleaned_content, file_path.name) - - # Process the content - return await self._process_content( - title=title, - content=cleaned_content, - source_metadata=source_metadata or {} - ) - - async def process_web_content( - self, - html_content: str, - title: Optional[str] = None, - url: Optional[str] = None - ) -> ProcessedKnowledge: - """Process HTML content from web sources.""" - # Extract text from HTML - soup = BeautifulSoup(html_content, 'html.parser') - - # Remove unwanted elements - for element in soup(['script', 'style', 'nav', 'footer', 'aside']): - element.decompose() - - # Extract title - if not title: - title_elem = soup.find('title') - title = title_elem.get_text(strip=True) if title_elem else "Web Content" - - # Extract main content - main_content = soup.get_text(separator='\n', strip=True) - cleaned_content = await self._clean_text(main_content) - - source_metadata = {'source_type': 'web', 'url': url} - return await self._process_content(title, cleaned_content, source_metadata) - - async def _process_content( - self, - title: str, - content: str, - source_metadata: Dict[str, Any] - ) -> ProcessedKnowledge: - """Core content processing logic.""" - - # Analyze content structure - chunks = await self._chunk_text(content) - - # Extract concepts and keywords - concepts = await self._extract_concepts(content) - keywords = await self._extract_keywords(content) - - # Classify content - category, subcategory = await self._classify_content(content, title) - - # Calculate quality scores - quality_score = await self._calculate_quality_score(content, title) - complexity_score = await self._calculate_complexity_score(content) - - # Generate summary - summary = await self._generate_summary(content) - - # Generate embeddings - content_embedding = await self._generate_embedding(content) - - # Process chunks with embeddings - processed_chunks = [] - for chunk in chunks: - chunk_embedding = await self._generate_embedding(chunk.content) - chunk_dict = { - 'content': chunk.content, - 'start_pos': chunk.start_pos, - 'end_pos': chunk.end_pos, - 'chunk_type': chunk.chunk_type, - 'importance_score': chunk.importance_score, - 'concepts': chunk.concepts, - 'embedding': chunk_embedding.tolist() if chunk_embedding is not None else None - } - processed_chunks.append(chunk_dict) - - # Prepare metadata - metadata = { - **source_metadata, - 'processing_timestamp': str(asyncio.get_event_loop().time()), - 'word_count': len(content.split()), - 'sentence_count': len(self.sentence_splitter.split(content)), - 'paragraph_count': len(self.paragraph_splitter.split(content)), - 'readability_score': textstat.flesch_reading_ease(content), - 'language': 'en' # Could be detected - } - - return ProcessedKnowledge( - title=title, - content=content, - summary=summary, - category=category, - subcategory=subcategory, - keywords=keywords, - concepts=concepts, - quality_score=quality_score, - complexity_score=complexity_score, - embedding=content_embedding, - chunks=processed_chunks, - metadata=metadata - ) - - async def _clean_text(self, raw_content: str) -> str: - """Clean and normalize text content.""" - # Remove excessive whitespace - content = re.sub(r'\n\s*\n\s*\n', '\n\n', raw_content) - content = re.sub(r'[ \t]+', ' ', content) - - # Remove common Gutenberg headers/footers - content = re.sub( - r'\*\*\*\s*START OF .*?\*\*\*.*?\n', - '', - content, - flags=re.DOTALL | re.IGNORECASE - ) - content = re.sub( - r'\*\*\*\s*END OF .*?\*\*\*.*', - '', - content, - flags=re.DOTALL | re.IGNORECASE - ) - - # Remove page numbers and chapter markers that might interfere - content = re.sub(r'\n\s*\d+\s*\n', '\n', content) - content = re.sub(r'\n\s*Page \d+\s*\n', '\n', content, flags=re.IGNORECASE) - - # Normalize quotes and dashes - content = content.replace('"', '"').replace('"', '"') - content = content.replace(''', "'").replace(''', "'") - content = content.replace('โ', '--').replace('โ', '-') - - return content.strip() - - async def _extract_title(self, content: str, filename: str) -> str: - """Extract title from content or filename.""" - lines = content.split('\n')[:10] # Check first 10 lines - - # Look for title patterns - for line in lines: - line = line.strip() - if len(line) > 10 and len(line) < 100: - # Check if line looks like a title - if line.isupper() or line.istitle(): - return line - - # Extract from filename as fallback - title = filename.replace('_', ' ').replace('-', ' ') - title = re.sub(r'\.[^.]+$', '', title) # Remove extension - title = re.sub(r'^\d+_?', '', title) # Remove leading numbers - - return title.title() - - async def _chunk_text(self, content: str) -> List[TextChunk]: - """Split text into meaningful chunks.""" - chunks = [] - paragraphs = self.paragraph_splitter.split(content) - - current_pos = 0 - for paragraph in paragraphs: - if len(paragraph.strip()) < 50: # Skip very short paragraphs - current_pos += len(paragraph) + 2 # +2 for newlines - continue - - # Determine chunk type - chunk_type = self._determine_chunk_type(paragraph) - - # Calculate importance score - importance_score = await self._calculate_chunk_importance(paragraph) - - # Extract concepts from chunk - chunk_concepts = await self._extract_chunk_concepts(paragraph) - - chunk = TextChunk( - content=paragraph.strip(), - start_pos=current_pos, - end_pos=current_pos + len(paragraph), - chunk_type=chunk_type, - importance_score=importance_score, - concepts=chunk_concepts - ) - - chunks.append(chunk) - current_pos += len(paragraph) + 2 - - return chunks - - def _determine_chunk_type(self, paragraph: str) -> str: - """Determine the type of text chunk.""" - if len(paragraph) < 100: - return 'short_paragraph' - elif any(keyword in paragraph.lower() for keyword in ['chapter', 'section', 'part']): - return 'section_header' - elif paragraph.strip().endswith(':'): - return 'list_header' - else: - return 'paragraph' - - async def _calculate_chunk_importance(self, chunk: str) -> float: - """Calculate importance score for a text chunk.""" - score = 0.5 # Base score - - # Length factor (not too short, not too long) - length = len(chunk.split()) - if 50 <= length <= 200: - score += 0.1 - elif length < 20: - score -= 0.2 - - # Keyword density - important_words = [ - 'important', 'significant', 'crucial', 'essential', 'key', - 'fundamental', 'principle', 'concept', 'theory', 'discovery' - ] - keyword_count = sum(1 for word in important_words if word in chunk.lower()) - score += min(0.3, keyword_count * 0.1) - - # Question presence (often indicates important information) - question_count = chunk.count('?') - score += min(0.2, question_count * 0.05) - - # Technical terms (using simple heuristic) - doc = self.nlp(chunk[:1000]) # Limit for performance - technical_terms = [ - token for token in doc - if token.pos_ in ['NOUN', 'PROPN'] and len(token.text) > 6 - ] - score += min(0.2, len(technical_terms) * 0.01) - - return min(1.0, max(0.0, score)) - - async def _extract_concepts(self, content: str) -> List[str]: - """Extract key concepts from content.""" - doc = self.nlp(content[:5000]) # Limit for performance - - # Extract noun phrases as concepts - concepts = [] - for chunk in doc.noun_chunks: - if len(chunk.text) > 3 and len(chunk.text.split()) <= 3: - concepts.append(chunk.text.lower()) - - # Extract named entities - for ent in doc.ents: - if ent.label_ in ['PERSON', 'ORG', 'GPE', 'EVENT', 'WORK_OF_ART']: - concepts.append(ent.text.lower()) - - # Remove duplicates and return top concepts - concept_counts = Counter(concepts) - return [concept for concept, count in concept_counts.most_common(20)] - - async def _extract_chunk_concepts(self, chunk: str) -> List[str]: - """Extract concepts from a specific chunk.""" - doc = self.nlp(chunk[:1000]) # Limit for performance - - concepts = [] - for chunk_span in doc.noun_chunks: - if len(chunk_span.text) > 3: - concepts.append(chunk_span.text.lower()) - - for ent in doc.ents: - concepts.append(ent.text.lower()) - - return list(set(concepts))[:10] # Return unique concepts, limited - - async def _extract_keywords(self, content: str) -> List[str]: - """Extract keywords from content.""" - doc = self.nlp(content[:5000]) # Limit for performance - - # Extract meaningful words - keywords = [] - for token in doc: - if (token.pos_ in ['NOUN', 'ADJ', 'VERB'] and - not token.is_stop and - not token.is_punct and - len(token.text) > 3): - keywords.append(token.lemma_.lower()) - - # Count frequency and return top keywords - keyword_counts = Counter(keywords) - return [word for word, count in keyword_counts.most_common(15)] - - async def _classify_content(self, content: str, title: str) -> Tuple[str, Optional[str]]: - """Classify content into categories.""" - # Combine title and first part of content for classification - classification_text = f"{title}. {content[:1000]}" - - # Use keyword-based classification first (faster) - category_scores = {} - for category, keywords in self.category_keywords.items(): - score = sum(1 for keyword in keywords if keyword in classification_text.lower()) - category_scores[category] = score - - if category_scores and max(category_scores.values()) > 0: - category = max(category_scores, key=category_scores.get) - else: - # Fallback to ML classification - categories = list(self.category_keywords.keys()) - try: - result = self.classifier(classification_text, categories) - category = result['labels'][0] - except Exception as e: - logger.warning(f"Classification failed: {e}") - category = 'general' - - # Determine subcategory based on more specific analysis - subcategory = await self._determine_subcategory(content, category) - - return category, subcategory - - async def _determine_subcategory(self, content: str, category: str) -> Optional[str]: - """Determine subcategory based on content analysis.""" - subcategory_mapping = { - 'science': { - 'physics': ['physics', 'quantum', 'relativity', 'mechanics'], - 'biology': ['biology', 'evolution', 'genetics', 'species'], - 'chemistry': ['chemistry', 'chemical', 'molecule', 'reaction'], - 'astronomy': ['astronomy', 'space', 'universe', 'planet', 'star'] - }, - 'history': { - 'ancient': ['ancient', 'rome', 'greece', 'egypt', 'civilization'], - 'medieval': ['medieval', 'middle ages', 'feudal', 'knight'], - 'modern': ['modern', 'industrial', 'revolution', 'war', 'century'] - }, - 'literature': { - 'fiction': ['novel', 'story', 'character', 'plot'], - 'poetry': ['poem', 'verse', 'rhyme', 'stanza'], - 'drama': ['play', 'theater', 'act', 'scene'] - } - } - - if category in subcategory_mapping: - content_lower = content[:2000].lower() - subcategory_scores = {} - - for subcategory, keywords in subcategory_mapping[category].items(): - score = sum(1 for keyword in keywords if keyword in content_lower) - subcategory_scores[subcategory] = score - - if subcategory_scores and max(subcategory_scores.values()) > 0: - return max(subcategory_scores, key=subcategory_scores.get) - - return None - - async def _calculate_quality_score(self, content: str, title: str) -> float: - """Calculate quality score for content.""" - score = 0.5 # Base score - - # Content length (optimal range) - word_count = len(content.split()) - if 500 <= word_count <= 10000: - score += 0.1 - elif word_count < 100: - score -= 0.2 - - # Readability - try: - readability = textstat.flesch_reading_ease(content) - if 30 <= readability <= 70: # Reasonable complexity - score += 0.1 - except: - pass - - # Quality indicators - content_lower = content.lower() - positive_indicators = sum( - 1 for indicator in self.quality_indicators['positive'] - if indicator in content_lower - ) - negative_indicators = sum( - 1 for indicator in self.quality_indicators['negative'] - if indicator in content_lower - ) - - score += min(0.2, positive_indicators * 0.05) - score -= min(0.3, negative_indicators * 0.1) - - # Title quality - if len(title.split()) >= 3 and not title.isupper(): - score += 0.05 - - return min(1.0, max(0.0, score)) - - async def _calculate_complexity_score(self, content: str) -> float: - """Calculate complexity score for content.""" - try: - # Use various readability metrics - flesch_score = textstat.flesch_reading_ease(content) - flesch_kincaid = textstat.flesch_kincaid_grade(content) - - # Normalize to 0-1 scale - complexity = 1.0 - (flesch_score / 100.0) - complexity = max(0.0, min(1.0, complexity)) - - return complexity - except: - return 0.5 # Default complexity - - async def _generate_summary(self, content: str) -> str: - """Generate summary of content.""" - try: - # Limit content length for summarization - max_length = 1024 - if len(content) > max_length: - # Take first part of content - content_to_summarize = content[:max_length] - else: - content_to_summarize = content - - # Generate summary - summary_result = self.summarizer( - content_to_summarize, - max_length=150, - min_length=50, - do_sample=False - ) - - return summary_result[0]['summary_text'] - - except Exception as e: - logger.warning(f"Summarization failed: {e}") - # Fallback: return first few sentences - sentences = self.sentence_splitter.split(content)[:3] - return ' '.join(sentences) - - async def _generate_embedding(self, text: str) -> Optional[np.ndarray]: - """Generate embedding for text.""" - try: - # Limit text length - if len(text) > 500: - text = text[:500] - - embedding = self.embedding_model.encode(text, convert_to_numpy=True) - return embedding - - except Exception as e: - logger.warning(f"Embedding generation failed: {e}") - return None - - def get_processing_statistics(self) -> Dict[str, Any]: - """Get statistics about processed knowledge.""" - return { - 'models_loaded': { - 'nlp': self.nlp is not None, - 'embedding_model': self.embedding_model is not None, - 'summarizer': self.summarizer is not None, - 'classifier': self.classifier is not None - }, - 'chunk_size': self.chunk_size, - 'chunk_overlap': self.chunk_overlap, - 'supported_categories': list(self.category_keywords.keys()), - 'device': str(self.device) - } \ No newline at end of file diff --git a/lyra/main.py b/lyra/main.py deleted file mode 100644 index 39cd796..0000000 --- a/lyra/main.py +++ /dev/null @@ -1,237 +0,0 @@ -""" -Main entry point for Lyra AI Discord Chatbot. - -This module initializes and runs the complete Lyra system including -personality matrix, emotional intelligence, knowledge processing, -and Discord bot integration. -""" - -import asyncio -import logging -import sys -from pathlib import Path -from typing import Optional -import signal - -from lyra.config import config -from lyra.database.manager import DatabaseManager -from lyra.personality.matrix import PersonalityMatrix -from lyra.emotions.system import EmotionalSystem -from lyra.core.self_evolution import SelfEvolutionEngine -from lyra.core.thinking_agent import ThinkingAgent -from lyra.knowledge.acquisition_manager import KnowledgeAcquisitionManager - - -# Configure logging -logging.basicConfig( - level=getattr(logging, config.log_level.upper()), - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(config.log_file), - logging.StreamHandler(sys.stdout) - ] -) - -logger = logging.getLogger(__name__) - - -class LyraSystem: - """ - Main Lyra AI system coordinator. - - Manages initialization, coordination, and shutdown of all Lyra components. - """ - - def __init__(self): - self.database_manager: Optional[DatabaseManager] = None - self.personality_matrix: Optional[PersonalityMatrix] = None - self.emotional_system: Optional[EmotionalSystem] = None - self.evolution_engine: Optional[SelfEvolutionEngine] = None - self.thinking_agent: Optional[ThinkingAgent] = None - self.knowledge_manager: Optional[KnowledgeAcquisitionManager] = None - self.discord_bot: Optional[object] = None # Will be implemented later - - self.is_running = False - self.shutdown_event = asyncio.Event() - - async def initialize(self): - """Initialize all Lyra components.""" - logger.info("Initializing Lyra AI System...") - - try: - # Ensure required directories exist - config.ensure_directories() - - # Initialize database - logger.info("Initializing database manager...") - self.database_manager = DatabaseManager( - database_url=config.database_url, - redis_url=config.redis_url - ) - await self.database_manager.initialize() - - # Initialize core AI components - logger.info("Initializing personality matrix...") - self.personality_matrix = PersonalityMatrix( - enable_self_modification=True - ) - - logger.info("Initializing emotional system...") - self.emotional_system = EmotionalSystem( - input_dim=config.hidden_size, - emotion_dim=19, - memory_capacity=1000 - ) - - logger.info("Initializing self-evolution engine...") - self.evolution_engine = SelfEvolutionEngine( - model_dim=config.hidden_size, - evolution_rate=0.001, - adaptation_threshold=0.7 - ) - - logger.info("Initializing thinking agent...") - self.thinking_agent = ThinkingAgent( - model_dim=config.hidden_size, - thought_types=8, - max_thought_depth=5 - ) - - # Initialize knowledge acquisition - logger.info("Initializing knowledge acquisition manager...") - # Will be implemented when we add the knowledge acquisition manager - - # Load any saved states - await self._load_saved_states() - - logger.info("Lyra AI System initialized successfully!") - - except Exception as e: - logger.error(f"Failed to initialize Lyra system: {e}") - raise - - async def _load_saved_states(self): - """Load saved personality and emotional states.""" - try: - # Load personality state - personality_path = config.data_dir / "personality" / "current_state.json" - if personality_path.exists(): - self.personality_matrix.load_personality(personality_path) - logger.info("Loaded saved personality state") - - # Load emotional state - emotional_path = config.data_dir / "personality" / "emotional_state.json" - if emotional_path.exists(): - self.emotional_system.load_emotional_state(emotional_path) - logger.info("Loaded saved emotional state") - - # Load evolution state - evolution_path = config.data_dir / "personality" / "evolution_state.json" - if evolution_path.exists(): - self.evolution_engine.load_evolution_state(evolution_path) - logger.info("Loaded saved evolution state") - - except Exception as e: - logger.warning(f"Failed to load some saved states: {e}") - - async def start(self): - """Start the Lyra system.""" - logger.info("Starting Lyra AI System...") - - self.is_running = True - - try: - # Start Discord bot (when implemented) - # await self.discord_bot.start() - - # For now, just run a placeholder - logger.info("Lyra is ready! (Discord bot integration pending)") - - # Wait for shutdown signal - await self.shutdown_event.wait() - - except Exception as e: - logger.error(f"Error running Lyra system: {e}") - raise - finally: - await self.shutdown() - - async def shutdown(self): - """Shutdown the Lyra system gracefully.""" - if not self.is_running: - return - - logger.info("Shutting down Lyra AI System...") - - try: - # Save current states - await self._save_current_states() - - # Shutdown components - if self.database_manager: - await self.database_manager.close() - logger.info("Database manager closed") - - # Additional cleanup would go here - - self.is_running = False - logger.info("Lyra AI System shutdown complete") - - except Exception as e: - logger.error(f"Error during shutdown: {e}") - - async def _save_current_states(self): - """Save current personality and emotional states.""" - try: - # Ensure personality directory exists - personality_dir = config.data_dir / "personality" - personality_dir.mkdir(parents=True, exist_ok=True) - - # Save personality state - if self.personality_matrix: - personality_path = personality_dir / "current_state.json" - self.personality_matrix.save_personality(personality_path) - logger.info("Saved personality state") - - # Save emotional state - if self.emotional_system: - emotional_path = personality_dir / "emotional_state.json" - self.emotional_system.save_emotional_state(emotional_path) - logger.info("Saved emotional state") - - # Save evolution state - if self.evolution_engine: - evolution_path = personality_dir / "evolution_state.json" - self.evolution_engine.save_evolution_state(evolution_path) - logger.info("Saved evolution state") - - except Exception as e: - logger.error(f"Failed to save states: {e}") - - def signal_handler(self, signum, frame): - """Handle shutdown signals.""" - logger.info(f"Received signal {signum}, initiating graceful shutdown...") - self.shutdown_event.set() - - -async def main(): - """Main entry point for Lyra.""" - lyra_system = LyraSystem() - - # Set up signal handlers for graceful shutdown - for sig in [signal.SIGINT, signal.SIGTERM]: - signal.signal(sig, lyra_system.signal_handler) - - try: - await lyra_system.initialize() - await lyra_system.start() - except KeyboardInterrupt: - logger.info("Received keyboard interrupt") - except Exception as e: - logger.error(f"Unhandled exception: {e}") - sys.exit(1) - - -if __name__ == "__main__": - # Run Lyra - asyncio.run(main()) \ No newline at end of file diff --git a/lyra/personality/__init__.py b/lyra/personality/__init__.py deleted file mode 100644 index d660c62..0000000 --- a/lyra/personality/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Lyra Personality Module - -Implements the sophisticated personality matrix system that allows Lyra to develop -and adapt her personality traits like a real person. -""" - -from .matrix import PersonalityMatrix, PersonalityTrait -from .traits import OCEANTraits, MyersBriggsType, PersonalityEvolution -from .adaptation import PersonalityAdapter - -__all__ = [ - "PersonalityMatrix", - "PersonalityTrait", - "OCEANTraits", - "MyersBriggsType", - "PersonalityEvolution", - "PersonalityAdapter" -] \ No newline at end of file diff --git a/lyra/personality/adaptation.py b/lyra/personality/adaptation.py deleted file mode 100644 index b082352..0000000 --- a/lyra/personality/adaptation.py +++ /dev/null @@ -1,519 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -import logging -from datetime import datetime, timedelta - -from .matrix import PersonalityMatrix, PersonalityTrait -from .traits import OCEANTraits - -logger = logging.getLogger(__name__) - -class PersonalityAdapter(nn.Module): - """ - Advanced personality adaptation system that helps Lyra adapt her personality - in real-time based on conversation context, user preferences, and social dynamics. - """ - - def __init__( - self, - personality_matrix: PersonalityMatrix, - adaptation_strength: float = 0.3, - memory_length: int = 50 - ): - super().__init__() - - self.personality_matrix = personality_matrix - self.adaptation_strength = adaptation_strength - self.memory_length = memory_length - - # Adaptation networks - self.context_analyzer = nn.Sequential( - nn.Linear(512, 256), # Context embedding input - nn.LayerNorm(256), - nn.ReLU(), - nn.Dropout(0.1), - nn.Linear(256, 128), - nn.ReLU(), - nn.Linear(128, 64) - ) - - self.user_preference_network = nn.Sequential( - nn.Linear(64 + 15, 128), # Context + personality features - nn.LayerNorm(128), - nn.ReLU(), - nn.Linear(128, 64), - nn.ReLU(), - nn.Linear(64, 15), # Output personality adjustments - nn.Tanh() - ) - - # Social dynamics understanding - self.social_dynamics_analyzer = nn.Sequential( - nn.Linear(32, 64), # Social context features - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, 10), # Social adjustment factors - nn.Sigmoid() - ) - - # Conversation memory for learning user preferences - self.conversation_memory = [] - self.user_preference_cache = {} - - # Adaptation history for analysis - self.adaptation_history = [] - - def forward( - self, - context_embedding: torch.Tensor, - user_id: Optional[str] = None, - social_context: Optional[Dict[str, Any]] = None, - conversation_history: Optional[List[str]] = None - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Adapt personality for current context and user. - - Args: - context_embedding: Current conversation context - user_id: ID of current user - social_context: Social context information - conversation_history: Recent conversation for preference learning - - Returns: - adapted_personality_weights: Personality adjustments for response - adaptation_info: Information about adaptations made - """ - batch_size = context_embedding.shape[0] - device = context_embedding.device - - # Analyze context - context_features = self.context_analyzer(context_embedding.mean(dim=1)) - - # Get base personality - base_personality = self._get_base_personality_features().to(device) - base_personality = base_personality.unsqueeze(0).repeat(batch_size, 1) - - # User-specific adaptations - user_adaptations = torch.zeros_like(base_personality) - if user_id: - user_adaptations = self._get_user_adaptations( - user_id, context_features, conversation_history - ) - - # Social context adaptations - social_adaptations = torch.zeros(batch_size, 10, device=device) - if social_context: - social_features = self._extract_social_features(social_context, device) - social_adaptations = self.social_dynamics_analyzer(social_features) - - # Combine base personality with context and user preferences - combined_input = torch.cat([context_features, base_personality], dim=1) - personality_adjustments = self.user_preference_network(combined_input) - - # Apply social adaptations - social_influence = social_adaptations.mean(dim=1, keepdim=True) - personality_adjustments = personality_adjustments * (0.7 + 0.6 * social_influence) - - # Apply user-specific adaptations - final_adjustments = ( - self.adaptation_strength * personality_adjustments + - 0.3 * user_adaptations - ) - - # Ensure reasonable adaptation bounds - final_adjustments = torch.clamp(final_adjustments, -0.5, 0.5) - - # Store adaptation for learning - adaptation_info = self._record_adaptation( - user_id, final_adjustments, context_features, social_context - ) - - return final_adjustments, adaptation_info - - def _get_base_personality_features(self) -> torch.Tensor: - """Get current base personality as feature vector.""" - ocean = self.personality_matrix.ocean_traits.to_tensor() - - custom_traits = torch.tensor([ - trait.value for trait in self.personality_matrix.custom_traits.values() - ], dtype=torch.float32) - - return torch.cat([ocean, custom_traits]) - - def _get_user_adaptations( - self, - user_id: str, - context_features: torch.Tensor, - conversation_history: Optional[List[str]] - ) -> torch.Tensor: - """Get personality adaptations specific to this user.""" - device = context_features.device - batch_size = context_features.shape[0] - - # Initialize with zero adaptations - adaptations = torch.zeros(batch_size, 15, device=device) - - # Check if we have learned preferences for this user - if user_id in self.user_preference_cache: - user_prefs = self.user_preference_cache[user_id] - - # Apply learned preferences - for trait_idx, adjustment in enumerate(user_prefs.get('trait_preferences', [])): - if trait_idx < 15: - adaptations[:, trait_idx] = adjustment - - # Learn from conversation history if available - if conversation_history: - learned_adaptations = self._learn_from_conversation( - user_id, conversation_history, context_features - ) - adaptations = 0.7 * adaptations + 0.3 * learned_adaptations - - return adaptations - - def _extract_social_features( - self, - social_context: Dict[str, Any], - device: torch.device - ) -> torch.Tensor: - """Extract features from social context.""" - features = torch.zeros(1, 32, device=device) - - # Group conversation indicators - if social_context.get('is_group_conversation', False): - features[0, 0] = 1.0 - features[0, 1] = social_context.get('group_size', 0) / 20.0 # Normalize - - # Formality level - formality = social_context.get('formality_level', 0.5) - features[0, 2] = formality - - # Emotional tone of conversation - emotional_tone = social_context.get('emotional_tone', {}) - for i, emotion in enumerate(['positive', 'negative', 'neutral', 'excited', 'calm']): - if i < 5: - features[0, 3 + i] = emotional_tone.get(emotion, 0.0) - - # Topic category - topic = social_context.get('topic_category', 'general') - topic_mapping = { - 'technical': 8, 'casual': 9, 'emotional': 10, 'creative': 11, - 'professional': 12, 'academic': 13, 'social': 14, 'personal': 15 - } - if topic in topic_mapping: - features[0, topic_mapping[topic]] = 1.0 - - # Conflict or disagreement present - if social_context.get('has_conflict', False): - features[0, 16] = 1.0 - - # User's apparent expertise level - expertise = social_context.get('user_expertise_level', 0.5) - features[0, 17] = expertise - - # Time pressure - time_pressure = social_context.get('time_pressure', 0.0) - features[0, 18] = time_pressure - - # Cultural context features - cultural_context = social_context.get('cultural_context', {}) - features[0, 19] = cultural_context.get('directness_preference', 0.5) - features[0, 20] = cultural_context.get('hierarchy_awareness', 0.5) - - return features - - def _learn_from_conversation( - self, - user_id: str, - conversation_history: List[str], - context_features: torch.Tensor - ) -> torch.Tensor: - """Learn user preferences from conversation patterns.""" - device = context_features.device - batch_size = context_features.shape[0] - - adaptations = torch.zeros(batch_size, 15, device=device) - - if len(conversation_history) < 3: - return adaptations - - # Analyze conversation patterns - conversation_analysis = self._analyze_conversation_patterns(conversation_history) - - # Update user preference cache - if user_id not in self.user_preference_cache: - self.user_preference_cache[user_id] = { - 'trait_preferences': [0.0] * 15, - 'conversation_count': 0, - 'satisfaction_history': [], - 'adaptation_success': {} - } - - user_cache = self.user_preference_cache[user_id] - - # Learn trait preferences based on conversation success - if conversation_analysis['engagement_level'] > 0.7: - # High engagement - strengthen current personality settings - current_traits = self._get_base_personality_features() - for i in range(min(15, len(current_traits))): - learning_rate = 0.05 - user_cache['trait_preferences'][i] = ( - 0.95 * user_cache['trait_preferences'][i] + - learning_rate * (current_traits[i].item() - 0.5) - ) - - elif conversation_analysis['engagement_level'] < 0.3: - # Low engagement - try different personality approach - for i in range(15): - # Slightly push toward opposite direction - current_adjustment = user_cache['trait_preferences'][i] - user_cache['trait_preferences'][i] = current_adjustment * 0.9 - - # Apply learned preferences to adaptations - for i, pref in enumerate(user_cache['trait_preferences']): - adaptations[:, i] = pref - - user_cache['conversation_count'] += 1 - - return adaptations - - def _analyze_conversation_patterns(self, conversation_history: List[str]) -> Dict[str, float]: - """Analyze conversation patterns to infer user preferences and engagement.""" - if not conversation_history: - return {'engagement_level': 0.5} - - # Simple heuristic analysis (in a real system, this would be more sophisticated) - total_length = sum(len(msg.split()) for msg in conversation_history) - avg_length = total_length / len(conversation_history) - - # Question frequency (indicates engagement) - question_count = sum(1 for msg in conversation_history if '?' in msg) - question_ratio = question_count / len(conversation_history) - - # Emotional indicators - positive_words = ['good', 'great', 'awesome', 'love', 'excellent', 'amazing', 'perfect'] - negative_words = ['bad', 'terrible', 'hate', 'awful', 'worst', 'horrible'] - - positive_count = sum( - sum(1 for word in positive_words if word in msg.lower()) - for msg in conversation_history - ) - negative_count = sum( - sum(1 for word in negative_words if word in msg.lower()) - for msg in conversation_history - ) - - # Calculate engagement level - engagement_score = 0.5 # Base engagement - - # Longer messages indicate engagement - if avg_length > 10: - engagement_score += 0.2 - elif avg_length < 3: - engagement_score -= 0.2 - - # Questions indicate engagement - engagement_score += question_ratio * 0.3 - - # Emotional valence - if positive_count > negative_count: - engagement_score += 0.2 - elif negative_count > positive_count: - engagement_score -= 0.2 - - engagement_score = np.clip(engagement_score, 0.0, 1.0) - - return { - 'engagement_level': engagement_score, - 'avg_message_length': avg_length, - 'question_ratio': question_ratio, - 'emotional_valence': (positive_count - negative_count) / max(1, len(conversation_history)) - } - - def _record_adaptation( - self, - user_id: Optional[str], - adaptations: torch.Tensor, - context_features: torch.Tensor, - social_context: Optional[Dict[str, Any]] - ) -> Dict[str, Any]: - """Record adaptation for analysis and learning.""" - adaptation_record = { - 'timestamp': datetime.now().isoformat(), - 'user_id': user_id, - 'adaptations': adaptations[0].detach().cpu().numpy().tolist(), - 'context_strength': torch.norm(context_features).item(), - 'social_context_type': social_context.get('topic_category', 'general') if social_context else 'general' - } - - self.adaptation_history.append(adaptation_record) - - # Keep history manageable - if len(self.adaptation_history) > 1000: - self.adaptation_history = self.adaptation_history[-500:] - - # Prepare return info - adaptation_info = { - 'adaptation_magnitude': torch.norm(adaptations).item(), - 'primary_adaptations': self._identify_primary_adaptations(adaptations[0]), - 'user_specific': user_id is not None, - 'social_context_present': social_context is not None - } - - return adaptation_info - - def _identify_primary_adaptations(self, adaptations: torch.Tensor) -> Dict[str, float]: - """Identify the main personality adaptations being made.""" - trait_names = [ - 'openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism', - 'humor_level', 'sarcasm_tendency', 'empathy_level', 'curiosity', 'playfulness', - 'intellectualism', 'spontaneity', 'supportiveness', 'assertiveness', 'creativity' - ] - - # Find adaptations with magnitude > 0.1 - significant_adaptations = {} - for i, adaptation in enumerate(adaptations): - if abs(adaptation.item()) > 0.1 and i < len(trait_names): - significant_adaptations[trait_names[i]] = adaptation.item() - - return significant_adaptations - - def learn_from_feedback( - self, - user_id: str, - feedback_score: float, - conversation_context: str, - adaptations_made: torch.Tensor - ): - """ - Learn from user feedback about personality adaptations. - - This helps Lyra understand which personality adaptations work well - with different users and contexts. - """ - if user_id not in self.user_preference_cache: - return - - user_cache = self.user_preference_cache[user_id] - - # Record satisfaction - user_cache['satisfaction_history'].append({ - 'feedback_score': feedback_score, - 'adaptations': adaptations_made.detach().cpu().numpy().tolist(), - 'context': conversation_context, - 'timestamp': datetime.now().isoformat() - }) - - # Keep only recent history - if len(user_cache['satisfaction_history']) > 50: - user_cache['satisfaction_history'] = user_cache['satisfaction_history'][-25:] - - # Update adaptation success tracking - adaptation_key = self._hash_adaptations(adaptations_made) - if adaptation_key not in user_cache['adaptation_success']: - user_cache['adaptation_success'][adaptation_key] = { - 'success_count': 0, - 'total_count': 0, - 'avg_feedback': 0.0 - } - - success_data = user_cache['adaptation_success'][adaptation_key] - success_data['total_count'] += 1 - success_data['avg_feedback'] = ( - (success_data['avg_feedback'] * (success_data['total_count'] - 1) + feedback_score) / - success_data['total_count'] - ) - - if feedback_score > 0.6: - success_data['success_count'] += 1 - - logger.info(f"Updated adaptation learning for user {user_id}: " - f"feedback={feedback_score:.2f}, adaptations={adaptation_key}") - - def _hash_adaptations(self, adaptations: torch.Tensor) -> str: - """Create a hash key for adaptation patterns.""" - # Quantize adaptations to reduce sensitivity - quantized = torch.round(adaptations * 10) / 10 - return str(quantized.detach().cpu().numpy().tolist()) - - def get_adaptation_analytics(self) -> Dict[str, Any]: - """Get analytics about personality adaptations.""" - if not self.adaptation_history: - return {'status': 'no_data'} - - recent_adaptations = [ - a for a in self.adaptation_history - if datetime.fromisoformat(a['timestamp']) > datetime.now() - timedelta(hours=24) - ] - - analytics = { - 'total_adaptations': len(self.adaptation_history), - 'recent_adaptations': len(recent_adaptations), - 'unique_users': len(set( - a['user_id'] for a in self.adaptation_history - if a['user_id'] is not None - )), - 'avg_adaptation_magnitude': np.mean([ - np.linalg.norm(a['adaptations']) for a in recent_adaptations - ]) if recent_adaptations else 0.0, - 'most_adapted_traits': self._get_most_adapted_traits(), - 'user_preference_learning': { - user_id: { - 'conversation_count': data['conversation_count'], - 'adaptation_success_rate': len([ - s for s in data['adaptation_success'].values() - if s['success_count'] / max(1, s['total_count']) > 0.6 - ]) / max(1, len(data['adaptation_success'])) - } - for user_id, data in self.user_preference_cache.items() - } - } - - return analytics - - def _get_most_adapted_traits(self) -> Dict[str, float]: - """Get traits that are adapted most frequently.""" - trait_names = [ - 'openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism', - 'humor_level', 'sarcasm_tendency', 'empathy_level', 'curiosity', 'playfulness', - 'intellectualism', 'spontaneity', 'supportiveness', 'assertiveness', 'creativity' - ] - - trait_adaptations = {name: [] for name in trait_names} - - for adaptation_record in self.adaptation_history: - for i, adaptation in enumerate(adaptation_record['adaptations']): - if i < len(trait_names): - trait_adaptations[trait_names[i]].append(abs(adaptation)) - - return { - name: np.mean(adaptations) if adaptations else 0.0 - for name, adaptations in trait_adaptations.items() - } - - def reset_user_adaptations(self, user_id: str): - """Reset learned adaptations for a specific user.""" - if user_id in self.user_preference_cache: - del self.user_preference_cache[user_id] - logger.info(f"Reset personality adaptations for user {user_id}") - - def export_personality_insights(self) -> Dict[str, Any]: - """Export insights about personality adaptation patterns.""" - return { - 'adaptation_history': self.adaptation_history[-100:], # Recent history - 'user_preferences': { - user_id: { - 'trait_preferences': data['trait_preferences'], - 'conversation_count': data['conversation_count'], - 'avg_satisfaction': np.mean([ - s['feedback_score'] for s in data['satisfaction_history'] - ]) if data['satisfaction_history'] else 0.0 - } - for user_id, data in self.user_preference_cache.items() - }, - 'analytics': self.get_adaptation_analytics() - } \ No newline at end of file diff --git a/lyra/personality/matrix.py b/lyra/personality/matrix.py deleted file mode 100644 index 1b9e4c4..0000000 --- a/lyra/personality/matrix.py +++ /dev/null @@ -1,699 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -from dataclasses import dataclass, field -import json -import logging -from pathlib import Path -import asyncio -from datetime import datetime, timedelta - -from .traits import OCEANTraits, MyersBriggsType, PersonalityEvolution, PersonalityDynamics -from .traits import MyersBriggsAnalyzer, PersonalityProfiler - -logger = logging.getLogger(__name__) - -@dataclass -class PersonalityTrait: - """Individual personality trait with evolution tracking.""" - name: str - value: float - variance: float = 0.1 - adaptation_rate: float = 0.01 - change_history: List[Tuple[float, str]] = field(default_factory=list) # (timestamp, reason) - stability: float = 0.8 - last_update: Optional[datetime] = None - - def evolve(self, influence: float, reason: str = "interaction"): - """Evolve the trait value based on influence.""" - # Calculate change with stability consideration - max_change = self.variance * (1 - self.stability) - change = np.clip(influence * self.adaptation_rate, -max_change, max_change) - - # Apply change - old_value = self.value - self.value = np.clip(self.value + change, 0.0, 1.0) - - # Record change - timestamp = datetime.now().timestamp() - self.change_history.append((timestamp, reason)) - - # Keep only recent history - cutoff = datetime.now() - timedelta(days=7) - self.change_history = [ - (ts, r) for ts, r in self.change_history - if datetime.fromtimestamp(ts) > cutoff - ] - - self.last_update = datetime.now() - - logger.debug(f"Trait {self.name} evolved: {old_value:.3f} -> {self.value:.3f} ({reason})") - -class PersonalityMatrix(nn.Module): - """ - Advanced personality matrix that allows Lyra to develop and modify her own personality. - - This system integrates OCEAN traits, Myers-Briggs types, and custom personality - dimensions that can evolve based on interactions and experiences. - """ - - def __init__( - self, - device: Optional[torch.device] = None, - enable_self_modification: bool = True - ): - super().__init__() - - self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") - self.enable_self_modification = enable_self_modification - - # Core personality traits - self.ocean_traits = OCEANTraits() - self.mb_type = MyersBriggsType.ENFP # Default, will be determined dynamically - self.evolution = PersonalityEvolution() - - # Additional personality dimensions - self.custom_traits = { - 'humor_level': PersonalityTrait('humor_level', 0.7, 0.2, 0.02), - 'sarcasm_tendency': PersonalityTrait('sarcasm_tendency', 0.3, 0.15, 0.01), - 'empathy_level': PersonalityTrait('empathy_level', 0.8, 0.1, 0.015), - 'curiosity': PersonalityTrait('curiosity', 0.9, 0.15, 0.02), - 'playfulness': PersonalityTrait('playfulness', 0.6, 0.2, 0.02), - 'intellectualism': PersonalityTrait('intellectualism', 0.7, 0.1, 0.01), - 'spontaneity': PersonalityTrait('spontaneity', 0.5, 0.25, 0.03), - 'supportiveness': PersonalityTrait('supportiveness', 0.85, 0.1, 0.015), - 'assertiveness': PersonalityTrait('assertiveness', 0.6, 0.2, 0.02), - 'creativity': PersonalityTrait('creativity', 0.8, 0.15, 0.02) - } - - # Neural components for personality dynamics - self.personality_dynamics = PersonalityDynamics( - input_dim=15, # Context features - personality_dim=5, # OCEAN - hidden_dim=128, - adaptation_rate=0.005 - ) - - # Personality analyzers - self.mb_analyzer = MyersBriggsAnalyzer() - self.profiler = PersonalityProfiler() - - # Self-modification network - allows Lyra to consciously change herself - if enable_self_modification: - self.self_modification_network = nn.Sequential( - nn.Linear(20, 64), # Current state + desired changes - nn.LayerNorm(64), - nn.ReLU(), - nn.Linear(64, 32), - nn.ReLU(), - nn.Linear(32, 15), # Output modifications for all traits - nn.Tanh() # Bounded modifications - ) - - # Relationship memory - how personality changes with different people - self.relationship_dynamics = {} - - # Meta-personality awareness - Lyra's understanding of her own personality - self.self_awareness = { - 'personality_insight': 0.5, - 'change_awareness': 0.5, - 'trait_understanding': 0.5 - } - - self.to(self.device) - - def forward( - self, - context_embedding: torch.Tensor, - emotional_state: torch.Tensor, - user_id: Optional[str] = None, - conscious_modification: Optional[Dict[str, float]] = None - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Generate personality-influenced response weighting. - - Args: - context_embedding: Current conversation context - emotional_state: Current emotional state - user_id: ID of user being talked to (for relationship dynamics) - conscious_modification: Explicit personality changes Lyra wants to make - - Returns: - personality_weights: Weights to influence response generation - personality_info: Information about current personality state - """ - batch_size = context_embedding.shape[0] - - # Get current OCEAN traits as tensor - current_ocean = self.ocean_traits.to_tensor(self.device).unsqueeze(0).repeat(batch_size, 1) - - # Create context features - context_features = self._create_context_features( - context_embedding, emotional_state, user_id - ) - - # Evolve personality based on context - evolved_ocean, evolution_info = self.personality_dynamics( - current_personality=current_ocean, - context_features=context_features, - feedback_signal=None # Will be provided after interaction - ) - - # Apply conscious modifications if Lyra decides to change herself - if conscious_modification and self.enable_self_modification: - modification_input = self._prepare_modification_input( - evolved_ocean, conscious_modification - ) - trait_modifications = self.self_modification_network(modification_input) - evolved_ocean = evolved_ocean + 0.1 * trait_modifications[:, :5] # Apply to OCEAN - evolved_ocean = torch.clamp(evolved_ocean, 0.0, 1.0) - - # Update actual personality traits (if training or evolving) - if self.training: - self._update_ocean_traits(evolved_ocean[0]) - - # Generate personality-influenced weights - personality_weights = self._generate_response_weights(evolved_ocean, context_features) - - # Prepare personality info - personality_info = { - 'current_ocean': self.ocean_traits.to_dict(), - 'myers_briggs': self.mb_type.value, - 'custom_traits': {name: trait.value for name, trait in self.custom_traits.items()}, - 'evolution_info': evolution_info, - 'self_awareness': self.self_awareness.copy(), - 'relationship_context': user_id if user_id in self.relationship_dynamics else None - } - - return personality_weights, personality_info - - def _create_context_features( - self, - context_embedding: torch.Tensor, - emotional_state: torch.Tensor, - user_id: Optional[str] - ) -> torch.Tensor: - """Create context features for personality dynamics.""" - batch_size = context_embedding.shape[0] - - # Base features from context and emotion - context_summary = context_embedding.mean(dim=1) # [batch, embed_dim] - emotion_summary = emotional_state.mean(dim=1) if emotional_state.dim() > 1 else emotional_state - - # Relationship context - relationship_features = torch.zeros(batch_size, 3, device=self.device) - if user_id and user_id in self.relationship_dynamics: - rel_data = self.relationship_dynamics[user_id] - relationship_features[:, 0] = rel_data.get('familiarity', 0.0) - relationship_features[:, 1] = rel_data.get('positive_interactions', 0.0) - relationship_features[:, 2] = rel_data.get('conflict_level', 0.0) - - # Time-based features (time of day, conversation length, etc.) - time_features = torch.zeros(batch_size, 2, device=self.device) - # These would be filled with actual time/context data - - # Combine all features - features = torch.cat([ - context_summary[:, :5], # First 5 dims of context - emotion_summary[:, :5], # First 5 dims of emotion - relationship_features, # 3 dims - time_features # 2 dims - ], dim=1) # Total: 15 dims - - return features - - def _prepare_modification_input( - self, - current_ocean: torch.Tensor, - conscious_modification: Dict[str, float] - ) -> torch.Tensor: - """Prepare input for self-modification network.""" - batch_size = current_ocean.shape[0] - - # Convert modifications to tensor - modifications = torch.zeros(batch_size, 15, device=self.device) - - # Map OCEAN trait modifications - ocean_mapping = { - 'openness': 0, 'conscientiousness': 1, 'extraversion': 2, - 'agreeableness': 3, 'neuroticism': 4 - } - - for trait, value in conscious_modification.items(): - if trait in ocean_mapping: - modifications[:, ocean_mapping[trait]] = value - elif trait in self.custom_traits: - # Map custom traits to remaining indices - custom_idx = 5 + list(self.custom_traits.keys()).index(trait) - if custom_idx < 15: - modifications[:, custom_idx] = value - - # Combine current state with desired modifications - combined_input = torch.cat([current_ocean, modifications], dim=1) - return combined_input - - def _generate_response_weights( - self, - personality_traits: torch.Tensor, - context_features: torch.Tensor - ) -> torch.Tensor: - """Generate weights that influence response generation based on personality.""" - batch_size = personality_traits.shape[0] - - # Extract OCEAN traits - openness = personality_traits[:, 0] - conscientiousness = personality_traits[:, 1] - extraversion = personality_traits[:, 2] - agreeableness = personality_traits[:, 3] - neuroticism = personality_traits[:, 4] - - # Generate response weights for different aspects - weights = torch.zeros(batch_size, 10, device=self.device) - - # Creativity weight (influenced by openness) - weights[:, 0] = openness * 0.8 + self.custom_traits['creativity'].value * 0.2 - - # Formality weight (influenced by conscientiousness) - weights[:, 1] = conscientiousness * 0.7 + (1 - self.custom_traits['playfulness'].value) * 0.3 - - # Social engagement weight (influenced by extraversion) - weights[:, 2] = extraversion * 0.6 + self.custom_traits['supportiveness'].value * 0.4 - - # Empathy weight (influenced by agreeableness) - weights[:, 3] = agreeableness * 0.5 + self.custom_traits['empathy_level'].value * 0.5 - - # Emotional expression weight (influenced by neuroticism and custom traits) - weights[:, 4] = neuroticism * 0.4 + self.custom_traits['spontaneity'].value * 0.6 - - # Humor weight - weights[:, 5] = self.custom_traits['humor_level'].value - - # Intellectual depth weight - weights[:, 6] = openness * 0.4 + self.custom_traits['intellectualism'].value * 0.6 - - # Assertiveness weight - weights[:, 7] = (1 - agreeableness) * 0.3 + self.custom_traits['assertiveness'].value * 0.7 - - # Curiosity weight - weights[:, 8] = openness * 0.3 + self.custom_traits['curiosity'].value * 0.7 - - # Sarcasm weight - weights[:, 9] = self.custom_traits['sarcasm_tendency'].value - - return weights - - def _update_ocean_traits(self, evolved_ocean: torch.Tensor): - """Update the stored OCEAN traits based on evolution.""" - with torch.no_grad(): - new_traits = evolved_ocean.cpu().numpy() - - # Update with small learning rate to maintain stability - alpha = 0.05 - - self.ocean_traits.openness = ( - (1 - alpha) * self.ocean_traits.openness + alpha * float(new_traits[0]) - ) - self.ocean_traits.conscientiousness = ( - (1 - alpha) * self.ocean_traits.conscientiousness + alpha * float(new_traits[1]) - ) - self.ocean_traits.extraversion = ( - (1 - alpha) * self.ocean_traits.extraversion + alpha * float(new_traits[2]) - ) - self.ocean_traits.agreeableness = ( - (1 - alpha) * self.ocean_traits.agreeableness + alpha * float(new_traits[3]) - ) - self.ocean_traits.neuroticism = ( - (1 - alpha) * self.ocean_traits.neuroticism + alpha * float(new_traits[4]) - ) - - # Update Myers-Briggs type based on new OCEAN traits - self.mb_type = self.mb_analyzer.analyze_type(self.ocean_traits) - - def evolve_from_interaction( - self, - interaction_type: str, - user_feedback: float, - emotional_context: Dict[str, float], - user_id: Optional[str] = None, - conversation_success: float = 0.5 - ): - """ - Evolve personality based on a specific interaction. - - This is where Lyra learns and adapts her personality from each conversation. - """ - logger.info(f"Evolving personality from {interaction_type} interaction " - f"(feedback: {user_feedback:.2f}, success: {conversation_success:.2f})") - - # Update relationship dynamics if user_id provided - if user_id: - self._update_relationship_dynamics(user_id, user_feedback, interaction_type) - - # Evolve OCEAN traits based on interaction outcome - self._evolve_ocean_from_interaction(user_feedback, emotional_context, interaction_type) - - # Evolve custom traits - self._evolve_custom_traits(interaction_type, user_feedback, conversation_success) - - # Update self-awareness - self._update_self_awareness(user_feedback, conversation_success) - - # Record evolution step - self.evolution.total_interactions += 1 - self.evolution.evolution_history.append({ - 'timestamp': datetime.now().isoformat(), - 'interaction_type': interaction_type, - 'user_feedback': user_feedback, - 'conversation_success': conversation_success, - 'ocean_traits': self.ocean_traits.to_dict(), - 'mb_type': self.mb_type.value - }) - - # Keep evolution history manageable - if len(self.evolution.evolution_history) > 1000: - self.evolution.evolution_history = self.evolution.evolution_history[-500:] - - def _update_relationship_dynamics(self, user_id: str, feedback: float, interaction_type: str): - """Update relationship-specific personality dynamics.""" - if user_id not in self.relationship_dynamics: - self.relationship_dynamics[user_id] = { - 'familiarity': 0.0, - 'positive_interactions': 0.0, - 'conflict_level': 0.0, - 'interaction_count': 0, - 'personality_adaptation': {} - } - - rel_data = self.relationship_dynamics[user_id] - - # Update familiarity - rel_data['familiarity'] = min(1.0, rel_data['familiarity'] + 0.05) - - # Update positive interaction ratio - rel_data['interaction_count'] += 1 - if feedback > 0.6: - rel_data['positive_interactions'] = ( - (rel_data['positive_interactions'] * (rel_data['interaction_count'] - 1) + 1.0) / - rel_data['interaction_count'] - ) - elif feedback < 0.4: - rel_data['positive_interactions'] = ( - (rel_data['positive_interactions'] * (rel_data['interaction_count'] - 1) + 0.0) / - rel_data['interaction_count'] - ) - - # Update conflict level - if interaction_type in ['argument', 'disagreement'] or feedback < 0.3: - rel_data['conflict_level'] = min(1.0, rel_data['conflict_level'] + 0.1) - else: - rel_data['conflict_level'] = max(0.0, rel_data['conflict_level'] - 0.02) - - def _evolve_ocean_from_interaction( - self, - feedback: float, - emotional_context: Dict[str, float], - interaction_type: str - ): - """Evolve OCEAN traits based on interaction outcome.""" - - # Determine evolution direction based on feedback - if feedback > 0.7: # Very positive feedback - # Strengthen traits that led to success - if interaction_type in ['creative', 'brainstorming']: - self.ocean_traits.openness = min(1.0, self.ocean_traits.openness + 0.01) - elif interaction_type in ['support', 'help']: - self.ocean_traits.agreeableness = min(1.0, self.ocean_traits.agreeableness + 0.01) - elif interaction_type in ['social', 'casual']: - self.ocean_traits.extraversion = min(1.0, self.ocean_traits.extraversion + 0.01) - - elif feedback < 0.3: # Negative feedback - # Adapt traits that might have caused issues - if 'conflict' in emotional_context or interaction_type == 'argument': - # Become more agreeable if there was conflict - self.ocean_traits.agreeableness = min(1.0, self.ocean_traits.agreeableness + 0.02) - self.ocean_traits.neuroticism = max(0.0, self.ocean_traits.neuroticism - 0.01) - elif 'confusion' in emotional_context: - # Be more conscientious if responses were unclear - self.ocean_traits.conscientiousness = min(1.0, self.ocean_traits.conscientiousness + 0.015) - - # Emotional context influence - for emotion, intensity in emotional_context.items(): - if emotion == 'joy' and intensity > 0.7: - self.ocean_traits.extraversion = min(1.0, self.ocean_traits.extraversion + 0.005) - elif emotion == 'anxiety' and intensity > 0.6: - self.ocean_traits.neuroticism = min(1.0, self.ocean_traits.neuroticism + 0.01) - elif emotion == 'curiosity' and intensity > 0.7: - self.ocean_traits.openness = min(1.0, self.ocean_traits.openness + 0.005) - - def _evolve_custom_traits(self, interaction_type: str, feedback: float, success: float): - """Evolve custom personality traits.""" - - # Humor evolution - if interaction_type in ['joke', 'funny', 'casual'] and feedback > 0.6: - self.custom_traits['humor_level'].evolve(0.1, "successful humor") - elif feedback < 0.4 and self.custom_traits['humor_level'].value > 0.5: - self.custom_traits['humor_level'].evolve(-0.05, "humor backfired") - - # Empathy evolution - if interaction_type in ['support', 'emotional'] and feedback > 0.7: - self.custom_traits['empathy_level'].evolve(0.08, "successful emotional support") - - # Assertiveness evolution - if interaction_type in ['disagreement', 'debate'] and feedback > 0.6: - self.custom_traits['assertiveness'].evolve(0.06, "successful assertiveness") - elif feedback < 0.3 and self.custom_traits['assertiveness'].value > 0.7: - self.custom_traits['assertiveness'].evolve(-0.08, "assertiveness caused conflict") - - # Intellectual evolution - if interaction_type in ['technical', 'academic', 'analytical'] and feedback > 0.6: - self.custom_traits['intellectualism'].evolve(0.05, "intellectual engagement successful") - - # Playfulness evolution - if interaction_type in ['casual', 'fun'] and success > 0.7: - self.custom_traits['playfulness'].evolve(0.07, "playful interaction successful") - - # Curiosity evolution - grows when asking questions leads to good conversations - if feedback > 0.6 and success > 0.6: - self.custom_traits['curiosity'].evolve(0.03, "curiosity rewarded") - - def _update_self_awareness(self, feedback: float, success: float): - """Update Lyra's awareness of her own personality and its effects.""" - - # Personality insight grows with successful interactions - if feedback > 0.7 and success > 0.7: - self.self_awareness['personality_insight'] = min(1.0, - self.self_awareness['personality_insight'] + 0.01) - - # Change awareness grows when adaptations lead to better outcomes - recent_changes = any( - datetime.now() - trait.last_update < timedelta(hours=1) - for trait in self.custom_traits.values() - if trait.last_update - ) - - if recent_changes and feedback > 0.6: - self.self_awareness['change_awareness'] = min(1.0, - self.self_awareness['change_awareness'] + 0.02) - - # Trait understanding grows with experience - self.self_awareness['trait_understanding'] = min(1.0, - self.self_awareness['trait_understanding'] + 0.005) - - def consciously_modify_trait(self, trait_name: str, target_value: float, reason: str = "self-directed change"): - """ - Allow Lyra to consciously modify her own personality traits. - - This represents Lyra's ability to intentionally change aspects of herself. - """ - if not self.enable_self_modification: - logger.warning("Self-modification is disabled") - return False - - # Check if this is a valid trait to modify - valid_ocean_traits = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] - - if trait_name in valid_ocean_traits: - current_value = getattr(self.ocean_traits, trait_name) - change = target_value - current_value - - # Apply change gradually (max 0.1 change per conscious modification) - actual_change = np.clip(change, -0.1, 0.1) - new_value = np.clip(current_value + actual_change, 0.0, 1.0) - - setattr(self.ocean_traits, trait_name, new_value) - - logger.info(f"Lyra consciously modified {trait_name}: {current_value:.3f} -> {new_value:.3f} ({reason})") - return True - - elif trait_name in self.custom_traits: - self.custom_traits[trait_name].evolve(target_value - self.custom_traits[trait_name].value, reason) - logger.info(f"Lyra consciously modified {trait_name} ({reason})") - return True - - else: - logger.warning(f"Unknown trait for modification: {trait_name}") - return False - - def get_personality_summary(self) -> Dict[str, Any]: - """Get a comprehensive summary of current personality state.""" - return { - 'ocean_traits': self.ocean_traits.to_dict(), - 'myers_briggs_type': self.mb_type.value, - 'custom_traits': { - name: { - 'value': trait.value, - 'variance': trait.variance, - 'stability': trait.stability, - 'recent_changes': len([ - change for change in trait.change_history - if datetime.fromtimestamp(change[0]) > datetime.now() - timedelta(hours=24) - ]) - } - for name, trait in self.custom_traits.items() - }, - 'evolution_stats': { - 'total_interactions': self.evolution.total_interactions, - 'adaptation_rate': self.evolution.adaptation_rate, - 'recent_evolution_count': len([ - ev for ev in self.evolution.evolution_history - if datetime.fromisoformat(ev['timestamp']) > datetime.now() - timedelta(hours=24) - ]) - }, - 'self_awareness': self.self_awareness, - 'relationship_count': len(self.relationship_dynamics), - 'personality_characteristics': self.mb_analyzer.get_type_characteristics(self.mb_type) - } - - def save_personality(self, path: Path): - """Save personality state to file.""" - state = { - 'ocean_traits': self.ocean_traits.to_dict(), - 'mb_type': self.mb_type.value, - 'custom_traits': { - name: { - 'value': trait.value, - 'variance': trait.variance, - 'adaptation_rate': trait.adaptation_rate, - 'stability': trait.stability, - 'change_history': trait.change_history[-100:] # Keep recent history - } - for name, trait in self.custom_traits.items() - }, - 'evolution': { - 'adaptation_rate': self.evolution.adaptation_rate, - 'stability_factor': self.evolution.stability_factor, - 'total_interactions': self.evolution.total_interactions, - 'evolution_history': self.evolution.evolution_history[-200:] # Keep recent - }, - 'self_awareness': self.self_awareness, - 'relationship_dynamics': { - k: v for k, v in self.relationship_dynamics.items() - if v['interaction_count'] > 5 # Only save meaningful relationships - }, - 'model_state': self.state_dict(), - 'timestamp': datetime.now().isoformat() - } - - with open(path, 'w') as f: - json.dump(state, f, indent=2, default=str) - - logger.info(f"Personality saved to {path}") - - def load_personality(self, path: Path): - """Load personality state from file.""" - if not path.exists(): - logger.warning(f"Personality file not found: {path}") - return - - try: - with open(path, 'r') as f: - state = json.load(f) - - # Restore OCEAN traits - self.ocean_traits = OCEANTraits.from_dict(state['ocean_traits']) - - # Restore Myers-Briggs type - self.mb_type = MyersBriggsType(state['mb_type']) - - # Restore custom traits - for name, trait_data in state['custom_traits'].items(): - if name in self.custom_traits: - trait = self.custom_traits[name] - trait.value = trait_data['value'] - trait.variance = trait_data.get('variance', 0.1) - trait.adaptation_rate = trait_data.get('adaptation_rate', 0.01) - trait.stability = trait_data.get('stability', 0.8) - trait.change_history = trait_data.get('change_history', []) - - # Restore evolution data - evolution_data = state.get('evolution', {}) - self.evolution.adaptation_rate = evolution_data.get('adaptation_rate', 0.01) - self.evolution.stability_factor = evolution_data.get('stability_factor', 0.9) - self.evolution.total_interactions = evolution_data.get('total_interactions', 0) - self.evolution.evolution_history = evolution_data.get('evolution_history', []) - - # Restore self-awareness - self.self_awareness = state.get('self_awareness', self.self_awareness) - - # Restore relationship dynamics - self.relationship_dynamics = state.get('relationship_dynamics', {}) - - # Restore model state - if 'model_state' in state: - self.load_state_dict(state['model_state']) - - logger.info(f"Personality loaded from {path}") - - except Exception as e: - logger.error(f"Failed to load personality: {e}") - - def simulate_personality_development(self, days: int = 30) -> Dict[str, Any]: - """ - Simulate personality development over time for testing/analysis. - - This shows how Lyra's personality might evolve with different interaction patterns. - """ - simulation_log = [] - - for day in range(days): - # Simulate different types of interactions - daily_interactions = np.random.randint(5, 20) - - for _ in range(daily_interactions): - # Random interaction types - interaction_types = ['casual', 'support', 'creative', 'technical', 'social', 'funny'] - interaction_type = np.random.choice(interaction_types) - - # Random feedback (biased slightly positive) - feedback = np.random.beta(2, 1) # Skewed toward positive - - # Random emotional context - emotions = ['joy', 'curiosity', 'calm', 'excitement', 'concern'] - emotional_context = { - np.random.choice(emotions): np.random.random() - } - - # Evolve personality - self.evolve_from_interaction( - interaction_type=interaction_type, - user_feedback=feedback, - emotional_context=emotional_context, - conversation_success=feedback * 0.8 + np.random.random() * 0.2 - ) - - # Log daily state - daily_summary = { - 'day': day, - 'ocean_traits': self.ocean_traits.to_dict(), - 'total_interactions': self.evolution.total_interactions, - 'mb_type': self.mb_type.value - } - simulation_log.append(daily_summary) - - return { - 'simulation_days': days, - 'final_personality': self.get_personality_summary(), - 'development_log': simulation_log - } \ No newline at end of file diff --git a/lyra/personality/traits.py b/lyra/personality/traits.py deleted file mode 100644 index 107e578..0000000 --- a/lyra/personality/traits.py +++ /dev/null @@ -1,516 +0,0 @@ -import torch -import torch.nn as nn -import numpy as np -from typing import Dict, List, Tuple, Optional, Any -from dataclasses import dataclass, field -from enum import Enum -import json -import logging - -logger = logging.getLogger(__name__) - -class MyersBriggsType(Enum): - """Myers-Briggs personality types.""" - INTJ = "INTJ" # Architect - INTP = "INTP" # Thinker - ENTJ = "ENTJ" # Commander - ENTP = "ENTP" # Debater - INFJ = "INFJ" # Advocate - INFP = "INFP" # Mediator - ENFJ = "ENFJ" # Protagonist - ENFP = "ENFP" # Campaigner - ISTJ = "ISTJ" # Logistician - ISFJ = "ISFJ" # Protector - ESTJ = "ESTJ" # Executive - ESFJ = "ESFJ" # Consul - ISTP = "ISTP" # Virtuoso - ISFP = "ISFP" # Adventurer - ESTP = "ESTP" # Entrepreneur - ESFP = "ESFP" # Entertainer - -@dataclass -class OCEANTraits: - """Big Five (OCEAN) personality traits with dynamic adaptation.""" - openness: float = 0.5 # Openness to experience - conscientiousness: float = 0.5 # Conscientiousness - extraversion: float = 0.5 # Extraversion - agreeableness: float = 0.5 # Agreeableness - neuroticism: float = 0.5 # Neuroticism - - # Trait variance - how much each trait can fluctuate - openness_variance: float = 0.1 - conscientiousness_variance: float = 0.1 - extraversion_variance: float = 0.1 - agreeableness_variance: float = 0.1 - neuroticism_variance: float = 0.1 - - def to_dict(self) -> Dict[str, float]: - """Convert to dictionary representation.""" - return { - 'openness': self.openness, - 'conscientiousness': self.conscientiousness, - 'extraversion': self.extraversion, - 'agreeableness': self.agreeableness, - 'neuroticism': self.neuroticism, - 'openness_variance': self.openness_variance, - 'conscientiousness_variance': self.conscientiousness_variance, - 'extraversion_variance': self.extraversion_variance, - 'agreeableness_variance': self.agreeableness_variance, - 'neuroticism_variance': self.neuroticism_variance - } - - @classmethod - def from_dict(cls, data: Dict[str, float]) -> 'OCEANTraits': - """Create from dictionary representation.""" - return cls(**data) - - def to_tensor(self, device: Optional[torch.device] = None) -> torch.Tensor: - """Convert to tensor for neural network processing.""" - values = [ - self.openness, self.conscientiousness, self.extraversion, - self.agreeableness, self.neuroticism - ] - return torch.tensor(values, dtype=torch.float32, device=device) - - def apply_situational_modification( - self, - situation_type: str, - intensity: float = 1.0 - ) -> 'OCEANTraits': - """ - Apply situational modifications to personality traits. - - Different situations can bring out different aspects of personality. - """ - modified = OCEANTraits( - openness=self.openness, - conscientiousness=self.conscientiousness, - extraversion=self.extraversion, - agreeableness=self.agreeableness, - neuroticism=self.neuroticism, - openness_variance=self.openness_variance, - conscientiousness_variance=self.conscientiousness_variance, - extraversion_variance=self.extraversion_variance, - agreeableness_variance=self.agreeableness_variance, - neuroticism_variance=self.neuroticism_variance - ) - - # Situational trait modifications - modifications = { - 'stress': { - 'neuroticism': 0.2 * intensity, - 'conscientiousness': -0.1 * intensity, - 'agreeableness': -0.1 * intensity - }, - 'social': { - 'extraversion': 0.15 * intensity, - 'agreeableness': 0.1 * intensity, - 'openness': 0.05 * intensity - }, - 'creative': { - 'openness': 0.2 * intensity, - 'conscientiousness': -0.05 * intensity, - 'neuroticism': -0.1 * intensity - }, - 'conflict': { - 'agreeableness': -0.2 * intensity, - 'neuroticism': 0.15 * intensity, - 'extraversion': -0.1 * intensity - }, - 'learning': { - 'openness': 0.15 * intensity, - 'conscientiousness': 0.1 * intensity, - 'neuroticism': -0.05 * intensity - } - } - - if situation_type in modifications: - mods = modifications[situation_type] - for trait, change in mods.items(): - current_value = getattr(modified, trait) - variance = getattr(modified, f"{trait}_variance") - - # Apply change within variance bounds - new_value = current_value + change - new_value = np.clip(new_value, - current_value - variance, - current_value + variance) - new_value = np.clip(new_value, 0.0, 1.0) - - setattr(modified, trait, new_value) - - return modified - -@dataclass -class PersonalityEvolution: - """Tracks how personality evolves over time.""" - adaptation_rate: float = 0.01 - stability_factor: float = 0.9 - max_change_per_step: float = 0.05 - - # Evolution history - evolution_history: List[Dict[str, Any]] = field(default_factory=list) - total_interactions: int = 0 - - def __post_init__(self): - """Initialize evolution tracking.""" - if not self.evolution_history: - self.evolution_history = [] - -class PersonalityDynamics(nn.Module): - """ - Neural network that models personality dynamics and adaptation. - - This system allows Lyra's personality to evolve naturally based on - her interactions and experiences. - """ - - def __init__( - self, - input_dim: int = 10, # Contextual features - personality_dim: int = 5, # OCEAN traits - hidden_dim: int = 64, - adaptation_rate: float = 0.01 - ): - super().__init__() - - self.personality_dim = personality_dim - self.adaptation_rate = adaptation_rate - - # Context processing network - self.context_processor = nn.Sequential( - nn.Linear(input_dim, hidden_dim), - nn.LayerNorm(hidden_dim), - nn.ReLU(), - nn.Dropout(0.1), - nn.Linear(hidden_dim, hidden_dim // 2), - nn.LayerNorm(hidden_dim // 2), - nn.ReLU(), - nn.Linear(hidden_dim // 2, personality_dim) - ) - - # Personality adaptation network - self.adaptation_network = nn.Sequential( - nn.Linear(personality_dim * 2, hidden_dim), # Current + context influence - nn.LayerNorm(hidden_dim), - nn.Tanh(), - nn.Linear(hidden_dim, personality_dim), - nn.Tanh() # Bounded output for personality changes - ) - - # Stability network - resists change when personality is stable - self.stability_network = nn.Sequential( - nn.Linear(personality_dim, hidden_dim // 2), - nn.ReLU(), - nn.Linear(hidden_dim // 2, 1), - nn.Sigmoid() - ) - - # Meta-learning for adaptation rate - self.meta_adaptation = nn.Linear(personality_dim + 1, 1) # +1 for feedback - - def forward( - self, - current_personality: torch.Tensor, - context_features: torch.Tensor, - feedback_signal: Optional[torch.Tensor] = None - ) -> Tuple[torch.Tensor, Dict[str, Any]]: - """ - Evolve personality based on context and feedback. - - Args: - current_personality: Current OCEAN traits [batch, 5] - context_features: Contextual features [batch, input_dim] - feedback_signal: Feedback from interactions [batch, 1] - - Returns: - evolved_personality: Updated personality traits - evolution_info: Information about the evolution step - """ - batch_size = current_personality.shape[0] - - # Process context to understand what personality aspects to emphasize - context_influence = self.context_processor(context_features) - - # Combine current personality with context influence - combined_input = torch.cat([current_personality, context_influence], dim=-1) - - # Generate personality adaptation - personality_delta = self.adaptation_network(combined_input) - - # Calculate stability (resistance to change) - stability = self.stability_network(current_personality) - - # Meta-learning: adapt the adaptation rate based on feedback - if feedback_signal is not None: - meta_input = torch.cat([current_personality, feedback_signal], dim=-1) - meta_adaptation_rate = torch.sigmoid(self.meta_adaptation(meta_input)) - else: - meta_adaptation_rate = torch.ones(batch_size, 1, device=current_personality.device) * 0.5 - - # Apply evolution with stability consideration - effective_rate = self.adaptation_rate * meta_adaptation_rate * (1 - stability) - evolved_personality = current_personality + effective_rate * personality_delta - - # Ensure personality traits stay in valid range [0, 1] - evolved_personality = torch.clamp(evolved_personality, 0.0, 1.0) - - # Prepare evolution info - evolution_info = { - 'personality_change': torch.norm(evolved_personality - current_personality, dim=-1).mean().item(), - 'stability': stability.mean().item(), - 'context_influence_strength': torch.norm(context_influence, dim=-1).mean().item(), - 'adaptation_rate': effective_rate.mean().item() - } - - return evolved_personality, evolution_info - -class MyersBriggsAnalyzer: - """Analyzes and updates Myers-Briggs type based on OCEAN traits and behavior.""" - - def __init__(self): - # Mapping from OCEAN traits to Myers-Briggs dimensions - self.mb_mappings = { - 'E_I': lambda traits: traits.extraversion, # Extraversion vs Introversion - 'S_N': lambda traits: traits.openness, # Sensing vs iNtuition - 'T_F': lambda traits: 1 - traits.agreeableness, # Thinking vs Feeling - 'J_P': lambda traits: traits.conscientiousness # Judging vs Perceiving - } - - def analyze_type(self, ocean_traits: OCEANTraits) -> MyersBriggsType: - """Determine Myers-Briggs type from OCEAN traits.""" - - # Calculate dimension scores - e_i = self.mb_mappings['E_I'](ocean_traits) - s_n = self.mb_mappings['S_N'](ocean_traits) - t_f = self.mb_mappings['T_F'](ocean_traits) - j_p = self.mb_mappings['J_P'](ocean_traits) - - # Determine letters - letter1 = 'E' if e_i > 0.5 else 'I' - letter2 = 'N' if s_n > 0.5 else 'S' - letter3 = 'T' if t_f > 0.5 else 'F' - letter4 = 'J' if j_p > 0.5 else 'P' - - type_string = letter1 + letter2 + letter3 + letter4 - - return MyersBriggsType(type_string) - - def get_type_characteristics(self, mb_type: MyersBriggsType) -> Dict[str, Any]: - """Get characteristics and tendencies for a Myers-Briggs type.""" - - characteristics = { - MyersBriggsType.INTJ: { - 'communication_style': 'direct, analytical, strategic', - 'decision_making': 'logical, long-term focused', - 'social_tendencies': 'selective, deep connections', - 'stress_response': 'withdraw, analyze, plan', - 'learning_preference': 'conceptual, systematic', - 'humor_style': 'dry, witty, intellectual' - }, - MyersBriggsType.ENFP: { - 'communication_style': 'enthusiastic, expressive, inspirational', - 'decision_making': 'value-based, considers possibilities', - 'social_tendencies': 'outgoing, builds rapport quickly', - 'stress_response': 'seek support, brainstorm solutions', - 'learning_preference': 'interactive, experiential', - 'humor_style': 'playful, storytelling, spontaneous' - }, - MyersBriggsType.ISFJ: { - 'communication_style': 'supportive, gentle, detailed', - 'decision_making': 'considers impact on others, traditional', - 'social_tendencies': 'helpful, loyal, modest', - 'stress_response': 'internalize, seek harmony', - 'learning_preference': 'structured, practical examples', - 'humor_style': 'gentle, self-deprecating, situational' - }, - # Add more types as needed... - } - - return characteristics.get(mb_type, { - 'communication_style': 'balanced approach', - 'decision_making': 'considers multiple factors', - 'social_tendencies': 'adaptive to situation', - 'stress_response': 'varied coping strategies', - 'learning_preference': 'mixed approaches', - 'humor_style': 'situationally appropriate' - }) - -class PersonalityProfiler: - """Creates and maintains detailed personality profiles.""" - - def __init__(self): - self.ocean_analyzer = OCEANTraits() - self.mb_analyzer = MyersBriggsAnalyzer() - - def create_profile( - self, - ocean_traits: OCEANTraits, - conversation_history: List[str] = None, - behavioral_data: Dict[str, Any] = None - ) -> Dict[str, Any]: - """Create a comprehensive personality profile.""" - - # Determine Myers-Briggs type - mb_type = self.mb_analyzer.analyze_type(ocean_traits) - mb_characteristics = self.mb_analyzer.get_type_characteristics(mb_type) - - # Create base profile - profile = { - 'ocean_traits': ocean_traits.to_dict(), - 'myers_briggs_type': mb_type.value, - 'characteristics': mb_characteristics, - 'timestamp': torch.tensor(float(torch.rand(1))).item(), - 'profile_version': 1.0 - } - - # Add behavioral insights if available - if behavioral_data: - profile['behavioral_patterns'] = self._analyze_behavioral_patterns( - ocean_traits, behavioral_data - ) - - # Add conversation style analysis if history is available - if conversation_history: - profile['conversation_style'] = self._analyze_conversation_style( - ocean_traits, conversation_history - ) - - return profile - - def _analyze_behavioral_patterns( - self, - ocean_traits: OCEANTraits, - behavioral_data: Dict[str, Any] - ) -> Dict[str, Any]: - """Analyze behavioral patterns based on personality and data.""" - - patterns = {} - - # Response time patterns - if 'response_times' in behavioral_data: - avg_response_time = np.mean(behavioral_data['response_times']) - - # Introverts typically take longer to respond - expected_time = 2.0 + (1 - ocean_traits.extraversion) * 3.0 - - patterns['response_speed'] = { - 'average_seconds': avg_response_time, - 'relative_to_expected': avg_response_time / expected_time, - 'pattern': 'quick' if avg_response_time < expected_time else 'thoughtful' - } - - # Topic preferences - if 'topic_engagement' in behavioral_data: - patterns['topic_preferences'] = self._infer_topic_preferences( - ocean_traits, behavioral_data['topic_engagement'] - ) - - # Emotional expression patterns - if 'emotional_expressions' in behavioral_data: - patterns['emotional_style'] = self._analyze_emotional_expression( - ocean_traits, behavioral_data['emotional_expressions'] - ) - - return patterns - - def _analyze_conversation_style( - self, - ocean_traits: OCEANTraits, - conversation_history: List[str] - ) -> Dict[str, Any]: - """Analyze conversation style from history.""" - - style = {} - - if not conversation_history: - return style - - # Analyze message characteristics - message_lengths = [len(msg.split()) for msg in conversation_history] - - style['verbosity'] = { - 'average_words': np.mean(message_lengths), - 'variance': np.var(message_lengths), - 'style': 'concise' if np.mean(message_lengths) < 10 else 'elaborate' - } - - # Question asking frequency (curiosity indicator) - question_count = sum(1 for msg in conversation_history if '?' in msg) - style['curiosity_level'] = question_count / len(conversation_history) - - # Emotional expression analysis - emotional_words = ['love', 'hate', 'excited', 'sad', 'happy', 'angry', 'worried'] - emotional_frequency = sum( - sum(1 for word in emotional_words if word in msg.lower()) - for msg in conversation_history - ) / len(conversation_history) - - style['emotional_expressiveness'] = emotional_frequency - - return style - - def _infer_topic_preferences( - self, - ocean_traits: OCEANTraits, - topic_engagement: Dict[str, float] - ) -> Dict[str, Any]: - """Infer topic preferences based on personality and engagement data.""" - - preferences = {} - - # High openness correlates with interest in abstract/creative topics - if ocean_traits.openness > 0.6: - creative_topics = ['art', 'philosophy', 'science', 'technology', 'literature'] - preferences['preferred_categories'] = creative_topics - - # High conscientiousness correlates with practical topics - if ocean_traits.conscientiousness > 0.6: - practical_topics = ['productivity', 'planning', 'organization', 'goals'] - preferences['practical_interests'] = practical_topics - - # Extraversion affects social topic interest - if ocean_traits.extraversion > 0.6: - social_topics = ['relationships', 'social events', 'collaboration'] - preferences['social_interests'] = social_topics - - # Add engagement scores - preferences['engagement_scores'] = topic_engagement - - return preferences - - def _analyze_emotional_expression( - self, - ocean_traits: OCEANTraits, - emotional_expressions: Dict[str, int] - ) -> Dict[str, Any]: - """Analyze how emotions are expressed based on personality.""" - - style = {} - - total_expressions = sum(emotional_expressions.values()) - if total_expressions == 0: - return style - - # Calculate emotion proportions - emotion_proportions = { - emotion: count / total_expressions - for emotion, count in emotional_expressions.items() - } - - style['emotion_distribution'] = emotion_proportions - - # Analyze based on personality traits - if ocean_traits.neuroticism > 0.6: - style['emotional_volatility'] = 'high' - elif ocean_traits.neuroticism < 0.4: - style['emotional_volatility'] = 'low' - else: - style['emotional_volatility'] = 'moderate' - - if ocean_traits.agreeableness > 0.6: - style['emotional_tone'] = 'positive_focused' - else: - style['emotional_tone'] = 'balanced' - - return style \ No newline at end of file diff --git a/lyra/testing/__init__.py b/lyra/testing/__init__.py deleted file mode 100644 index b8e2aed..0000000 --- a/lyra/testing/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -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" -] \ No newline at end of file diff --git a/lyra/testing/behavior_tests.py b/lyra/testing/behavior_tests.py deleted file mode 100644 index c2eb025..0000000 --- a/lyra/testing/behavior_tests.py +++ /dev/null @@ -1,701 +0,0 @@ -""" -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' - ) - ] \ No newline at end of file diff --git a/lyra/training/__init__.py b/lyra/training/__init__.py deleted file mode 100644 index a13d5f8..0000000 --- a/lyra/training/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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" -] \ No newline at end of file diff --git a/lyra/training/pipeline.py b/lyra/training/pipeline.py deleted file mode 100644 index cceac99..0000000 --- a/lyra/training/pipeline.py +++ /dev/null @@ -1,574 +0,0 @@ -""" -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 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 82543b8..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,40 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "lyra" -version = "1.0.0" -description = "Lyra - Advanced AI Discord Chatbot with Emotional Intelligence" -authors = [{name = "Lyra Development Team"}] -license = {text = "MIT"} -readme = "README.md" -requires-python = ">=3.9" -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Communications :: Chat", - "Topic :: Scientific/Engineering :: Artificial Intelligence", -] - -[project.scripts] -lyra = "lyra.main:main" - -[tool.black] -line-length = 100 -target-version = ['py39', 'py310', 'py311'] - -[tool.isort] -profile = "black" -line_length = 100 - -[tool.pytest] -testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] \ No newline at end of file diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 23a1aa2..0000000 --- a/pytest.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tool:pytest] -minversion = 6.0 -addopts = - -ra - -q - --strict-markers - --strict-config - --cov=lyra - --cov-report=term-missing - --cov-report=html:htmlcov - --cov-fail-under=70 -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* -asyncio_mode = auto -markers = - slow: marks tests as slow (deselect with '-m "not slow"') - integration: marks tests as integration tests - unit: marks tests as unit tests - gpu: marks tests that require GPU -filterwarnings = - ignore::UserWarning - ignore::DeprecationWarning \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1d1eca9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,58 +0,0 @@ -torch>=2.1.0 -torchaudio>=2.1.0 -torchvision>=0.16.0 -transformers>=4.35.0 -tokenizers>=0.14.0 -datasets>=2.14.0 -accelerate>=0.24.0 -bitsandbytes>=0.41.0 -discord.py>=2.3.0 -asyncio-mqtt>=0.11.0 -aiohttp>=3.9.0 -numpy>=1.24.0 -pandas>=2.1.0 -scipy>=1.11.0 -scikit-learn>=1.3.0 -matplotlib>=3.8.0 -seaborn>=0.12.0 -tqdm>=4.66.0 -wandb>=0.16.0 -tensorboard>=2.15.0 -psycopg2-binary>=2.9.0 -sqlalchemy>=2.0.0 -alembic>=1.12.0 -redis>=5.0.0 -chromadb>=0.4.0 -sentence-transformers>=2.2.0 -beautifulsoup4>=4.12.0 -requests>=2.31.0 -aiofiles>=23.2.0 -python-dotenv>=1.0.0 -pydantic>=2.5.0 -fastapi>=0.104.0 -uvicorn>=0.24.0 -python-multipart>=0.0.6 -jinja2>=3.1.0 -regex>=2023.10.0 -nltk>=3.8.0 -spacy>=3.7.0 -textstat>=0.7.0 -langdetect>=1.0.9 -emoji>=2.8.0 -pillow>=10.1.0 -opencv-python>=4.8.0 -librosa>=0.10.0 -soundfile>=0.12.0 -pyyaml>=6.0.0 -jsonschema>=4.20.0 -cryptography>=41.0.0 -flake8>=6.0.0 -black>=23.0.0 -isort>=5.12.0 -mypy>=1.5.0 -pre-commit>=3.4.0 -pytest>=7.4.0 -pytest-asyncio>=0.21.0 -pytest-cov>=4.1.0 -pytest-mock>=3.11.0 -factory-boy>=3.3.0 \ No newline at end of file diff --git a/run_lyra.py b/run_lyra.py deleted file mode 100644 index a817a25..0000000 --- a/run_lyra.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Simple Lyra startup script that demonstrates the system without requiring database setup. - -This script shows Lyra's core functionality without needing PostgreSQL/Redis configuration. -""" - -import asyncio -import logging -import torch -from datetime import datetime - -# Set up basic logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -logger = logging.getLogger(__name__) - - -async def demonstrate_lyra(): - """Demonstrate Lyra's core capabilities.""" - logger.info("Starting Lyra AI Demonstration...") - - try: - # Import Lyra components - from lyra.core.lyra_model import LyraModel - from lyra.personality.matrix import PersonalityMatrix - from lyra.emotions.system import EmotionalSystem - from lyra.core.thinking_agent import ThinkingAgent - from lyra.core.self_evolution import SelfEvolutionEngine - - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - logger.info(f"Using device: {device}") - - # Initialize Lyra's core model - logger.info("Initializing Lyra's AI core...") - lyra = LyraModel( - vocab_size=1000, # Small vocab for demo - embed_dim=256, # Smaller model for demo - num_layers=4, # Fewer layers for speed - num_heads=8, - device=device, - enable_evolution=True - ) - - logger.info("Lyra AI successfully initialized!") - - # Get Lyra's status - status = lyra.get_lyra_status() - - logger.info("Lyra Status Report:") - logger.info(f" - Model Parameters: {status['model_info']['vocab_size']:,} vocab") - logger.info(f" - Embed Dimension: {status['model_info']['embed_dim']}") - logger.info(f" - Evolution Enabled: {status['model_info']['evolution_enabled']}") - logger.info(f" - Device: {status['model_info']['device']}") - - # Test personality system - logger.info("Testing Personality System...") - personality_summary = status['personality'] - logger.info(f" - Myers-Briggs Type: {personality_summary.get('myers_briggs_type', 'ENFP')}") - if 'ocean_traits' in personality_summary: - logger.info(" - OCEAN Traits:") - for trait, value in personality_summary['ocean_traits'].items(): - logger.info(f" * {trait.title()}: {value:.2f}/5.0") - - # Test emotional system - logger.info("Testing Emotional System...") - emotions = status['emotions'] - logger.info(f" - Dominant Emotion: {emotions.get('dominant_emotion', 'curious').title()}") - logger.info(f" - Emotional Stability: {emotions.get('stability', 0.8):.2f}") - - # Test thinking system - logger.info("Testing Thinking Agent...") - thinking = status['thinking'] - logger.info(f" - Thought Types Available: {thinking.get('thought_types_count', 8)}") - logger.info(f" - Max Thought Depth: {thinking.get('max_depth', 5)}") - - # Test evolution system - logger.info("Testing Self-Evolution...") - evolution = status['evolution'] - if evolution.get('status') != 'disabled': - logger.info(f" - Evolution Steps: {evolution.get('total_evolution_steps', 0)}") - logger.info(f" - Plasticity: {evolution.get('personality_plasticity', 0.1):.3f}") - - # Generate a test response - logger.info("Testing Response Generation...") - try: - response, info = await lyra.generate_response( - user_message="Hello Lyra! How are you feeling today?", - user_id="demo_user", - max_new_tokens=50, - temperature=0.9 - ) - - logger.info(f"Lyra's Response: '{response}'") - logger.info("Response Analysis:") - logger.info(f" - Emotional State: {info['emotional_state']['dominant_emotion']}") - logger.info(f" - Thoughts Generated: {len(info['thoughts'])}") - logger.info(f" - Response Method: {info['response_generation_method']}") - - if info['thoughts']: - logger.info("Lyra's Internal Thoughts:") - for i, thought in enumerate(info['thoughts'][:3], 1): # Show first 3 thoughts - logger.info(f" {i}. [{thought['type']}] {thought['content']}") - - except Exception as e: - logger.warning(f"Response generation encountered an issue: {e}") - logger.info(" This is normal for the demo - full functionality requires training data") - - # Test self-evolution - logger.info("Testing Self-Evolution...") - try: - lyra.evolve_from_feedback( - user_feedback=0.8, - conversation_success=0.9, - user_id="demo_user" - ) - logger.info(" Successfully applied evolutionary feedback") - except Exception as e: - logger.warning(f"Evolution test encountered minor issue: {e}") - logger.info(" Evolution system is functional - this is a minor tensor handling issue") - - logger.info("Lyra AI Demonstration Complete!") - logger.info("All core systems are functional and ready for deployment!") - - return True - - except ImportError as e: - logger.error(f"Import error: {e}") - logger.error(" Please ensure all dependencies are installed: pip install -r requirements.txt") - return False - except Exception as e: - logger.error(f"Unexpected error: {e}") - return False - - -async def main(): - """Main demonstration function.""" - print("=" * 60) - print("LYRA AI - CORE SYSTEM DEMONSTRATION") - print("=" * 60) - print() - - success = await demonstrate_lyra() - - print() - print("=" * 60) - if success: - print("DEMONSTRATION SUCCESSFUL!") - print("Lyra AI is ready for full deployment with Discord integration.") - print() - print("Next steps:") - print("1. Set up PostgreSQL and Redis databases") - print("2. Configure Discord bot token in .env file") - print("3. Run: python -m lyra.main") - else: - print("DEMONSTRATION FAILED!") - print("Please check the error messages above and ensure dependencies are installed.") - print("=" * 60) - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/test_database_connections.py b/test_database_connections.py deleted file mode 100644 index 72ae014..0000000 --- a/test_database_connections.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -Test database connections for Lyra AI. - -This script tests both PostgreSQL and Redis connections to ensure -they're properly configured before running Lyra. -""" - -import asyncio -import sys -import os -from pathlib import Path - -# Add the project root to Python path -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) - -async def test_postgresql(): - """Test PostgreSQL connection.""" - print("Testing PostgreSQL connection...") - - try: - import asyncpg - from lyra.config import config - - # Parse the database URL - database_url = config.database_url - print(f"Connecting to: {database_url.replace('your_password_here', '****')}") - - # Test connection - conn = await asyncpg.connect(database_url) - - # Test query - result = await conn.fetchval('SELECT version()') - print(f"โ PostgreSQL connected successfully!") - print(f" Version: {result}") - - # Test creating a simple table - await conn.execute(''' - CREATE TABLE IF NOT EXISTS test_connection ( - id SERIAL PRIMARY KEY, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Insert test data - await conn.execute( - 'INSERT INTO test_connection (message) VALUES ($1)', - 'Lyra database test successful' - ) - - # Verify test data - test_result = await conn.fetchval( - 'SELECT message FROM test_connection ORDER BY id DESC LIMIT 1' - ) - print(f" Test query result: {test_result}") - - # Clean up test table - await conn.execute('DROP TABLE IF EXISTS test_connection') - - await conn.close() - return True - - except ImportError: - print("โ asyncpg not installed. Run: pip install asyncpg") - return False - except Exception as e: - print(f"โ PostgreSQL connection failed: {e}") - print("\n๐ก Troubleshooting tips:") - print(" 1. Make sure PostgreSQL is running") - print(" 2. Check your password in .env file") - print(" 3. Create the 'lyra' database if it doesn't exist") - print(" 4. Verify PostgreSQL is listening on port 5432") - return False - - -async def test_redis(): - """Test Redis connection.""" - print("\nTesting Redis connection...") - - try: - import redis.asyncio as redis - from lyra.config import config - - # Connect to Redis - redis_client = redis.from_url(config.redis_url) - - # Test ping - response = await redis_client.ping() - print(f"โ Redis connected successfully!") - print(f" Ping response: {response}") - - # Test basic operations - await redis_client.set('lyra_test', 'Hello from Lyra AI!') - test_value = await redis_client.get('lyra_test') - print(f" Test value: {test_value.decode('utf-8')}") - - # Clean up - await redis_client.delete('lyra_test') - await redis_client.close() - - return True - - except ImportError: - print("โ redis not installed. Run: pip install redis") - return False - except Exception as e: - print(f"โ Redis connection failed: {e}") - print("\n๐ก Troubleshooting tips:") - print(" 1. Make sure Redis/Memurai is running") - print(" 2. Check if port 6379 is available") - print(" 3. Try restarting the Redis service") - return False - - -async def create_lyra_database(): - """Create the Lyra database if it doesn't exist.""" - print("\nCreating Lyra database...") - - try: - import asyncpg - from lyra.config import config - - # Connect to postgres database (default) - base_url = config.database_url.replace('/lyra', '/postgres') - conn = await asyncpg.connect(base_url) - - # Check if lyra database exists - db_exists = await conn.fetchval( - "SELECT 1 FROM pg_database WHERE datname = 'lyra'" - ) - - if not db_exists: - await conn.execute('CREATE DATABASE lyra') - print("โ Created 'lyra' database") - else: - print("โ 'lyra' database already exists") - - await conn.close() - return True - - except Exception as e: - print(f"โ Failed to create database: {e}") - return False - - -async def main(): - """Main test function.""" - print("=" * 60) - print("LYRA AI - DATABASE CONNECTION TESTS") - print("=" * 60) - - # Load environment variables - try: - from dotenv import load_dotenv - load_dotenv() - print("โ Environment variables loaded") - except ImportError: - print("โ ๏ธ python-dotenv not found - using system environment") - - # Check if database URL is configured - if 'your_password_here' in os.getenv('DATABASE_URL', ''): - print("\nโ Please update your PostgreSQL password in .env file") - print(" Replace 'your_password_here' with your actual PostgreSQL password") - return False - - # Test database creation - await create_lyra_database() - - # Test connections - postgresql_ok = await test_postgresql() - redis_ok = await test_redis() - - print("\n" + "=" * 60) - if postgresql_ok and redis_ok: - print("๐ ALL DATABASE TESTS PASSED!") - print("Lyra is ready for full deployment with database support.") - print("\nNext step: Run 'python -m lyra.main' to start Lyra") - else: - print("โ SOME TESTS FAILED") - print("Please fix the issues above before running Lyra") - print("=" * 60) - - return postgresql_ok and redis_ok - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/test_discord_bot.py b/test_discord_bot.py deleted file mode 100644 index ccb8a9b..0000000 --- a/test_discord_bot.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Test Discord bot connection for Lyra AI. - -This script tests if the Discord token is valid and the bot can connect. -""" - -import asyncio -import os -import discord -from discord.ext import commands -from dotenv import load_dotenv - -# Load environment variables -load_dotenv() - -async def test_discord_connection(): - """Test Discord bot connection.""" - print("Testing Discord bot connection...") - - # Get token from environment - token = os.getenv('DISCORD_TOKEN') - guild_id = os.getenv('DISCORD_GUILD_ID') - - if not token: - print("โ DISCORD_TOKEN not found in .env file") - return False - - if token == 'your_discord_token_here': - print("โ Please update DISCORD_TOKEN in .env file with your actual bot token") - return False - - print(f"Token starts with: {token[:20]}...") - if guild_id: - print(f"Guild ID: {guild_id}") - - # Create bot instance - intents = discord.Intents.default() - intents.message_content = True - intents.guilds = True - - bot = commands.Bot( - command_prefix='!lyra ', - intents=intents, - description="Lyra AI Test Bot" - ) - - # Test connection - connection_successful = False - - @bot.event - async def on_ready(): - nonlocal connection_successful - print(f"โ Bot connected successfully!") - print(f" Bot name: {bot.user.name}") - print(f" Bot ID: {bot.user.id}") - print(f" Connected to {len(bot.guilds)} server(s)") - - if bot.guilds: - for guild in bot.guilds: - print(f" - Server: {guild.name} (ID: {guild.id})") - - connection_successful = True - await bot.close() - - @bot.event - async def on_connect(): - print("๐ก Bot connected to Discord") - - @bot.event - async def on_disconnect(): - print("๐ก Bot disconnected from Discord") - - try: - print("๐ Attempting to connect to Discord...") - await bot.start(token) - return connection_successful - - except discord.LoginFailure: - print("โ Login failed - Invalid bot token") - print("๐ก Steps to fix:") - print(" 1. Go to https://discord.com/developers/applications") - print(" 2. Select your application") - print(" 3. Go to 'Bot' section") - print(" 4. Reset and copy the new token") - print(" 5. Update DISCORD_TOKEN in .env file") - return False - - except discord.HTTPException as e: - print(f"โ HTTP error: {e}") - return False - - except Exception as e: - print(f"โ Connection failed: {e}") - return False - -async def main(): - """Main test function.""" - print("=" * 60) - print("LYRA AI - DISCORD BOT CONNECTION TEST") - print("=" * 60) - - success = await test_discord_connection() - - print("\n" + "=" * 60) - if success: - print("๐ DISCORD CONNECTION TEST PASSED!") - print("Your Discord bot is ready to use!") - print("\nNext steps:") - print("1. Invite bot to your server (if not already done)") - print("2. Run: python -m lyra.main") - print("3. Test Lyra in Discord by mentioning her or DMing") - else: - print("โ DISCORD CONNECTION TEST FAILED") - print("Please check the error messages above and fix the issues") - print("=" * 60) - - return success - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/test_simple_databases.py b/test_simple_databases.py deleted file mode 100644 index 1f577cf..0000000 --- a/test_simple_databases.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Test simple database setup for Lyra AI using SQLite and FakeRedis. - -This provides a working database solution without complex setup. -""" - -import asyncio -import sqlite3 -import os -from pathlib import Path - -async def test_sqlite(): - """Test SQLite database connection.""" - print("Testing SQLite database...") - - try: - # Ensure data directory exists - data_dir = Path("data") - data_dir.mkdir(exist_ok=True) - - db_path = data_dir / "lyra.db" - print(f"Database path: {db_path}") - - # Test SQLite connection - conn = sqlite3.connect(str(db_path)) - cursor = conn.cursor() - - # Test creating a table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS test_connection ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Insert test data - cursor.execute( - 'INSERT INTO test_connection (message) VALUES (?)', - ('Lyra SQLite test successful',) - ) - conn.commit() - - # Verify test data - cursor.execute('SELECT message FROM test_connection ORDER BY id DESC LIMIT 1') - result = cursor.fetchone() - print(f"โ SQLite connected successfully!") - print(f" Test query result: {result[0] if result else 'No data'}") - - # Clean up test data - cursor.execute('DELETE FROM test_connection') - conn.commit() - conn.close() - - return True - - except Exception as e: - print(f"โ SQLite connection failed: {e}") - return False - -async def test_fakeredis(): - """Test FakeRedis connection.""" - print("\nTesting FakeRedis...") - - try: - import fakeredis.aioredis as redis - - # Connect to FakeRedis (in-memory) - redis_client = redis.FakeRedis() - - # Test ping - response = await redis_client.ping() - print(f"โ FakeRedis connected successfully!") - print(f" Ping response: {response}") - - # Test basic operations - await redis_client.set('lyra_test', 'Hello from Lyra AI!') - test_value = await redis_client.get('lyra_test') - print(f" Test value: {test_value.decode('utf-8')}") - - # Clean up - await redis_client.delete('lyra_test') - await redis_client.close() - - return True - - except ImportError: - print("โ fakeredis not installed. Run: pip install fakeredis") - return False - except Exception as e: - print(f"โ FakeRedis connection failed: {e}") - return False - -async def test_lyra_with_simple_databases(): - """Test Lyra with the simple database setup.""" - print("\nTesting Lyra with simple databases...") - - try: - # Try to import and initialize Lyra - from lyra.core.lyra_model import LyraModel - import torch - - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - - # Create a minimal Lyra model - lyra = LyraModel( - vocab_size=100, # Very small for testing - embed_dim=128, # Smaller - num_layers=2, # Fewer layers - num_heads=4, # Fewer heads - device=device, - enable_evolution=True - ) - - print("โ Lyra initialized successfully with simple databases!") - print(f" Device: {device}") - print(f" Model size: {sum(p.numel() for p in lyra.parameters()):,} parameters") - - return True - - except Exception as e: - print(f"โ Lyra initialization failed: {e}") - return False - -def create_env_backup(): - """Create a backup of the current .env file.""" - if os.path.exists('.env'): - import shutil - shutil.copy('.env', '.env.backup') - print("โ Created .env backup") - -async def main(): - """Main test function.""" - print("=" * 60) - print("LYRA AI - SIMPLE DATABASE SETUP TEST") - print("=" * 60) - - create_env_backup() - - # Test individual components - sqlite_ok = await test_sqlite() - redis_ok = await test_fakeredis() - lyra_ok = await test_lyra_with_simple_databases() - - print("\n" + "=" * 60) - if sqlite_ok and redis_ok and lyra_ok: - print("๐ ALL TESTS PASSED!") - print("Lyra is ready to run with simple databases!") - print("\nBenefits of this setup:") - print("โ No complex database server setup required") - print("โ SQLite provides full SQL features") - print("โ FakeRedis provides Redis compatibility") - print("โ Everything works offline") - print("โ Easy to backup (just copy the data/ folder)") - print("\nNext step: Run 'python -m lyra.main' to start Lyra") - else: - print("โ SOME TESTS FAILED") - print("Check the error messages above") - print("=" * 60) - - return sqlite_ok and redis_ok and lyra_ok - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 8bf13e7..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Tests for Lyra AI System \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 1bc22bb..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -Test configuration and fixtures for Lyra tests. -""" - -import pytest -import torch -import numpy as np -from pathlib import Path -import tempfile -import asyncio -from unittest.mock import Mock, AsyncMock -from typing import Dict, Any, Optional - -from lyra.config import LyraConfig -from lyra.personality.matrix import PersonalityMatrix -from lyra.personality.traits import OCEANTraits -from lyra.emotions.system import EmotionalSystem, EmotionalState -from lyra.core.self_evolution import SelfEvolutionEngine -from lyra.core.thinking_agent import ThinkingAgent - - -@pytest.fixture -def device(): - """Get appropriate device for testing.""" - return torch.device("cpu") # Use CPU for tests to avoid GPU dependencies - - -@pytest.fixture -def mock_config(): - """Mock configuration for testing.""" - config = Mock(spec=LyraConfig) - config.vocab_size = 1000 - config.hidden_size = 128 - config.num_layers = 2 - config.num_heads = 2 - config.context_length = 256 - config.max_memory_gb = 1.0 - config.personality_update_frequency = 10 - config.emotion_decay_rate = 0.95 - config.project_root = Path(tempfile.mkdtemp()) - config.data_dir = config.project_root / "data" - config.models_dir = config.project_root / "models" - config.logs_dir = config.project_root / "logs" - return config - - -@pytest.fixture -def sample_ocean_traits(): - """Sample OCEAN personality traits for testing.""" - return OCEANTraits( - openness=0.7, - conscientiousness=0.6, - extraversion=0.8, - agreeableness=0.9, - neuroticism=0.3 - ) - - -@pytest.fixture -def sample_emotional_state(): - """Sample emotional state for testing.""" - return EmotionalState( - joy=0.7, - trust=0.8, - curiosity=0.9, - emotional_intensity=0.6, - emotional_stability=0.7 - ) - - -@pytest.fixture -def personality_matrix(device): - """Create personality matrix for testing.""" - matrix = PersonalityMatrix(device=device, enable_self_modification=True) - return matrix - - -@pytest.fixture -def emotional_system(device): - """Create emotional system for testing.""" - system = EmotionalSystem( - input_dim=128, - emotion_dim=19, - memory_capacity=100, - device=device - ) - return system - - -@pytest.fixture -def self_evolution_engine(device): - """Create self-evolution engine for testing.""" - engine = SelfEvolutionEngine( - model_dim=128, - evolution_rate=0.01, - adaptation_threshold=0.7, - device=device - ) - return engine - - -@pytest.fixture -def thinking_agent(device): - """Create thinking agent for testing.""" - agent = ThinkingAgent( - model_dim=128, - thought_types=8, - max_thought_depth=3, - device=device - ) - return agent - - -@pytest.fixture -def sample_context_embedding(device): - """Sample context embedding tensor.""" - return torch.randn(1, 10, 128, device=device) - - -@pytest.fixture -def sample_personality_tensor(device): - """Sample personality state tensor.""" - return torch.rand(1, 24, device=device) - - -@pytest.fixture -def sample_emotional_tensor(device): - """Sample emotional state tensor.""" - return torch.rand(1, 19, device=device) - - -@pytest.fixture -def sample_conversation_history(): - """Sample conversation history for testing.""" - return [ - "Hello, how are you today?", - "I'm doing well, thank you! How can I help you?", - "I'm working on a project and feeling a bit stuck.", - "I'd be happy to help! What kind of project are you working on?" - ] - - -@pytest.fixture -def sample_user_message(): - """Sample user message for testing.""" - return "I'm really excited about this new AI project I'm working on!" - - -@pytest.fixture -def sample_book_content(): - """Sample book content for knowledge processing tests.""" - return """ - The Art of Science - - Chapter 1: Introduction to Scientific Method - - Science is a systematic approach to understanding the natural world through - observation, hypothesis formation, and experimentation. The scientific method - has been the foundation of human progress for centuries. - - The key principles of scientific inquiry include: - 1. Observation of natural phenomena - 2. Formation of testable hypotheses - 3. Design and execution of controlled experiments - 4. Analysis of results and data - 5. Drawing conclusions based on evidence - - Scientists throughout history have used these principles to make groundbreaking - discoveries that have shaped our understanding of the universe. From Newton's - laws of motion to Einstein's theory of relativity, scientific inquiry has - revealed the fundamental principles governing our reality. - - Chapter 2: The Role of Hypothesis in Science - - A hypothesis is a proposed explanation for observed phenomena that can be - tested through experimentation. Good hypotheses are specific, testable, - and based on existing knowledge. - """ - - -@pytest.fixture -async def mock_database_manager(): - """Mock database manager for testing.""" - manager = AsyncMock() - manager.is_connected = True - manager.async_session = AsyncMock() - manager.create_user = AsyncMock() - manager.get_user_by_discord_id = AsyncMock() - manager.store_conversation = AsyncMock() - manager.get_recent_conversations = AsyncMock(return_value=[]) - manager.store_personality_state = AsyncMock() - manager.store_emotional_memory = AsyncMock() - manager.store_knowledge = AsyncMock() - return manager - - -@pytest.fixture -def temp_directory(): - """Create temporary directory for testing.""" - with tempfile.TemporaryDirectory() as temp_dir: - yield Path(temp_dir) - - -@pytest.fixture -def sample_gutenberg_book(): - """Sample Gutenberg book data for testing.""" - from lyra.knowledge.gutenberg_crawler import GutenbergBook - - return GutenbergBook( - id=12345, - title="Sample Public Domain Book", - author="Test Author", - language="en", - category="Fiction", - url="https://www.gutenberg.org/ebooks/12345", - file_format="txt", - download_url="https://www.gutenberg.org/files/12345/12345-0.txt", - metadata={"test": True} - ) - - -class AsyncContextManager: - """Helper for testing async context managers.""" - - def __init__(self, return_value=None): - self.return_value = return_value - - async def __aenter__(self): - return self.return_value - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - -@pytest.fixture -def async_context_manager(): - """Factory for creating async context managers.""" - return AsyncContextManager - - -# Event loop fixture for async tests -@pytest.fixture(scope="session") -def event_loop(): - """Create an instance of the default event loop for the test session.""" - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - -# Utility functions for tests -def assert_tensor_shape(tensor: torch.Tensor, expected_shape: tuple, name: str = "tensor"): - """Assert that a tensor has the expected shape.""" - assert tensor.shape == expected_shape, ( - f"{name} shape mismatch: expected {expected_shape}, got {tensor.shape}" - ) - - -def assert_tensor_range(tensor: torch.Tensor, min_val: float, max_val: float, name: str = "tensor"): - """Assert that tensor values are within expected range.""" - actual_min = tensor.min().item() - actual_max = tensor.max().item() - assert min_val <= actual_min, f"{name} minimum {actual_min} below expected {min_val}" - assert actual_max <= max_val, f"{name} maximum {actual_max} above expected {max_val}" - - -def create_mock_response(status: int = 200, text: str = "", json_data: Optional[Dict[str, Any]] = None): - """Create a mock HTTP response.""" - response = Mock() - response.status = status - response.text = AsyncMock(return_value=text) - if json_data: - response.json = AsyncMock(return_value=json_data) - return response \ No newline at end of file diff --git a/tests/test_core_systems.py b/tests/test_core_systems.py deleted file mode 100644 index c65ebe0..0000000 --- a/tests/test_core_systems.py +++ /dev/null @@ -1,496 +0,0 @@ -""" -Tests for core AI systems including transformer, self-evolution, and thinking agent. -""" - -import pytest -import torch -import numpy as np - -from lyra.core.transformer import LyraTransformer, LyraTransformerBlock -from lyra.core.attention import SelfEvolvingAttention, MultiHeadAttention -from lyra.core.self_evolution import SelfEvolutionEngine, EvolutionMetrics -from lyra.core.thinking_agent import ThinkingAgent, ThoughtProcess -from tests.conftest import assert_tensor_shape, assert_tensor_range - - -class TestSelfEvolvingAttention: - """Tests for self-evolving attention mechanism.""" - - def test_attention_initialization(self, device): - """Test attention mechanism initialization.""" - attention = SelfEvolvingAttention( - embed_dim=128, - num_heads=8, - dropout=0.1, - device=device - ) - - assert attention.embed_dim == 128 - assert attention.num_heads == 8 - assert attention.head_dim == 16 # 128 / 8 - - def test_attention_forward_pass(self, device): - """Test attention forward pass.""" - attention = SelfEvolvingAttention( - embed_dim=128, - num_heads=8, - device=device - ) - - batch_size, seq_len = 2, 10 - x = torch.randn(batch_size, seq_len, 128, device=device) - - output, weights, evolution_info = attention( - query=x, key=x, value=x, evolve=True - ) - - assert_tensor_shape(output, (batch_size, seq_len, 128), "attention output") - assert_tensor_shape(weights, (batch_size, 8, seq_len, seq_len), "attention weights") - assert isinstance(evolution_info, dict) - - def test_attention_evolution_learning(self, device): - """Test attention pattern evolution from feedback.""" - attention = SelfEvolvingAttention( - embed_dim=128, - num_heads=8, - device=device - ) - - # Store initial evolution matrix - initial_evolution = attention.attention_evolution.clone() - - # Apply positive feedback - attention.evolve_attention_patterns(feedback_signal=0.8) - - # Evolution matrix should change - assert not torch.equal(initial_evolution, attention.attention_evolution) - - def test_attention_diversity_calculation(self, device): - """Test attention diversity measurement.""" - attention = SelfEvolvingAttention( - embed_dim=128, - num_heads=8, - device=device - ) - - # Get baseline diversity - diversity = attention.get_attention_diversity() - assert isinstance(diversity, float) - assert 0.0 <= diversity <= 10.0 # Reasonable entropy range - - -class TestLyraTransformerBlock: - """Tests for Lyra transformer block.""" - - def test_transformer_block_initialization(self, device): - """Test transformer block initialization.""" - block = LyraTransformerBlock( - embed_dim=128, - num_heads=8, - ff_dim=512, - dropout=0.1, - use_evolution=True, - device=device - ) - - assert block.embed_dim == 128 - assert block.num_heads == 8 - assert block.use_evolution is True - - def test_transformer_block_forward(self, device): - """Test transformer block forward pass.""" - block = LyraTransformerBlock( - embed_dim=128, - num_heads=8, - ff_dim=512, - use_evolution=True, - device=device - ) - - batch_size, seq_len = 2, 10 - x = torch.randn(batch_size, seq_len, 128, device=device) - emotional_state = torch.rand(batch_size, 19, device=device) - - output, layer_info = block( - x=x, - emotional_state=emotional_state, - evolve=True - ) - - assert_tensor_shape(output, (batch_size, seq_len, 128), "transformer block output") - assert isinstance(layer_info, dict) - assert 'layer_id' in layer_info - assert 'attention_entropy' in layer_info - - def test_transformer_block_evolution_from_feedback(self, device): - """Test block evolution from user feedback.""" - block = LyraTransformerBlock( - embed_dim=128, - num_heads=8, - ff_dim=512, - use_evolution=True, - device=device - ) - - initial_adaptation = float(block.adaptation_strength) - - # Apply positive feedback - block.evolve_from_feedback(feedback_signal=0.9) - - # Adaptation strength should change - new_adaptation = float(block.adaptation_strength) - assert new_adaptation != initial_adaptation - - -class TestLyraTransformer: - """Tests for the complete Lyra transformer model.""" - - def test_transformer_initialization(self, device): - """Test transformer model initialization.""" - model = LyraTransformer( - vocab_size=1000, - embed_dim=128, - num_layers=4, - num_heads=8, - ff_dim=512, - max_len=256, - use_evolution=True, - device=device - ) - - assert model.vocab_size == 1000 - assert model.embed_dim == 128 - assert model.num_layers == 4 - assert len(model.layers) == 4 - - def test_transformer_forward_pass(self, device): - """Test transformer forward pass.""" - model = LyraTransformer( - vocab_size=1000, - embed_dim=128, - num_layers=2, - num_heads=8, - ff_dim=512, - device=device - ) - - batch_size, seq_len = 2, 10 - input_ids = torch.randint(0, 1000, (batch_size, seq_len), device=device) - emotional_state = torch.rand(batch_size, 19, device=device) - - logits, model_info = model( - input_ids=input_ids, - emotional_state=emotional_state, - evolve=True - ) - - assert_tensor_shape(logits, (batch_size, seq_len, 1000), "transformer logits") - assert isinstance(model_info, dict) - assert 'layer_info' in model_info - assert 'evolution_active' in model_info - - def test_transformer_generation(self, device): - """Test autoregressive text generation.""" - model = LyraTransformer( - vocab_size=100, # Small vocab for testing - embed_dim=64, # Small model for speed - num_layers=2, - num_heads=4, - ff_dim=256, - device=device - ) - - batch_size, input_len = 1, 5 - input_ids = torch.randint(0, 100, (batch_size, input_len), device=device) - - generated_ids, generation_info = model.generate( - input_ids=input_ids, - max_new_tokens=10, - temperature=1.0, - top_k=10, - evolve=False # Disable evolution for faster testing - ) - - expected_len = input_len + generation_info['tokens_generated'] - assert generated_ids.shape == (batch_size, expected_len) - assert 'average_confidence' in generation_info - assert 'generation_steps' in generation_info - - def test_transformer_evolution_from_conversation(self, device): - """Test model evolution from conversation feedback.""" - model = LyraTransformer( - vocab_size=100, - embed_dim=64, - num_layers=2, - num_heads=4, - use_evolution=True, - device=device - ) - - initial_feedback = model.last_feedback - - # Apply conversation feedback - model.evolve_from_conversation(feedback_signal=0.8) - - # Feedback should be recorded - assert model.last_feedback != initial_feedback - - def test_transformer_model_stats(self, device): - """Test model statistics generation.""" - model = LyraTransformer( - vocab_size=100, - embed_dim=64, - num_layers=2, - num_heads=4, - use_evolution=True, - device=device - ) - - stats = model.get_model_stats() - - required_keys = [ - 'generation_count', 'last_feedback', 'model_parameters', - 'trainable_parameters' - ] - - for key in required_keys: - assert key in stats - - # With evolution enabled, should have evolution stats - assert 'layer_evolution' in stats - - -class TestSelfEvolutionEngine: - """Tests for the self-evolution system.""" - - def test_evolution_engine_initialization(self, device): - """Test evolution engine initialization.""" - engine = SelfEvolutionEngine( - model_dim=128, - evolution_rate=0.01, - adaptation_threshold=0.7, - device=device - ) - - assert engine.model_dim == 128 - assert engine.evolution_rate == 0.01 - assert engine.adaptation_threshold == 0.7 - assert len(engine.experience_buffer) == 0 - - def test_evolution_metrics_initialization(self): - """Test evolution metrics initialization.""" - metrics = EvolutionMetrics() - - assert metrics.conversation_satisfaction == 0.0 - assert metrics.learning_rate_adaptation == 0.0 - assert 0.0 <= metrics.personality_drift <= 1.0 - - def test_evolution_forward_pass(self, self_evolution_engine, device): - """Test evolution engine forward pass.""" - batch_size, seq_len, dim = 2, 10, 128 - - current_state = torch.randn(batch_size, seq_len, dim, device=device) - context = torch.randn(batch_size, seq_len, dim, device=device) - - evolved_state, evolution_info = self_evolution_engine( - current_state=current_state, - context=context - ) - - assert_tensor_shape(evolved_state, (batch_size, seq_len, dim), "evolved state") - assert isinstance(evolution_info, dict) - assert 'state_change_magnitude' in evolution_info - assert 'adaptive_lr' in evolution_info - - def test_evolution_from_conversation(self, self_evolution_engine, device): - """Test evolution from conversation interaction.""" - conversation_embedding = torch.randn(10, 128, device=device) - user_satisfaction = 0.8 - emotional_context = {'joy': 0.7, 'trust': 0.8} - - evolved_embedding, evolution_info = self_evolution_engine.evolve_from_conversation( - conversation_embedding=conversation_embedding, - user_satisfaction=user_satisfaction, - emotional_context=emotional_context - ) - - assert_tensor_shape(evolved_embedding, (10, 128), "evolved conversation embedding") - assert isinstance(evolution_info, dict) - - # Metrics should be updated - assert self_evolution_engine.metrics.conversation_satisfaction > 0.0 - - def test_long_term_evolution(self, self_evolution_engine): - """Test long-term evolution consolidation.""" - # Add some fake experiences - for _ in range(150): # Above the 100 threshold - fake_experience = { - 'state': torch.randn(1, 10, 128), - 'context': torch.randn(1, 10, 128), - 'evolution': torch.randn(1, 10, 128), - 'meta_params': torch.randn(1, 5), - 'timestamp': torch.rand(1) - } - self_evolution_engine.experience_buffer.append(fake_experience) - - initial_plasticity = self_evolution_engine.personality_plasticity - - # Trigger long-term evolution - self_evolution_engine.long_term_evolution() - - # Should have analyzed and potentially adjusted parameters - assert len(self_evolution_engine.experience_buffer) >= 100 - - def test_evolution_summary(self, self_evolution_engine): - """Test evolution summary generation.""" - summary = self_evolution_engine.get_evolution_summary() - - if summary.get("status") != "no_evolution_data": - required_keys = [ - 'total_evolution_steps', 'current_metrics', - 'personality_plasticity', 'adaptive_learning_rate' - ] - - for key in required_keys: - assert key in summary - - def test_evolution_state_persistence(self, self_evolution_engine, temp_directory): - """Test saving and loading evolution state.""" - save_path = temp_directory / "evolution_test.json" - - # Modify some state - self_evolution_engine.metrics.conversation_satisfaction = 0.8 - self_evolution_engine.personality_plasticity = 0.2 - - # Save state - self_evolution_engine.save_evolution_state(save_path) - assert save_path.exists() - - # Create new engine and load - new_engine = SelfEvolutionEngine(device=self_evolution_engine.device) - new_engine.load_evolution_state(save_path) - - assert abs(new_engine.metrics.conversation_satisfaction - 0.8) < 0.01 - assert abs(new_engine.personality_plasticity - 0.2) < 0.01 - - -class TestThinkingAgent: - """Tests for the behind-the-scenes thinking agent.""" - - def test_thinking_agent_initialization(self, device): - """Test thinking agent initialization.""" - agent = ThinkingAgent( - model_dim=128, - thought_types=8, - max_thought_depth=5, - device=device - ) - - assert agent.model_dim == 128 - assert agent.thought_types == 8 - assert agent.max_thought_depth == 5 - assert len(agent.thought_type_names) == 8 - - def test_thought_process_creation(self): - """Test thought process object creation.""" - thought = ThoughtProcess( - thought_type="analytical", - content="I need to think about this carefully.", - confidence=0.8, - reasoning="This requires analytical thinking.", - emotional_influence=0.3, - personality_influence=0.6 - ) - - assert thought.thought_type == "analytical" - assert thought.confidence == 0.8 - assert hasattr(thought, 'timestamp') - - @pytest.mark.asyncio - async def test_thinking_agent_forward_pass(self, thinking_agent, sample_context_embedding, - sample_personality_tensor, sample_emotional_tensor, - sample_user_message): - """Test thinking agent forward pass.""" - thought_chain, thinking_info = thinking_agent( - context_embedding=sample_context_embedding, - personality_state=sample_personality_tensor, - emotional_state=sample_emotional_tensor, - user_message=sample_user_message - ) - - assert isinstance(thought_chain, list) - assert len(thought_chain) > 0 - assert all(isinstance(thought, ThoughtProcess) for thought in thought_chain) - - assert isinstance(thinking_info, dict) - assert 'total_thoughts' in thinking_info - assert 'avg_confidence' in thinking_info - assert 'thinking_time' in thinking_info - - def test_thinking_from_feedback_learning(self, thinking_agent): - """Test learning from response feedback.""" - # Create mock thought chain - thought_chain = [ - ThoughtProcess("analytical", "Test thought", 0.8, "Test reasoning"), - ThoughtProcess("empathetic", "Another thought", 0.7, "More reasoning") - ] - - initial_patterns = len(thinking_agent.thinking_patterns.get('successful_strategies', {})) - - # Apply feedback - thinking_agent.learn_from_response_feedback( - thought_chain=thought_chain, - response_quality=0.8, - user_satisfaction=0.9 - ) - - # Should have recorded the pattern - final_patterns = len(thinking_agent.thinking_patterns.get('successful_strategies', {})) - assert final_patterns >= initial_patterns - - def test_thinking_summary_generation(self, thinking_agent): - """Test thinking summary generation.""" - # Add some fake history - fake_thought = ThoughtProcess("creative", "Test", 0.7, "Test reasoning") - thinking_agent.thought_history = [fake_thought] * 10 - - summary = thinking_agent.get_thinking_summary() - - if summary.get('status') != 'no_thinking_history': - required_keys = [ - 'total_thoughts', 'recent_thoughts', 'thought_type_distribution', - 'avg_confidence' - ] - - for key in required_keys: - assert key in summary - - def test_optimal_thinking_strategy(self, thinking_agent): - """Test optimal thinking strategy determination.""" - # Test with unknown context (should return default) - strategy = thinking_agent.get_optimal_thinking_strategy("unknown_context") - assert isinstance(strategy, list) - assert len(strategy) > 0 - assert all(isinstance(thought_type, str) for thought_type in strategy) - - def test_internal_dialogue_simulation(self, thinking_agent): - """Test internal dialogue simulation.""" - scenario = "User is asking for help with a difficult problem." - - thought_chain = thinking_agent.simulate_internal_dialogue(scenario) - - assert isinstance(thought_chain, list) - assert len(thought_chain) > 0 - assert all(isinstance(thought, ThoughtProcess) for thought in thought_chain) - - def test_thinking_patterns_export(self, thinking_agent): - """Test thinking patterns export.""" - export_data = thinking_agent.export_thinking_patterns() - - required_keys = [ - 'thinking_patterns', 'thought_history_summary', - 'thought_type_names', 'total_thinking_experiences' - ] - - for key in required_keys: - assert key in export_data \ No newline at end of file diff --git a/tests/test_emotional_system.py b/tests/test_emotional_system.py deleted file mode 100644 index c343599..0000000 --- a/tests/test_emotional_system.py +++ /dev/null @@ -1,452 +0,0 @@ -""" -Tests for the emotional intelligence system. -""" - -import pytest -import torch -import numpy as np -from datetime import datetime, timedelta - -from lyra.emotions.system import ( - EmotionalSystem, EmotionalState, EmotionMemory -) -from lyra.emotions.expressions import EmotionalExpressionEngine -from tests.conftest import assert_tensor_shape, assert_tensor_range - - -class TestEmotionalState: - """Tests for emotional state representation.""" - - def test_emotional_state_initialization(self): - """Test emotional state initialization with default values.""" - state = EmotionalState() - - # Check that all emotions are within valid range [0, 1] - emotions = [ - state.joy, state.sadness, state.anger, state.fear, - state.surprise, state.disgust, state.trust, state.anticipation, - state.love, state.guilt, state.shame, state.pride, - state.jealousy, state.hope, state.despair, state.curiosity - ] - - for emotion in emotions: - assert 0.0 <= emotion <= 1.0 - - # Check meta-emotional states - assert 0.0 <= state.emotional_intensity <= 1.0 - assert 0.0 <= state.emotional_stability <= 1.0 - assert 0.0 <= state.emotional_clarity <= 1.0 - - # Check timestamp is set - assert state.timestamp is not None - assert isinstance(state.timestamp, datetime) - - def test_emotional_state_to_tensor(self, sample_emotional_state, device): - """Test conversion to tensor.""" - tensor = sample_emotional_state.to_tensor(device) - - assert_tensor_shape(tensor, (19,), "emotional state tensor") - assert_tensor_range(tensor, 0.0, 1.0, "emotional values") - - def test_emotional_state_from_tensor(self, device): - """Test creation from tensor.""" - tensor = torch.rand(19, device=device) - state = EmotionalState.from_tensor(tensor, trigger="test") - - assert state.trigger == "test" - assert 0.0 <= state.joy <= 1.0 - assert 0.0 <= state.emotional_intensity <= 1.0 - - def test_dominant_emotion_detection(self, sample_emotional_state): - """Test dominant emotion detection.""" - emotion, intensity = sample_emotional_state.get_dominant_emotion() - - assert isinstance(emotion, str) - assert 0.0 <= intensity <= 1.0 - assert emotion in [ - 'joy', 'sadness', 'anger', 'fear', 'surprise', 'disgust', - 'trust', 'anticipation', 'love', 'guilt', 'shame', 'pride', - 'jealousy', 'hope', 'despair', 'curiosity' - ] - - def test_emotional_valence_calculation(self): - """Test emotional valence (positive/negative) calculation.""" - # Very positive state - positive_state = EmotionalState(joy=0.9, love=0.8, hope=0.9) - valence = positive_state.get_emotional_valence() - assert valence > 0.5 # Should be positive - - # Very negative state - negative_state = EmotionalState(sadness=0.9, anger=0.8, despair=0.9) - valence = negative_state.get_emotional_valence() - assert valence < -0.5 # Should be negative - - def test_emotional_arousal_calculation(self): - """Test emotional arousal (calm/excited) calculation.""" - # High arousal state - excited_state = EmotionalState(anger=0.9, surprise=0.8, joy=0.9) - arousal = excited_state.get_emotional_arousal() - assert arousal > 0.5 # Should be high arousal - - # Low arousal state - calm_state = EmotionalState(trust=0.8, sadness=0.3) - arousal = calm_state.get_emotional_arousal() - assert arousal < 0.7 # Should be lower arousal - - -class TestEmotionMemory: - """Tests for emotional memory system.""" - - def test_emotion_memory_initialization(self, sample_emotional_state): - """Test emotion memory initialization.""" - memory = EmotionMemory( - emotional_state=sample_emotional_state, - context="test interaction", - intensity=0.8, - impact_score=0.7 - ) - - assert memory.emotional_state == sample_emotional_state - assert memory.context == "test interaction" - assert memory.intensity == 0.8 - assert memory.impact_score == 0.7 - assert memory.decay_rate == 0.95 - assert hasattr(memory, 'creation_time') - - def test_memory_impact_decay(self, sample_emotional_state): - """Test memory impact decay over time.""" - memory = EmotionMemory( - emotional_state=sample_emotional_state, - context="test", - intensity=0.8, - impact_score=1.0, - decay_rate=0.9 - ) - - # Simulate time passage by modifying creation_time - memory.creation_time = datetime.now() - timedelta(hours=1) - - current_impact = memory.get_current_impact() - assert current_impact < memory.impact_score # Should decay - - def test_memory_significance_check(self, sample_emotional_state): - """Test memory significance determination.""" - # High impact memory - high_impact_memory = EmotionMemory( - emotional_state=sample_emotional_state, - context="important event", - intensity=0.9, - impact_score=0.8 - ) - assert high_impact_memory.is_significant(threshold=0.5) - - # Low impact memory after decay - low_impact_memory = EmotionMemory( - emotional_state=sample_emotional_state, - context="minor event", - intensity=0.3, - impact_score=0.1 - ) - assert not low_impact_memory.is_significant(threshold=0.5) - - -class TestEmotionalSystem: - """Tests for the core emotional system.""" - - @pytest.mark.asyncio - async def test_emotional_system_initialization(self, device): - """Test emotional system initialization.""" - system = EmotionalSystem( - input_dim=128, - emotion_dim=19, - memory_capacity=100, - device=device - ) - - assert system.device == device - assert system.emotion_dim == 19 - assert system.memory_capacity == 100 - assert isinstance(system.current_state, EmotionalState) - assert len(system.emotion_memories) == 0 - - @pytest.mark.asyncio - async def test_emotional_processing_forward_pass(self, emotional_system, - sample_context_embedding): - """Test emotional processing forward pass.""" - new_state, emotion_info = emotional_system( - context_embedding=sample_context_embedding - ) - - assert isinstance(new_state, EmotionalState) - assert isinstance(emotion_info, dict) - - # Check required keys in emotion_info - required_keys = [ - 'dominant_emotion', 'emotional_valence', 'emotional_arousal', - 'memory_influence_strength', 'emotional_maturity' - ] - for key in required_keys: - assert key in emotion_info - - @pytest.mark.asyncio - async def test_emotional_memory_storage(self, emotional_system, - sample_context_embedding): - """Test storage of significant emotional experiences.""" - # Create an emotionally significant state - significant_state = EmotionalState( - joy=0.9, - emotional_intensity=0.9, - trigger="amazing_news" - ) - - emotional_system.current_state = significant_state - - # Process context to trigger memory storage - new_state, info = emotional_system( - context_embedding=sample_context_embedding, - social_context={'trigger': 'positive_interaction'} - ) - - # Check if memory was stored - assert len(emotional_system.emotion_memories) > 0 - - @pytest.mark.asyncio - async def test_emotional_learning_from_feedback(self, emotional_system, - sample_context_embedding): - """Test learning from user feedback.""" - original_lr = float(emotional_system.emotional_learning_rate) - - # Positive feedback should increase learning rate - positive_feedback = torch.tensor([[0.9]], device=emotional_system.device) - new_state, info = emotional_system( - context_embedding=sample_context_embedding, - user_feedback=positive_feedback - ) - - assert 'feedback_received' in info - assert info['feedback_received'] > 0.7 - - # Learning rate should have been adjusted - new_lr = float(emotional_system.emotional_learning_rate) - assert new_lr != original_lr - - @pytest.mark.asyncio - async def test_emotional_regulation(self, emotional_system, - sample_context_embedding): - """Test emotional regulation in different contexts.""" - # Test with formal social context (should regulate emotions) - formal_context = { - 'formality_level': 0.9, - 'group_size': 10, - 'has_conflict': False - } - - regulated_state, info = emotional_system( - context_embedding=sample_context_embedding, - social_context=formal_context, - regulate_emotions=True - ) - - assert info['regulation_applied'] is True - - # Test without regulation - unregulated_state, info = emotional_system( - context_embedding=sample_context_embedding, - social_context=formal_context, - regulate_emotions=False - ) - - assert info['regulation_applied'] is False - - @pytest.mark.asyncio - async def test_emotional_context_for_response(self, emotional_system): - """Test generation of emotional context for responses.""" - context = emotional_system.get_emotional_context_for_response() - - required_keys = [ - 'dominant_emotion', 'emotion_intensity', 'emotional_valence', - 'emotional_arousal', 'emotional_stability', 'emotional_maturity' - ] - - for key in required_keys: - assert key in context - assert isinstance(context[key], (int, float, str)) - - def test_emotional_reaction_simulation(self, emotional_system): - """Test simulation of emotional reactions to triggers.""" - # Test different triggers - triggers = ['praise', 'criticism', 'surprise', 'threat', 'love'] - - for trigger in triggers: - reaction = emotional_system.simulate_emotional_reaction(trigger, intensity=0.8) - assert isinstance(reaction, EmotionalState) - assert reaction.trigger == trigger - assert reaction.emotional_intensity == 0.8 - - @pytest.mark.asyncio - async def test_emotional_summary(self, emotional_system): - """Test emotional system summary generation.""" - summary = emotional_system.get_emotional_summary() - - required_sections = [ - 'current_state', 'emotional_growth', 'memory_system', - 'emotional_patterns' - ] - - for section in required_sections: - assert section in summary - - # Check current state details - current_state = summary['current_state'] - assert 'dominant_emotion' in current_state - assert 'valence' in current_state - assert 'arousal' in current_state - - @pytest.mark.asyncio - async def test_emotional_persistence(self, emotional_system, temp_directory): - """Test saving and loading emotional state.""" - save_path = temp_directory / "emotional_state_test.json" - - # Modify emotional state - emotional_system.current_state.joy = 0.9 - emotional_system.emotional_maturity = 0.8 - emotional_system.emotional_experiences = 100 - - # Save state - emotional_system.save_emotional_state(save_path) - assert save_path.exists() - - # Create new system and load - new_system = EmotionalSystem(device=emotional_system.device) - new_system.load_emotional_state(save_path) - - assert abs(new_system.current_state.joy - 0.9) < 0.01 - assert abs(new_system.emotional_maturity - 0.8) < 0.01 - assert new_system.emotional_experiences == 100 - - -class TestEmotionalExpressionEngine: - """Tests for emotional expression in text.""" - - @pytest.mark.asyncio - async def test_expression_engine_initialization(self, device): - """Test expression engine initialization.""" - engine = EmotionalExpressionEngine( - vocab_size=1000, - expression_dim=128, - device=device - ) - - assert engine.vocab_size == 1000 - assert engine.expression_dim == 128 - assert engine.device == device - assert hasattr(engine, 'emotional_vocabularies') - assert hasattr(engine, 'expression_patterns') - - @pytest.mark.asyncio - async def test_emotional_expression_application(self, sample_emotional_state): - """Test application of emotional expression to text.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - - base_text = "I think this is a good idea." - - expressed_text, expression_info = engine( - text=base_text, - emotional_state=sample_emotional_state, - intensity_multiplier=1.0 - ) - - assert isinstance(expressed_text, str) - assert isinstance(expression_info, dict) - assert 'modifications' in expression_info - assert 'dominant_emotion' in expression_info - - @pytest.mark.asyncio - async def test_emotion_specific_expressions(self): - """Test different emotions produce different expressions.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - base_text = "That's interesting." - - # Test joy expression - joy_state = EmotionalState(joy=0.9, emotional_intensity=0.8) - joy_text, joy_info = engine(base_text, joy_state) - - # Test sadness expression - sad_state = EmotionalState(sadness=0.9, emotional_intensity=0.8) - sad_text, sad_info = engine(base_text, sad_state) - - # Should produce different expressions - assert joy_info['dominant_emotion'][0] != sad_info['dominant_emotion'][0] - - def test_expression_analysis(self): - """Test analysis of emotional expression in text.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - - # Test text with clear emotional indicators - emotional_text = "I'm SO excited about this!!! This is amazing!" - - analysis = engine.analyze_emotional_expression(emotional_text) - - assert 'detected_emotions' in analysis - assert 'expression_intensity' in analysis - assert 'punctuation_analysis' in analysis - - # Should detect excitement/joy - emotions = [e['emotion'] for e in analysis['detected_emotions']] - assert any(emotion in ['joy', 'excitement'] for emotion in emotions) - - def test_expression_statistics(self): - """Test expression statistics generation.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - stats = engine.get_expression_statistics() - - required_keys = [ - 'current_typo_probability', 'excitement_threshold', - 'available_emotions', 'expression_patterns' - ] - - for key in required_keys: - assert key in stats - - def test_contextual_expression_adjustments(self, sample_emotional_state): - """Test contextual adjustments for different conversation contexts.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - base_text = "I understand your concern about this issue." - - # Test formal context - formal_text, formal_info = engine( - base_text, sample_emotional_state, context='formal' - ) - - # Test casual context - casual_text, casual_info = engine( - base_text, sample_emotional_state, context='casual' - ) - - # Should apply different modifications - assert formal_info['modifications'] != casual_info['modifications'] - - def test_human_like_inconsistencies(self): - """Test human-like inconsistencies in expression.""" - engine = EmotionalExpressionEngine(device=torch.device("cpu")) - - # High arousal state should potentially add typos - excited_state = EmotionalState( - joy=0.9, - surprise=0.8, - emotional_intensity=0.9 - ) - - base_text = "This is really great news!" - - # Test multiple times to check for variability - results = [] - for _ in range(10): - expressed_text, info = engine( - base_text, excited_state, intensity_multiplier=2.0 - ) - results.append(expressed_text) - - # Should show some variation in outputs - unique_results = set(results) - assert len(unique_results) > 1 # Should have some variation \ No newline at end of file diff --git a/tests/test_knowledge_systems.py b/tests/test_knowledge_systems.py deleted file mode 100644 index ddb3221..0000000 --- a/tests/test_knowledge_systems.py +++ /dev/null @@ -1,454 +0,0 @@ -""" -Tests for knowledge acquisition and processing systems. -""" - -import pytest -import asyncio -from pathlib import Path -from unittest.mock import Mock, AsyncMock, patch -import json - -from lyra.knowledge.gutenberg_crawler import GutenbergCrawler, GutenbergBook -from lyra.knowledge.knowledge_processor import KnowledgeProcessor, ProcessedKnowledge -from tests.conftest import create_mock_response - - -class TestGutenbergBook: - """Tests for Gutenberg book representation.""" - - def test_gutenberg_book_initialization(self, sample_gutenberg_book): - """Test Gutenberg book initialization.""" - book = sample_gutenberg_book - - assert book.id == 12345 - assert book.title == "Sample Public Domain Book" - assert book.author == "Test Author" - assert book.language == "en" - assert book.category == "Fiction" - assert book.copyright_status == "public_domain" - assert book.quality_score == 0.8 - assert book.metadata is not None - - def test_gutenberg_book_post_init(self): - """Test book post-initialization.""" - book = GutenbergBook( - id=1, - title="Test", - author="Author", - language="en", - category="Test", - url="http://test.com", - file_format="txt", - download_url="http://test.com/file.txt" - ) - - assert book.metadata == {} # Should initialize empty dict - - -class TestGutenbergCrawler: - """Tests for the Gutenberg crawler.""" - - def test_crawler_initialization(self): - """Test crawler initialization.""" - crawler = GutenbergCrawler( - base_url="https://www.gutenberg.org", - rate_limit=1.0, - max_concurrent=2 - ) - - assert crawler.base_url == "https://www.gutenberg.org" - assert crawler.rate_limit == 1.0 - assert crawler.max_concurrent == 2 - assert len(crawler.crawled_books) == 0 - assert len(crawler.failed_downloads) == 0 - - @pytest.mark.asyncio - async def test_crawler_async_context_manager(self): - """Test crawler as async context manager.""" - with patch.object(GutenbergCrawler, '_verify_gutenberg_access', new_callable=AsyncMock): - async with GutenbergCrawler() as crawler: - assert crawler.session is not None - - @pytest.mark.asyncio - async def test_book_details_extraction(self): - """Test extraction of book details.""" - crawler = GutenbergCrawler() - - # Mock HTML content - mock_html = """ - - Test Book - - Test Author - Language: - English - Download TXT - - - """ - - with patch.object(crawler, '_rate_limited_request') as mock_request: - mock_response = Mock() - mock_response.status = 200 - mock_response.text = AsyncMock(return_value=mock_html) - mock_request.return_value = mock_response - - book = await crawler._get_book_details(123, "Test Book", "Fiction") - - assert book is not None - assert book.id == 123 - assert book.title == "Test Book" - assert book.category == "Fiction" - - def test_download_appropriateness_check(self, sample_gutenberg_book): - """Test checking if a book is appropriate for download.""" - crawler = GutenbergCrawler() - - # Should be appropriate (public domain, allowed format) - assert crawler._is_download_appropriate(sample_gutenberg_book) is True - - # Test with excluded language - crawler.excluded_languages = ['en'] - assert crawler._is_download_appropriate(sample_gutenberg_book) is False - - # Test with disallowed format - crawler.excluded_languages = [] - sample_gutenberg_book.file_format = 'pdf' - assert crawler._is_download_appropriate(sample_gutenberg_book) is False - - # Test with non-public domain - sample_gutenberg_book.file_format = 'txt' - sample_gutenberg_book.copyright_status = 'copyrighted' - assert crawler._is_download_appropriate(sample_gutenberg_book) is False - - @pytest.mark.asyncio - async def test_legal_validation(self, sample_gutenberg_book): - """Test legal status validation.""" - crawler = GutenbergCrawler() - - # Public domain book should be valid - is_valid = await crawler.validate_legal_status(sample_gutenberg_book) - assert is_valid is True - - # Test with non-public domain - sample_gutenberg_book.copyright_status = "copyrighted" - is_valid = await crawler.validate_legal_status(sample_gutenberg_book) - assert is_valid is True # Still returns True for Gutenberg books - - def test_file_format_determination(self): - """Test file format determination from URL.""" - crawler = GutenbergCrawler() - - test_cases = [ - ("http://example.com/book.txt", "txt"), - ("http://example.com/book.html", "html"), - ("http://example.com/book.epub", "epub"), - ("http://example.com/book", "txt") # Default - ] - - for url, expected_format in test_cases: - result = crawler._determine_file_format(url) - assert result == expected_format - - def test_download_statistics(self): - """Test download statistics generation.""" - crawler = GutenbergCrawler() - - # Add some mock data - book1 = GutenbergBook(1, "Book 1", "Author 1", "en", "Fiction", - "url1", "txt", "download1", quality_score=0.8) - book2 = GutenbergBook(2, "Book 2", "Author 2", "fr", "Science", - "url2", "html", "download2", quality_score=0.9) - - crawler.crawled_books = {1: book1, 2: book2} - crawler.failed_downloads = [3, 4] - - stats = crawler.get_download_statistics() - - assert stats['total_discovered'] == 2 - assert stats['failed_downloads'] == 2 - assert stats['success_rate'] == 0.5 # 2 success, 2 failures - assert 'en' in stats['languages_discovered'] - assert 'fr' in stats['languages_discovered'] - assert 'Fiction' in stats['categories_discovered'] - assert 'Science' in stats['categories_discovered'] - - @pytest.mark.asyncio - async def test_book_recommendations(self): - """Test book recommendation generation.""" - crawler = GutenbergCrawler() - - with patch.object(crawler, '_discover_books_in_category') as mock_discover: - async def mock_generator(category, languages): - if category == "Science": - yield GutenbergBook(1, "Science Book", "Author", "en", - "Science", "url", "txt", "download") - - mock_discover.return_value = mock_generator("Science", ["en"]) - - recommendations = await crawler.get_book_recommendations( - interests=['science'], limit=5 - ) - - assert len(recommendations) >= 0 # May be empty due to mocking - - -class TestKnowledgeProcessor: - """Tests for knowledge processing system.""" - - @pytest.mark.asyncio - async def test_processor_initialization(self, device): - """Test knowledge processor initialization.""" - processor = KnowledgeProcessor( - device=device, - chunk_size=256, - chunk_overlap=25 - ) - - assert processor.device == device - assert processor.chunk_size == 256 - assert processor.chunk_overlap == 25 - assert processor.nlp is None # Loaded lazily - - @pytest.mark.asyncio - async def test_text_cleaning(self): - """Test text cleaning functionality.""" - processor = KnowledgeProcessor() - - # Test text with common Gutenberg artifacts - dirty_text = """ - *** START OF THE PROJECT GUTENBERG EBOOK TEST *** - - This is the actual content. - It has multiple spaces. - - And multiple - - - - - newlines. - - *** END OF THE PROJECT GUTENBERG EBOOK TEST *** - """ - - cleaned = await processor._clean_text(dirty_text) - - assert "*** START OF" not in cleaned - assert "*** END OF" not in cleaned - assert "multiple spaces" not in cleaned - assert cleaned.count('\n\n\n') == 0 # No triple newlines - - @pytest.mark.asyncio - async def test_title_extraction(self): - """Test title extraction from content and filename.""" - processor = KnowledgeProcessor() - - # Test with content containing title - content_with_title = """ - THE GREAT WORK - - Chapter 1 - - This is the beginning of the story... - """ - - title = await processor._extract_title(content_with_title, "test_file.txt") - assert "GREAT WORK" in title - - # Test with filename fallback - title = await processor._extract_title("No clear title here", "12345_the_book_title.txt") - assert "Book Title" in title - - def test_chunk_type_determination(self): - """Test text chunk type determination.""" - processor = KnowledgeProcessor() - - test_cases = [ - ("Short text", "short_paragraph"), - ("Chapter 1: Introduction", "section_header"), - ("This is a normal paragraph with sufficient length to be classified properly.", "paragraph"), - ("List of items:", "list_header") - ] - - for text, expected_type in test_cases: - result = processor._determine_chunk_type(text) - assert result == expected_type - - @pytest.mark.asyncio - async def test_quality_score_calculation(self): - """Test content quality score calculation.""" - processor = KnowledgeProcessor() - - # High quality content - high_quality = """ - This is a well-researched scientific study that presents important - findings based on rigorous analysis. The research methodology was - peer-reviewed and published in an academic journal. The results - show significant evidence for the hypothesis tested. - """ * 10 # Make it longer - - quality = await processor._calculate_quality_score(high_quality, "Scientific Research Study") - assert quality > 0.5 - - # Lower quality content - low_quality = "unverified rumor gossip speculation fake news" - - quality = await processor._calculate_quality_score(low_quality, "Gossip") - assert quality < 0.5 - - @pytest.mark.asyncio - async def test_category_classification(self): - """Test content category classification.""" - processor = KnowledgeProcessor() - - # Science content - science_content = """ - This research examines the quantum mechanics of particle physics. - The experiment was conducted using advanced scientific methods - to test the hypothesis about atomic behavior. - """ - - category, subcategory = await processor._classify_content( - science_content, "Quantum Physics Research" - ) - assert category == "science" - - # History content - history_content = """ - The ancient Roman Empire was a vast civilization that - dominated the Mediterranean world for centuries. The empire's - military conquests and cultural achievements shaped history. - """ - - category, subcategory = await processor._classify_content( - history_content, "Roman Empire History" - ) - assert category == "history" - - @pytest.mark.asyncio - async def test_complexity_score_calculation(self): - """Test complexity score calculation.""" - processor = KnowledgeProcessor() - - # Simple text - simple_text = "This is easy to read. The words are simple. Anyone can understand this." - complexity = await processor._calculate_complexity_score(simple_text) - assert 0.0 <= complexity <= 1.0 - - # Complex text - complex_text = """ - The epistemological ramifications of phenomenological investigations - require sophisticated methodological approaches to hermeneutical analysis. - """ - complexity = await processor._calculate_complexity_score(complex_text) - assert 0.0 <= complexity <= 1.0 - - def test_processing_statistics(self): - """Test processing statistics generation.""" - processor = KnowledgeProcessor() - - stats = processor.get_processing_statistics() - - required_keys = [ - 'models_loaded', 'chunk_size', 'chunk_overlap', - 'supported_categories', 'device' - ] - - for key in required_keys: - assert key in stats - - assert isinstance(stats['supported_categories'], list) - assert len(stats['supported_categories']) > 0 - - @pytest.mark.asyncio - async def test_processed_knowledge_creation(self, sample_book_content): - """Test creation of ProcessedKnowledge object.""" - processor = KnowledgeProcessor() - - # Mock the heavy NLP models for testing - with patch.object(processor, '_generate_summary') as mock_summary, \ - patch.object(processor, '_extract_concepts') as mock_concepts, \ - patch.object(processor, '_extract_keywords') as mock_keywords, \ - patch.object(processor, '_classify_content') as mock_classify, \ - patch.object(processor, '_generate_embedding') as mock_embedding: - - mock_summary.return_value = "Test summary" - mock_concepts.return_value = ["science", "method", "hypothesis"] - mock_keywords.return_value = ["scientific", "research", "study"] - mock_classify.return_value = ("science", "methodology") - mock_embedding.return_value = None - - result = await processor._process_content( - title="The Art of Science", - content=sample_book_content, - source_metadata={'source': 'test'} - ) - - assert isinstance(result, ProcessedKnowledge) - assert result.title == "The Art of Science" - assert result.category == "science" - assert result.subcategory == "methodology" - assert len(result.keywords) > 0 - assert len(result.concepts) > 0 - - @pytest.mark.asyncio - async def test_web_content_processing(self): - """Test processing of web HTML content.""" - processor = KnowledgeProcessor() - - html_content = """ - - Test Article - - Navigation menu - - Main Content - This is the main content of the article. - - - - - - """ - - with patch.object(processor, '_process_content') as mock_process: - mock_process.return_value = Mock(spec=ProcessedKnowledge) - - await processor.process_web_content(html_content, url="http://test.com") - - # Should have called _process_content with cleaned text - mock_process.assert_called_once() - args, kwargs = mock_process.call_args - - # Should not contain script or nav content - assert "alert('test')" not in args[1] - assert "Navigation menu" not in args[1] - assert "Main Content" in args[1] - - -class TestProcessedKnowledge: - """Tests for ProcessedKnowledge data structure.""" - - def test_processed_knowledge_structure(self): - """Test ProcessedKnowledge data structure.""" - knowledge = ProcessedKnowledge( - title="Test Knowledge", - content="Test content", - summary="Test summary", - category="science", - subcategory="physics", - keywords=["test", "science"], - concepts=["quantum", "mechanics"], - quality_score=0.8, - complexity_score=0.6, - embedding=None, - chunks=[], - metadata={"source": "test"} - ) - - assert knowledge.title == "Test Knowledge" - assert knowledge.category == "science" - assert knowledge.quality_score == 0.8 - assert len(knowledge.keywords) == 2 - assert len(knowledge.concepts) == 2 \ No newline at end of file diff --git a/tests/test_personality_matrix.py b/tests/test_personality_matrix.py deleted file mode 100644 index 01984b1..0000000 --- a/tests/test_personality_matrix.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -Tests for the personality matrix system. -""" - -import pytest -import torch -import numpy as np -from datetime import datetime, timedelta - -from lyra.personality.matrix import PersonalityMatrix, PersonalityTrait -from lyra.personality.traits import OCEANTraits, MyersBriggsType, MyersBriggsAnalyzer -from tests.conftest import assert_tensor_shape, assert_tensor_range - - -class TestPersonalityTrait: - """Tests for individual personality traits.""" - - def test_trait_initialization(self): - """Test trait initialization with default values.""" - trait = PersonalityTrait("test_trait", 0.7) - - assert trait.name == "test_trait" - assert trait.value == 0.7 - assert trait.variance == 0.1 - assert trait.adaptation_rate == 0.01 - assert len(trait.change_history) == 0 - assert trait.stability == 0.8 - - def test_trait_evolution(self): - """Test trait evolution with influence.""" - trait = PersonalityTrait("test_trait", 0.5, adaptation_rate=0.1) - original_value = trait.value - - # Positive influence - trait.evolve(0.5, "positive_interaction") - assert trait.value > original_value - assert len(trait.change_history) == 1 - assert trait.change_history[0][1] == "positive_interaction" - - # Negative influence - trait.evolve(-0.3, "negative_feedback") - assert len(trait.change_history) == 2 - - def test_trait_value_bounds(self): - """Test that trait values stay within bounds [0, 1].""" - trait = PersonalityTrait("test_trait", 0.9) - - # Try to exceed upper bound - trait.evolve(1.0, "extreme_positive") - assert 0.0 <= trait.value <= 1.0 - - # Try to exceed lower bound - trait.value = 0.1 - trait.evolve(-1.0, "extreme_negative") - assert 0.0 <= trait.value <= 1.0 - - -class TestOCEANTraits: - """Tests for OCEAN personality traits.""" - - def test_ocean_initialization(self, sample_ocean_traits): - """Test OCEAN traits initialization.""" - traits = sample_ocean_traits - - assert 0.0 <= traits.openness <= 1.0 - assert 0.0 <= traits.conscientiousness <= 1.0 - assert 0.0 <= traits.extraversion <= 1.0 - assert 0.0 <= traits.agreeableness <= 1.0 - assert 0.0 <= traits.neuroticism <= 1.0 - - def test_ocean_to_tensor(self, sample_ocean_traits, device): - """Test conversion to tensor.""" - tensor = sample_ocean_traits.to_tensor(device) - - assert_tensor_shape(tensor, (5,), "OCEAN tensor") - assert_tensor_range(tensor, 0.0, 1.0, "OCEAN values") - - def test_ocean_to_dict(self, sample_ocean_traits): - """Test conversion to dictionary.""" - trait_dict = sample_ocean_traits.to_dict() - - expected_keys = [ - 'openness', 'conscientiousness', 'extraversion', - 'agreeableness', 'neuroticism', - 'openness_variance', 'conscientiousness_variance', - 'extraversion_variance', 'agreeableness_variance', - 'neuroticism_variance' - ] - - for key in expected_keys: - assert key in trait_dict - assert isinstance(trait_dict[key], float) - - def test_situational_modification(self, sample_ocean_traits): - """Test situational personality modifications.""" - original_traits = sample_ocean_traits - modified_traits = original_traits.apply_situational_modification('stress', 1.0) - - # Stress should increase neuroticism - assert modified_traits.neuroticism >= original_traits.neuroticism - - # Should stay within bounds - assert 0.0 <= modified_traits.neuroticism <= 1.0 - - -class TestMyersBriggsAnalyzer: - """Tests for Myers-Briggs analysis.""" - - def test_analyzer_initialization(self): - """Test analyzer initialization.""" - analyzer = MyersBriggsAnalyzer() - - assert hasattr(analyzer, 'mb_mappings') - assert len(analyzer.mb_mappings) == 4 # E_I, S_N, T_F, J_P - - def test_type_analysis(self, sample_ocean_traits): - """Test Myers-Briggs type determination.""" - analyzer = MyersBriggsAnalyzer() - mb_type = analyzer.analyze_type(sample_ocean_traits) - - assert isinstance(mb_type, MyersBriggsType) - assert len(mb_type.value) == 4 - assert all(c in "ENTJFPS" for c in mb_type.value) - - def test_type_characteristics(self): - """Test getting type characteristics.""" - analyzer = MyersBriggsAnalyzer() - characteristics = analyzer.get_type_characteristics(MyersBriggsType.ENFP) - - expected_keys = [ - 'communication_style', 'decision_making', 'social_tendencies', - 'stress_response', 'learning_preference', 'humor_style' - ] - - for key in expected_keys: - assert key in characteristics - assert isinstance(characteristics[key], str) - - -class TestPersonalityMatrix: - """Tests for the personality matrix system.""" - - @pytest.mark.asyncio - async def test_matrix_initialization(self, device): - """Test personality matrix initialization.""" - matrix = PersonalityMatrix(device=device, enable_self_modification=True) - - assert matrix.device == device - assert matrix.enable_self_modification is True - assert isinstance(matrix.ocean_traits, OCEANTraits) - assert isinstance(matrix.mb_type, MyersBriggsType) - assert len(matrix.custom_traits) > 0 - - @pytest.mark.asyncio - async def test_matrix_forward_pass(self, personality_matrix, sample_context_embedding, - sample_emotional_tensor): - """Test personality matrix forward pass.""" - weights, info = personality_matrix( - context_embedding=sample_context_embedding, - emotional_state=sample_emotional_tensor - ) - - assert_tensor_shape(weights, (1, 10), "personality weights") - assert isinstance(info, dict) - assert 'current_ocean' in info - assert 'myers_briggs' in info - assert 'custom_traits' in info - - @pytest.mark.asyncio - async def test_personality_evolution_from_interaction(self, personality_matrix): - """Test personality evolution from interaction.""" - original_traits = personality_matrix.ocean_traits.to_dict() - - # Simulate positive interaction - personality_matrix.evolve_from_interaction( - interaction_type='support', - user_feedback=0.9, - emotional_context={'joy': 0.8}, - conversation_success=0.8 - ) - - # Check that evolution occurred - new_traits = personality_matrix.ocean_traits.to_dict() - assert personality_matrix.evolution.total_interactions == 1 - assert len(personality_matrix.evolution.evolution_history) == 1 - - @pytest.mark.asyncio - async def test_conscious_personality_modification(self, personality_matrix): - """Test conscious personality modification.""" - original_openness = personality_matrix.ocean_traits.openness - - # Consciously modify openness - result = personality_matrix.consciously_modify_trait( - 'openness', 0.8, 'self-directed_growth' - ) - - assert result is True - assert personality_matrix.ocean_traits.openness != original_openness - - @pytest.mark.asyncio - async def test_relationship_dynamics_update(self, personality_matrix): - """Test relationship dynamics tracking.""" - user_id = "test_user_123" - - # First interaction - personality_matrix.evolve_from_interaction( - interaction_type='casual', - user_feedback=0.7, - emotional_context={'curiosity': 0.6}, - user_id=user_id, - conversation_success=0.7 - ) - - assert user_id in personality_matrix.relationship_dynamics - rel_data = personality_matrix.relationship_dynamics[user_id] - assert rel_data['interaction_count'] == 1 - assert rel_data['familiarity'] > 0 - - @pytest.mark.asyncio - async def test_personality_summary(self, personality_matrix): - """Test personality summary generation.""" - summary = personality_matrix.get_personality_summary() - - required_keys = [ - 'ocean_traits', 'myers_briggs_type', 'custom_traits', - 'evolution_stats', 'self_awareness', 'relationship_count' - ] - - for key in required_keys: - assert key in summary - - assert isinstance(summary['ocean_traits'], dict) - assert isinstance(summary['custom_traits'], dict) - - @pytest.mark.asyncio - async def test_personality_persistence(self, personality_matrix, temp_directory): - """Test saving and loading personality state.""" - save_path = temp_directory / "personality_test.json" - - # Modify personality and save - personality_matrix.ocean_traits.openness = 0.9 - personality_matrix.custom_traits['humor_level'].value = 0.8 - personality_matrix.save_personality(save_path) - - assert save_path.exists() - - # Create new matrix and load - new_matrix = PersonalityMatrix(device=personality_matrix.device) - new_matrix.load_personality(save_path) - - assert abs(new_matrix.ocean_traits.openness - 0.9) < 0.01 - assert abs(new_matrix.custom_traits['humor_level'].value - 0.8) < 0.01 - - @pytest.mark.asyncio - async def test_personality_simulation(self, personality_matrix): - """Test personality development simulation.""" - original_interactions = personality_matrix.evolution.total_interactions - - # Run short simulation - simulation_result = personality_matrix.simulate_personality_development(days=3) - - assert 'simulation_days' in simulation_result - assert 'final_personality' in simulation_result - assert 'development_log' in simulation_result - - # Should have more interactions - assert personality_matrix.evolution.total_interactions > original_interactions - - def test_personality_matrix_device_handling(self, device): - """Test proper device handling.""" - matrix = PersonalityMatrix(device=device) - assert matrix.device == device - - # Test with CUDA if available - if torch.cuda.is_available(): - cuda_device = torch.device("cuda:0") - cuda_matrix = PersonalityMatrix(device=cuda_device) - assert cuda_matrix.device == cuda_device - - @pytest.mark.asyncio - async def test_self_awareness_updates(self, personality_matrix): - """Test self-awareness metric updates.""" - original_awareness = personality_matrix.self_awareness.copy() - - # Multiple successful interactions should increase self-awareness - for _ in range(5): - personality_matrix.evolve_from_interaction( - interaction_type='analytical', - user_feedback=0.8, - emotional_context={'curiosity': 0.7}, - conversation_success=0.8 - ) - - # Check that some aspect of self-awareness improved - new_awareness = personality_matrix.self_awareness - awareness_increased = any( - new_awareness[key] > original_awareness[key] - for key in original_awareness.keys() - ) - assert awareness_increased \ No newline at end of file
This is the main content of the article.