#!/usr/bin/env python3 """ Discord Fishbowl Interactive Setup Script Comprehensive installation and configuration wizard """ import os import sys import subprocess import platform import json import secrets import getpass from pathlib import Path from typing import Dict, Any, Optional class FishbowlSetup: def __init__(self): self.project_root = Path(__file__).parent self.venv_path = self.project_root / "venv" self.config_path = self.project_root / "config" self.env_file = self.project_root / ".env" self.config_file = self.config_path / "fishbowl_config.json" self.python_executable = None self.config = {} def print_header(self): """Print welcome header""" print("\n" + "=" * 80) print("🐠 DISCORD FISHBOWL INTERACTIVE SETUP") print("Autonomous Character Ecosystem Installation Wizard") print("=" * 80 + "\n") def print_section(self, title: str): """Print section header""" print(f"\nšŸ”§ {title}") print("-" * (len(title) + 3)) def print_success(self, message: str): """Print success message""" print(f"āœ… {message}") def print_error(self, message: str): """Print error message""" print(f"āŒ {message}") def print_warning(self, message: str): """Print warning message""" print(f"āš ļø {message}") def print_info(self, message: str): """Print info message""" print(f"ā„¹ļø {message}") def ask_question(self, question: str, default: str = None, required: bool = True, secret: bool = False) -> str: """Ask user a question with optional default""" if default: prompt = f"{question} [{default}]: " else: prompt = f"{question}: " while True: if secret: answer = getpass.getpass(prompt).strip() else: answer = input(prompt).strip() if answer: return answer elif default: return default elif not required: return "" else: self.print_error("This field is required. Please enter a value.") def ask_yes_no(self, question: str, default: bool = True) -> bool: """Ask yes/no question""" default_str = "Y/n" if default else "y/N" answer = input(f"{question} [{default_str}]: ").strip().lower() if not answer: return default return answer in ['y', 'yes', 'true', '1'] def ask_choice(self, question: str, choices: list, default: int = 0) -> str: """Ask user to choose from a list""" print(f"\n{question}") for i, choice in enumerate(choices, 1): marker = "→" if i == default + 1 else " " print(f" {marker} {i}. {choice}") while True: try: answer = input(f"Choose [1-{len(choices)}] (default: {default + 1}): ").strip() if not answer: return choices[default] choice_num = int(answer) - 1 if 0 <= choice_num < len(choices): return choices[choice_num] else: self.print_error(f"Please choose a number between 1 and {len(choices)}") except ValueError: self.print_error("Please enter a valid number") def check_python_version(self): """Check Python version compatibility""" self.print_section("Checking Python Version") major, minor = sys.version_info[:2] if major < 3 or (major == 3 and minor < 8): self.print_error(f"Python 3.8+ required. Found Python {major}.{minor}") self.print_info("Please install Python 3.8 or higher and run this script again.") sys.exit(1) self.print_success(f"Python {major}.{minor} is compatible") def check_system_dependencies(self): """Check system dependencies""" self.print_section("Checking System Dependencies") # Check for Git try: result = subprocess.run(["git", "--version"], check=True, capture_output=True, text=True) self.print_success(f"Git found: {result.stdout.strip()}") except (subprocess.CalledProcessError, FileNotFoundError): self.print_error("Git is required but not found.") self.print_info("Please install Git from https://git-scm.com/") sys.exit(1) # Check for Node.js (optional, for frontend) try: result = subprocess.run(["node", "--version"], check=True, capture_output=True, text=True) version = result.stdout.strip() self.print_success(f"Node.js found: {version}") # Check npm npm_result = subprocess.run(["npm", "--version"], check=True, capture_output=True, text=True) self.print_success(f"npm found: {npm_result.stdout.strip()}") except (subprocess.CalledProcessError, FileNotFoundError): self.print_warning("Node.js/npm not found. Admin frontend won't be available.") if not self.ask_yes_no("Continue without frontend support?", True): self.print_info("Please install Node.js from https://nodejs.org/") sys.exit(1) def create_virtual_environment(self): """Create Python virtual environment""" self.print_section("Setting Up Python Virtual Environment") if self.venv_path.exists(): if self.ask_yes_no("Virtual environment already exists. Recreate it?", False): import shutil shutil.rmtree(self.venv_path) else: self.print_info("Using existing virtual environment") self.python_executable = self.get_venv_python() return self.print_info("Creating virtual environment...") try: subprocess.run([sys.executable, "-m", "venv", str(self.venv_path)], check=True, capture_output=True) self.print_success("Virtual environment created successfully") self.python_executable = self.get_venv_python() except subprocess.CalledProcessError as e: self.print_error(f"Failed to create virtual environment: {e}") sys.exit(1) def get_venv_python(self) -> str: """Get path to Python executable in virtual environment""" if platform.system() == "Windows": return str(self.venv_path / "Scripts" / "python.exe") else: return str(self.venv_path / "bin" / "python") def install_python_dependencies(self): """Install Python dependencies""" self.print_section("Installing Python Dependencies") self.print_info("Upgrading pip...") try: subprocess.run([ self.python_executable, "-m", "pip", "install", "--upgrade", "pip" ], check=True, capture_output=True) self.print_info("Installing project dependencies...") subprocess.run([ self.python_executable, "-m", "pip", "install", "-r", "requirements.txt" ], check=True) self.print_success("All Python dependencies installed successfully") except subprocess.CalledProcessError as e: self.print_error(f"Failed to install dependencies: {e}") self.print_info("Try running manually: pip install -r requirements.txt") sys.exit(1) def setup_frontend_dependencies(self): """Set up frontend dependencies""" self.print_section("Setting Up Admin Frontend") frontend_path = self.project_root / "admin-frontend" if not frontend_path.exists(): self.print_warning("Frontend directory not found. Skipping frontend setup.") return if not self.ask_yes_no("Install admin frontend? (requires Node.js)", True): self.print_info("Skipping frontend installation") return self.print_info("Installing frontend dependencies...") try: os.chdir(frontend_path) subprocess.run(["npm", "install"], check=True, capture_output=True) self.print_success("Frontend dependencies installed") if self.ask_yes_no("Build frontend for production?", False): self.print_info("Building frontend...") subprocess.run(["npm", "run", "build"], check=True, capture_output=True) self.print_success("Frontend built for production") except subprocess.CalledProcessError as e: self.print_error(f"Frontend setup failed: {e}") self.print_warning("You can set up the frontend manually later") except FileNotFoundError: self.print_warning("npm not found. Install Node.js to enable frontend") finally: os.chdir(self.project_root) def collect_discord_config(self): """Collect Discord configuration""" print("\nšŸ“± Discord Bot Configuration") print("You'll need to create a Discord application and bot at:") print("https://discord.com/developers/applications") print() self.config["discord"] = { "bot_token": self.ask_question("Discord Bot Token", secret=True), "guild_id": self.ask_question("Discord Server (Guild) ID"), "channel_id": self.ask_question("Discord Channel ID for conversations"), } def collect_database_config(self): """Collect database configuration""" print("\nšŸ—„ļø Database Configuration") db_choices = ["SQLite (simple, file-based)", "PostgreSQL (recommended for production)"] db_choice = self.ask_choice("Choose database type:", db_choices, 0) if "PostgreSQL" in db_choice: self.config["database"] = { "type": "postgresql", "host": self.ask_question("PostgreSQL host", "localhost"), "port": int(self.ask_question("PostgreSQL port", "5432")), "name": self.ask_question("Database name", "discord_fishbowl"), "username": self.ask_question("Database username", "postgres"), "password": self.ask_question("Database password", secret=True), } else: self.config["database"] = { "type": "sqlite", "path": "data/fishbowl.db" } self.print_info("SQLite database will be created automatically") def collect_redis_config(self): """Collect Redis configuration""" print("\nšŸ”“ Redis Configuration") self.print_info("Redis is used for caching and pub/sub messaging") if self.ask_yes_no("Use Redis? (recommended)", True): self.config["redis"] = { "enabled": True, "host": self.ask_question("Redis host", "localhost"), "port": int(self.ask_question("Redis port", "6379")), "password": self.ask_question("Redis password (leave empty if none)", "", required=False), "db": int(self.ask_question("Redis database number", "0")), } else: self.config["redis"] = {"enabled": False} def collect_vector_db_config(self): """Collect vector database configuration""" print("\nšŸ” Vector Database Configuration") self.print_info("Vector database stores character memories and enables semantic search") vector_choices = ["Qdrant (recommended)", "In-memory (for testing)", "Skip vector database"] vector_choice = self.ask_choice("Choose vector database:", vector_choices, 0) if "Qdrant" in vector_choice: self.config["vector_db"] = { "type": "qdrant", "host": self.ask_question("Qdrant host", "localhost"), "port": int(self.ask_question("Qdrant port", "6333")), "collection_name": self.ask_question("Collection name", "fishbowl_memories"), } elif "In-memory" in vector_choice: self.config["vector_db"] = {"type": "memory"} self.print_warning("In-memory vector database won't persist between restarts") else: self.config["vector_db"] = {"type": "none"} self.print_warning("Without vector database, character memories will be limited") def collect_ai_config(self): """Collect AI provider configuration""" print("\nšŸ¤– AI Provider Configuration") ai_choices = ["OpenAI (GPT models)", "Anthropic (Claude models)", "Local/Custom API"] ai_choice = self.ask_choice("Choose AI provider:", ai_choices, 0) if "OpenAI" in ai_choice: self.config["ai"] = { "provider": "openai", "api_key": self.ask_question("OpenAI API Key", secret=True), "model": self.ask_question("Model name", "gpt-4"), "max_tokens": int(self.ask_question("Max tokens per response", "2000")), "temperature": float(self.ask_question("Temperature (0.0-2.0)", "0.8")), } elif "Anthropic" in ai_choice: self.config["ai"] = { "provider": "anthropic", "api_key": self.ask_question("Anthropic API Key", secret=True), "model": self.ask_question("Model name", "claude-3-sonnet-20240229"), "max_tokens": int(self.ask_question("Max tokens per response", "2000")), "temperature": float(self.ask_question("Temperature (0.0-1.0)", "0.8")), } else: self.config["ai"] = { "provider": "custom", "api_base": self.ask_question("API Base URL"), "api_key": self.ask_question("API Key", "", required=False, secret=True), "model": self.ask_question("Model name"), "max_tokens": int(self.ask_question("Max tokens per response", "2000")), } def collect_system_config(self): """Collect system configuration""" print("\nāš™ļø System Configuration") self.config["system"] = { "conversation_frequency": float(self.ask_question("Conversation frequency (0.1-1.0, higher=more active)", "0.5")), "response_delay_min": float(self.ask_question("Minimum response delay (seconds)", "1.0")), "response_delay_max": float(self.ask_question("Maximum response delay (seconds)", "5.0")), "memory_retention_days": int(self.ask_question("Memory retention days", "90")), "max_conversation_length": int(self.ask_question("Max conversation length (messages)", "50")), "creativity_boost": self.ask_yes_no("Enable creativity boost?", True), "safety_monitoring": self.ask_yes_no("Enable safety monitoring?", True), "auto_moderation": self.ask_yes_no("Enable auto-moderation?", False), "personality_change_rate": float(self.ask_question("Personality change rate (0.0-1.0)", "0.1")), } def collect_admin_config(self): """Collect admin interface configuration""" print("\nšŸ” Admin Interface Configuration") self.config["admin"] = { "enabled": self.ask_yes_no("Enable admin web interface?", True), "host": self.ask_question("Admin interface host", "127.0.0.1"), "port": int(self.ask_question("Admin interface port", "8000")), "secret_key": secrets.token_urlsafe(32), "admin_username": self.ask_question("Admin username", "admin"), "admin_password": self.ask_question("Admin password", secret=True), } def collect_configuration(self): """Collect all configuration from user""" self.print_section("Configuration Setup") self.print_info("We'll now collect configuration for all components") self.collect_discord_config() self.collect_database_config() self.collect_redis_config() self.collect_vector_db_config() self.collect_ai_config() self.collect_system_config() self.collect_admin_config() def create_config_files(self): """Create configuration files""" self.print_section("Creating Configuration Files") # Create config directory self.config_path.mkdir(exist_ok=True) # Save JSON configuration with open(self.config_file, "w") as f: json.dump(self.config, f, indent=2) self.print_success(f"Configuration saved to {self.config_file}") # Create .env file env_content = self.create_env_content() with open(self.env_file, "w") as f: f.write(env_content) self.print_success(f"Environment variables saved to {self.env_file}") # Create necessary directories (self.project_root / "data").mkdir(exist_ok=True) (self.project_root / "logs").mkdir(exist_ok=True) self.print_success("Required directories created") def create_env_content(self) -> str: """Create .env file content""" lines = [ "# Discord Fishbowl Configuration", "# Generated by interactive setup script", "", "# Discord", f"DISCORD_BOT_TOKEN={self.config['discord']['bot_token']}", f"DISCORD_GUILD_ID={self.config['discord']['guild_id']}", f"DISCORD_CHANNEL_ID={self.config['discord']['channel_id']}", "", ] # Database db_config = self.config["database"] if db_config["type"] == "postgresql": db_url = f"postgresql://{db_config['username']}:{db_config['password']}@{db_config['host']}:{db_config['port']}/{db_config['name']}" else: db_url = f"sqlite:///{db_config['path']}" lines.extend([ "# Database", f"DATABASE_URL={db_url}", "", ]) # Redis if self.config["redis"]["enabled"]: redis_config = self.config["redis"] if redis_config.get("password"): redis_url = f"redis://:{redis_config['password']}@{redis_config['host']}:{redis_config['port']}/{redis_config['db']}" else: redis_url = f"redis://{redis_config['host']}:{redis_config['port']}/{redis_config['db']}" lines.extend([ "# Redis", f"REDIS_URL={redis_url}", "", ]) # Vector Database vector_config = self.config["vector_db"] if vector_config["type"] == "qdrant": lines.extend([ "# Vector Database", f"QDRANT_HOST={vector_config['host']}", f"QDRANT_PORT={vector_config['port']}", f"QDRANT_COLLECTION={vector_config['collection_name']}", "", ]) # AI Provider ai_config = self.config["ai"] lines.extend([ "# AI Provider", f"AI_PROVIDER={ai_config['provider']}", f"AI_API_KEY={ai_config['api_key']}", f"AI_MODEL={ai_config['model']}", f"AI_MAX_TOKENS={ai_config['max_tokens']}", ]) if "temperature" in ai_config: lines.append(f"AI_TEMPERATURE={ai_config['temperature']}") if "api_base" in ai_config: lines.append(f"AI_API_BASE={ai_config['api_base']}") lines.append("") # Admin Interface if self.config["admin"]["enabled"]: admin_config = self.config["admin"] lines.extend([ "# Admin Interface", f"ADMIN_HOST={admin_config['host']}", f"ADMIN_PORT={admin_config['port']}", f"SECRET_KEY={admin_config['secret_key']}", f"ADMIN_USERNAME={admin_config['admin_username']}", f"ADMIN_PASSWORD={admin_config['admin_password']}", "", ]) # System Configuration system_config = self.config["system"] lines.extend([ "# System Configuration", f"CONVERSATION_FREQUENCY={system_config['conversation_frequency']}", f"RESPONSE_DELAY_MIN={system_config['response_delay_min']}", f"RESPONSE_DELAY_MAX={system_config['response_delay_max']}", f"MEMORY_RETENTION_DAYS={system_config['memory_retention_days']}", f"MAX_CONVERSATION_LENGTH={system_config['max_conversation_length']}", f"CREATIVITY_BOOST={str(system_config['creativity_boost']).lower()}", f"SAFETY_MONITORING={str(system_config['safety_monitoring']).lower()}", f"AUTO_MODERATION={str(system_config['auto_moderation']).lower()}", f"PERSONALITY_CHANGE_RATE={system_config['personality_change_rate']}", "", "# Environment", "ENVIRONMENT=production", "LOG_LEVEL=INFO", ]) return "\n".join(lines) def setup_database_schema(self): """Set up database schema""" self.print_section("Setting Up Database Schema") if self.config["database"]["type"] == "sqlite": # Ensure data directory exists (self.project_root / "data").mkdir(exist_ok=True) self.print_info("Initializing database schema...") try: # Run database migrations subprocess.run([ self.python_executable, "-m", "alembic", "upgrade", "head" ], check=True, cwd=self.project_root, capture_output=True) self.print_success("Database schema initialized") except subprocess.CalledProcessError as e: self.print_warning("Database migration failed - you may need to set it up manually") self.print_info("Run: python -m alembic upgrade head") def create_startup_scripts(self): """Create convenient startup scripts""" self.print_section("Creating Startup Scripts") # Main application script (Unix) if platform.system() != "Windows": run_script = f"""#!/bin/bash # Discord Fishbowl - Main Application echo "🐠 Starting Discord Fishbowl..." # Activate virtual environment source "{self.venv_path}/bin/activate" # Set Python path export PYTHONPATH="{self.project_root}:$PYTHONPATH" # Load environment variables if [ -f "{self.env_file}" ]; then export $(cat "{self.env_file}" | xargs) fi # Start the application python -m src.main "$@" """ script_path = self.project_root / "start.sh" with open(script_path, "w") as f: f.write(run_script) script_path.chmod(0o755) # Admin interface script (Unix) if self.config["admin"]["enabled"]: admin_script = f"""#!/bin/bash # Discord Fishbowl - Admin Interface echo "🌐 Starting Admin Interface..." # Activate virtual environment source "{self.venv_path}/bin/activate" # Set Python path export PYTHONPATH="{self.project_root}:$PYTHONPATH" # Load environment variables if [ -f "{self.env_file}" ]; then export $(cat "{self.env_file}" | xargs) fi # Start admin interface python -m src.admin.app """ admin_path = self.project_root / "start-admin.sh" with open(admin_path, "w") as f: f.write(admin_script) admin_path.chmod(0o755) # Windows batch files if platform.system() == "Windows": run_bat = f"""@echo off echo 🐠 Starting Discord Fishbowl... call "{self.venv_path}\\Scripts\\activate.bat" set PYTHONPATH={self.project_root};%PYTHONPATH% python -m src.main %* """ with open(self.project_root / "start.bat", "w") as f: f.write(run_bat) if self.config["admin"]["enabled"]: admin_bat = f"""@echo off echo 🌐 Starting Admin Interface... call "{self.venv_path}\\Scripts\\activate.bat" set PYTHONPATH={self.project_root};%PYTHONPATH% python -m src.admin.app """ with open(self.project_root / "start-admin.bat", "w") as f: f.write(admin_bat) self.print_success("Startup scripts created") def create_character_configs(self): """Create initial character configurations""" self.print_section("Setting Up Initial Characters") if self.ask_yes_no("Create default character configurations?", True): try: subprocess.run([ self.python_executable, "-m", "scripts.init_characters" ], check=True, cwd=self.project_root, capture_output=True) self.print_success("Default characters initialized") except subprocess.CalledProcessError: self.print_warning("Character initialization failed - you can run it manually later") self.print_info("Run: python -m scripts.init_characters") def print_completion_summary(self): """Print setup completion summary""" self.print_section("šŸŽ‰ Setup Complete!") print("Discord Fishbowl has been successfully installed and configured!") print() # Prerequisites reminder print("šŸ“‹ Before starting, ensure these services are running:") if self.config["database"]["type"] == "postgresql": print(" • PostgreSQL database server") if self.config["redis"]["enabled"]: print(" • Redis server") if self.config["vector_db"]["type"] == "qdrant": print(" • Qdrant vector database") print() # How to start print("šŸš€ To start the fishbowl:") if platform.system() == "Windows": print(" > start.bat") else: print(" $ ./start.sh") print() # Admin interface if self.config["admin"]["enabled"]: print("🌐 To start the admin interface:") if platform.system() == "Windows": print(" > start-admin.bat") else: print(" $ ./start-admin.sh") print() print(f" Admin interface will be available at:") print(f" http://{self.config['admin']['host']}:{self.config['admin']['port']}/admin") print(f" Login: {self.config['admin']['admin_username']}") print() # Important files print("šŸ“ Important files:") print(f" Configuration: {self.config_file}") print(f" Environment: {self.env_file}") print(f" Virtual Env: {self.venv_path}") print() # Troubleshooting print("šŸ”§ Troubleshooting:") print(" • Check logs in the logs/ directory") print(" • Verify all API keys and credentials") print(" • Ensure external services are accessible") print(" • See README.md for detailed documentation") print() self.print_success("Happy fishing! 🐠✨") def run(self): """Run the complete setup process""" try: self.print_header() # Pre-flight checks self.check_python_version() self.check_system_dependencies() # Environment setup self.create_virtual_environment() self.install_python_dependencies() self.setup_frontend_dependencies() # Configuration self.collect_configuration() self.create_config_files() # Database and scripts self.setup_database_schema() self.create_startup_scripts() self.create_character_configs() # Final summary self.print_completion_summary() except KeyboardInterrupt: print("\n\nāš ļø Setup interrupted by user") print("You can run this script again to resume setup.") sys.exit(1) except Exception as e: self.print_error(f"Setup failed: {e}") self.print_info("Please check the error and run the script again.") sys.exit(1) if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]: print("Discord Fishbowl Interactive Setup Script") print("Usage: python install.py") print() print("This script will guide you through setting up the Discord Fishbowl") print("autonomous character ecosystem with all required dependencies") print("and configuration.") sys.exit(0) setup = FishbowlSetup() setup.run()