diff --git a/.github/workflows/mor-agents-build-windows.yml b/.github/workflows/mor-agents-build-windows.yml new file mode 100644 index 0000000..ddc0597 --- /dev/null +++ b/.github/workflows/mor-agents-build-windows.yml @@ -0,0 +1,42 @@ +name: MOR Agents Build Windows + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build with PyInstaller + run: pyinstaller --icon=images/moragents.ico --name=MORagents main.py + + - name: Install Inno Setup + run: choco install innosetup -y + + - name: Compile Inno Setup Script + run: | + iscc /O".\MORagentsWindowsInstaller" "wizard_windows.iss" + + - name: Upload Installer + uses: actions/upload-artifact@v4 + with: + name: MORagentsSetup + path: .\MORagentsWindowsInstaller\MORagentsSetup.exe diff --git a/MORagents.spec b/MORagents.spec index de8ffde..6f0a26f 100644 --- a/MORagents.spec +++ b/MORagents.spec @@ -5,7 +5,7 @@ a = Analysis( ['main.py'], pathex=[], binaries=[], - datas=[('resources', 'resources')], + datas=[], hiddenimports=[], hookspath=[], hooksconfig={}, @@ -26,13 +26,13 @@ exe = EXE( bootloader_ignore_signals=False, strip=False, upx=True, - console=True, + console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, - entitlements_file=None, - icon=['images\\moragents.ico'], + entitlements_file='build_assets/macOS/MORagents.entitlements', + icon=['images/moragents.icns'], ) coll = COLLECT( exe, @@ -43,3 +43,9 @@ coll = COLLECT( upx_exclude=[], name='MORagents', ) +app = BUNDLE( + coll, + name='MORagents.app', + icon='images/moragents.icns', + bundle_identifier=None, +) diff --git a/build_assets/macOS/README_MACOS_DEV_BUILD.md b/build_assets/macOS/README_MACOS_DEV_BUILD.md index 661bec1..fc1e0c5 100644 --- a/build_assets/macOS/README_MACOS_DEV_BUILD.md +++ b/build_assets/macOS/README_MACOS_DEV_BUILD.md @@ -33,7 +33,7 @@ For Intel (x86_64) 5. Build App for Local Installation ```shell - $ pyinstaller --windowed --add-data "resources:resources" --name="MORagents" --icon="images/moragents.icns" --osx-entitlements-file "build_assets/macOS/MORagents.entitlements" main.py + $ pyinstaller --windowed --add-data --name="MORagents" --icon="images/moragents.icns" --osx-entitlements-file "build_assets/macOS/MORagents.entitlements" main.py ``` # If you have issues, try python -m PyInstaller --windowed --runtime-hook runtime_hook.py --name="MORagents" --icon="moragents.icns" main.py diff --git a/build_assets/windows/Packaging_Instructions_Windows.md b/build_assets/windows/Packaging_Instructions_Windows.md index 72f6190..51d3147 100644 --- a/build_assets/windows/Packaging_Instructions_Windows.md +++ b/build_assets/windows/Packaging_Instructions_Windows.md @@ -10,7 +10,7 @@ For Windows: 2. ```shell - pyinstaller --name="MORagents" --add-data "resources;resources" --icon=".\images\moragents.ico" main.py + pyinstaller --name="MORagents" --icon=".\images\moragents.ico" main.py ``` Windows Inno Setup for Wizard: diff --git a/config.py b/config.py index ca7ea60..00ed5df 100644 --- a/config.py +++ b/config.py @@ -1,9 +1,7 @@ import os import sys - from utils.host_utils import get_os_and_arch - os_name, arch = get_os_and_arch() if os_name == "macOS": @@ -16,13 +14,28 @@ f"MORagents needs Linux support! Would you like to help?\n" f"https://github.com/MorpheusAIs/moragents/issues/27") - class AgentDockerConfig: - CURRENT_IMAGE_NAMES = ["moragents_dockers-nginx:latest", "moragents_dockers-agents:latest"] - CURRENT_IMAGE_FILENAMES = ["moragents_dockers-nginx.tar", "moragents_dockers-agents.tar"] - CURRENT_IMAGE_PATHS = [os.path.join(repo_root, "resources", img_filename) - for img_filename in CURRENT_IMAGE_FILENAMES] - + MACOS_IMAGE_NAMES = [ + "lachsbagel/moragents_dockers-nginx:apple-0.0.9", + "lachsbagel/moragents_dockers-agents:apple-0.0.9" + ] + WINDOWS_IMAGE_NAMES = [ + "lachsbagel/moragents_dockers-nginx:amd64-0.0.9", + "lachsbagel/moragents_dockers-agents:amd64-0.0.9" + ] + + @staticmethod + def get_current_image_names(): + if os_name == "macOS": + return AgentDockerConfig.MACOS_IMAGE_NAMES + elif os_name == "Windows": + return AgentDockerConfig.WINDOWS_IMAGE_NAMES + else: + raise RuntimeError(f"Unsupported OS: {os_name}") class AgentDockerConfigDeprecate: - OLD_IMAGE_NAMES = ["morpheus/price_fetcher_agent:latest"] + OLD_IMAGE_NAMES = [ + "morpheus/price_fetcher_agent:latest", + "moragents_dockers-nginx:latest", + "moragents_dockers-agents:latest" + ] \ No newline at end of file diff --git a/images/moragents.ico b/images/moragents.ico index 6c951a1..5c8d256 100644 Binary files a/images/moragents.ico and b/images/moragents.ico differ diff --git a/runtime_setup_macos.py b/runtime_setup_macos.py index 3c8da37..7df048a 100644 --- a/runtime_setup_macos.py +++ b/runtime_setup_macos.py @@ -7,7 +7,6 @@ logger = setup_logger(__name__) - def get_docker_path(): docker_paths = ['/Applications/Docker.app/Contents/Resources/bin/docker', shutil.which('docker')] for docker_path in docker_paths: @@ -17,7 +16,6 @@ def get_docker_path(): logger.error("Docker executable not found in PATH.") return None - def check_docker_installed(docker_path): try: subprocess.run([docker_path, "--version"], @@ -28,16 +26,6 @@ def check_docker_installed(docker_path): logger.error(f"Error checking Docker installation: {str(e)}") return False - -def load_docker_image(docker_path, image_path): - try: - subprocess.run([docker_path, "load", "-i", image_path], check=True) - logger.info(f"Docker image loaded from '{image_path}'.") - except (subprocess.CalledProcessError, TypeError) as e: - logger.error(f"Error loading Docker image from '{image_path}': {str(e)}") - raise - - def delete_docker_image(docker_path, image_name): try: # List all images @@ -57,7 +45,6 @@ def delete_docker_image(docker_path, image_name): 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( @@ -68,7 +55,6 @@ def list_containers_for_image(docker_path, image_name): 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, @@ -76,7 +62,6 @@ def remove_container(docker_path, container): 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, @@ -85,27 +70,19 @@ def docker_image_present_on_host(docker_path, image_name): except (subprocess.CalledProcessError, TypeError) as e: return False - def remove_containers_for_image(docker_path, image_name): - # List containers using the specified image containers = list_containers_for_image(docker_path, image_name) - - # Remove each container 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 containers with the specified name list_command = [docker_path, "ps", "-a", "--format", "{{.Names}}"] output = subprocess.check_output(list_command, universal_newlines=True) containers = output.strip().split("\n") - # Check if the specified container name exists if container_name in containers: - # Remove the container remove_command = [docker_path, "rm", "-f", container_name] subprocess.run(remove_command, check=True) logger.info(f"Removed container '{container_name}'") @@ -114,31 +91,20 @@ def remove_containers_by_name(docker_path, container_name): except subprocess.CalledProcessError as e: logger.error(f"Error removing container '{container_name}': {str(e)}") - -def migration_load_current_docker_images(docker_path): - for image_name, image_path in zip(AgentDockerConfig.CURRENT_IMAGE_NAMES, AgentDockerConfig.CURRENT_IMAGE_PATHS): - if docker_image_present_on_host(docker_path, image_name): - # Remove containers corresponding to the image - remove_containers_for_image(docker_path, image_name) - - # Remove the existing image - delete_docker_image(docker_path, image_name) - logger.info(f"Removed existing docker image '{image_name}'") - - if not os.path.exists(image_path): - logger.critical(f"Docker image file: {image_name} was not found at path: '{image_path}'") - raise FileNotFoundError(f"Docker image file: {image_name} was not found at path: '{image_path}'") - - load_docker_image(docker_path, image_path) - logger.info(f"Loaded docker image '{image_name}'") - - 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 docker_setup(): docker_path = get_docker_path() @@ -148,32 +114,32 @@ def docker_setup(): logger.critical("Docker is not installed.") raise RuntimeError("Docker is not installed.") - # remove old images on user device, if present + # Remove old images and containers logger.info("Checking whether old images need removal.") migration_remove_old_images(docker_path) - migration_load_current_docker_images(docker_path) - - remove_containers_for_image(docker_path, "moragents_dockers-agents:latest") - remove_containers_for_image(docker_path, "moragents_dockers-nginx:latest") + 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", "-v", "/app/src", - "moragents_dockers-agents:latest" + 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", - "moragents_dockers-nginx:latest" + AgentDockerConfig.get_current_image_names()[0] # nginx image ], check=True) - if __name__ == "__main__": - docker_setup() + docker_setup() \ No newline at end of file diff --git a/runtime_setup_windows.py b/runtime_setup_windows.py index 22aebde..8bfc22f 100644 --- a/runtime_setup_windows.py +++ b/runtime_setup_windows.py @@ -1,5 +1,3 @@ -import os -import sys import subprocess import time @@ -8,10 +6,8 @@ logger = setup_logger(__name__) - docker_path = "docker" - def check_docker_installed(): try: subprocess.run([docker_path, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -19,7 +15,6 @@ def check_docker_installed(): except (subprocess.CalledProcessError, FileNotFoundError): return False - def start_docker(): try: subprocess.run(["C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"]) @@ -38,39 +33,19 @@ def start_docker(): logger.info("Waiting for Docker engine to start...") time.sleep(2) - -def load_docker_image(image_path): - print(f"Loading Docker image {image_path}. This will take about 5-10 minutes, please wait...") - try: - result = subprocess.run(["docker", "load", "-i", image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, timeout=300) - print(f"Docker image loaded successfully: {result.stdout}") - except subprocess.CalledProcessError as e: - print(f"Error loading Docker image: {e}") - print(f"Command: {e.cmd}") - print(f"Output: {e.output}") - print(f"Error: {e.stderr}") - - def delete_docker_image(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(image_name): try: output = subprocess.check_output( @@ -81,7 +56,6 @@ def list_containers_for_image(image_name): logger.error(f"Failed to list containers for image '{image_name}': {e}") return [] - def remove_container(container): try: subprocess.run([docker_path, "rm", "-f", container], check=True, stdout=subprocess.DEVNULL, @@ -89,36 +63,27 @@ def remove_container(container): except subprocess.CalledProcessError as e: logger.error(f"Failed to remove container '{container}': {e}") - def docker_image_present_on_host(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: + except (subprocess.CalledProcessError, TypeError): return False - def remove_containers_for_image(image_name): - # List containers using the specified image containers = list_containers_for_image(image_name) - - # Remove each container for container in containers: remove_container(container) logger.info(f"Removed container '{container}' for image '{image_name}'") - def remove_containers_by_name(container_name): try: - # List containers with the specified name list_command = [docker_path, "ps", "-a", "--format", "{{.Names}}"] output = subprocess.check_output(list_command, universal_newlines=True) containers = output.strip().split("\n") - # Check if the specified container name exists if container_name in containers: - # Remove the container remove_command = [docker_path, "rm", "-f", container_name] subprocess.run(remove_command, check=True) logger.info(f"Removed container '{container_name}'") @@ -127,49 +92,20 @@ def remove_containers_by_name(container_name): except subprocess.CalledProcessError as e: logger.error(f"Error removing container '{container_name}': {str(e)}") - -def migration_load_current_docker_images(): - for image_name, image_path in zip(AgentDockerConfig.CURRENT_IMAGE_NAMES, AgentDockerConfig.CURRENT_IMAGE_PATHS): - - # # FIXME, this is temporary - # if getattr(sys, 'frozen', False): - # image_path = os.path.join(sys._MEIPASS, "resources", os.path.basename(image_path)) - # else: - # image_path = os.path.join(os.path.dirname(__file__), "resources", os.path.basename(image_path)) - - if docker_image_present_on_host(image_name): - logger.info(f"Docker image '{image_name}' is already present, skipping loading") - continue - - if not os.path.exists(image_path): - logger.critical(f"Docker image file: {image_name} was not found at path: '{image_path}'") - raise FileNotFoundError(f"Docker image file: {image_name} was not found at path: '{image_path}'") - - load_docker_image(image_path) - logger.info(f"Loaded docker image '{image_name}'") - - -def check_container_running(image_name): - try: - output = subprocess.check_output(["docker", "ps", "-f", f"ancestor={image_name}", "--format", "{{.Names}}"]) - if not output: - return False - return output.decode().strip() != "" - except (subprocess.CalledProcessError, FileNotFoundError): - return False - - -def start_container(image_name, port_mapping): - logger.info(f"Spinning up container for image {image_name} with port mapping: {port_mapping}") - subprocess.run(["docker", "run", "-d", "-p", port_mapping, image_name], check=True) - - def migration_remove_old_images(): for image_name in AgentDockerConfigDeprecate.OLD_IMAGE_NAMES: if docker_image_present_on_host(image_name): delete_docker_image(image_name) - logger.info(f"Deleted image '{image_name} from previous release") + logger.info(f"Deleted image '{image_name}' from previous release") +def pull_docker_images(): + for image_name in AgentDockerConfig.get_current_image_names(): + try: + subprocess.run([docker_path, "pull", image_name], check=True) + logger.info(f"Successfully pulled image: {image_name}") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to pull image {image_name}: {e}") + raise def docker_setup(): if not check_docker_installed(): @@ -178,14 +114,13 @@ def docker_setup(): start_docker() - # remove old images on user device, if present logger.info("Checking whether old images need removal.") migration_remove_old_images() - migration_load_current_docker_images() + pull_docker_images() - remove_containers_for_image("moragents_dockers-agents:latest") - remove_containers_for_image("moragents_dockers-nginx:latest") + for image_name in AgentDockerConfig.get_current_image_names(): + remove_containers_for_image(image_name) remove_containers_by_name("agents") remove_containers_by_name("nginx") @@ -195,15 +130,14 @@ def docker_setup(): docker_path, "run", "-d", "--name", "agents", "-p", "8080:5000", "--restart", "always", "-v", "/var/lib/agents", "-v", "/app/src", - "moragents_dockers-agents:latest" + 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", - "moragents_dockers-nginx:latest" + AgentDockerConfig.get_current_image_names()[0] # nginx image ], check=True) - if __name__ == "__main__": - docker_setup() + docker_setup() \ No newline at end of file diff --git a/wizard_windows.iss b/wizard_windows.iss index 01a6c78..1654c92 100644 --- a/wizard_windows.iss +++ b/wizard_windows.iss @@ -4,35 +4,92 @@ AppVersion=0.0.8 DefaultDirName={commonpf}\MORagents OutputDir=.\MORagentsWindowsInstaller OutputBaseFilename=MORagentsSetup -DiskSpanning=yes -SlicesPerDisk=1 -DiskSliceSize=max -Compression = none +WizardStyle=modern [Messages] -WelcomeLabel1=Welcome to the MORagents Setup Wizard. By proceeding you acknowledge you had read and agreed to the License found at: https://github.com/MorpheusAIs/moragents/blob/778b0aba68ae873d7bb355f2ed4419389369e042/LICENSE +WelcomeLabel1=Welcome to the MORagents Setup Wizard WelcomeLabel2=This will install MORagents on your computer. Please click Next to continue. [Files] Source: "dist\MORagents\MORagents.exe"; DestDir: "{app}" Source: "dist\MORagents\_internal\*"; DestDir: "{app}\_internal"; Flags: recursesubdirs Source: "images\moragents.ico"; DestDir: "{app}" -Source: "resources\Docker Desktop Installer.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall -Source: "resources\OllamaSetup.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall Source: "LICENSE"; DestDir: "{app}"; Flags: isreadme +Source: "{tmp}\DockerDesktopInstaller.exe"; DestDir: "{tmp}"; Flags: external deleteafterinstall +Source: "{tmp}\OllamaSetup.exe"; DestDir: "{tmp}"; Flags: external deleteafterinstall +Source: "runtime_setup_windows.py"; DestDir: "{app}" [Icons] Name: "{commondesktop}\MORagents"; Filename: "{app}\MORagents.exe"; IconFilename: "{app}\moragents.ico" [Run] -Filename: "{app}\LICENSE"; Description: "License Agreement"; Flags: postinstall shellexec skipifsilent -Filename: "{tmp}\Docker Desktop Installer.exe"; Description: "Installing Docker Desktop..."; StatusMsg: "Installing Docker Desktop..." -Filename: "{tmp}\OllamaSetup.exe"; Description: "Installing Ollama..."; StatusMsg: "Installing Ollama..." +Filename: "{tmp}\DockerDesktopInstaller.exe"; Parameters: "install"; StatusMsg: "Installing Docker Desktop..."; Flags: waituntilterminated +Filename: "{tmp}\OllamaSetup.exe"; StatusMsg: "Installing Ollama..."; Flags: waituntilterminated +Filename: "{app}\LICENSE"; Description: "View License Agreement"; Flags: postinstall shellexec skipifsilent +Filename: "{app}\MORagents.exe"; Description: "Launch MORagents"; Flags: postinstall nowait skipifsilent unchecked +Filename: "cmd.exe"; Parameters: "/c ollama pull llama3 && ollama pull nomic-embed-text"; StatusMsg: "Pulling Ollama models..."; Flags: runhidden waituntilterminated [Code] -function InitializeSetup(): Boolean; +var + EULAAccepted: Boolean; + DownloadPage: TDownloadWizardPage; + EULAPage: TOutputMsgWizardPage; + +procedure InitializeWizard; +begin + EULAPage := CreateOutputMsgPage(wpWelcome, + 'License Agreement', 'Please read the following License Agreement carefully', + 'By continuing, you acknowledge that you have read and agreed to the License. ' + + 'The full license text can be found at: ' + + 'https://github.com/MorpheusAIs/moragents/blob/778b0aba68ae873d7bb355f2ed4419389369e042/LICENSE' + #13#10 + #13#10 + + 'Do you accept the terms of the License agreement?'); + + DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); +end; + +function NextButtonClick(CurPageID: Integer): Boolean; +begin + Result := True; + + if CurPageID = EULAPage.ID then + begin + EULAAccepted := True; + end + else if CurPageID = wpReady then + begin + if not EULAAccepted then + begin + MsgBox('You must accept the License Agreement to continue.', mbError, MB_OK); + Result := False; + Exit; + end; + + DownloadPage.Clear; + DownloadPage.Add('https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe', 'DockerDesktopInstaller.exe', ''); + DownloadPage.Add('https://ollama.com/download/OllamaSetup.exe', 'OllamaSetup.exe', ''); + DownloadPage.Show; + try + try + DownloadPage.Download; + Result := True; + except + if DownloadPage.AbortedByUser then + Log('Aborted by user.') + else + SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); + Result := False; + end; + finally + DownloadPage.Hide; + end; + end; +end; + +function ShouldSkipPage(PageID: Integer): Boolean; begin - Result := MsgBox('Please read the license agreement found at https://github.com/MorpheusAIs/moragents/blob/778b0aba68ae873d7bb355f2ed4419389369e042/LICENSE carefully. Do you accept the terms of the License agreement?', mbConfirmation, MB_YESNO) = idYes; - if not Result then - MsgBox('Setup cannot continue without accepting the License agreement.', mbInformation, MB_OK); + Result := False; + + { Skip EULA page if already accepted } + if (PageID = EULAPage.ID) and EULAAccepted then + Result := True; end;