diff --git a/.github/workflows/mor-agents-build-linux.yml b/.github/workflows/mor-agents-build-linux.yml index 3d7dcac..09d09cb 100644 --- a/.github/workflows/mor-agents-build-linux.yml +++ b/.github/workflows/mor-agents-build-linux.yml @@ -2,178 +2,178 @@ name: MOR Agents Build Linux on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: jobs: build: runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pyinstaller - - - name: Build with PyInstaller - run: | - pyinstaller --name="MORagents" --add-data "images/moragents.png:images" main.py - - - name: Create Debian package - run: | - mkdir -p debian/DEBIAN - mkdir -p debian/usr/bin - mkdir -p debian/usr/share/applications - mkdir -p debian/usr/share/icons/hicolor/256x256/apps - cp -r dist/MORagents/* debian/usr/bin/ - cp images/moragents.png debian/usr/share/icons/hicolor/256x256/apps/moragents.png - echo "[Desktop Entry] - Name=MORagents - Exec=/usr/bin/MORagents - Icon=moragents - Type=Application - Categories=Utility;" > debian/usr/share/applications/moragents.desktop - echo "Package: moragents - Version: 1.0 - Section: utils - Priority: optional - Architecture: amd64 - Maintainer: LachsBagel - Description: MORagents application - MORagents is a desktop application for AI agents." > debian/DEBIAN/control - - dpkg-deb --build debian moragents.deb - - - name: Create setup script - run: | - cat << EOF > moragents-setup.sh - #!/bin/bash - set -e - - # Colors for output - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[0;33m' - NC='\033[0m' # No Color - - # Function to check if a command exists - command_exists() { - command -v "$1" >/dev/null 2>&1 - } - - # Function to add current user to docker group - add_user_to_docker_group() { - local current_user=\$(whoami) - if [ "\$current_user" != "root" ]; then - echo -e "\${YELLOW}Adding current user to docker group...${NC}" - sudo usermod -aG docker "\$current_user" - echo -e "\${GREEN}User added to docker group. Please log out and log back in for changes to take effect.${NC}" - else - echo -e "\${YELLOW}Running as root. Skipping user addition to docker group.${NC}" - fi - } - - # Function to wait for Ollama service to be ready - wait_for_ollama() { - echo -e "\${YELLOW}Waiting for Ollama service to be ready...${NC}" - for i in {1..30}; do - if curl -s http://localhost:11434/api/tags >/dev/null; then - echo -e "\${GREEN}Ollama service is ready.${NC}" - return 0 - fi - sleep 2 - done - echo -e "\${RED}Timed out waiting for Ollama service.${NC}" - return 1 - } - - # Function to pull Ollama model with retries - pull_ollama_model() { - local model=\$1 - local max_attempts=3 - local attempt=1 - - while [ \$attempt -le \$max_attempts ]; do - echo -e "\${YELLOW}Pulling Ollama model \$model (Attempt \$attempt)...${NC}" - if ollama pull \$model; then - echo -e "\${GREEN}Successfully pulled \$model.${NC}" - return 0 - fi - echo -e "\${YELLOW}Failed to pull \$model. Retrying...${NC}" - sleep 5 - attempt=\$((attempt + 1)) - done - - echo -e "\${RED}Failed to pull \$model after \$max_attempts attempts.${NC}" - return 1 - } - - # Install curl if not present - if ! command_exists curl; then - echo -e "\${YELLOW}Installing curl...${NC}" - sudo apt-get update - sudo apt-get install -y curl - fi - - # Install Docker if not present - if ! command_exists docker; then - echo -e "\${YELLOW}Installing Docker...${NC}" - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - add_user_to_docker_group - sudo systemctl enable docker - sudo systemctl start docker - else - echo -e "\${GREEN}Docker is already installed.${NC}" - fi - - # Install Ollama - echo -e "\${YELLOW}Installing Ollama...${NC}" - curl -fsSL https://ollama.com/install.sh | sh - - # Start Ollama service - echo -e "\${YELLOW}Starting Ollama service...${NC}" - nohup ollama serve > /dev/null 2>&1 & - - # Wait for Ollama service to be ready - wait_for_ollama - - # Pull Ollama models - echo -e "\${YELLOW}Pulling Ollama models...${NC}" - pull_ollama_model llama3.1 - pull_ollama_model nomic-embed-text - - # Pull necessary Docker images - echo -e "\${YELLOW}Pulling Docker images...${NC}" - sudo docker pull lachsbagel/moragents_dockers-nginx:amd64-0.1.0 - sudo docker pull lachsbagel/moragents_dockers-agents:amd64-0.1.0 - - # Start Docker containers - echo -e "\${YELLOW}Starting Docker containers...${NC}" - sudo docker run -d --name agents -p 8080:5000 --restart always -v /var/lib/agents -v /app/src lachsbagel/moragents_dockers-agents:amd64-0.1.0 - sudo docker run -d --name nginx -p 3333:80 lachsbagel/moragents_dockers-nginx:amd64-0.1.0 - - echo -e "\${GREEN}Setup complete!${NC}" - EOF - - chmod +x moragents-setup.sh - - - name: Upload Debian Package and Setup Script - uses: actions/upload-artifact@v4 - with: - name: MORagentsSetup-Linux - path: | - moragents.deb - moragents-setup.sh \ No newline at end of file + - uses: actions/checkout@v4 + with: + submodules: "recursive" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build with PyInstaller + run: | + pyinstaller --name="MORagents" --add-data "images/moragents.png:images" main.py + + - name: Create Debian package + run: | + mkdir -p debian/DEBIAN + mkdir -p debian/usr/bin + mkdir -p debian/usr/share/applications + mkdir -p debian/usr/share/icons/hicolor/256x256/apps + cp -r dist/MORagents/* debian/usr/bin/ + cp images/moragents.png debian/usr/share/icons/hicolor/256x256/apps/moragents.png + echo "[Desktop Entry] + Name=MORagents + Exec=/usr/bin/MORagents + Icon=moragents + Type=Application + Categories=Utility;" > debian/usr/share/applications/moragents.desktop + echo "Package: moragents + Version: 1.0 + Section: utils + Priority: optional + Architecture: amd64 + Maintainer: LachsBagel + Description: MORagents application + MORagents is a desktop application for AI agents." > debian/DEBIAN/control + + dpkg-deb --build debian moragents.deb + + - name: Create setup script + run: | + cat << EOF > moragents-setup.sh + #!/bin/bash + set -e + + # Colors for output + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + NC='\033[0m' # No Color + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Function to add current user to docker group + add_user_to_docker_group() { + local current_user=\$(whoami) + if [ "\$current_user" != "root" ]; then + echo -e "\${YELLOW}Adding current user to docker group...${NC}" + sudo usermod -aG docker "\$current_user" + echo -e "\${GREEN}User added to docker group. Please log out and log back in for changes to take effect.${NC}" + else + echo -e "\${YELLOW}Running as root. Skipping user addition to docker group.${NC}" + fi + } + + # Function to wait for Ollama service to be ready + wait_for_ollama() { + echo -e "\${YELLOW}Waiting for Ollama service to be ready...${NC}" + for i in {1..30}; do + if curl -s http://localhost:11434/api/tags >/dev/null; then + echo -e "\${GREEN}Ollama service is ready.${NC}" + return 0 + fi + sleep 2 + done + echo -e "\${RED}Timed out waiting for Ollama service.${NC}" + return 1 + } + + # Function to pull Ollama model with retries + pull_ollama_model() { + local model=\$1 + local max_attempts=3 + local attempt=1 + + while [ \$attempt -le \$max_attempts ]; do + echo -e "\${YELLOW}Pulling Ollama model \$model (Attempt \$attempt)...${NC}" + if ollama pull \$model; then + echo -e "\${GREEN}Successfully pulled \$model.${NC}" + return 0 + fi + echo -e "\${YELLOW}Failed to pull \$model. Retrying...${NC}" + sleep 5 + attempt=\$((attempt + 1)) + done + + echo -e "\${RED}Failed to pull \$model after \$max_attempts attempts.${NC}" + return 1 + } + + # Install curl if not present + if ! command_exists curl; then + echo -e "\${YELLOW}Installing curl...${NC}" + sudo apt-get update + sudo apt-get install -y curl + fi + + # Install Docker if not present + if ! command_exists docker; then + echo -e "\${YELLOW}Installing Docker...${NC}" + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + add_user_to_docker_group + sudo systemctl enable docker + sudo systemctl start docker + else + echo -e "\${GREEN}Docker is already installed.${NC}" + fi + + # Install Ollama + echo -e "\${YELLOW}Installing Ollama...${NC}" + curl -fsSL https://ollama.com/install.sh | sh + + # Start Ollama service + echo -e "\${YELLOW}Starting Ollama service...${NC}" + nohup ollama serve > /dev/null 2>&1 & + + # Wait for Ollama service to be ready + wait_for_ollama + + # Pull Ollama models + echo -e "\${YELLOW}Pulling Ollama models...${NC}" + pull_ollama_model llama3.2:3b + pull_ollama_model nomic-embed-text + + # Pull necessary Docker images + echo -e "\${YELLOW}Pulling Docker images...${NC}" + sudo docker pull lachsbagel/moragents_dockers-nginx:amd64-0.2.0 + sudo docker pull lachsbagel/moragents_dockers-agents:amd64-0.2.0 + + # Start Docker containers + echo -e "\${YELLOW}Starting Docker containers...${NC}" + sudo docker run -d --name agents -p 8080:5000 --restart always -v /var/lib/agents -v /app/src lachsbagel/moragents_dockers-agents:amd64-0.2.0 + sudo docker run -d --name nginx -p 3333:80 lachsbagel/moragents_dockers-nginx:amd64-0.2.0 + + echo -e "\${GREEN}Setup complete!${NC}" + EOF + + chmod +x moragents-setup.sh + + - name: Upload Debian Package and Setup Script + uses: actions/upload-artifact@v4 + with: + name: MORagentsSetup-Linux + path: | + moragents.deb + moragents-setup.sh diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 0000000..ab7ebac --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,46 @@ +name: Security Scan + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + security_scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install safety bandit + + - name: Run Safety check + run: safety check -r requirements.txt + continue-on-error: true + + - name: Run Bandit + run: bandit -r . -f custom + continue-on-error: true + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@0.20.0 + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' diff --git a/build_assets/macOS/preinstall_ollama.sh b/build_assets/macOS/preinstall_ollama.sh index dfcb2e7..a74c1d5 100644 --- a/build_assets/macOS/preinstall_ollama.sh +++ b/build_assets/macOS/preinstall_ollama.sh @@ -7,5 +7,5 @@ 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.1 +/usr/local/bin/ollama pull llama3.2:3b /usr/local/bin/ollama pull nomic-embed-text diff --git a/build_assets/macOS/welcome.html b/build_assets/macOS/welcome.html index 18ca86d..a355874 100644 --- a/build_assets/macOS/welcome.html +++ b/build_assets/macOS/welcome.html @@ -1,44 +1,57 @@ - - - - Welcome to MORagents v0.1.0 Installer - - - -

Welcome to MORagents v0.1.0 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.

- + li { + margin-bottom: 10px; + } + + + +

Welcome to MORagents v0.2.0 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/config.py b/config.py index 43bae40..1738205 100644 --- a/config.py +++ b/config.py @@ -16,20 +16,20 @@ class AgentDockerConfig: MACOS_APPLE_IMAGE_NAMES = [ - "lachsbagel/moragents_dockers-nginx:apple-0.1.0", - "lachsbagel/moragents_dockers-agents:apple-0.1.0" + "lachsbagel/moragents_dockers-nginx:apple-0.2.0", + "lachsbagel/moragents_dockers-agents:apple-0.2.0" ] MACOS_INTEL_IMAGE_NAMES = [ - "lachsbagel/moragents_dockers-nginx:amd64-0.1.0", - "lachsbagel/moragents_dockers-agents:amd64-0.1.0" + "lachsbagel/moragents_dockers-nginx:amd64-0.2.0", + "lachsbagel/moragents_dockers-agents:amd64-0.2.0" ] WINDOWS_IMAGE_NAMES = [ - "lachsbagel/moragents_dockers-nginx:amd64-0.1.0", - "lachsbagel/moragents_dockers-agents:amd64-0.1.0" + "lachsbagel/moragents_dockers-nginx:amd64-0.2.0", + "lachsbagel/moragents_dockers-agents:amd64-0.2.0" ] LINUX_IMAGE_NAMES = [ # TODO, may need linux specific tagged images - "lachsbagel/moragents_dockers-nginx:amd64-0.1.0", - "lachsbagel/moragents_dockers-agents:amd64-0.1.0" + "lachsbagel/moragents_dockers-nginx:amd64-0.2.0", + "lachsbagel/moragents_dockers-agents:amd64-0.2.0" ] @staticmethod @@ -53,5 +53,9 @@ class AgentDockerConfigDeprecate: "lachsbagel/moragents_dockers-nginx:apple-0.0.9", "lachsbagel/moragents_dockers-agents:apple-0.0.9", "lachsbagel/moragents_dockers-nginx:amd64-0.0.9", - "lachsbagel/moragents_dockers-agents:amd64-0.0.9" + "lachsbagel/moragents_dockers-agents:amd64-0.0.9", + "lachsbagel/moragents_dockers-nginx:apple-0.1.0", + "lachsbagel/moragents_dockers-agents:apple-0.1.0", + "lachsbagel/moragents_dockers-nginx:amd64-0.1.0", + "lachsbagel/moragents_dockers-agents:amd64-0.1.0" ] diff --git a/requirements.txt b/requirements.txt index 70969cd..c4a1bda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ requests setuptools pyinstaller torch +safety +bandit diff --git a/submodules/moragents_dockers/README.md b/submodules/moragents_dockers/README.md index 38327fc..e07693e 100644 --- a/submodules/moragents_dockers/README.md +++ b/submodules/moragents_dockers/README.md @@ -1,41 +1,44 @@ -# Moragents +# Moragents ## Overview -This project is a Flask-based AI chat application featuring intelligent responses from various language models and embeddings. It includes file uploading, cryptocurrency swapping, and a delegator system to manage multiple agents. The application, along with a dApp for agent interaction, runs locally and is containerized with Docker. +This project is a Flask-based AI chat application featuring intelligent responses from various language models and embeddings. It includes file uploading, cryptocurrency swapping, and a delegator system to manage multiple agents. The application, along with a dApp for agent interaction, runs locally and is containerized with Docker. ## Pre-requisites -* [Download Ollama](https://ollama.com/ )for your operating system -* Then after finishing installation pull these two models: -```ollama pull llama3.1``` +- [Download Ollama](https://ollama.com/)for your operating system +- Then after finishing installation pull these two models: + +`ollama pull llama3.2:3b` -```ollama pull nomic-embed-text``` +`ollama pull nomic-embed-text` ## Run with Docker Compose -Docker compose will build and run two containers. One will be for the agents, the other will be for the UI. +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 - ``` + + ```sh + $ cd submodules/moragents_dockers + ``` 2. Build Images and Launch Containers: - 1. For Intel / AMD / x86_64 - ```sh - docker-compose up - ``` + 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 - ``` + ```sh + docker-compose -f docker-compose-apple.yml up + ``` -Open in the browser: ```http://localhost:3333/``` +Open in the browser: `http://localhost:3333/` -Docker build will download the model. The first time that one of the agents are called, the model will be loaded into memory and this instance will be shared between all agents. +Docker build will download the model. The first time that one of the agents are called, the model will be loaded into memory and this instance will be shared between all agents. ## Agents + Five agents are included: ### Data Agent @@ -53,6 +56,7 @@ It currently supports the following metrics: It is possible to ask questions about assets by referring to them either by their name or their ticker symbol. ### Swap Agent + This agent will enable you to perform swaps between cryptoassets. It should be used with the accompanying UI which provides a browser-based front-end to chat with the agent, display quotes and sign transactions. A typical flow looks like this: @@ -65,35 +69,43 @@ A typical flow looks like this: - 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. ## Tweet Sizzler Agent -This agent will let you generate tweets, edit with a WSYWIG. -Provided you enter API creds in the Settings you can also directly post to your X account. + +This agent will let you generate tweets, edit with a WSYWIG. +Provided you enter API creds in the Settings you can also directly post to your X account. ## MOR Rewards Agent + Ask the agent to check your MOR rewards and it will retrieve claimable MOR stats from both capital and coder pools. --- # Delegator + The Delegator handles user queries by analyzing the prompt and delegating it to the appropriate agent. ## API Endpoints 1. **Chat Functionality** + - Endpoint: `POST /` - Handles chat interactions, delegating to appropriate agents when necessary. 2. **Message History** + - Endpoint: `GET /messages` - Retrieves chat message history. 3. **Clear Messages** + - Endpoint: `GET /clear_messages` - Clears the chat message history. 4. **Swap Operations** + - Endpoints: - `POST /tx_status`: Check transaction status - `POST /allowance`: Get allowance @@ -129,6 +141,7 @@ This allows the delegator to delegate to the correct task agent based on the use - `upload`: A boolean indicating if the agent requires a file to be uploaded from the front-end before it should be called. #### Example: + ```python:agents/src/config.py DELEGATOR_CONFIG = { "agents": [ @@ -144,13 +157,13 @@ DELEGATOR_CONFIG = { } ``` - ### 3. Implement Agent Logic 1. **Define the agent class** in the specified path. 2. **Ensure the agent can handle the queries** it is designed for. #### Example: + ```python:agents/src/new_agent/src/agent.py class NewAgent: def __init__(self, agent_info, llm, llm_ollama, embeddings, flask_app): @@ -177,12 +190,12 @@ class NewAgent: # Add other methods as needed ``` - ### 4. Handle Multi-Turn Conversations Agents can handle multi-turn conversations by returning a next_turn_agent which indicates the name of the agent that should handle the next turn. #### Example: + ```python class NewAgent: def __init__(self, agent_info, llm, llm_ollama, embeddings, flask_app): @@ -205,7 +218,7 @@ class NewAgent: def chat(self, request, user_id): # Process the query and determine the next agent next_turn_agent = self.agent_info["name"] - + # Generate response where we want to initiate a multi-turn conversation with the same agent. return response, next_turn_agent @@ -215,6 +228,7 @@ class NewAgent: ### 5. Integration The `Delegator` will automatically: + - Import the agent module. - Instantiate the agent class. - Add the agent to its internal dictionary. diff --git a/submodules/benchmarks/__init__.py b/submodules/moragents_dockers/__init__.py similarity index 100% rename from submodules/benchmarks/__init__.py rename to submodules/moragents_dockers/__init__.py diff --git a/submodules/moragents_dockers/agents/Dockerfile b/submodules/moragents_dockers/agents/Dockerfile index 52bcec7..14b0556 100644 --- a/submodules/moragents_dockers/agents/Dockerfile +++ b/submodules/moragents_dockers/agents/Dockerfile @@ -12,20 +12,11 @@ RUN apt-get update && apt-get install -y gcc g++ procps && rm -rf /var/lib/apt/l # Install Python dependencies RUN python3 -m pip install --no-cache-dir --upgrade pip && \ python3 -m pip install --no-cache-dir --upgrade -r requirements.txt - -COPY download_model.py . -COPY model_config.py . - -RUN python3 download_model.py - -copy . . +COPY . . # Expose the port your application listens on EXPOSE 5000 -# Set the environment variable for Flask -ENV FLASK_APP=src/app.py - -# Run the application -CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file +# Run the application using uvicorn with auto-reload enabled +CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "5000", "--reload"] \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/Dockerfile-apple b/submodules/moragents_dockers/agents/Dockerfile-apple index ad80504..2e28f3b 100644 --- a/submodules/moragents_dockers/agents/Dockerfile-apple +++ b/submodules/moragents_dockers/agents/Dockerfile-apple @@ -12,21 +12,11 @@ RUN apt-get update && apt-get install -y gcc g++ procps && rm -rf /var/lib/apt/l # Install Python dependencies RUN python3 -m pip install --no-cache-dir --upgrade pip && \ python3 -m pip install --no-cache-dir --upgrade -r requirements.txt - -COPY download_model.py . -COPY model_config.py . - -RUN python3 download_model.py - -copy . . +COPY . . # Expose the port your application listens on EXPOSE 5000 - -# Set the environment variable for Flask -ENV FLASK_APP=src/app.py - -# Run the application -CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file +# Run the application using uvicorn with auto-reload enabled +CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "5000", "--reload"] \ No newline at end of file diff --git a/submodules/benchmarks/claim_agent_benchmarks/__init__.py b/submodules/moragents_dockers/agents/__init__.py similarity index 100% rename from submodules/benchmarks/claim_agent_benchmarks/__init__.py rename to submodules/moragents_dockers/agents/__init__.py diff --git a/submodules/moragents_dockers/agents/download_model.py b/submodules/moragents_dockers/agents/download_model.py deleted file mode 100644 index 55f3fd4..0000000 --- a/submodules/moragents_dockers/agents/download_model.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import shutil -from huggingface_hub import hf_hub_download -from model_config import Config - - -def download_model(model_name, revision): - """Function to download model from the hub""" - model_directory = hf_hub_download(repo_id=model_name, filename=revision) - return model_directory - - -def move_files(src_dir, dest_dir): - """Move files from source to destination directory.""" - for f in os.listdir(src_dir): - src_path = os.path.join(src_dir, f) - dst_path = os.path.join(dest_dir, f) - shutil.copy2(src_path, dst_path) - os.remove(src_path) - - -if __name__ == "__main__": - download_dir = Config.DOWNLOAD_DIR - os.makedirs(download_dir, exist_ok=True) - model_name = Config.MODEL_NAME - revision = Config.MODEL_REVISION - path = download_model(model_name, revision) - model_path = "/".join(path.split("/")[:-1]) + "/" - move_files(model_path, download_dir) diff --git a/submodules/moragents_dockers/agents/model_config.py b/submodules/moragents_dockers/agents/model_config.py deleted file mode 100644 index a5868e4..0000000 --- a/submodules/moragents_dockers/agents/model_config.py +++ /dev/null @@ -1,13 +0,0 @@ -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" diff --git a/submodules/moragents_dockers/agents/requirements.txt b/submodules/moragents_dockers/agents/requirements.txt index e87e422..8542cb4 100644 --- a/submodules/moragents_dockers/agents/requirements.txt +++ b/submodules/moragents_dockers/agents/requirements.txt @@ -1,17 +1,23 @@ llama-cpp-python==0.2.90 -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 +fastapi==0.115.0 Werkzeug==2.2.2 -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 +feedparser +langchain-text-splitters==0.3.0 +langchain-core==0.3.9 +langchain-community==0.3.1 +langchain-ollama==0.2.0 +tweepy==4.14.0 +uvicorn==0.31.0 +python-dateutil +python-multipart==0.0.12 +beautifulsoup4==4.12.3 +selenium==4.25.0 torch -tweepy \ No newline at end of file +pytz +pyshorteners diff --git a/submodules/benchmarks/claim_agent_benchmarks/adapters/__init__.py b/submodules/moragents_dockers/agents/src/__init__.py similarity index 100% rename from submodules/benchmarks/claim_agent_benchmarks/adapters/__init__.py rename to submodules/moragents_dockers/agents/src/__init__.py diff --git a/submodules/benchmarks/price_fetching/__init__.py b/submodules/moragents_dockers/agents/src/agents/__init__.py similarity index 100% rename from submodules/benchmarks/price_fetching/__init__.py rename to submodules/moragents_dockers/agents/src/agents/__init__.py diff --git a/submodules/moragents_dockers/agents/src/data_agent/README.md b/submodules/moragents_dockers/agents/src/agents/crypto_data/README.md similarity index 100% rename from submodules/moragents_dockers/agents/src/data_agent/README.md rename to submodules/moragents_dockers/agents/src/agents/crypto_data/README.md diff --git a/submodules/benchmarks/price_fetching/adapters/__init__.py b/submodules/moragents_dockers/agents/src/agents/crypto_data/__init__.py similarity index 100% rename from submodules/benchmarks/price_fetching/adapters/__init__.py rename to submodules/moragents_dockers/agents/src/agents/crypto_data/__init__.py diff --git a/submodules/moragents_dockers/agents/src/agents/crypto_data/agent.py b/submodules/moragents_dockers/agents/src/agents/crypto_data/agent.py new file mode 100644 index 0000000..4240e55 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/crypto_data/agent.py @@ -0,0 +1,93 @@ +import json +import logging + +from src.agents.crypto_data import tools +from src.models.messages import ChatRequest +from src.stores import agent_manager + +logger = logging.getLogger(__name__) + + +class CryptoDataAgent: + def __init__(self, config, llm, embeddings): + self.config = config + self.llm = llm + self.embeddings = embeddings + self.tools_provided = tools.get_tools() + + def get_response(self, message): + system_prompt = ( + "Don't make assumptions about the value of the arguments for the function " + "they should always be supplied by the user and do not alter the value of the arguments. " + "Don't make assumptions about what values to plug into functions. Ask for clarification if a user " + "request is ambiguous." + ) + + messages = [ + {"role": "system", "content": system_prompt}, + ] + messages.extend(message) + + logger.info("Sending request to LLM with %d messages", len(messages)) + + llm_with_tools = self.llm.bind_tools(self.tools_provided) + + try: + result = llm_with_tools.invoke(messages) + logger.info("Received response from LLM: %s", result) + + if result.tool_calls: + tool_call = result.tool_calls[0] + func_name = tool_call.get("name") + args = tool_call.get("args") + logger.info("LLM suggested using tool: %s", func_name) + + if func_name == "get_price": + return tools.get_coin_price_tool(args["coin_name"]), "assistant" + elif func_name == "get_floor_price": + return tools.get_nft_floor_price_tool(args["nft_name"]), "assistant" + elif func_name == "get_fdv": + return ( + tools.get_fully_diluted_valuation_tool(args["coin_name"]), + "assistant", + ) + elif func_name == "get_tvl": + return ( + tools.get_protocol_total_value_locked_tool( + args["protocol_name"] + ), + "assistant", + ) + elif func_name == "get_market_cap": + return ( + tools.get_coin_market_cap_tool(args["coin_name"]), + "assistant", + ) + else: + logger.info("LLM provided a direct response without using tools") + return result.content, "assistant" + except Exception as e: + logger.error(f"Error in get_response: {str(e)}") + return f"An error occurred: {str(e)}", "assistant" + + def generate_response(self, prompt): + response, role = self.get_response([prompt]) + return response, role + + def chat(self, request: ChatRequest): + try: + data = request.dict() + if "prompt" in data: + prompt = data["prompt"] + logger.info( + "Received chat request with prompt: %s", + prompt[:50] + "..." if len(prompt) > 50 else prompt, + ) + response, role = self.generate_response(prompt) + return {"role": role, "content": response} + else: + logger.warning("Received chat request without 'prompt' in data") + return {"error": "Missing required parameters"}, 400 + except Exception as e: + logger.error("Error in chat method: %s", str(e), exc_info=True) + return {"Error": str(e)}, 500 diff --git a/submodules/moragents_dockers/agents/src/data_agent/src/config.py b/submodules/moragents_dockers/agents/src/agents/crypto_data/config.py similarity index 70% rename from submodules/moragents_dockers/agents/src/data_agent/src/config.py rename to submodules/moragents_dockers/agents/src/agents/crypto_data/config.py index 0c916ec..63db777 100644 --- a/submodules/moragents_dockers/agents/src/data_agent/src/config.py +++ b/submodules/moragents_dockers/agents/src/agents/crypto_data/config.py @@ -3,25 +3,25 @@ # 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." + 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." + 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/agents/crypto_data/routes.py b/submodules/moragents_dockers/agents/src/agents/crypto_data/routes.py new file mode 100644 index 0000000..d8417f5 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/crypto_data/routes.py @@ -0,0 +1,14 @@ +import logging + +from flask import Blueprint, request, jsonify + +crypto_data_agent_bp = Blueprint('crypto_data_agent', __name__) +logger = logging.getLogger(__name__) + +@crypto_data_agent_bp.route('/process_data', methods=['POST']) +def process_data(): + logger.info("Data Agent: Received process_data request") + data = request.get_json() + # Implement your data processing logic here + response = {"status": "success", "message": "Data processed"} + return jsonify(response) diff --git a/submodules/moragents_dockers/agents/src/data_agent/src/tools.py b/submodules/moragents_dockers/agents/src/agents/crypto_data/tools.py similarity index 64% rename from submodules/moragents_dockers/agents/src/data_agent/src/tools.py rename to submodules/moragents_dockers/agents/src/agents/crypto_data/tools.py index e02839a..0b0b100 100644 --- a/submodules/moragents_dockers/agents/src/data_agent/src/tools.py +++ b/submodules/moragents_dockers/agents/src/agents/crypto_data/tools.py @@ -1,9 +1,9 @@ import requests import logging -from data_agent.src.config import Config from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity +from src.agents.crypto_data.config import Config def get_most_similar(text, data): @@ -13,9 +13,12 @@ def get_most_similar(text, data): text_vector = vectorizer.transform([text]) similarity_scores = cosine_similarity(text_vector, sentence_vectors) top_indices = similarity_scores.argsort()[0][-20:] - top_matches = [data[item] for item in top_indices if similarity_scores[0][item] > 0.5] + top_matches = [ + data[item] for item in top_indices if similarity_scores[0][item] > 0.5 + ] return top_matches + def get_coingecko_id(text, type="coin"): """Get the CoinGecko ID for a given coin or NFT.""" url = f"{Config.COINGECKO_BASE_URL}/search" @@ -25,30 +28,32 @@ def get_coingecko_id(text, type="coin"): response.raise_for_status() data = response.json() if type == "coin": - return data['coins'][0]['id'] if data['coins'] else None + return data["coins"][0]["id"] if data["coins"] else None elif type == "nft": - return data['nfts'][0]['id'] if data.get('nfts') else None + return data["nfts"][0]["id"] if data.get("nfts") else None else: raise ValueError("Invalid type specified") except requests.exceptions.RequestException as e: logging.error(f"API request failed: {str(e)}") raise + def get_price(coin): """Get the price of a coin from CoinGecko API.""" coin_id = get_coingecko_id(coin, type="coin") if not coin_id: return None url = f"{Config.COINGECKO_BASE_URL}/simple/price" - params = {'ids': coin_id, 'vs_currencies': 'USD'} + params = {"ids": coin_id, "vs_currencies": "USD"} try: response = requests.get(url, params=params) response.raise_for_status() - return response.json()[coin_id]['usd'] + return response.json()[coin_id]["usd"] except requests.exceptions.RequestException as e: logging.error(f"Failed to retrieve price: {str(e)}") raise + def get_floor_price(nft): """Get the floor price of an NFT from CoinGecko API.""" nft_id = get_coingecko_id(str(nft), type="nft") @@ -63,6 +68,7 @@ def get_floor_price(nft): logging.error(f"Failed to retrieve floor price: {str(e)}") raise + def get_fdv(coin): """Get the fully diluted valuation of a coin from CoinGecko API.""" coin_id = get_coingecko_id(coin, type="coin") @@ -78,21 +84,23 @@ def get_fdv(coin): logging.error(f"Failed to retrieve FDV: {str(e)}") raise + def get_market_cap(coin): """Get the market cap of a coin from CoinGecko API.""" coin_id = get_coingecko_id(coin, type="coin") if not coin_id: return None url = f"{Config.COINGECKO_BASE_URL}/coins/markets" - params = {'ids': coin_id, 'vs_currency': 'USD'} + params = {"ids": coin_id, "vs_currency": "USD"} try: response = requests.get(url, params=params) response.raise_for_status() - return response.json()[0]['market_cap'] + return response.json()[0]["market_cap"] except requests.exceptions.RequestException as e: logging.error(f"Failed to retrieve market cap: {str(e)}") raise + def get_protocols_list(): """Get the list of protocols from DefiLlama API.""" url = f"{Config.DEFILLAMA_BASE_URL}/protocols" @@ -100,11 +108,16 @@ def get_protocols_list(): response = requests.get(url) response.raise_for_status() data = response.json() - return [item['slug'] for item in data] ,[item['name'] for item in data] ,[item['gecko_id'] for item in data] + return ( + [item["slug"] for item in data], + [item["name"] for item in data], + [item["gecko_id"] for item in data], + ) except requests.exceptions.RequestException as e: logging.error(f"Failed to retrieve protocols list: {str(e)}") raise + def get_tvl_value(protocol_id): """Gets the TVL value using the protocol ID from DefiLlama API.""" url = f"{Config.DEFILLAMA_BASE_URL}/tvl/{protocol_id}" @@ -114,11 +127,12 @@ def get_tvl_value(protocol_id): return response.json() except requests.exceptions.RequestException as e: logging.error(f"Failed to retrieve protocol TVL: {str(e)}") - raise + raise + def get_protocol_tvl(protocol_name): """Get the TVL (Total Value Locked) of a protocol from DefiLlama API.""" - id,name,gecko = get_protocols_list() + id, name, gecko = get_protocols_list() tag = get_coingecko_id(protocol_name) if tag: protocol_id = next((i for i, j in zip(id, gecko) if j == tag), None) @@ -157,7 +171,9 @@ def get_nft_floor_price_tool(nft_name): floor_price = get_floor_price(nft_name) if floor_price is None: return Config.FLOOR_PRICE_FAILURE_MESSAGE - return Config.FLOOR_PRICE_SUCCESS_MESSAGE.format(nft_name=nft_name, floor_price=floor_price) + return Config.FLOOR_PRICE_SUCCESS_MESSAGE.format( + nft_name=nft_name, floor_price=floor_price + ) except requests.exceptions.RequestException: return Config.API_ERROR_MESSAGE @@ -168,8 +184,10 @@ def get_protocol_total_value_locked_tool(protocol_name): tvl = get_protocol_tvl(protocol_name) if tvl is None: return Config.TVL_FAILURE_MESSAGE - protocol,tvl_value=list(tvl.items())[0][0],list(tvl.items())[0][1] - return Config.TVL_SUCCESS_MESSAGE.format(protocol_name=protocol_name, tvl=tvl_value) + protocol, tvl_value = list(tvl.items())[0][0], list(tvl.items())[0][1] + return Config.TVL_SUCCESS_MESSAGE.format( + protocol_name=protocol_name, tvl=tvl_value + ) except requests.exceptions.RequestException: return Config.API_ERROR_MESSAGE @@ -191,96 +209,99 @@ def get_coin_market_cap_tool(coin_name): market_cap = get_market_cap(coin_name) if market_cap is None: return Config.MARKET_CAP_FAILURE_MESSAGE - return Config.MARKET_CAP_SUCCESS_MESSAGE.format(coin_name=coin_name, market_cap=market_cap) + return Config.MARKET_CAP_SUCCESS_MESSAGE.format( + coin_name=coin_name, market_cap=market_cap + ) except requests.exceptions.RequestException: return Config.API_ERROR_MESSAGE + def get_tools(): """Return a list of tools for the agent.""" return [ { - "type": "function", - "function": { - "name": "get_price", - "description": "Get the price of a cryptocurrency", - "parameters": { - "type": "object", - "properties": { - "coin_name": { - "type": "string", - "description": "The name of the coin.", - } + "type": "function", + "function": { + "name": "get_price", + "description": "Get the price of a cryptocurrency", + "parameters": { + "type": "object", + "properties": { + "coin_name": { + "type": "string", + "description": "The name of the coin.", + } + }, + "required": ["coin_name"], }, - "required": ["coin_name"], }, }, - }, { - "type": "function", - "function": { - "name": "get_floor_price", - "description": "Get the floor price of an NFT", - "parameters": { - "type": "object", - "properties": { - "nft_name": { - "type": "string", - "description": "Name of the NFT", - } + "type": "function", + "function": { + "name": "get_floor_price", + "description": "Get the floor price of an NFT", + "parameters": { + "type": "object", + "properties": { + "nft_name": { + "type": "string", + "description": "Name of the NFT", + } + }, + "required": ["nft_name"], }, - "required": ["nft_name"], }, }, - }, - { - "type": "function", - "function": { - "name": "get_tvl", - "description": "Get the TVL (Total Value Locked) of a protocol.", - "parameters": { - "type": "object", - "properties": { - "protocol_name": { - "type": "string", - "description": "Name of the protocol", - } + { + "type": "function", + "function": { + "name": "get_tvl", + "description": "Get the TVL (Total Value Locked) of a protocol.", + "parameters": { + "type": "object", + "properties": { + "protocol_name": { + "type": "string", + "description": "Name of the protocol", + } + }, + "required": ["protocol_name"], }, - "required": ["protocol_name"], }, }, - }, - { - "type": "function", - "function": { - "name": "get_fdv", - "description": "Get the fdv or fully diluted valuation of a coin", - "parameters": { - "type": "object", - "properties": { - "coin_name": { - "type": "string", - "description": "Name of the coin", - } + { + "type": "function", + "function": { + "name": "get_fdv", + "description": "Get the fdv or fully diluted valuation of a coin", + "parameters": { + "type": "object", + "properties": { + "coin_name": { + "type": "string", + "description": "Name of the coin", + } + }, + "required": ["coin_name"], }, - "required": ["coin_name"], }, }, - } , - { - "type": "function", - "function": { - "name": "get_market_cap", - "description": "Get the mc or market cap of a coin", - "parameters": { - "type": "object", - "properties": { - "coin_name": { - "type": "string", - "description": "Name of the coin", - } + { + "type": "function", + "function": { + "name": "get_market_cap", + "description": "Get the mc or market cap of a coin", + "parameters": { + "type": "object", + "properties": { + "coin_name": { + "type": "string", + "description": "Name of the coin", + } + }, + "required": ["coin_name"], }, - "required": ["coin_name"], }, }, - } ] diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/__init__.py b/submodules/moragents_dockers/agents/src/agents/mor_claims/__init__.py similarity index 100% rename from submodules/benchmarks/reward_check_agent_benchmarks/__init__.py rename to submodules/moragents_dockers/agents/src/agents/mor_claims/__init__.py diff --git a/submodules/moragents_dockers/agents/src/agents/mor_claims/agent.py b/submodules/moragents_dockers/agents/src/agents/mor_claims/agent.py new file mode 100644 index 0000000..a18f7f4 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/mor_claims/agent.py @@ -0,0 +1,170 @@ +from src.agents.mor_claims import tools +from src.models.messages import ChatRequest +from src.stores import agent_manager + + +class MorClaimsAgent: + def __init__(self, agent_info, llm, embeddings): + self.agent_info = agent_info + self.llm = llm + self.embeddings = embeddings + self.tools_provided = tools.get_tools() + self.conversation_state = {} + + def _get_response(self, message, wallet_address): + if wallet_address not in self.conversation_state: + self.conversation_state[wallet_address] = {"state": "initial"} + + state = self.conversation_state[wallet_address]["state"] + + if state == "initial": + agent_manager.set_active_agent(self.agent_info["name"]) + + rewards = { + 0: tools.get_current_user_reward(wallet_address, 0), + 1: tools.get_current_user_reward(wallet_address, 1), + } + available_rewards = { + pool: amount for pool, amount in rewards.items() if amount > 0 + } + + if available_rewards: + selected_pool = max(available_rewards, key=available_rewards.get) + self.conversation_state[wallet_address]["available_rewards"] = { + selected_pool: available_rewards[selected_pool] + } + self.conversation_state[wallet_address][ + "receiver_address" + ] = wallet_address + self.conversation_state[wallet_address][ + "state" + ] = "awaiting_confirmation" + return ( + f"You have {available_rewards[selected_pool]} MOR rewards available in pool {selected_pool}. Would you like to proceed with claiming these rewards?", + "assistant", + self.agent_info["name"], + ) + else: + return ( + f"No rewards found for your wallet address {wallet_address} in either pool. Claim cannot be processed.", + "assistant", + None, + ) + + elif state == "awaiting_confirmation": + user_input = message[-1]["content"].lower() + if any( + word in user_input for word in ["yes", "proceed", "confirm", "claim"] + ): + return self.prepare_transactions(wallet_address) + else: + return ( + "Please confirm if you want to proceed with the claim by saying 'yes', 'proceed', 'confirm', or 'claim'.", + "assistant", + self.agent_info["name"], + ) + + return ( + "I'm sorry, I didn't understand that. Can you please rephrase your request?", + "assistant", + self.agent_info["name"], + ) + + def prepare_transactions(self, wallet_address): + available_rewards = self.conversation_state[wallet_address]["available_rewards"] + receiver_address = self.conversation_state[wallet_address]["receiver_address"] + transactions = [] + + for pool_id in available_rewards.keys(): + try: + tx_data = tools.prepare_claim_transaction(pool_id, receiver_address) + transactions.append({"pool": pool_id, "transaction": tx_data}) + except Exception as e: + return ( + f"Error preparing transaction for pool {pool_id}: {str(e)}", + "assistant", + None, + ) + + self.conversation_state[wallet_address]["transactions"] = transactions + + # Return a structured response + return ( + { + "role": "claim", + "content": {"transactions": transactions, "claim_tx_cb": "/claim"}, + }, + "claim", + None, + ) + + def chat(self, request: ChatRequest): + try: + data = request.dict() + if "prompt" in data and "wallet_address" in data: + prompt = data["prompt"] + wallet_address = data["wallet_address"] + response, role, next_turn_agent = self._get_response( + [prompt], wallet_address + ) + return { + "role": role, + "content": response, + "next_turn_agent": next_turn_agent, + } + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 + + def claim(self, request: ChatRequest): + try: + data = request.dict() + wallet_address = data["wallet_address"] + transactions = self.conversation_state[wallet_address]["transactions"] + agent_manager.clear_active_agent() + return {"transactions": transactions} + except Exception as e: + return {"error": str(e)}, 500 + + def claim_status(self, request: ChatRequest): + try: + data = request.dict() + wallet_address = data.get("wallet_address") + transaction_hash = data.get("transaction_hash") + status = data.get("status") + + if not all([wallet_address, transaction_hash, status]): + return {"error": "Missing required parameters"}, 400 + + # Generate and return the status message + response = self.get_status(status, transaction_hash, "claim") + return response, 200 + except Exception as e: + return {"error": str(e)}, 500 + + def get_status(self, flag, tx_hash, tx_type): + response = "" + + if flag == "cancelled": + response = f"The claim transaction has been cancelled." + elif flag == "success": + response = f"The claim transaction was successful." + elif flag == "failed": + response = f"The claim transaction has failed." + elif flag == "initiated": + response = ( + f"Claim transaction has been sent, please wait for it to be confirmed." + ) + + if tx_hash: + response = ( + response + f" The transaction hash is {tx_hash}. " + f"Here's the link to the Etherscan transaction: " + f"https://etherscan.io/tx/{tx_hash}" + ) + + if flag != "initiated": + response = response + " Is there anything else I can help you with?" + + return {"role": "assistant", "content": response} diff --git a/submodules/moragents_dockers/agents/src/agents/mor_claims/config.py b/submodules/moragents_dockers/agents/src/agents/mor_claims/config.py new file mode 100644 index 0000000..44d7639 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/mor_claims/config.py @@ -0,0 +1,35 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + + +# Configuration object +class Config: + + WEB3RPCURL = {"1": "https://eth.llamarpc.com/"} + MINT_FEE = 0.001 # in ETH + + DISTRIBUTION_PROXY_ADDRESS = "0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790" + DISTRIBUTION_ABI = [ + { + "inputs": [ + {"internalType": "uint256", "name": "poolId_", "type": "uint256"}, + {"internalType": "address", "name": "receiver_", "type": "address"}, + ], + "name": "claim", + "outputs": [], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "poolId_", "type": "uint256"}, + {"internalType": "address", "name": "user_", "type": "address"}, + ], + "name": "getCurrentUserReward", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + ] diff --git a/submodules/moragents_dockers/agents/src/claim_agent/src/tools.py b/submodules/moragents_dockers/agents/src/agents/mor_claims/tools.py similarity index 71% rename from submodules/moragents_dockers/agents/src/claim_agent/src/tools.py rename to submodules/moragents_dockers/agents/src/agents/mor_claims/tools.py index bee91c6..e5f0143 100644 --- a/submodules/moragents_dockers/agents/src/claim_agent/src/tools.py +++ b/submodules/moragents_dockers/agents/src/agents/mor_claims/tools.py @@ -1,11 +1,13 @@ from web3 import Web3 -from claim_agent.src.config import Config + +from src.agents.mor_claims.config import Config + def get_current_user_reward(wallet_address, pool_id): web3 = Web3(Web3.HTTPProvider(Config.WEB3RPCURL["1"])) distribution_contract = web3.eth.contract( address=web3.to_checksum_address(Config.DISTRIBUTION_PROXY_ADDRESS), - abi=Config.DISTRIBUTION_ABI + abi=Config.DISTRIBUTION_ABI, ) try: @@ -13,37 +15,41 @@ def get_current_user_reward(wallet_address, pool_id): raise Exception("Unable to connect to Ethereum network") reward = distribution_contract.functions.getCurrentUserReward( - pool_id, - web3.to_checksum_address(wallet_address) + pool_id, web3.to_checksum_address(wallet_address) ).call() - formatted_reward = web3.from_wei(reward, 'ether') + formatted_reward = web3.from_wei(reward, "ether") return round(formatted_reward, 4) except Exception as e: raise Exception(f"Error occurred while fetching the reward: {str(e)}") + def prepare_claim_transaction(pool_id, wallet_address): try: web3 = Web3(Web3.HTTPProvider(Config.WEB3RPCURL["1"])) contract = web3.eth.contract( address=web3.to_checksum_address(Config.DISTRIBUTION_PROXY_ADDRESS), - abi=Config.DISTRIBUTION_ABI + abi=Config.DISTRIBUTION_ABI, + ) + tx_data = contract.encode_abi( + fn_name="claim", args=[pool_id, web3.to_checksum_address(wallet_address)] + ) + mint_fee = web3.to_wei(Config.MINT_FEE, "ether") + estimated_gas = contract.functions.claim( + pool_id, web3.to_checksum_address(wallet_address) + ).estimate_gas( + {"from": web3.to_checksum_address(wallet_address), "value": mint_fee} ) - tx_data = contract.encode_abi(fn_name="claim", args=[pool_id, web3.to_checksum_address(wallet_address)]) - mint_fee = web3.to_wei(Config.MINT_FEE, 'ether') - estimated_gas = contract.functions.claim(pool_id, web3.to_checksum_address(wallet_address)).estimate_gas({ - 'from': web3.to_checksum_address(wallet_address), - 'value': mint_fee - }) return { "to": Config.DISTRIBUTION_PROXY_ADDRESS, "data": tx_data, "value": str(mint_fee), "gas": str(estimated_gas), - "chainId": "1" + "chainId": "1", } except Exception as e: raise Exception(f"Failed to prepare claim transaction: {str(e)}") + def get_tools(): return [ { @@ -56,16 +62,16 @@ def get_tools(): "properties": { "wallet_address": { "type": "string", - "description": "The wallet address to check rewards for" + "description": "The wallet address to check rewards for", }, "pool_id": { "type": "integer", - "description": "The ID of the pool to check rewards from" - } + "description": "The ID of the pool to check rewards from", + }, }, - "required": ["wallet_address", "pool_id"] - } - } + "required": ["wallet_address", "pool_id"], + }, + }, }, { "type": "function", @@ -77,15 +83,15 @@ def get_tools(): "properties": { "pool_id": { "type": "integer", - "description": "The ID of the pool to claim from" + "description": "The ID of the pool to claim from", }, "wallet_address": { "type": "string", - "description": "The wallet address to claim rewards for" - } + "description": "The wallet address to claim rewards for", + }, }, - "required": ["pool_id", "wallet_address"] - } - } - } - ] \ No newline at end of file + "required": ["pool_id", "wallet_address"], + }, + }, + }, + ] diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/adapters/__init__.py b/submodules/moragents_dockers/agents/src/agents/mor_rewards/__init__.py similarity index 100% rename from submodules/benchmarks/reward_check_agent_benchmarks/adapters/__init__.py rename to submodules/moragents_dockers/agents/src/agents/mor_rewards/__init__.py diff --git a/submodules/moragents_dockers/agents/src/reward_agent/src/agent.py b/submodules/moragents_dockers/agents/src/agents/mor_rewards/agent.py similarity index 62% rename from submodules/moragents_dockers/agents/src/reward_agent/src/agent.py rename to submodules/moragents_dockers/agents/src/agents/mor_rewards/agent.py index 44555da..d3dfff1 100644 --- a/submodules/moragents_dockers/agents/src/reward_agent/src/agent.py +++ b/submodules/moragents_dockers/agents/src/agents/mor_rewards/agent.py @@ -1,17 +1,16 @@ -import json import logging -from reward_agent.src import tools + +from src.agents.mor_rewards import tools +from src.models.messages import ChatRequest logger = logging.getLogger(__name__) -class RewardAgent: - def __init__(self, agent_info, llm, llm_ollama, embeddings, flask_app): +class MorRewardsAgent: + def __init__(self, agent_info, llm, embeddings): self.agent_info = agent_info self.llm = llm - self.llm_ollama = llm_ollama self.embeddings = embeddings - self.flask_app = flask_app self.tools_provided = tools.get_tools() def get_response(self, message, wallet_address): @@ -20,7 +19,7 @@ def get_response(self, message, wallet_address): try: rewards = { 0: tools.get_current_user_reward(wallet_address, 0), - 1: tools.get_current_user_reward(wallet_address, 1) + 1: tools.get_current_user_reward(wallet_address, 1), } response = f"Your current MOR rewards:\n" @@ -31,16 +30,26 @@ def get_response(self, message, wallet_address): return response, "assistant", None except Exception as e: logger.error(f"Error occurred while checking rewards: {str(e)}") - return f"An error occurred while checking your rewards: {str(e)}", "assistant", None + return ( + f"An error occurred while checking your rewards: {str(e)}", + "assistant", + None, + ) - def chat(self, request): + def chat(self, request: ChatRequest): try: - data = request.get_json() - if 'prompt' in data and 'wallet_address' in data: - prompt = data['prompt'] - wallet_address = data['wallet_address'] - response, role, next_turn_agent = self.get_response(prompt, wallet_address) - return {"role": role, "content": response, "next_turn_agent": next_turn_agent} + data = request.dict() + if "prompt" in data and "wallet_address" in data: + prompt = data["prompt"] + wallet_address = data["wallet_address"] + response, role, next_turn_agent = self.get_response( + prompt, wallet_address + ) + return { + "role": role, + "content": response, + "next_turn_agent": next_turn_agent, + } else: logger.warning("Missing required parameters in request") return {"error": "Missing required parameters"}, 400 diff --git a/submodules/moragents_dockers/agents/src/agents/mor_rewards/config.py b/submodules/moragents_dockers/agents/src/agents/mor_rewards/config.py new file mode 100644 index 0000000..e8ac97e --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/mor_rewards/config.py @@ -0,0 +1,26 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + + +# Configuration object +class Config: + + WEB3RPCURL = { + "1": "https://eth.llamarpc.com/", + } + + DISTRIBUTION_PROXY_ADDRESS = "0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790" + DISTRIBUTION_ABI = [ + { + "inputs": [ + {"internalType": "uint256", "name": "poolId_", "type": "uint256"}, + {"internalType": "address", "name": "user_", "type": "address"}, + ], + "name": "getCurrentUserReward", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + } + ] diff --git a/submodules/moragents_dockers/agents/src/reward_agent/src/tools.py b/submodules/moragents_dockers/agents/src/agents/mor_rewards/tools.py similarity index 73% rename from submodules/moragents_dockers/agents/src/reward_agent/src/tools.py rename to submodules/moragents_dockers/agents/src/agents/mor_rewards/tools.py index cbb749f..553041e 100644 --- a/submodules/moragents_dockers/agents/src/reward_agent/src/tools.py +++ b/submodules/moragents_dockers/agents/src/agents/mor_rewards/tools.py @@ -1,11 +1,13 @@ from web3 import Web3 -from reward_agent.src.config import Config + +from src.agents.mor_rewards.config import Config + def get_current_user_reward(wallet_address, pool_id): web3 = Web3(Web3.HTTPProvider(Config.WEB3RPCURL["1"])) distribution_contract = web3.eth.contract( - address=web3.to_checksum_address(Config.DISTRIBUTION_PROXY_ADDRESS), - abi=Config.DISTRIBUTION_ABI + address=web3.to_checksum_address(Config.DISTRIBUTION_PROXY_ADDRESS), + abi=Config.DISTRIBUTION_ABI, ) try: @@ -13,14 +15,14 @@ def get_current_user_reward(wallet_address, pool_id): raise Exception("Unable to connect to Ethereum network") reward = distribution_contract.functions.getCurrentUserReward( - pool_id, - web3.to_checksum_address(wallet_address) + pool_id, web3.to_checksum_address(wallet_address) ).call() - formatted_reward = web3.from_wei(reward, 'ether') + formatted_reward = web3.from_wei(reward, "ether") return round(formatted_reward, 4) except Exception as e: raise Exception(f"Error occurred while fetching the reward: {str(e)}") + def get_tools(): return [ { @@ -33,15 +35,15 @@ def get_tools(): "properties": { "wallet_address": { "type": "string", - "description": "The wallet address to check rewards for" + "description": "The wallet address to check rewards for", }, "pool_id": { "type": "integer", - "description": "The ID of the pool to check rewards from" - } + "description": "The ID of the pool to check rewards from", + }, }, - "required": ["wallet_address", "pool_id"] - } - } + "required": ["wallet_address", "pool_id"], + }, + }, } - ] \ No newline at end of file + ] diff --git a/submodules/moragents_dockers/agents/src/agents/news_agent/__init__.py b/submodules/moragents_dockers/agents/src/agents/news_agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/agents/news_agent/agent.py b/submodules/moragents_dockers/agents/src/agents/news_agent/agent.py new file mode 100644 index 0000000..ecbb4d0 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/news_agent/agent.py @@ -0,0 +1,160 @@ +import json +import logging +import re +import urllib.parse +from src.agents.news_agent.config import Config +from src.agents.news_agent.tools import ( + clean_html, + is_within_time_window, + fetch_rss_feed, +) +from src.models.messages import ChatRequest +import pyshorteners + +logger = logging.getLogger(__name__) + + +class NewsAgent: + def __init__(self, agent_info, llm, embeddings): + self.agent_info = agent_info + self.llm = llm + self.embeddings = embeddings + self.tools_provided = self.get_tools() + self.url_shortener = pyshorteners.Shortener() + logger.info("NewsAgent initialized") + + def get_tools(self): + return [ + { + "type": "function", + "function": { + "name": "fetch_crypto_news", + "description": "Fetch and analyze cryptocurrency news for potential price impacts", + "parameters": { + "type": "object", + "properties": { + "coins": { + "type": "array", + "items": {"type": "string"}, + "description": "List of cryptocurrency symbols to fetch news for", + } + }, + "required": ["coins"], + }, + }, + + } + ] + + def check_relevance_and_summarize(self, title, content, coin): + logger.info(f"Checking relevance for {coin}: {title}") + prompt = Config.RELEVANCE_PROMPT.format(coin=coin, title=title, content=content) + result = self.llm.invoke( + input=[{"role": "user", "content": prompt}], + max_tokens=Config.LLM_MAX_TOKENS, + temperature=Config.LLM_TEMPERATURE, + ) + return result.content.strip() + + def process_rss_feed(self, feed_url, coin): + logger.info(f"Processing RSS feed for {coin}: {feed_url}") + feed = fetch_rss_feed(feed_url) + results = [] + for entry in feed.entries: + published_time = entry.get("published") or entry.get("updated") + if is_within_time_window(published_time): + title = clean_html(entry.title) + content = clean_html(entry.summary) + logger.info(f"Checking relevance for article: {title}") + result = self.check_relevance_and_summarize(title, content, coin) + if not result.upper().startswith("NOT RELEVANT"): + results.append( + {"Title": title, "Summary": result, "Link": entry.link} + ) + if len(results) >= Config.ARTICLES_PER_TOKEN: + break + else: + logger.info( + f"Skipping article: {entry.title} (published: {published_time})" + ) + logger.info(f"Found {len(results)} relevant articles for {coin}") + return results + + def fetch_crypto_news(self, coins): + logger.info(f"Fetching news for coins: {coins}") + all_news = [] + for coin in coins: + logger.info(f"Processing news for {coin}") + coin_name = Config.CRYPTO_DICT.get(coin.upper(), coin) + google_news_url = Config.GOOGLE_NEWS_BASE_URL.format(coin_name) + results = self.process_rss_feed(google_news_url, coin_name) + all_news.extend( + [ + {"Coin": coin, **result} + for result in results[: Config.ARTICLES_PER_TOKEN] + ] + ) + + logger.info(f"Total news items fetched: {len(all_news)}") + return all_news + + def chat(self, request: ChatRequest): + try: + data = request.dict() + if "prompt" in data: + prompt = data["prompt"] + if isinstance(prompt, dict) and "content" in prompt: + prompt = prompt["content"] + + # Updated coin detection logic + coins = re.findall( + r"\b(" + + "|".join(re.escape(key) for key in Config.CRYPTO_DICT.keys()) + + r")\b", + prompt.upper(), + ) + + if not coins: + return { + "role": "assistant", + "content": "I couldn't identify any cryptocurrency symbols in your message. Please specify the cryptocurrencies you want news for.", + "next_turn_agent": None, + } + + news = self.fetch_crypto_news(coins) + + if not news: + return { + "role": "assistant", + "content": "No relevant news found for the specified cryptocurrencies in the last 24 hours.", + "next_turn_agent": None, + } + + response = "Here are the latest news items relevant to changes in price movement of the mentioned tokens in the last 24 hours:\n\n" + for index, item in enumerate(news, start=1): + coin_name = Config.CRYPTO_DICT.get(item["Coin"], item["Coin"]) + short_url = self.url_shortener.tinyurl.short(item["Link"]) + response += f"{index}. ***{coin_name} News***:\n" + response += f"{item['Title']}\n" + response += f"{item['Summary']}\n" + response += f"Read more: {short_url}\n\n" + + return { + "role": "assistant", + "content": response, + "next_turn_agent": None, + } + else: + return { + "role": "assistant", + "content": "Missing required parameters", + "next_turn_agent": None, + } + + except Exception as e: + logger.error(f"Error in chat method: {str(e)}", exc_info=True) + return { + "role": "assistant", + "content": f"An error occurred: {str(e)}", + "next_turn_agent": None, + } diff --git a/submodules/moragents_dockers/agents/src/agents/news_agent/config.py b/submodules/moragents_dockers/agents/src/agents/news_agent/config.py new file mode 100644 index 0000000..e9fe7d3 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/news_agent/config.py @@ -0,0 +1,130 @@ +import logging + +logging.basicConfig(level=logging.INFO) + +class Config: + # RSS Feed URL + GOOGLE_NEWS_BASE_URL = "https://news.google.com/rss/search?q={}&hl=en-US&gl=US&ceid=US:en" + + # Time window for news (in hours) + NEWS_TIME_WINDOW = 24 + + # Number of articles to show per token + ARTICLES_PER_TOKEN = 1 + + # LLM configuration + LLM_MAX_TOKENS = 150 + LLM_TEMPERATURE = 0.3 + + # Prompts + RELEVANCE_PROMPT = ( + "Consider the following news article about {coin}:\n\n" + "Title: {title}\n\nContent: {content}\n\n" + "Is this article relevant to potential price impacts on the cryptocurrency? " + "If yes, provide a concise summary focused on how it might impact trading or prices. " + "If it's not relevant or only about price movements, respond with 'NOT RELEVANT'." + ) + + # Dictionary of top 100 popular tickers and their crypto names + CRYPTO_DICT = { + 'BTC': 'Bitcoin', + 'ETH': 'Ethereum', + 'USDT': 'Tether', + 'BNB': 'BNB', + 'SOL': 'Solana', + 'USDC': 'USDC', + 'XRP': 'XRP', + 'STETH': 'Lido Staked Ether', + 'DOGE': 'Dogecoin', + 'TON': 'Toncoin', + 'ADA': 'Cardano', + 'TRX': 'TRON', + 'AVAX': 'Avalanche', + 'WSTETH': 'Wrapped stETH', + 'SHIB': 'Shiba Inu', + 'WBTC': 'Wrapped Bitcoin', + 'WETH': 'Binance-Peg WETH', + 'LINK': 'Chainlink', + 'BCH': 'Bitcoin Cash', + 'DOT': 'Polkadot', + 'NEAR': 'NEAR Protocol', + 'UNI': 'Uniswap', + 'LEO': 'LEO Token', + 'DAI': 'Dai', + 'SUI': 'Sui', + 'LTC': 'Litecoin', + 'PEPE': 'Pepe', + 'ICP': 'Internet Computer', + 'WEETH': 'Wrapped eETH', + 'TAO': 'Bittensor', + 'FET': 'Artificial Superintelligence Alliance', + 'APT': 'Aptos', + 'KAS': 'Kaspa', + 'POL': 'POL (ex-MATIC)', + 'XLM': 'Stellar', + 'ETC': 'Ethereum Classic', + 'STX': 'Stacks', + 'FDUSD': 'First Digital USD', + 'IMX': 'Immutable', + 'XMR': 'Monero', + 'RENDER': 'Render', + 'WIF': 'dogwifhat', + 'USDE': 'Ethena USDe', + 'OKB': 'OKB', + 'AAVE': 'Aave', + 'INJ': 'Injective', + 'OP': 'Optimism', + 'FIL': 'Filecoin', + 'CRO': 'Cronos', + 'ARB': 'Arbitrum', + 'HBAR': 'Hedera', + 'FTM': 'Fantom', + 'MNT': 'Mantle', + 'VET': 'VeChain', + 'ATOM': 'Cosmos Hub', + 'RUNE': 'THORChain', + 'BONK': 'Bonk', + 'GRT': 'The Graph', + 'SEI': 'Sei', + 'WBT': 'WhiteBIT Coin', + 'FLOKI': 'FLOKI', + 'AR': 'Arweave', + 'THETA': 'Theta Network', + 'RETH': 'Rocket Pool ETH', + 'BGB': 'Bitget Token', + 'MKR': 'Maker', + 'HNT': 'Helium', + 'METH': 'Mantle Staked Ether', + 'SOLVBTC': 'Solv Protocol SolvBTC', + 'PYTH': 'Pyth Network', + 'TIA': 'Celestia', + 'JUP': 'Jupiter', + 'LDO': 'Lido DAO', + 'MATIC': 'Polygon', + 'ONDO': 'Ondo', + 'ALGO': 'Algorand', + 'GT': 'Gate', + 'JASMY': 'JasmyCoin', + 'QNT': 'Quant', + 'OM': 'MANTRA', + 'BEAM': 'Beam', + 'POPCAT': 'Popcat', + 'BSV': 'Bitcoin SV', + 'KCS': 'KuCoin', + 'EZETH': 'Renzo Restaked ETH', + 'CORE': 'Core', + 'BRETT': 'Brett', + 'WLD': 'Worldcoin', + 'GALA': 'GALA', + 'BTT': 'BitTorrent', + 'FLOW': 'Flow', + 'NOT': 'Notcoin', + 'STRK': 'Starknet', + 'EETH': 'ether.fi Staked ETH', + 'MSOL': 'Marinade Staked SOL', + 'EIGEN': 'Eigenlayer', + 'ORDI': 'ORDI', + 'CFX': 'Conflux', + 'W': 'Wormhole', + 'MOR': 'Morpheus AI' + } \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/agents/news_agent/tools.py b/submodules/moragents_dockers/agents/src/agents/news_agent/tools.py new file mode 100644 index 0000000..f017b22 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/news_agent/tools.py @@ -0,0 +1,71 @@ +import feedparser +from datetime import datetime, timedelta +import pytz +from dateutil import parser +import re +from html import unescape +from src.agents.news_agent.config import Config +import logging +import urllib.parse + +logger = logging.getLogger(__name__) + + +def clean_html(raw_html): + cleanr = re.compile('<.*?>') + cleantext = re.sub(cleanr, '', raw_html) + cleantext = unescape(cleantext) + cleantext = ' '.join(cleantext.split()) + return cleantext + + +def is_within_time_window(published_time, hours=24): + if not published_time: + return False + try: + pub_date = parser.parse(published_time, fuzzy=True) + now = datetime.now(pytz.UTC) + if pub_date.tzinfo is None: + pub_date = pub_date.replace(tzinfo=pytz.UTC) + return (now - pub_date) <= timedelta(hours=hours) + except Exception as e: + logger.error(f"Error parsing date: {str(e)} for date {published_time}") + return False + + +def fetch_rss_feed(feed_url): + # URL encode the query parameter + parsed_url = urllib.parse.urlparse(feed_url) + query_params = urllib.parse.parse_qs(parsed_url.query) + if 'q' in query_params: + query_params['q'] = [urllib.parse.quote(q) for q in query_params['q']] + encoded_query = urllib.parse.urlencode(query_params, doseq=True) + encoded_url = urllib.parse.urlunparse(parsed_url._replace(query=encoded_query)) + + return feedparser.parse(encoded_url) + + +def get_tools(): + """Return a list of tools for the agent.""" + return [ + { + "type": "function", + "function": { + "name": "fetch_crypto_news", + "description": "Fetch and analyze cryptocurrency news for potential price impacts", + "parameters": { + "type": "object", + "properties": { + "coins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of cryptocurrency symbols to fetch news for" + } + }, + "required": ["coins"] + } + } + } + ] \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/agents/rag/__init__.py b/submodules/moragents_dockers/agents/src/agents/rag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/agents/rag/agent.py b/submodules/moragents_dockers/agents/src/agents/rag/agent.py new file mode 100644 index 0000000..977de93 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/rag/agent.py @@ -0,0 +1,119 @@ +import os +import logging + +from fastapi import Request +from werkzeug.utils import secure_filename + +from langchain_community.document_loaders import PyMuPDFLoader +from langchain_community.vectorstores import FAISS +from langchain_core.prompts import ChatPromptTemplate +from langchain_text_splitters.character import RecursiveCharacterTextSplitter + +from src.models.messages import ChatRequest +from src.stores import chat_manager + +logger = logging.getLogger(__name__) + +UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads") + + +class RagAgent: + def __init__(self, config, llm, embeddings): + self.config = config + self.llm = llm + self.embedding = embeddings + self.messages = [ + {"role": "assistant", "content": "Please upload a file to begin"} + ] + + self.prompt = ChatPromptTemplate.from_template( + """ + Answer the following question only based on the given context + + + {context} + + + Question: {input} + """ + ) + self.max_size = 5 * 1024 * 1024 + self.retriever = None + + async def handle_file_upload(self, file): + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + filename = secure_filename(file.filename) + file_path = os.path.join(UPLOAD_FOLDER, filename) + + # Save the file + with open(file_path, "wb") as buffer: + content = await file.read() + buffer.write(content) + + # DocumentToolsGenerator class instantiation + loader = PyMuPDFLoader(file_path) + docs = loader.load() + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1024, + chunk_overlap=20, + length_function=len, + is_separator_regex=False, + ) + split_documents = text_splitter.split_documents(docs) + vector_store = FAISS.from_documents(split_documents, self.embedding) + self.retriever = vector_store.as_retriever(search_kwargs={"k": 7}) + + async def upload_file(self, request: Request): + logger.info(f"Received upload request: {request}") + file = request["file"] + if file.filename == "": + return {"error": "No selected file"}, 400 + + # Check file size to ensure it's less than 5 MB + content = await file.read() + await file.seek(0) + if len(content) > self.max_size: + return {"role": "assistant", "content": "Please use a file less than 5 MB"} + + try: + await self.handle_file_upload(file) + chat_manager.set_uploaded_file(True) + return { + "role": "assistant", + "content": "You have successfully uploaded the text", + } + except Exception as e: + logging.error(f"Error during file upload: {str(e)}") + return {"error": str(e)}, 500 + + def _get_rag_response(self, prompt): + retrieved_docs = self.retriever.invoke(prompt) + formatted_context = "\n\n".join(doc.page_content for doc in retrieved_docs) + formatted_prompt = f"Question: {prompt}\n\nContext: {formatted_context}" + messages = [ + { + "role": "system", + "content": "You are a helpful assistant. Use the provided context to respond to the following question.", + }, + {"role": "user", "content": formatted_prompt}, + ] + result = self.llm.invoke(messages) + return result.content.strip() + + def chat(self, request: ChatRequest): + try: + data = request.dict() + if "prompt" in data: + prompt = data["prompt"]["content"] + if chat_manager.get_uploaded_file_status(): + response = self._get_rag_response(prompt) + else: + response = "Please upload a file first" + return {"role": "assistant", "content": response} + + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + logging.error(f"Error in chat endpoint: {str(e)}") + return {"Error": str(e)}, 500 diff --git a/submodules/moragents_dockers/agents/src/rag_agent/src/config.py b/submodules/moragents_dockers/agents/src/agents/rag/config.py similarity index 59% rename from submodules/moragents_dockers/agents/src/rag_agent/src/config.py rename to submodules/moragents_dockers/agents/src/agents/rag/config.py index 63983c2..13716f4 100644 --- a/submodules/moragents_dockers/agents/src/rag_agent/src/config.py +++ b/submodules/moragents_dockers/agents/src/agents/rag/config.py @@ -3,7 +3,8 @@ # Logging configuration logging.basicConfig(level=logging.INFO) + # Configuration object class Config: - MAX_FILE_SIZE=5 * 1024 * 1024 # 5 MB - MAX_LENGTH=16 * 1024 * 1024 \ No newline at end of file + MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB + MAX_LENGTH = 16 * 1024 * 1024 diff --git a/submodules/moragents_dockers/agents/src/agents/realtime_search/__init__.py b/submodules/moragents_dockers/agents/src/agents/realtime_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/agents/realtime_search/agent.py b/submodules/moragents_dockers/agents/src/agents/realtime_search/agent.py new file mode 100644 index 0000000..1c098b9 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/realtime_search/agent.py @@ -0,0 +1,137 @@ +import logging +import requests +import time + +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.options import Options + +from src.models.messages import ChatRequest + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + + +class RealtimeSearchAgent: + def __init__(self, config, llm, embeddings): + self.config = config + self.llm = llm + self.embeddings = embeddings + self.last_search_term = None + + def perform_search_with_web_scraping(self, search_term=None): + if search_term is not None: + self.last_search_term = search_term + elif self.last_search_term is None: + logger.warning("No search term available for web search") + return "Web search failed. Please provide a search term." + else: + search_term = self.last_search_term + + logger.info(f"Performing web search for: {search_term}") + + try: + url = f"https://www.google.com/search?q={search_term}" + headers = {"User-Agent": USER_AGENT} + response = requests.get(url, headers=headers) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + search_results = soup.find_all("div", class_="g") + + formatted_results = [] + for result in search_results[:5]: + result_text = result.get_text(strip=True) + formatted_results.append(f"Result:\n{result_text}") + + return "\n\n".join(formatted_results) + + except requests.RequestException as e: + logger.error(f"Error performing web search: {str(e)}") + logger.info("Attempting fallback to headless browsing") + return self.perform_search_with_headless_browsing(search_term) + + def perform_search_with_headless_browsing(self, search_term): + chrome_options = Options() + chrome_options.add_argument("--headless") + + driver = webdriver.Chrome(options=chrome_options) + + try: + driver.get("https://www.google.com") + + search_box = driver.find_element(By.NAME, "q") + search_box.send_keys(search_term) + search_box.send_keys(Keys.RETURN) + + time.sleep(2) + + soup = BeautifulSoup(driver.page_source, "html.parser") + + search_results = soup.find_all("div", class_="g") + + formatted_results = [] + for result in search_results[:5]: + result_text = result.get_text(strip=True) + formatted_results.append(f"Result:\n{result_text}") + + return "\n\n".join(formatted_results) + + except Exception as e: + logger.error(f"Error performing headless web search: {str(e)}") + return f"Error performing web search: {str(e)}" + finally: + driver.quit() + + def synthesize_answer(self, search_term, search_results): + logger.info("Synthesizing answer from search results") + messages = [ + { + "role": "system", + "content": """You are a helpful assistant that synthesizes information from web search results to answer user queries. + Do not preface your answer with 'Based on the search results, I can tell you that:' or anything similar. + Just provide the answer.""", + }, + { + "role": "user", + "content": f"""Based on the following search results for the query '{search_term}', provide a concise and informative answer: {search_results}""", + }, + ] + + try: + result = self.llm.invoke(messages) + logger.info(f"Received response from LLM: {result}") + return result.content.strip() + except Exception as e: + logger.error(f"Error synthesizing answer: {str(e)}") + raise + + def chat(self, request: ChatRequest): + try: + data = request.dict() + logger.info(f"Received chat request: {data}") + if "prompt" in data: + prompt = data["prompt"] + search_term = prompt["content"] + logger.info(f"Performing web search for prompt: {search_term}") + + search_results = self.perform_search_with_web_scraping(search_term) + logger.info(f"Search results obtained") + + synthesized_answer = self.synthesize_answer(search_term, search_results) + logger.info(f"Synthesized answer: {synthesized_answer}") + + return {"role": "assistant", "content": synthesized_answer} + else: + logger.error("Missing 'prompt' in chat request data") + return {"error": "Missing parameters"}, 400 + except Exception as e: + logger.exception(f"Unexpected error in chat method: {str(e)}") + return {"Error": str(e)}, 500 diff --git a/submodules/moragents_dockers/agents/src/swap_agent/README.md b/submodules/moragents_dockers/agents/src/agents/token_swap/README.md similarity index 100% rename from submodules/moragents_dockers/agents/src/swap_agent/README.md rename to submodules/moragents_dockers/agents/src/agents/token_swap/README.md diff --git a/submodules/moragents_dockers/agents/src/agents/token_swap/__init__.py b/submodules/moragents_dockers/agents/src/agents/token_swap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/agents/token_swap/agent.py b/submodules/moragents_dockers/agents/src/agents/token_swap/agent.py new file mode 100644 index 0000000..e99e9b3 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/token_swap/agent.py @@ -0,0 +1,221 @@ +import json +import requests +import logging + +from src.agents.token_swap import tools +from src.agents.token_swap.config import Config +from src.models.messages import ChatRequest +from src.stores import agent_manager + +logger = logging.getLogger(__name__) + + +class TokenSwapAgent: + def __init__(self, config, llm, embeddings): + self.config = config + self.llm = llm + self.embeddings = embeddings + self.tools_provided = tools.get_tools() + self.context = [] + + def api_request_url(self, method_name, query_params, chain_id): + base_url = Config.APIBASEURL + str(chain_id) + return f"{base_url}{method_name}?{'&'.join([f'{key}={value}' for key, value in query_params.items()])}" + + def check_allowance(self, token_address, wallet_address, chain_id): + url = self.api_request_url( + "/approve/allowance", + {"tokenAddress": token_address, "walletAddress": wallet_address}, + chain_id, + ) + response = requests.get(url, headers=Config.HEADERS) + data = response.json() + return data + + def approve_transaction(self, token_address, chain_id, amount=None): + query_params = ( + {"tokenAddress": token_address, "amount": amount} + if amount + else {"tokenAddress": token_address} + ) + url = self.api_request_url("/approve/transaction", query_params, chain_id) + response = requests.get(url, headers=Config.HEADERS) + transaction = response.json() + return transaction + + def build_tx_for_swap(self, swap_params, chain_id): + url = self.api_request_url("/swap", swap_params, chain_id) + swap_transaction = requests.get(url, headers=Config.HEADERS).json() + return swap_transaction + + def get_response(self, message, chain_id, wallet_address): + system_prompt = ( + "Don't make assumptions about the value of the arguments for the function " + "they should always be supplied by the user and do not alter the value of the arguments. " + "Don't make assumptions about what values to plug into functions. Ask for clarification if a user " + "request is ambiguous. you only need the value of token1 we dont need the value of token2. After " + "starting from scratch do not assume the name of token1 or token2" + ) + + messages = [ + {"role": "system", "content": system_prompt}, + ] + messages.extend(message) + + logger.info("Sending request to LLM with %d messages", len(messages)) + + llm_with_tools = self.llm.bind_tools(self.tools_provided) + + try: + result = llm_with_tools.invoke(messages) + logger.info("Received response from LLM: %s", result) + + if result.tool_calls: + tool_call = result.tool_calls[0] + logger.info("Selected tool: %s", tool_call) + func_name = tool_call.get("name") + args = tool_call.get("args") + logger.info("LLM suggested using tool: %s", func_name) + + if func_name == "swap_agent": + tok1 = args["token1"] + tok2 = args["token2"] + value = args["value"] + try: + res, role = tools.swap_coins( + tok1, tok2, float(value), chain_id, wallet_address + ) + except ( + tools.InsufficientFundsError, + tools.TokenNotFoundError, + tools.SwapNotPossibleError, + ) as e: + self.context = [] + return str(e), "assistant", None + return res, role, None + else: + logger.info("LLM provided a direct response without using tools") + return result.content, "assistant", "crypto swap agent" + except Exception as e: + logger.error(f"Error in get_response: {str(e)}") + return f"An error occurred: {str(e)}", "assistant", None + + def get_status(self, flag, tx_hash, tx_type): + response = "" + + if flag == "cancelled": + response = f"The {tx_type} transaction has been cancelled." + elif flag == "success": + response = f"The {tx_type} transaction was successful." + elif flag == "failed": + response = f"The {tx_type} transaction has failed." + elif flag == "initiated": + response = f"Transaction has been sent, please wait for it to be confirmed." + + if tx_hash: + response = response + f" The transaction hash is {tx_hash}." + + if flag == "success" and tx_type == "approve": + response = response + " Please proceed with the swap transaction." + elif flag != "initiated": + response = response + " Is there anything else I can help you with?" + + if flag != "initiated": + self.context = [] + self.context.append({"role": "assistant", "content": response}) + self.context.append( + {"role": "user", "content": "okay lets start again from scratch"} + ) + + return {"role": "assistant", "content": response} + + def generate_response(self, prompt, chain_id, wallet_address): + self.context.append(prompt) + response, role, next_turn_agent = self.get_response( + self.context, chain_id, wallet_address + ) + return response, role, next_turn_agent + + def chat(self, request: ChatRequest): + data = request.dict() + try: + if "prompt" in data: + prompt = data["prompt"] + wallet_address = data["wallet_address"] + chain_id = data["chain_id"] + response, role, next_turn_agent = self.generate_response( + prompt, chain_id, wallet_address + ) + return { + "role": role, + "content": response, + "next_turn_agent": next_turn_agent, + } + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 + + def tx_status(self, data): + try: + if "status" in data: + prompt = data["status"] + tx_hash = data.get("tx_hash", "") + tx_type = data.get("tx_type", "") + response = self.get_status(prompt, tx_hash, tx_type) + return response + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 + + def get_allowance(self, request_data): + try: + if "tokenAddress" in request_data: + token = request_data["tokenAddress"] + wallet_address = request_data["walletAddress"] + chain_id = request_data["chain_id"] + res = self.check_allowance(token, wallet_address, chain_id) + return {"response": res} + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 + + def approve(self, request_data): + try: + if "tokenAddress" in request_data: + token = request_data["tokenAddress"] + chain_id = request_data["chain_id"] + amount = request_data["amount"] + res = self.approve_transaction(token, chain_id, amount) + return {"response": res} + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 + + def swap(self, request_data): + try: + if "src" in request_data: + token1 = request_data["src"] + token2 = request_data["dst"] + wallet_address = request_data["walletAddress"] + amount = request_data["amount"] + slippage = request_data["slippage"] + chain_id = request_data["chain_id"] + swap_params = { + "src": token1, + "dst": token2, + "amount": amount, + "from": wallet_address, + "slippage": slippage, + "disableEstimate": False, + "allowPartialFill": False, + } + swap_transaction = self.build_tx_for_swap(swap_params, chain_id) + return swap_transaction + else: + return {"error": "Missing required parameters"}, 400 + except Exception as e: + return {"Error": str(e)}, 500 diff --git a/submodules/moragents_dockers/agents/src/agents/token_swap/config.py b/submodules/moragents_dockers/agents/src/agents/token_swap/config.py new file mode 100644 index 0000000..d479874 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/agents/token_swap/config.py @@ -0,0 +1,54 @@ +import logging + +# Logging configuration +logging.basicConfig(level=logging.INFO) + + +# Configuration object +class Config: + + # 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://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", + }, + { + "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" diff --git a/submodules/moragents_dockers/agents/src/swap_agent/src/tools.py b/submodules/moragents_dockers/agents/src/agents/token_swap/tools.py similarity index 61% rename from submodules/moragents_dockers/agents/src/swap_agent/src/tools.py rename to submodules/moragents_dockers/agents/src/agents/token_swap/tools.py index 1800dfe..2e2c871 100644 --- a/submodules/moragents_dockers/agents/src/swap_agent/src/tools.py +++ b/submodules/moragents_dockers/agents/src/agents/token_swap/tools.py @@ -2,52 +2,69 @@ import logging import time from web3 import Web3 -from swap_agent.src.config import Config + +from src.agents.token_swap.config import Config class InsufficientFundsError(Exception): pass + class TokenNotFoundError(Exception): pass + class SwapNotPossibleError(Exception): pass def search_tokens(query, chain_id, limit=1, ignore_listed="false"): endpoint = f"/v1.2/{chain_id}/search" - params = { - "query": query, - "limit": limit, - "ignore_listed": ignore_listed - } - response = requests.get(Config.INCH_URL + endpoint, params=params, headers=Config.HEADERS) + params = {"query": query, "limit": limit, "ignore_listed": ignore_listed} + response = requests.get( + Config.INCH_URL + endpoint, params=params, headers=Config.HEADERS + ) if response.status_code == 200: return response.json() else: logging.error(f"Failed to search tokens. Status code: {response.status_code}") return None -def get_token_balance(web3: Web3, wallet_address: str, token_address: str, abi: list) -> int: - """ Get the balance of an ERC-20 token for a given wallet address. """ - if not token_address: # If no token address is provided, assume checking ETH or native token balance + +def get_token_balance( + web3: Web3, wallet_address: str, token_address: str, abi: list +) -> int: + """Get the balance of an ERC-20 token for a given wallet address.""" + if ( + not token_address + ): # If no token address is provided, assume checking ETH or native token balance return web3.eth.get_balance(web3.to_checksum_address(wallet_address)) else: - contract = web3.eth.contract(address=web3.to_checksum_address(token_address), abi=abi) - return contract.functions.balanceOf(web3.to_checksum_address(wallet_address)).call() + contract = web3.eth.contract( + address=web3.to_checksum_address(token_address), abi=abi + ) + return contract.functions.balanceOf( + web3.to_checksum_address(wallet_address) + ).call() + def eth_to_wei(amount_in_eth: float) -> int: """Convert an amount in ETH to wei.""" return int(amount_in_eth * 10**18) + def validate_swap(web3: Web3, token1, token2, chain_id, amount, wallet_address): native = Config.NATIVE_TOKENS # token1 is the native token if token1.lower() == native[str(chain_id)].lower(): - t1 = [{'symbol': native[str(chain_id)], 'address': Config.INCH_NATIVE_TOKEN_ADDRESS}] - t1_bal = get_token_balance(web3, wallet_address, '', Config.ERC20_ABI) + t1 = [ + { + "symbol": native[str(chain_id)], + "address": Config.INCH_NATIVE_TOKEN_ADDRESS, + } + ] + t1_bal = get_token_balance(web3, wallet_address, "", Config.ERC20_ABI) smallest_amount = eth_to_wei(amount) # token1 is an erc20 token @@ -56,12 +73,19 @@ def validate_swap(web3: Web3, token1, token2, chain_id, amount, wallet_address): time.sleep(2) if not t1: raise TokenNotFoundError(f"Token {token1} not found.") - t1_bal = get_token_balance(web3, wallet_address, t1[0]['address'], Config.ERC20_ABI) - smallest_amount = convert_to_smallest_unit(web3, amount, t1[0]['address']) + t1_bal = get_token_balance( + web3, wallet_address, t1[0]["address"], Config.ERC20_ABI + ) + smallest_amount = convert_to_smallest_unit(web3, amount, t1[0]["address"]) # Check if token2 is the native token if token2.lower() == native[str(chain_id)].lower(): - t2 = [{'symbol': native[str(chain_id)], 'address': Config.INCH_NATIVE_TOKEN_ADDRESS}] + t2 = [ + { + "symbol": native[str(chain_id)], + "address": Config.INCH_NATIVE_TOKEN_ADDRESS, + } + ] else: t2 = search_tokens(token2, chain_id) time.sleep(2) @@ -72,54 +96,64 @@ def validate_swap(web3: Web3, token1, token2, chain_id, amount, wallet_address): if t1_bal < smallest_amount: raise InsufficientFundsError(f"Insufficient funds to perform the swap.") - return t1[0]['address'], t1[0]['symbol'], t2[0]['address'], t2[0]['symbol'] + return t1[0]["address"], t1[0]["symbol"], t2[0]["address"], t2[0]["symbol"] + def get_quote(token1, token2, amount_in_wei, chain_id): endpoint = f"/v6.0/{chain_id}/quote" - params = { - "src": token1, - "dst": token2, - "amount": int(amount_in_wei) - } - response = requests.get(Config.QUOTE_URL + endpoint, params=params, headers=Config.HEADERS) + params = {"src": token1, "dst": token2, "amount": int(amount_in_wei)} + response = requests.get( + Config.QUOTE_URL + endpoint, params=params, headers=Config.HEADERS + ) if response.status_code == 200: return response.json() else: logging.error(f"Failed to get quote. Status code: {response.status_code}") return None + def get_token_decimals(web3: Web3, token_address: str) -> int: if not token_address: return 18 # Assuming 18 decimals for the native gas token else: - contract = web3.eth.contract(address=Web3.to_checksum_address(token_address), abi=Config.ERC20_ABI) + contract = web3.eth.contract( + address=Web3.to_checksum_address(token_address), abi=Config.ERC20_ABI + ) return contract.functions.decimals().call() + def convert_to_smallest_unit(web3: Web3, amount: float, token_address: str) -> int: decimals = get_token_decimals(web3, token_address) - return int(amount * (10 ** decimals)) + return int(amount * (10**decimals)) + -def convert_to_readable_unit(web3: Web3, smallest_unit_amount: int, token_address: str) -> float: +def convert_to_readable_unit( + web3: Web3, smallest_unit_amount: int, token_address: str +) -> float: decimals = get_token_decimals(web3, token_address) - return smallest_unit_amount / (10 ** decimals) + return smallest_unit_amount / (10**decimals) def swap_coins(token1, token2, amount, chain_id, wallet_address): """Swap two crypto coins with each other""" web3 = Web3(Web3.HTTPProvider(Config.WEB3RPCURL[str(chain_id)])) - t1_a, t1_id, t2_a, t2_id = validate_swap(web3, token1, token2, chain_id, amount, wallet_address) + t1_a, t1_id, t2_a, t2_id = validate_swap( + web3, token1, token2, chain_id, amount, wallet_address + ) time.sleep(2) - t1_address = '' if t1_a == Config.INCH_NATIVE_TOKEN_ADDRESS else t1_a + t1_address = "" if t1_a == Config.INCH_NATIVE_TOKEN_ADDRESS else t1_a smallest_unit_amount = convert_to_smallest_unit(web3, amount, t1_address) result = get_quote(t1_a, t2_a, smallest_unit_amount, chain_id) - + if result: price = result["dstAmount"] - t2_address = '' if t2_a == Config.INCH_NATIVE_TOKEN_ADDRESS else t2_a + t2_address = "" if t2_a == Config.INCH_NATIVE_TOKEN_ADDRESS else t2_a t2_quote = convert_to_readable_unit(web3, int(price), t2_address) else: - raise SwapNotPossibleError("Failed to generate a quote. Please ensure you're on the correct network.") + raise SwapNotPossibleError( + "Failed to generate a quote. Please ensure you're on the correct network." + ) return { "dst": t2_id, @@ -129,9 +163,10 @@ def swap_coins(token1, token2, amount, chain_id, wallet_address): "src_address": t1_a, "src_amount": amount, "approve_tx_cb": "/approve", - "swap_tx_cb": "/swap" + "swap_tx_cb": "/swap", }, "swap" + def get_tools(): """Return a list of tools for the agent.""" return [ @@ -145,19 +180,19 @@ def get_tools(): "properties": { "token1": { "type": "string", - "description": "name or address of the cryptocurrency to sell" + "description": "name or address of the cryptocurrency to sell", }, "token2": { "type": "string", - "description": "name or address of the cryptocurrency to buy" + "description": "name or address of the cryptocurrency to buy", }, "value": { "type": "string", - "description": "Value or amount of the cryptocurrency to sell" - } + "description": "Value or amount of the cryptocurrency to sell", + }, }, - "required": ["token1", "token2", "value"] - } - } + "required": ["token1", "token2", "value"], + }, + }, } - ] \ No newline at end of file + ] diff --git a/submodules/moragents_dockers/agents/src/tweet_sizzler_agent/README.md b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/README.md similarity index 100% rename from submodules/moragents_dockers/agents/src/tweet_sizzler_agent/README.md rename to submodules/moragents_dockers/agents/src/agents/tweet_sizzler/README.md diff --git a/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/__init__.py b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/agent.py b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/agent.py similarity index 63% rename from submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/agent.py rename to submodules/moragents_dockers/agents/src/agents/tweet_sizzler/agent.py index 08dbf32..2cbd3ff 100644 --- a/submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/agent.py +++ b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/agent.py @@ -1,6 +1,8 @@ import logging import tweepy -from .config import Config + +from src.agents.tweet_sizzler.config import Config +from src.models.messages import ChatRequest # Configure logging logging.basicConfig( @@ -10,10 +12,10 @@ class TweetSizzlerAgent: - def __init__(self, config, llm, llm_ollama, embeddings, flask_app): - self.llm = llm - self.flask_app = flask_app + def __init__(self, config, llm, embeddings): self.config = config + self.llm = llm + self.embeddings = embeddings self.x_api_key = None self.last_prompt_content = None self.twitter_client = None @@ -38,20 +40,26 @@ def generate_tweet(self, prompt_content=None): ] try: - result = self.llm.create_chat_completion( - messages=messages, - max_tokens=Config.LLM_MAX_TOKENS, - temperature=Config.LLM_TEMPERATURE, - ) - tweet = result["choices"][0]["message"]["content"] + result = self.llm.invoke(messages) + logger.info(f"Received response from LLM: {result}") + tweet = result.content.strip() + tweet = " ".join(tweet.split()) + + # Remove any dictionary-like formatting, if present + if tweet.startswith("{") and tweet.endswith("}"): + tweet = tweet.strip("{}").split(":", 1)[-1].strip().strip('"') + logger.info(f"Tweet generated successfully: {tweet}") return tweet except Exception as e: logger.error(f"Error generating tweet: {str(e)}") raise - def post_tweet(self, request): - data = request.get_json() + async def post_tweet(self, request): + logger.info(f"Received tweet request: {request}") + data = await request.json() + logger.info(f"Received tweet data: {data}") + tweet_content = data.get("post_content") logger.info(f"Received tweet content: {tweet_content}") @@ -111,33 +119,37 @@ def set_x_api_key(self, request): return {"success": "API credentials saved successfully"}, 200 - def chat(self, request): + def chat(self, chat_request: ChatRequest): try: - data = request.get_json() - logger.info(f"Received chat request: {data}") - if "prompt" in data: - prompt = data["prompt"] - action = data.get("action", Config.DEFAULT_ACTION) - logger.debug(f"Extracted prompt: {prompt}, action: {action}") - - if action == "generate": - logger.info(f"Generating tweet for prompt: {prompt['content']}") - tweet = self.generate_tweet(prompt["content"]) - logger.info(f"Generated tweet: {tweet}") - return {"role": "assistant", "content": tweet} - elif action == "post": - logger.info("Attempting to post tweet") - result, status_code = self.post_tweet(request) - logger.info( - f"Posted tweet result: {result}, status code: {status_code}" - ) + prompt = chat_request.prompt.dict() + logger.info(f"Received chat request: {prompt}") + + action = prompt.get("action", Config.DEFAULT_ACTION) + logger.debug( + f"Extracted prompt content: {prompt['content']}, action: {action}" + ) + + if action == "generate": + logger.info(f"Generating tweet for prompt: {prompt['content']}") + tweet = self.generate_tweet(prompt["content"]) + logger.info(f"Generated tweet: {tweet}") + return {"role": "assistant", "content": tweet} + elif action == "post": + logger.info("Attempting to post tweet") + result, status_code = self.post_tweet(chat_request) + logger.info( + f"Posted tweet result: {result}, status code: {status_code}" + ) + if isinstance(result, dict) and "error" in result: return result, status_code - else: - logger.error(f"Invalid action received: {action}") - return {"error": Config.ERROR_INVALID_ACTION}, 400 + return { + "role": "assistant", + "content": f"Tweet posted successfully: {result.get('tweet', '')}", + }, status_code else: - logger.error("Missing 'prompt' in chat request data") - return {"error": Config.ERROR_MISSING_PARAMETERS}, 400 + logger.error(f"Invalid action received: {action}") + return {"role": "assistant", "content": Config.ERROR_INVALID_ACTION} + except Exception as e: logger.exception(f"Unexpected error in chat method: {str(e)}") - return {"Error": str(e)}, 500 + return {"role": "assistant", "content": f"Error: {str(e)}"} diff --git a/submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/config.py b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/config.py similarity index 76% rename from submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/config.py rename to submodules/moragents_dockers/agents/src/agents/tweet_sizzler/config.py index b7380b7..3b8e37b 100644 --- a/submodules/moragents_dockers/agents/src/tweet_sizzler_agent/src/config.py +++ b/submodules/moragents_dockers/agents/src/agents/tweet_sizzler/config.py @@ -21,9 +21,10 @@ class Config: "attention-grabbing tweets based on the user's prompt. It is CRUCIAL that you " "keep the tweets strictly under 280 characters - this is a hard limit. Make the " "tweets as engaging as possible while adhering to this character limit. Do not " - "surround your tweet with quotes. Do not preface it with any text like 'here is " - "your tweet'. Simply generate and output the tweet, ensuring it is less than " - "280 characters long." + "surround your tweet with quotes or any other formatting. Do not preface it with " + "any text like 'here is your tweet'. Simply generate and output the tweet, ensuring " + "it is less than 280 characters long. Use newlines sparingly. Do not surrounded with " + "quotes or braces. Do not use any other formatting." ) DEFAULT_ACTION = "generate" diff --git a/submodules/moragents_dockers/agents/src/app.py b/submodules/moragents_dockers/agents/src/app.py index dd84abe..ba9e865 100644 --- a/submodules/moragents_dockers/agents/src/app.py +++ b/submodules/moragents_dockers/agents/src/app.py @@ -1,21 +1,21 @@ import os import logging import time -from functools import wraps -from config import Config -from llama_cpp import Llama -from flask_cors import CORS -from flask import Flask, request, jsonify -from langchain_community.llms import Ollama -from delegator import Delegator -from llama_cpp.llama_tokenizer import LlamaHFTokenizer +import uvicorn + +from fastapi import FastAPI, HTTPException, Request, UploadFile, File +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +from langchain_ollama import ChatOllama from langchain_community.embeddings import OllamaEmbeddings +from src.config import Config +from src.delegator import Delegator +from src.stores import agent_manager, chat_manager +from src.models.messages import ChatRequest + # Constants -INITIAL_MESSAGE = { - "role": "assistant", - "content": "This highly experimental chatbot is not intended for making important decisions, and its responses are generated based on incomplete data and algorithms that may evolve rapidly. By using this chatbot, you acknowledge that you use it at your own discretion and assume all risks associated with its limitations and potential errors.", -} UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads") # Configure logging @@ -27,202 +27,158 @@ ) logger = logging.getLogger(__name__) +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -def load_llm(): - logger.info("Loading LLM model") - try: - llm = Llama( - model_path=Config.MODEL_PATH, - chat_format="functionary-v2", - tokenizer=LlamaHFTokenizer.from_pretrained( - "meetkai/functionary-small-v2.4-GGUF" - ), - n_gpu_layers=-1, # Use all available GPU layers - n_batch=1024, # Increase batch size for faster processing - n_ctx=1024, # Increase context size for better performance - verbose=False, # Disable verbose output for speed - use_mlock=True, # Lock memory to prevent swapping - use_mmap=True, # Use memory mapping for faster loading - n_threads=16, # Increase number of threads for more parallel processing - ) - logger.info("LLM model loaded successfully") - return llm - except Exception as e: - logger.error(f"Error loading LLM model: {str(e)}") - raise - - -app = Flask(__name__) -CORS(app) - -upload_state = False os.makedirs(UPLOAD_FOLDER, exist_ok=True) -app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER -app.config["MAX_CONTENT_LENGTH"] = Config.MAX_UPLOAD_LENGTH -try: - llm = load_llm() -except TimeoutError: - logger.error("LLM loading timed out") - llm = None -except Exception as e: - logger.error(f"Failed to load LLM: {str(e)}") - llm = None - -llm_ollama = Ollama(model="llama3.1", base_url=Config.OLLAMA_URL) -embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url=Config.OLLAMA_URL) +llm = ChatOllama( + model=Config.OLLAMA_MODEL, + base_url=Config.OLLAMA_URL, +) +embeddings = OllamaEmbeddings( + model=Config.OLLAMA_EMBEDDING_MODEL, base_url=Config.OLLAMA_URL +) -delegator = Delegator(Config.DELEGATOR_CONFIG, llm, llm_ollama, embeddings, app) -messages = [INITIAL_MESSAGE] -next_turn_agent = None +delegator = Delegator(Config.DELEGATOR_CONFIG, llm, embeddings) -@app.route("/", methods=["POST"]) -def chat(): - global next_turn_agent, messages - data = request.get_json() - logger.info(f"Received chat request: {data}") +@app.post("/chat") +async def chat(chat_request: ChatRequest): + prompt = chat_request.prompt.dict() + chat_manager.add_message(prompt) try: - current_agent = None - if "prompt" in data: - prompt = data["prompt"] - messages.append(prompt) + active_agent = agent_manager.get_active_agent() - if not next_turn_agent: - logger.info("No next turn agent, getting delegator response") + if not active_agent: + logger.info("No active agent, getting delegator response") start_time = time.time() - result = delegator.get_delegator_response(prompt, upload_state) + result = delegator.get_delegator_response( + prompt["content"], chat_manager.get_uploaded_file_status() + ) + end_time = time.time() + logger.info(f"Delegator response time: {end_time - start_time:.2f} seconds") logger.info(f"Delegator response: {result}") - if "next" not in result: - logger.error(f"Missing 'next' key in delegator response: {result}") - raise ValueError("Invalid delegator response: missing 'next' key") + if "agent" not in result: + logger.error(f"Missing 'agent' key in delegator response: {result}") + raise ValueError("Invalid delegator response: missing 'agent' key") - next_agent = result["next"] - current_agent, response_swap = delegator.delegate_chat(next_agent, request) - else: - logger.info(f"Delegating chat to next turn agent: {next_turn_agent}") - current_agent, response_swap = delegator.delegate_chat( - next_turn_agent, request - ) + active_agent = result["agent"] - # Handle both dictionary and tuple returns from delegate_chat - response, status_code = ( - response_swap if isinstance(response_swap, tuple) else (response_swap, 200) - ) + logger.info(f"Delegating chat to active agent: {active_agent}") + current_agent, response = delegator.delegate_chat(active_agent, chat_request) - # If response_swap is an error, reset next_turn_agent - next_turn_agent = ( - response_swap.get("next_turn_agent") - if isinstance(response_swap, dict) - else None - ) + if isinstance(response, tuple) and len(response) == 2: + error_message, status_code = response + logger.error(f"Error from agent: {error_message}") + raise HTTPException(status_code=status_code, detail=error_message) if isinstance(response, dict) and "role" in response and "content" in response: - response_with_agent = response.copy() - response_with_agent["agentName"] = current_agent or "Unknown" - - messages.append(response_with_agent) + chat_manager.add_response(response, current_agent or "Unknown") - logger.info("Sending response: %s", response_with_agent) - return jsonify(response_with_agent), status_code + logger.info(f"Sending response: {response}") + return response else: logger.error(f"Invalid response format: {response}") - return jsonify({"error": "Invalid response format"}), 500 + raise HTTPException(status_code=500, detail="Invalid response format") except TimeoutError: logger.error("Chat request timed out") - return jsonify({"error": "Request timed out"}), 504 + raise HTTPException(status_code=504, detail="Request timed out") except ValueError as ve: logger.error(f"Input formatting error: {str(ve)}") - return jsonify({"error": str(ve)}), 400 + raise HTTPException(status_code=400, detail=str(ve)) except Exception as e: logger.error(f"Error in chat route: {str(e)}", exc_info=True) - return jsonify({"error": str(e)}), 500 + raise HTTPException(status_code=500, detail=str(e)) -@app.route("/tx_status", methods=["POST"]) -def swap_agent_tx_status(): +@app.post("/tx_status") +async def swap_agent_tx_status(request: Request): logger.info("Received tx_status request") - response = delegator.delegate_route("crypto swap agent", request, "tx_status") - messages.append(response) - return jsonify(response) + response = await delegator.delegate_route("crypto swap agent", request, "tx_status") + chat_manager.add_message(response) + return response -@app.route("/messages", methods=["GET"]) -def get_messages(): +@app.get("/messages") +async def get_messages(): logger.info("Received get_messages request") - return jsonify({"messages": messages}) + return {"messages": chat_manager.get_messages()} -@app.route("/clear_messages", methods=["GET"]) -def clear_messages(): - global messages +@app.get("/clear_messages") +async def clear_messages(): logger.info("Clearing message history") - messages = [INITIAL_MESSAGE] - return jsonify({"response": "successfully cleared message history"}) + chat_manager.clear_messages() + return {"response": "successfully cleared message history"} -@app.route("/allowance", methods=["POST"]) -def swap_agent_allowance(): +@app.post("/allowance") +async def swap_agent_allowance(request: Request): logger.info("Received allowance request") return delegator.delegate_route("crypto swap agent", request, "get_allowance") -@app.route("/approve", methods=["POST"]) -def swap_agent_approve(): +@app.post("/approve") +async def swap_agent_approve(request: Request): logger.info("Received approve request") return delegator.delegate_route("crypto swap agent", request, "approve") -@app.route("/swap", methods=["POST"]) -def swap_agent_swap(): +@app.post("/swap") +async def swap_agent_swap(request: Request): logger.info("Received swap request") return delegator.delegate_route("crypto swap agent", request, "swap") -@app.route("/upload", methods=["POST"]) -def rag_agent_upload(): - global messages, upload_state +@app.post("/upload") +async def rag_agent_upload(file: UploadFile = File(...)): logger.info("Received upload request") - response = delegator.delegate_route( - "general purpose and context-based rag agent", request, "upload_file" + response = await delegator.delegate_route( + + "general purpose and context-based rag agent", {"file": file}, "upload_file" ) - messages.append(response) - upload_state = True - return jsonify(response) + chat_manager.add_message(response) + return response -@app.route("/regenerate_tweet", methods=["POST"]) -def regenerate_tweet(): +@app.post("/regenerate_tweet") +async def regenerate_tweet(): logger.info("Received generate tweet request") return delegator.delegate_route("tweet sizzler agent", None, "generate_tweet") -@app.route("/post_tweet", methods=["POST"]) -def post_tweet(): +@app.post("/post_tweet") +async def post_tweet(request: Request): logger.info("Received x post request") - return delegator.delegate_route("tweet sizzler agent", request, "post_tweet") + return await delegator.delegate_route("tweet sizzler agent", request, "post_tweet") -# TODO: Persist the X API key in the database (once we set this up) -@app.route("/set_x_api_key", methods=["POST"]) -def set_x_api_key(): +@app.post("/set_x_api_key") +async def set_x_api_key(request: Request): logger.info("Received set X API key request") - return delegator.delegate_route("tweet sizzler agent", request, "set_x_api_key") + return await delegator.delegate_route( + "tweet sizzler agent", request, "set_x_api_key" + ) + -@app.route("/claim", methods=["POST"]) -def claim_agent_claim(): +@app.post("/claim") +async def claim_agent_claim(request: Request): logger.info("Received claim request") return delegator.delegate_route("claim agent", request, "claim") -@app.route("/claim_status", methods=["POST"]) -def update_claim_status(): - return claim_agent.claim_status(request) if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=True) + uvicorn.run(app, host="0.0.0.0", port=5000, reload=True) diff --git a/submodules/moragents_dockers/agents/src/claim_agent/src/agent.py b/submodules/moragents_dockers/agents/src/claim_agent/src/agent.py deleted file mode 100644 index d753f52..0000000 --- a/submodules/moragents_dockers/agents/src/claim_agent/src/agent.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -from claim_agent.src import tools -from claim_agent.src.config import Config - -class ClaimAgent: - def __init__(self, agent_info, llm, llm_ollama, embeddings, flask_app): - self.agent_info = agent_info - self.llm = llm - self.tools_provided = tools.get_tools() - self.conversation_state = {} - - def get_response(self, message, wallet_address): - if wallet_address not in self.conversation_state: - self.conversation_state[wallet_address] = {"state": "initial"} - - state = self.conversation_state[wallet_address]["state"] - - if state == "initial": - rewards = { - 0: tools.get_current_user_reward(wallet_address, 0), - 1: tools.get_current_user_reward(wallet_address, 1) - } - available_rewards = {pool: amount for pool, amount in rewards.items() if amount > 0} - - if available_rewards: - selected_pool = max(available_rewards, key=available_rewards.get) - self.conversation_state[wallet_address]["available_rewards"] = {selected_pool: available_rewards[selected_pool]} - self.conversation_state[wallet_address]["receiver_address"] = wallet_address - self.conversation_state[wallet_address]["state"] = "awaiting_confirmation" - return f"You have {available_rewards[selected_pool]} MOR rewards available in pool {selected_pool}. Would you like to proceed with claiming these rewards?", "assistant", self.agent_info["name"] - else: - return f"No rewards found for your wallet address {wallet_address} in either pool. Claim cannot be processed.", "assistant", None - - elif state == "awaiting_confirmation": - user_input = message[-1]['content'].lower() - if any(word in user_input for word in ['yes', 'proceed', 'confirm', 'claim']): - return self.prepare_transactions(wallet_address) - else: - return "Please confirm if you want to proceed with the claim by saying 'yes', 'proceed', 'confirm', or 'claim'.", "assistant", self.agent_info["name"] - - return "I'm sorry, I didn't understand that. Can you please rephrase your request?", "assistant", self.agent_info["name"] - - def prepare_transactions(self, wallet_address): - available_rewards = self.conversation_state[wallet_address]["available_rewards"] - receiver_address = self.conversation_state[wallet_address]["receiver_address"] - transactions = [] - - for pool_id in available_rewards.keys(): - try: - tx_data = tools.prepare_claim_transaction(pool_id, receiver_address) - transactions.append({"pool": pool_id, "transaction": tx_data}) - except Exception as e: - return f"Error preparing transaction for pool {pool_id}: {str(e)}", "assistant", None - - self.conversation_state[wallet_address]["transactions"] = transactions - - # Return a structured response - return { - "role": "claim", - "content": { - "transactions": transactions, - "claim_tx_cb": "/claim" - } - }, "claim", None - - def chat(self, request): - try: - data = request.get_json() - if 'prompt' in data and 'wallet_address' in data: - prompt = data['prompt'] - wallet_address = data['wallet_address'] - response, role, next_turn_agent = self.get_response([prompt], wallet_address) - return {"role": role, "content": response, "next_turn_agent": next_turn_agent} - else: - return {"error": "Missing required parameters"}, 400 - except Exception as e: - return {"Error": str(e)}, 500 - - - def claim(self, request): - try: - data = request.get_json() - wallet_address = data['wallet_address'] - transactions = self.conversation_state[wallet_address]["transactions"] - return jsonify({"transactions": transactions}) - except Exception as e: - return jsonify({"error": str(e)}), 500 - - def claim_status(self, request): - try: - data = request.get_json() - wallet_address = data.get('wallet_address') - transaction_hash = data.get('transaction_hash') - status = data.get('status') - - if not all([wallet_address, transaction_hash, status]): - return jsonify({"error": "Missing required parameters"}), 400 - - # Generate and return the status message - response = self.get_status(status, transaction_hash, "claim") - return jsonify(response), 200 - except Exception as e: - return jsonify({"error": str(e)}), 500 - - - def get_status(self, flag, tx_hash, tx_type): - response = '' - - if flag == "cancelled": - response = f"The claim transaction has been cancelled." - elif flag == "success": - response = f"The claim transaction was successful." - elif flag == "failed": - response = f"The claim transaction has failed." - elif flag == "initiated": - response = f"Claim transaction has been sent, please wait for it to be confirmed." - - if tx_hash: - response = response + f" The transaction hash is {tx_hash}. " \ - f"Here's the link to the Etherscan transaction: " \ - f"https://etherscan.io/tx/{tx_hash}" - - if flag != "initiated": - response = response + " Is there anything else I can help you with?" - - return {"role": "assistant", "content": response} \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/src/claim_agent/src/config.py b/submodules/moragents_dockers/agents/src/claim_agent/src/config.py deleted file mode 100644 index c7bbddc..0000000 --- a/submodules/moragents_dockers/agents/src/claim_agent/src/config.py +++ /dev/null @@ -1,58 +0,0 @@ -import logging - -# Logging configuration -logging.basicConfig(level=logging.INFO) - -# Configuration object -class Config: - - WEB3RPCURL = { - "1": "https://eth.llamarpc.com/" - } - MINT_FEE = 0.001 # in ETH - - DISTRIBUTION_PROXY_ADDRESS = "0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790" - DISTRIBUTION_ABI = [ - { - "inputs": [ - { - "internalType": "uint256", - "name": "poolId_", - "type": "uint256" - }, - { - "internalType": "address", - "name": "receiver_", - "type": "address" - } - ], - "name": "claim", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "poolId_", - "type": "uint256" - }, - { - "internalType": "address", - "name": "user_", - "type": "address" - } - ], - "name": "getCurrentUserReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } - ] diff --git a/submodules/moragents_dockers/agents/src/config.py b/submodules/moragents_dockers/agents/src/config.py index f52f542..05a06ee 100644 --- a/submodules/moragents_dockers/agents/src/config.py +++ b/submodules/moragents_dockers/agents/src/config.py @@ -8,55 +8,68 @@ 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" + OLLAMA_MODEL = "llama3.2:3b" + OLLAMA_EMBEDDING_MODEL = "nomic-embed-text" OLLAMA_URL = "http://host.docker.internal:11434" + MAX_UPLOAD_LENGTH = 16 * 1024 * 1024 DELEGATOR_CONFIG = { "agents": [ { - "path": "rag_agent.src.agent", + "path": "src.agents.rag.agent", "class": "RagAgent", - "description": "Handles general queries, information retrieval, and context-based questions. Use for any query that doesn't explicitly match other agents' specialties.", + "description": "Must be used anytime an upload or uploaded document is referred to. Handles general queries, information retrieval, and context-based questions. Use for any query that doesn't explicitly match other agents' specialties.", "name": "general purpose and context-based rag agent", "upload_required": True, }, { - "path": "data_agent.src.agent", - "class": "DataAgent", + "path": "src.agents.crypto_data.agent", + "class": "CryptoDataAgent", "description": "Crypto-specific. Provides real-time cryptocurrency data such as price, market cap, and fully diluted valuation (FDV).", "name": "crypto data agent", "upload_required": False, }, + # { + # "path": "src.agents.token_swap.agent", + # "class": "TokenSwapAgent", + # "description": "Handles cryptocurrency swapping operations. Use when the query explicitly mentions swapping, exchanging, or converting one cryptocurrency to another.", + # "name": "token swap agent", + # "upload_required": False, + # }, { - "path": "swap_agent.src.agent", - "class": "SwapAgent", - "description": "Handles cryptocurrency swapping operations. Use when the query explicitly mentions swapping, exchanging, or converting one cryptocurrency to another.", - "name": "crypto swap agent", - "upload_required": False, - }, - { - "path": "tweet_sizzler_agent.src.agent", + "path": "src.agents.tweet_sizzler.agent", "class": "TweetSizzlerAgent", "description": "Generates and posts engaging tweets. Use when the query explicitly mentions Twitter, tweeting, or X platform.", "name": "tweet sizzler agent", "upload_required": False, }, + # { + # "path": "src.agents.mor_claims.agent", + # "class": "MorClaimsAgent", + # "description": "Manages the process of claiming rewards or tokens, specifically MOR rewards. Use when the query explicitly mentions claiming rewards or tokens.", + # "name": "mor claims agent", + # "upload_required": False, + # }, { - "path": "claim_agent.src.agent", - "class": "ClaimAgent", - "description": "Manages the process of claiming rewards or tokens, specifically MOR rewards. Use when the query explicitly mentions claiming rewards or tokens.", - "name": "claim agent", + "path": "src.agents.mor_rewards.agent", + "class": "MorRewardsAgent", + "description": "Provides information about user's accrued MOR rewards or tokens. Use when the query is about checking or querying reward balances.", + "name": "mor rewards agent", "upload_required": False, }, { - "path": "reward_agent.src.agent", - "class": "RewardAgent", - "description": "Provides information about user's accrued MOR rewards or tokens. Use when the query is about checking or querying reward balances.", - "name": "reward agent", + "path": "src.agents.realtime_search.agent", + "class": "RealtimeSearchAgent", + "description": "Only use this agent for real-time data. This agent is not for general purpose queries. Use when the query is about searching the web for real-time information.", + "name": "realtime search agent", "upload_required": False, }, + { + "path": "src.agents.news_agent.agent", + "class": "NewsAgent", + "description": "Fetches and analyzes cryptocurrency news for potential price impacts.", + "name": "crypto news agent", + "upload_required": False, + } ] } diff --git a/submodules/moragents_dockers/agents/src/data_agent/src/agent.py b/submodules/moragents_dockers/agents/src/data_agent/src/agent.py deleted file mode 100644 index 1f5dd54..0000000 --- a/submodules/moragents_dockers/agents/src/data_agent/src/agent.py +++ /dev/null @@ -1,79 +0,0 @@ -import json -from data_agent.src import tools -import logging - -logger = logging.getLogger(__name__) - - -class DataAgent: - def __init__(self, config, llm, llm_ollama, embeddings, flask_app): - self.llm = llm - self.flask_app = flask_app - self.config = config - self.tools_provided = tools.get_tools() - - def get_response(self, message): - messages = [ - { - "role": "system", - "content": ( - "Don't make assumptions about the value of the arguments for the function " - "they should always be supplied by the user and do not alter the value of the arguments. " - "Don't make assumptions about what values to plug into functions. Ask for clarification if a user " - "request is ambiguous." - ), - } - ] - messages.extend(message) - logger.info("Sending request to LLM with %d messages", len(messages)) - result = self.llm.create_chat_completion( - messages=messages, tools=self.tools_provided, tool_choice="auto" - ) - - logger.info("Received response from LLM: %s", result) - - if "tool_calls" in result["choices"][0]["message"].keys(): - func = result["choices"][0]["message"]["tool_calls"][0]["function"] - logger.info("LLM suggested using tool: %s", func["name"]) - args = json.loads(func["arguments"]) - if func["name"] == "get_price": - return tools.get_coin_price_tool(args["coin_name"]), "assistant" - elif func["name"] == "get_floor_price": - return tools.get_nft_floor_price_tool(args["nft_name"]), "assistant" - elif func["name"] == "get_fdv": - return ( - tools.get_fully_diluted_valuation_tool(args["coin_name"]), - "assistant", - ) - elif func["name"] == "get_tvl": - return ( - tools.get_protocol_total_value_locked_tool(args["protocol_name"]), - "assistant", - ) - elif func["name"] == "get_market_cap": - return tools.get_coin_market_cap_tool(args["coin_name"]), "assistant" - else: - logger.info("LLM provided a direct response without using tools") - return result["choices"][0]["message"]["content"], "assistant" - - def generate_response(self, prompt): - response, role = self.get_response([prompt]) - return response, role - - def chat(self, request): - try: - data = request.get_json() - if "prompt" in data: - prompt = data["prompt"] - logger.info( - "Received chat request with prompt: %s", - prompt[:50] + "..." if len(prompt) > 50 else prompt, - ) - response, role = self.generate_response(prompt) - return {"role": role, "content": response} - else: - logger.warning("Received chat request without 'prompt' in data") - return {"error": "Missing required parameters"}, 400 - except Exception as e: - logger.error("Error in chat method: %s", str(e), exc_info=True) - return {"Error": str(e)}, 500 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 deleted file mode 100644 index 0c916ec..0000000 --- a/submodules/moragents_dockers/agents/src/data_agent/src/data_agent_config.py +++ /dev/null @@ -1,27 +0,0 @@ -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/delegator.py b/submodules/moragents_dockers/agents/src/delegator.py index bc4d605..a0f9b5e 100644 --- a/submodules/moragents_dockers/agents/src/delegator.py +++ b/submodules/moragents_dockers/agents/src/delegator.py @@ -2,6 +2,8 @@ import logging import json +from langchain.schema import SystemMessage, HumanMessage, AIMessage + logger = logging.getLogger(__name__) # Configurable default agent @@ -9,12 +11,10 @@ class Delegator: - def __init__(self, config, llm, llm_ollama, embeddings, flask_app): - self.llm = llm - self.flask_app = flask_app - self.llm_ollama = llm_ollama - self.embeddings = embeddings + def __init__(self, config, llm, embeddings): self.config = config + self.llm = llm # This is now a ChatOllama instance + self.embeddings = embeddings self.agents = self.load_agents(config) logger.info("Delegator initialized with %d agents", len(self.agents)) @@ -27,9 +27,7 @@ def load_agents(self, config): agent_instance = agent_class( agent_info, self.llm, - self.llm_ollama, self.embeddings, - self.flask_app, ) agents[agent_info["name"]] = agent_instance logger.info("Loaded agent: %s", agent_info["name"]) @@ -43,78 +41,57 @@ def get_delegator_response(self, prompt, upload_state): for agent_info in self.config["agents"] if not (agent_info["upload_required"] and not upload_state) ] + logger.info(f"Available agents: {available_agents}") + agent_descriptions = "\n".join( f"- {agent_info['name']}: {agent_info['description']}" for agent_info in self.config["agents"] if agent_info["name"] in available_agents ) - prompt_text = ( - "### Instruction: Your name is Morpheus. " + system_prompt = ( + "Your name is Morpheus. " "Your primary function is to select the correct agent based on the user's input. " - "You MUST use the 'route' function to select an agent. " - "Available agents and their descriptions:\n" - f"{agent_descriptions}\n" + "You MUST use the 'select_agent' function to select an agent. " + f"Available agents and their descriptions: {agent_descriptions}\n" "Analyze the user's input and select the most appropriate agent. " - "Do not respond with any text other than calling the 'route' function. " - "###" ) tools = [ { - "type": "function", - "function": { - "name": "route", - "description": "Choose which agent to run next", - "parameters": { - "type": "object", - "properties": { - "next": { - "type": "string", - "enum": available_agents, - "description": "The name of the next agent to run", - } + "name": "select_agent", + "description": "Choose which agent should be used to respond to the user query", + "parameters": { + "type": "object", + "properties": { + "agent": { + "type": "string", + "enum": available_agents, + "description": "The name of the agent to be used to respond to the user query", }, - "required": ["next"], }, + "required": ["agent"], }, } ] - message_list = [ - {"role": "system", "content": prompt_text}, - prompt, - { - "role": "system", - "content": "Remember, you must use the 'route' function to select an agent.", - }, + agent_selection_llm = self.llm.bind_tools(tools) + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=prompt), ] - logger.info("Sending prompt to LLM: %s", prompt) - result = self.llm.create_chat_completion( - messages=message_list, - tools=tools, - tool_choice="auto", - temperature=0.3, - ) - logger.info("Received response from LLM: %s", result) + result = agent_selection_llm.invoke(messages) + tool_calls = result.tool_calls + if not tool_calls: + raise ValueError("No agent was selected by the model.") - response = result["choices"][0]["message"] + selected_agent = tool_calls[0] + logger.info(f"Selected agent: {selected_agent}") + selected_agent_name = selected_agent.get("args").get("agent") - if response.get("tool_calls"): - try: - function_args = json.loads( - response["tool_calls"][0]["function"]["arguments"] - ) - return {"next": function_args["next"]} - except (json.JSONDecodeError, KeyError) as e: - logger.error(f"Error parsing function call: {e}") - return {"next": DEFAULT_AGENT} - else: - logger.warning( - "No tool calls in LLM response, defaulting to general purpose agent" - ) - return {"next": DEFAULT_AGENT} + return {"agent": selected_agent_name} def delegate_chat(self, agent_name, request): logger.info(f"Attempting to delegate chat to agent: {agent_name}") diff --git a/submodules/moragents_dockers/agents/src/models/__init__.py b/submodules/moragents_dockers/agents/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/src/models/messages.py b/submodules/moragents_dockers/agents/src/models/messages.py new file mode 100644 index 0000000..e17d693 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/models/messages.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class ChatMessage(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + prompt: ChatMessage + chain_id: str + wallet_address: str diff --git a/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py b/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py deleted file mode 100644 index 0880cf0..0000000 --- a/submodules/moragents_dockers/agents/src/rag_agent/src/agent.py +++ /dev/null @@ -1,90 +0,0 @@ -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) - - -class RagAgent: - def __init__(self, config, llm, llm_ollama, embeddings,flask_app): - self.llm = llm_ollama - self.flask_app = flask_app - self.embedding=embeddings - self.config = config - self.agent = None - self.messages = [{'role': "assistant", "content": "Please upload a file to begin"}] - self.upload_state = False - self.prompt = ChatPromptTemplate.from_template( - """ - Answer the following question only based on the given context - - - {context} - - - Question: {input} - """ - ) - self.UPLOAD_FOLDER = flask_app.config['UPLOAD_FOLDER'] - self.max_size = 5 * 1024 * 1024 - self.retriever = None - - - def handle_file_upload(self,file): - if not os.path.exists(self.UPLOAD_FOLDER): - os.makedirs(self.UPLOAD_FOLDER, exist_ok=True) - filename = secure_filename(file.filename) - file.save(os.path.join(self.UPLOAD_FOLDER, filename)) - # DocumentToolsGenerator class instantiation - loader = PyMuPDFLoader(os.path.join(self.UPLOAD_FOLDER,filename)) - docs = loader.load() - text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024,chunk_overlap=20,length_function=len,is_separator_regex=False) - split_documents = text_splitter.split_documents(docs) - vector_store = FAISS.from_documents(split_documents, self.embedding) - self.retriever = vector_store.as_retriever(search_kwargs={"k": 7}) - - - def upload_file(self,request): - if 'file' not in request.files: - return {'error': 'No file part'}, 400 - file = request.files['file'] - if file.filename == '': - return {'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 > self.max_size: - return {"role": "assistant", "content": 'please use a file less than 5 MB'} - try: - self.handle_file_upload(file) - self.upload_state = True - return {"role": "assistant", "content": 'You have successfully uploaded the text'} - except Exception as e: - logging.error(f'Error during file upload: {str(e)}') - return {'error': str(e)}, 500 - - def chat(self,request): - try: - data = request.get_json() - if 'prompt' in data: - prompt = data['prompt']['content'] - role = "assistant" - retrieved_docs = self.retriever.invoke(prompt) - formatted_context = "\n\n".join(doc.page_content for doc in retrieved_docs) - formatted_prompt = f"Question: {prompt}\n\nContext: {formatted_context}" - answer=self.llm(formatted_prompt) - response = answer if self.upload_state else "please upload a file first" - return {"role": role, "content": response} - else: - return {"error": "Missing required parameters"}, 400 - except Exception as e: - logging.error(f'Error in chat endpoint: {str(e)}') - return {"Error": str(e)}, 500 diff --git a/submodules/moragents_dockers/agents/src/reward_agent/src/config.py b/submodules/moragents_dockers/agents/src/reward_agent/src/config.py deleted file mode 100644 index ff7c6d6..0000000 --- a/submodules/moragents_dockers/agents/src/reward_agent/src/config.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging - -# Logging configuration -logging.basicConfig(level=logging.INFO) - -# Configuration object -class Config: - - WEB3RPCURL = { - "1": "https://eth.llamarpc.com/", - } - - DISTRIBUTION_PROXY_ADDRESS = "0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790" - DISTRIBUTION_ABI = [ - { - "inputs": [ - { - "internalType": "uint256", - "name": "poolId_", - "type": "uint256" - }, - { - "internalType": "address", - "name": "user_", - "type": "address" - } - ], - "name": "getCurrentUserReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } - ] diff --git a/submodules/moragents_dockers/agents/src/stores/__init__.py b/submodules/moragents_dockers/agents/src/stores/__init__.py new file mode 100644 index 0000000..18cc281 --- /dev/null +++ b/submodules/moragents_dockers/agents/src/stores/__init__.py @@ -0,0 +1,5 @@ +from src.stores.agent_manager import AgentManager +from src.stores.chat_manager import ChatManager + +agent_manager = AgentManager() +chat_manager = ChatManager() diff --git a/submodules/moragents_dockers/agents/src/stores/agent_manager.py b/submodules/moragents_dockers/agents/src/stores/agent_manager.py new file mode 100644 index 0000000..82508df --- /dev/null +++ b/submodules/moragents_dockers/agents/src/stores/agent_manager.py @@ -0,0 +1,12 @@ +class AgentManager: + def __init__(self): + self.active_agent = None + + def get_active_agent(self): + return self.active_agent + + def set_active_agent(self, agent_name): + self.active_agent = agent_name + + def clear_active_agent(self): + self.active_agent = None diff --git a/submodules/moragents_dockers/agents/src/stores/chat_manager.py b/submodules/moragents_dockers/agents/src/stores/chat_manager.py new file mode 100644 index 0000000..5bd79eb --- /dev/null +++ b/submodules/moragents_dockers/agents/src/stores/chat_manager.py @@ -0,0 +1,48 @@ +import logging +from typing import List, Dict + +logger = logging.getLogger(__name__) + + +class ChatManager: + def __init__(self): + self.has_uploaded_file = False + self.messages: List[Dict[str, str]] = [ + { + "role": "assistant", + "content": """This highly experimental chatbot is not intended for making important decisions, + and its responses are generated based on incomplete data and algorithms that may evolve rapidly. + By using this chatbot, you acknowledge that you use it at your own discretion + and assume all risks associated with its limitations and potential errors.""", + } + ] + + def add_message(self, message: Dict[str, str]): + self.messages.append(message) + logger.info(f"Added message: {message}") + + def get_messages(self) -> List[Dict[str, str]]: + return self.messages + + def set_uploaded_file(self, has_file: bool): + self.has_uploaded_file = has_file + logger.info(f"Set uploaded file status to: {has_file}") + + def get_uploaded_file_status(self) -> bool: + return self.has_uploaded_file + + def clear_messages(self): + self.messages = [self.messages[0]] # Keep the initial message + logger.info("Cleared message history") + + def get_last_message(self) -> Dict[str, str]: + return self.messages[-1] if self.messages else {} + + def add_response(self, response: Dict[str, str], agent_name: str): + response_with_agent = response.copy() + response_with_agent["agentName"] = agent_name + self.add_message(response_with_agent) + logger.info(f"Added response from agent {agent_name}: {response_with_agent}") + + def get_chat_history(self) -> str: + return "\n".join([f"{msg['role']}: {msg['content']}" for msg in self.messages]) diff --git a/submodules/moragents_dockers/agents/src/swap_agent/src/agent.py b/submodules/moragents_dockers/agents/src/swap_agent/src/agent.py deleted file mode 100644 index 73f2694..0000000 --- a/submodules/moragents_dockers/agents/src/swap_agent/src/agent.py +++ /dev/null @@ -1,190 +0,0 @@ -import json -import requests -from flask import jsonify -from swap_agent.src import tools -from swap_agent.src.config import Config - - -class SwapAgent: - def __init__(self, config, llm, llm_ollama, embeddings, flask_app): - self.llm = llm - self.flask_app = flask_app - self.config = config - self.tools_provided = tools.get_tools() - self.context = [] - - def api_request_url(self, method_name, query_params, chain_id): - base_url = Config.APIBASEURL + str(chain_id) - return f"{base_url}{method_name}?{'&'.join([f'{key}={value}' for key, value in query_params.items()])}" - - def check_allowance(self, token_address, wallet_address, chain_id): - url = self.api_request_url( - "/approve/allowance", - {"tokenAddress": token_address, "walletAddress": wallet_address}, - chain_id - ) - response = requests.get(url, headers=Config.HEADERS) - data = response.json() - return data - - def approve_transaction(self, token_address, chain_id, amount=None): - query_params = {"tokenAddress": token_address, "amount": amount} if amount else {"tokenAddress": token_address} - url = self.api_request_url("/approve/transaction", query_params, chain_id) - response = requests.get(url, headers=Config.HEADERS) - transaction = response.json() - return transaction - - def build_tx_for_swap(self, swap_params, chain_id): - url = self.api_request_url("/swap", swap_params, chain_id) - swap_transaction = requests.get(url, headers=Config.HEADERS).json() - return swap_transaction - - def get_response(self, message, chain_id, wallet_address): - prompt = [ - { - "role": "system", - "content": ( - "Don't make assumptions about the value of the arguments for the function " - "they should always be supplied by the user and do not alter the value of the arguments. " - "Don't make assumptions about what values to plug into functions. Ask for clarification if a user " - "request is ambiguous. you only need the value of token1 we dont need the value of token2. After " - "starting from scratch do not assume the name of token1 or token2" - ) - } - ] - prompt.extend(message) - result = self.llm.create_chat_completion( - messages=prompt, - tools=self.tools_provided, - tool_choice="auto", - temperature=0.01 - ) - if "tool_calls" in result["choices"][0]["message"].keys(): - func = result["choices"][0]["message"]["tool_calls"][0]['function'] - if func["name"] == "swap_agent": - args = json.loads(func["arguments"]) - tok1 = args["token1"] - tok2 = args["token2"] - value = args["value"] - try: - res, role = tools.swap_coins(tok1, tok2, float(value), chain_id, wallet_address) - except (tools.InsufficientFundsError, tools.TokenNotFoundError, tools.SwapNotPossibleError) as e: - self.context = [] - return str(e), "assistant", None - return res, role, None - self.context.append({"role": "assistant", "content": result["choices"][0]["message"]['content']}) - return result["choices"][0]["message"]['content'], "assistant", "crypto swap agent" - - def get_status(self, flag, tx_hash, tx_type): - response = '' - - if flag == "cancelled": - response = f"The {tx_type} transaction has been cancelled." - elif flag == "success": - response = f"The {tx_type} transaction was successful." - elif flag == "failed": - response = f"The {tx_type} transaction has failed." - elif flag == "initiated": - response = f"Transaction has been sent, please wait for it to be confirmed." - - if tx_hash: - response = response + f" The transaction hash is {tx_hash}." - - if flag == "success" and tx_type == "approve": - response = response + " Please proceed with the swap transaction." - elif flag != "initiated": - response = response + " Is there anything else I can help you with?" - - if flag != "initiated": - self.context = [] - self.context.append({"role": "assistant", "content": response}) - self.context.append({"role": "user", "content": "okay lets start again from scratch"}) - - return {"role": "assistant", "content": response} - - def generate_response(self, prompt, chain_id, wallet_address): - self.context.append(prompt) - response, role, next_turn_agent = self.get_response(self.context, chain_id, wallet_address) - return response, role, next_turn_agent - - def chat(self, request): - try: - data = request.get_json() - if 'prompt' in data: - prompt = data['prompt'] - wallet_address = data['wallet_address'] - chain_id = data['chain_id'] - response, role, next_turn_agent = self.generate_response(prompt, chain_id, wallet_address) - return {"role": role, "content": response, "next_turn_agent": next_turn_agent} - else: - return {"error": "Missing required parameters"}, 400 - except Exception as e: - return {"Error": str(e)}, 500 - - def tx_status(self, request): - try: - data = request.get_json() - if 'status' in data: - prompt = data['status'] - tx_hash = data.get('tx_hash', '') - tx_type = data.get('tx_type', '') - response = self.get_status(prompt, tx_hash, tx_type) - return response - else: - return {"error": "Missing required parameters"}, 400 - except Exception as e: - return {"Error": str(e)}, 500 - - def get_allowance(self, request): - try: - data = request.get_json() - if 'tokenAddress' in data: - token = data['tokenAddress'] - wallet_address = data['walletAddress'] - chain_id = data["chain_id"] - res = self.check_allowance(token, wallet_address, chain_id) - return jsonify({"response": res}) - else: - return jsonify({"error": "Missing required parameters"}), 400 - except Exception as e: - return jsonify({"Error": str(e)}), 500 - - def approve(self, request): - try: - data = request.get_json() - if 'tokenAddress' in data: - token = data['tokenAddress'] - chain_id = data['chain_id'] - amount = data['amount'] - res = self.approve_transaction(token, chain_id, amount) - return jsonify({"response": res}) - else: - return jsonify({"error": "Missing required parameters"}), 400 - except Exception as e: - return jsonify({"Error": str(e)}), 500 - - def swap(self, request): - try: - data = request.get_json() - if 'src' in data: - token1 = data['src'] - token2 = data['dst'] - wallet_address = data['walletAddress'] - amount = data['amount'] - slippage = data['slippage'] - chain_id = data['chain_id'] - swap_params = { - "src": token1, - "dst": token2, - "amount": amount, - "from": wallet_address, - "slippage": slippage, - "disableEstimate": False, - "allowPartialFill": False, - } - swap_transaction = self.build_tx_for_swap(swap_params, chain_id) - return swap_transaction - else: - return jsonify({"error": "Missing required parameters"}), 400 - except Exception as e: - return jsonify({"Error": str(e)}), 500 diff --git a/submodules/moragents_dockers/agents/src/swap_agent/src/config.py b/submodules/moragents_dockers/agents/src/swap_agent/src/config.py deleted file mode 100644 index 2e57c81..0000000 --- a/submodules/moragents_dockers/agents/src/swap_agent/src/config.py +++ /dev/null @@ -1,26 +0,0 @@ -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://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"}, - {"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/src/swap_agent/src/swap_agent_config.py b/submodules/moragents_dockers/agents/src/swap_agent/src/swap_agent_config.py deleted file mode 100644 index b91a88f..0000000 --- a/submodules/moragents_dockers/agents/src/swap_agent/src/swap_agent_config.py +++ /dev/null @@ -1,25 +0,0 @@ -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/__init__.py b/submodules/moragents_dockers/agents/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/benchmarks/claim_agent_benchmarks/README.md b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/README.md similarity index 100% rename from submodules/benchmarks/claim_agent_benchmarks/README.md rename to submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/README.md diff --git a/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/__init__.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/adapters/__init__.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/benchmarks/claim_agent_benchmarks/adapters/claim_adapter.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/adapters/claim_adapter.py similarity index 100% rename from submodules/benchmarks/claim_agent_benchmarks/adapters/claim_adapter.py rename to submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/adapters/claim_adapter.py diff --git a/submodules/benchmarks/claim_agent_benchmarks/benchmarks.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/benchmarks.py similarity index 92% rename from submodules/benchmarks/claim_agent_benchmarks/benchmarks.py rename to submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/benchmarks.py index d550827..880365d 100644 --- a/submodules/benchmarks/claim_agent_benchmarks/benchmarks.py +++ b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/benchmarks.py @@ -1,6 +1,6 @@ import pytest from helpers import request_claim, provide_receiver_address, confirm_transaction -from submodules.benchmarks.claim_agent_benchmarks.config import Config +from submodules.moragents_dockers.agents.tests.claim_agent_benchmarks.config import Config def test_claim_process(): diff --git a/submodules/benchmarks/claim_agent_benchmarks/config.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/config.py similarity index 100% rename from submodules/benchmarks/claim_agent_benchmarks/config.py rename to submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/config.py diff --git a/submodules/benchmarks/claim_agent_benchmarks/helpers.py b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/helpers.py similarity index 71% rename from submodules/benchmarks/claim_agent_benchmarks/helpers.py rename to submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/helpers.py index afab9a6..a319834 100644 --- a/submodules/benchmarks/claim_agent_benchmarks/helpers.py +++ b/submodules/moragents_dockers/agents/tests/claim_agent_benchmarks/helpers.py @@ -1,5 +1,5 @@ -from submodules.benchmarks.claim_agent_benchmarks.config import Config -from submodules.benchmarks.claim_agent_benchmarks.adapters.claim_adapter import ClaimAdapter +from submodules.moragents_dockers.agents.tests.claim_agent_benchmarks.config import Config +from submodules.moragents_dockers.agents.tests.claim_agent_benchmarks.adapters.claim_adapter import ClaimAdapter claim_adapter = ClaimAdapter(Config.URL, Config.HEADERS) diff --git a/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/README.md b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/README.md new file mode 100644 index 0000000..d1c4b90 --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/README.md @@ -0,0 +1,20 @@ +# Benchmarking & Testing News Agent Guide + + +## How to Run the Tests: +1) In the parent directory: +- ```cd submodules/moragents_dockers/agents``` + +2) ```docker build -t agent .``` + +2. NOTE: If you are using Apple Silicon then you may experience problems due to the base image not being build for arm64. We have included a separate Dockerfile in order to deal with this issue, run: + +- ```docker build . -t agent -f Dockerfile-apple``` + +3) To run the agent: + +- ```docker run --name agent -p 5000:5000 agent``` + +4) Check if the agent is up and running on docker or not +5) If it is running, navigate to `submodules/news_agent_benchmarks` +6) run `pytest benchmarks.py` \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/__init__.py b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/benchmarks.py b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/benchmarks.py new file mode 100644 index 0000000..7b7df4c --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/benchmarks.py @@ -0,0 +1,38 @@ +# submodules/benchmarks/news_agent_benchmarks/benchmarks.py + +import pytest +import logging +from submodules.moragents_dockers.agents.tests.news_agent_benchmarks.config import Config +from submodules.moragents_dockers.agents.tests.news_agent_benchmarks.helpers import ask_news_agent, extract_classification + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def test_news_classification(): + for i, test_case in enumerate(Config.TEST_CASES): + article_text = test_case["article_text"] + expected_classification = test_case["expected_classification"] + + logger.info(f"Testing article classification (Test {i + 1})") + + # Ask the news agent to classify the article + response = ask_news_agent(article_text, Config.LOCAL_AGENT_URL) + + # Extract the classification from the response + classification = extract_classification(response) + + if classification == "UNKNOWN": + logger.warning(f"Test case {i + 1} resulted in UNKNOWN classification. Response: {response}") + assert False, f"Test case {i + 1} failed: Could not determine classification" + else: + # Check if the classification matches the expected classification + assert classification == expected_classification, f"Test case {i + 1} failed: Expected {expected_classification}, but got {classification}" + + logger.info(f"Test case {i + 1} passed: Correctly classified as {classification}") + + logger.info("All test cases passed successfully") + + +if __name__ == "__main__": + pytest.main() diff --git a/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/config.py b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/config.py new file mode 100644 index 0000000..2868b17 --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/config.py @@ -0,0 +1,14 @@ +class Config: + TEST_CASES = [ + { + "article_text": "ETH Prices zooms 10 percent today while Bitcoin's price surged to $70,000 today, breaking all previous records. Analysts attribute this to increased institutional adoption and positive regulatory news from several countries.", + "expected_classification": "RELEVANT", + }, + { + "article_text": "A new Tesla facility has opened in Texas, utilizing 100% renewable energy.", + "expected_classification": "NOT RELEVANT", + }, + # Add more test cases as needed + ] + + LOCAL_AGENT_URL = "http://127.0.0.1:5000/" diff --git a/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/helpers.py b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/helpers.py new file mode 100644 index 0000000..7ce2623 --- /dev/null +++ b/submodules/moragents_dockers/agents/tests/news_agent_benchmarks/helpers.py @@ -0,0 +1,48 @@ +# submodules/benchmarks/news_agent_benchmarks/helpers.py + +import requests +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def ask_news_agent(article_text: str, url: str) -> dict: + headers = {'Content-Type': 'application/json'} + payload = { + "prompt": { + "role": "user", + "content": f"Classify if this article is relevant to cryptocurrency price movements: {article_text}" + } + } + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"Request failed with status code {response.status_code}: {response.text}") + + +def extract_classification(response: dict) -> str: + if not isinstance(response, dict): + logger.warning(f"Unexpected response type: {type(response)}") + return "UNKNOWN" + + content = response.get('content') + + if content is None: + logger.warning("Response content is None") + return "UNKNOWN" + + if not isinstance(content, str): + logger.warning(f"Unexpected content type: {type(content)}") + return "UNKNOWN" + + content = content.upper() + + if "NOT RELEVANT" in content: + return "NOT RELEVANT" + elif "RELEVANT" in content: + return "RELEVANT" + else: + logger.warning(f"Could not determine relevance from content: {content}") + return "NOT RELEVANT" \ No newline at end of file diff --git a/submodules/moragents_dockers/agents/tests/price_fetching/__init__.py b/submodules/moragents_dockers/agents/tests/price_fetching/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/tests/price_fetching/adapters/__init__.py b/submodules/moragents_dockers/agents/tests/price_fetching/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/benchmarks/price_fetching/adapters/base_adapter.py b/submodules/moragents_dockers/agents/tests/price_fetching/adapters/base_adapter.py similarity index 100% rename from submodules/benchmarks/price_fetching/adapters/base_adapter.py rename to submodules/moragents_dockers/agents/tests/price_fetching/adapters/base_adapter.py diff --git a/submodules/benchmarks/price_fetching/adapters/coingecko_adapter.py b/submodules/moragents_dockers/agents/tests/price_fetching/adapters/coingecko_adapter.py similarity index 100% rename from submodules/benchmarks/price_fetching/adapters/coingecko_adapter.py rename to submodules/moragents_dockers/agents/tests/price_fetching/adapters/coingecko_adapter.py diff --git a/submodules/benchmarks/price_fetching/adapters/defillama_adapter.py b/submodules/moragents_dockers/agents/tests/price_fetching/adapters/defillama_adapter.py similarity index 100% rename from submodules/benchmarks/price_fetching/adapters/defillama_adapter.py rename to submodules/moragents_dockers/agents/tests/price_fetching/adapters/defillama_adapter.py diff --git a/submodules/benchmarks/price_fetching/benchmarks.py b/submodules/moragents_dockers/agents/tests/price_fetching/benchmarks.py similarity index 94% rename from submodules/benchmarks/price_fetching/benchmarks.py rename to submodules/moragents_dockers/agents/tests/price_fetching/benchmarks.py index 728a9b4..d376fcd 100644 --- a/submodules/benchmarks/price_fetching/benchmarks.py +++ b/submodules/moragents_dockers/agents/tests/price_fetching/benchmarks.py @@ -1,7 +1,7 @@ import time import argparse from helpers import ask_data_agent, compare_usd_values, extract_agent_usd_value -from submodules.benchmarks.price_fetching.config import coins, price_prompts, mcap_prompts, price_error_tolerance, mcap_error_tolerance, loop_delay +from submodules.moragents_dockers.agents.tests.price_fetching 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 diff --git a/submodules/benchmarks/price_fetching/config.py b/submodules/moragents_dockers/agents/tests/price_fetching/config.py similarity index 100% rename from submodules/benchmarks/price_fetching/config.py rename to submodules/moragents_dockers/agents/tests/price_fetching/config.py diff --git a/submodules/benchmarks/price_fetching/helpers.py b/submodules/moragents_dockers/agents/tests/price_fetching/helpers.py similarity index 100% rename from submodules/benchmarks/price_fetching/helpers.py rename to submodules/moragents_dockers/agents/tests/price_fetching/helpers.py diff --git a/submodules/benchmarks/price_fetching/readme.md b/submodules/moragents_dockers/agents/tests/price_fetching/readme.md similarity index 100% rename from submodules/benchmarks/price_fetching/readme.md rename to submodules/moragents_dockers/agents/tests/price_fetching/readme.md diff --git a/submodules/benchmarks/price_fetching/requirements.txt b/submodules/moragents_dockers/agents/tests/price_fetching/requirements.txt similarity index 100% rename from submodules/benchmarks/price_fetching/requirements.txt rename to submodules/moragents_dockers/agents/tests/price_fetching/requirements.txt diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/README.md b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/README.md similarity index 100% rename from submodules/benchmarks/reward_check_agent_benchmarks/README.md rename to submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/README.md diff --git a/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/__init__.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/adapters/__init__.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/adapters/reward_check_adapter.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/adapters/reward_check_adapter.py similarity index 100% rename from submodules/benchmarks/reward_check_agent_benchmarks/adapters/reward_check_adapter.py rename to submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/adapters/reward_check_adapter.py diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/benchmarks.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/benchmarks.py similarity index 93% rename from submodules/benchmarks/reward_check_agent_benchmarks/benchmarks.py rename to submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/benchmarks.py index 3e8df66..03395e7 100644 --- a/submodules/benchmarks/reward_check_agent_benchmarks/benchmarks.py +++ b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/benchmarks.py @@ -1,6 +1,6 @@ import pytest -from submodules.benchmarks.claim_agent_benchmarks.helpers import request_claim, confirm_transaction -from submodules.benchmarks.claim_agent_benchmarks.config import Config +from submodules.benchmarks.claim_agent_benchmarks.helpers import request_claim +from submodules.moragents_dockers.agents.tests.claim_agent_benchmarks.config import Config def test_claim_process(): diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/config.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/config.py similarity index 97% rename from submodules/benchmarks/reward_check_agent_benchmarks/config.py rename to submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/config.py index 9f75ab4..2522622 100644 --- a/submodules/benchmarks/reward_check_agent_benchmarks/config.py +++ b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/config.py @@ -7,7 +7,7 @@ # Configuration object class Config: WEB3RPCURL = { - "1": "https://cloudflare-eth.com", + "1": "https://eth.llamarpc.com/", } DISTRIBUTION_PROXY_ADDRESS = "0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790" diff --git a/submodules/benchmarks/reward_check_agent_benchmarks/helpers.py b/submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/helpers.py similarity index 100% rename from submodules/benchmarks/reward_check_agent_benchmarks/helpers.py rename to submodules/moragents_dockers/agents/tests/reward_check_agent_benchmarks/helpers.py diff --git a/submodules/moragents_dockers/docker-compose-apple.yml b/submodules/moragents_dockers/docker-compose-apple.yml index 9cf6833..27b8acb 100644 --- a/submodules/moragents_dockers/docker-compose-apple.yml +++ b/submodules/moragents_dockers/docker-compose-apple.yml @@ -2,7 +2,7 @@ version: "3.8" services: agents: - image: lachsbagel/moragents_dockers-agents:apple-0.1.0 + image: lachsbagel/moragents_dockers-agents:apple-0.2.0 build: dockerfile: Dockerfile-apple context: ./agents @@ -18,7 +18,7 @@ services: - BASE_URL=http://host.docker.internal:11434 nginx: - image: lachsbagel/moragents_dockers-nginx:apple-0.1.0 + image: lachsbagel/moragents_dockers-nginx:apple-0.2.0 build: context: ./frontend dockerfile: Dockerfile diff --git a/submodules/moragents_dockers/docker-compose.yml b/submodules/moragents_dockers/docker-compose.yml index 135b524..f9ad6b0 100644 --- a/submodules/moragents_dockers/docker-compose.yml +++ b/submodules/moragents_dockers/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: agents: - image: lachsbagel/moragents_dockers-agents:amd64-0.1.0 + image: lachsbagel/moragents_dockers-agents:amd64-0.2.0 build: dockerfile: Dockerfile context: ./agents @@ -18,7 +18,7 @@ services: - BASE_URL=http://host.docker.internal:11434 nginx: - image: lachsbagel/moragents_dockers-nginx:amd64-0.1.0 + image: lachsbagel/moragents_dockers-nginx:amd64-0.2.0 build: context: ./frontend dockerfile: Dockerfile diff --git a/submodules/moragents_dockers/frontend/components/Chat/index.tsx b/submodules/moragents_dockers/frontend/components/Chat/index.tsx index 31d6123..37011db 100644 --- a/submodules/moragents_dockers/frontend/components/Chat/index.tsx +++ b/submodules/moragents_dockers/frontend/components/Chat/index.tsx @@ -9,7 +9,7 @@ import { sendSwapStatus, getHttpClient, SWAP_STATUS, - CLAIM_STATUS + CLAIM_STATUS, } from "../../services/backendClient"; import { useAccount, @@ -65,7 +65,7 @@ export const Chat: FC = ({ async (status: string, hash: string, isApprove: number) => { try { const response: ChatMessage = await sendSwapStatus( - getHttpClient(selectedAgent), + getHttpClient(), chainId, address?.toLowerCase() || "0x", status, @@ -158,94 +158,93 @@ export const Chat: FC = ({ [address, handleSwapStatus, sendTransaction] ); - const handleClaimStatus = useCallback( - async (status: string, hash: string) => { - try { - const response: ChatMessage = await sendClaimStatus( - getHttpClient(selectedAgent), - chainId, - address?.toLowerCase() || "0x", - status, - hash - ); + async (status: string, hash: string) => { + try { + const response: ChatMessage = await sendClaimStatus( + getHttpClient(), + chainId, + address?.toLowerCase() || "0x", + status, + hash + ); - if ( - response.role === "assistant" && - typeof response.content === "string" - ) { - setMessagesData((prev) => [ - ...prev, - { - role: "assistant", - content: response.content, - } as UserOrAssistantMessage, - ]); - } else if (response.role === "claim") { - setMessagesData((prev) => [...prev, response as ClaimMessage]); - } + if ( + response.role === "assistant" && + typeof response.content === "string" + ) { + setMessagesData((prev) => [ + ...prev, + { + role: "assistant", + content: response.content, + } as UserOrAssistantMessage, + ]); + } else if (response.role === "claim") { + setMessagesData((prev) => [...prev, response as ClaimMessage]); + } - setTxHash(""); - setShowSpinner(false); - } catch (error) { - console.log(`Error sending claim status: ${error}`); - onBackendError(); - setTxHash(""); - setShowSpinner(false); - } - }, - [selectedAgent, chainId, address, onBackendError] -); + setTxHash(""); + setShowSpinner(false); + } catch (error) { + console.log(`Error sending claim status: ${error}`); + onBackendError(); + setTxHash(""); + setShowSpinner(false); + } + }, + [selectedAgent, chainId, address, onBackendError] + ); // Add this near your other useTransactionConfirmations hooks -const claimConfirmations = useTransactionConfirmations({ - hash: (txHash || "0x") as `0x${string}`, -}); + const claimConfirmations = useTransactionConfirmations({ + hash: (txHash || "0x") as `0x${string}`, + }); -// Add this effect to watch for claim transaction confirmations -useEffect(() => { - if (txHash && claimConfirmations.data && claimConfirmations.data >= 1) { - handleClaimStatus(CLAIM_STATUS.SUCCESS, txHash); - } -}, [txHash, claimConfirmations.data, handleClaimStatus]); + // Add this effect to watch for claim transaction confirmations + useEffect(() => { + if (txHash && claimConfirmations.data && claimConfirmations.data >= 1) { + handleClaimStatus(CLAIM_STATUS.SUCCESS, txHash); + } + }, [txHash, claimConfirmations.data, handleClaimStatus]); -// Modify handleClaimSubmit to use the same txHash state -const handleClaimSubmit = useCallback( - (claimTx: ClaimTransactionPayload) => { - setTxHash(""); - console.log("Claim transaction to be sent:", claimTx); - sendTransaction( - { - account: address, - data: claimTx.data as `0x${string}`, - to: claimTx.to as `0x${string}`, - value: BigInt(claimTx.value), - chainId: parseInt(claimTx.chainId), - }, - { - onSuccess: (hash) => { - setTxHash(hash); - handleClaimStatus(CLAIM_STATUS.INIT, hash); - }, - onError: (error) => { - console.log(`Error sending transaction: ${error}`); - handleClaimStatus(CLAIM_STATUS.FAIL, ""); + // Modify handleClaimSubmit to use the same txHash state + const handleClaimSubmit = useCallback( + (claimTx: ClaimTransactionPayload) => { + setTxHash(""); + console.log("Claim transaction to be sent:", claimTx); + sendTransaction( + { + account: address, + data: claimTx.data as `0x${string}`, + to: claimTx.to as `0x${string}`, + value: BigInt(claimTx.value), + chainId: parseInt(claimTx.chainId), }, - } - ); - }, - [address, handleClaimStatus, sendTransaction] -); + { + onSuccess: (hash) => { + setTxHash(hash); + handleClaimStatus(CLAIM_STATUS.INIT, hash); + }, + onError: (error) => { + console.log(`Error sending transaction: ${error}`); + handleClaimStatus(CLAIM_STATUS.FAIL, ""); + }, + } + ); + }, + [address, handleClaimStatus, sendTransaction] + ); return ( + messages={messagesData} + selectedAgent={selectedAgent} + onCancelSwap={onCancelSwap} + onSwapSubmit={handleSwapSubmit} + onClaimSubmit={handleClaimSubmit} + /> {showSpinner && } { onAgentChanged(agent: string): void; @@ -11,6 +16,18 @@ export interface HeaderBarProps extends ComponentPropsWithoutRef<"div"> { } export const HeaderBar: FC = (props) => { + const backendClient = getHttpClient(); + const router = useRouter(); + + const handleClearChatHistory = async () => { + try { + await clearMessagesHistory(backendClient); + router.reload(); + } catch (error) { + console.error("Failed to clear chat history:", error); + } + }; + return ( @@ -19,6 +36,9 @@ export const HeaderBar: FC = (props) => { + diff --git a/submodules/moragents_dockers/frontend/components/SwapForm/index.tsx b/submodules/moragents_dockers/frontend/components/SwapForm/index.tsx index 5b323d8..1d44164 100644 --- a/submodules/moragents_dockers/frontend/components/SwapForm/index.tsx +++ b/submodules/moragents_dockers/frontend/components/SwapForm/index.tsx @@ -151,7 +151,7 @@ export const SwapForm: FC = ({ } const _payload = await getApprovalTxPayload( - getHttpClient(selectedAgent), + getHttpClient(), chainId, src_address, Number(src_amount), @@ -166,14 +166,7 @@ export const SwapForm: FC = ({ setIsButtonLoading(false); } }, - [ - onSubmitApprove, - isNativeToken, - decimals?.data, - fromMessage, - chainId, - selectedAgent, - ] + [onSubmitApprove, isNativeToken, decimals?.data, fromMessage, chainId] ); const handleSwap = useCallback( @@ -182,7 +175,7 @@ export const SwapForm: FC = ({ try { const _payload = await getSwapTxPayload( - getHttpClient(selectedAgent), + getHttpClient(), fromMessage.src_address, fromMessage.dst_address, address, @@ -202,14 +195,7 @@ export const SwapForm: FC = ({ setIsButtonLoading(false); } }, - [ - fromMessage, - chainId, - selectedAgent, - formData.slippage, - onSubmitSwap, - decimals, - ] + [fromMessage, chainId, formData.slippage, onSubmitSwap, decimals] ); useEffect(() => { diff --git a/submodules/moragents_dockers/frontend/components/Tweet/index.tsx b/submodules/moragents_dockers/frontend/components/Tweet/index.tsx index 4563293..1deab63 100644 --- a/submodules/moragents_dockers/frontend/components/Tweet/index.tsx +++ b/submodules/moragents_dockers/frontend/components/Tweet/index.tsx @@ -54,7 +54,7 @@ export const Tweet: FC = ({ initialContent, selectedAgent }) => { const handleTweet = async () => { setIsTweeting(true); - const backendClient = getHttpClient(selectedAgent); + const backendClient = getHttpClient(); try { await postTweet(backendClient, tweetContent); @@ -72,7 +72,7 @@ export const Tweet: FC = ({ initialContent, selectedAgent }) => { const handleRegenerate = async () => { setIsRegenerating(true); - const backendClient = getHttpClient(selectedAgent); + const backendClient = getHttpClient(); try { const newTweet = await regenerateTweet(backendClient); diff --git a/submodules/moragents_dockers/frontend/config.ts b/submodules/moragents_dockers/frontend/config.ts index 0f4d11a..647d724 100644 --- a/submodules/moragents_dockers/frontend/config.ts +++ b/submodules/moragents_dockers/frontend/config.ts @@ -1,25 +1,25 @@ -export const routerAddress = '0x111111125421cA6dc452d289314280a0f8842A65'; -export const oneInchNativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +export const routerAddress = "0x111111125421cA6dc452d289314280a0f8842A65"; +export const oneInchNativeToken = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; export const availableAgents: { - [key: string]: { - name: string, - description: string, - endpoint: string, - requirements: { - connectedWallet: boolean - }, - supportsFiles?: boolean - - } + [key: string]: { + name: string; + description: string; + endpoint: string; + requirements: { + connectedWallet: boolean; + }; + supportsFiles?: boolean; + }; } = { - 'swap-agent': { - 'name': 'Morpheus', - 'description': 'performs multiple tasks crypto data agent,swap agent and rag agent', - 'endpoint': 'http://127.0.0.1:8080', - requirements: { - connectedWallet: true - }, - supportsFiles: true - } -} \ No newline at end of file + "swap-agent": { + name: "Morpheus", + description: + "performs multiple tasks crypto data agent,swap agent and rag agent", + endpoint: "http://127.0.0.1:8080", + requirements: { + connectedWallet: true, + }, + supportsFiles: true, + }, +}; diff --git a/submodules/moragents_dockers/frontend/package-lock.json b/submodules/moragents_dockers/frontend/package-lock.json index 217bdae..0529c6b 100644 --- a/submodules/moragents_dockers/frontend/package-lock.json +++ b/submodules/moragents_dockers/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "Morpheus AI", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Morpheus AI", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@chakra-ui/icons": "^2.1.1", @@ -6482,6 +6482,7 @@ "version": "18.19.49", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.49.tgz", "integrity": "sha512-ALCeIR6n0nQ7j0FUF1ycOhrp6+XutJWqEu/vtdEqXFUQwkBfgUA5cEg3ZNmjWGF/ZYA/FcF9QMkL55Ar0O6UrA==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -6500,6 +6501,7 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -18291,6 +18293,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/submodules/moragents_dockers/frontend/package.json b/submodules/moragents_dockers/frontend/package.json index b7ecc2c..eca1a8c 100644 --- a/submodules/moragents_dockers/frontend/package.json +++ b/submodules/moragents_dockers/frontend/package.json @@ -1,7 +1,7 @@ { "name": "Morpheus AI", "private": true, - "version": "0.1.0", + "version": "0.2.0", "scripts": { "dev": "next dev", "build": "next build", @@ -21,9 +21,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.3.0", + "react-markdown": "^9.0.0", "viem": "^2.8.12", - "wagmi": "^2.5.11", - "react-markdown": "^9.0.0" + "wagmi": "^2.5.11" }, "devDependencies": { "@swc/cli": "^0.3.12", diff --git a/submodules/moragents_dockers/frontend/pages/index.tsx b/submodules/moragents_dockers/frontend/pages/index.tsx index 8b0d192..d156de9 100644 --- a/submodules/moragents_dockers/frontend/pages/index.tsx +++ b/submodules/moragents_dockers/frontend/pages/index.tsx @@ -1,33 +1,41 @@ -import { ConnectButton } from '@rainbow-me/rainbowkit'; -import type { NextPage } from 'next'; -import { Flex, Grid, GridItem } from '@chakra-ui/react'; -import { LeftSidebar } from '../components/LeftSidebar'; -import { Chat } from '../components/Chat'; -import { writeMessage, getHttpClient, ChatMessage, getMessagesHistory, sendSwapStatus, SWAP_STATUS, uploadFile } from '../services/backendClient'; -import { useEffect, useMemo, useState } from 'react'; -import { useAccount, useChainId, useWalletClient } from 'wagmi'; -import { HeaderBar } from '../components/HeaderBar'; -import { availableAgents } from '../config'; -import { WalletRequiredModal } from '../components/WalletRequiredModal'; -import { ErrorBackendModal } from '../components/ErrorBackendModal'; +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import type { NextPage } from "next"; +import { Flex, Grid, GridItem } from "@chakra-ui/react"; +import { LeftSidebar } from "../components/LeftSidebar"; +import { Chat } from "../components/Chat"; +import { + writeMessage, + getHttpClient, + ChatMessage, + getMessagesHistory, + sendSwapStatus, + SWAP_STATUS, + uploadFile, +} from "../services/backendClient"; +import { useEffect, useMemo, useState } from "react"; +import { useAccount, useChainId, useWalletClient } from "wagmi"; +import { HeaderBar } from "../components/HeaderBar"; +import { availableAgents } from "../config"; +import { WalletRequiredModal } from "../components/WalletRequiredModal"; +import { ErrorBackendModal } from "../components/ErrorBackendModal"; const Home: NextPage = () => { const [chatHistory, setChatHistory] = useState([]); const chainId = useChainId(); const { address } = useAccount(); - const [selectedAgent, setSelectedAgent] = useState('swap-agent'); // default is swap agent for now. + const [selectedAgent, setSelectedAgent] = useState("swap-agent"); // default is swap agent for now. const [showBackendError, setShowBackendError] = useState(false); useEffect(() => { - getMessagesHistory( - getHttpClient(selectedAgent), - ).then((messages: ChatMessage[]) => { - setChatHistory([...messages]) - }).catch((e) => { - console.error(`Failed to get initial messages history. Error: ${e}`); - setShowBackendError(true); - }); - }, [selectedAgent]); + getMessagesHistory(getHttpClient()) + .then((messages: ChatMessage[]) => { + setChatHistory([...messages]); + }) + .catch((e) => { + console.error(`Failed to get initial messages history. Error: ${e}`); + setShowBackendError(true); + }); + }, []); // Empty dependency array to run only on component initialization const isWalletRequired = useMemo(() => { const agent = availableAgents[selectedAgent] || null; @@ -40,34 +48,34 @@ const Home: NextPage = () => { }, [selectedAgent]); return ( -
+
{ setSelectedAgent(agent); }} - currentAgent={selectedAgent || ''} + currentAgent={selectedAgent || ""} /> - - + - - + { // 0 is swap, 1 is approve @@ -76,29 +84,36 @@ const Home: NextPage = () => { } try { - await sendSwapStatus(getHttpClient(selectedAgent), chainId, address, SWAP_STATUS.CANCELLED, '', fromAction); + await sendSwapStatus( + getHttpClient(), + chainId, + address, + SWAP_STATUS.CANCELLED, + "", + fromAction + ); } catch (e) { console.error(`Failed to cancel swap . Error: ${e}`); setShowBackendError(true); - } finally { try { const _updatedMessages = await getMessagesHistory( - getHttpClient(selectedAgent), - ) + getHttpClient() + ); setChatHistory([..._updatedMessages]); } catch (e) { - console.error(`Failed to get messages history after send swap status. Error: ${e}`); + console.error( + `Failed to get messages history after send swap status. Error: ${e}` + ); setShowBackendError(true); } } - - - }} - onSubmitMessage={async (message: string, file: File | null): Promise => { - + onSubmitMessage={async ( + message: string, + file: File | null + ): Promise => { const agent = availableAgents[selectedAgent] || null; if (null !== agent && agent.requirements.connectedWallet) { @@ -107,26 +122,29 @@ const Home: NextPage = () => { } } - setChatHistory([...chatHistory, { - role: 'user', - content: message - } as ChatMessage]); + setChatHistory([ + ...chatHistory, + { + role: "user", + content: message, + } as ChatMessage, + ]); let _newHistory = []; try { if (!file) { - _newHistory = await writeMessage(chatHistory, message, getHttpClient(selectedAgent), chainId, address || ''); - + _newHistory = await writeMessage( + chatHistory, + message, + getHttpClient(), + chainId, + address || "" + ); } else { - await uploadFile( - getHttpClient(selectedAgent), - file, - ) - - _newHistory = await getMessagesHistory( - getHttpClient(selectedAgent), - ) + await uploadFile(getHttpClient(), file); + + _newHistory = await getMessagesHistory(getHttpClient()); } - setChatHistory([..._newHistory]) + setChatHistory([..._newHistory]); } catch (e) { console.error(`Failed to send message. Error: ${e}`); setShowBackendError(true); @@ -142,7 +160,6 @@ const Home: NextPage = () => { {/* */} - {/* { return; } - const _newHistory = await writeMessage(chatHistory, message, getHttpClient(selectedAgent), chainId, address); + const _newHistory = await writeMessage(chatHistory, message, getHttpClient(), chainId, address); setChatHistory([..._newHistory]) }} /> */} -
diff --git a/submodules/moragents_dockers/frontend/services/backendClient.ts b/submodules/moragents_dockers/frontend/services/backendClient.ts index e1d936e..84a03e9 100644 --- a/submodules/moragents_dockers/frontend/services/backendClient.ts +++ b/submodules/moragents_dockers/frontend/services/backendClient.ts @@ -1,11 +1,9 @@ import axios, { Axios } from "axios"; -import { availableAgents } from "../config"; export type ChatMessageBase = { role: "user" | "assistant" | "swap" | "claim"; }; - export type UserOrAssistantMessage = ChatMessageBase & { role: "user" | "assistant"; content: string; @@ -90,25 +88,20 @@ export type ClaimMessage = ChatMessageBase & { }; // Update the ChatMessage type to include ClaimMessage -export type ChatMessage = UserOrAssistantMessage | SwapMessage | SystemMessage | ClaimMessage; +export type ChatMessage = + | UserOrAssistantMessage + | SwapMessage + | SystemMessage + | ClaimMessage; export type ChatsListItem = { index: number; // index at chats array title: string; // title of the chat (first message content) }; -export const getHttpClient = (selectedAgent: string) => { - const agentData = availableAgents[selectedAgent]; - - if (typeof agentData === "undefined") { - // if no agent selected lets select by default swap agent for now. - } - - const swapAgentUrl = - agentData?.endpoint || availableAgents["swap-agent"].endpoint; - +export const getHttpClient = () => { return axios.create({ - baseURL: swapAgentUrl || "http://localhost:8080", + baseURL: "http://localhost:8080", }); }; @@ -155,7 +148,7 @@ export const getApprovalTxPayload = async ( export const uploadFile = async (backendClient: Axios, file: File) => { const formData = new FormData(); formData.append("file", file); - + console.log("Uploading file:", file); return await backendClient.post("/upload", formData, { headers: { "Content-Type": "multipart/form-data", @@ -220,6 +213,18 @@ export const getMessagesHistory = async ( } as ChatMessage; }); }; + +export const clearMessagesHistory = async ( + backendClient: Axios +): Promise => { + try { + await backendClient.get("/clear_messages"); + } catch (error) { + console.error("Failed to clear message history:", error); + throw error; + } +}; + export const writeMessage = async ( history: ChatMessage[], message: string, @@ -235,7 +240,7 @@ export const writeMessage = async ( history.push(newMessage); let resp; try { - resp = await backendClient.post("/", { + resp = await backendClient.post("/chat", { prompt: { role: "user", content: message, @@ -338,4 +343,4 @@ export const sendClaimStatus = async ( role: responseBody.data.role, content: responseBody.data.content, } as ChatMessage; -}; \ No newline at end of file +}; diff --git a/wizard_windows.iss b/wizard_windows.iss index 34d0565..0fdcf7b 100644 --- a/wizard_windows.iss +++ b/wizard_windows.iss @@ -1,6 +1,6 @@ [Setup] AppName=MORagents -AppVersion=0.1.0 +AppVersion=0.2.0 DefaultDirName={commonpf}\MORagents OutputDir=.\MORagentsWindowsInstaller OutputBaseFilename=MORagentsSetup @@ -27,7 +27,7 @@ Filename: "{tmp}\DockerDesktopInstaller.exe"; Parameters: "install"; StatusMsg: 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.1"; StatusMsg: "Pulling llama3.1 model..."; Flags: runhidden waituntilterminated +Filename: "cmd.exe"; Parameters: "/c ollama pull llama3.2:3b"; StatusMsg: "Pulling llama3.2:3b model..."; Flags: runhidden waituntilterminated Filename: "cmd.exe"; Parameters: "/c ollama pull nomic-embed-text"; StatusMsg: "Pulling nomic-embed-text model..."; Flags: runhidden waituntilterminated [Code]