diff --git a/.env.template b/.env.template index 98d2ca91..aa7b04e9 100644 --- a/.env.template +++ b/.env.template @@ -1,16 +1,107 @@ -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 -ELEVENLABS_API_KEY=your-elevenlabs-api-key -SMART_LLM_MODEL=gpt-4 -FAST_LLM_MODEL=gpt-3.5-turbo -GOOGLE_API_KEY= -CUSTOM_SEARCH_ENGINE_ID= +TEMPERATURE=1 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 + +### 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_API_TOKEN= + +### 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 + +### STREAMELEMENTS +# USE_BRIAN_TTS - Use Brian TTS or not (Default: False) USE_BRIAN_TTS=False \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/1.bug.yml b/.github/ISSUE_TEMPLATE/1.bug.yml index cf49ab5f..e2404c76 100644 --- a/.github/ISSUE_TEMPLATE/1.bug.yml +++ b/.github/ISSUE_TEMPLATE/1.bug.yml @@ -7,7 +7,19 @@ body: value: | Please provide a searchable summary of the issue in the title above ⬆️. - Thanks for contributing by creating an issue! ❤️ + ⚠️ SUPER-busy repo, please help the volunteer maintainers. + The less time we spend here, the more time we spend building AutoGPT. + + Please help us help you: + - Does it work on `stable` branch (https://github.com/Torantulino/Auto-GPT/tree/stable)? + - Does it work on current `master` (https://github.com/Torantulino/Auto-GPT/tree/master)? + - Search for existing issues, "add comment" is tidier than "new issue" + - Ask on our Discord (https://discord.gg/autogpt) + - Provide relevant info: + - Provide commit-hash (`git rev-parse HEAD` gets it) + - If it's a pip/packages issue, provide pip version, python version + - If it's a crash, provide traceback. + - type: checkboxes attributes: label: Duplicates @@ -32,8 +44,8 @@ body: attributes: label: Your prompt 📝 description: | - Please provide the prompt you are using. You can find your last-used prompt in last_run_ai_settings.yaml. + If applicable please provide the prompt you are using. You can find your last-used prompt in last_run_ai_settings.yaml. value: | ```yaml # Paste your prompt here - ``` \ No newline at end of file + ``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1ac8f864..c355965a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,10 +23,10 @@ By following these guidelines, your PRs are more likely to be merged quickly aft ### PR Quality Checklist - [ ] My pull request is atomic and focuses on a single change. -- [ ] I have thouroughly tested my changes with multiple different prompts. +- [ ] I have thoroughly tested my changes with multiple different prompts. - [ ] I have considered potential risks and mitigations for my changes. - [ ] I have documented my changes clearly and comprehensively. -- [ ] I have not snuck in any "extra" small tweaks changes +- [ ] I have not snuck in any "extra" small tweaks changes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0b90b55d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: Python CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.8] + + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint with flake8 + continue-on-error: false + run: flake8 scripts/ tests/ --select E303,W293,W291,W292,E305,E231,E302 + + - name: Run unittest tests with coverage + run: | + coverage run --source=scripts -m unittest discover tests + + - name: Generate coverage report + run: | + coverage report + coverage xml diff --git a/.gitignore b/.gitignore index 2d603801..cf6e75df 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,20 @@ package-lock.json auto_gpt_workspace/* *.mpeg .env -venv/* +azure.yaml +*venv/* outputs/* ai_settings.yaml +last_run_ai_settings.yaml .vscode +.idea/* auto-gpt.json log.txt + +# Coverage reports +.coverage +coverage.xml +htmlcov/ + +# For Macs Dev Environs: ignoring .Desktop Services_Store +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09a604eb..0529cbd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ To contribute to this GitHub project, you can follow these steps: 2. Clone the repository to your local machine using the following command: ``` -git clone https://github.com/Torantulino/Auto-GPT +git clone https://github.com//Auto-GPT ``` 3. Create a new branch for your changes using the following command: diff --git a/Dockerfile b/Dockerfile index 146a3747..4d264c88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ -FROM python:3.11 - +FROM python:3.11-slim +ENV PIP_NO_CACHE_DIR=yes WORKDIR /app -COPY scripts/ /app -COPY requirements.txt /app - +COPY requirements.txt . RUN pip install -r requirements.txt - -CMD ["python", "main.py"] +COPY scripts/ . +ENTRYPOINT ["python", "main.py"] diff --git a/README.md b/README.md index 749c8791..d1121976 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # 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) -[![](https://dcbadge.vercel.app/api/server/PQ7VX6TY4t?style=flat)](https://discord.gg/PQ7VX6TY4t) -Auto-GPT is an experimental open-source application showcasing the capabilities of the GPT-4 language model. This program, driven by GPT-4, autonomously develops and manages businesses to increase net worth. As one of the first examples of GPT-4 running fully autonomously, Auto-GPT pushes the boundaries of what is possible with AI. +![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)](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. ### Demo (30/03/2023): -https://user-images.githubusercontent.com/22963551/228855501-2f5777cf-755b-4407-a643-c7299e5b6419.mp4 +https://user-images.githubusercontent.com/22963551/228855501-2f5777cf-755b-4407-a643-c7299e5b6419.mp4

💖 Help Fund Auto-GPT's Development 💖

@@ -26,27 +28,32 @@ Your support is greatly appreciated   SpacingLily  merwanehamadi  m  zkonduit  maxxflyer  tekelsey  digisomni  nocodeclarity  tjarmain

- ## Table of Contents - [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) + - [Demo (30/03/2023):](#demo-30032023) - [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,59 +64,83 @@ Your support is greatly appreciated - 🗃️ File storage and summarization with GPT-3.5 ## 📋 Requirements + - [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) -- OpenAI API key +- [OpenAI API key](https://platform.openai.com/account/api-keys) - [PINECONE API key](https://www.pinecone.io/) Optional: -- ElevenLabs Key (If you want the AI to speak) + +- [ElevenLabs Key](https://elevenlabs.io/) (If you want the AI to speak) ## 💾 Installation 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.* +_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._ + +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 ☝️ -1. 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: -*(Type this into your CMD window, you're aiming to navigate the CMD window to the repository you just downloaded)* +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: -*(Again, type this into your CMD window)* +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 provide the `OPENAI_AZURE_API_BASE`, `OPENAI_AZURE_API_VERSION` and `OPENAI_AZURE_DEPLOYMENT_ID` values as explained here: https://pypi.org/project/openai/ in the `Microsoft Azure Endpoints` section + - If you want to use GPT on an Azure instance, set `USE_AZURE` to `True` and then: + - Rename `azure.yaml.template` to `azure.yaml` and provide the relevant `azure_api_base`, `azure_api_version` and all of the deployment ids for the relevant models in the `azure_model_map` section: + - `fast_llm_model_deployment_id` - your gpt-3.5-turbo or gpt-4 deployment id + - `smart_llm_model_deployment_id` - your gpt-4 deployment id + - `embedding_model_deployment_id` - your text-embedding-ada-002 v2 deployment id + - Please specify all of these values as double quoted strings + - details can be found here: https://pypi.org/project/openai/ in the `Microsoft Azure Endpoints` section and here: https://learn.microsoft.com/en-us/azure/cognitive-services/openai/tutorials/embeddings?tabs=command-line for the embedding model. ## 🔧 Usage 1. Run the `main.py` Python script in your terminal: -*(Type this into your CMD window)* + _(Type this into your CMD window)_ + ``` python scripts/main.py ``` + 2. After each of AUTO-GPT's actions, type "NEXT COMMAND" to authorise them to continue. 3. To exit the program, type "exit" and press Enter. +### Logs + +You will find activity and error logs in the folder `./output/logs` + +To output debug logs: + +``` +python scripts/main.py --debug +``` + ## 🗣️ Speech Mode + Use this to use TTS for Auto-GPT + ``` python scripts/main.py --speak - ``` ## 🔍 Google API Keys Configuration @@ -125,18 +156,22 @@ To use the `google_official_search` command, you need to set up your Google API 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. +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. -*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.* +_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._ ### Setting up environment variables - For Windows Users: + +For Windows Users: + ``` setx GOOGLE_API_KEY "YOUR_GOOGLE_API_KEY" setx CUSTOM_SEARCH_ENGINE_ID "YOUR_CUSTOM_SEARCH_ENGINE_ID" ``` + For macOS and Linux users: + ``` export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY" export CUSTOM_SEARCH_ENGINE_ID="YOUR_CUSTOM_SEARCH_ENGINE_ID" @@ -148,12 +183,15 @@ export CUSTOM_SEARCH_ENGINE_ID="YOUR_CUSTOM_SEARCH_ENGINE_ID" Install docker desktop. Run: + ``` docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest ``` + See https://hub.docker.com/r/redis/redis-stack-server for setting a password and additional configuration. Set the following environment variables: + ``` MEMORY_BACKEND=redis REDIS_HOST=localhost @@ -173,72 +211,93 @@ To persist memory stored in Redis. You can specify the memory index for redis using the following: -```` +``` MEMORY_INDEX=whatever -```` +``` ## 🌲 Pinecone API Key Setup Pinecone enables the storage of vast amounts of vector-based memory, allowing for only relevant memories to be loaded for the agent at any given time. -1. Go to app.pinecone.io and make an account if you don't already have one. +1. Go to [pinecone](https://app.pinecone.io/) and make an account if you don't already have one. 2. Choose the `Starter` plan to avoid being charged. 3. Find your API key and region under the default project in the left sidebar. ### Setting up environment variables -Simply set them in the `.env` file. +Simply set them in the `.env` file. Alternatively, you can set them from the command line (advanced): For Windows Users: + ``` setx PINECONE_API_KEY "YOUR_PINECONE_API_KEY" setx PINECONE_ENV "Your pinecone region" # something like: us-east4-gcp ``` + For macOS and Linux users: + ``` export PINECONE_API_KEY="YOUR_PINECONE_API_KEY" export PINECONE_ENV="Your pinecone region" # something like: us-east4-gcp ``` +## Setting Your Cache Type + +By default Auto-GPT is going to use LocalCache instead of redis or Pinecone. + +To switch to either, change the `MEMORY_BACKEND` env variable to the value that you want: + +`local` (default) uses a local JSON cache file +`pinecone` uses the Pinecone.io account you configured in your ENV settings +`redis` will use the redis cache that you configured ## View Memory Usage 1. View memory usage by using the `--debug` flag :) - ## 💀 Continuous Mode ⚠️ + Run the AI **without** user authorisation, 100% automated. 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. + 1. Run the `main.py` Python script in your terminal: + ``` python scripts/main.py --continuous ``` + 2. To exit the program, press Ctrl + C ## GPT3.5 ONLY Mode + If you don't have access to the GPT4 api, this mode will allow you to use Auto-GPT! + ``` python scripts/main.py --gpt3only ``` + It is recommended to use a virtual machine for tasks that require high security measures to prevent any potential harm to the main computer's system and data. ## 🖼 Image Generation + By default, Auto-GPT uses DALL-e for image generation. To use Stable Diffusion, a [HuggingFace API Token](https://huggingface.co/settings/tokens) is required. Once you have a token, set these variables in your `.env`: + ``` IMAGE_PROVIDER=sd HUGGINGFACE_API_TOKEN="YOUR_HUGGINGFACE_API_TOKEN" ``` ## ⚠️ Limitations + This experiment aims to showcase the potential of GPT-4 but comes with some limitations: 1. Not a polished application or product, just an experiment @@ -267,3 +326,35 @@ Stay up-to-date with the latest news, updates, and insights about Auto-GPT by fo We look forward to connecting with you and hearing your thoughts, ideas, and experiences with Auto-GPT. Join us on Twitter and let's explore the future of AI together! +

