diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..f3b2e2dbb5 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,23 @@ +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT=3-bullseye +FROM python:3.8 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 + && apt-get purge -y imagemagick imagemagick-6-common + +# Temporary: Upgrade python packages due to https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-40897 +# They are installed by the base image (python) which does not have the patch. +RUN python3 -m pip install --upgrade setuptools + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..5fefd9c13d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/python:1": "none", + "ghcr.io/devcontainers/features/node:1": "none", + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + } + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python" + } + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.env.template b/.env.template index 733597d911..22bf8d74d9 100644 --- a/.env.template +++ b/.env.template @@ -9,6 +9,8 @@ BROWSE_CHUNK_MAX_LENGTH=8192 BROWSE_SUMMARY_MAX_TOKEN=300 # USER_AGENT - Define the user-agent used by the requests library to browse website (string) # 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" +# AI_SETTINGS_FILE - Specifies which AI Settings file to use (defaults to ai_settings.yaml) +AI_SETTINGS_FILE=ai_settings.yaml ################################################################################ ### LLM PROVIDER @@ -108,6 +110,10 @@ CUSTOM_SEARCH_ENGINE_ID=your-custom-search-engine-id # 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 + ### ELEVENLABS # ELEVENLABS_API_KEY - Eleven Labs API key (Example: my-elevenlabs-api-key) # ELEVENLABS_VOICE_1_ID - Eleven Labs voice 1 ID (Example: my-voice-id-1) diff --git a/.gitignore b/.gitignore index cf6e75df15..b0be8967f2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ last_run_ai_settings.yaml .idea/* auto-gpt.json log.txt +log-ingestion.txt +logs # Coverage reports .coverage diff --git a/Dockerfile b/Dockerfile index 4d264c88c8..e776664e84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,23 @@ +# Use an official Python base image from the Docker Hub FROM python:3.11-slim -ENV PIP_NO_CACHE_DIR=yes -WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY scripts/ . -ENTRYPOINT ["python", "main.py"] + +# Set environment variables +ENV PIP_NO_CACHE_DIR=yes \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +# Create a non-root user and set permissions +RUN useradd --create-home appuser +WORKDIR /home/appuser +RUN chown appuser:appuser /home/appuser +USER appuser + +# Copy the requirements.txt file and install the requirements +COPY --chown=appuser:appuser requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# Copy the application files +COPY --chown=appuser:appuser scripts/ . + +# Set the entrypoint +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index d11219769c..27150fa284 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Your support is greatly appreciated - [Setting up environment variables](#setting-up-environment-variables-1) - [Setting Your Cache Type](#setting-your-cache-type) - [View Memory Usage](#view-memory-usage) + - [๐Ÿง  Memory pre-seeding](#memory-pre-seeding) - [๐Ÿ’€ Continuous Mode โš ๏ธ](#-continuous-mode-๏ธ) - [GPT3.5 ONLY Mode](#gpt35-only-mode) - [๐Ÿ–ผ Image Generation](#-image-generation) @@ -65,13 +66,16 @@ Your support is greatly appreciated ## ๐Ÿ“‹ Requirements -- [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) +- environments(just choose one) + - [vscode + devcontainer](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): It has been configured in the .devcontainer folder and can be used directly + - [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) - [OpenAI API key](https://platform.openai.com/account/api-keys) -- [PINECONE API key](https://www.pinecone.io/) + Optional: -- [ElevenLabs Key](https://elevenlabs.io/) (If you want the AI to speak) +- [PINECONE API key](https://www.pinecone.io/) (If you want Pinecone backed memory) +- ElevenLabs Key (If you want the AI to speak) ## ๐Ÿ’พ Installation @@ -122,8 +126,8 @@ pip install -r requirements.txt 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. +2. After each of action, enter 'y' to authorise command, 'y -N' to run N continuous commands, 'n' to exit program, or enter additional feedback for the AI. + ### Logs @@ -134,6 +138,14 @@ To output debug logs: ``` python scripts/main.py --debug ``` +### Command Line Arguments +Here are some common arguments you can use when running Auto-GPT: +> Replace anything in angled brackets (<>) to a value you want to specify +* `python scripts/main.py --help` to see a list of all available command line arguments. +* `python scripts/main.py --ai-settings ` to run Auto-GPT with a different AI Settings file. +* `python scripts/main.py --use-memory ` to specify one of 3 memory backends: `local`, `redis`, `pinecone` or 'no_memory'. + +> **NOTE**: There are shorthands for some of these flags, for example `-m` for `--use-memory`. Use `python scripts/main.py --help` for more information ## ๐Ÿ—ฃ๏ธ Speech Mode @@ -154,9 +166,10 @@ To use the `google_official_search` command, you need to set up your Google API 4. Go to the [APIs & Services Dashboard](https://console.cloud.google.com/apis/dashboard) and click "Enable APIs and Services". Search for "Custom Search API" and click on it, then click "Enable". 5. Go to the [Credentials](https://console.cloud.google.com/apis/credentials) page and click "Create Credentials". Choose "API Key". 6. Copy the API key and set it as an environment variable named `GOOGLE_API_KEY` on your machine. See setting up environment variables below. -7. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page and click "Add". -8. Set up your search engine by following the prompts. You can choose to search the entire web or specific sites. -9. Once you've created your search engine, click on "Control Panel" and then "Basics". Copy the "Search engine ID" and set it as an environment variable named `CUSTOM_SEARCH_ENGINE_ID` on your machine. See setting up environment variables below. +7. [Enable](https://console.developers.google.com/apis/api/customsearch.googleapis.com) the Custom Search API on your project. (Might need to wait few minutes to propagate) +8. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page and click "Add". +9. Set up your search engine by following the prompts. You can choose to search the entire web or specific sites. +10. Once you've created your search engine, click on "Control Panel" and then "Basics". Copy the "Search engine ID" and set it as an environment variable named `CUSTOM_SEARCH_ENGINE_ID` on your machine. See setting up environment variables below. _Remember that your free daily custom search quota allows only up to 100 searches. To increase this limit, you need to assign a billing account to the project to profit from up to 10K daily searches._ @@ -225,7 +238,10 @@ Pinecone enables the storage of vast amounts of vector-based memory, allowing fo ### Setting up environment variables -Simply set them in the `.env` file. +In the `.env` file set: +- `PINECONE_API_KEY` +- `PINECONE_ENV` (something like: us-east4-gcp) +- `MEMORY_BACKEND=pinecone` Alternatively, you can set them from the command line (advanced): @@ -234,7 +250,7 @@ For Windows Users: ``` setx PINECONE_API_KEY "YOUR_PINECONE_API_KEY" setx PINECONE_ENV "Your pinecone region" # something like: us-east4-gcp - +setx MEMORY_BACKEND "pinecone" ``` For macOS and Linux users: @@ -242,7 +258,7 @@ For macOS and Linux users: ``` export PINECONE_API_KEY="YOUR_PINECONE_API_KEY" export PINECONE_ENV="Your pinecone region" # something like: us-east4-gcp - +export MEMORY_BACKEND="pinecone" ``` ## Setting Your Cache Type @@ -259,6 +275,52 @@ To switch to either, change the `MEMORY_BACKEND` env variable to the value that 1. View memory usage by using the `--debug` flag :) + +## ๐Ÿง  Memory pre-seeding + +``` +# python scripts/data_ingestion.py -h +usage: data_ingestion.py [-h] (--file FILE | --dir DIR) [--init] [--overlap OVERLAP] [--max_length MAX_LENGTH] + +Ingest a file or a directory with multiple files into memory. Make sure to set your .env before running this script. + +options: + -h, --help show this help message and exit + --file FILE The file to ingest. + --dir DIR The directory containing the files to ingest. + --init Init the memory and wipe its content (default: False) + --overlap OVERLAP The overlap size between chunks when ingesting files (default: 200) + --max_length MAX_LENGTH The max_length of each chunk when ingesting files (default: 4000 + +# python scripts/data_ingestion.py --dir seed_data --init --overlap 200 --max_length 1000 +``` + +This script located at scripts/data_ingestion.py, allows you to ingest files into memory and pre-seed it before running Auto-GPT. + +Memory pre-seeding is a technique that involves ingesting relevant documents or data into the AI's memory so that it can use this information to generate more informed and accurate responses. + +To pre-seed the memory, the content of each document is split into chunks of a specified maximum length with a specified overlap between chunks, and then each chunk is added to the memory backend set in the .env file. When the AI is prompted to recall information, it can then access those pre-seeded memories to generate more informed and accurate responses. + +This technique is particularly useful when working with large amounts of data or when there is specific information that the AI needs to be able to access quickly. +By pre-seeding the memory, the AI can retrieve and use this information more efficiently, saving time, API call and improving the accuracy of its responses. + +You could for example download the documentation of an API, a Github repository, etc. and ingest it into memory before running Auto-GPT. + +โš ๏ธ If you use Redis as your memory, make sure to run Auto-GPT with the WIPE_REDIS_ON_START set to False in your .env file. + +โš ๏ธFor other memory backend, we currently forcefully wipe the memory when starting Auto-GPT. To ingest data with those memory backend, you can call the data_ingestion.py script anytime during an Auto-GPT run. + +Memories will be available to the AI immediately as they are ingested, even if ingested while Auto-GPT is running. + +In the example above, the script initializes the memory, ingests all files within the seed_data directory into memory with an overlap between chunks of 200 and a maximum length of each chunk of 4000. +Note that you can also use the --file argument to ingest a single file into memory and that the script will only ingest files within the auto_gpt_workspace directory. + +You can adjust the max_length and overlap parameters to fine-tune the way the docuents are presented to the AI when it "recall" that memory: + +- Adjusting the overlap value allows the AI to access more contextual information from each chunk when recalling information, but will result in more chunks being created and therefore increase memory backend usage and OpenAI API requests. +- Reducing the max_length value will create more chunks, which can save prompt tokens by allowing for more message history in the context, but will also increase the number of chunks. +- Increasing the max_length value will provide the AI with more contextual information from each chunk, reducing the number of chunks created and saving on OpenAI API requests. However, this may also use more prompt tokens and decrease the overall context available to the AI. + ## ๐Ÿ’€ Continuous Mode โš ๏ธ Run the AI **without** user authorisation, 100% automated. @@ -357,4 +419,4 @@ 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/scripts/ai_config.py b/scripts/ai_config.py index 4eb076ef2d..89a4e07eaa 100644 --- a/scripts/ai_config.py +++ b/scripts/ai_config.py @@ -70,8 +70,8 @@ class AIConfig: """ config = {"ai_name": self.ai_name, "ai_role": self.ai_role, "ai_goals": self.ai_goals} - with open(config_file, "w") as file: - yaml.dump(config, file) + with open(config_file, "w", encoding='utf-8') as file: + yaml.dump(config, file, allow_unicode=True) def construct_full_prompt(self) -> str: """ diff --git a/scripts/call_ai_function.py b/scripts/call_ai_function.py index 6f1d6ceee8..940eacfe0f 100644 --- a/scripts/call_ai_function.py +++ b/scripts/call_ai_function.py @@ -13,7 +13,7 @@ def call_ai_function(function, args, description, model=None): model = cfg.smart_llm_model # For each arg, if any are None, convert to "None": args = [str(arg) if arg is not None else "None" for arg in args] - # parse args to comma seperated string + # parse args to comma separated string args = ", ".join(args) messages = [ { diff --git a/scripts/config.py b/scripts/config.py index a856265a7b..dc791d8d0c 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -38,7 +38,9 @@ class Config(metaclass=Singleton): self.continuous_mode = False self.continuous_limit = 0 self.speak_mode = False + self.skip_reprompt = False + self.ai_settings_file = os.getenv("AI_SETTINGS_FILE", "ai_settings.yaml") self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo") self.smart_llm_model = os.getenv("SMART_LLM_MODEL", "gpt-4") self.fast_token_limit = int(os.getenv("FAST_TOKEN_LIMIT", 4000)) @@ -64,6 +66,9 @@ class Config(metaclass=Singleton): self.use_mac_os_tts = False self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS") + self.use_brian_tts = False + self.use_brian_tts = os.getenv("USE_BRIAN_TTS") + self.google_api_key = os.getenv("GOOGLE_API_KEY") self.custom_search_engine_id = os.getenv("CUSTOM_SEARCH_ENGINE_ID") diff --git a/scripts/data_ingestion.py b/scripts/data_ingestion.py new file mode 100644 index 0000000000..9addc34bb2 --- /dev/null +++ b/scripts/data_ingestion.py @@ -0,0 +1,70 @@ +import argparse +import logging +from config import Config +from memory import get_memory +from file_operations import ingest_file, search_files + +cfg = Config() + + +def configure_logging(): + logging.basicConfig(filename='log-ingestion.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-Ingestion') + + +def ingest_directory(directory, memory, args): + """ + Ingest all files in a directory by calling the ingest_file function for each file. + + :param directory: The directory containing the files to ingest + :param memory: An object with an add() method to store the chunks in memory + """ + try: + files = search_files(directory) + for file in files: + ingest_file(file, memory, args.max_length, args.overlap) + except Exception as e: + print(f"Error while ingesting directory '{directory}': {str(e)}") + + +def main(): + logger = configure_logging() + + parser = argparse.ArgumentParser(description="Ingest a file or a directory with multiple files into memory. Make sure to set your .env before running this script.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--file", type=str, help="The file to ingest.") + group.add_argument("--dir", type=str, help="The directory containing the files to ingest.") + parser.add_argument("--init", action='store_true', help="Init the memory and wipe its content (default: False)", default=False) + parser.add_argument("--overlap", type=int, help="The overlap size between chunks when ingesting files (default: 200)", default=200) + parser.add_argument("--max_length", type=int, help="The max_length of each chunk when ingesting files (default: 4000)", default=4000) + + args = parser.parse_args() + + # Initialize memory + memory = get_memory(cfg, init=args.init) + print('Using memory of type: ' + memory.__class__.__name__) + + if args.file: + try: + ingest_file(args.file, memory, args.max_length, args.overlap) + print(f"File '{args.file}' ingested successfully.") + except Exception as e: + logger.error(f"Error while ingesting file '{args.file}': {str(e)}") + print(f"Error while ingesting file '{args.file}': {str(e)}") + elif args.dir: + try: + ingest_directory(args.dir, memory, args) + print(f"Directory '{args.dir}' ingested successfully.") + except Exception as e: + logger.error(f"Error while ingesting directory '{args.dir}': {str(e)}") + print(f"Error while ingesting directory '{args.dir}': {str(e)}") + else: + print("Please provide either a file path (--file) or a directory name (--dir) inside the auto_gpt_workspace directory as input.") + + +if __name__ == "__main__": + main() diff --git a/scripts/file_operations.py b/scripts/file_operations.py index 3bbe9da60e..1a072561b4 100644 --- a/scripts/file_operations.py +++ b/scripts/file_operations.py @@ -20,6 +20,29 @@ def safe_join(base, *paths): return norm_new_path +def split_file(content, max_length=4000, overlap=0): + """ + Split text into chunks of a specified maximum length with a specified overlap + between chunks. + + :param text: The input text to be split into chunks + :param max_length: The maximum length of each chunk, default is 4000 (about 1k token) + :param overlap: The number of overlapping characters between chunks, default is no overlap + :return: A generator yielding chunks of text + """ + start = 0 + content_length = len(content) + + while start < content_length: + end = start + max_length + if end + overlap < content_length: + chunk = content[start:end+overlap] + else: + chunk = content[start:content_length] + yield chunk + start += max_length - overlap + + def read_file(filename): """Read a file and return the contents""" try: @@ -31,6 +54,37 @@ def read_file(filename): return "Error: " + str(e) +def ingest_file(filename, memory, max_length=4000, overlap=200): + """ + Ingest a file by reading its content, splitting it into chunks with a specified + maximum length and overlap, and adding the chunks to the memory storage. + + :param filename: The name of the file to ingest + :param memory: An object with an add() method to store the chunks in memory + :param max_length: The maximum length of each chunk, default is 4000 + :param overlap: The number of overlapping characters between chunks, default is 200 + """ + try: + print(f"Working with file {filename}") + content = read_file(filename) + content_length = len(content) + print(f"File length: {content_length} characters") + + chunks = list(split_file(content, max_length=max_length, overlap=overlap)) + + num_chunks = len(chunks) + for i, chunk in enumerate(chunks): + print(f"Ingesting chunk {i + 1} / {num_chunks} into memory") + memory_to_add = f"Filename: {filename}\n" \ + f"Content part#{i + 1}/{num_chunks}: {chunk}" + + memory.add(memory_to_add) + + print(f"Done ingesting {num_chunks} chunks from {filename}.") + except Exception as e: + print(f"Error while ingesting file '{filename}': {str(e)}") + + def write_to_file(filename, text): """Write text to a file""" try: diff --git a/scripts/logger.py b/scripts/logger.py index 91bdb6f605..4c7e588f20 100644 --- a/scripts/logger.py +++ b/scripts/logger.py @@ -24,7 +24,8 @@ 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') + this_files_dir_path = os.path.dirname(__file__) + log_dir = os.path.join(this_files_dir_path, '../logs') if not os.path.exists(log_dir): os.makedirs(log_dir) diff --git a/scripts/main.py b/scripts/main.py index 466f50dd1c..a12f9c7f52 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -129,64 +129,14 @@ def print_assistant_thoughts(assistant_reply): logger.error("Error: \n", call_stack) -def load_variables(config_file="config.yaml"): - """Load variables from yaml file if it exists, otherwise prompt the user for input""" - try: - with open(config_file) as file: - config = yaml.load(file, Loader=yaml.FullLoader) - ai_name = config.get("ai_name") - ai_role = config.get("ai_role") - ai_goals = config.get("ai_goals") - except FileNotFoundError: - ai_name = "" - ai_role = "" - ai_goals = [] - - # Prompt the user for input if config file is missing or empty values - if not ai_name: - ai_name = utils.clean_input("Name your AI: ") - if ai_name == "": - ai_name = "Entrepreneur-GPT" - - if not ai_role: - ai_role = utils.clean_input(f"{ai_name} is: ") - if ai_role == "": - ai_role = "an AI designed to autonomously develop and run businesses with the sole goal of increasing your net worth." - - if not ai_goals: - print("Enter up to 5 goals for your AI: ") - print("For example: \nIncrease net worth, Grow Twitter Account, Develop and manage multiple businesses autonomously'") - print("Enter nothing to load defaults, enter nothing when finished.") - ai_goals = [] - for i in range(5): - ai_goal = utils.clean_input(f"Goal {i+1}: ") - if ai_goal == "": - break - ai_goals.append(ai_goal) - if len(ai_goals) == 0: - ai_goals = ["Increase net worth", "Grow Twitter Account", "Develop and manage multiple businesses autonomously"] - - # Save variables to yaml file - config = {"ai_name": ai_name, "ai_role": ai_role, "ai_goals": ai_goals} - with open(config_file, "w") as file: - documents = yaml.dump(config, file) - - prompt = 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" - for i, goal in enumerate(ai_goals): - full_prompt += f"{i+1}. {goal}\n" - - full_prompt += f"\n\n{prompt}" - return full_prompt - - def construct_prompt(): """Construct the prompt for the AI to respond to""" - config = AIConfig.load() - if config.ai_name: + config = AIConfig.load(cfg.ai_settings_file) + if cfg.skip_reprompt and config.ai_name: + logger.typewriter_log("Name :", Fore.GREEN, config.ai_name) + logger.typewriter_log("Role :", Fore.GREEN, config.ai_role) + logger.typewriter_log("Goals:", Fore.GREEN, config.ai_goals) + elif config.ai_name: logger.typewriter_log( f"Welcome back! ", Fore.GREEN, @@ -274,13 +224,15 @@ def parse_arguments(): 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', '-c', 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') + parser.add_argument('--skip-reprompt', '-y', dest='skip_reprompt', action='store_true', help='Skips the re-prompting messages at the beginning of the script') + parser.add_argument('--ai-settings', '-C', dest='ai_settings_file', help="Specifies which ai_settings.yaml file to use, will also automatically skip the re-prompt.") args = parser.parse_args() if args.debug: @@ -318,10 +270,6 @@ def parse_arguments(): logger.typewriter_log("GPT4 Only Mode: ", Fore.GREEN, "ENABLED") cfg.set_fast_llm_model(cfg.smart_llm_model) - if args.debug: - logger.typewriter_log("Debug Mode: ", Fore.GREEN, "ENABLED") - cfg.set_debug_mode(True) - if args.memory_type: supported_memory = get_supported_memory_backends() chosen = args.memory_type @@ -331,6 +279,24 @@ def parse_arguments(): else: cfg.memory_backend = chosen + if args.skip_reprompt: + logger.typewriter_log("Skip Re-prompt: ", Fore.GREEN, "ENABLED") + cfg.skip_reprompt = True + + if args.ai_settings_file: + file = args.ai_settings_file + + # Validate file + (validated, message) = utils.validate_yaml_file(file) + if not validated: + logger.typewriter_log("FAILED FILE VALIDATION", Fore.RED, message) + logger.double_check() + exit(1) + + logger.typewriter_log("Using AI Settings File:", Fore.GREEN, file) + cfg.ai_settings_file = file + cfg.skip_reprompt = True + def main(): global ai_name, memory @@ -351,110 +317,148 @@ def main(): # 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 + agent = Agent( + ai_name=ai_name, + memory=memory, + full_message_history=full_message_history, + next_action_count=next_action_count, + prompt=prompt, + user_input=user_input + ) + agent.start_interaction_loop() - # 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) +class Agent: + """Agent class for interacting with Auto-GPT. - # 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)) + Attributes: + ai_name: The name of the agent. + memory: The memory object to use. + full_message_history: The full message history. + next_action_count: The number of actions to execute. + prompt: The prompt to use. + user_input: The user input. - 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 + """ + def __init__(self, + ai_name, + memory, + full_message_history, + next_action_count, + prompt, + user_input): + self.ai_name = ai_name + self.memory = memory + self.full_message_history = full_message_history + self.next_action_count = next_action_count + self.prompt = prompt + self.user_input = user_input - if user_input == "GENERATE NEXT COMMAND JSON": - logger.typewriter_log( - "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", - Fore.MAGENTA, - "") - elif user_input == "EXIT": - print("Exiting...", flush=True) + def start_interaction_loop(self): + # 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 - 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 + # Send message to AI, get response + with Spinner("Thinking... "): + assistant_reply = chat.chat_with_ai( + self.prompt, + self.user_input, + self.full_message_history, + self.memory, + cfg.fast_token_limit) # TODO: This hardcodes the model to use GPT3.5. Make this an argument - 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)) - 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 not cfg.continuous_mode and self.next_action_count == 0: + ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### + # Get key press: Prompt the user to press enter to continue or escape + # to exit + self.user_input = "" + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") + print( + f"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 'n' to exit program, or enter feedback for {self.ai_name}...", + flush=True) + while True: + console_input = utils.clean_input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) + if console_input.lower().rstrip() == "y": + self.user_input = "GENERATE NEXT COMMAND JSON" + break + elif console_input.lower().startswith("y -"): + try: + self.next_action_count = abs(int(console_input.split(" ")[1])) + self.user_input = "GENERATE NEXT COMMAND JSON" + except ValueError: + print("Invalid input format. Please enter 'y -n' where n is the number of continuous tasks.") + continue + break + elif console_input.lower() == "n": + self.user_input = "EXIT" + break + else: + self.user_input = console_input + command_name = "human_feedback" + break + + if self.user_input == "GENERATE NEXT COMMAND JSON": + logger.typewriter_log( + "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", + Fore.MAGENTA, + "") + elif self.user_input == "EXIT": + print("Exiting...", flush=True) + break + else: + # Print command + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}") + + # Execute command + if command_name is not None and command_name.lower().startswith("error"): + result = f"Command {command_name} threw the following error: " + arguments + elif command_name == "human_feedback": + result = f"Human feedback: {self.user_input}" + else: + result = f"Command {command_name} returned: {cmd.execute_command(command_name, arguments)}" + if self.next_action_count > 0: + self.next_action_count -= 1 + + memory_to_add = f"Assistant Reply: {assistant_reply} " \ + f"\nResult: {result} " \ + f"\nHuman Feedback: {self.user_input} " + + self.memory.add(memory_to_add) + + # Check if there's a result from the command append it to the message + # history + if result is not None: + self.full_message_history.append(chat.create_chat_message("system", result)) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) + else: + self.full_message_history.append( + chat.create_chat_message( + "system", "Unable to execute command")) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, "Unable to execute command") if __name__ == "__main__": diff --git a/scripts/memory/__init__.py b/scripts/memory/__init__.py index a0afc874e2..9b53d8d29a 100644 --- a/scripts/memory/__init__.py +++ b/scripts/memory/__init__.py @@ -3,7 +3,7 @@ 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'] +supported_memory = ['local', 'no_memory'] try: from memory.redismem import RedisMemory diff --git a/scripts/speak.py b/scripts/speak.py index 7a17873c5e..3afa591dd5 100644 --- a/scripts/speak.py +++ b/scripts/speak.py @@ -53,6 +53,24 @@ def eleven_labs_speech(text, voice_index=0): return False +def brian_speech(text): + """Speak text using Brian with the streamelements API""" + tts_url = f"https://api.streamelements.com/kappa/v2/speech?voice=Brian&text={text}" + response = requests.get(tts_url) + + if response.status_code == 200: + with mutex_lock: + with open("speech.mp3", "wb") as f: + f.write(response.content) + playsound("speech.mp3") + os.remove("speech.mp3") + return True + else: + print("Request failed with status code:", response.status_code) + print("Response content:", response.content) + return False + + def gtts_speech(text): tts = gtts.gTTS(text) with mutex_lock: @@ -76,7 +94,11 @@ def say_text(text, voice_index=0): def speak(): if not cfg.elevenlabs_api_key: if cfg.use_mac_os_tts == 'True': - macos_tts_speech(text, voice_index) + macos_tts_speech(text) + elif cfg.use_brian_tts == 'True': + success = brian_speech(text) + if not success: + gtts_speech(text) else: gtts_speech(text) else: diff --git a/scripts/spinner.py b/scripts/spinner.py index df1f4ddf93..d232152926 100644 --- a/scripts/spinner.py +++ b/scripts/spinner.py @@ -17,10 +17,10 @@ class Spinner: def spin(self): """Spin the spinner""" while self.running: - sys.stdout.write(next(self.spinner) + " " + self.message + "\r") + sys.stdout.write(f"{next(self.spinner)} {self.message}\r") sys.stdout.flush() time.sleep(self.delay) - sys.stdout.write('\r' + ' ' * (len(self.message) + 2) + '\r') + sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r") def __enter__(self): """Start the spinner""" @@ -32,5 +32,5 @@ class Spinner: """Stop the spinner""" self.running = False self.spinner_thread.join() - sys.stdout.write('\r' + ' ' * (len(self.message) + 2) + '\r') + sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r") sys.stdout.flush() diff --git a/scripts/utils.py b/scripts/utils.py index 5039796fbd..7521df298e 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,3 +1,7 @@ +import yaml +from colorama import Fore + + def clean_input(prompt: str=''): try: return input(prompt) @@ -6,3 +10,14 @@ def clean_input(prompt: str=''): print("Quitting...") exit(0) + +def validate_yaml_file(file: str): + try: + with open(file) as file: + yaml.load(file, Loader=yaml.FullLoader) + except FileNotFoundError: + return (False, f"The file {Fore.CYAN}`{file}`{Fore.RESET} wasn't found") + except yaml.YAMLError as e: + return (False, f"There was an issue while trying to read with your AI Settings file: {e}") + + return (True, f"Successfully validated {Fore.CYAN}`{file}`{Fore.RESET}!")