diff --git a/.github/workflows/mor-agents-build-linux.yml b/.github/workflows/mor-agents-build-linux.yml new file mode 100644 index 0000000..cd4b5ed --- /dev/null +++ b/.github/workflows/mor-agents-build-linux.yml @@ -0,0 +1,101 @@ +name: MOR Agents Build Linux + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build with PyInstaller + run: | + pyinstaller --name="MORagents" --add-data "images/moragents.png:images" main.py + + - name: Create Debian package + run: | + mkdir -p debian/DEBIAN + mkdir -p debian/usr/bin + mkdir -p debian/usr/share/applications + mkdir -p debian/usr/share/icons/hicolor/256x256/apps + cp -r dist/MORagents/* debian/usr/bin/ + cp images/moragents.png debian/usr/share/icons/hicolor/256x256/apps/moragents.png + echo "[Desktop Entry] + Name=MORagents + Exec=/usr/bin/MORagents + Icon=moragents + Type=Application + Categories=Utility;" > debian/usr/share/applications/moragents.desktop + echo "Package: moragents + Version: 1.0 + Section: utils + Priority: optional + Architecture: amd64 + Maintainer: LachsBagel + Description: MORagents application + MORagents is a desktop application for AI agents." > debian/DEBIAN/control + + # Create install_moragents.sh script + cat << EOF > debian/DEBIAN/postinst + #!/bin/bash + set -e + + # Ensure curl is installed + if ! command -v curl &> /dev/null; then + echo "Installing curl..." + apt-get update + apt-get install -y curl + fi + + # Install Docker if not present + if ! command -v docker &> /dev/null; then + echo "Installing Docker..." + curl -fsSL https://get.docker.com -o get-docker.sh + sh get-docker.sh + usermod -aG docker $SUDO_USER + systemctl enable docker + systemctl start docker + fi + + # Pull necessary Docker images + echo "Pulling Docker images..." + docker pull morpheusai/nginx-agent:latest + docker pull morpheusai/agents:latest + + # Start Docker containers + echo "Starting Docker containers..." + docker run -d --name agents -p 8080:5000 --restart always -v /var/lib/agents -v /app/src morpheusai/agents:latest + docker run -d --name nginx -p 3333:80 morpheusai/nginx-agent:latest + + echo "Installation complete!" + echo "You can now start MORagents from your application menu or by running 'MORagents' in the terminal." + EOF + + chmod 755 debian/DEBIAN/postinst + + dpkg-deb --build debian moragents.deb + + - name: Upload Debian Package + uses: actions/upload-artifact@v4 + with: + name: MORagentsSetup-Linux + path: moragents.deb \ No newline at end of file diff --git a/build_assets/linux/install_moragents.sh b/build_assets/linux/install_moragents.sh new file mode 100644 index 0000000..2506c7f --- /dev/null +++ b/build_assets/linux/install_moragents.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if script is run as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root${NC}" + exit 1 +fi + +echo "Welcome to the MORagents installer for Linux" + +# Check and install Docker if not present +if ! command_exists docker; then + echo "Docker not found. Installing Docker..." + curl -fsSL https://get.docker.com -o get-docker.sh + sh get-docker.sh + usermod -aG docker $SUDO_USER + systemctl enable docker + systemctl start docker +else + echo "Docker is already installed." +fi + +# Check and install Python if not present +if ! command_exists python3; then + echo "Python3 not found. Installing Python3..." + apt update + apt install -y python3 python3-pip +else + echo "Python3 is already installed." +fi + +# Install the .deb package +echo "Installing MORagents..." +dpkg -i moragents.deb +apt-get install -f -y + +# Pull necessary Docker images +echo "Pulling Docker images..." +docker pull morpheusai/nginx-agent:latest +docker pull morpheusai/agents:latest + +# Start Docker containers +echo "Starting Docker containers..." +docker run -d --name agents -p 8080:5000 --restart always -v /var/lib/agents -v /app/src morpheusai/agents:latest +docker run -d --name nginx -p 3333:80 morpheusai/nginx-agent:latest + +echo -e "${GREEN}Installation complete!${NC}" +echo "You can now start MORagents from your application menu or by running 'MORagents' in the terminal." \ No newline at end of file diff --git a/config.py b/config.py index 00ed5df..61e3a62 100644 --- a/config.py +++ b/config.py @@ -10,9 +10,9 @@ # Run as bundled executable if condition is met, else run as regular Python script repo_root = sys._MEIPASS if getattr(sys, 'frozen', False) else os.path.dirname(__file__) elif os_name == "Linux": - raise RuntimeError( - f"MORagents needs Linux support! Would you like to help?\n" - f"https://github.com/MorpheusAIs/moragents/issues/27") + repo_root = os.path.dirname(__file__) +else: + raise RuntimeError(f"Unsupported OS: {os_name}") class AgentDockerConfig: MACOS_IMAGE_NAMES = [ @@ -23,6 +23,10 @@ class AgentDockerConfig: "lachsbagel/moragents_dockers-nginx:amd64-0.0.9", "lachsbagel/moragents_dockers-agents:amd64-0.0.9" ] + LINUX_IMAGE_NAMES = [ + "lachsbagel/moragents_dockers-nginx:amd64-0.0.9", + "lachsbagel/moragents_dockers-agents:amd64-0.0.9" + ] @staticmethod def get_current_image_names(): @@ -30,6 +34,8 @@ def get_current_image_names(): return AgentDockerConfig.MACOS_IMAGE_NAMES elif os_name == "Windows": return AgentDockerConfig.WINDOWS_IMAGE_NAMES + elif os_name == "Linux": + return AgentDockerConfig.LINUX_IMAGE_NAMES else: raise RuntimeError(f"Unsupported OS: {os_name}") diff --git a/images/moragents.png b/images/moragents.png new file mode 100644 index 0000000..151585f Binary files /dev/null and b/images/moragents.png differ diff --git a/main.py b/main.py index f94c23e..520a593 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from runtime_setup_macos import main as macos_setup from runtime_setup_windows import main as windows_setup +from runtime_setup_linux import main as linux_setup from utils.logger_config import setup_logger from utils.host_utils import get_os_and_arch @@ -21,9 +22,7 @@ elif os_name == "Windows": windows_setup() elif os_name == "Linux": - raise RuntimeError( - f"MORagents needs Linux support! Would you like to help?\n" - f"https://github.com/MorpheusAIs/moragents/issues/27") + linux_setup() except Exception as e: logging.critical(f"Error during Docker setup: {str(e)}") diff --git a/runtime_setup_linux.py b/runtime_setup_linux.py new file mode 100644 index 0000000..3439dd4 --- /dev/null +++ b/runtime_setup_linux.py @@ -0,0 +1,167 @@ +import os +import shutil +import subprocess + +from utils.logger_config import setup_logger +from config import AgentDockerConfig, AgentDockerConfigDeprecate + +logger = setup_logger(__name__) + +def get_docker_path(): + docker_paths = [ + '/usr/bin/docker', # Common Linux path + '/usr/local/bin/docker', # Alternative Linux path + shutil.which('docker') + ] + for docker_path in docker_paths: + if docker_path and os.path.exists(docker_path): + return docker_path + + logger.error("Docker executable not found in PATH.") + return None + +def check_docker_installed(docker_path): + try: + subprocess.run([docker_path, "--version"], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logger.info(f"Docker was found at {docker_path}") + return True + except (subprocess.CalledProcessError, TypeError) as e: + logger.error(f"Error checking Docker installation: {str(e)}") + return False + +def delete_docker_image(docker_path, image_name): + try: + # List all images + list_command = [docker_path, "images", "--format", "{{.Repository}}:{{.Tag}}"] + output = subprocess.check_output(list_command, universal_newlines=True) + images = output.strip().split("\n") + + # Find the image with the specified name + if image_name in images: + # Remove the image + remove_command = [docker_path, "rmi", "-f", image_name] + subprocess.run(remove_command, check=True) + logger.info(f"Image '{image_name}' deleted successfully.") + else: + pass + + except subprocess.CalledProcessError as e: + logger.warning(f"Error deleting image: {e}") + +def list_containers_for_image(docker_path, image_name): + try: + output = subprocess.check_output( + [docker_path, "ps", "-a", "--filter", f"ancestor={image_name}", "--format", "{{.ID}}"]) + containers = output.decode().strip().split("\n") + return [container for container in containers if container] + except subprocess.CalledProcessError as e: + logger.error(f"Failed to list containers for image '{image_name}': {e}") + return [] + +def remove_container(docker_path, container): + try: + subprocess.run([docker_path, "rm", "-f", container], check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to remove container '{container}': {e}") + +def docker_image_present_on_host(docker_path, image_name): + try: + subprocess.run([docker_path, "inspect", image_name], check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + return True + except (subprocess.CalledProcessError, TypeError) as e: + return False + +def remove_containers_for_image(docker_path, image_name): + containers = list_containers_for_image(docker_path, image_name) + for container in containers: + remove_container(docker_path, container) + logger.info(f"Removed container '{container}' for image '{image_name}'") + +def remove_containers_by_name(docker_path, container_name): + try: + list_command = [docker_path, "ps", "-a", "--format", "{{.Names}}"] + output = subprocess.check_output(list_command, universal_newlines=True) + containers = output.strip().split("\n") + + if container_name in containers: + remove_command = [docker_path, "rm", "-f", container_name] + subprocess.run(remove_command, check=True) + logger.info(f"Removed container '{container_name}'") + else: + logger.info(f"Container '{container_name}' not found") + except subprocess.CalledProcessError as e: + logger.error(f"Error removing container '{container_name}': {str(e)}") + +def migration_remove_old_images(docker_path): + for image_name in AgentDockerConfigDeprecate.OLD_IMAGE_NAMES: + if docker_image_present_on_host(docker_path, image_name): + delete_docker_image(docker_path, image_name) + logger.info(f"Deleted image '{image_name} from previous release") + +def pull_docker_images(docker_path): + for image in AgentDockerConfig.get_current_image_names(): + try: + subprocess.run([docker_path, "pull", image], check=True) + logger.info(f"Successfully pulled image: {image}") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to pull image {image}: {e}") + raise + +def start_ollama_server(): + ollama_path = '/usr/local/bin/ollama' # This path might need to be adjusted for Linux + + try: + # Start Ollama server + logger.info("Starting Ollama server...") + subprocess.Popen([ollama_path, "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logger.info("Ollama server started successfully.") + except Exception as e: + logger.info("Failed to start Ollama server.") + logger.error(f"Failed to start Ollama server: {e}") + +def docker_setup(): + docker_path = get_docker_path() + logger.info(f"Docker path: {docker_path}") + + if not check_docker_installed(docker_path): + logger.critical("Docker is not installed.") + raise RuntimeError("Docker is not installed.") + + # Remove old images and containers + logger.info("Checking whether old images need removal.") + migration_remove_old_images(docker_path) + + for image_name in AgentDockerConfig.get_current_image_names(): + remove_containers_for_image(docker_path, image_name) + + remove_containers_by_name(docker_path, "agents") + remove_containers_by_name(docker_path, "nginx") + + # Pull the latest images + pull_docker_images(docker_path) + + # Spin up Agent container + subprocess.run([ + docker_path, "run", "-d", "--name", "agents", + "-p", "8080:5000", "--restart", "always", + "-v", "/var/lib/agents:/var/lib/agents", "-v", "/app/src:/app/src", # Adjusted volume paths for Linux + AgentDockerConfig.get_current_image_names()[1] # agents image + ], check=True) + + # Spin up Nginx container + subprocess.run([ + docker_path, "run", "-d", "--name", "nginx", "-p", "3333:80", + AgentDockerConfig.get_current_image_names()[0] # nginx image + ], check=True) + +def main(): + # main() called every time the app is opened (from main.py). Put all app open code here. + logger.info("Starting app...") + start_ollama_server() + docker_setup() + +if __name__ == "__main__": + main() \ No newline at end of file