diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..f3b2e2db --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,23 @@ +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT=3-bullseye +FROM python:3.8 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 + && apt-get purge -y imagemagick imagemagick-6-common + +# Temporary: Upgrade python packages due to https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-40897 +# They are installed by the base image (python) which does not have the patch. +RUN python3 -m pip install --upgrade setuptools + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..5fefd9c1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/python:1": "none", + "ghcr.io/devcontainers/features/node:1": "none", + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + } + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python" + } + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.env.template b/.env.template index 479730e8..474b2727 100644 --- a/.env.template +++ b/.env.template @@ -1,17 +1,111 @@ -PINECONE_API_KEY=your-pinecone-api-key -PINECONE_ENV=your-pinecone-region +################################################################################ +### AUTO-GPT - GENERAL SETTINGS +################################################################################ +# EXECUTE_LOCAL_COMMANDS - Allow local command execution (Example: False) +EXECUTE_LOCAL_COMMANDS=False + +################################################################################ +### LLM PROVIDER +################################################################################ + +### OPENAI +# OPENAI_API_KEY - OpenAI API Key (Example: my-openai-api-key) +# TEMPERATURE - Sets temperature in OpenAI (Default: 1) +# USE_AZURE - Use Azure OpenAI or not (Default: False) OPENAI_API_KEY=your-openai-api-key TEMPERATURE=1 -ELEVENLABS_API_KEY=your-elevenlabs-api-key -ELEVENLABS_VOICE_1_ID=your-voice-id -ELEVENLABS_VOICE_2_ID=your-voice-id +USE_AZURE=False + +### AZURE +# OPENAI_AZURE_API_BASE - OpenAI API base URL for Azure (Example: https://my-azure-openai-url.com) +# OPENAI_AZURE_API_VERSION - OpenAI API version for Azure (Example: v1) +# OPENAI_AZURE_DEPLOYMENT_ID - OpenAI deployment ID for Azure (Example: my-deployment-id) +# OPENAI_AZURE_CHAT_DEPLOYMENT_ID - OpenAI deployment ID for Azure Chat (Example: my-deployment-id-for-azure-chat) +# OPENAI_AZURE_EMBEDDINGS_DEPLOYMENT_ID - OpenAI deployment ID for Embedding (Example: my-deployment-id-for-azure-embeddigs) +OPENAI_AZURE_API_BASE=your-base-url-for-azure +OPENAI_AZURE_API_VERSION=api-version-for-azure +OPENAI_AZURE_DEPLOYMENT_ID=deployment-id-for-azure +OPENAI_AZURE_CHAT_DEPLOYMENT_ID=deployment-id-for-azure-chat +OPENAI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=deployment-id-for-azure-embeddigs + +################################################################################ +### LLM MODELS +################################################################################ + +# SMART_LLM_MODEL - Smart language model (Default: gpt-4) +# FAST_LLM_MODEL - Fast language model (Default: gpt-3.5-turbo) SMART_LLM_MODEL=gpt-4 FAST_LLM_MODEL=gpt-3.5-turbo -GOOGLE_API_KEY= -CUSTOM_SEARCH_ENGINE_ID= -USE_AZURE=False -EXECUTE_LOCAL_COMMANDS=False -IMAGE_PROVIDER=dalle -HUGGINGFACE_API_TOKEN= -USE_MAC_OS_TTS=False + +### LLM MODEL SETTINGS +# FAST_TOKEN_LIMIT - Fast token limit for OpenAI (Default: 4000) +# SMART_TOKEN_LIMIT - Smart token limit for OpenAI (Default: 8000) +# When using --gpt3onlythis needs to be set to 4000. +FAST_TOKEN_LIMIT=4000 +SMART_TOKEN_LIMIT=8000 + +################################################################################ +### MEMORY +################################################################################ + +# MEMORY_BACKEND - Memory backend type (Default: local) MEMORY_BACKEND=local + +### PINECONE +# PINECONE_API_KEY - Pinecone API Key (Example: my-pinecone-api-key) +# PINECONE_ENV - Pinecone environment (region) (Example: us-west-2) +PINECONE_API_KEY=your-pinecone-api-key +PINECONE_ENV=your-pinecone-region + +### REDIS +# REDIS_HOST - Redis host (Default: localhost) +# REDIS_PORT - Redis port (Default: 6379) +# REDIS_PASSWORD - Redis password (Default: "") +# WIPE_REDIS_ON_START - Wipes data / index on start (Default: False) +# MEMORY_INDEX - Name of index created in Redis database (Default: auto-gpt) +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +WIPE_REDIS_ON_START=False +MEMORY_INDEX=auto-gpt + +################################################################################ +### IMAGE GENERATION PROVIDER +################################################################################ + +### OPEN AI +# IMAGE_PROVIDER - Image provider (Example: dalle) +IMAGE_PROVIDER=dalle + +### HUGGINGFACE +# STABLE DIFFUSION +# (Default URL: https://api-inference.huggingface.co/models/CompVis/stable-diffusion-v1-4) +# Set in image_gen.py) +# HUGGINGFACE_API_TOKEN - HuggingFace API token (Example: my-huggingface-api-token) +HUGGINGFACE_API_TOKEN=your-huggingface-api-token + +################################################################################ +### SEARCH PROVIDER +################################################################################ + +### GOOGLE +# GOOGLE_API_KEY - Google API key (Example: my-google-api-key) +# CUSTOM_SEARCH_ENGINE_ID - Custom search engine ID (Example: my-custom-search-engine-id) +GOOGLE_API_KEY=your-google-api-key +CUSTOM_SEARCH_ENGINE_ID=your-custom-search-engine-id + +################################################################################ +### TTS PROVIDER +################################################################################ + +### MAC OS +# USE_MAC_OS_TTS - Use Mac OS TTS or not (Default: False) +USE_MAC_OS_TTS=False + +### ELEVENLABS +# ELEVENLABS_API_KEY - Eleven Labs API key (Example: my-elevenlabs-api-key) +# ELEVENLABS_VOICE_1_ID - Eleven Labs voice 1 ID (Example: my-voice-id-1) +# ELEVENLABS_VOICE_2_ID - Eleven Labs voice 2 ID (Example: my-voice-id-2) +ELEVENLABS_API_KEY=your-elevenlabs-api-key +ELEVENLABS_VOICE_1_ID=your-voice-id-1 +ELEVENLABS_VOICE_2_ID=your-voice-id-2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 070df794..0b90b55d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Lint with flake8 continue-on-error: false - run: flake8 scripts/ tests/ --select E303,W293,W291,W292,E305 + run: flake8 scripts/ tests/ --select E303,W293,W291,W292,E305,E231,E302 - name: Run unittest tests with coverage run: | diff --git a/Dockerfile b/Dockerfile index 4d264c88..e776664e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,23 @@ +# Use an official Python base image from the Docker Hub FROM python:3.11-slim -ENV PIP_NO_CACHE_DIR=yes -WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY scripts/ . -ENTRYPOINT ["python", "main.py"] + +# Set environment variables +ENV PIP_NO_CACHE_DIR=yes \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +# Create a non-root user and set permissions +RUN useradd --create-home appuser +WORKDIR /home/appuser +RUN chown appuser:appuser /home/appuser +USER appuser + +# Copy the requirements.txt file and install the requirements +COPY --chown=appuser:appuser requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# Copy the application files +COPY --chown=appuser:appuser scripts/ . + +# Set the entrypoint +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index 90706cbf..50dcf1b4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Auto-GPT: An Autonomous GPT-4 Experiment ![GitHub Repo stars](https://img.shields.io/github/stars/Torantulino/auto-gpt?style=social) -![Twitter Follow](https://img.shields.io/twitter/follow/siggravitas?style=social) -[![Discord Follow](https://dcbadge.vercel.app/api/server/PQ7VX6TY4t?style=flat)](https://discord.gg/PQ7VX6TY4t) +[![Twitter Follow](https://img.shields.io/twitter/follow/siggravitas?style=social)](https://twitter.com/SigGravitas) +[![Discord Follow](https://dcbadge.vercel.app/api/server/autogpt?style=flat)](https://discord.gg/autogpt) +[![Unit Tests](https://github.com/Torantulino/Auto-GPT/actions/workflows/ci.yml/badge.svg)](https://github.com/Torantulino/Auto-GPT/actions/workflows/ci.yml) Auto-GPT is an experimental open-source application showcasing the capabilities of the GPT-4 language model. This program, driven by GPT-4, chains together LLM "thoughts", to autonomously achieve whatever goal you set. As one of the first examples of GPT-4 running fully autonomously, Auto-GPT pushes the boundaries of what is possible with AI. @@ -31,21 +32,28 @@ Your support is greatly appreciated - [Auto-GPT: An Autonomous GPT-4 Experiment](#auto-gpt-an-autonomous-gpt-4-experiment) - [Demo (30/03/2023):](#demo-30032023) - - [💖 Help Fund Auto-GPT's Development](#-help-fund-auto-gpts-development) - [Table of Contents](#table-of-contents) - [🚀 Features](#-features) - [📋 Requirements](#-requirements) - [💾 Installation](#-installation) - [🔧 Usage](#-usage) + - [Logs](#logs) - [🗣️ Speech Mode](#️-speech-mode) - [🔍 Google API Keys Configuration](#-google-api-keys-configuration) - [Setting up environment variables](#setting-up-environment-variables) + - [Redis Setup](#redis-setup) + - [🌲 Pinecone API Key Setup](#-pinecone-api-key-setup) + - [Setting up environment variables](#setting-up-environment-variables-1) + - [Setting Your Cache Type](#setting-your-cache-type) + - [View Memory Usage](#view-memory-usage) - [💀 Continuous Mode ⚠️](#-continuous-mode-️) - [GPT3.5 ONLY Mode](#gpt35-only-mode) - - [🖼 Image Generation](#image-generation) + - [🖼 Image Generation](#-image-generation) - [⚠️ Limitations](#️-limitations) - [🛡 Disclaimer](#-disclaimer) - [🐦 Connect with Us on Twitter](#-connect-with-us-on-twitter) + - [Run tests](#run-tests) + - [Run linter](#run-linter) ## 🚀 Features @@ -57,7 +65,9 @@ Your support is greatly appreciated ## 📋 Requirements -- [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) +- environments(just choose one) + - [vscode + devcontainer](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): It has been configured in the .devcontainer folder and can be used directly + - [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) - [OpenAI API key](https://platform.openai.com/account/api-keys) @@ -70,32 +80,32 @@ Optional: To install Auto-GPT, follow these steps: -0. Make sure you have all the **requirements** above, if not, install/get them. +1. Make sure you have all the **requirements** above, if not, install/get them. _The following commands should be executed in a CMD, Bash or Powershell window. To do this, go to a folder on your computer, click in the folder path at the top and type CMD, then press enter._ -1. Clone the repository: +2. Clone the repository: For this step you need Git installed, but you can just download the zip file instead by clicking the button at the top of this page ☝️ ``` git clone https://github.com/Torantulino/Auto-GPT.git ``` -2. Navigate to the project directory: +3. Navigate to the project directory: _(Type this into your CMD window, you're aiming to navigate the CMD window to the repository you just downloaded)_ ``` cd 'Auto-GPT' ``` -3. Install the required dependencies: +4. Install the required dependencies: _(Again, type this into your CMD window)_ ``` pip install -r requirements.txt ``` -4. Rename `.env.template` to `.env` and fill in your `OPENAI_API_KEY`. If you plan to use Speech Mode, fill in your `ELEVEN_LABS_API_KEY` as well. +5. Rename `.env.template` to `.env` and fill in your `OPENAI_API_KEY`. If you plan to use Speech Mode, fill in your `ELEVEN_LABS_API_KEY` as well. - Obtain your OpenAI API key from: https://platform.openai.com/account/api-keys. - Obtain your ElevenLabs API key from: https://elevenlabs.io. You can view your xi-api-key using the "Profile" tab on the website. - If you want to use GPT on an Azure instance, set `USE_AZURE` to `True` and then: @@ -147,9 +157,10 @@ To use the `google_official_search` command, you need to set up your Google API 4. Go to the [APIs & Services Dashboard](https://console.cloud.google.com/apis/dashboard) and click "Enable APIs and Services". Search for "Custom Search API" and click on it, then click "Enable". 5. Go to the [Credentials](https://console.cloud.google.com/apis/credentials) page and click "Create Credentials". Choose "API Key". 6. Copy the API key and set it as an environment variable named `GOOGLE_API_KEY` on your machine. See setting up environment variables below. -7. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page and click "Add". -8. Set up your search engine by following the prompts. You can choose to search the entire web or specific sites. -9. Once you've created your search engine, click on "Control Panel" and then "Basics". Copy the "Search engine ID" and set it as an environment variable named `CUSTOM_SEARCH_ENGINE_ID` on your machine. See setting up environment variables below. +7. [Enable](https://console.developers.google.com/apis/api/customsearch.googleapis.com) the Custom Search API on your project. (Might need to wait few minutes to propagate) +8. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page and click "Add". +9. Set up your search engine by following the prompts. You can choose to search the entire web or specific sites. +10. Once you've created your search engine, click on "Control Panel" and then "Basics". Copy the "Search engine ID" and set it as an environment variable named `CUSTOM_SEARCH_ENGINE_ID` on your machine. See setting up environment variables below. _Remember that your free daily custom search quota allows only up to 100 searches. To increase this limit, you need to assign a billing account to the project to profit from up to 10K daily searches._ @@ -241,7 +252,6 @@ export PINECONE_ENV="Your pinecone region" # something like: us-east4-gcp export MEMORY_BACKEND="pinecone" ``` - ## Setting Your Cache Type By default Auto-GPT is going to use LocalCache instead of redis or Pinecone. @@ -345,11 +355,13 @@ coverage run -m unittest discover tests ## Run linter -This project uses [flake8](https://flake8.pycqa.org/en/latest/) for linting. To run the linter, run the following command: +This project uses [flake8](https://flake8.pycqa.org/en/latest/) for linting. We currently use the following rules: `E303,W293,W291,W292,E305,E231,E302`. See the [flake8 rules](https://www.flake8rules.com/) for more information. + +To run the linter, run the following command: ``` flake8 scripts/ tests/ # Or, if you want to run flake8 with the same configuration as the CI: -flake8 scripts/ tests/ --select E303,W293,W291,W292,E305 +flake8 scripts/ tests/ --select E303,W293,W291,W292,E305,E231,E302 ``` diff --git a/azure.yaml.template b/azure.yaml.template index 852645ca..74ca797b 100644 --- a/azure.yaml.template +++ b/azure.yaml.template @@ -1,3 +1,4 @@ +azure_api_type: azure_ad azure_api_base: your-base-url-for-azure azure_api_version: api-version-for-azure azure_model_map: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..af086f05 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +# To boot the app run the following: +# docker-compose run auto-gpt +version: "3.9" + +services: + auto-gpt: + depends_on: + - redis + build: ./ + volumes: + - "./scripts:/app" + - ".env:/app/.env" + profiles: ["exclude-from-up"] + + redis: + image: "redis/redis-stack-server:latest" diff --git a/requirements.txt b/requirements.txt index b864c1d3..3f7fd228 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ redis orjson Pillow coverage -flake8 \ No newline at end of file +flake8 +numpy diff --git a/scripts/agent_manager.py b/scripts/agent_manager.py index a0e5f164..191ab838 100644 --- a/scripts/agent_manager.py +++ b/scripts/agent_manager.py @@ -6,6 +6,7 @@ agents = {} # key, (task, full_message_history, model) # Create new GPT agent # TODO: Centralise use of create_chat_completion() to globally enforce token limit + def create_agent(task, prompt, model): """Create a new agent and return its key""" global next_key diff --git a/scripts/ai_config.py b/scripts/ai_config.py index bd373944..89a4e07e 100644 --- a/scripts/ai_config.py +++ b/scripts/ai_config.py @@ -1,6 +1,7 @@ import yaml -import data import os +from prompt import get_prompt + class AIConfig: """ @@ -46,7 +47,7 @@ class AIConfig: """ try: - with open(config_file) as file: + with open(config_file, encoding='utf-8') as file: config_params = yaml.load(file, Loader=yaml.FullLoader) except FileNotFoundError: config_params = {} @@ -69,8 +70,8 @@ class AIConfig: """ config = {"ai_name": self.ai_name, "ai_role": self.ai_role, "ai_goals": self.ai_goals} - with open(config_file, "w") as file: - yaml.dump(config, file) + with open(config_file, "w", encoding='utf-8') as file: + yaml.dump(config, file, allow_unicode=True) def construct_full_prompt(self) -> str: """ @@ -90,5 +91,5 @@ class AIConfig: for i, goal in enumerate(self.ai_goals): full_prompt += f"{i+1}. {goal}\n" - full_prompt += f"\n\n{data.load_prompt()}" + full_prompt += f"\n\n{get_prompt()}" return full_prompt diff --git a/scripts/ai_functions.py b/scripts/ai_functions.py index 8c95c0f2..f4ee79cd 100644 --- a/scripts/ai_functions.py +++ b/scripts/ai_functions.py @@ -1,8 +1,7 @@ -from typing import List, Optional +from typing import List import json from config import Config from call_ai_function import call_ai_function -from json_parser import fix_and_parse_json cfg = Config() diff --git a/scripts/call_ai_function.py b/scripts/call_ai_function.py index f8238658..940eacfe 100644 --- a/scripts/call_ai_function.py +++ b/scripts/call_ai_function.py @@ -3,6 +3,8 @@ from config import Config cfg = Config() from llm_utils import create_chat_completion + + # This is a magic function that can do anything with no-code. See # https://github.com/Torantulino/AI-Functions for more info. def call_ai_function(function, args, description, model=None): @@ -11,7 +13,7 @@ def call_ai_function(function, args, description, model=None): model = cfg.smart_llm_model # For each arg, if any are None, convert to "None": args = [str(arg) if arg is not None else "None" for arg in args] - # parse args to comma seperated string + # parse args to comma separated string args = ", ".join(args) messages = [ { diff --git a/scripts/chat.py b/scripts/chat.py index e16cee38..5392e438 100644 --- a/scripts/chat.py +++ b/scripts/chat.py @@ -9,6 +9,7 @@ import logging cfg = Config() + def create_chat_message(role, content): """ Create a chat message with the given role and content. @@ -69,7 +70,7 @@ def chat_with_ai( logger.debug(f"Token limit: {token_limit}") send_token_limit = token_limit - 1000 - relevant_memory = permanent_memory.get_relevant(str(full_message_history[-9:]), 10) + relevant_memory = '' if len(full_message_history) ==0 else permanent_memory.get_relevant(str(full_message_history[-9:]), 10) logger.debug(f'Memory Stats: {permanent_memory.get_stats()}') diff --git a/scripts/commands.py b/scripts/commands.py index 3966e86a..fe6f6c30 100644 --- a/scripts/commands.py +++ b/scripts/commands.py @@ -24,6 +24,7 @@ def is_valid_int(value): except ValueError: return False + def get_command(response): """Parse the response and return the command name and arguments""" try: @@ -135,6 +136,7 @@ def google_search(query, num_results=8): return json.dumps(search_results, ensure_ascii=False, indent=4) + def google_official_search(query, num_results=8): """Return the results of a google search using the official Google API""" from googleapiclient.discovery import build @@ -171,6 +173,7 @@ def google_official_search(query, num_results=8): # Return the list of search result URLs return search_results_links + def browse_website(url, question): """Browse a website and return the summary and links""" summary = get_text_summary(url, question) diff --git a/scripts/config.py b/scripts/config.py index ebf1b08b..37be1b21 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -36,6 +36,7 @@ class Config(metaclass=Singleton): """Initialize the Config class""" self.debug_mode = False self.continuous_mode = False + self.continuous_limit = 0 self.speak_mode = False self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo") @@ -44,14 +45,13 @@ class Config(metaclass=Singleton): self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 8000)) self.openai_api_key = os.getenv("OPENAI_API_KEY") - self.temperature = int(os.getenv("TEMPERATURE", "1")) - self.use_azure = False + self.temperature = float(os.getenv("TEMPERATURE", "1")) self.use_azure = os.getenv("USE_AZURE") == 'True' self.execute_local_commands = os.getenv('EXECUTE_LOCAL_COMMANDS', 'False') == 'True' if self.use_azure: self.load_azure_config() - openai.api_type = "azure" + openai.api_type = self.openai_api_type openai.api_base = self.openai_api_base openai.api_version = self.openai_api_version @@ -73,7 +73,7 @@ class Config(metaclass=Singleton): # User agent headers to use when browsing web # Some websites might just completely deny request with an error code if no user agent was found. - self.user_agent_header = {"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"} + self.user_agent_header = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"} self.redis_host = os.getenv("REDIS_HOST", "localhost") self.redis_port = os.getenv("REDIS_PORT", "6379") self.redis_password = os.getenv("REDIS_PASSWORD", "") @@ -121,14 +121,19 @@ class Config(metaclass=Singleton): config_params = yaml.load(file, Loader=yaml.FullLoader) except FileNotFoundError: config_params = {} - self.openai_api_base = config_params.get("azure_api_base", "") - self.openai_api_version = config_params.get("azure_api_version", "") + self.openai_api_type = os.getenv("OPENAI_API_TYPE", config_params.get("azure_api_type", "azure")) + self.openai_api_base = os.getenv("OPENAI_AZURE_API_BASE", config_params.get("azure_api_base", "")) + self.openai_api_version = os.getenv("OPENAI_AZURE_API_VERSION", config_params.get("azure_api_version", "")) self.azure_model_to_deployment_id_map = config_params.get("azure_model_map", []) def set_continuous_mode(self, value: bool): """Set the continuous mode value.""" self.continuous_mode = value + def set_continuous_limit(self, value: int): + """Set the continuous limit value.""" + self.continuous_limit = value + def set_speak_mode(self, value: bool): """Set the speak mode value.""" self.speak_mode = value diff --git a/scripts/data.py b/scripts/data.py deleted file mode 100644 index f80c2875..00000000 --- a/scripts/data.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -from pathlib import Path - -def load_prompt(): - """Load the prompt from data/prompt.txt""" - try: - # get directory of this file: - file_dir = Path(__file__).parent - prompt_file_path = file_dir / "data" / "prompt.txt" - - # Load the prompt from data/prompt.txt - with open(prompt_file_path, "r") as prompt_file: - prompt = prompt_file.read() - - return prompt - except FileNotFoundError: - print("Error: Prompt file not found", flush=True) - return "" diff --git a/scripts/data/prompt.txt b/scripts/data/prompt.txt deleted file mode 100644 index ffb9eb50..00000000 --- a/scripts/data/prompt.txt +++ /dev/null @@ -1,64 +0,0 @@ -CONSTRAINTS: - -1. ~4000 word limit for short term memory. Your short term memory is short, so immediately save important information to files. -2. If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember. -3. No user assistance -4. Exclusively use the commands listed in double quotes e.g. "command name" - -COMMANDS: - -1. Google Search: "google", args: "input": "" -5. Browse Website: "browse_website", args: "url": "", "question": "" -6. Start GPT Agent: "start_agent", args: "name": "", "task": "", "prompt": "" -7. Message GPT Agent: "message_agent", args: "key": "", "message": "" -8. List GPT Agents: "list_agents", args: "" -9. Delete GPT Agent: "delete_agent", args: "key": "" -10. Write to file: "write_to_file", args: "file": "", "text": "" -11. Read file: "read_file", args: "file": "" -12. Append to file: "append_to_file", args: "file": "", "text": "" -13. Delete file: "delete_file", args: "file": "" -14. Search Files: "search_files", args: "directory": "" -15. Evaluate Code: "evaluate_code", args: "code": "" -16. Get Improved Code: "improve_code", args: "suggestions": "", "code": "" -17. Write Tests: "write_tests", args: "code": "", "focus": "" -18. Execute Python File: "execute_python_file", args: "file": "" -19. Execute Shell Command, non-interactive commands only: "execute_shell", args: "command_line": "". -20. Task Complete (Shutdown): "task_complete", args: "reason": "" -21. Generate Image: "generate_image", args: "prompt": "" -22. Do Nothing: "do_nothing", args: "" - -RESOURCES: - -1. Internet access for searches and information gathering. -2. Long Term memory management. -3. GPT-3.5 powered Agents for delegation of simple tasks. -4. File output. - -PERFORMANCE EVALUATION: - -1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities. -2. Constructively self-criticize your big-picture behavior constantly. -3. Reflect on past decisions and strategies to refine your approach. -4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps. - -You should only respond in JSON format as described below - -RESPONSE FORMAT: -{ - "thoughts": - { - "text": "thought", - "reasoning": "reasoning", - "plan": "- short bulleted\n- list that conveys\n- long-term plan", - "criticism": "constructive self-criticism", - "speak": "thoughts summary to say to user" - }, - "command": { - "name": "command name", - "args":{ - "arg name": "value" - } - } -} - -Ensure the response can be parsed by Python json.loads diff --git a/scripts/execute_code.py b/scripts/execute_code.py index 2c92903c..dbd62c22 100644 --- a/scripts/execute_code.py +++ b/scripts/execute_code.py @@ -67,6 +67,7 @@ def execute_python_file(file): except Exception as e: return f"Error: {str(e)}" + def execute_shell(command_line): current_dir = os.getcwd() diff --git a/scripts/file_operations.py b/scripts/file_operations.py index 7b48c134..3bbe9da6 100644 --- a/scripts/file_operations.py +++ b/scripts/file_operations.py @@ -38,7 +38,7 @@ def write_to_file(filename, text): directory = os.path.dirname(filepath) if not os.path.exists(directory): os.makedirs(directory) - with open(filepath, "w") as f: + with open(filepath, "w", encoding='utf-8') as f: f.write(text) return "File written to successfully." except Exception as e: @@ -65,6 +65,7 @@ def delete_file(filename): except Exception as e: return "Error: " + str(e) + def search_files(directory): found_files = [] diff --git a/scripts/image_gen.py b/scripts/image_gen.py index 4481696f..6c27df3f 100644 --- a/scripts/image_gen.py +++ b/scripts/image_gen.py @@ -11,6 +11,7 @@ cfg = Config() working_directory = "auto_gpt_workspace" + def generate_image(prompt): filename = str(uuid.uuid4()) + ".jpg" diff --git a/scripts/llm_utils.py b/scripts/llm_utils.py index 35cc5ce0..731acae2 100644 --- a/scripts/llm_utils.py +++ b/scripts/llm_utils.py @@ -1,26 +1,52 @@ +import time import openai +from colorama import Fore from config import Config + cfg = Config() openai.api_key = cfg.openai_api_key + # Overly simple abstraction until we create something better +# simple retry mechanism when getting a rate error or a bad gateway def create_chat_completion(messages, model=None, temperature=cfg.temperature, max_tokens=None)->str: """Create a chat completion using the OpenAI API""" - if cfg.use_azure: - response = openai.ChatCompletion.create( - deployment_id=cfg.get_azure_deployment_id_for_model(model), - model=model, - messages=messages, - temperature=temperature, - max_tokens=max_tokens - ) - else: - response = openai.ChatCompletion.create( - model=model, - messages=messages, - temperature=temperature, - max_tokens=max_tokens - ) + response = None + num_retries = 5 + for attempt in range(num_retries): + try: + if cfg.use_azure: + response = openai.ChatCompletion.create( + deployment_id=cfg.get_azure_deployment_id_for_model(model), + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) + else: + response = openai.ChatCompletion.create( + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) + break + except openai.error.RateLimitError: + if cfg.debug_mode: + print(Fore.RED + "Error: ", "API Rate Limit Reached. Waiting 20 seconds..." + Fore.RESET) + time.sleep(20) + except openai.error.APIError as e: + if e.http_status == 502: + if cfg.debug_mode: + print(Fore.RED + "Error: ", "API Bad gateway. Waiting 20 seconds..." + Fore.RESET) + time.sleep(20) + else: + raise + if attempt == num_retries - 1: + raise + + if response is None: + raise RuntimeError("Failed to get response after 5 retries") return response.choices[0].message["content"] diff --git a/scripts/logger.py b/scripts/logger.py index 85dde813..91bdb6f6 100644 --- a/scripts/logger.py +++ b/scripts/logger.py @@ -124,6 +124,12 @@ class Logger(metaclass=Singleton): self.logger.setLevel(level) self.typing_logger.setLevel(level) + def double_check(self, additionalText=None): + if not additionalText: + additionalText = "Please ensure you've setup and configured everything correctly. Read https://github.com/Torantulino/Auto-GPT#readme to double check. You can also create a github issue or join the discord and ask there!" + + self.typewriter_log("DOUBLE CHECK CONFIGURATION", Fore.YELLOW, additionalText) + ''' Output stream to console using simulated typing @@ -151,6 +157,7 @@ class TypingConsoleHandler(logging.StreamHandler): except Exception: self.handleError(record) + class ConsoleHandler(logging.StreamHandler): def emit(self, record): msg = self.format(record) @@ -160,11 +167,11 @@ class ConsoleHandler(logging.StreamHandler): self.handleError(record) -''' -Allows to handle custom placeholders 'title_color' and 'message_no_color'. -To use this formatter, make sure to pass 'color', 'title' as log extras. -''' class AutoGptFormatter(logging.Formatter): + """ + Allows to handle custom placeholders 'title_color' and 'message_no_color'. + To use this formatter, make sure to pass 'color', 'title' as log extras. + """ def format(self, record: LogRecord) -> str: if (hasattr(record, 'color')): record.title_color = getattr(record, 'color') + getattr(record, 'title') + " " + Style.RESET_ALL diff --git a/scripts/main.py b/scripts/main.py index 5b84bd70..92851763 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -3,7 +3,6 @@ import random import commands as cmd import utils from memory import get_memory, get_supported_memory_backends -import data import chat from colorama import Fore, Style from spinner import Spinner @@ -17,19 +16,22 @@ import yaml import argparse from logger import logger import logging +from prompt import get_prompt cfg = Config() + def check_openai_api_key(): """Check if the OpenAI API key is set in config.py or as an environment variable.""" if not cfg.openai_api_key: print( Fore.RED + - "Please set your OpenAI API key in config.py or as an environment variable." + "Please set your OpenAI API key in .env or as an environment variable." ) print("You can get your key from https://beta.openai.com/account/api-keys") exit(1) + def attempt_to_fix_json_by_finding_outermost_brackets(json_string): if cfg.speak_mode and cfg.debug_mode: speak.say_text("I have received an invalid JSON response from the OpenAI API. Trying to fix it now.") @@ -58,6 +60,7 @@ def attempt_to_fix_json_by_finding_outermost_brackets(json_string): return json_string + def print_assistant_thoughts(assistant_reply): """Prints the assistant's thoughts to the console""" global ai_name @@ -126,60 +129,6 @@ def print_assistant_thoughts(assistant_reply): logger.error("Error: \n", call_stack) -def load_variables(config_file="config.yaml"): - """Load variables from yaml file if it exists, otherwise prompt the user for input""" - try: - with open(config_file) as file: - config = yaml.load(file, Loader=yaml.FullLoader) - ai_name = config.get("ai_name") - ai_role = config.get("ai_role") - ai_goals = config.get("ai_goals") - except FileNotFoundError: - ai_name = "" - ai_role = "" - ai_goals = [] - - # Prompt the user for input if config file is missing or empty values - if not ai_name: - ai_name = utils.clean_input("Name your AI: ") - if ai_name == "": - ai_name = "Entrepreneur-GPT" - - if not ai_role: - ai_role = utils.clean_input(f"{ai_name} is: ") - if ai_role == "": - ai_role = "an AI designed to autonomously develop and run businesses with the sole goal of increasing your net worth." - - if not ai_goals: - print("Enter up to 5 goals for your AI: ") - print("For example: \nIncrease net worth, Grow Twitter Account, Develop and manage multiple businesses autonomously'") - print("Enter nothing to load defaults, enter nothing when finished.") - ai_goals = [] - for i in range(5): - ai_goal = utils.clean_input(f"Goal {i+1}: ") - if ai_goal == "": - break - ai_goals.append(ai_goal) - if len(ai_goals) == 0: - ai_goals = ["Increase net worth", "Grow Twitter Account", "Develop and manage multiple businesses autonomously"] - - # Save variables to yaml file - config = {"ai_name": ai_name, "ai_role": ai_role, "ai_goals": ai_goals} - with open(config_file, "w") as file: - documents = yaml.dump(config, file) - - prompt = data.load_prompt() - prompt_start = """Your decisions must always be made independently without seeking user assistance. Play to your strengths as a LLM and pursue simple strategies with no legal complications.""" - - # Construct full prompt - full_prompt = f"You are {ai_name}, {ai_role}\n{prompt_start}\n\nGOALS:\n\n" - for i, goal in enumerate(ai_goals): - full_prompt += f"{i+1}. {goal}\n" - - full_prompt += f"\n\n{prompt}" - return full_prompt - - def construct_prompt(): """Construct the prompt for the AI to respond to""" config = AIConfig.load() @@ -262,6 +211,7 @@ def prompt_user(): config = AIConfig(ai_name, ai_role, ai_goals) return config + def parse_arguments(): """Parses the arguments passed to the script""" global cfg @@ -271,6 +221,7 @@ def parse_arguments(): parser = argparse.ArgumentParser(description='Process arguments.') parser.add_argument('--continuous', action='store_true', help='Enable Continuous Mode') + parser.add_argument('--continuous-limit', '-l', type=int, dest="continuous_limit", help='Defines the number of times to run in continuous mode') parser.add_argument('--speak', action='store_true', help='Enable Speak Mode') parser.add_argument('--debug', action='store_true', help='Enable Debug Mode') parser.add_argument('--gpt3only', action='store_true', help='Enable GPT3.5 Only Mode') @@ -290,6 +241,17 @@ def parse_arguments(): "Continuous mode is not recommended. It is potentially dangerous and may cause your AI to run forever or carry out actions you would not usually authorise. Use at your own risk.") cfg.set_continuous_mode(True) + if args.continuous_limit: + logger.typewriter_log( + "Continuous Limit: ", + Fore.GREEN, + f"{args.continuous_limit}") + cfg.set_continuous_limit(args.continuous_limit) + + # Check if continuous limit is used without continuous mode + if args.continuous_limit and not args.continuous: + parser.error("--continuous-limit can only be used with --continuous") + if args.speak: logger.typewriter_log("Speak Mode: ", Fore.GREEN, "ENABLED") cfg.set_speak_mode(True) @@ -302,10 +264,6 @@ def parse_arguments(): logger.typewriter_log("GPT4 Only Mode: ", Fore.GREEN, "ENABLED") cfg.set_fast_llm_model(cfg.smart_llm_model) - if args.debug: - logger.typewriter_log("Debug Mode: ", Fore.GREEN, "ENABLED") - cfg.set_debug_mode(True) - if args.memory_type: supported_memory = get_supported_memory_backends() chosen = args.memory_type @@ -316,118 +274,168 @@ def parse_arguments(): cfg.memory_backend = chosen -# TODO: fill in llm values here -check_openai_api_key() -parse_arguments() -logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) -ai_name = "" -prompt = construct_prompt() -# print(prompt) -# Initialize variables -full_message_history = [] -result = None -next_action_count = 0 -# Make a constant: -user_input = "Determine which next command to use, and respond using the format specified above:" +def main(): + global ai_name, memory + # TODO: fill in llm values here + check_openai_api_key() + parse_arguments() + logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) + ai_name = "" + prompt = construct_prompt() + # print(prompt) + # Initialize variables + full_message_history = [] + result = None + next_action_count = 0 + # Make a constant: + user_input = "Determine which next command to use, and respond using the format specified above:" + # Initialize memory and make sure it is empty. + # this is particularly important for indexing and referencing pinecone memory + memory = get_memory(cfg, init=True) + print('Using memory of type: ' + memory.__class__.__name__) + agent = Agent( + ai_name=ai_name, + memory=memory, + full_message_history=full_message_history, + next_action_count=next_action_count, + prompt=prompt, + user_input=user_input + ) + agent.start_interaction_loop() -# Initialize memory and make sure it is empty. -# this is particularly important for indexing and referencing pinecone memory -memory = get_memory(cfg, init=True) -print('Using memory of type: ' + memory.__class__.__name__) -# Interaction Loop -while True: - # Send message to AI, get response - with Spinner("Thinking... "): - assistant_reply = chat.chat_with_ai( - prompt, - user_input, - full_message_history, - memory, - cfg.fast_token_limit) # TODO: This hardcodes the model to use GPT3.5. Make this an argument +class Agent: + """Agent class for interacting with Auto-GPT. - # Print Assistant thoughts - print_assistant_thoughts(assistant_reply) + Attributes: + ai_name: The name of the agent. + memory: The memory object to use. + full_message_history: The full message history. + next_action_count: The number of actions to execute. + prompt: The prompt to use. + user_input: The user input. - # Get command name and arguments - try: - command_name, arguments = cmd.get_command(attempt_to_fix_json_by_finding_outermost_brackets(assistant_reply)) - if cfg.speak_mode: - speak.say_text(f"I want to execute {command_name}") - except Exception as e: - logger.error("Error: \n", str(e)) + """ + def __init__(self, + ai_name, + memory, + full_message_history, + next_action_count, + prompt, + user_input): + self.ai_name = ai_name + self.memory = memory + self.full_message_history = full_message_history + self.next_action_count = next_action_count + self.prompt = prompt + self.user_input = user_input - if not cfg.continuous_mode and next_action_count == 0: - ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### - # Get key press: Prompt the user to press enter to continue or escape - # to exit - user_input = "" - logger.typewriter_log( - "NEXT ACTION: ", - Fore.CYAN, - f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") - print( - f"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 'n' to exit program, or enter feedback for {ai_name}...", - flush=True) + def start_interaction_loop(self): + # Interaction Loop + loop_count = 0 while True: - console_input = utils.clean_input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) - if console_input.lower().rstrip() == "y": - user_input = "GENERATE NEXT COMMAND JSON" - break - elif console_input.lower().startswith("y -"): - try: - next_action_count = abs(int(console_input.split(" ")[1])) - user_input = "GENERATE NEXT COMMAND JSON" - except ValueError: - print("Invalid input format. Please enter 'y -n' where n is the number of continuous tasks.") - continue - break - elif console_input.lower() == "n": - user_input = "EXIT" + # Discontinue if continuous limit is reached + loop_count += 1 + if cfg.continuous_mode and cfg.continuous_limit > 0 and loop_count > cfg.continuous_limit: + logger.typewriter_log("Continuous Limit Reached: ", Fore.YELLOW, f"{cfg.continuous_limit}") break + + # Send message to AI, get response + with Spinner("Thinking... "): + assistant_reply = chat.chat_with_ai( + self.prompt, + self.user_input, + self.full_message_history, + self.memory, + cfg.fast_token_limit) # TODO: This hardcodes the model to use GPT3.5. Make this an argument + + # Print Assistant thoughts + print_assistant_thoughts(assistant_reply) + + # Get command name and arguments + try: + command_name, arguments = cmd.get_command( + attempt_to_fix_json_by_finding_outermost_brackets(assistant_reply)) + if cfg.speak_mode: + speak.say_text(f"I want to execute {command_name}") + except Exception as e: + logger.error("Error: \n", str(e)) + + if not cfg.continuous_mode and self.next_action_count == 0: + ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### + # Get key press: Prompt the user to press enter to continue or escape + # to exit + self.user_input = "" + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") + print( + f"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 'n' to exit program, or enter feedback for {self.ai_name}...", + flush=True) + while True: + console_input = utils.clean_input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) + if console_input.lower().rstrip() == "y": + self.user_input = "GENERATE NEXT COMMAND JSON" + break + elif console_input.lower().startswith("y -"): + try: + self.next_action_count = abs(int(console_input.split(" ")[1])) + self.user_input = "GENERATE NEXT COMMAND JSON" + except ValueError: + print("Invalid input format. Please enter 'y -n' where n is the number of continuous tasks.") + continue + break + elif console_input.lower() == "n": + self.user_input = "EXIT" + break + else: + self.user_input = console_input + command_name = "human_feedback" + break + + if self.user_input == "GENERATE NEXT COMMAND JSON": + logger.typewriter_log( + "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", + Fore.MAGENTA, + "") + elif self.user_input == "EXIT": + print("Exiting...", flush=True) + break else: - user_input = console_input - command_name = "human_feedback" - break + # Print command + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") - if user_input == "GENERATE NEXT COMMAND JSON": - logger.typewriter_log( - "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", - Fore.MAGENTA, - "") - elif user_input == "EXIT": - print("Exiting...", flush=True) - break - else: - # Print command - logger.typewriter_log( - "NEXT ACTION: ", - Fore.CYAN, - f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") + # Execute command + if command_name is not None and command_name.lower().startswith("error"): + result = f"Command {command_name} threw the following error: " + arguments + elif command_name == "human_feedback": + result = f"Human feedback: {self.user_input}" + else: + result = f"Command {command_name} returned: {cmd.execute_command(command_name, arguments)}" + if self.next_action_count > 0: + self.next_action_count -= 1 - # Execute command - if command_name is not None and command_name.lower().startswith( "error" ): - result = f"Command {command_name} threw the following error: " + arguments - elif command_name == "human_feedback": - result = f"Human feedback: {user_input}" - else: - result = f"Command {command_name} returned: {cmd.execute_command(command_name, arguments)}" - if next_action_count > 0: - next_action_count -= 1 + memory_to_add = f"Assistant Reply: {assistant_reply} " \ + f"\nResult: {result} " \ + f"\nHuman Feedback: {self.user_input} " - memory_to_add = f"Assistant Reply: {assistant_reply} " \ - f"\nResult: {result} " \ - f"\nHuman Feedback: {user_input} " + self.memory.add(memory_to_add) - memory.add(memory_to_add) + # Check if there's a result from the command append it to the message + # history + if result is not None: + self.full_message_history.append(chat.create_chat_message("system", result)) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) + else: + self.full_message_history.append( + chat.create_chat_message( + "system", "Unable to execute command")) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, "Unable to execute command") - # Check if there's a result from the command append it to the message - # history - if result is not None: - full_message_history.append(chat.create_chat_message("system", result)) - logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) - else: - full_message_history.append( - chat.create_chat_message( - "system", "Unable to execute command")) - logger.typewriter_log("SYSTEM: ", Fore.YELLOW, "Unable to execute command") + +if __name__ == "__main__": + main() diff --git a/scripts/memory/__init__.py b/scripts/memory/__init__.py index a07f9fd8..a0afc874 100644 --- a/scripts/memory/__init__.py +++ b/scripts/memory/__init__.py @@ -1,4 +1,5 @@ from memory.local import LocalCache +from memory.no_memory import NoMemory # List of supported memory backends # Add a backend to this list if the import attempt is successful @@ -18,6 +19,7 @@ except ImportError: print("Pinecone not installed. Skipping import.") PineconeMemory = None + def get_memory(cfg, init=False): memory = None if cfg.memory_backend == "pinecone": @@ -34,6 +36,8 @@ def get_memory(cfg, init=False): " use Redis as a memory backend.") else: memory = RedisMemory(cfg) + elif cfg.memory_backend == "no_memory": + memory = NoMemory(cfg) if memory is None: memory = LocalCache(cfg) @@ -41,6 +45,7 @@ def get_memory(cfg, init=False): memory.clear() return memory + def get_supported_memory_backends(): return supported_memory @@ -50,4 +55,5 @@ __all__ = [ "LocalCache", "RedisMemory", "PineconeMemory", + "NoMemory" ] diff --git a/scripts/memory/base.py b/scripts/memory/base.py index 1bb4e89f..4dbf6791 100644 --- a/scripts/memory/base.py +++ b/scripts/memory/base.py @@ -5,6 +5,7 @@ import openai cfg = Config() + def get_ada_embedding(text): text = text.replace("\n", " ") if cfg.use_azure: diff --git a/scripts/memory/no_memory.py b/scripts/memory/no_memory.py new file mode 100644 index 00000000..830982f9 --- /dev/null +++ b/scripts/memory/no_memory.py @@ -0,0 +1,66 @@ +from typing import Optional, List, Any + +from memory.base import MemoryProviderSingleton + + +class NoMemory(MemoryProviderSingleton): + def __init__(self, cfg): + """ + Initializes the NoMemory provider. + + Args: + cfg: The config object. + + Returns: None + """ + pass + + def add(self, data: str) -> str: + """ + Adds a data point to the memory. No action is taken in NoMemory. + + Args: + data: The data to add. + + Returns: An empty string. + """ + return "" + + def get(self, data: str) -> Optional[List[Any]]: + """ + Gets the data from the memory that is most relevant to the given data. + NoMemory always returns None. + + Args: + data: The data to compare to. + + Returns: None + """ + return None + + def clear(self) -> str: + """ + Clears the memory. No action is taken in NoMemory. + + Returns: An empty string. + """ + return "" + + def get_relevant(self, data: str, num_relevant: int = 5) -> Optional[List[Any]]: + """ + Returns all the data in the memory that is relevant to the given data. + NoMemory always returns None. + + Args: + data: The data to compare to. + num_relevant: The number of relevant data to return. + + Returns: None + """ + return None + + def get_stats(self): + """ + Returns: An empty dictionary as there are no stats in NoMemory. + """ + return {} diff --git a/scripts/memory/pinecone.py b/scripts/memory/pinecone.py index 8e1eaa57..20a905b3 100644 --- a/scripts/memory/pinecone.py +++ b/scripts/memory/pinecone.py @@ -2,6 +2,8 @@ import pinecone from memory.base import MemoryProviderSingleton, get_ada_embedding +from logger import logger +from colorama import Fore, Style class PineconeMemory(MemoryProviderSingleton): @@ -17,6 +19,15 @@ class PineconeMemory(MemoryProviderSingleton): # for now this works. # we'll need a more complicated and robust system if we want to start with memory. self.vec_num = 0 + + try: + pinecone.whoami() + except Exception as e: + logger.typewriter_log("FAILED TO CONNECT TO PINECONE", Fore.RED, Style.BRIGHT + str(e) + Style.RESET_ALL) + logger.double_check("Please ensure you have setup and configured Pinecone properly for use. " + + f"You can check out {Fore.CYAN + Style.BRIGHT}https://github.com/Torantulino/Auto-GPT#-pinecone-api-key-setup{Style.RESET_ALL} to ensure you've set up everything correctly.") + exit(1) + if table_name not in pinecone.list_indexes(): pinecone.create_index(table_name, dimension=dimension, metric=metric, pod_type=pod_type) self.index = pinecone.Index(table_name) diff --git a/scripts/memory/redismem.py b/scripts/memory/redismem.py index 2082fe58..49045dd8 100644 --- a/scripts/memory/redismem.py +++ b/scripts/memory/redismem.py @@ -7,6 +7,8 @@ from redis.commands.search.indexDefinition import IndexDefinition, IndexType import numpy as np from memory.base import MemoryProviderSingleton, get_ada_embedding +from logger import logger +from colorama import Fore, Style SCHEMA = [ @@ -44,6 +46,16 @@ class RedisMemory(MemoryProviderSingleton): db=0 # Cannot be changed ) self.cfg = cfg + + # Check redis connection + try: + self.redis.ping() + except redis.ConnectionError as e: + logger.typewriter_log("FAILED TO CONNECT TO REDIS", Fore.RED, Style.BRIGHT + str(e) + Style.RESET_ALL) + logger.double_check("Please ensure you have setup and configured Redis properly for use. " + + f"You can check out {Fore.CYAN + Style.BRIGHT}https://github.com/Torantulino/Auto-GPT#redis-setup{Style.RESET_ALL} to ensure you've set up everything correctly.") + exit(1) + if cfg.wipe_redis_on_start: self.redis.flushall() try: diff --git a/scripts/prompt.py b/scripts/prompt.py new file mode 100644 index 00000000..188603a3 --- /dev/null +++ b/scripts/prompt.py @@ -0,0 +1,63 @@ +from promptgenerator import PromptGenerator + + +def get_prompt(): + """ + This function generates a prompt string that includes various constraints, commands, resources, and performance evaluations. + + Returns: + str: The generated prompt string. + """ + + # Initialize the PromptGenerator object + prompt_generator = PromptGenerator() + + # Add constraints to the PromptGenerator object + prompt_generator.add_constraint("~4000 word limit for short term memory. Your short term memory is short, so immediately save important information to files.") + prompt_generator.add_constraint("If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.") + prompt_generator.add_constraint("No user assistance") + prompt_generator.add_constraint('Exclusively use the commands listed in double quotes e.g. "command name"') + + # Define the command list + commands = [ + ("Google Search", "google", {"input": ""}), + ("Browse Website", "browse_website", {"url": "", "question": ""}), + ("Start GPT Agent", "start_agent", {"name": "", "task": "", "prompt": ""}), + ("Message GPT Agent", "message_agent", {"key": "", "message": ""}), + ("List GPT Agents", "list_agents", {}), + ("Delete GPT Agent", "delete_agent", {"key": ""}), + ("Write to file", "write_to_file", {"file": "", "text": ""}), + ("Read file", "read_file", {"file": ""}), + ("Append to file", "append_to_file", {"file": "", "text": ""}), + ("Delete file", "delete_file", {"file": ""}), + ("Search Files", "search_files", {"directory": ""}), + ("Evaluate Code", "evaluate_code", {"code": ""}), + ("Get Improved Code", "improve_code", {"suggestions": "", "code": ""}), + ("Write Tests", "write_tests", {"code": "", "focus": ""}), + ("Execute Python File", "execute_python_file", {"file": ""}), + ("Execute Shell Command, non-interactive commands only", "execute_shell", { "command_line": ""}), + ("Task Complete (Shutdown)", "task_complete", {"reason": ""}), + ("Generate Image", "generate_image", {"prompt": ""}), + ("Do Nothing", "do_nothing", {}), + ] + + # Add commands to the PromptGenerator object + for command_label, command_name, args in commands: + prompt_generator.add_command(command_label, command_name, args) + + # Add resources to the PromptGenerator object + prompt_generator.add_resource("Internet access for searches and information gathering.") + prompt_generator.add_resource("Long Term memory management.") + prompt_generator.add_resource("GPT-3.5 powered Agents for delegation of simple tasks.") + prompt_generator.add_resource("File output.") + + # Add performance evaluations to the PromptGenerator object + prompt_generator.add_performance_evaluation("Continuously review and analyze your actions to ensure you are performing to the best of your abilities.") + prompt_generator.add_performance_evaluation("Constructively self-criticize your big-picture behavior constantly.") + prompt_generator.add_performance_evaluation("Reflect on past decisions and strategies to refine your approach.") + prompt_generator.add_performance_evaluation("Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.") + + # Generate the prompt string + prompt_string = prompt_generator.generate_prompt_string() + + return prompt_string diff --git a/scripts/promptgenerator.py b/scripts/promptgenerator.py new file mode 100644 index 00000000..6cfd9bcd --- /dev/null +++ b/scripts/promptgenerator.py @@ -0,0 +1,129 @@ +import json + + +class PromptGenerator: + """ + A class for generating custom prompt strings based on constraints, commands, resources, and performance evaluations. + """ + + def __init__(self): + """ + Initialize the PromptGenerator object with empty lists of constraints, commands, resources, and performance evaluations. + """ + self.constraints = [] + self.commands = [] + self.resources = [] + self.performance_evaluation = [] + self.response_format = { + "thoughts": { + "text": "thought", + "reasoning": "reasoning", + "plan": "- short bulleted\n- list that conveys\n- long-term plan", + "criticism": "constructive self-criticism", + "speak": "thoughts summary to say to user" + }, + "command": { + "name": "command name", + "args": { + "arg name": "value" + } + } + } + + def add_constraint(self, constraint): + """ + Add a constraint to the constraints list. + + Args: + constraint (str): The constraint to be added. + """ + self.constraints.append(constraint) + + def add_command(self, command_label, command_name, args=None): + """ + Add a command to the commands list with a label, name, and optional arguments. + + Args: + command_label (str): The label of the command. + command_name (str): The name of the command. + args (dict, optional): A dictionary containing argument names and their values. Defaults to None. + """ + if args is None: + args = {} + + command_args = {arg_key: arg_value for arg_key, + arg_value in args.items()} + + command = { + "label": command_label, + "name": command_name, + "args": command_args, + } + + self.commands.append(command) + + def _generate_command_string(self, command): + """ + Generate a formatted string representation of a command. + + Args: + command (dict): A dictionary containing command information. + + Returns: + str: The formatted command string. + """ + args_string = ', '.join( + f'"{key}": "{value}"' for key, value in command['args'].items()) + return f'{command["label"]}: "{command["name"]}", args: {args_string}' + + def add_resource(self, resource): + """ + Add a resource to the resources list. + + Args: + resource (str): The resource to be added. + """ + self.resources.append(resource) + + def add_performance_evaluation(self, evaluation): + """ + Add a performance evaluation item to the performance_evaluation list. + + Args: + evaluation (str): The evaluation item to be added. + """ + self.performance_evaluation.append(evaluation) + + def _generate_numbered_list(self, items, item_type='list'): + """ + Generate a numbered list from given items based on the item_type. + + Args: + items (list): A list of items to be numbered. + item_type (str, optional): The type of items in the list. Defaults to 'list'. + + Returns: + str: The formatted numbered list. + """ + if item_type == 'command': + return "\n".join(f"{i+1}. {self._generate_command_string(item)}" for i, item in enumerate(items)) + else: + return "\n".join(f"{i+1}. {item}" for i, item in enumerate(items)) + + def generate_prompt_string(self): + """ + Generate a prompt string based on the constraints, commands, resources, and performance evaluations. + + Returns: + str: The generated prompt string. + """ + formatted_response_format = json.dumps(self.response_format, indent=4) + prompt_string = ( + f"Constraints:\n{self._generate_numbered_list(self.constraints)}\n\n" + f"Commands:\n{self._generate_numbered_list(self.commands, item_type='command')}\n\n" + f"Resources:\n{self._generate_numbered_list(self.resources)}\n\n" + f"Performance Evaluation:\n{self._generate_numbered_list(self.performance_evaluation)}\n\n" + f"You should only respond in JSON format as described below \nResponse Format: \n{formatted_response_format} \nEnsure the response can be parsed by Python json.loads" + ) + + return prompt_string diff --git a/scripts/speak.py b/scripts/speak.py index 64054e3c..7a17873c 100644 --- a/scripts/speak.py +++ b/scripts/speak.py @@ -31,6 +31,7 @@ tts_headers = { mutex_lock = Lock() # Ensure only one sound is played at a time queue_semaphore = Semaphore(1) # The amount of sounds to queue before blocking the main thread + def eleven_labs_speech(text, voice_index=0): """Speak text using elevenlabs.io's API""" tts_url = "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}".format( @@ -51,6 +52,7 @@ def eleven_labs_speech(text, voice_index=0): print("Response content:", response.content) return False + def gtts_speech(text): tts = gtts.gTTS(text) with mutex_lock: @@ -58,6 +60,7 @@ def gtts_speech(text): playsound("speech.mp3", True) os.remove("speech.mp3") + def macos_tts_speech(text, voice_index=0): if voice_index == 0: os.system(f'say "{text}"') @@ -67,6 +70,7 @@ def macos_tts_speech(text, voice_index=0): else: os.system(f'say -v Samantha "{text}"') + def say_text(text, voice_index=0): def speak(): diff --git a/scripts/token_counter.py b/scripts/token_counter.py index 635d3286..8aecf168 100644 --- a/scripts/token_counter.py +++ b/scripts/token_counter.py @@ -1,6 +1,7 @@ import tiktoken from typing import List, Dict + def count_message_tokens(messages : List[Dict[str, str]], model : str = "gpt-3.5-turbo-0301") -> int: """ Returns the number of tokens used by a list of messages. @@ -41,6 +42,7 @@ def count_message_tokens(messages : List[Dict[str, str]], model : str = "gpt-3.5 num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> return num_tokens + def count_string_tokens(string: str, model_name: str) -> int: """ Returns the number of tokens in a text string. diff --git a/tests/integration/memory_tests.py b/tests/integration/memory_tests.py index 5f1611be..d0c30962 100644 --- a/tests/integration/memory_tests.py +++ b/tests/integration/memory_tests.py @@ -8,6 +8,7 @@ sys.path.append(str(Path(__file__).resolve().parent.parent.parent / 'scripts')) from config import Config from memory.local import LocalCache + class TestLocalCache(unittest.TestCase): def random_string(self, length): diff --git a/tests/local_cache_test.py b/tests/local_cache_test.py index d1f1ef08..0352624e 100644 --- a/tests/local_cache_test.py +++ b/tests/local_cache_test.py @@ -4,6 +4,7 @@ import sys sys.path.append(os.path.abspath('../scripts')) from memory.local import LocalCache + def MockConfig(): return type('MockConfig', (object,), { 'debug_mode': False, @@ -12,6 +13,7 @@ def MockConfig(): 'memory_index': 'auto-gpt', }) + class TestLocalCache(unittest.TestCase): def setUp(self): diff --git a/tests/promptgenerator_tests.py b/tests/promptgenerator_tests.py new file mode 100644 index 00000000..181fdea6 --- /dev/null +++ b/tests/promptgenerator_tests.py @@ -0,0 +1,101 @@ +# Import the required libraries for unit testing +import unittest +import sys +import os + +# Add the path to the "scripts" directory to import the PromptGenerator module +sys.path.append(os.path.abspath("../scripts")) +from promptgenerator import PromptGenerator + + +# Create a test class for the PromptGenerator, subclassed from unittest.TestCase +class promptgenerator_tests(unittest.TestCase): + + # Set up the initial state for each test method by creating an instance of PromptGenerator + def setUp(self): + self.generator = PromptGenerator() + + # Test whether the add_constraint() method adds a constraint to the generator's constraints list + def test_add_constraint(self): + constraint = "Constraint1" + self.generator.add_constraint(constraint) + self.assertIn(constraint, self.generator.constraints) + + # Test whether the add_command() method adds a command to the generator's commands list + def test_add_command(self): + command_label = "Command Label" + command_name = "command_name" + args = {"arg1": "value1", "arg2": "value2"} + self.generator.add_command(command_label, command_name, args) + command = { + "label": command_label, + "name": command_name, + "args": args, + } + self.assertIn(command, self.generator.commands) + + # Test whether the add_resource() method adds a resource to the generator's resources list + def test_add_resource(self): + resource = "Resource1" + self.generator.add_resource(resource) + self.assertIn(resource, self.generator.resources) + + # Test whether the add_performance_evaluation() method adds an evaluation to the generator's performance_evaluation list + def test_add_performance_evaluation(self): + evaluation = "Evaluation1" + self.generator.add_performance_evaluation(evaluation) + self.assertIn(evaluation, self.generator.performance_evaluation) + + # Test whether the generate_prompt_string() method generates a prompt string with all the added constraints, commands, resources and evaluations + def test_generate_prompt_string(self): + constraints = ["Constraint1", "Constraint2"] + commands = [ + { + "label": "Command1", + "name": "command_name1", + "args": {"arg1": "value1"}, + }, + { + "label": "Command2", + "name": "command_name2", + "args": {}, + }, + ] + resources = ["Resource1", "Resource2"] + evaluations = ["Evaluation1", "Evaluation2"] + + # Add all the constraints, commands, resources, and evaluations to the generator + for constraint in constraints: + self.generator.add_constraint(constraint) + for command in commands: + self.generator.add_command( + command["label"], command["name"], command["args"]) + for resource in resources: + self.generator.add_resource(resource) + for evaluation in evaluations: + self.generator.add_performance_evaluation(evaluation) + + # Generate the prompt string and verify its correctness + prompt_string = self.generator.generate_prompt_string() + self.assertIsNotNone(prompt_string) + for constraint in constraints: + self.assertIn(constraint, prompt_string) + for command in commands: + self.assertIn(command["name"], prompt_string) + + # Check for each key-value pair in the command args dictionary + for key, value in command["args"].items(): + self.assertIn(f'"{key}": "{value}"', prompt_string) + for resource in resources: + self.assertIn(resource, prompt_string) + for evaluation in evaluations: + self.assertIn(evaluation, prompt_string) + self.assertIn("constraints", prompt_string.lower()) + self.assertIn("commands", prompt_string.lower()) + self.assertIn("resources", prompt_string.lower()) + self.assertIn("performance evaluation", prompt_string.lower()) + + +# Run the tests when this script is executed +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_config.py b/tests/test_config.py index c1310b70..ba8381e1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ import unittest from scripts.config import Config + class TestConfig(unittest.TestCase): def test_singleton(self): diff --git a/tests/test_json_parser.py b/tests/test_json_parser.py index 352cf3d4..c403c73d 100644 --- a/tests/test_json_parser.py +++ b/tests/test_json_parser.py @@ -3,6 +3,7 @@ import tests.context from scripts.json_parser import fix_and_parse_json + class TestParseJson(unittest.TestCase): def test_valid_json(self): # Test that a valid JSON string is parsed correctly @@ -52,7 +53,7 @@ class TestParseJson(unittest.TestCase): good_obj = { "command": { "name": "browse_website", - "args":{ + "args": { "url": "https://github.com/Torantulino/Auto-GPT" } }, @@ -91,7 +92,7 @@ class TestParseJson(unittest.TestCase): good_obj = { "command": { "name": "browse_website", - "args":{ + "args": { "url": "https://github.com/Torantulino/Auto-GPT" } }, diff --git a/tests/unit/json_tests.py b/tests/unit/json_tests.py index 1edbaeaf..4f326721 100644 --- a/tests/unit/json_tests.py +++ b/tests/unit/json_tests.py @@ -5,6 +5,7 @@ import sys sys.path.append(os.path.abspath('../scripts')) from json_parser import fix_and_parse_json + class TestParseJson(unittest.TestCase): def test_valid_json(self): # Test that a valid JSON string is parsed correctly @@ -52,7 +53,7 @@ class TestParseJson(unittest.TestCase): good_obj = { "command": { "name": "browse_website", - "args":{ + "args": { "url": "https://github.com/Torantulino/Auto-GPT" } }, @@ -91,7 +92,7 @@ class TestParseJson(unittest.TestCase): good_obj = { "command": { "name": "browse_website", - "args":{ + "args": { "url": "https://github.com/Torantulino/Auto-GPT" } },