Files
discord-fishbowl/install.py
matt 824b118e93 Add comprehensive Docker setup with PostgreSQL, Redis, ChromaDB, and Qdrant
- Enhanced install.py with Docker detection and automatic service setup
- Added docker-compose.services.yml for standalone database services
- Created docker-services.sh management script for easy service control
- Added DOCKER.md documentation with complete setup instructions
- Updated requirements.txt for Python 3.13 compatibility
- Added multiple test scripts and configuration files
- Enhanced collaborative creative projects with proper database integration
- Fixed SQLAlchemy metadata field conflicts in database models
- Added comprehensive quickstart and testing guides

Services now available:
- PostgreSQL with Docker
- Redis with Docker
- ChromaDB vector database
- Qdrant vector database (recommended)
- PgAdmin for database administration

The setup script now automatically detects Docker and offers streamlined
installation with one-command service deployment.
2025-07-05 10:01:41 -07:00

916 lines
38 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_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": 5432,
"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", "5432")),
"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": 8000,
"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")
# Start Docker services
if self.ask_yes_no("Start Docker services now?", True):
try:
self.print_info("Starting PostgreSQL and Redis 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")
else:
self.print_info("To start services later: docker compose -f docker-compose.services.yml --env-file .env.docker up -d")
def create_docker_env_content(self) -> str:
"""Create Docker environment file content"""
lines = [
"# Docker Compose Environment Variables",
"# Generated by Discord Fishbowl setup script",
"",
]
if self.config["database"].get("use_docker"):
lines.extend([
f"DB_PASSWORD={self.config['database']['password']}",
"",
])
if self.config["redis"].get("use_docker"):
lines.extend([
f"REDIS_PASSWORD={self.config['redis']['password']}",
"",
])
lines.extend([
"# Optional PgAdmin credentials (if using --profile admin)",
"PGADMIN_PASSWORD=admin123",
"",
])
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 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()