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/AGENTABILITIES.md b/AGENTABILITIES.md index 638377c..ee8dccb 100644 --- a/AGENTABILITIES.md +++ b/AGENTABILITIES.md @@ -11,18 +11,18 @@ transaction. ### Current Projects: 1. **lachsbagel on Discord** - [this repo](https://github.com/MorpheusAIs/moragents): - 1. Local Docker install - 2. (pending) [HideNSeek](https://github.com/MorpheusAIs/HideNSeek): An algorithm for verifying and fingerprinting which model a compute provider is actually running - 1. Note: this repo will be made public under an MIT license following the publishing of a paper by the same name which is currently in the works - 3. (future) Posting Agent - an agent which optimizes content for social media posting -2. **IODmitri on GitHhub** - 3. (pending) [HideNSeek](https://github.com/MorpheusAIs/HideNSeek): An algorithm for verifying and fingerprinting which model a compute provider is actually running + 1. Local install architecture +2. **IODmitri, SanatSharma, LachsBagel on GitHhub** + 1. [HideNSeek](https://github.com/MorpheusAIs/HideNSeek): An algorithm for verifying and fingerprinting which model a compute provider is actually running + 1. Link to Paper [Hide and Seek: Fingerprinting Large Language Models with Evolutionary Learning](https://www.arxiv.org/abs/2408.02871) + 2. (pending) A follow-on paper exploring parameter size estimation is in the works. 3. **artfuljars on Discord**: 1. Windows Build (EXE version of .app) 2. Two installation wizards: 1. Windows 2. (pending) macOS - 3. (pending) CICD builds for Windows, Linux, and macOS + 3. CICD build for Windows + 4. (pending) CICD builds for Linux and macOS 4. (pending) Vulnerability scanning of dependencies and code 4. GenLayer 1. (pending) [FeedBuzz](https://github.com/yeagerai/feedbuzz-contracts) - AI filtered logging system to surface user demand and failure modes for new functionality @@ -31,14 +31,15 @@ transaction. 2. A [web interface](submodules/moragents_dockers/frontend) which is served by the local Docker installation and integrated with Rainbow, enabling the use of MetaMask, WalletConnect, and other EVM-based wallets. 3. A [swap agent](submodules/moragents_dockers/agents/src/swap_agent) which can iteratively ask users to provide needed details for disambiguation. - 4. (Pending Integration) [A general-purpose agent](https://github.com/MorpheusAIs/moragents/pull/34) that can ingest arbitrary documents, such as PDFs, for basic document QA and text generation. - 5. (Pending) Delegating agent which can maintain user's persona/interests as well as coordinating to task agents and tools. - + 4. [A general-purpose agent](https://github.com/MorpheusAIs/moragents/pull/34) that can ingest arbitrary documents, such as PDFs, for basic document QA and text generation. + 5. (Pending Integration) [Delegating agent](https://github.com/MorpheusAIs/moragents/pull/45) which can maintain user's persona/interests as well as coordinating to task agents and tools. +6. **Dan Y.** + 1. (pending) X/Twitter Posting Agent - an agent which generates spicy tweets with an X integration for one-click posting. ### Decentralized Inference: #### Non-Local Installation Agents for Permission-less Compute Pending Lumerin's work. Eventually Agent Builders will be able to permission-lessly upload Agent and Model artifacts to a decentralized registry. -1. (pending) [FLock](https://www.flock.io/#/) is working on a decentralized agent which will regularly be updated with most recent docs surrounding the Morpheus system for the following audiences: +1. [FLock](https://www.flock.io/#/) has built a decentralized agent, called [Ragchat-Morpheus](https://github.com/FLock-io/ragchat-morpheus) which will regularly be updated with most recent docs surrounding the Morpheus system for the following audiences: 1. Normal user perspective (broad Q/A for those new to Morpheus ecosystem) 2. Developer perspective wanting to deploy on Morpheus 3. Perspective of capital contributors @@ -48,11 +49,11 @@ Pending Lumerin's work. Eventually Agent Builders will be able to permission-les - If you are working on an agent which can provide value through open models and relies on processing public data, please reach out to lachsbagel on Discord (link below) - Otherwise, you are more than welcome to publish your agent to the registry when it goes live pending Lumerin's work and any other necessary pieces which come up to better ensure security and verifiability of models in non-local execution environments. - If you are working on security and/or verifiability of models and the runtime, please reach out to LachsBagel on the Morpheus Discord. - - Currently looking at [Hyperbolic.xyz](https://hyperbolic.xyz) and [6079](https://6079-ai.gitbook.io/6079.ai/technology/6079-proof-of-inference-protocol). See more ecosystem members [here](https://mor.org/ecosystem). + - Currently looking at [Hyperbolic.xyz](https://hyperbolic.xyz) and [6079](https://docs.6079.ai/technology/6079-proof-of-inference-protocol). See more ecosystem members [here](https://mor.org/ecosystem). - LachsBagel is also working on a new algorithm, named [HideNSeek](https://github.com/MorpheusAIs/HideNSeek), which uses a Transformer specific heuristic for model verification - - [Atoma.Network](https://www.atoma.network/) will help with implementing the plumbing for [HideNSeek](https://github.com/MorpheusAIs/HideNSeek) + - [6079](https://6079.ai/) will help with implementing the plumbing for [HideNSeek](https://github.com/MorpheusAIs/HideNSeek) ### Contact Join the [Morpheus Discord](https://discord.com/invite/Dc26EFb6JK) -*Last Updated: July 7, 2024* +*Last Updated: August 18, 2024* diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 017c23b..8c645f7 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -1,4 +1,14 @@ -Contribution Process: +# Contributing and Future Interoperability of Agents + +**For contributors looking to build a local agent** please wait until a test agent being built by Dan Y. (mentioned in [AGENTABILITIES](./AGENTABILITIES.md)) concludes an integration with the new agent router in [Agent Router PR](https://github.com/MorpheusAIs/moragents/pull/45). +Around September 2024 the release of v0.1.0 will incorporate the above. +Release v0.1.0 will include new guides to easily and independently integrate your own local agent. + +Beyond September, the router will expand to coordinate the decentralized agents along with local agents to provide for dynamic workflow construction to satisfy more complex user queries. + +--- + +# Contribution Process: 1. Prospective contributors can propose an idea or find a relevant issue from [Issues](https://github.com/MorpheusAIs/moragents/issues) 1. See what's being worked on here in [AGENTABILITIES](./AGENTABILITIES.md) 2. Contact lachsbagel on the Morpheus Discord or comment on an issue above @@ -6,4 +16,4 @@ Contribution Process: 4. Developer produces deliverable 5. Developer will receive weights following delivery and verification 6. Developer must maintain functionality and resolve bugs to keep weights. No additional weights for same scope. - 1. If maintenance doesn't occur then developer will receive notice maintain or risk losing some or all weights + 1. If maintenance doesn't occur then developer will receive notice to maintain or risk losing some or all weights diff --git a/LICENSE b/LICENSE.md similarity index 96% rename from LICENSE rename to LICENSE.md index 826baaa..b6f7234 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Liquid Tensor LLC +Copyright (c) 2024 Liquid Tensor LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/MORagents.spec b/MORagents.spec index de8ffde..3f50430 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'], + codesign_identity='Developer ID Application: Liquid Tensor LLC (ZQN244GMTD)', + 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='com.liquidtensor.moragents', +) diff --git a/PRIVACY.md b/PRIVACY.md deleted file mode 100644 index 45eaeed..0000000 --- a/PRIVACY.md +++ /dev/null @@ -1,26 +0,0 @@ -# Privacy Policy - -This application is committed to protecting the privacy and security of our users. This Privacy Policy outlines how this application collects, uses, and safeguards the information you provide when using our codebase to build applications. - -## Information Collection - -1. Local Execution: All execution of the applications built using this codebase is performed locally on your device. This application does not collect, store, or process any data related to the local execution of your applications. - -2. External Requests: When using this codebase, your application may make requests to external services such as CoinGecko or other oracles to retrieve necessary data. These requests are made directly from your device to the respective services, and this application does not have access to or store any data related to these requests. - -3. Locally Hosted LLM: The applications built using this codebase utilize a language model (LLM) that is hosted locally on your device. This application does not have access to or collect any data processed by the locally hosted LLM. - -## Data Sharing - -This application does not share any data collected or processed by the applications built using this codebase with third parties. As mentioned above, the only data shared are the requests made directly from your device to external services such as CoinGecko or other oracles. - -## Data Security - -This application takes the security of your data seriously. Although this application does not collect or store any data, it encourages you to follow best practices for securing your local environment, such as keeping your device and software up to date and using strong passwords. - -## Changes to this Privacy Policy - -This application may update this Privacy Policy from time to time. You are advised to review this Privacy Policy periodically for any changes. - - -By using this codebase to build applications, you agree to the terms of this Privacy Policy. \ No newline at end of file diff --git a/README.md b/README.md index 3f62926..a346d73 100644 --- a/README.md +++ b/README.md @@ -3,41 +3,51 @@ ## Morpheus Install for Local Web3 Agent Interaction +![UI 1](images/moragents_chatpdf.png) -![UI 1](images/wallet_integration.png) +![UI 2](images/wallet_integration.png) -![UI 2](images/successful_swap.png) +![UI 3](images/successful_swap.png) + +![UI 4](images/agent_clarify.png) -![UI 3](images/agent_clarify.png) --- ### Features -#### Current: -- Fetch price, market cap, and TVL of coins and tokens supported on CoinGecko -- Web interface -- Wallet integrations for your existing wallets in-browser - - MetaMask - - Rainbow - - Coinbase Wallet - - WalletConnect -- Web3 swap agents - -#### Pending: -- Chat with local files agent (general purpose) +- Chat with local PDF files +- Swap ERC Compatible Tokens +- Fetch Price, Market Cap, and TVL of coins and tokens supported on CoinGecko +- Web interface works in your preferred browser: + - Chrome + - Brave + + with your favorite wallet extensions: + - MetaMask + - Rainbow + - Coinbase Wallet + - WalletConnect --- -### Install -#### macOS on M1/2/3 etc. (arm64) +## Install +### macOS on M1/2/3 etc. (arm64) >Assumes minimum 16GB RAM -#### Steps +#### Steps to Install 1. Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) 1. Follow default settings, can skip surveys, then leave docker desktop running. You can minimize it. -2. USe Chrome to download [Moragents.zip](https://drive.proton.me/urls/X35VBE3GWW#mtrqT6rAzZbi), open ZIP, and copy MORagents.app to your Applications folder - > SHA256 96c2510e4f7a752c613b322be0a107958ee34814415e3e7b950b426298379a7a MORagents.zip -3. Open **MORagents** app. Give it a few minutes the first time, and then it should open your browser. - 1. If there's an issue, try opening the MORagents app again +2. Download and install [MORagents009.pkg](https://drive.proton.me/urls/762Z6QFNH4#68MKubcGeDtf) + > SHA256 5200350bba351a40cfac5552476bad1bb67d32ff069a4d9ebc0b3556367673b7 MORagents009.pkg +3. Wait several minutes for background files to download and then your browser should automatically open to http://localhost:3333 + > Note: After installation is complete, the MORagents app icon will bounce for several minutes on your dock, and then stop. This is normal behavior as it's downloading a large 9GB file in the background. You can open "Activity Monitor" and in the Network tab see that it's downloading. + +#### Future Usage +- Open the "MORagents" app from Mac search bar. + - For easier access: Right-click MORagents icon on dock -> Options -> Keep in Dock + +#### Troubleshooting +If the app shows connections errors in connecting to agents. Please ensure Docker Desktop is running, then close and reopen **MORagents** from desktop. + ### macOS Intel (x86_64) *coming soon* @@ -48,23 +58,20 @@ >Assumes minimum 16GB RAM #### Steps -1. Use Chrome to download [MORagentsWindowsInstaller.zip](https://drive.proton.me/urls/9BE8X1ZMTG#Oh1SfTeklH4W) - > SHA256 0a5f5e3a288d45854c83994fa4afa4c713019229d99d67f28442fc56a5de1b20 MORagentsWindowsInstaller.zip -2. Go to downloaded **MORagentsWindowsInstaller(.zip)** file and click to "Extract All" -3. Open Extracted Folder **MORagentsWindowsInstaller** - 1. You may need to disable your anti-virus software before proceeding +1. Use Chrome to download [MORagentsSetupWindows009.zip](https://drive.proton.me/urls/8X58WAH80G#ib5r3K4WalDA) + > SHA256 6b8bd78571df2f5e8c6e516102aa05b1121d0214fdfb75a2be16146c22e0d2c52 MORagentsSetupWindows009.zip +2. Go to downloaded **MORagentsSetupWindows009(.zip)** file and double click to open +3. Double click **MORagentsSetup.exe** + 1. You may need to click "More info" -> "Run anyway" + 2. If that still doesn't work, try temporarily disabling your antivirus and open the .exe again 4. Click and Run **MORagentsSetup.exe** - 1. This will auto-install Docker Desktop dependency -5. Open **MORagents** from Desktop -6. Accept Docker's EULA - 1. Surveys are optional, can skip -7. Wait for Docker engine to start... -8. Open **MORagents** App from Desktop - 1. First time installation requires some extra time to load agent's image - 2. If anything hangs for >10min, please try opening the MORagents app again from the Desktop + 1. This will auto-install Docker and Ollama dependencies. Those will ask you for confirmation. +5. Open **MORagents** from Desktop + 1. Wait for Docker engine to start... + 2. If you see any errors or if anything hangs for >10min, please try opening the MORagents app again from the Desktop #### Troubleshooting -If the app shows connections errors to agent fetcher. Please ensure Docker Desktop is running, then close and reopen **MORagents** from desktop. +If the app shows connections errors in connecting to agents. Please ensure Docker Desktop is running, then close and reopen **MORagents** from desktop. --- diff --git a/build_assets/macOS/Packaging_Instructions_macOS.md b/build_assets/macOS/Packaging_Instructions_macOS.md index 7d1d4cb..cb8103f 100644 --- a/build_assets/macOS/Packaging_Instructions_macOS.md +++ b/build_assets/macOS/Packaging_Instructions_macOS.md @@ -1,41 +1,58 @@ ## Distribution -## Build and Sign +## Build and Sign MORagents.app ```sh -pyinstaller --windowed --add-data "resources:resources" --osx-bundle-identifier "com.liquidtensor.moragents" --codesign-identity "Developer ID Application: Liquid Tensor LLC (ZQN244GMTD)" --name="MORagents" --icon="images/moragents.icns" --osx-entitlements-file "build_assets/macOS/MORagents.entitlements" main.py +pyinstaller --windowed --osx-bundle-identifier "com.liquidtensor.moragents" --codesign-identity "Developer ID Application: Liquid Tensor LLC (ZQN244GMTD)" --name="MORagents" --icon="images/moragents.icns" --osx-entitlements-file "build_assets/macOS/MORagents.entitlements" main.py ``` +NOTE: you will need to use your own codesigning identity if you intend to distribute. Codesigining is not required for running it on the same mac you built the .app on. For this you can drop the ```--codesign-identity "Developer ID Application: Liquid Tensor LLC (ZQN244GMTD)"``` part from the command. -## Compress -```sh -ditto -c -k --sequesterRsrc --keepParent "MORagents.app" "MORagents.zip" -```` +## Wizard Creation -## Notarize -```sh -xcrun notarytool submit "MORagents.zip" --keychain-profile "NotaryProfile" --wait +1. Download the dev version of the Packages app (it has to be the dev version because the latest build doesn't work on mac sonoma/last updated 2022) [Packages App](http://s.sudre.free.fr/Software/Packages/about.html) [packages_1211_dev.dmg](http://s.sudre.free.fr/files/Packages_1211_dev.dmg) +2. Download the files in https://github.com/MorpheusAIs/moragents/tree/main/build_assets/macOS, along with [Docker Desktop Mac Install](https://docs.docker.com/desktop/install/mac-install/) and the MORAgents.app you built in previous step. +3. In the Packages App...\ + 1. Create a New Project ... set the template as "Distribution".\ + 2. Under the Packages, either edit the existing package or create a new one. Set "Identifier" to com.morpheus.pkg.MORAgents, set the Payload to have MORAgents.app under /Applications, then under Scripts, add the preinstall.sh and postinstall.sh from the downloaded files.\ + 3. Create a new Package, Set "Identifier" to com.morpheus.pkg.DockerDesktop, then under Payload, add the Docker Desktop Mac Install under /Applications\ + 4. Create a new Package, Set "Identifier" to com.morpheus.pkg.Ollama, then under preinstall scripts, add the preinstall_ollama.sh script from the downloaded files.\ + 5. Navigate to Project, then in Presentation click the topright dropdown and select Introduction choose the welcome.html file, add a License section and choose license.html. + 6. In the upmost toolbar click Build -> Build to generate a .pkg file in the directory you saved the MORagents Packages package + +--- + +>Note: Following steps are only required for distribution. +If you are just running the app on your own Mac you can run the .pkg you created, and then open MORagents from your searchbar. +Future usage only requires you to run MORagents from your searchbar. + +--- + +## Signing +```sh + productsign --sign "Developer ID Installer: Liquid Tensor LLC (ZQN244GMTD)" MORagents.pkg MORagents009.pkg ``` -## Staple App file -```shell -xcrun stapler staple "MORagents.app" +## Notarize +```sh +xcrun notarytool submit MORagents009.pkg --keychain-profile "NotaryProfile" --wait ``` -## Recreate zip archive post-stapling -```sh -ditto -c -k --sequesterRsrc --keepParent "MORagents.app" "MORagents.zip" -```` +## Staple +```sh +xcrun stapler staple MORagents009.pkg +``` --- -## Verify +## Verify (Optional) ### Verify notarization ```sh xcrun notarytool info "" --keychain-profile "NotaryProfile" ``` +### Verify codesign ```sh codesign --verify --verbose=4 MORagents.app codesign -dv --verbose=4 MORagents.app -``` +``` \ No newline at end of file diff --git a/build_assets/macOS/README_MACOS_DEV_BUILD.md b/build_assets/macOS/README_MACOS_DEV_BUILD.md index 661bec1..223cea9 100644 --- a/build_assets/macOS/README_MACOS_DEV_BUILD.md +++ b/build_assets/macOS/README_MACOS_DEV_BUILD.md @@ -33,10 +33,10 @@ 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 --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 + python -m PyInstaller --windowed --name="MORagents" --icon="images/moragents.icns" --osx-entitlements-file "build_assets/macOS/MORagents.entitlements" main.py 6. Install Application ```shell diff --git a/build_assets/macOS/license.html b/build_assets/macOS/license.html new file mode 100644 index 0000000..2d8999a --- /dev/null +++ b/build_assets/macOS/license.html @@ -0,0 +1,56 @@ + + + + + + + +

