- 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.
916 lines
38 KiB
Python
Executable File
916 lines
38 KiB
Python
Executable File
#!/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() |