diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6ba4ae76..db8b910e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,3 +6,4 @@ /src/autogpt_plugins/bing_search @ForestLinSen /src/autogpt_plugins/news_search @PalAditya /src/autogpt_plugins/wikipedia_search @pierluigi-failla +/src/autogpt_plugins/random_values @sidewaysthought \ No newline at end of file diff --git a/README.md b/README.md index c4aab1eb..554c9755 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ For interactionless use, set `ALLOWLISTED_PLUGINS=example-plugin1,example-plugin | Bing Search | This search plugin integrates Bing search engines into Auto-GPT. | [autogpt_plugins/bing_search](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/bing_search) | News Search | This search plugin integrates News Articles searches, using the NewsAPI aggregator into Auto-GPT. | [autogpt_plugins/news_search](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/news_search) | Wikipedia Search | This allows AutoGPT to use Wikipedia directly. | [autogpt_plugins/wikipedia_search](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/wikipedia_search) +| Random Values | Enable AutoGPT to generate various random numbers and strings. | [autogpt_plugins/random_values](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/random_values) Some third-party plugins have been created by contributors that are not included in this repository. For more information about these plugins, please visit their respective GitHub pages. diff --git a/requirements.txt b/requirements.txt index 285945da..a729b1f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ build twine tweepy==4.13.0 pandas +python-lorem auto_gpt_plugin_template newsapi-python pytest \ No newline at end of file diff --git a/src/autogpt_plugins/random_values/README.md b/src/autogpt_plugins/random_values/README.md new file mode 100644 index 00000000..1773606b --- /dev/null +++ b/src/autogpt_plugins/random_values/README.md @@ -0,0 +1,17 @@ +# Random Values Plugin + +The Random Values plugin will enable AutoGPT to generate various random assorted things like numbers and strings. + +## Key Features: +- make_uuids generates 1 or more UUIDs (128-bit label) +- generate_string generates 1 or more alphanumeric strings of at least 2 characters in length +- generate_password generates 1 or more passwords of 6 or more characters using letters, numbers and punctuation +- generate_placeholder_text generates 1 or more sentences of lorem ipsum text +- random_number draws 1 or more random numbers between min and max + +## Installation: +As part of the AutoGPT plugins package, follow the [installation instructions](https://github.com/Significant-Gravitas/Auto-GPT-Plugins) on the Auto-GPT-Plugins GitHub reporistory README page. + +## AutoGPT Configuration + +Set `ALLOWLISTED_PLUGINS=autogpt-random-values,example-plugin1,example-plugin2,etc` in your AutoGPT `.env` file. diff --git a/src/autogpt_plugins/random_values/__init__.py b/src/autogpt_plugins/random_values/__init__.py new file mode 100644 index 00000000..f8285161 --- /dev/null +++ b/src/autogpt_plugins/random_values/__init__.py @@ -0,0 +1,240 @@ +"""Random Values commands.""" +from typing import Any, Dict, List, Optional, Tuple, TypedDict, TypeVar + +from auto_gpt_plugin_template import AutoGPTPluginTemplate +from .random_values import _random_number +from .random_values import _make_uuids +from .random_values import _generate_string +from .random_values import _generate_password +from .random_values import _generate_placeholder_text + +PromptGenerator = TypeVar("PromptGenerator") + + +class Message(TypedDict): + role: str + content: str + + +class AutoGPTRandomValues(AutoGPTPluginTemplate): + """ + Random Values plugin for Auto-GPT. + """ + + def __init__(self): + super().__init__() + self._name = "autogpt-random-values" + self._version = "0.1.0" + self._description = "Enable Auto-GPT with the power of random values." + + def can_handle_on_response(self) -> bool: + """This method is called to check that the plugin can + handle the on_response method. + Returns: + bool: True if the plugin can handle the on_response method.""" + return False + + def on_response(self, response: str, *args, **kwargs) -> str: + """This method is called when a response is received from the model.""" + pass + + def can_handle_post_prompt(self) -> bool: + """This method is called to check that the plugin can + handle the post_prompt method. + Returns: + bool: True if the plugin can handle the post_prompt method.""" + return True + + def can_handle_on_planning(self) -> bool: + """This method is called to check that the plugin can + handle the on_planning method. + Returns: + bool: True if the plugin can handle the on_planning method.""" + return False + + def on_planning( + self, prompt: PromptGenerator, messages: List[str] + ) -> Optional[str]: + """This method is called before the planning chat completeion is done. + Args: + prompt (PromptGenerator): The prompt generator. + messages (List[str]): The list of messages. + """ + pass + + def can_handle_post_planning(self) -> bool: + """This method is called to check that the plugin can + handle the post_planning method. + Returns: + bool: True if the plugin can handle the post_planning method.""" + return False + + def post_planning(self, response: str) -> str: + """This method is called after the planning chat completeion is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the pre_instruction method. + Returns: + bool: True if the plugin can handle the pre_instruction method.""" + return False + + def pre_instruction(self, messages: List[str]) -> List[str]: + """This method is called before the instruction chat is done. + Args: + messages (List[str]): The list of context messages. + Returns: + List[str]: The resulting list of messages. + """ + pass + + def can_handle_on_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the on_instruction method. + Returns: + bool: True if the plugin can handle the on_instruction method.""" + return False + + def on_instruction(self, messages: List[str]) -> Optional[str]: + """This method is called when the instruction chat is done. + Args: + messages (List[str]): The list of context messages. + Returns: + Optional[str]: The resulting message. + """ + pass + + def can_handle_post_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the post_instruction method. + Returns: + bool: True if the plugin can handle the post_instruction method.""" + return False + + def post_instruction(self, response: str) -> str: + """This method is called after the instruction chat is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_command(self) -> bool: + """This method is called to check that the plugin can + handle the pre_command method. + Returns: + bool: True if the plugin can handle the pre_command method.""" + return False + + def pre_command( + self, command_name: str, arguments: Dict[str, Any] + ) -> Tuple[str, Dict[str, Any]]: + """This method is called before the command is executed. + Args: + command_name (str): The command name. + arguments (Dict[str, Any]): The arguments. + Returns: + Tuple[str, Dict[str, Any]]: The command name and the arguments. + """ + pass + + def can_handle_post_command(self) -> bool: + """This method is called to check that the plugin can + handle the post_command method. + Returns: + bool: True if the plugin can handle the post_command method.""" + return False + + def post_command(self, command_name: str, response: str) -> str: + """This method is called after the command is executed. + Args: + command_name (str): The command name. + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_chat_completion( + self, + messages: list[Dict[Any, Any]], + model: str, + temperature: float, + max_tokens: int, + ) -> bool: + """This method is called to check that the plugin can + handle the chat_completion method. + Args: + messages (Dict[Any, Any]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + bool: True if the plugin can handle the chat_completion method.""" + return False + + def handle_chat_completion( + self, + messages: list[Dict[Any, Any]], + model: str, + temperature: float, + max_tokens: int, + ) -> str: + """This method is called when the chat completion is done. + Args: + messages (Dict[Any, Any]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + str: The resulting response. + """ + return None + + def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: + """This method is called just after the generate_prompt is called, + but actually before the prompt is generated. + Args: + prompt (PromptGenerator): The prompt generator. + Returns: + PromptGenerator: The prompt generator. + """ + + prompt.add_command( + "random_number", + "Random Number", + {"min": "", "max": "", "count": ""}, + _random_number + ) + prompt.add_command( + "make_uuids", + "Make UUIDs", + {"count": ""}, + _make_uuids + ) + prompt.add_command( + "generate_string", + "Generate String", + {"length": "", "count": ""}, + _generate_string + ) + prompt.add_command( + "generate_password", + "Generate Password", + {"length": "", "count": ""}, + _generate_password + ) + prompt.add_command( + "generate_placeholder_text", + "Generate Placeholder Text", + {"sentences": ""}, + _generate_placeholder_text + ) + return prompt diff --git a/src/autogpt_plugins/random_values/random_values.py b/src/autogpt_plugins/random_values/random_values.py new file mode 100644 index 00000000..dfeccab7 --- /dev/null +++ b/src/autogpt_plugins/random_values/random_values.py @@ -0,0 +1,188 @@ +"""Random Values classes for Autogpt.""" + +import json +import random +import uuid +import string +import lorem + +"""Random Number function for Autogpt.""" + +def _random_number(min = 0, max = 65535, count = 1) -> str: + """Return a random integer between min and max + Args: + min (int): The lowest number in the range + max (int): The highest number in the range + count (int): The number of random numbers to return + Returns: + str: a json array with 1 to "count" random numbers in the format + [""] + """ + + # Type-check the arguments + if not isinstance(min, int): + try: + min = int(min) + except ValueError: + raise ValueError("min must be an integer") + if not isinstance(max, int): + try: + max = int(max) + except ValueError: + raise ValueError("max must be an integer") + if not isinstance(count, int): + try: + count = int(count) + except ValueError: + raise ValueError("count must be an integer") + + # Make values sane + if not (0 <= min <= 65535): + raise ValueError("min must be between 0 and 65535") + if not (0 <= max <= 65535): + raise ValueError("max must be between 0 and 65535") + if not (1 <= count <= 65535): + raise ValueError("count must be between 1 and 65535") + + # Do the thing + random_numbers = [] + for _ in range(count): + random_numbers.append(random.randint(min, max)) + + return json.dumps(random_numbers) + + +"""Random UUID function for Autogpt.""" + +def _make_uuids(count = 1) -> str: + """Return a UUID + Args: + count (int): The number of UUIDs to return + Returns: + str: a json array with 1 to "count" UUIDs + [""] + """ + + # Type-check the arguments + if not isinstance(count, int): + try: + count = int(count) + except ValueError: + raise ValueError("count must be an integer") + + # Make values sane + if not (1 <= count <= 65535): + raise ValueError("count must be between 1 and 65535") + + # Do the thing + uuids = [] + for _ in range(count): + uuids.append(str(uuid.uuid4())) + + return json.dumps(uuids) + + +"""Random String function for Autogpt.""" + +def _generate_string(length = 10, count = 1) -> str: + """Return a random string + Args: + length (int): The length of the string + count (int): The number of strings to return + Returns: + str: a json array with 1 to "count" strings of "length" length + [""] + """ + + # Type-check the arguments + if not isinstance(length, int): + try: + length = int(length) + except ValueError: + raise ValueError("length must be an integer") + if not isinstance(count, int): + try: + count = int(count) + except ValueError: + raise ValueError("count must be an integer") + + # Make values sane + if not (2 <= length <= 65535): + raise ValueError("length must be between 2 and 65535") + if not (1 <= count <= 65535): + raise ValueError("count must be between 1 and 65535") + + # Do the thing + strings = [] + for _ in range(count): + strings.append(''.join(random.choice(string.ascii_letters) for i in range(length))) + + return json.dumps(strings) + + +"""Random Password function for Autogpt.""" + +def _generate_password(length = 16, count = 1) -> str: + """Return a random password of letters, numbers, and punctuation + Args: + length (int): The length of the password + count (int): The number of passwords to return + Returns: + str: a json array with 1 to "count" passwords of "length" length + [""] + """ + + # Type-check the arguments + if not isinstance(length, int): + try: + length = int(length) + except ValueError: + raise ValueError("length must be an integer") + if not isinstance(count, int): + try: + count = int(count) + except ValueError: + raise ValueError("count must be an integer") + + # Make values sane + if not (6 <= length <= 65535): + raise ValueError("length must be between 6 and 65535") + if not (1 <= count <= 65535): + raise ValueError("count must be between 1 and 65535") + + # Do the thing + passwords = [] + for _ in range(count): + passwords.append(''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for i in range(length))) + + return json.dumps(passwords) + + +"""Random Lorem Ipsum function for Autogpt.""" + +def _generate_placeholder_text(sentences = 1) -> str: + """Return a random sentence of lorem ipsum text + Args: + sentences (int): The number of strings to return + Returns: + str: a json array with 1 to "sentences" strings of lorem ipsum + [""] + """ + + # Type-check the arguments + if not isinstance(sentences, int): + try: + sentences = int(sentences) + except ValueError: + raise ValueError("sentences must be an integer") + + # Make values sane + if not (1 <= sentences <= 65535): + raise ValueError("sentences must be between 1 and 65535") + + # Do the thing + strings = [] + for _ in range(sentences): + strings.append(lorem.get_sentence()) + + return json.dumps(strings) \ No newline at end of file diff --git a/src/autogpt_plugins/random_values/test_random_valaues.py b/src/autogpt_plugins/random_values/test_random_valaues.py new file mode 100644 index 00000000..b47ed01a --- /dev/null +++ b/src/autogpt_plugins/random_values/test_random_valaues.py @@ -0,0 +1,195 @@ +from unittest.mock import Mock +import pytest +import json +from .random_values import _random_number +from .random_values import _make_uuids +from .random_values import _generate_string +from .random_values import _generate_password +from .random_values import _generate_placeholder_text + +class TestRandomValueCommands(): + + # _random_number Tests + + def test_random_number(self): + result = json.loads(_random_number(min=10, max=20, count=5)) + assert len(result) == 5 + for num in result: + assert 10 <= num <= 20 + + def test_random_number_using_strings(self): + result = json.loads(_random_number(min="10", max="20", count="5")) + assert len(result) == 5 + for num in result: + assert 10 <= num <= 20 + + def test_random_number_using_missing_min(self): + # If missing, min defaults to zero + result = json.loads(_random_number(max=20, count=5)) + assert len(result) == 5 + for num in result: + assert 0 <= num <= 20 + + def test_random_number_using_missing_max(self): + # If missing, max defaults to 65535 + result = json.loads(_random_number(min=10, count=5)) + assert len(result) == 5 + for num in result: + assert 10 <= num <= 65535 + + def test_random_number_using_missing_count(self): + # If missing, count defaults to 1 + result = json.loads(_random_number(min=10, max=20)) + assert len(result) == 1 + for num in result: + assert 10 <= num <= 20 + + def test_random_number_min_using_garbage(self): + with pytest.raises(ValueError) as e: + _random_number(min="foo", max="20", count="5") + assert str(e.value) == "min must be an integer" + + def test_random_number_max_using_garbage(self): + with pytest.raises(ValueError) as e: + _random_number(min="10", max="bar", count="5") + assert str(e.value) == "max must be an integer" + + def test_random_number_count_using_garbage(self): + with pytest.raises(ValueError) as e: + _random_number(min="10", max="20", count="baz") + assert str(e.value) == "count must be an integer" + + # _make_uuids Tests + + def test_make_uuids(self): + result = json.loads(_make_uuids(count=5)) + assert len(result) == 5 + for uid in result: + assert isinstance(uid, str) + assert len(uid) == 36 # UUIDs have 36 characters + + def test_make_uuids_using_strings(self): + result = json.loads(_make_uuids(count="5")) + assert len(result) == 5 + for uid in result: + assert isinstance(uid, str) + assert len(uid) == 36 + + def test_make_uuids_using_missing_count(self): + # If missing, count defaults to 1 + result = json.loads(_make_uuids()) + assert len(result) == 1 + for uid in result: + assert isinstance(uid, str) + assert len(uid) == 36 + + def test_make_uuids_using_garbage(self): + with pytest.raises(ValueError) as e: + _make_uuids(count="foo") + assert str(e.value) == "count must be an integer" + + # _generate_string Tests + + def test_generate_string(self): + result = json.loads(_generate_string(length=10, count=5)) + assert len(result) == 5 + for string in result: + assert len(string) == 10 + # Strings should only contain letters and numbers + assert string.isalnum() + + def test_generate_string_using_strings(self): + result = json.loads(_generate_string(length="10", count="5")) + assert len(result) == 5 + for string in result: + assert len(string) == 10 + # Strings should only contain letters and numbers + assert string.isalnum() + + def test_generate_string_using_missing_length(self): + # If missing, length defaults to 10 + result = json.loads(_generate_string(count=5)) + assert len(result) == 5 + for string in result: + assert len(string) == 10 + # Strings should only contain letters and numbers + assert string.isalnum() + + def test_generate_string_using_missing_count(self): + # If missing, count defaults to 1 + result = json.loads(_generate_string(length=10)) + assert len(result) == 1 + for string in result: + assert len(string) == 10 + # Strings should only contain letters and numbers + assert string.isalnum() + + def test_generate_string_using_garbage(self): + with pytest.raises(ValueError) as e: + _generate_string(length="foo", count="bar") + assert str(e.value) == "length must be an integer" + + # _generate_password Tests + + def test_generate_password(self): + result = json.loads(_generate_password(length=10, count=5)) + assert len(result) == 5 + for password in result: + assert len(password) == 10 + # Passwords should contain letters, numbers, and symbols + assert not password.isalnum() + + def test_generate_password_using_strings(self): + result = json.loads(_generate_password(length="10", count="5")) + assert len(result) == 5 + for password in result: + assert len(password) == 10 + # Passwords should contain letters, numbers, and symbols + assert not password.isalnum() + + def test_generate_password_using_missing_length(self): + # If missing, length defaults to 10 + result = json.loads(_generate_password(count=5)) + assert len(result) == 5 + for password in result: + assert len(password) == 16 + # Passwords should contain letters, numbers, and symbols + assert not password.isalnum() + + def test_generate_password_using_missing_count(self): + # If missing, count defaults to 1 + result = json.loads(_generate_password(length=10)) + assert len(result) == 1 + for password in result: + assert len(password) == 10 + # Passwords should contain letters, numbers, and symbols + assert not password.isalnum() + + def test_generate_password_using_garbage(self): + with pytest.raises(ValueError) as e: + _generate_password(length="foo", count="bar") + assert str(e.value) == "length must be an integer" + + # _generate_placeholder_text Tests + + def test_generate_placeholder_text(self): + result = json.loads(_generate_placeholder_text(sentences=5)) + assert len(result) == 5 + for text in result: + assert len(text) > 3 + + def test_generate_placeholder_text_using_strings(self): + result = json.loads(_generate_placeholder_text(sentences="5")) + assert len(result) == 5 + for text in result: + assert len(text) > 3 + + def test_generate_placeholder_text_using_empty_string(self): + with pytest.raises(ValueError) as e: + _generate_placeholder_text(sentences="") + assert str(e.value) == "sentences must be an integer" + + def test_generate_placeholder_text_using_garbage(self): + with pytest.raises(ValueError) as e: + _generate_placeholder_text(sentences="foo") + assert str(e.value) == "sentences must be an integer" \ No newline at end of file