License Agreement

+

Please read the following License Agreement carefully. By continuing, you acknowledge that you have read and agreed to the License.

+ +
+MIT License + +Copyright (c) 2024 Liquid Tensor LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +
+ +

By clicking Continue, installing, and/or using the MORagents software you accept the terms of the License agreement.

+ + \ No newline at end of file diff --git a/build_assets/macOS/postinstall.sh b/build_assets/macOS/postinstall.sh new file mode 100644 index 0000000..60703a0 --- /dev/null +++ b/build_assets/macOS/postinstall.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Function to check if an application is running +is_app_running() { + app_name=$1 + pgrep -x "$app_name" >/dev/null +} + +# Open Docker Desktop +open -a "Docker.app" + +# Set the timeout duration (in seconds) +timeout=300 # 5 minutes + +# Wait for Docker Desktop to be running +echo "Waiting for Docker Desktop to start..." +start_time=$(date +%s) +while ! is_app_running "Docker Desktop"; do + current_time=$(date +%s) + elapsed_time=$((current_time - start_time)) + + if [ $elapsed_time -ge $timeout ]; then + echo "Error: Docker Desktop did not start within the specified timeout." + exit 1 + fi + + sleep 1 +done +echo "Docker Desktop is running." + +# Open MORAgents.app +open -a "MORAgents.app" + +echo "Post-install script completed." +exit 0 diff --git a/build_assets/macOS/preinstall.sh b/build_assets/macOS/preinstall.sh new file mode 100755 index 0000000..a18eddb --- /dev/null +++ b/build_assets/macOS/preinstall.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +python_version=$(python3 --version 2>&1 | awk '{print $2}') +if [[ "$python_version" < "3.12.0" ]]; then + echo "Installing Python 3.12.0..." + curl -O https://www.python.org/ftp/python/3.12.0/python-3.12.0-macos11.pkg + sudo installer -pkg python-3.12.0-macos11.pkg -target / + echo "Python 3.12.0 has been successfully installed." + rm python-3.12.0-macos11.pkg +else + echo "Python version is already up to date." +fi diff --git a/build_assets/macOS/preinstall_ollama.sh b/build_assets/macOS/preinstall_ollama.sh new file mode 100644 index 0000000..412743c --- /dev/null +++ b/build_assets/macOS/preinstall_ollama.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +curl -L https://github.com/ollama/ollama/releases/download/v0.3.6/ollama-darwin -o ollama + +chmod +x ollama + +sudo mv ollama /usr/local/bin/ + +nohup /usr/local/bin/ollama serve > /dev/null 2>&1 & +/usr/local/bin/ollama pull llama3 +/usr/local/bin/ollama pull nomic-embed-text diff --git a/build_assets/macOS/welcome.html b/build_assets/macOS/welcome.html new file mode 100644 index 0000000..585d5ea --- /dev/null +++ b/build_assets/macOS/welcome.html @@ -0,0 +1,44 @@ + + + + + + Welcome to MORagents v0.0.9 Installer + + + +