+ + Star History Chart + +

+ +## Run tests + +To run tests, run the following command: + +``` +python -m unittest discover tests +``` + +To run tests and see coverage, run the following command: + +``` +coverage run -m unittest discover tests +``` + +## Run linter + +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,E231,E302 +``` \ No newline at end of file diff --git a/ai_settings.yaml b/ai_settings.yaml deleted file mode 100644 index b37ba849..00000000 --- a/ai_settings.yaml +++ /dev/null @@ -1,7 +0,0 @@ -ai_goals: -- Increase net worth. -- Develop and manage multiple businesses autonomously. -- Play to your strengths as a Large Language Model. -ai_name: Entrepreneur-GPT -ai_role: an AI designed to autonomously develop and run businesses with the sole goal - of increasing your net worth. diff --git a/azure.yaml.template b/azure.yaml.template new file mode 100644 index 00000000..74ca797b --- /dev/null +++ b/azure.yaml.template @@ -0,0 +1,7 @@ +azure_api_type: azure_ad +azure_api_base: your-base-url-for-azure +azure_api_version: api-version-for-azure +azure_model_map: + fast_llm_model_deployment_id: gpt35-deployment-id-for-azure + smart_llm_model_deployment_id: gpt4-deployment-id-for-azure + embedding_model_deployment_id: embedding-deployment-id-for-azure 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/main.py b/main.py new file mode 100644 index 00000000..656c34ec --- /dev/null +++ b/main.py @@ -0,0 +1 @@ +from scripts.main import main diff --git a/requirements.txt b/requirements.txt index 6a9ba643..3f7fd228 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,6 @@ pinecone-client==2.2.1 redis orjson Pillow +coverage +flake8 +numpy diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/agent_manager.py b/scripts/agent_manager.py index e6bf3b86..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 @@ -13,7 +14,7 @@ def create_agent(task, prompt, model): messages = [{"role": "user", "content": prompt}, ] - # Start GTP3 instance + # Start GPT instance agent_reply = create_chat_completion( model=model, messages=messages, @@ -41,7 +42,7 @@ def message_agent(key, message): # Add user message to message history before sending to agent messages.append({"role": "user", "content": message}) - # Start GTP3 instance + # Start GPT instance agent_reply = create_chat_completion( model=model, messages=messages, diff --git a/scripts/ai_config.py b/scripts/ai_config.py index 2a4854cb..4eb076ef 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: """ @@ -42,11 +43,11 @@ class AIConfig: config_file (int): The path to the config yaml file. DEFAULT: "../ai_settings.yaml" Returns: - cls (object): A instance of given cls object + cls (object): An instance of given cls object """ 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 = {} @@ -80,7 +81,7 @@ class AIConfig: None Returns: - full_prompt (str): A string containing the intitial prompt for the user including the ai_name, ai_role and ai_goals. + full_prompt (str): A string containing the initial prompt for the user including the ai_name, ai_role and ai_goals. """ prompt_start = """Your decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.""" @@ -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 8ad77441..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() @@ -46,7 +45,6 @@ def improve_code(suggestions: List[str], code: str) -> str: return result_string - def write_tests(code: str, focus: List[str]) -> str: """ A function that takes in code and focus topics and returns a response from create chat completion api call. diff --git a/scripts/browse.py b/scripts/browse.py index b0c745ef..9e93c55a 100644 --- a/scripts/browse.py +++ b/scripts/browse.py @@ -2,32 +2,64 @@ import requests from bs4 import BeautifulSoup from config import Config from llm_utils import create_chat_completion +from urllib.parse import urlparse, urljoin cfg = Config() + +# Function to check if the URL is valid +def is_valid_url(url): + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +# Function to sanitize the URL +def sanitize_url(url): + return urljoin(url, urlparse(url).path) + + # Define and check for local file address prefixes def check_local_file_access(url): local_prefixes = ['file:///', 'file://localhost', 'http://localhost', 'https://localhost'] return any(url.startswith(prefix) for prefix in local_prefixes) + +def get_response(url, headers=cfg.user_agent_header, timeout=10): + try: + # Restrict access to local files + if check_local_file_access(url): + raise ValueError('Access to local files is restricted') + + # Most basic check if the URL is valid: + if not url.startswith('http://') and not url.startswith('https://'): + raise ValueError('Invalid URL format') + + sanitized_url = sanitize_url(url) + + response = requests.get(sanitized_url, headers=headers, timeout=timeout) + + # Check if the response contains an HTTP error + if response.status_code >= 400: + return None, "Error: HTTP " + str(response.status_code) + " error" + + return response, None + except ValueError as ve: + # Handle invalid URL format + return None, "Error: " + str(ve) + + except requests.exceptions.RequestException as re: + # Handle exceptions related to the HTTP request (e.g., connection errors, timeouts, etc.) + return None, "Error: " + str(re) + + def scrape_text(url): """Scrape text from a webpage""" - # Most basic check if the URL is valid: - if not url.startswith('http'): - return "Error: Invalid URL" - - # Restrict access to local files - if check_local_file_access(url): - return "Error: Access to local files is restricted" - - try: - response = requests.get(url, headers=cfg.user_agent_header) - except requests.exceptions.RequestException as e: - return "Error: " + str(e) - - # Check if the response contains an HTTP error - if response.status_code >= 400: - return "Error: HTTP " + str(response.status_code) + " error" + response, error_message = get_response(url) + if error_message: + return error_message soup = BeautifulSoup(response.text, "html.parser") @@ -60,11 +92,9 @@ def format_hyperlinks(hyperlinks): def scrape_links(url): """Scrape links from a webpage""" - response = requests.get(url, headers=cfg.user_agent_header) - - # Check if the response contains an HTTP error - if response.status_code >= 400: - return "error" + response, error_message = get_response(url) + if error_message: + return error_message soup = BeautifulSoup(response.text, "html.parser") @@ -102,6 +132,7 @@ def create_message(chunk, question): "content": f"\"\"\"{chunk}\"\"\" Using the above text, please answer the following question: \"{question}\" -- if the question cannot be answered using the text, please summarize the text." } + def summarize_text(text, question): """Summarize text using the LLM model""" if not text: diff --git a/scripts/call_ai_function.py b/scripts/call_ai_function.py index f8238658..6f1d6cee 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): diff --git a/scripts/chat.py b/scripts/chat.py index 23e5b501..5392e438 100644 --- a/scripts/chat.py +++ b/scripts/chat.py @@ -4,9 +4,12 @@ from dotenv import load_dotenv from config import Config import token_counter from llm_utils import create_chat_completion +from logger import logger +import logging cfg = Config() + def create_chat_message(role, content): """ Create a chat message with the given role and content. @@ -64,15 +67,12 @@ def chat_with_ai( model = cfg.fast_llm_model # TODO: Change model from hardcode to argument # Reserve 1000 tokens for the response - if cfg.debug: - print(f"Token limit: {token_limit}") - + logger.debug(f"Token limit: {token_limit}") send_token_limit = token_limit - 1000 - relevant_memory = permanent_memory.get_relevant(str(full_message_history[-5:]), 10) + relevant_memory = '' if len(full_message_history) ==0 else permanent_memory.get_relevant(str(full_message_history[-9:]), 10) - if cfg.debug: - print('Memory Stats: ', permanent_memory.get_stats()) + logger.debug(f'Memory Stats: {permanent_memory.get_stats()}') next_message_to_add_index, current_tokens_used, insertion_index, current_context = generate_context( prompt, relevant_memory, full_message_history, model) @@ -110,19 +110,17 @@ def chat_with_ai( # assert tokens_remaining >= 0, "Tokens remaining is negative. This should never happen, please submit a bug report at https://www.github.com/Torantulino/Auto-GPT" # Debug print the current context - if cfg.debug: - print(f"Token limit: {token_limit}") - print(f"Send Token Count: {current_tokens_used}") - print(f"Tokens remaining for response: {tokens_remaining}") - print("------------ CONTEXT SENT TO AI ---------------") - for message in current_context: - # Skip printing the prompt - if message["role"] == "system" and message["content"] == prompt: - continue - print( - f"{message['role'].capitalize()}: {message['content']}") - print() - print("----------- END OF CONTEXT ----------------") + logger.debug(f"Token limit: {token_limit}") + logger.debug(f"Send Token Count: {current_tokens_used}") + logger.debug(f"Tokens remaining for response: {tokens_remaining}") + logger.debug("------------ CONTEXT SENT TO AI ---------------") + for message in current_context: + # Skip printing the prompt + if message["role"] == "system" and message["content"] == prompt: + continue + logger.debug(f"{message['role'].capitalize()}: {message['content']}") + logger.debug("") + logger.debug("----------- END OF CONTEXT ----------------") # TODO: use a model defined elsewhere, so that model can contain temperature and other settings we care about assistant_reply = create_chat_completion( @@ -141,6 +139,6 @@ def chat_with_ai( return assistant_reply except openai.error.RateLimitError: - # TODO: WHen we switch to langchain, this is built in + # TODO: When we switch to langchain, this is built in print("Error: ", "API Rate Limit Reached. Waiting 10 seconds...") time.sleep(10) diff --git a/scripts/commands.py b/scripts/commands.py index ce5d04ff..fe6f6c30 100644 --- a/scripts/commands.py +++ b/scripts/commands.py @@ -7,7 +7,7 @@ import speak from config import Config import ai_functions as ai from file_operations import read_file, write_to_file, append_to_file, delete_file, search_files -from execute_code import execute_python_file +from execute_code import execute_python_file, execute_shell from json_parser import fix_and_parse_json from image_gen import generate_image from duckduckgo_search import ddg @@ -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: @@ -103,6 +104,11 @@ def execute_command(command_name, arguments): return ai.write_tests(arguments["code"], arguments.get("focus")) elif command_name == "execute_python_file": # Add this command return execute_python_file(arguments["file"]) + elif command_name == "execute_shell": + if cfg.execute_local_commands: + return execute_shell(arguments["command_line"]) + else: + return "You are not allowed to run local shell commands. To execute shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' in your config. Do not attempt to bypass the restriction." elif command_name == "generate_image": return generate_image(arguments["prompt"]) elif command_name == "do_nothing": @@ -110,7 +116,7 @@ def execute_command(command_name, arguments): elif command_name == "task_complete": shutdown() else: - return f"Unknown command '{command_name}'. Please refer to the 'COMMANDS' list for availabe commands and only respond in the specified JSON format." + return f"Unknown command '{command_name}'. Please refer to the 'COMMANDS' list for available commands and only respond in the specified JSON format." # All errors, return "Error: + error message" except Exception as e: return "Error: " + str(e) @@ -130,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 @@ -166,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 c4ad3bf4..267852be 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -1,6 +1,7 @@ import abc import os import openai +import yaml from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() @@ -35,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") @@ -43,17 +45,19 @@ 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.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.openai_api_base = os.getenv("OPENAI_AZURE_API_BASE") - self.openai_api_version = os.getenv("OPENAI_AZURE_API_VERSION") - self.openai_deployment_id = os.getenv("OPENAI_AZURE_DEPLOYMENT_ID") - openai.api_type = "azure" + self.load_azure_config() + openai.api_type = self.openai_api_type openai.api_base = self.openai_api_base openai.api_version = self.openai_api_version self.elevenlabs_api_key = os.getenv("ELEVENLABS_API_KEY") + self.elevenlabs_voice_1_id = os.getenv("ELEVENLABS_VOICE_1_ID") + self.elevenlabs_voice_2_id = os.getenv("ELEVENLABS_VOICE_2_ID") self.use_mac_os_tts = False self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS") @@ -72,22 +76,67 @@ 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", "") self.wipe_redis_on_start = os.getenv("WIPE_REDIS_ON_START", "True") == 'True' self.memory_index = os.getenv("MEMORY_INDEX", 'auto-gpt') - # Note that indexes must be created on db 0 in redis, this is not configureable. + # Note that indexes must be created on db 0 in redis, this is not configurable. self.memory_backend = os.getenv("MEMORY_BACKEND", 'local') # Initialize the OpenAI API client openai.api_key = self.openai_api_key + def get_azure_deployment_id_for_model(self, model: str) -> str: + """ + Returns the relevant deployment id for the model specified. + + Parameters: + model(str): The model to map to the deployment id. + + Returns: + The matching deployment id if found, otherwise an empty string. + """ + if model == self.fast_llm_model: + return self.azure_model_to_deployment_id_map["fast_llm_model_deployment_id"] + elif model == self.smart_llm_model: + return self.azure_model_to_deployment_id_map["smart_llm_model_deployment_id"] + elif model == "text-embedding-ada-002": + return self.azure_model_to_deployment_id_map["embedding_model_deployment_id"] + else: + return "" + + AZURE_CONFIG_FILE = os.path.join(os.path.dirname(__file__), '..', 'azure.yaml') + + def load_azure_config(self, config_file: str=AZURE_CONFIG_FILE) -> None: + """ + Loads the configuration parameters for Azure hosting from the specified file path as a yaml file. + + Parameters: + config_file(str): The path to the config yaml file. DEFAULT: "../azure.yaml" + + Returns: + None + """ + try: + with open(config_file) as file: + config_params = yaml.load(file, Loader=yaml.FullLoader) + except FileNotFoundError: + config_params = {} + 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 @@ -116,6 +165,14 @@ class Config(metaclass=Singleton): """Set the ElevenLabs API key value.""" self.elevenlabs_api_key = value + def set_elevenlabs_voice_1_id(self, value: str): + """Set the ElevenLabs Voice 1 ID value.""" + self.elevenlabs_voice_1_id = value + + def set_elevenlabs_voice_2_id(self, value: str): + """Set the ElevenLabs Voice 2 ID value.""" + self.elevenlabs_voice_2_id = value + def set_google_api_key(self, value: str): """Set the Google API key value.""" self.google_api_key = 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 fc68f3ae..00000000 --- a/scripts/data/prompt.txt +++ /dev/null @@ -1,63 +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. Task Complete (Shutdown): "task_complete", args: "reason": "" -20. Generate Image: "generate_image", args: "prompt": "" -21. 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 a8f90911..dbd62c22 100644 --- a/scripts/execute_code.py +++ b/scripts/execute_code.py @@ -1,17 +1,20 @@ import docker import os +import subprocess + + +WORKSPACE_FOLDER = "auto_gpt_workspace" def execute_python_file(file): """Execute a Python file in a Docker container and return the output""" - workspace_folder = "auto_gpt_workspace" - print (f"Executing file '{file}' in workspace '{workspace_folder}'") + print (f"Executing file '{file}' in workspace '{WORKSPACE_FOLDER}'") if not file.endswith(".py"): return "Error: Invalid file type. Only .py files are allowed." - file_path = os.path.join(workspace_folder, file) + file_path = os.path.join(WORKSPACE_FOLDER, file) if not os.path.isfile(file_path): return f"Error: File '{file}' does not exist." @@ -19,14 +22,31 @@ def execute_python_file(file): try: client = docker.from_env() + image_name = 'python:3.10' + try: + client.images.get(image_name) + print(f"Image '{image_name}' found locally") + except docker.errors.ImageNotFound: + print(f"Image '{image_name}' not found locally, pulling from Docker Hub") + # Use the low-level API to stream the pull response + low_level_client = docker.APIClient() + for line in low_level_client.pull(image_name, stream=True, decode=True): + # Print the status and progress, if available + status = line.get('status') + progress = line.get('progress') + if status and progress: + print(f"{status}: {progress}") + elif status: + print(status) + # You can replace 'python:3.8' with the desired Python image/version # You can find available Python images on Docker Hub: # https://hub.docker.com/_/python container = client.containers.run( - 'python:3.10', + image_name, f'python {file}', volumes={ - os.path.abspath(workspace_folder): { + os.path.abspath(WORKSPACE_FOLDER): { 'bind': '/workspace', 'mode': 'ro'}}, working_dir='/workspace', @@ -46,3 +66,23 @@ def execute_python_file(file): except Exception as e: return f"Error: {str(e)}" + + +def execute_shell(command_line): + + current_dir = os.getcwd() + + if not WORKSPACE_FOLDER in current_dir: # Change dir into workspace if necessary + work_dir = os.path.join(os.getcwd(), WORKSPACE_FOLDER) + os.chdir(work_dir) + + print (f"Executing command '{command_line}' in working directory '{os.getcwd()}'") + + result = subprocess.run(command_line, capture_output=True, shell=True) + output = f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + + # Change back to whatever the prior working dir was + + os.chdir(current_dir) + + return output diff --git a/scripts/file_operations.py b/scripts/file_operations.py index c6066ef9..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 = [] @@ -80,4 +81,4 @@ def search_files(directory): relative_path = os.path.relpath(os.path.join(root, file), working_directory) found_files.append(relative_path) - return found_files \ No newline at end of file + return 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/json_parser.py b/scripts/json_parser.py index 8c17dfa2..29995629 100644 --- a/scripts/json_parser.py +++ b/scripts/json_parser.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Union from call_ai_function import call_ai_function from config import Config from json_utils import correct_json +from logger import logger cfg = Config() @@ -26,7 +27,7 @@ JSON_SCHEMA = """ """ -def fix_and_parse_json( +def fix_and_parse_json( json_str: str, try_to_fix_with_gpt: bool = True ) -> Union[str, Dict[Any, Any]]: @@ -35,8 +36,8 @@ def fix_and_parse_json( json_str = json_str.replace('\t', '') return json.loads(json_str) except json.JSONDecodeError as _: # noqa: F841 - json_str = correct_json(json_str) try: + json_str = correct_json(json_str) return json.loads(json_str) except json.JSONDecodeError as _: # noqa: F841 pass @@ -53,9 +54,10 @@ def fix_and_parse_json( last_brace_index = json_str.rindex("}") json_str = json_str[:last_brace_index+1] return json.loads(json_str) - except json.JSONDecodeError as e: # noqa: F841 + # Can throw a ValueError if there is no "{" or "}" in the json_str + except (json.JSONDecodeError, ValueError) as e: # noqa: F841 if try_to_fix_with_gpt: - print("Warning: Failed to parse AI output, attempting to fix." + logger.warn("Warning: Failed to parse AI output, attempting to fix." "\n If you see this warning frequently, it's likely that" " your prompt is confusing the AI. Try changing it up" " slightly.") @@ -67,22 +69,21 @@ def fix_and_parse_json( else: # This allows the AI to react to the error message, # which usually results in it correcting its ways. - print("Failed to fix ai output, telling the AI.") + logger.error("Failed to fix AI output, telling the AI.") return json_str else: raise e def fix_json(json_str: str, schema: str) -> str: - """Fix the given JSON string to make it parseable and fully complient with the provided schema.""" - - # Try to fix the JSON using gpt: + """Fix the given JSON string to make it parseable and fully compliant with the provided schema.""" + # Try to fix the JSON using GPT: function_string = "def fix_json(json_str: str, schema:str=None) -> str:" args = [f"'''{json_str}'''", f"'''{schema}'''"] description_string = "Fixes the provided JSON string to make it parseable"\ - " and fully complient with the provided schema.\n If an object or"\ + " and fully compliant with the provided schema.\n If an object or"\ " field specified in the schema isn't contained within the correct"\ - " JSON, it is ommited.\n This function is brilliant at guessing"\ + " JSON, it is omitted.\n This function is brilliant at guessing"\ " when the format is incorrect." # If it doesn't already start with a "`", add one: @@ -91,12 +92,11 @@ def fix_json(json_str: str, schema: str) -> str: result_string = call_ai_function( function_string, args, description_string, model=cfg.fast_llm_model ) - if cfg.debug: - print("------------ JSON FIX ATTEMPT ---------------") - print(f"Original JSON: {json_str}") - print("-----------") - print(f"Fixed JSON: {result_string}") - print("----------- END OF FIX ATTEMPT ----------------") + logger.debug("------------ JSON FIX ATTEMPT ---------------") + logger.debug(f"Original JSON: {json_str}") + logger.debug("-----------") + logger.debug(f"Fixed JSON: {result_string}") + logger.debug("----------- END OF FIX ATTEMPT ----------------") try: json.loads(result_string) # just check the validity diff --git a/scripts/llm_utils.py b/scripts/llm_utils.py index 94ba5f13..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 -def create_chat_completion(messages, model=None, temperature=None, max_tokens=None)->str: +# 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.openai_deployment_id, - 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 new file mode 100644 index 00000000..91bdb6f6 --- /dev/null +++ b/scripts/logger.py @@ -0,0 +1,192 @@ +import logging +import os +import random +import re +import time +from logging import LogRecord +from colorama import Fore + +from colorama import Style + +import speak +from config import Config +from config import Singleton + +cfg = Config() + +''' +Logger that handle titles in different colors. +Outputs logs in console, activity.log, and errors.log +For console handler: simulates typing +''' + + +class Logger(metaclass=Singleton): + def __init__(self): + # create log directory if it doesn't exist + log_dir = os.path.join('..', 'logs') + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + log_file = "activity.log" + error_file = "error.log" + + console_formatter = AutoGptFormatter('%(title_color)s %(message)s') + + # Create a handler for console which simulate typing + self.typing_console_handler = TypingConsoleHandler() + self.typing_console_handler.setLevel(logging.INFO) + self.typing_console_handler.setFormatter(console_formatter) + + # Create a handler for console without typing simulation + self.console_handler = ConsoleHandler() + self.console_handler.setLevel(logging.DEBUG) + self.console_handler.setFormatter(console_formatter) + + # Info handler in activity.log + self.file_handler = logging.FileHandler(os.path.join(log_dir, log_file)) + self.file_handler.setLevel(logging.DEBUG) + info_formatter = AutoGptFormatter('%(asctime)s %(levelname)s %(title)s %(message_no_color)s') + self.file_handler.setFormatter(info_formatter) + + # Error handler error.log + error_handler = logging.FileHandler(os.path.join(log_dir, error_file)) + error_handler.setLevel(logging.ERROR) + error_formatter = AutoGptFormatter( + '%(asctime)s %(levelname)s %(module)s:%(funcName)s:%(lineno)d %(title)s %(message_no_color)s') + error_handler.setFormatter(error_formatter) + + self.typing_logger = logging.getLogger('TYPER') + self.typing_logger.addHandler(self.typing_console_handler) + self.typing_logger.addHandler(self.file_handler) + self.typing_logger.addHandler(error_handler) + self.typing_logger.setLevel(logging.DEBUG) + + self.logger = logging.getLogger('LOGGER') + self.logger.addHandler(self.console_handler) + self.logger.addHandler(self.file_handler) + self.logger.addHandler(error_handler) + self.logger.setLevel(logging.DEBUG) + + def typewriter_log( + self, + title='', + title_color='', + content='', + speak_text=False, + level=logging.INFO): + if speak_text and cfg.speak_mode: + speak.say_text(f"{title}. {content}") + + if content: + if isinstance(content, list): + content = " ".join(content) + else: + content = "" + + self.typing_logger.log(level, content, extra={'title': title, 'color': title_color}) + + def debug( + self, + message, + title='', + title_color='', + ): + self._log(title, title_color, message, logging.DEBUG) + + def warn( + self, + message, + title='', + title_color='', + ): + self._log(title, title_color, message, logging.WARN) + + def error( + self, + title, + message='' + ): + self._log(title, Fore.RED, message, logging.ERROR) + + def _log( + self, + title='', + title_color='', + message='', + level=logging.INFO): + if message: + if isinstance(message, list): + message = " ".join(message) + self.logger.log(level, message, extra={'title': title, 'color': title_color}) + + def set_level(self, level): + 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 +''' + + +class TypingConsoleHandler(logging.StreamHandler): + def emit(self, record): + min_typing_speed = 0.05 + max_typing_speed = 0.01 + + msg = self.format(record) + try: + words = msg.split() + for i, word in enumerate(words): + print(word, end="", flush=True) + if i < len(words) - 1: + print(" ", end="", flush=True) + typing_speed = random.uniform(min_typing_speed, max_typing_speed) + time.sleep(typing_speed) + # type faster after each word + min_typing_speed = min_typing_speed * 0.95 + max_typing_speed = max_typing_speed * 0.95 + print() + except Exception: + self.handleError(record) + + +class ConsoleHandler(logging.StreamHandler): + def emit(self, record): + msg = self.format(record) + try: + print(msg) + except Exception: + self.handleError(record) + + +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 + else: + record.title_color = getattr(record, 'title') + if hasattr(record, 'msg'): + record.message_no_color = remove_color_codes(getattr(record, 'msg')) + else: + record.message_no_color = '' + return super().format(record) + + +def remove_color_codes(s: str) -> str: + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + return ansi_escape.sub('', s) + + +logger = Logger() diff --git a/scripts/main.py b/scripts/main.py index d84e1508..466f50dd 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -2,8 +2,7 @@ import json import random import commands as cmd import utils -from memory import get_memory -import data +from memory import get_memory, get_supported_memory_backends import chat from colorama import Fore, Style from spinner import Spinner @@ -15,56 +14,51 @@ from ai_config import AIConfig import traceback import yaml import argparse +from logger import logger import logging +from prompt import get_prompt cfg = Config() -def configure_logging(): - logging.basicConfig(filename='log.txt', - filemode='a', - format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', - datefmt='%H:%M:%S', - level=logging.DEBUG) - return logging.getLogger('AutoGPT') 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 print_to_console( - title, - title_color, - content, - speak_text=False, - min_typing_speed=0.05, - max_typing_speed=0.01): - """Prints text to the console with a typing effect""" - global cfg - global logger - if speak_text and cfg.speak_mode: - speak.say_text(f"{title}. {content}") - print(title_color + title + " " + Style.RESET_ALL, end="") - if content: - logger.info(title + ': ' + content) - if isinstance(content, list): - content = " ".join(content) - words = content.split() - for i, word in enumerate(words): - print(word, end="", flush=True) - if i < len(words) - 1: - print(" ", end="", flush=True) - typing_speed = random.uniform(min_typing_speed, max_typing_speed) - time.sleep(typing_speed) - # type faster after each word - min_typing_speed = min_typing_speed * 0.95 - max_typing_speed = max_typing_speed * 0.95 - print() + +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.") + logger.typewriter_log("Attempting to fix JSON by finding outermost brackets\n") + + try: + # Use regex to search for JSON objects + import regex + json_pattern = regex.compile(r"\{(?:[^{}]|(?R))*\}") + json_match = json_pattern.search(json_string) + + if json_match: + # Extract the valid JSON object from the string + json_string = json_match.group(0) + logger.typewriter_log(title="Apparently json was fixed.", title_color=Fore.GREEN) + if cfg.speak_mode and cfg.debug_mode: + speak.say_text("Apparently json was fixed.") + else: + raise ValueError("No valid JSON object found") + + except (json.JSONDecodeError, ValueError) as e: + if cfg.speak_mode: + speak.say_text("Didn't work. I will have to ignore this response then.") + logger.error("Error: Invalid JSON, setting it to empty JSON now.\n") + json_string = {} + + return json_string def print_assistant_thoughts(assistant_reply): @@ -72,16 +66,21 @@ def print_assistant_thoughts(assistant_reply): global ai_name global cfg try: - # Parse and print Assistant response - assistant_reply_json = fix_and_parse_json(assistant_reply) + try: + # Parse and print Assistant response + assistant_reply_json = fix_and_parse_json(assistant_reply) + except json.JSONDecodeError as e: + logger.error("Error: Invalid JSON in assistant thoughts\n", assistant_reply) + assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets(assistant_reply) + assistant_reply_json = fix_and_parse_json(assistant_reply_json) # Check if assistant_reply_json is a string and attempt to parse it into a JSON object if isinstance(assistant_reply_json, str): try: assistant_reply_json = json.loads(assistant_reply_json) except json.JSONDecodeError as e: - print_to_console("Error: Invalid JSON\n", Fore.RED, assistant_reply) - assistant_reply_json = {} + logger.error("Error: Invalid JSON\n", assistant_reply) + assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets(assistant_reply_json) assistant_thoughts_reasoning = None assistant_thoughts_plan = None @@ -96,11 +95,11 @@ def print_assistant_thoughts(assistant_reply): assistant_thoughts_criticism = assistant_thoughts.get("criticism") assistant_thoughts_speak = assistant_thoughts.get("speak") - print_to_console(f"{ai_name.upper()} THOUGHTS:", Fore.YELLOW, assistant_thoughts_text) - print_to_console("REASONING:", Fore.YELLOW, assistant_thoughts_reasoning) + logger.typewriter_log(f"{ai_name.upper()} THOUGHTS:", Fore.YELLOW, assistant_thoughts_text) + logger.typewriter_log("REASONING:", Fore.YELLOW, assistant_thoughts_reasoning) if assistant_thoughts_plan: - print_to_console("PLAN:", Fore.YELLOW, "") + logger.typewriter_log("PLAN:", Fore.YELLOW, "") # If it's a list, join it into a string if isinstance(assistant_thoughts_plan, list): assistant_thoughts_plan = "\n".join(assistant_thoughts_plan) @@ -111,20 +110,23 @@ def print_assistant_thoughts(assistant_reply): lines = assistant_thoughts_plan.split('\n') for line in lines: line = line.lstrip("- ") - print_to_console("- ", Fore.GREEN, line.strip()) + logger.typewriter_log("- ", Fore.GREEN, line.strip()) - print_to_console("CRITICISM:", Fore.YELLOW, assistant_thoughts_criticism) + logger.typewriter_log("CRITICISM:", Fore.YELLOW, assistant_thoughts_criticism) # Speak the assistant's thoughts if cfg.speak_mode and assistant_thoughts_speak: speak.say_text(assistant_thoughts_speak) - except json.decoder.JSONDecodeError: - print_to_console("Error: Invalid JSON\n", Fore.RED, assistant_reply) + return assistant_reply_json + except json.decoder.JSONDecodeError as e: + logger.error("Error: Invalid JSON\n", assistant_reply) + if cfg.speak_mode: + speak.say_text("I have received an invalid JSON response from the OpenAI API. I cannot ignore this response.") # All other errors, return "Error: + error message" except Exception as e: call_stack = traceback.format_exc() - print_to_console("Error: \n", Fore.RED, call_stack) + logger.error("Error: \n", call_stack) def load_variables(config_file="config.yaml"): @@ -169,8 +171,8 @@ def load_variables(config_file="config.yaml"): 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.""" + prompt = get_prompt() + prompt_start = """Your decisions must always be made independently without seeking user assistance. Play to your strengths as an 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" @@ -185,7 +187,7 @@ def construct_prompt(): """Construct the prompt for the AI to respond to""" config = AIConfig.load() if config.ai_name: - print_to_console( + logger.typewriter_log( f"Welcome back! ", Fore.GREEN, f"Would you like me to return to being {config.ai_name}?", @@ -214,14 +216,14 @@ def prompt_user(): """Prompt the user for input""" ai_name = "" # Construct the prompt - print_to_console( + logger.typewriter_log( "Welcome to Auto-GPT! ", Fore.GREEN, "Enter the name of your AI and its role below. Entering nothing will load defaults.", speak_text=True) # Get AI Name from User - print_to_console( + logger.typewriter_log( "Name your AI: ", Fore.GREEN, "For example, 'Entrepreneur-GPT'") @@ -229,14 +231,14 @@ def prompt_user(): if ai_name == "": ai_name = "Entrepreneur-GPT" - print_to_console( + logger.typewriter_log( f"{ai_name} here!", Fore.LIGHTBLUE_EX, "I am at your service.", speak_text=True) # Get AI Role from User - print_to_console( + logger.typewriter_log( "Describe your AI's role: ", Fore.GREEN, "For example, 'an AI designed to autonomously develop and run businesses with the sole goal of increasing your net worth.'") @@ -245,7 +247,7 @@ def prompt_user(): ai_role = "an AI designed to autonomously develop and run businesses with the sole goal of increasing your net worth." # Enter up to 5 goals for the AI - print_to_console( + logger.typewriter_log( "Enter up to 5 goals for your AI: ", Fore.GREEN, "For example: \nIncrease net worth, Grow Twitter Account, Develop and manage multiple businesses autonomously'") @@ -263,148 +265,197 @@ 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 + cfg.set_debug_mode(False) cfg.set_continuous_mode(False) cfg.set_speak_mode(False) 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') + parser.add_argument('--gpt4only', action='store_true', help='Enable GPT4 Only Mode') + parser.add_argument('--use-memory', '-m', dest="memory_type", help='Defines which Memory backend to use') args = parser.parse_args() + if args.debug: + logger.typewriter_log("Debug Mode: ", Fore.GREEN, "ENABLED") + cfg.set_debug_mode(True) + if args.continuous: - print_to_console("Continuous Mode: ", Fore.RED, "ENABLED") - print_to_console( + logger.typewriter_log("Continuous Mode: ", Fore.RED, "ENABLED") + logger.typewriter_log( "WARNING: ", Fore.RED, "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: - print_to_console("Speak Mode: ", Fore.GREEN, "ENABLED") + logger.typewriter_log("Speak Mode: ", Fore.GREEN, "ENABLED") cfg.set_speak_mode(True) if args.gpt3only: - print_to_console("GPT3.5 Only Mode: ", Fore.GREEN, "ENABLED") + logger.typewriter_log("GPT3.5 Only Mode: ", Fore.GREEN, "ENABLED") cfg.set_smart_llm_model(cfg.fast_llm_model) + if args.gpt4only: + 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 + if not chosen in supported_memory: + logger.typewriter_log("ONLY THE FOLLOWING MEMORY BACKENDS ARE SUPPORTED: ", Fore.RED, f'{supported_memory}') + logger.typewriter_log(f"Defaulting to: ", Fore.YELLOW, cfg.memory_backend) + else: + cfg.memory_backend = chosen -# TODO: fill in llm values here -check_openai_api_key() -cfg = Config() -logger = configure_logging() -parse_arguments() -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__) - -# 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 - - # Print Assistant thoughts - print_assistant_thoughts(assistant_reply) - - # Get command name and arguments - try: - command_name, arguments = cmd.get_command(assistant_reply) - except Exception as e: - print_to_console("Error: \n", Fore.RED, str(e)) - - 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 = "" - print_to_console( - "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) - while True: - console_input = utils.clean_input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) - if console_input.lower() == "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" - break - else: - user_input = console_input - command_name = "human_feedback" - break - - if user_input == "GENERATE NEXT COMMAND JSON": - print_to_console( - "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", - Fore.MAGENTA, - "") - elif user_input == "EXIT": - print("Exiting...", flush=True) +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__) + # Interaction Loop + loop_count = 0 + while True: + # 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 - else: - # Print command - print_to_console( - "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.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 + # 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 - memory_to_add = f"Assistant Reply: {assistant_reply} " \ - f"\nResult: {result} " \ - f"\nHuman Feedback: {user_input} " + # Print Assistant thoughts + print_assistant_thoughts(assistant_reply) - memory.add(memory_to_add) + # 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)) - # 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)) - print_to_console("SYSTEM: ", Fore.YELLOW, result) - else: - full_message_history.append( - chat.create_chat_message( - "system", "Unable to execute command")) - print_to_console("SYSTEM: ", Fore.YELLOW, "Unable to execute command") + 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) + 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" + break + else: + user_input = console_input + command_name = "human_feedback" + break + + 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: {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: {user_input} " + + 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: + 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 a441a46a..a0afc874 100644 --- a/scripts/memory/__init__.py +++ b/scripts/memory/__init__.py @@ -1,12 +1,20 @@ 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 +supported_memory = ['local'] + try: from memory.redismem import RedisMemory + supported_memory.append('redis') except ImportError: print("Redis not installed. Skipping import.") RedisMemory = None try: from memory.pinecone import PineconeMemory + supported_memory.append('pinecone') except ImportError: print("Pinecone not installed. Skipping import.") PineconeMemory = None @@ -28,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) @@ -36,9 +46,14 @@ def get_memory(cfg, init=False): return memory +def get_supported_memory_backends(): + return supported_memory + + __all__ = [ "get_memory", "LocalCache", "RedisMemory", "PineconeMemory", + "NoMemory" ] diff --git a/scripts/memory/base.py b/scripts/memory/base.py index d7ab7fcf..4dbf6791 100644 --- a/scripts/memory/base.py +++ b/scripts/memory/base.py @@ -1,12 +1,17 @@ """Base class for memory providers.""" import abc -from config import AbstractSingleton +from config import AbstractSingleton, Config import openai +cfg = Config() + def get_ada_embedding(text): text = text.replace("\n", " ") - return openai.Embedding.create(input=[text], model="text-embedding-ada-002")["data"][0]["embedding"] + if cfg.use_azure: + return openai.Embedding.create(input=[text], engine=cfg.get_azure_deployment_id_for_model("text-embedding-ada-002"))["data"][0]["embedding"] + else: + return openai.Embedding.create(input=[text], model="text-embedding-ada-002")["data"][0]["embedding"] class MemoryProviderSingleton(AbstractSingleton): diff --git a/scripts/memory/local.py b/scripts/memory/local.py index 8dc90021..b0afacf6 100644 --- a/scripts/memory/local.py +++ b/scripts/memory/local.py @@ -28,10 +28,20 @@ class LocalCache(MemoryProviderSingleton): def __init__(self, cfg) -> None: self.filename = f"{cfg.memory_index}.json" if os.path.exists(self.filename): - with open(self.filename, 'rb') as f: - loaded = orjson.loads(f.read()) - self.data = CacheContent(**loaded) + try: + with open(self.filename, 'w+b') as f: + file_content = f.read() + if not file_content.strip(): + file_content = b'{}' + f.write(file_content) + + loaded = orjson.loads(file_content) + self.data = CacheContent(**loaded) + except orjson.JSONDecodeError: + print(f"Error: The file '{self.filename}' is not in JSON format.") + self.data = CacheContent() else: + print(f"Warning: The file '{self.filename}' does not exist. Local memory would not be saved to a file.") self.data = CacheContent() def add(self, text: str): @@ -54,8 +64,8 @@ class LocalCache(MemoryProviderSingleton): vector = vector[np.newaxis, :] self.data.embeddings = np.concatenate( [ - vector, self.data.embeddings, + vector, ], axis=0, ) 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 4934ecef..d71b5bca 100644 --- a/scripts/speak.py +++ b/scripts/speak.py @@ -7,9 +7,21 @@ import gtts import threading from threading import Lock, Semaphore +# Default voice IDs +default_voices = ["ErXwobaYiN019PkySvjV", "EXAVITQu4vr4xnSDxMaL"] -# TODO: Nicer names for these ids -voices = ["ErXwobaYiN019PkySvjV", "EXAVITQu4vr4xnSDxMaL"] +# Retrieve custom voice IDs from the Config class +custom_voice_1 = cfg.elevenlabs_voice_1_id +custom_voice_2 = cfg.elevenlabs_voice_2_id + +# Placeholder values that should be treated as empty +placeholders = {"your-voice-id"} + +# Use custom voice IDs if provided and not placeholders, otherwise use default voice IDs +voices = [ + custom_voice_1 if custom_voice_1 and custom_voice_1 not in placeholders else default_voices[0], + custom_voice_2 if custom_voice_2 and custom_voice_2 not in placeholders else default_voices[1] +] tts_headers = { "Content-Type": "application/json", @@ -19,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( @@ -63,8 +76,16 @@ def gtts_speech(text): playsound("speech.mp3", True) os.remove("speech.mp3") -def macos_tts_speech(text): - os.system(f'say "{text}"') + +def macos_tts_speech(text, voice_index=0): + if voice_index == 0: + os.system(f'say "{text}"') + else: + if voice_index == 1: + os.system(f'say -v "Ava (Premium)" "{text}"') + else: + os.system(f'say -v Samantha "{text}"') + def say_text(text, voice_index=0): @@ -82,7 +103,7 @@ def say_text(text, voice_index=0): success = eleven_labs_speech(text, voice_index) if not success: gtts_speech(text) - + queue_semaphore.release() queue_semaphore.acquire(True) diff --git a/scripts/spinner.py b/scripts/spinner.py index df39dbbd..df1f4ddf 100644 --- a/scripts/spinner.py +++ b/scripts/spinner.py @@ -20,7 +20,7 @@ class Spinner: sys.stdout.write(next(self.spinner) + " " + self.message + "\r") sys.stdout.flush() time.sleep(self.delay) - sys.stdout.write('\b' * (len(self.message) + 2)) + sys.stdout.write('\r' + ' ' * (len(self.message) + 2) + '\r') def __enter__(self): """Start the spinner""" diff --git a/scripts/token_counter.py b/scripts/token_counter.py index a28a9868..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. @@ -15,7 +16,7 @@ def count_message_tokens(messages : List[Dict[str, str]], model : str = "gpt-3.5 try: encoding = tiktoken.encoding_for_model(model) except KeyError: - print("Warning: model not found. Using cl100k_base encoding.") + logger.warn("Warning: model not found. Using cl100k_base encoding.") encoding = tiktoken.get_encoding("cl100k_base") if model == "gpt-3.5-turbo": # !Node: gpt-3.5-turbo may change over time. Returning num tokens assuming gpt-3.5-turbo-0301.") @@ -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.py b/tests.py new file mode 100644 index 00000000..4dbfdd46 --- /dev/null +++ b/tests.py @@ -0,0 +1,8 @@ +import unittest + +if __name__ == "__main__": + # Load all tests from the 'scripts/tests' package + suite = unittest.defaultTestLoader.discover('scripts/tests') + + # Run the tests + unittest.TextTestRunner().run(suite) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 00000000..b668c8dc --- /dev/null +++ b/tests/context.py @@ -0,0 +1,5 @@ +import sys +import os + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '../scripts'))) diff --git a/tests/integration/memory_tests.py b/tests/integration/memory_tests.py new file mode 100644 index 00000000..d0c30962 --- /dev/null +++ b/tests/integration/memory_tests.py @@ -0,0 +1,51 @@ +import unittest +import random +import string +import sys +from pathlib import Path +# Add the parent directory of the 'scripts' folder to the Python path +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): + return ''.join(random.choice(string.ascii_letters) for _ in range(length)) + + def setUp(self): + cfg = cfg = Config() + self.cache = LocalCache(cfg) + self.cache.clear() + + # Add example texts to the cache + self.example_texts = [ + 'The quick brown fox jumps over the lazy dog', + 'I love machine learning and natural language processing', + 'The cake is a lie, but the pie is always true', + 'ChatGPT is an advanced AI model for conversation' + ] + + for text in self.example_texts: + self.cache.add(text) + + # Add some random strings to test noise + for _ in range(5): + self.cache.add(self.random_string(10)) + + def test_get_relevant(self): + query = "I'm interested in artificial intelligence and NLP" + k = 3 + relevant_texts = self.cache.get_relevant(query, k) + + print(f"Top {k} relevant texts for the query '{query}':") + for i, text in enumerate(relevant_texts, start=1): + print(f"{i}. {text}") + + self.assertEqual(len(relevant_texts), k) + self.assertIn(self.example_texts[1], relevant_texts) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/local_cache_test.py b/tests/local_cache_test.py new file mode 100644 index 00000000..0352624e --- /dev/null +++ b/tests/local_cache_test.py @@ -0,0 +1,54 @@ +import os +import sys +# Probably a better way: +sys.path.append(os.path.abspath('../scripts')) +from memory.local import LocalCache + + +def MockConfig(): + return type('MockConfig', (object,), { + 'debug_mode': False, + 'continuous_mode': False, + 'speak_mode': False, + 'memory_index': 'auto-gpt', + }) + + +class TestLocalCache(unittest.TestCase): + + def setUp(self): + self.cfg = MockConfig() + self.cache = LocalCache(self.cfg) + + def test_add(self): + text = "Sample text" + self.cache.add(text) + self.assertIn(text, self.cache.data.texts) + + def test_clear(self): + self.cache.clear() + self.assertEqual(self.cache.data, [""]) + + def test_get(self): + text = "Sample text" + self.cache.add(text) + result = self.cache.get(text) + self.assertEqual(result, [text]) + + def test_get_relevant(self): + text1 = "Sample text 1" + text2 = "Sample text 2" + self.cache.add(text1) + self.cache.add(text2) + result = self.cache.get_relevant(text1, 1) + self.assertEqual(result, [text1]) + + def test_get_stats(self): + text = "Sample text" + self.cache.add(text) + stats = self.cache.get_stats() + self.assertEqual(stats, (1, self.cache.data.embeddings.shape)) + + +if __name__ == '__main__': + unittest.main() 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 new file mode 100644 index 00000000..ba8381e1 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,59 @@ +import unittest +from scripts.config import Config + + +class TestConfig(unittest.TestCase): + + def test_singleton(self): + config1 = Config() + config2 = Config() + self.assertIs(config1, config2) + + def test_initial_values(self): + config = Config() + self.assertFalse(config.debug_mode) + self.assertFalse(config.continuous_mode) + self.assertFalse(config.speak_mode) + self.assertEqual(config.fast_llm_model, "gpt-3.5-turbo") + self.assertEqual(config.smart_llm_model, "gpt-4") + self.assertEqual(config.fast_token_limit, 4000) + self.assertEqual(config.smart_token_limit, 8000) + + def test_set_continuous_mode(self): + config = Config() + config.set_continuous_mode(True) + self.assertTrue(config.continuous_mode) + + def test_set_speak_mode(self): + config = Config() + config.set_speak_mode(True) + self.assertTrue(config.speak_mode) + + def test_set_fast_llm_model(self): + config = Config() + config.set_fast_llm_model("gpt-3.5-turbo-test") + self.assertEqual(config.fast_llm_model, "gpt-3.5-turbo-test") + + def test_set_smart_llm_model(self): + config = Config() + config.set_smart_llm_model("gpt-4-test") + self.assertEqual(config.smart_llm_model, "gpt-4-test") + + def test_set_fast_token_limit(self): + config = Config() + config.set_fast_token_limit(5000) + self.assertEqual(config.fast_token_limit, 5000) + + def test_set_smart_token_limit(self): + config = Config() + config.set_smart_token_limit(9000) + self.assertEqual(config.smart_token_limit, 9000) + + def test_set_debug_mode(self): + config = Config() + config.set_debug_mode(True) + self.assertTrue(config.debug_mode) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_json_parser.py b/tests/test_json_parser.py new file mode 100644 index 00000000..c403c73d --- /dev/null +++ b/tests/test_json_parser.py @@ -0,0 +1,113 @@ +import unittest +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 + json_str = '{"name": "John", "age": 30, "city": "New York"}' + obj = fix_and_parse_json(json_str) + self.assertEqual(obj, {"name": "John", "age": 30, "city": "New York"}) + + def test_invalid_json_minor(self): + # Test that an invalid JSON string can be fixed with gpt + json_str = '{"name": "John", "age": 30, "city": "New York",}' + with self.assertRaises(Exception): + fix_and_parse_json(json_str, try_to_fix_with_gpt=False) + + def test_invalid_json_major_with_gpt(self): + # Test that an invalid JSON string raises an error when try_to_fix_with_gpt is False + json_str = 'BEGIN: "name": "John" - "age": 30 - "city": "New York" :END' + with self.assertRaises(Exception): + fix_and_parse_json(json_str, try_to_fix_with_gpt=False) + + def test_invalid_json_major_without_gpt(self): + # Test that a REALLY invalid JSON string raises an error when try_to_fix_with_gpt is False + json_str = 'BEGIN: "name": "John" - "age": 30 - "city": "New York" :END' + # Assert that this raises an exception: + with self.assertRaises(Exception): + fix_and_parse_json(json_str, try_to_fix_with_gpt=False) + + def test_invalid_json_leading_sentence_with_gpt(self): + # Test that a REALLY invalid JSON string raises an error when try_to_fix_with_gpt is False + json_str = """I suggest we start by browsing the repository to find any issues that we can fix. + +{ + "command": { + "name": "browse_website", + "args":{ + "url": "https://github.com/Torantulino/Auto-GPT" + } + }, + "thoughts": + { + "text": "I suggest we start browsing the repository to find any issues that we can fix.", + "reasoning": "Browsing the repository will give us an idea of the current state of the codebase and identify any issues that we can address to improve the repo.", + "plan": "- Look through the repository to find any issues.\n- Investigate any issues to determine what needs to be fixed\n- Identify possible solutions to fix the issues\n- Open Pull Requests with fixes", + "criticism": "I should be careful while browsing so as not to accidentally introduce any new bugs or issues.", + "speak": "I will start browsing the repository to find any issues we can fix." + } +}""" + good_obj = { + "command": { + "name": "browse_website", + "args": { + "url": "https://github.com/Torantulino/Auto-GPT" + } + }, + "thoughts": + { + "text": "I suggest we start browsing the repository to find any issues that we can fix.", + "reasoning": "Browsing the repository will give us an idea of the current state of the codebase and identify any issues that we can address to improve the repo.", + "plan": "- Look through the repository to find any issues.\n- Investigate any issues to determine what needs to be fixed\n- Identify possible solutions to fix the issues\n- Open Pull Requests with fixes", + "criticism": "I should be careful while browsing so as not to accidentally introduce any new bugs or issues.", + "speak": "I will start browsing the repository to find any issues we can fix." + } + } + # Assert that this raises an exception: + self.assertEqual(fix_and_parse_json(json_str, try_to_fix_with_gpt=False), good_obj) + + def test_invalid_json_leading_sentence_with_gpt(self): + # Test that a REALLY invalid JSON string raises an error when try_to_fix_with_gpt is False + json_str = """I will first need to browse the repository (https://github.com/Torantulino/Auto-GPT) and identify any potential bugs that need fixing. I will use the "browse_website" command for this. + +{ + "command": { + "name": "browse_website", + "args":{ + "url": "https://github.com/Torantulino/Auto-GPT" + } + }, + "thoughts": + { + "text": "Browsing the repository to identify potential bugs", + "reasoning": "Before fixing bugs, I need to identify what needs fixing. I will use the 'browse_website' command to analyze the repository.", + "plan": "- Analyze the repository for potential bugs and areas of improvement", + "criticism": "I need to ensure I am thorough and pay attention to detail while browsing the repository.", + "speak": "I am browsing the repository to identify potential bugs." + } +}""" + good_obj = { + "command": { + "name": "browse_website", + "args": { + "url": "https://github.com/Torantulino/Auto-GPT" + } + }, + "thoughts": + { + "text": "Browsing the repository to identify potential bugs", + "reasoning": "Before fixing bugs, I need to identify what needs fixing. I will use the 'browse_website' command to analyze the repository.", + "plan": "- Analyze the repository for potential bugs and areas of improvement", + "criticism": "I need to ensure I am thorough and pay attention to detail while browsing the repository.", + "speak": "I am browsing the repository to identify potential bugs." + } +} + # Assert that this raises an exception: + self.assertEqual(fix_and_parse_json(json_str, try_to_fix_with_gpt=False), good_obj) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/json_tests.py b/tests/unit/json_tests.py similarity index 99% rename from tests/json_tests.py rename to tests/unit/json_tests.py index fdac9c2f..4f326721 100644 --- a/tests/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" } }, @@ -68,8 +69,6 @@ class TestParseJson(unittest.TestCase): # Assert that this raises an exception: self.assertEqual(fix_and_parse_json(json_str, try_to_fix_with_gpt=False), good_obj) - - def test_invalid_json_leading_sentence_with_gpt(self): # Test that a REALLY invalid JSON string raises an error when try_to_fix_with_gpt is False json_str = """I will first need to browse the repository (https://github.com/Torantulino/Auto-GPT) and identify any potential bugs that need fixing. I will use the "browse_website" command for this. @@ -93,7 +92,7 @@ class TestParseJson(unittest.TestCase): good_obj = { "command": { "name": "browse_website", - "args":{ + "args": { "url": "https://github.com/Torantulino/Auto-GPT" } }, @@ -110,6 +109,5 @@ class TestParseJson(unittest.TestCase): self.assertEqual(fix_and_parse_json(json_str, try_to_fix_with_gpt=False), good_obj) - if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_browse_scrape_links.py b/tests/unit/test_browse_scrape_links.py new file mode 100644 index 00000000..2172d1a2 --- /dev/null +++ b/tests/unit/test_browse_scrape_links.py @@ -0,0 +1,118 @@ + +# Generated by CodiumAI + +# Dependencies: +# pip install pytest-mock +import pytest + +from scripts.browse import scrape_links + +""" +Code Analysis + +Objective: +The objective of the 'scrape_links' function is to scrape hyperlinks from a +given URL and return them in a formatted way. + +Inputs: +- url: a string representing the URL to be scraped. + +Flow: +1. Send a GET request to the given URL using the requests library and the user agent header from the config file. +2. Check if the response contains an HTTP error. If it does, return "error". +3. Parse the HTML content of the response using the BeautifulSoup library. +4. Remove any script and style tags from the parsed HTML. +5. Extract all hyperlinks from the parsed HTML using the 'extract_hyperlinks' function. +6. Format the extracted hyperlinks using the 'format_hyperlinks' function. +7. Return the formatted hyperlinks. + +Outputs: +- A list of formatted hyperlinks. + +Additional aspects: +- The function uses the 'requests' and 'BeautifulSoup' libraries to send HTTP +requests and parse HTML content, respectively. +- The 'extract_hyperlinks' function is called to extract hyperlinks from the parsed HTML. +- The 'format_hyperlinks' function is called to format the extracted hyperlinks. +- The function checks for HTTP errors and returns "error" if any are found. +""" + + +class TestScrapeLinks: + + # Tests that the function returns a list of formatted hyperlinks when + # provided with a valid url that returns a webpage with hyperlinks. + def test_valid_url_with_hyperlinks(self): + url = "https://www.google.com" + result = scrape_links(url) + assert len(result) > 0 + assert isinstance(result, list) + assert isinstance(result[0], str) + + # Tests that the function returns correctly formatted hyperlinks when given a valid url. + def test_valid_url(self, mocker): + # Mock the requests.get() function to return a response with sample HTML containing hyperlinks + mock_response = mocker.Mock() + mock_response.status_code = 200 + mock_response.text = "Google" + mocker.patch('requests.get', return_value=mock_response) + + # Call the function with a valid URL + result = scrape_links("https://www.example.com") + + # Assert that the function returns correctly formatted hyperlinks + assert result == ["Google (https://www.google.com)"] + + # Tests that the function returns "error" when given an invalid url. + def test_invalid_url(self, mocker): + # Mock the requests.get() function to return an HTTP error response + mock_response = mocker.Mock() + mock_response.status_code = 404 + mocker.patch('requests.get', return_value=mock_response) + + # Call the function with an invalid URL + result = scrape_links("https://www.invalidurl.com") + + # Assert that the function returns "error" + assert "Error:" in result + + # Tests that the function returns an empty list when the html contains no hyperlinks. + def test_no_hyperlinks(self, mocker): + # Mock the requests.get() function to return a response with sample HTML containing no hyperlinks + mock_response = mocker.Mock() + mock_response.status_code = 200 + mock_response.text = "

