- Fix remaining datetime timezone errors across all database operations - Implement dynamic vector database backend (Qdrant/ChromaDB) based on install.py configuration - Add LLM timeout handling with immediate fallback responses for slow self-hosted models - Use proper install.py configuration (2000 max tokens, 5min timeout, correct LLM endpoint) - Fix PostgreSQL schema to use timezone-aware columns throughout - Implement async LLM request handling with background processing - Add configurable prompt limits and conversation history controls - Start missing database services (PostgreSQL, Redis) automatically - Fix environment variable mapping between install.py and application code - Resolve all timezone-naive vs timezone-aware datetime conflicts System now properly uses Qdrant vector database as specified in install.py instead of hardcoded ChromaDB. Characters respond immediately with fallback messages during long LLM processing times. All database timezone errors resolved with proper timestamptz columns.
1021 lines
42 KiB
Python
Executable File
1021 lines
42 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_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() |