#!/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 = {} self.docker_available = False self.use_docker_services = False 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 Docker and Docker Compose self.docker_available = False try: docker_result = subprocess.run(["docker", "--version"], check=True, capture_output=True, text=True) compose_result = subprocess.run(["docker", "compose", "version"], check=True, capture_output=True, text=True) self.print_success(f"Docker found: {docker_result.stdout.strip()}") self.print_success(f"Docker Compose found: {compose_result.stdout.strip()}") self.docker_available = True except (subprocess.CalledProcessError, FileNotFoundError): self.print_warning("Docker/Docker Compose not found.") self.print_info("Install Docker Desktop for easier PostgreSQL/Redis setup: https://docker.com/") # 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: # Try with sys.executable first result = subprocess.run([sys.executable, "-m", "venv", str(self.venv_path)], check=True, capture_output=True, text=True) self.print_success("Virtual environment created successfully") self.python_executable = self.get_venv_python() except subprocess.CalledProcessError as e: self.print_warning(f"Failed with {sys.executable}: {e}") # Try with python3 as fallback try: self.print_info("Trying with python3...") result = subprocess.run(["python3", "-m", "venv", str(self.venv_path)], check=True, capture_output=True, text=True) self.print_success("Virtual environment created successfully") self.python_executable = self.get_venv_python() except subprocess.CalledProcessError as e2: self.print_error(f"Failed to create virtual environment: {e2}") self.print_info("Error details:") if hasattr(e2, 'stderr') and e2.stderr: print(f" {e2.stderr}") self.print_info("Try manually: python3 -m venv venv") if self.ask_yes_no("Continue without virtual environment? (not recommended)", False): self.python_executable = sys.executable self.print_warning("Proceeding without virtual environment") else: 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_info("Installing additional production dependencies...") additional_deps = ["asyncpg", "python-dotenv"] subprocess.run([ self.python_executable, "-m", "pip", "install" ] + additional_deps, 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") if self.docker_available: db_choices = [ "SQLite (simple, file-based)", "PostgreSQL with Docker (recommended)", "PostgreSQL (manual setup)" ] else: db_choices = ["SQLite (simple, file-based)", "PostgreSQL (manual setup)"] db_choice = self.ask_choice("Choose database type:", db_choices, 0) if "PostgreSQL with Docker" in db_choice: self.config["database"] = { "type": "postgresql", "host": "localhost", "port": 15432, "name": "discord_fishbowl", "username": "postgres", "password": self.ask_question("Database password", "fishbowl_password"), "use_docker": True } self.use_docker_services = True elif "PostgreSQL" in db_choice: self.config["database"] = { "type": "postgresql", "host": self.ask_question("PostgreSQL host", "localhost"), "port": int(self.ask_question("PostgreSQL port", "15432")), "name": self.ask_question("Database name", "discord_fishbowl"), "username": self.ask_question("Database username", "postgres"), "password": self.ask_question("Database password", secret=True), "use_docker": False } else: self.config["database"] = { "type": "sqlite", "path": "data/fishbowl.db", "use_docker": False } 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): if self.docker_available and hasattr(self, 'use_docker_services') and self.use_docker_services: if self.ask_yes_no("Use Redis with Docker?", True): self.config["redis"] = { "enabled": True, "host": "localhost", "port": 6379, "password": self.ask_question("Redis password", "redis_password"), "db": 0, "use_docker": True } else: 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")), "use_docker": False } else: 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")), "use_docker": False } else: self.config["redis"] = {"enabled": False, "use_docker": 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") if self.docker_available: vector_choices = [ "ChromaDB with Docker (simple)", "Qdrant with Docker (recommended)", "Qdrant (manual setup)", "In-memory (for testing)", "Skip vector database" ] else: vector_choices = ["Qdrant (manual setup)", "In-memory (for testing)", "Skip vector database"] vector_choice = self.ask_choice("Choose vector database:", vector_choices, 0) if "ChromaDB with Docker" in vector_choice: self.config["vector_db"] = { "type": "chromadb", "host": "localhost", "port": 8001, "use_docker": True } self.use_docker_services = True elif "Qdrant with Docker" in vector_choice: self.config["vector_db"] = { "type": "qdrant", "host": "localhost", "port": 6333, "collection_name": self.ask_question("Collection name", "fishbowl_memories"), "use_docker": True } self.use_docker_services = True elif "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"), "use_docker": False } elif "In-memory" in vector_choice: self.config["vector_db"] = {"type": "memory", "use_docker": False} self.print_warning("In-memory vector database won't persist between restarts") else: self.config["vector_db"] = {"type": "none", "use_docker": False} 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 setup_docker_services(self): """Set up Docker services if enabled""" if not self.use_docker_services: return self.print_section("Setting Up Docker Services") # Create .env file for Docker Compose docker_env_content = self.create_docker_env_content() docker_env_file = self.project_root / ".env.docker" with open(docker_env_file, "w") as f: f.write(docker_env_content) self.print_success("Docker environment file created") # Ask which Docker setup to use docker_choices = [ "Services only (PostgreSQL, Redis, ChromaDB)", "Complete application stack (includes Discord bot and admin interface)", "Don't start services now" ] docker_choice = self.ask_choice("Choose Docker setup:", docker_choices, 1) if "Services only" in docker_choice: try: self.print_info("Starting PostgreSQL, Redis, and ChromaDB containers...") subprocess.run([ "docker", "compose", "-f", "docker-compose.services.yml", "--env-file", ".env.docker", "up", "-d" ], check=True, cwd=self.project_root) self.print_success("Docker services started successfully") # Wait for services to be ready self.print_info("Waiting for services to be ready...") import time time.sleep(10) # Check service health self.check_docker_services() except subprocess.CalledProcessError as e: self.print_error(f"Failed to start Docker services: {e}") self.print_info("You can start them manually with: docker compose -f docker-compose.services.yml up -d") elif "Complete application" in docker_choice: try: self.print_info("Building and starting complete Docker stack...") self.print_warning("This will build the application container, which may take a few minutes...") # Determine vector database profile cmd = ["docker", "compose", "--env-file", ".env.docker"] if self.config["vector_db"]["type"] == "chromadb": cmd.extend(["--profile", "chromadb"]) elif self.config["vector_db"]["type"] == "qdrant": cmd.extend(["--profile", "qdrant"]) cmd.extend(["up", "-d", "--build"]) subprocess.run(cmd, check=True, cwd=self.project_root) self.print_success("Complete Docker stack started successfully") self.print_success("Discord Fishbowl services are now running!") print("\nšŸ“ Services available at:") print(" šŸ¤– Discord Fishbowl App: Running in container") print(" 🌐 Admin Interface: http://localhost:8000") print(" šŸ“Š PostgreSQL: localhost:15432") print(" šŸ”“ Redis: localhost:6379") # Show correct vector database if self.config["vector_db"]["type"] == "chromadb": print(" 🧠 ChromaDB: http://localhost:8001") elif self.config["vector_db"]["type"] == "qdrant": print(" šŸ” Qdrant: http://localhost:6333") print(" Dashboard: http://localhost:6333/dashboard") print("\nšŸ’” Use './docker-start.sh' to restart the complete stack later") except subprocess.CalledProcessError as e: self.print_error(f"Failed to start complete Docker stack: {e}") self.print_info("You can start it manually with: ./docker-start.sh") else: self.print_info("Docker services not started. You can start them later with:") print(" Services only: docker compose -f docker-compose.services.yml --env-file .env.docker up -d") print(" Complete stack: ./docker-start.sh") def create_docker_env_content(self) -> str: """Create Docker environment file content""" lines = [ "# Docker Compose Environment Variables", "# Generated by Discord Fishbowl setup script", "", ] # Database configuration if self.config["database"].get("use_docker"): lines.extend([ "# Database", f"DB_PASSWORD={self.config['database']['password']}", "", ]) # Redis configuration if self.config["redis"].get("use_docker"): lines.extend([ "# Redis", f"REDIS_PASSWORD={self.config['redis']['password']}", "", ]) # Discord configuration lines.extend([ "# Discord Bot (Replace with your actual tokens)", f"DISCORD_BOT_TOKEN={self.config['discord']['token']}", f"DISCORD_GUILD_ID={self.config['discord']['guild_id']}", f"DISCORD_CHANNEL_ID={self.config['discord']['channel_id']}", "", ]) # LLM configuration ai_config = self.config["ai"] lines.extend([ "# LLM Configuration", f"LLM_BASE_URL={ai_config.get('api_base', ai_config.get('base_url', 'http://localhost:11434'))}", f"LLM_MODEL={ai_config['model']}", f"LLM_TIMEOUT=300", f"LLM_MAX_TOKENS={ai_config['max_tokens']}", f"LLM_TEMPERATURE={ai_config.get('temperature', 0.8)}", f"LLM_MAX_PROMPT_LENGTH=6000", f"LLM_MAX_HISTORY_MESSAGES=5", f"LLM_MAX_MEMORIES=5", "", ]) # Vector database configuration if self.config.get("vector_db", {}).get("type") in ["chromadb", "qdrant"]: lines.extend([ "# Vector Database", f"VECTOR_DB_TYPE={self.config['vector_db']['type']}", "", ]) # Admin interface configuration lines.extend([ "# Admin Interface", f"SECRET_KEY={self.config['admin']['secret_key']}", f"ADMIN_USERNAME={self.config['admin']['admin_username']}", f"ADMIN_PASSWORD={self.config['admin']['admin_password']}", "", ]) # Optional services lines.extend([ "# Optional PgAdmin credentials (if using --profile admin)", "PGADMIN_PASSWORD=admin123", "", "# Logging", "LOG_LEVEL=INFO", "", ]) return "\n".join(lines) def check_docker_services(self): """Check if Docker services are running""" try: result = subprocess.run([ "docker", "compose", "-f", "docker-compose.services.yml", "ps", "--services", "--filter", "status=running" ], check=True, capture_output=True, text=True, cwd=self.project_root) running_services = result.stdout.strip().split('\n') if result.stdout.strip() else [] if "postgres" in running_services: self.print_success("PostgreSQL is running") if "redis" in running_services: self.print_success("Redis is running") if "chromadb" in running_services: self.print_success("ChromaDB is running") except subprocess.CalledProcessError: self.print_warning("Could not check Docker service status") 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 self.use_docker_services: print(" Complete Docker stack: $ ./docker-start.sh") print(" Services only: $ docker compose -f docker-compose.services.yml up -d") print(" Local development: $ ./start.sh") else: 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() # Docker services setup self.setup_docker_services() # 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()