diff --git a/.env.template b/.env.template index 525cd61c..01735615 100644 --- a/.env.template +++ b/.env.template @@ -2,13 +2,14 @@ PINECONE_API_KEY=your-pinecone-api-key PINECONE_ENV=your-pinecone-region 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" +SMART_LLM_MODEL=gpt-4 +FAST_LLM_MODEL=gpt-3.5-turbo GOOGLE_API_KEY= CUSTOM_SEARCH_ENGINE_ID= USE_AZURE=False -OPENAI_API_BASE=your-base-url-for-azure -OPENAI_API_VERSION=api-version-for-azure -OPENAI_DEPLOYMENT_ID=deployment-id-for-azure +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 IMAGE_PROVIDER=dalle -HUGGINGFACE_API_TOKEN= \ No newline at end of file +HUGGINGFACE_API_TOKEN= +USE_MAC_OS_TTS=False diff --git a/.gitignore b/.gitignore index 0edd3047..2d603801 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ outputs/* ai_settings.yaml .vscode auto-gpt.json +log.txt diff --git a/README.md b/README.md index 118131e4..749c8791 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Your support is greatly appreciated ## 📋 Requirements - [Python 3.8 or later](https://www.tutorialspoint.com/how-to-install-python-in-windows) - OpenAI API key -- PINECONE API key +- [PINECONE API key](https://www.pinecone.io/) Optional: - ElevenLabs Key (If you want the AI to speak) @@ -92,8 +92,8 @@ 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. - Obtain your OpenAI API key from: https://platform.openai.com/account/api-keys. - - Obtain your ElevenLabs API key from: https://beta.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_API_BASE`, `OPENAI_API_VERSION` and `OPENAI_DEPLOYMENT_ID` values as explained here: https://pypi.org/project/openai/ in the `Microsoft Azure Endpoints` section + - 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 ## 🔧 Usage @@ -179,8 +179,7 @@ MEMORY_INDEX=whatever ## 🌲 Pinecone API Key Setup -Pinecone enable a vector based memory so a vast memory can be stored and only relevant memories -are loaded for the agent at any given time. +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. 2. Choose the `Starter` plan to avoid being charged. diff --git a/scripts/ai_config.py b/scripts/ai_config.py index 8fabf3dc..2a4854cb 100644 --- a/scripts/ai_config.py +++ b/scripts/ai_config.py @@ -34,7 +34,7 @@ class AIConfig: @classmethod def load(cls: object, config_file: str=SAVE_FILE) -> object: """ - Returns class object with parameters (ai_name, ai_role, ai_goals) loaded from yaml file if yaml file exists, + Returns class object with parameters (ai_name, ai_role, ai_goals) loaded from yaml file if yaml file exists, else returns class with no parameters. Parameters: @@ -42,7 +42,7 @@ 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): A instance of given cls object """ try: @@ -61,11 +61,11 @@ class AIConfig: """ Saves the class parameters to the specified file yaml file path as a yaml file. - Parameters: + Parameters: config_file(str): The path to the config yaml file. DEFAULT: "../ai_settings.yaml" Returns: - None + None """ config = {"ai_name": self.ai_name, "ai_role": self.ai_role, "ai_goals": self.ai_goals} @@ -76,7 +76,7 @@ class AIConfig: """ Returns a prompt to the user with the class information in an organized fashion. - Parameters: + Parameters: None Returns: @@ -92,4 +92,3 @@ class AIConfig: full_prompt += f"\n\n{data.load_prompt()}" return full_prompt - diff --git a/scripts/ai_functions.py b/scripts/ai_functions.py index dc774b37..8ad77441 100644 --- a/scripts/ai_functions.py +++ b/scripts/ai_functions.py @@ -27,7 +27,7 @@ def evaluate_code(code: str) -> List[str]: def improve_code(suggestions: List[str], code: str) -> str: """ - A function that takes in code and suggestions and returns a response from create chat completion api call. + A function that takes in code and suggestions and returns a response from create chat completion api call. Parameters: suggestions (List): A list of suggestions around what needs to be improved. diff --git a/scripts/browse.py b/scripts/browse.py index 64a25ec9..c3fc0662 100644 --- a/scripts/browse.py +++ b/scripts/browse.py @@ -27,11 +27,20 @@ def make_request(url, timeout=10): except requests.exceptions.RequestException as e: return "Error: " + str(e) +# 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 scrape_text(url): """Scrape text from a webpage""" # 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" # Validate the input URL if not is_valid_url(url): @@ -155,4 +164,4 @@ def summarize_text(text, question): max_tokens=300, ) - return final_summary \ No newline at end of file + return final_summary diff --git a/scripts/chat.py b/scripts/chat.py index c00e4d4a..23e5b501 100644 --- a/scripts/chat.py +++ b/scripts/chat.py @@ -63,10 +63,10 @@ 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}") - + send_token_limit = token_limit - 1000 relevant_memory = permanent_memory.get_relevant(str(full_message_history[-5:]), 10) diff --git a/scripts/commands.py b/scripts/commands.py index 073ecc56..ce5d04ff 100644 --- a/scripts/commands.py +++ b/scripts/commands.py @@ -42,9 +42,6 @@ def get_command(response): # Use an empty dictionary if 'args' field is not present in 'command' object arguments = command.get("args", {}) - if not arguments: - arguments = {} - return command_name, arguments except json.decoder.JSONDecodeError: return "Error:", "Invalid JSON" diff --git a/scripts/config.py b/scripts/config.py index 1eb74b2b..24911bce 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -33,11 +33,11 @@ class Config(metaclass=Singleton): def __init__(self): """Initialize the Config class""" - self.debug = False + self.debug_mode = False self.continuous_mode = False self.speak_mode = False - self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo") + 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)) self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 8000)) @@ -46,15 +46,18 @@ class Config(metaclass=Singleton): self.use_azure = False self.use_azure = os.getenv("USE_AZURE") == 'True' if self.use_azure: - self.openai_api_base = os.getenv("OPENAI_API_BASE") - self.openai_api_version = os.getenv("OPENAI_API_VERSION") - self.openai_deployment_id = os.getenv("OPENAI_DEPLOYMENT_ID") + 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" openai.api_base = self.openai_api_base openai.api_version = self.openai_api_version self.elevenlabs_api_key = os.getenv("ELEVENLABS_API_KEY") + self.use_mac_os_tts = False + self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS") + self.google_api_key = os.getenv("GOOGLE_API_KEY") self.custom_search_engine_id = os.getenv("CUSTOM_SEARCH_ENGINE_ID") @@ -86,9 +89,6 @@ class Config(metaclass=Singleton): """Set the speak mode value.""" self.speak_mode = value - def set_debug_mode(self, value: bool): - self.debug_mode = value - def set_fast_llm_model(self, value: str): """Set the fast LLM model value.""" self.fast_llm_model = value @@ -131,4 +131,4 @@ class Config(metaclass=Singleton): def set_debug_mode(self, value: bool): """Set the debug mode value.""" - self.debug = value + self.debug_mode = value diff --git a/scripts/file_operations.py b/scripts/file_operations.py index 1b87cc28..c6066ef9 100644 --- a/scripts/file_operations.py +++ b/scripts/file_operations.py @@ -24,7 +24,7 @@ def read_file(filename): """Read a file and return the contents""" try: filepath = safe_join(working_directory, filename) - with open(filepath, "r") as f: + with open(filepath, "r", encoding='utf-8') as f: content = f.read() return content except Exception as e: diff --git a/scripts/json_parser.py b/scripts/json_parser.py index 1fd68244..8c17dfa2 100644 --- a/scripts/json_parser.py +++ b/scripts/json_parser.py @@ -71,11 +71,11 @@ def fix_and_parse_json( 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: function_string = "def fix_json(json_str: str, schema:str=None) -> str:" args = [f"'''{json_str}'''", f"'''{schema}'''"] diff --git a/scripts/json_utils.py b/scripts/json_utils.py index b3ffe4b9..80aab192 100644 --- a/scripts/json_utils.py +++ b/scripts/json_utils.py @@ -76,7 +76,7 @@ def balance_braces(json_string: str) -> str: json.loads(json_string) return json_string except json.JSONDecodeError as e: - raise e + pass def fix_invalid_escape(json_str: str, error_message: str) -> str: @@ -88,7 +88,7 @@ def fix_invalid_escape(json_str: str, error_message: str) -> str: json.loads(json_str) return json_str except json.JSONDecodeError as e: - if cfg.debug: + if cfg.debug_mode: print('json loads error - fix invalid escape', e) error_message = str(e) return json_str @@ -103,12 +103,12 @@ def correct_json(json_str: str) -> str: """ try: - if cfg.debug: + if cfg.debug_mode: print("json", json_str) json.loads(json_str) return json_str except json.JSONDecodeError as e: - if cfg.debug: + if cfg.debug_mode: print('json loads error', e) error_message = str(e) if error_message.startswith('Invalid \\escape'): @@ -119,7 +119,7 @@ def correct_json(json_str: str) -> str: json.loads(json_str) return json_str except json.JSONDecodeError as e: - if cfg.debug: + if cfg.debug_mode: print('json loads error - add quotes', e) error_message = str(e) if balanced_str := balance_braces(json_str): diff --git a/scripts/main.py b/scripts/main.py index 3dfcaa15..d84e1508 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -1,6 +1,7 @@ import json import random import commands as cmd +import utils from memory import get_memory import data import chat @@ -8,17 +9,24 @@ from colorama import Fore, Style from spinner import Spinner import time import speak -from enum import Enum, auto -import sys from config import Config from json_parser import fix_and_parse_json from ai_config import AIConfig import traceback import yaml import argparse +import logging 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: @@ -29,7 +37,6 @@ def check_openai_api_key(): print("You can get your key from https://beta.openai.com/account/api-keys") exit(1) - def print_to_console( title, title_color, @@ -39,10 +46,12 @@ def print_to_console( 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() @@ -133,12 +142,12 @@ def load_variables(config_file="config.yaml"): # Prompt the user for input if config file is missing or empty values if not ai_name: - ai_name = input("Name your AI: ") + ai_name = utils.clean_input("Name your AI: ") if ai_name == "": ai_name = "Entrepreneur-GPT" if not ai_role: - ai_role = input(f"{ai_name} is: ") + 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." @@ -148,7 +157,7 @@ def load_variables(config_file="config.yaml"): print("Enter nothing to load defaults, enter nothing when finished.") ai_goals = [] for i in range(5): - ai_goal = input(f"Goal {i+1}: ") + ai_goal = utils.clean_input(f"Goal {i+1}: ") if ai_goal == "": break ai_goals.append(ai_goal) @@ -161,7 +170,7 @@ def load_variables(config_file="config.yaml"): 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 an LLM and pursue simple strategies with no legal complications.""" + prompt_start = """Your decisions must always be made independently without seeking user assistance. Play to your strengths as a LLM and pursue simple strategies with no legal complications.""" # Construct full prompt full_prompt = f"You are {ai_name}, {ai_role}\n{prompt_start}\n\nGOALS:\n\n" @@ -181,7 +190,7 @@ def construct_prompt(): Fore.GREEN, f"Would you like me to return to being {config.ai_name}?", speak_text=True) - should_continue = input(f"""Continue with the last settings? + should_continue = utils.clean_input(f"""Continue with the last settings? Name: {config.ai_name} Role: {config.ai_role} Goals: {config.ai_goals} @@ -216,7 +225,7 @@ def prompt_user(): "Name your AI: ", Fore.GREEN, "For example, 'Entrepreneur-GPT'") - ai_name = input("AI Name: ") + ai_name = utils.clean_input("AI Name: ") if ai_name == "": ai_name = "Entrepreneur-GPT" @@ -231,7 +240,7 @@ def prompt_user(): "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.'") - ai_role = input(f"{ai_name} is: ") + 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." @@ -243,7 +252,7 @@ def prompt_user(): print("Enter nothing to load defaults, enter nothing when finished.", flush=True) ai_goals = [] for i in range(5): - ai_goal = input(f"{Fore.LIGHTBLUE_EX}Goal{Style.RESET_ALL} {i+1}: ") + ai_goal = utils.clean_input(f"{Fore.LIGHTBLUE_EX}Goal{Style.RESET_ALL} {i+1}: ") if ai_goal == "": break ai_goals.append(ai_goal) @@ -279,22 +288,16 @@ def parse_arguments(): print_to_console("Speak Mode: ", Fore.GREEN, "ENABLED") cfg.set_speak_mode(True) - if args.debug: - print_to_console("Debug Mode: ", Fore.GREEN, "ENABLED") - cfg.set_debug_mode(True) - if args.gpt3only: print_to_console("GPT3.5 Only Mode: ", Fore.GREEN, "ENABLED") cfg.set_smart_llm_model(cfg.fast_llm_model) - if args.debug: - print_to_console("Debug Mode: ", Fore.GREEN, "ENABLED") - cfg.set_debug_mode(True) # TODO: fill in llm values here check_openai_api_key() cfg = Config() +logger = configure_logging() parse_arguments() ai_name = "" prompt = construct_prompt() @@ -344,7 +347,7 @@ while True: 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 = input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) + console_input = utils.clean_input(Fore.MAGENTA + "Input:" + Style.RESET_ALL) if console_input.lower() == "y": user_input = "GENERATE NEXT COMMAND JSON" break @@ -405,4 +408,3 @@ while True: chat.create_chat_message( "system", "Unable to execute command")) print_to_console("SYSTEM: ", Fore.YELLOW, "Unable to execute command") - diff --git a/scripts/speak.py b/scripts/speak.py index 10dd7c07..08b0c1c9 100644 --- a/scripts/speak.py +++ b/scripts/speak.py @@ -4,6 +4,8 @@ import requests from config import Config cfg = Config() import gtts +import threading +from threading import Lock, Semaphore # TODO: Nicer names for these ids @@ -14,6 +16,9 @@ tts_headers = { "xi-api-key": cfg.elevenlabs_api_key } +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( @@ -23,10 +28,11 @@ def eleven_labs_speech(text, voice_index=0): tts_url, headers=tts_headers, json=formatted_message) if response.status_code == 200: - with open("speech.mpeg", "wb") as f: - f.write(response.content) - playsound("speech.mpeg") - os.remove("speech.mpeg") + with mutex_lock: + with open("speech.mpeg", "wb") as f: + f.write(response.content) + playsound("speech.mpeg", True) + os.remove("speech.mpeg") return True else: print("Request failed with status code:", response.status_code) @@ -35,15 +41,29 @@ def eleven_labs_speech(text, voice_index=0): def gtts_speech(text): tts = gtts.gTTS(text) - tts.save("speech.mp3") - playsound("speech.mp3") - os.remove("speech.mp3") + with mutex_lock: + tts.save("speech.mp3") + playsound("speech.mp3", True) + os.remove("speech.mp3") + +def macos_tts_speech(text): + os.system(f'say "{text}"') def say_text(text, voice_index=0): - if not cfg.elevenlabs_api_key: - gtts_speech(text) - else: - success = eleven_labs_speech(text, voice_index) - if not success: - gtts_speech(text) + def speak(): + if not cfg.elevenlabs_api_key: + if cfg.use_mac_os_tts == 'True': + macos_tts_speech(text) + else: + gtts_speech(text) + else: + success = eleven_labs_speech(text, voice_index) + if not success: + gtts_speech(text) + + queue_semaphore.release() + + queue_semaphore.acquire(True) + thread = threading.Thread(target=speak) + thread.start() diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 00000000..5039796f --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,8 @@ +def clean_input(prompt: str=''): + try: + return input(prompt) + except KeyboardInterrupt: + print("You interrupted Auto-GPT") + print("Quitting...") + exit(0) + diff --git a/tests/test_browse_scrape_text.py b/tests/test_browse_scrape_text.py index 27ebc0f6..bfc3b425 100644 --- a/tests/test_browse_scrape_text.py +++ b/tests/test_browse_scrape_text.py @@ -37,7 +37,7 @@ Additional aspects: class TestScrapeText: - # Tests that scrape_text() returns the expected text when given a valid URL. + # Tests that scrape_text() returns the expected text when given a valid URL. def test_scrape_text_with_valid_url(self, mocker): # Mock the requests.get() method to return a response with expected text expected_text = "This is some sample text" @@ -50,7 +50,7 @@ class TestScrapeText: url = "http://www.example.com" assert scrape_text(url) == expected_text - # Tests that the function returns an error message when an invalid or unreachable url is provided. + # Tests that the function returns an error message when an invalid or unreachable url is provided. def test_invalid_url(self, mocker): # Mock the requests.get() method to raise an exception mocker.patch("requests.get", side_effect=requests.exceptions.RequestException) @@ -60,7 +60,7 @@ class TestScrapeText: error_message = scrape_text(url) assert "Error:" in error_message - # Tests that the function returns an empty string when the html page contains no text to be scraped. + # Tests that the function returns an empty string when the html page contains no text to be scraped. def test_no_text(self, mocker): # Mock the requests.get() method to return a response with no text mock_response = mocker.Mock() @@ -72,7 +72,7 @@ class TestScrapeText: url = "http://www.example.com" assert scrape_text(url) == "" - # Tests that the function returns an error message when the response status code is an http error (>=400). + # Tests that the function returns an error message when the response status code is an http error (>=400). def test_http_error(self, mocker): # Mock the requests.get() method to return a response with a 404 status code mocker.patch('requests.get', return_value=mocker.Mock(status_code=404)) @@ -83,7 +83,7 @@ class TestScrapeText: # Check that the function returns an error message assert result == "Error: HTTP 404 error" - # Tests that scrape_text() properly handles HTML tags. + # Tests that scrape_text() properly handles HTML tags. def test_scrape_text_with_html_tags(self, mocker): # Create a mock response object with HTML containing tags html = "
This is bold text.
" @@ -96,4 +96,4 @@ class TestScrapeText: result = scrape_text("https://www.example.com") # Check that the function properly handles HTML tags - assert result == "This is bold text." \ No newline at end of file + assert result == "This is bold text."