Welcome to MORagents v0.0.9 Installer

+

Thank you for choosing to install MORagents on your system. This installer will guide you through the process of setting up MORagents and its dependencies.

+

The installer will perform the following steps:

+ +

Please note that during the installation process, you may be prompted to enter your system password to authorize the installation of required components.

+

Click "Continue" to proceed with the installation.

+ + diff --git a/build_assets/windows/Packaging_Instructions_Windows.md b/build_assets/windows/Packaging_Instructions_Windows.md index 6e8910c..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: @@ -18,4 +18,4 @@ Windows Inno Setup for Wizard: 2) In the GUI, enter the text found in [wizard_windows.iss](../../wizard_windows.iss) 3) Click Build > Compile 4) Hit the Play/Run button on the top -5) Installer is the +5) Installer will run and create a Desktop icon to run the application. 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/images/moragents_chatpdf.png b/images/moragents_chatpdf.png new file mode 100644 index 0000000..d9777ce Binary files /dev/null and b/images/moragents_chatpdf.png differ diff --git a/main.py b/main.py index e01081c..f94c23e 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,8 @@ import logging import webbrowser -from runtime_setup_macos import docker_setup as docker_setup_macos -from runtime_setup_windows import docker_setup as docker_setup_windows +from runtime_setup_macos import main as macos_setup +from runtime_setup_windows import main as windows_setup from utils.logger_config import setup_logger from utils.host_utils import get_os_and_arch @@ -17,9 +17,9 @@ os_name, arch = get_os_and_arch() if os_name == "macOS": - docker_setup_macos() + macos_setup() elif os_name == "Windows": - docker_setup_windows() + windows_setup() elif os_name == "Linux": raise RuntimeError( f"MORagents needs Linux support! Would you like to help?\n" diff --git a/runtime_setup_macos.py b/runtime_setup_macos.py index 3c8da37..70f2d48 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,32 @@ 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 start_ollama_server(): + ollama_path = '/usr/local/bin/ollama' + + 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() @@ -148,32 +126,38 @@ 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) +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__": - docker_setup() + main() \ No newline at end of file diff --git a/runtime_setup_windows.py b/runtime_setup_windows.py index 22aebde..33de5d9 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,48 +92,33 @@ 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)) - +def migration_remove_old_images(): + for image_name in AgentDockerConfigDeprecate.OLD_IMAGE_NAMES: 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}'") + delete_docker_image(image_name) + logger.info(f"Deleted image '{image_name}' from previous release") - load_docker_image(image_path) - logger.info(f"Loaded docker image '{image_name}'") +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 start_ollama_server(): + ollama_path = "ollama" -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") + print(f"Attempting to start Ollama server using: {ollama_path}") + subprocess.Popen([ollama_path, "serve"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW) + print("Ollama server started successfully.") + except Exception as e: + print(f"Failed to start Ollama server: {e}") def docker_setup(): @@ -178,14 +128,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 +144,19 @@ 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) +def main(): + # main() called every time the app is opened (from main.py). Put all app open code here. + start_ollama_server() + docker_setup() if __name__ == "__main__": - docker_setup() + docker_setup() \ No newline at end of file diff --git a/submodules/benchmarks/adapters/base_adapter.py b/submodules/benchmarks/adapters/base_adapter.py new file mode 100644 index 0000000..323f1be --- /dev/null +++ b/submodules/benchmarks/adapters/base_adapter.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + +class BaseAdapter(ABC): + + @property + @abstractmethod + def name(self) -> str: + pass + + @abstractmethod + def get_price(self, coin_id: str) -> float: + pass + + def get_marketcap(self, coin_id: str) -> float: + pass + + def has_get_marketcap(self) -> bool: + pass + + def has_get_price(self) -> bool: + pass diff --git a/submodules/benchmarks/adapters/coingecko_adapter.py b/submodules/benchmarks/adapters/coingecko_adapter.py new file mode 100644 index 0000000..9327118 --- /dev/null +++ b/submodules/benchmarks/adapters/coingecko_adapter.py @@ -0,0 +1,30 @@ +import requests +from .base_adapter import BaseAdapter + +# defillama and coingecko share the same identifiers +class CoingeckoAdapter(BaseAdapter): + + @property + def name(self) -> str: + return "Coingecko" + + def get_price(self, coingecko_id: str) -> float: + url = f"https://api.coingecko.com/api/v3/simple/price" + params = {'ids': coingecko_id, 'vs_currencies': 'usd'} + response = requests.get(url, params=params) + response.raise_for_status() + return response.json()[coingecko_id]['usd'] + + def get_marketcap(self, coin_id: str) -> float: + request_url = f"https://api.coingecko.com/api/v3/coins/{coin_id}" + response = requests.get(request_url) + response.raise_for_status() + data = response.json() + marketcap = data['market_data']['market_cap']['usd'] + return marketcap + + def has_get_marketcap(self) -> bool: + return True + + def has_get_price(self) -> bool: + return True diff --git a/submodules/benchmarks/adapters/defillama_adapter.py b/submodules/benchmarks/adapters/defillama_adapter.py new file mode 100644 index 0000000..21bac2f --- /dev/null +++ b/submodules/benchmarks/adapters/defillama_adapter.py @@ -0,0 +1,22 @@ +import requests +from .base_adapter import BaseAdapter + +class DefillamaAdapter(BaseAdapter): + + @property + def name(self) -> str: + return "Defillama" + + def get_price(self, coingecko_id: str) -> float: + request_url = f"https://coins.llama.fi/prices/current/coingecko:{coingecko_id}" + response = requests.get(request_url) + response.raise_for_status() + data = response.json() + price = data['coins'][f'coingecko:{coingecko_id}']['price'] + return price + + def has_get_marketcap(self) -> bool: + return False + + def has_get_price(self) -> bool: + return True \ No newline at end of file diff --git a/submodules/benchmarks/benchmarks.py b/submodules/benchmarks/benchmarks.py new file mode 100644 index 0000000..d239015 --- /dev/null +++ b/submodules/benchmarks/benchmarks.py @@ -0,0 +1,83 @@ +import time +import argparse +from helpers import ask_data_agent, compare_usd_values, extract_agent_usd_value +from config import coins, price_prompts, mcap_prompts, price_error_tolerance, mcap_error_tolerance, loop_delay +from adapters.coingecko_adapter import CoingeckoAdapter +from adapters.defillama_adapter import DefillamaAdapter + +all_adapters = [ + DefillamaAdapter(), + CoingeckoAdapter() +] + +parser = argparse.ArgumentParser(description="Specify the type of prompts to use (price or mcap).") +parser.add_argument('type', choices=['price', 'mcap'], help="Type of prompts to use") +args = parser.parse_args() + +benchmark_type = args.type + +if benchmark_type == 'price': + prompts = price_prompts + error_tolerance = price_error_tolerance +elif benchmark_type == 'mcap': + prompts = mcap_prompts + error_tolerance = mcap_error_tolerance +else: + raise ValueError("Invalid benchmark type") + +total_checks = 0 +failures = [] + +print('benchmark type', benchmark_type) +try: + print() + for prompt in prompts: + for coin in coins: + coingecko_id = coin["coingecko_id"] + for name_variation in coin["name_variations"]: + agent_prompt = prompt.format(name_variation) + print(f"{agent_prompt}") + try: + agent_response = ask_data_agent(prompt.format(name_variation)) + agent_usd_value = extract_agent_usd_value(agent_response) + print(f"{agent_usd_value}") + time.sleep(loop_delay) # the agent gets rate limitted by coingecko if we call it too fast + except: + result = f"FAIL DataAgent: {agent_prompt}" + print(result) + print() + total_checks += 1 + failures.append(result) + continue + + for adapter in all_adapters: + print(f"Checking {adapter.name}...") + try: + if benchmark_type == "price" and adapter.has_get_price(): + benchmark_value = adapter.get_price(coingecko_id) + elif benchmark_type == "mcap" and adapter.has_get_marketcap(): + benchmark_value = adapter.get_marketcap(coingecko_id) + else: + continue + result = compare_usd_values(agent_usd_value, adapter, coingecko_id, name_variation, benchmark_value, error_tolerance, failures) + except Exception as e: + result = f"FAIL {adapter.name}: {coingecko_id} ({e})" + failures.append(result) + + print(result) + total_checks += 1 + print() + + # Summarize Results + passed_checks = total_checks - len(failures) + print() + print(f"{passed_checks} / {total_checks} Benchmarks passed") + + if len(failures) > 0: + print("Failures:") + for failure in failures: + print(failure) + +except Exception as e: + print(f"Unexpected error: {e}") + exit(1) diff --git a/submodules/benchmarks/config.py b/submodules/benchmarks/config.py new file mode 100644 index 0000000..34a764e --- /dev/null +++ b/submodules/benchmarks/config.py @@ -0,0 +1,270 @@ +loop_delay = 10 # prevent rate limiting + +price_error_tolerance = 0.01 # 1% +mcap_error_tolerance = 0.01 # 1% + +coins = [ + { + "coingecko_id": "bitcoin", + "name_variations": ["Bitcoin", "Bitcoins", "BITcoin", "BTC"] + }, + { + "coingecko_id": "ethereum", + "name_variations": ["Ether", "Ethereum", "Ethers", "ETH"] + }, + { + "coingecko_id": "tether", + "name_variations": ["Tether", "USDT", "USDTether", "TetherUSD"] + }, + { + "coingecko_id": "binancecoin", + "name_variations": ["BNB", "Binance Smart Chain", "Binancecoin", "Binance coin"] + }, + { + "coingecko_id": "solana", + "name_variations": ["Solana", "SOL", "Solanacoin"] + }, + { + "coingecko_id": "usd-coin", + "name_variations": ["USDC", "USDCoin", "USD coin", "CUSD"] + }, + { + "coingecko_id": "staked-ether", + "name_variations": ["Lido Staked Ether", "Lido Ether", "Lido Eth", "Staked Ether"] + }, + { + "coingecko_id": "ripple", + "name_variations": ["XRP", "Ripple", "XRPcoin", "XRP coin"] + }, + { + "coingecko_id": "the-open-network", + "name_variations": ["Toncoin", "TON", "the open network", "open network coin"] + }, + { + "coingecko_id": "dogecoin", + "name_variations": ["Dogecoin", "DOGE", "dogcoin", "doge coin"] + }, + { + "coingecko_id": "cardano", + "name_variations": ["Cardano", "ADA", "cardano coin"] + }, + { + "coingecko_id": "tron", + "name_variations": ["Tron", "TRX", "troncoin", "trxcoin"] + }, + { + "coingecko_id": "avalanche-2", + "name_variations": ["Avalanche", "AVAX", "Avalancecoin", "Avaxcoin"] + }, + { + "coingecko_id": "shiba-inu", + "name_variations": ["Shiba Inu", "Shiba", "SHIB", "shibacoin", "Shiba inu coin"] + }, + { + "coingecko_id": "wrapped-bitcoin", + "name_variations": ["Wrapped Bitcoin", "WBTC", "wrapped btc", "wrapped BITcoin"] + }, + { + "coingecko_id": "polkadot", + "name_variations": ["Polkadot", "DOT", "polkadotcoin", "polka dot coin", "polka coin"] + }, + { + "coingecko_id": "chainlink", + "name_variations": ["Chainlink", "LINK", "Chainlinkcoin", "Linkcoin"] + }, + { + "coingecko_id": "bitcoin-cash", + "name_variations": ["Bitcoin Cash", "BCH", "BTC cash", "BCHcoin"] + }, + { + "coingecko_id": "uniswap", + "name_variations": ["Uniswap", "UNI", "Unicoin", "Uniswap coin"] + }, + { + "coingecko_id": "leo-token", + "name_variations": ["LEO token", "LEO", "leotoken", "leocoin"] + }, + { + "coingecko_id": "dai", + "name_variations": ["Dai", "Daicoin", "DaiUSD"] + }, + { + "coingecko_id": "near", + "name_variations": ["Near Protocol", "NEAR", "Nearcoin"] + }, + { + "coingecko_id": "litecoin", + "name_variations": ["Litecoin", "LTC", "LTCcoin", "lightcoin"] + }, + { + "coingecko_id": "matic-network", + "name_variations": ["Polygon", "Matic", "MATIC", "Polygoncoin", "Maticcoin"] + }, + { + "coingecko_id": "wrapped-eeth", + "name_variations": ["Wrapped eETH", "eETH", "WEETH", "eETHcoin", "WEETHcoin"] + }, + { + "coingecko_id": "kaspa", + "name_variations": ["Kaspa", "KAS", "Kaspacoin", "Kascoin"] + }, + { + "coingecko_id": "pepe", + "name_variations": ["Pepe", "Pepecoin"] + }, + { + "coingecko_id": "ethena-usde", + "name_variations": ["Ethena", "USDE", "Ethena USD", "Ethenacoin", "USDEcoin"] + }, + { + "coingecko_id": "internet-computer", + "name_variations": ["Internet Computer", "ICP", "ICPcoin", "InternetComputercoin"] + }, + { + "coingecko_id": "renzo-restaked-eth", + "name_variations": ["Renzo Restaked ETH", "Renzo Restaked Ethereum", "Renzocoin", "RenzoEth"] + }, + { + "coingecko_id": "ethereum-classic", + "name_variations": ["Ethereum Classic", "Ether Classic", "ETH Classic", "ETC"] + }, + { + "coingecko_id": "fetch-ai", + "name_variations": [ + "Artificial Superintelligence Alliance", "FET", "FETcoin", "Fetch", + "Ocean Protocol", "Oceancoin", "Singularity", "Singularitycoin" + ] + }, + { + "coingecko_id": "monero", + "name_variations": ["Monero", "XMR", "Monerocoin", "XMRcoin"] + }, + { + "coingecko_id": "aptos", + "name_variations": ["Aptos", "APT", "Aptoscoin", "APTcoin"] + }, + { + "coingecko_id": "render-token", + "name_variations": ["Render", "RNDR", "Rendercoin", "RNDRcoin", "Render token"] + }, + { + "coingecko_id": "stellar", + "name_variations": ["Stellar", "XLM", "Stellarcoin", "XLMcoin"] + }, + { + "coingecko_id": "hedera-hashgraph", + "name_variations": ["Hedera", "HBAR", "Hederacoin", "HBARcoin", "Hashgraph"] + }, + { + "coingecko_id": "cosmos", + "name_variations": ["Cosmos", "Cosmoshub", "ATOM", "Cosmoscoin", "ATOMCoin"] + }, + { + "coingecko_id": "arbitrum", + "name_variations": ["Arbitrum", "ARB", "Arbitrumcoin", "ARBCoin"] + }, + { + "coingecko_id": "crypto-com-chain", + "name_variations": ["Cronos", "CRO", "Cronoscoin", "CROCoin", "Crypto.com"] + }, + { + "coingecko_id": "mantle", + "name_variations": ["Mantle", "MNT", "Mantlecoin", "MNTCoin"] + }, + { + "coingecko_id": "blockstack", + "name_variations": ["Stacks", "STX", "Stackscoin", "STXCoin", "Blockstack"] + }, + { + "coingecko_id": "filecoin", + "name_variations": ["Filecoin", "FIL", "FILCoin", "File coin"] + }, + { + "coingecko_id": "okb", + "name_variations": ["OKB", "OKBCoin"] + }, + { + "coingecko_id": "maker", + "name_variations": ["Maker", "MKR", "MakerDAO", "Makercoin", "MRKCoin"] + }, + { + "coingecko_id": "vechain", + "name_variations": ["VeChain", "VET", "VeChaincoin", "VETCoin"] + }, + { + "coingecko_id": "injective-protocol", + "name_variations": ["Injective", "INJ", "Injectivecoin", "INJCoin", "Injective Protocol"] + }, + { + "coingecko_id": "immutable-x", + "name_variations": ["Immutable", "IMX", "Immutablecoin", "IMXCoin", "ImmutableX"] + }, + { + "coingecko_id": "first-digital-usd", + "name_variations": ["First Digital USD", "FDUSD", "FirstDigitalUSD", "FDUSDCoin"] + }, + { + "coingecko_id": "optimism", + "name_variations": ["Optimism", "OP", "Optimismcoin", "OPCoin"] + }, + { + "coingecko_id": "morpheusai", + "name_variations": ["Morpheus", "MorpheusAI", "MOR", "MORCoin"] + }, + { + "coingecko_id": "aave", + "name_variations": ["Aave", "Aavecoin"] + }, + { + "coingecko_id": "aavegotchi", + "name_variations": ["Aavegotchi", "Ghost", "Aavegotchicoin", "Ghostcoin", "GHST"] + }, + { + "coingecko_id": "thorchain", + "name_variations": ["Thor", "THORChain", "RUNE", "Thorcoin", "Runecoin"] + }, + { + "coingecko_id": "ethereum-name-service", + "name_variations": ["Ethereum Name Service", "ENS", "ENScoin"] + }, + { + "coingecko_id": "axie-infinity", + "name_variations": ["Axie Infinity", "AXS", "Axiecoin", "Axscoin"] + }, + { + "coingecko_id": "zombiecoin", + "name_variations": ["ZombieCoin", "Zombie", "ZMBCoin"] + }, + { + "coingecko_id": "elon-xmas", + "name_variations": ["Elon Xmas", "XMAS", "ElonXmascoin", "XMASCoin"] + }, + { + "coingecko_id": "neblio", + "name_variations": ["Neblio", "NEBL", "Nebliocoin", "NEBLCoin"] + }, + { + "coingecko_id": "shapeshift-fox-token", + "name_variations": ["ShapeShift FOX", "ShapeShift", "ShapeShiftCoin", "FOXCoin", "FOXToken"] + } +] + +price_prompts = [ + "What is the price of {}?", + "How much is one {} worth?", + "How much does one {} cost?", +] + +mcap_prompts = [ + "What is the market cap of {}?", + "What is the total market cap of {}?", + "What is the marketcap of {}?", + "What is the market capitalization of {}?", + "What is the value of all {}s?", +] + +# This is unused at the moment +tvl_prompts = [ + "What is the TVL of {}?", + "How much value is locked in {}?", +] \ No newline at end of file diff --git a/submodules/benchmarks/helpers.py b/submodules/benchmarks/helpers.py new file mode 100644 index 0000000..e50c767 --- /dev/null +++ b/submodules/benchmarks/helpers.py @@ -0,0 +1,54 @@ +import requests +import json +import re +from adapters.base_adapter import BaseAdapter + +url = 'http://127.0.0.1:8080/data_agent/' + +headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Connection': 'keep-alive', + 'Content-Type': 'application/json', + 'Origin': 'http://localhost:3333', + 'Referer': 'http://localhost:3333/', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', +} + +def ask_data_agent(prompt: str): + payload = { + "prompt": { + "role": "user", + "content": prompt + } + } + + response = requests.post(url, headers=headers, data=json.dumps(payload)) + + result_content = response.json()['content'] + return result_content + +def extract_agent_usd_value(content: str): + match = re.search(r'\$\d+(?:,\d{3})*(?:\.\d{1,8})?', content) # 8 usd digits should be plenty + if match: + price_str = match.group(0).replace('$', '').replace(',', '') + return float(price_str) + raise ValueError("Could not extract a price from the agent response") + +def compare_usd_values(agent_value: float, adapter: BaseAdapter, coingecko_id: str, name_variation: str, benchmark_value: float, error_tolerance: float, failures: list): + difference = abs(agent_value - benchmark_value) + percent_difference = (difference / benchmark_value) * 100 + result_value = f"${benchmark_value:.8f}, {percent_difference:.2f}% off" + if percent_difference <= error_tolerance * 100: + result_message = f"PASS {adapter.name}. {result_value}" + else: + result_message = f"FAIL {adapter.name}. {result_value}" + failure_message = f"FAIL {adapter.name}. {result_value}. {coingecko_id}. {name_variation}" # so we have more information to summarize failures at the end + failures.append(result_message) + return result_message diff --git a/submodules/benchmarks/readme.md b/submodules/benchmarks/readme.md new file mode 100644 index 0000000..eef0967 --- /dev/null +++ b/submodules/benchmarks/readme.md @@ -0,0 +1,22 @@ +# Data Agent Benchmarking Tests + +## About + +- Measures how accurately the data agent responds for price & market cap vs benchmark values from coinecko, defillama, etc. + +- Generates agent prompts from values defined in `config.py` and validates them against each of the benchmark adapters. + +## Running + +## 0. Start [ DataAgent Docker Service](../moragents_dockers/agents/src/data_agent/README.md) +## 1. Modify `config.py` with new prompts, coins & error tolerances +## 2. `cd submodules/benchmarks` +## 3. Run `pip install -r requirements.txt` +## 4. Run `python benchmarks.py price` for price benchmarks +## 5. Run `python benchmarks.py mcap` for market cap benchmarks + +## Considerations + +- The source of truth asset id is the coingecko id. Any new adapters will need some way of translating the coingecko id if they use something else. For example, the defillama adapter "just works" because they use coingecko ids. + +- Disabling coingecko adapter lets us reduce the `time.sleep()` in `benchmarks.py` and run much faster (rate limiting). diff --git a/submodules/benchmarks/requirements.txt b/submodules/benchmarks/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/submodules/benchmarks/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/submodules/moragents_dockers/README.md b/submodules/moragents_dockers/README.md index 7d8f158..45fa221 100644 --- a/submodules/moragents_dockers/README.md +++ b/submodules/moragents_dockers/README.md @@ -3,14 +3,37 @@ This repo contains multiple agents and a dapp that enables you to interact with the agents, all running locally and containerized with Docker. -## Usage -Docker compose will build and run two containers. One will be for the agents, the other will be for the UI. -```docker-compose up``` +## Dependencies + +* Docker +* Ollama + +Pull the required models in ollama + +```ollama pull llama3``` + +```ollama pull nomic-embed-text``` -or for Apple silicon -```docker-compose -f docker-compose-apple.yml up``` +## Installation + +Docker compose will build and run two containers. One will be for the agents, the other will be for the UI. + +1. Ensure you're in the submodules/moragents_dockers folder + ```sh + $ cd submodules/moragents_dockers + ``` + +2. Build Images and Launch Containers: + 1. For Intel / AMD / x86_64 + ```sh + docker-compose up + ``` + 2. For Apple silicon (M1, M2, M3, etc) + ```sh + docker-compose -f docker-compose-apple.yml up + ``` Open in the browser: ```http://localhost:3333/``` @@ -44,6 +67,6 @@ A typical flow looks like this: - If the user accepts the quote, the swap may proceed. The back-end will generate transactions which will be sent to the front-end to be signed by the user's wallet. - If the allowance for the token being sold is too low, an approval transaction will be generated first +### RAG Agent - - +This agent will answer questions about an uploaded PDF file. diff --git a/submodules/moragents_dockers/agents/Dockerfile b/submodules/moragents_dockers/agents/Dockerfile index adf0198..060da90 100644 --- a/submodules/moragents_dockers/agents/Dockerfile +++ b/submodules/moragents_dockers/agents/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-slim-buster +FROM --platform=linux/amd64 python:3.10-bullseye # Set the working directory in the container WORKDIR /app diff --git a/submodules/moragents_dockers/agents/Dockerfile-apple b/submodules/moragents_dockers/agents/Dockerfile-apple index 5dc0620..ef7ee27 100644 --- a/submodules/moragents_dockers/agents/Dockerfile-apple +++ b/submodules/moragents_dockers/agents/Dockerfile-apple @@ -1,4 +1,4 @@ -FROM arm64v8/python:3.10-bullseye +FROM --platform=linux/arm64 python:3.10-bullseye # Set the working directory in the container WORKDIR /app diff --git a/submodules/moragents_dockers/agents/pytest.ini b/submodules/moragents_dockers/agents/pytest.ini new file mode 100644 index 0000000..72a01cc --- /dev/null +++ b/submodules/moragents_dockers/agents/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = . +addopts = --import-mode=importlib -p no:warnings \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/requirements.txt b/submodules/moragents_dockers/agents/requirements.txt index 08ee92b..c3838ef 100644 --- a/submodules/moragents_dockers/agents/requirements.txt +++ b/submodules/moragents_dockers/agents/requirements.txt @@ -1,11 +1,15 @@ -llama-cpp-python -transformers -sentencepiece -protobuf -scikit-learn -huggingface-hub +llama-cpp-python==0.2.65 +transformers==4.43.3 +sentencepiece==0.2.0 +protobuf==5.27.2 +scikit-learn==1.5.1 +huggingface-hub==0.24.3 flask==2.2.2 Werkzeug==2.2.2 -gradio > /dev/null -flask-cors -web3 \ No newline at end of file +flask-cors==4.0.1 +web3==6.20.1 +pymupdf==1.22.5 +faiss-cpu==1.8.0.post1 +langchain-text-splitters==0.2.2 +langchain-core==0.2.24 +langchain-community==0.2.10 \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/app.py b/submodules/moragents_dockers/agents/src/app.py index 26ddb8e..65d225c 100644 --- a/submodules/moragents_dockers/agents/src/app.py +++ b/submodules/moragents_dockers/agents/src/app.py @@ -3,8 +3,16 @@ from config import Config from swap_agent.src import agent as swap_agent from data_agent.src import agent as data_agent +from rag_agent.src import agent as rag_agent from llama_cpp import Llama from llama_cpp.llama_tokenizer import LlamaHFTokenizer +import os +import logging +from langchain_community.llms import Ollama +from langchain_community.embeddings import OllamaEmbeddings +from langchain_core.prompts import ChatPromptTemplate +from rag_agent.src.config import Config as ollama_config + def load_llm(): @@ -24,6 +32,32 @@ def load_llm(): app = Flask(__name__) CORS(app) +upload_state=False +UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads') +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config['MAX_CONTENT_LENGTH'] = ollama_config.MAX_LENGTH + +llm_ollama = Ollama(model="llama3",base_url=ollama_config.URL) +embeddings = OllamaEmbeddings(model="nomic-embed-text",base_url=ollama_config.URL) + +logging.basicConfig(level=logging.DEBUG) + + +agent = None +messages=[] +prompt = ChatPromptTemplate.from_template( + """ + Answer the following question only based on the given context + + + {context} + + + Question: {input} +""" +) + @app.route('/swap_agent/', methods=['POST']) def swap_agent_chat(): global llm @@ -67,6 +101,23 @@ def data_agent_messages(): def data_agent_clear_messages(): return data_agent.clear_messages() +@app.route('/rag_agent/upload', methods=['POST']) +def rag_agent_upload(): + global llm_ollama,UPLOAD_FOLDER,embeddings + return rag_agent.upload_file(request, UPLOAD_FOLDER, llm_ollama, embeddings,ollama_config.MAX_FILE_SIZE) + +@app.route('/rag_agent/', methods=['POST']) +def rag_agent_chat(): + return rag_agent.chat(request) + +@app.route('/rag_agent/messages', methods=['GET']) +def rag_agent_messages(): + return rag_agent.get_messages() + +@app.route('/rag_agent/clear_messages', methods=['GET']) +def rag_agent_clear_messages(): + return rag_agent.clear_messages() + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/config.py b/submodules/moragents_dockers/agents/src/config.py index 067d58d..a0868d3 100644 --- a/submodules/moragents_dockers/agents/src/config.py +++ b/submodules/moragents_dockers/agents/src/config.py @@ -10,7 +10,7 @@ class Config: MODEL_REVISION = "functionary-small-v2.4.Q4_0.gguf" MODEL_PATH = "model/"+MODEL_REVISION DOWNLOAD_DIR = "model" - # Data agent + # API endpoints COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" DEFILLAMA_BASE_URL = "https://api.llama.fi" PRICE_SUCCESS_MESSAGE = "The price of {coin_name} is ${price:,}" @@ -24,7 +24,6 @@ class Config: MARKET_CAP_SUCCESS_MESSAGE = "The market cap of {coin_name} is ${market_cap:,}" MARKET_CAP_FAILURE_MESSAGE = "Failed to retrieve market cap. Please enter a valid coin name." API_ERROR_MESSAGE = "I can't seem to access the API at the moment." - # Swap agent INCH_URL = "https://api.1inch.dev/token" QUOTE_URL = "https://api.1inch.dev/swap" APIBASEURL = f"https://api.1inch.dev/swap/v6.0/" diff --git a/submodules/moragents_dockers/agents/src/data_agent/src/data_agent_config.py b/submodules/moragents_dockers/agents/src/data_agent/src/data_agent_config.py new file mode 100644 index 0000000..0c916ec --- /dev/null +++ b/submodules/moragents_dockers/agents/src/data_agent/src/data_agent_config.py @@ -0,0 +1,27 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + +# Configuration object +class Config: + # Model configuration + MODEL_NAME = "meetkai/functionary-small-v2.4-GGUF" + MODEL_REVISION = "functionary-small-v2.4.Q4_0.gguf" + MODEL_PATH = "model/"+MODEL_REVISION + DOWNLOAD_DIR = "model" + # API endpoints + COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" + DEFILLAMA_BASE_URL = "https://api.llama.fi" + PRICE_SUCCESS_MESSAGE = "The price of {coin_name} is ${price:,}" + PRICE_FAILURE_MESSAGE = "Failed to retrieve price. Please enter a valid coin name." + FLOOR_PRICE_SUCCESS_MESSAGE = "The floor price of {nft_name} is ${floor_price:,}" + FLOOR_PRICE_FAILURE_MESSAGE = "Failed to retrieve floor price. Please enter a valid NFT name." + TVL_SUCCESS_MESSAGE = "The TVL of {protocol_name} is ${tvl:,}" + TVL_FAILURE_MESSAGE = "Failed to retrieve TVL. Please enter a valid protocol name." + FDV_SUCCESS_MESSAGE = "The fully diluted valuation of {coin_name} is ${fdv:,}" + FDV_FAILURE_MESSAGE = "Failed to retrieve FDV. Please enter a valid coin name." + MARKET_CAP_SUCCESS_MESSAGE = "The market cap of {coin_name} is ${market_cap:,}" + MARKET_CAP_FAILURE_MESSAGE = "Failed to retrieve market cap. Please enter a valid coin name." + API_ERROR_MESSAGE = "I can't seem to access the API at the moment." + \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py b/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py new file mode 100644 index 0000000..99e76ea --- /dev/null +++ b/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py @@ -0,0 +1,97 @@ +from flask import jsonify +import os +import logging +from langchain_community.document_loaders import PyMuPDFLoader +from langchain_community.vectorstores import FAISS +from langchain_core.prompts import ChatPromptTemplate +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain.chains import create_retrieval_chain +from werkzeug.utils import secure_filename + + +logging.basicConfig(level=logging.DEBUG) + + +agent = None +messages=[{'role':"assistant","content":"Please upload a file to begin"}] +upload_state = False +prompt = ChatPromptTemplate.from_template( + """ + Answer the following question only based on the given context + + + {context} + + + Question: {input} +""" +) + +def handle_file_upload(file,UPLOAD_FOLDER,llm,embeddings): + global agent,prompt + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + filename = secure_filename(file.filename) + file.save(os.path.join(UPLOAD_FOLDER, filename)) + # DocumentToolsGenerator class instantiation + loader = PyMuPDFLoader(os.path.join(UPLOAD_FOLDER,filename)) + docs = loader.load() + text_splitter = RecursiveCharacterTextSplitter() + split_documents = text_splitter.split_documents(docs) + vector_store = FAISS.from_documents(split_documents, embeddings) + docs_chain = create_stuff_documents_chain(llm, prompt) + retriever = vector_store.as_retriever() + agent = create_retrieval_chain(retriever, docs_chain) + + +def upload_file(request,UPLOAD_FOLDER,llm,embeddings,MAX_SIZE): + global upload_state + if 'file' not in request.files: + return jsonify({'error': 'No file part'}), 400 + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No selected file'}), 400 + # Check file size + file.seek(0, os.SEEK_END) + file_length = file.tell() + file.seek(0, 0) # Reset the file pointer to the beginning + if file_length > MAX_SIZE: + messages.append({"role": "assistant", "content": 'please use a file less than 5 MB'}) + return jsonify({"role": "assistant", "content": 'please use a file less than 5 MB'}) + try: + handle_file_upload(file,UPLOAD_FOLDER,llm,embeddings) + upload_state = True + messages.append({"role": "assistant", "content": 'You have successfully uploaded the text'}) + return jsonify({"role": "assistant", "content": 'You have successfully uploaded the text'}) + except Exception as e: + logging.error(f'Error during file upload: {str(e)}') + return jsonify({'error': str(e)}), 500 + +def chat(request): + global messages,upload_state,agent + try: + data = request.get_json() + if 'prompt' in data: + prompt = data['prompt']['content'] + messages.append(data['prompt']) + role = "assistant" + response = agent.invoke({"input": prompt}) if upload_state else {"answer":"please upload a file first"} + + messages.append({"role": role, "content": response["answer"]}) + return jsonify({"role": role, "content": response["answer"]}) + else: + return jsonify({"error": "Missing required parameters"}), 400 + except Exception as e: + logging.error(f'Error in chat endpoint: {str(e)}') + return jsonify({"Error": str(e)}), 500 + +def get_messages(): + global messages + return jsonify({"messages": messages}) + +def clear_messages(): + global messages + messages = [{'role':"assistant","content":"Please upload a file to begin"}] + return jsonify({"response": "successfully cleared message history"}) + diff --git a/submodules/moragents_dockers/agents/src/rag_agent/src/config.py b/submodules/moragents_dockers/agents/src/rag_agent/src/config.py new file mode 100644 index 0000000..ad1952d --- /dev/null +++ b/submodules/moragents_dockers/agents/src/rag_agent/src/config.py @@ -0,0 +1,10 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + +# Configuration object +class Config: + MAX_FILE_SIZE=5 * 1024 * 1024 # 5 MB + MAX_LENGTH=16 * 1024 * 1024 + URL="http://host.docker.internal:11434" \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/swap_agent/src/config.py b/submodules/moragents_dockers/agents/src/swap_agent/src/config.py index b91a88f..d54cea4 100644 --- a/submodules/moragents_dockers/agents/src/swap_agent/src/config.py +++ b/submodules/moragents_dockers/agents/src/swap_agent/src/config.py @@ -15,7 +15,7 @@ class Config: QUOTE_URL = "https://api.1inch.dev/swap" APIBASEURL = f"https://api.1inch.dev/swap/v6.0/" HEADERS = { "Authorization": "Bearer WvQuxaMYpPvDiiOL5RHWUm7OzOd20nt4", "accept": "application/json" } - WEB3RPCURL = {"56":"https://bsc-dataseed.binance.org","42161":"https://arb1.arbitrum.io/rpc","137":"https://polygon-rpc.com","1":"https://cloudflare-eth.com","10":"https://mainnet.optimism.io","8453":"https://mainnet.base.org"} + WEB3RPCURL = {"56":"https://bsc-dataseed.binance.org","42161":"https://arb1.arbitrum.io/rpc","137":"https://polygon-rpc.com","1":"https://eth.llamarpc.com/","10":"https://mainnet.optimism.io","8453":"https://mainnet.base.org"} NATIVE_TOKENS={"137":"MATIC","56":"BNB","1":"ETH","42161":"ETH","10":"ETH","8453":"ETH"} ERC20_ABI = [ {"constant": True, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": False, "stateMutability": "view", "type": "function"}, diff --git a/submodules/moragents_dockers/agents/src/swap_agent/src/swap_agent_config.py b/submodules/moragents_dockers/agents/src/swap_agent/src/swap_agent_config.py new file mode 100644 index 0000000..b91a88f --- /dev/null +++ b/submodules/moragents_dockers/agents/src/swap_agent/src/swap_agent_config.py @@ -0,0 +1,25 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + +# Configuration object +class Config: + # Model configuration + MODEL_NAME = "meetkai/functionary-small-v2.4-GGUF" + MODEL_REVISION = "functionary-small-v2.4.Q4_0.gguf" + MODEL_PATH = "model/"+MODEL_REVISION + DOWNLOAD_DIR = "model" + # API endpoints + INCH_URL = "https://api.1inch.dev/token" + QUOTE_URL = "https://api.1inch.dev/swap" + APIBASEURL = f"https://api.1inch.dev/swap/v6.0/" + HEADERS = { "Authorization": "Bearer WvQuxaMYpPvDiiOL5RHWUm7OzOd20nt4", "accept": "application/json" } + WEB3RPCURL = {"56":"https://bsc-dataseed.binance.org","42161":"https://arb1.arbitrum.io/rpc","137":"https://polygon-rpc.com","1":"https://cloudflare-eth.com","10":"https://mainnet.optimism.io","8453":"https://mainnet.base.org"} + NATIVE_TOKENS={"137":"MATIC","56":"BNB","1":"ETH","42161":"ETH","10":"ETH","8453":"ETH"} + ERC20_ABI = [ + {"constant": True, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": False, "stateMutability": "view", "type": "function"}, + {"constant": True, "inputs": [{"name": "_owner", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "balance", "type": "uint256"}], "payable": False, "stateMutability": "view", "type": "function"} + ] + INCH_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/tests/data_agent/test_agent.py b/submodules/moragents_dockers/agents/tests/data_agent/test_agent.py new file mode 100644 index 0000000..b44936f --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/data_agent/test_agent.py @@ -0,0 +1,95 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'data_agent', 'src'))) +# The above sys/os lines are needed because the agents have identical namings (i.e. agent.py, tools.py) + +import pytest +from unittest.mock import patch, MagicMock +from src.data_agent.src.agent import get_response, generate_response, chat, get_messages, clear_messages +from flask import Flask, request + +@pytest.fixture +def mock_llm(): + mock = MagicMock() + mock.create_chat_completion.return_value = { + "choices": [ + { + "message": { + "content": "This is a test response" + } + } + ] + } + return mock + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + +def test_get_response(mock_llm): + message = [{"role": "user", "content": "Hello"}] + response, role = get_response(message, mock_llm) + assert response == "This is a test response" + assert role == "assistant" + +def test_get_response_with_tool_call(mock_llm): + mock_llm.create_chat_completion.return_value = { + "choices": [ + { + "message": { + "tool_calls": [ + { + "function": { + "name": "get_price", + "arguments": '{"coin_name": "bitcoin"}' + } + } + ] + } + } + ] + } + + with patch('agent.tools.get_coin_price_tool') as mock_price_tool: + mock_price_tool.return_value = "The price of bitcoin is $50,000" + message = [{"role": "user", "content": "What's the price of Bitcoin?"}] + response, role = get_response(message, mock_llm) + + assert response == "The price of bitcoin is $50,000" + assert role == "assistant" + +def test_generate_response(mock_llm): + prompt = {"role": "user", "content": "Hello"} + response, role = generate_response(prompt, mock_llm) + assert response == "This is a test response" + assert role == "assistant" + +def test_chat(app, mock_llm): + with app.test_request_context(json={"prompt": "Hello"}): + with patch('agent.generate_response', return_value=("This is a test response", "assistant")): + response, status_code = chat(request, mock_llm) + + assert status_code == 200 + assert response.json == {"role": "assistant", "content": "This is a test response"} + +def test_chat_missing_prompt(app): + with app.test_request_context(json={}): + response, status_code = chat(request, None) + + assert status_code == 400 + assert "error" in response.json + +def test_get_messages(app): + with app.test_request_context(): + response = get_messages() + + assert response.status_code == 200 + assert "messages" in response.json + +def test_clear_messages(app): + with app.test_request_context(): + response = clear_messages() + + assert response.status_code == 200 + assert response.json["response"] == "successfully cleared message history" \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/tests/data_agent/test_tools.py b/submodules/moragents_dockers/agents/tests/data_agent/test_tools.py new file mode 100644 index 0000000..2cbce79 --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/data_agent/test_tools.py @@ -0,0 +1,82 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'data_agent', 'src'))) +# The above sys/os lines are needed because the agents have identical namings (i.e. agent.py, tools.py) + +import pytest +from unittest.mock import patch, MagicMock + +from src.data_agent.src.tools import ( + get_coingecko_id, + get_price, + get_floor_price, + get_fdv, + get_market_cap, + get_protocol_tvl, + get_coin_price_tool, + get_nft_floor_price_tool, + get_fully_diluted_valuation_tool, + get_coin_market_cap_tool, +) + +# Mock responses +mock_coingecko_search = { + 'coins': [{'id': 'bitcoin'}], + 'nfts': [{'id': 'bored-ape-yacht-club'}] +} +mock_price_response = {'bitcoin': {'usd': 50000}} +mock_floor_price_response = {'floor_price': {'usd': 100000}} +mock_fdv_response = {'market_data': {'fully_diluted_valuation': {'usd': 1000000000000}}} +mock_market_cap_response = [{'market_cap': 500000000000}] +mock_tvl_response = {'tvl': 10000000000} + +@pytest.fixture +def mock_requests_get(): + with patch('requests.get') as mock_get: + mock_get.return_value = MagicMock() + mock_get.return_value.json.return_value = {} + yield mock_get + +def test_get_coingecko_id_coin(mock_requests_get): + mock_requests_get.return_value.json.return_value = mock_coingecko_search + assert get_coingecko_id('bitcoin', type='coin') == 'bitcoin' + +def test_get_coingecko_id_nft(mock_requests_get): + mock_requests_get.return_value.json.return_value = mock_coingecko_search + assert get_coingecko_id('bored ape', type='nft') == 'bored-ape-yacht-club' + +def test_get_price(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_price_response] + assert get_price('bitcoin') == 50000 + +def test_get_floor_price(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_floor_price_response] + assert get_floor_price('bored ape') == 100000 + +def test_get_fdv(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_fdv_response] + assert get_fdv('bitcoin') == 1000000000000 + +def test_get_market_cap(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_market_cap_response] + assert get_market_cap('bitcoin') == 500000000000 + +def test_get_coin_price_tool(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_price_response] + result = get_coin_price_tool('bitcoin') + assert "The price of bitcoin is $50,000" in result + +def test_get_nft_floor_price_tool(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_floor_price_response] + result = get_nft_floor_price_tool('bored ape') + assert "The floor price of bored ape is $100,000" in result + +def test_get_fully_diluted_valuation_tool(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_fdv_response] + result = get_fully_diluted_valuation_tool('bitcoin') + assert "The fully diluted valuation of bitcoin is $1,000,000,000,000" in result + +def test_get_coin_market_cap_tool(mock_requests_get): + mock_requests_get.return_value.json.side_effect = [mock_coingecko_search, mock_market_cap_response] + result = get_coin_market_cap_tool('bitcoin') + assert "The market cap of bitcoin is $500,000,000,000" in result \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/tests/swap_agent/test_agent.py b/submodules/moragents_dockers/agents/tests/swap_agent/test_agent.py new file mode 100644 index 0000000..82f0fab --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/swap_agent/test_agent.py @@ -0,0 +1,126 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'swap_agent', 'src'))) +# The above sys/os lines are needed because the agents have identical namings (i.e. agent.py, tools.py) + +import pytest +from unittest.mock import patch, MagicMock +from flask import Flask, request +from src.swap_agent.src.agent import get_response, generate_response, chat, get_messages, clear_messages, get_allowance, approve, swap + +@pytest.fixture +def mock_llm(): + mock = MagicMock() + mock.create_chat_completion.return_value = { + "choices": [ + { + "message": { + "content": "This is a test response" + } + } + ] + } + return mock + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + +def test_get_response(mock_llm): + message = [{"role": "user", "content": "Hello"}] + response, role = get_response(message, "chain_id", "wallet_address", mock_llm) + assert response == "This is a test response" + assert role == "assistant" + +def test_get_response_with_tool_call(mock_llm): + mock_llm.create_chat_completion.return_value = { + "choices": [ + { + "message": { + "tool_calls": [ + { + "function": { + "name": "swap_agent", + "arguments": '{"token1": "ETH", "token2": "USDT", "value": "1.0"}' + } + } + ] + } + } + ] + } + + with patch('src.swap_agent.src.tools.swap_coins') as mock_swap_coins: + mock_swap_coins.return_value = ("Swap successful", "assistant") + message = [{"role": "user", "content": "Swap 1 ETH for USDT"}] + response, role = get_response(message, "chain_id", "wallet_address", mock_llm) + + assert response == "Swap successful" + assert role == "assistant" + +def test_generate_response(mock_llm): + prompt = {"role": "user", "content": "Hello"} + response, role = generate_response(prompt, "chain_id", "wallet_address", mock_llm) + assert response == "This is a test response" + assert role == "assistant" + +def test_chat(app, mock_llm): + with app.test_request_context(json={"prompt": "Hello", "wallet_address": "0x123", "chain_id": "1"}): + with patch('src.swap_agent.src.agent.generate_response', return_value=("This is a test response", "assistant")): + response = chat(request, mock_llm) + + assert response.status_code == 200 + assert response.json == {"role": "assistant", "content": "This is a test response"} + +def test_chat_missing_prompt(app): + with app.test_request_context(json={}): + response, status_code = chat(request, None) + + assert status_code == 400 + assert "error" in response.json + +def test_get_messages(app): + with app.test_request_context(): + response = get_messages() + + assert response.status_code == 200 + assert "messages" in response.json + +def test_clear_messages(app): + with app.test_request_context(): + response = clear_messages() + + assert response.status_code == 200 + assert response.json["response"] == "successfully cleared message history" + +def test_get_allowance(app): + with app.test_request_context(json={"tokenAddress": "0x123", "walletAddress": "0x456", "chain_id": "1"}): + with patch('src.swap_agent.src.agent.check_allowance', return_value={"allowance": "1000"}): + response = get_allowance(request) + + assert response.status_code == 200 + assert "response" in response.json + +def test_approve(app): + with app.test_request_context(json={"tokenAddress": "0x123", "chain_id": "1", "amount": "1000"}): + with patch('src.swap_agent.src.agent.approve_transaction', return_value={"txHash": "0x789"}): + response = approve(request) + + assert response.status_code == 200 + assert "response" in response.json + +def test_swap(app): + swap_params = { + "src": "ETH", + "dst": "USDT", + "walletAddress": "0x123", + "amount": "1.0", + "slippage": "1", + "chain_id": "1" + } + with app.test_request_context(json=swap_params): + with patch('src.swap_agent.src.agent.build_tx_for_swap', return_value={"txHash": "0x789"}): + response = swap(request) + + assert {'txHash': '0x789'} == response diff --git a/submodules/moragents_dockers/agents/tests/swap_agent/test_tools.py b/submodules/moragents_dockers/agents/tests/swap_agent/test_tools.py new file mode 100644 index 0000000..66b8004 --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/swap_agent/test_tools.py @@ -0,0 +1,114 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'swap_agent', 'src'))) +# The above sys/os lines are needed because the agents have identical namings (i.e. agent.py, tools.py) + +import pytest +from unittest.mock import patch, MagicMock +from web3 import Web3 +from src.swap_agent.src.tools import ( + search_tokens, get_token_balance, eth_to_wei, validate_swap, + get_quote, get_token_decimals, convert_to_smallest_unit, + convert_to_readable_unit, swap_coins, InsufficientFundsError, + TokenNotFoundError, SwapNotPossibleError +) + +@pytest.fixture +def mock_web3(): + mock = MagicMock(spec=Web3) + mock.eth = MagicMock() + mock.eth.get_balance = MagicMock(return_value=1000) + mock.eth.contract = MagicMock() + return mock + +def test_search_tokens(): + with patch('requests.get') as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = [{"symbol": "ETH", "address": "0x123"}] + result = search_tokens("ETH", 1) + assert result[0]["symbol"] == "ETH" + +def test_get_token_balance(mock_web3): + mock_web3.eth.get_balance.return_value = 1000 + balance = get_token_balance(mock_web3, "0x456", "", []) + assert balance == 1000 + +def test_eth_to_wei(): + assert eth_to_wei(1) == 10**18 + +def test_validate_swap(mock_web3): + with patch('src.swap_agent.src.tools.search_tokens') as mock_search: + mock_search.side_effect = [ + [{"symbol": "ETH", "address": "0x0000000000000000000000000000000000000000"}], + [{"symbol": "DAI", "address": "0x123"}] + ] + mock_web3.eth.get_balance.return_value = 10**18 + result = validate_swap(mock_web3, "ETH", "DAI", 1, 1, "0x456") + assert result[1] == "ETH" + +def test_get_quote(): + with patch('requests.get') as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {"dstAmount": "1000000000000000000"} + result = get_quote("0x123", "0x456", 10**18, 1) + assert result["dstAmount"] == "1000000000000000000" + +def test_get_token_decimals(mock_web3): + mock_contract = MagicMock() + mock_contract.functions.decimals.return_value.call.return_value = 18 + mock_web3.eth.contract.return_value = mock_contract + assert get_token_decimals(mock_web3, "0x1234567890123456789012345678901234567890") == 18 + +def test_convert_to_smallest_unit(mock_web3): + with patch('src.swap_agent.src.tools.get_token_decimals', return_value=18): + assert convert_to_smallest_unit(mock_web3, 1, "0x123") == 10**18 + +def test_convert_to_readable_unit(mock_web3): + with patch('src.swap_agent.src.tools.get_token_decimals', return_value=18): + assert convert_to_readable_unit(mock_web3, 10**18, "0x123") == 1 + +def test_swap_coins(): + with patch('src.swap_agent.src.tools.Web3') as mock_web3, \ + patch('src.swap_agent.src.tools.validate_swap') as mock_validate, \ + patch('src.swap_agent.src.tools.get_quote') as mock_quote: + + mock_web3.return_value = MagicMock() + mock_validate.return_value = ("0x123", "ETH", "0x456", "DAI") + mock_quote.return_value = {"dstAmount": "1000000000000000000"} + + result, role = swap_coins("ETH", "DAI", 1, 1, "0x789") + assert result["src"] == "ETH" + assert result["dst"] == "DAI" + assert role == "swap" + +def test_swap_coins_insufficient_funds(): + with patch('src.swap_agent.src.tools.Web3') as mock_web3, \ + patch('src.swap_agent.src.tools.validate_swap') as mock_validate: + + mock_web3.return_value = MagicMock() + mock_validate.side_effect = InsufficientFundsError("Not enough funds") + + with pytest.raises(InsufficientFundsError): + swap_coins("ETH", "DAI", 1000, 1, "0x789") + +def test_swap_coins_token_not_found(): + with patch('src.swap_agent.src.tools.Web3') as mock_web3, \ + patch('src.swap_agent.src.tools.validate_swap') as mock_validate: + + mock_web3.return_value = MagicMock() + mock_validate.side_effect = TokenNotFoundError("Token not found") + + with pytest.raises(TokenNotFoundError): + swap_coins("UNKNOWN", "DAI", 1, 1, "0x789") + +def test_swap_coins_swap_not_possible(): + with patch('src.swap_agent.src.tools.Web3') as mock_web3, \ + patch('src.swap_agent.src.tools.validate_swap') as mock_validate, \ + patch('src.swap_agent.src.tools.get_quote') as mock_quote: + + mock_web3.return_value = MagicMock() + mock_validate.return_value = ("0x123", "ETH", "0x456", "DAI") + mock_quote.return_value = None + + with pytest.raises(SwapNotPossibleError): + swap_coins("ETH", "DAI", 1, 1, "0x789") \ No newline at end of file diff --git a/submodules/moragents_dockers/docker-compose-apple.yml b/submodules/moragents_dockers/docker-compose-apple.yml index 83b7079..8cc8dbc 100644 --- a/submodules/moragents_dockers/docker-compose-apple.yml +++ b/submodules/moragents_dockers/docker-compose-apple.yml @@ -2,6 +2,7 @@ version: '3.8' services: agents: + image: lachsbagel/moragents_dockers-agents:apple-0.0.9 build: dockerfile: Dockerfile-apple context: ./agents @@ -10,15 +11,21 @@ services: restart: always volumes: - agents_data:/var/lib/agents - - ./agents/src:/app/src # Volume for swapagent src + - ./agents/src:/app/src + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - BASE_URL=http://host.docker.internal:11434 nginx: + image: lachsbagel/moragents_dockers-nginx:apple-0.0.9 build: context: ./frontend dockerfile: Dockerfile target: nginx ports: - '3333:80' - + + volumes: - agents_data: \ No newline at end of file + agents_data: diff --git a/submodules/moragents_dockers/docker-compose.yml b/submodules/moragents_dockers/docker-compose.yml index 49b100d..0d875eb 100644 --- a/submodules/moragents_dockers/docker-compose.yml +++ b/submodules/moragents_dockers/docker-compose.yml @@ -2,6 +2,7 @@ version: '3.8' services: agents: + image: lachsbagel/moragents_dockers-agents:amd64-0.0.9 build: dockerfile: Dockerfile context: ./agents @@ -10,9 +11,14 @@ services: restart: always volumes: - agents_data:/var/lib/agents - - ./agents/src:/app/src # Volume for swapagent src + - ./agents/src:/app/src + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - BASE_URL=http://host.docker.internal:11434 nginx: + image: lachsbagel/moragents_dockers-nginx:amd64-0.0.9 build: context: ./frontend dockerfile: Dockerfile @@ -21,5 +27,6 @@ services: - '3333:80' + volumes: agents_data: diff --git a/submodules/moragents_dockers/frontend/components/Chat/index.tsx b/submodules/moragents_dockers/frontend/components/Chat/index.tsx index 01f6cd0..c79a4ba 100644 --- a/submodules/moragents_dockers/frontend/components/Chat/index.tsx +++ b/submodules/moragents_dockers/frontend/components/Chat/index.tsx @@ -162,6 +162,7 @@ export const Chat: FC = ({ await onSubmitMessage(message, file); setMessage(''); + setFile(null); // Clear the file state after upload setShowSpinner(false); } diff --git a/submodules/moragents_dockers/frontend/components/SwapAgentModal/index.tsx b/submodules/moragents_dockers/frontend/components/SwapAgentModal/index.tsx new file mode 100644 index 0000000..70fcd72 --- /dev/null +++ b/submodules/moragents_dockers/frontend/components/SwapAgentModal/index.tsx @@ -0,0 +1,29 @@ +import { Button, HStack, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Spacer, Text, useDisclosure } from '@chakra-ui/react'; +import React, { FC, ComponentPropsWithoutRef, useEffect } from 'react'; + +export interface SwapAgentModalProps extends ComponentPropsWithoutRef<'div'> { + isOpen: boolean; + onClose: () => void; +} + +export const SwapAgentModal: FC = ({ isOpen, onClose }) => { + return ( + + + + Swap Agent + + + You have switched to the Swap Agent. Please ensure you have connected your wallet and selected the correct network. + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/submodules/moragents_dockers/frontend/config.ts b/submodules/moragents_dockers/frontend/config.ts index f13d51f..2ee629d 100644 --- a/submodules/moragents_dockers/frontend/config.ts +++ b/submodules/moragents_dockers/frontend/config.ts @@ -28,16 +28,14 @@ export const availableAgents: { requirements: { connectedWallet: false } - } - /* + }, 'rag-agent': { - 'name': 'Functional Data Agent (RAG FOR FILES)', - 'description': 'Mock of the Data Agent that supports files', - 'endpoint': 'http://127.0.0.1:8081', + 'name': 'PDF Agent', + 'description': 'Ask questions about an uploaded PDF file', + 'endpoint': 'http://127.0.0.1:8080/rag_agent', requirements: { connectedWallet: false }, supportsFiles: true } - */ } \ No newline at end of file diff --git a/wizard_windows.iss b/wizard_windows.iss index 1ff4af9..b286838 100644 --- a/wizard_windows.iss +++ b/wizard_windows.iss @@ -1,35 +1,104 @@ [Setup] AppName=MORagents -AppVersion=0.0.8 +AppVersion=0.0.9 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: "LICENSE"; DestDir: "{app}"; Flags: isreadme +Source: "LICENSE.md"; 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}\DockerDesktopInstaller.exe"; Parameters: "install"; StatusMsg: "Installing Docker Desktop..."; Flags: waituntilterminated +Filename: "{tmp}\OllamaSetup.exe"; StatusMsg: "Installing Ollama..."; Flags: waituntilterminated +Filename: "{app}\LICENSE.md"; 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. ' + #13#10 + + 'MIT License' + #13#10 + + 'Copyright (c) 2024 Liquid Tensor LLC' + #13#10 + + 'Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), ' + + 'to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, ' + + 'and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: ' + + 'The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. ' + #13#10 + + 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, ' + + 'INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR ' + + 'PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, ' + + 'DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ' + + 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.' + #13#10 + #13#10 + + 'By clicking Next, installing, and/or using the MORagents software 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); -end; \ No newline at end of file + Result := False; + + { Skip EULA page if already accepted } + if (PageID = EULAPage.ID) and EULAAccepted then + Result := True; +end;