No hyperlinks here

" + mocker.patch('requests.get', return_value=mock_response) + + # Call the function with a URL containing no hyperlinks + result = scrape_links("https://www.example.com") + + # Assert that the function returns an empty list + assert result == [] + + # Tests that scrape_links() correctly extracts and formats hyperlinks from + # a sample HTML containing a few hyperlinks. + def test_scrape_links_with_few_hyperlinks(self, mocker): + # Mock the requests.get() function to return a response with a sample HTML containing hyperlinks + mock_response = mocker.Mock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + + """ + mocker.patch('requests.get', return_value=mock_response) + + # Call the function being tested + result = scrape_links("https://www.example.com") + + # Assert that the function returns a list of formatted hyperlinks + assert isinstance(result, list) + assert len(result) == 3 + assert result[0] == "Google (https://www.google.com)" + assert result[1] == "GitHub (https://github.com)" + assert result[2] == "CodiumAI (https://www.codium.ai)" diff --git a/tests/test_browse_scrape_text.py b/tests/unit/test_browse_scrape_text.py similarity index 97% rename from tests/test_browse_scrape_text.py rename to tests/unit/test_browse_scrape_text.py index bfc3b425..9385cde7 100644 --- a/tests/test_browse_scrape_text.py +++ b/tests/unit/test_browse_scrape_text.py @@ -2,7 +2,6 @@ # Generated by CodiumAI import requests -import pytest from scripts.browse import scrape_text @@ -10,7 +9,8 @@ from scripts.browse import scrape_text Code Analysis Objective: -The objective of the "scrape_text" function is to scrape the text content from a given URL and return it as a string, after removing any unwanted HTML tags and scripts. +The objective of the "scrape_text" function is to scrape the text content from +a given URL and return it as a string, after removing any unwanted HTML tags and scripts. Inputs: - url: a string representing the URL of the webpage to be scraped. @@ -34,7 +34,6 @@ Additional aspects: """ - class TestScrapeText: # Tests that scrape_text() returns the expected text when given a valid URL.