diff options
Diffstat (limited to 'autogpts/autogpt/autogpt/core/ability')
8 files changed, 588 insertions, 0 deletions
diff --git a/autogpts/autogpt/autogpt/core/ability/__init__.py b/autogpts/autogpt/autogpt/core/ability/__init__.py new file mode 100644 index 000000000..3709b9277 --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/__init__.py @@ -0,0 +1,18 @@ +"""The command system provides a way to extend the functionality of the AI agent.""" +from autogpt.core.ability.base import Ability, AbilityConfiguration, AbilityRegistry +from autogpt.core.ability.schema import AbilityResult +from autogpt.core.ability.simple import ( + AbilityRegistryConfiguration, + AbilityRegistrySettings, + SimpleAbilityRegistry, +) + +__all__ = [ + "Ability", + "AbilityConfiguration", + "AbilityRegistry", + "AbilityResult", + "AbilityRegistryConfiguration", + "AbilityRegistrySettings", + "SimpleAbilityRegistry", +] diff --git a/autogpts/autogpt/autogpt/core/ability/base.py b/autogpts/autogpt/autogpt/core/ability/base.py new file mode 100644 index 000000000..2686c101c --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/base.py @@ -0,0 +1,88 @@ +import abc +from pprint import pformat +from typing import Any, ClassVar + +import inflection +from pydantic import Field + +from autogpt.core.configuration import SystemConfiguration +from autogpt.core.planning.simple import LanguageModelConfiguration +from autogpt.core.plugin.base import PluginLocation +from autogpt.core.resource.model_providers import CompletionModelFunction +from autogpt.core.utils.json_schema import JSONSchema + +from .schema import AbilityResult + + +class AbilityConfiguration(SystemConfiguration): + """Struct for model configuration.""" + + location: PluginLocation + packages_required: list[str] = Field(default_factory=list) + language_model_required: LanguageModelConfiguration = None + memory_provider_required: bool = False + workspace_required: bool = False + + +class Ability(abc.ABC): + """A class representing an agent ability.""" + + default_configuration: ClassVar[AbilityConfiguration] + + @classmethod + def name(cls) -> str: + """The name of the ability.""" + return inflection.underscore(cls.__name__) + + @property + @classmethod + @abc.abstractmethod + def description(cls) -> str: + """A detailed description of what the ability does.""" + ... + + @property + @classmethod + @abc.abstractmethod + def parameters(cls) -> dict[str, JSONSchema]: + ... + + @abc.abstractmethod + async def __call__(self, *args: Any, **kwargs: Any) -> AbilityResult: + ... + + def __str__(self) -> str: + return pformat(self.spec) + + @property + @classmethod + def spec(cls) -> CompletionModelFunction: + return CompletionModelFunction( + name=cls.name(), + description=cls.description, + parameters=cls.parameters, + ) + + +class AbilityRegistry(abc.ABC): + @abc.abstractmethod + def register_ability( + self, ability_name: str, ability_configuration: AbilityConfiguration + ) -> None: + ... + + @abc.abstractmethod + def list_abilities(self) -> list[str]: + ... + + @abc.abstractmethod + def dump_abilities(self) -> list[CompletionModelFunction]: + ... + + @abc.abstractmethod + def get_ability(self, ability_name: str) -> Ability: + ... + + @abc.abstractmethod + async def perform(self, ability_name: str, **kwargs: Any) -> AbilityResult: + ... diff --git a/autogpts/autogpt/autogpt/core/ability/builtins/__init__.py b/autogpts/autogpt/autogpt/core/ability/builtins/__init__.py new file mode 100644 index 000000000..93936dbc6 --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/builtins/__init__.py @@ -0,0 +1,12 @@ +from autogpt.core.ability.builtins.create_new_ability import CreateNewAbility +from autogpt.core.ability.builtins.query_language_model import QueryLanguageModel + +BUILTIN_ABILITIES = { + QueryLanguageModel.name(): QueryLanguageModel, +} + +__all__ = [ + "BUILTIN_ABILITIES", + "CreateNewAbility", + "QueryLanguageModel", +] diff --git a/autogpts/autogpt/autogpt/core/ability/builtins/create_new_ability.py b/autogpts/autogpt/autogpt/core/ability/builtins/create_new_ability.py new file mode 100644 index 000000000..55550cafc --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/builtins/create_new_ability.py @@ -0,0 +1,107 @@ +import logging +from typing import ClassVar + +from autogpt.core.ability.base import Ability, AbilityConfiguration +from autogpt.core.ability.schema import AbilityResult +from autogpt.core.plugin.simple import PluginLocation, PluginStorageFormat +from autogpt.core.utils.json_schema import JSONSchema + + +class CreateNewAbility(Ability): + default_configuration = AbilityConfiguration( + location=PluginLocation( + storage_format=PluginStorageFormat.INSTALLED_PACKAGE, + storage_route="autogpt.core.ability.builtins.CreateNewAbility", + ), + ) + + def __init__( + self, + logger: logging.Logger, + configuration: AbilityConfiguration, + ): + self._logger = logger + self._configuration = configuration + + description: ClassVar[str] = "Create a new ability by writing python code." + + parameters: ClassVar[dict[str, JSONSchema]] = { + "ability_name": JSONSchema( + description="A meaningful and concise name for the new ability.", + type=JSONSchema.Type.STRING, + required=True, + ), + "description": JSONSchema( + description=( + "A detailed description of the ability and its uses, " + "including any limitations." + ), + type=JSONSchema.Type.STRING, + required=True, + ), + "arguments": JSONSchema( + description="A list of arguments that the ability will accept.", + type=JSONSchema.Type.ARRAY, + items=JSONSchema( + type=JSONSchema.Type.OBJECT, + properties={ + "name": JSONSchema( + description="The name of the argument.", + type=JSONSchema.Type.STRING, + ), + "type": JSONSchema( + description=( + "The type of the argument. " + "Must be a standard json schema type." + ), + type=JSONSchema.Type.STRING, + ), + "description": JSONSchema( + description=( + "A detailed description of the argument and its uses." + ), + type=JSONSchema.Type.STRING, + ), + }, + ), + ), + "required_arguments": JSONSchema( + description="A list of the names of the arguments that are required.", + type=JSONSchema.Type.ARRAY, + items=JSONSchema( + description="The names of the arguments that are required.", + type=JSONSchema.Type.STRING, + ), + ), + "package_requirements": JSONSchema( + description=( + "A list of the names of the Python packages that are required to " + "execute the ability." + ), + type=JSONSchema.Type.ARRAY, + items=JSONSchema( + description=( + "The of the Python package that is required to execute the ability." + ), + type=JSONSchema.Type.STRING, + ), + ), + "code": JSONSchema( + description=( + "The Python code that will be executed when the ability is called." + ), + type=JSONSchema.Type.STRING, + required=True, + ), + } + + async def __call__( + self, + ability_name: str, + description: str, + arguments: list[dict], + required_arguments: list[str], + package_requirements: list[str], + code: str, + ) -> AbilityResult: + raise NotImplementedError diff --git a/autogpts/autogpt/autogpt/core/ability/builtins/file_operations.py b/autogpts/autogpt/autogpt/core/ability/builtins/file_operations.py new file mode 100644 index 000000000..08dc8c7a9 --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/builtins/file_operations.py @@ -0,0 +1,170 @@ +import logging +import os +from typing import ClassVar + +from autogpt.core.ability.base import Ability, AbilityConfiguration +from autogpt.core.ability.schema import AbilityResult, ContentType, Knowledge +from autogpt.core.plugin.simple import PluginLocation, PluginStorageFormat +from autogpt.core.utils.json_schema import JSONSchema +from autogpt.core.workspace import Workspace + + +class ReadFile(Ability): + default_configuration = AbilityConfiguration( + location=PluginLocation( + storage_format=PluginStorageFormat.INSTALLED_PACKAGE, + storage_route="autogpt.core.ability.builtins.ReadFile", + ), + packages_required=["unstructured"], + workspace_required=True, + ) + + def __init__( + self, + logger: logging.Logger, + workspace: Workspace, + ): + self._logger = logger + self._workspace = workspace + + description: ClassVar[str] = "Read and parse all text from a file." + + parameters: ClassVar[dict[str, JSONSchema]] = { + "filename": JSONSchema( + type=JSONSchema.Type.STRING, + description="The name of the file to read.", + ), + } + + def _check_preconditions(self, filename: str) -> AbilityResult | None: + message = "" + try: + pass + except ImportError: + message = "Package charset_normalizer is not installed." + + try: + file_path = self._workspace.get_path(filename) + if not file_path.exists(): + message = f"File {filename} does not exist." + if not file_path.is_file(): + message = f"{filename} is not a file." + except ValueError as e: + message = str(e) + + if message: + return AbilityResult( + ability_name=self.name(), + ability_args={"filename": filename}, + success=False, + message=message, + data=None, + ) + + def __call__(self, filename: str) -> AbilityResult: + if result := self._check_preconditions(filename): + return result + + from unstructured.partition.auto import partition + + file_path = self._workspace.get_path(filename) + try: + elements = partition(str(file_path)) + # TODO: Lots of other potentially useful information is available + # in the partitioned file. Consider returning more of it. + new_knowledge = Knowledge( + content="\n\n".join([element.text for element in elements]), + content_type=ContentType.TEXT, + content_metadata={"filename": filename}, + ) + success = True + message = f"File {file_path} read successfully." + except IOError as e: + new_knowledge = None + success = False + message = str(e) + + return AbilityResult( + ability_name=self.name(), + ability_args={"filename": filename}, + success=success, + message=message, + new_knowledge=new_knowledge, + ) + + +class WriteFile(Ability): + default_configuration = AbilityConfiguration( + location=PluginLocation( + storage_format=PluginStorageFormat.INSTALLED_PACKAGE, + storage_route="autogpt.core.ability.builtins.WriteFile", + ), + packages_required=["unstructured"], + workspace_required=True, + ) + + def __init__( + self, + logger: logging.Logger, + workspace: Workspace, + ): + self._logger = logger + self._workspace = workspace + + description: ClassVar[str] = "Write text to a file." + + parameters: ClassVar[dict[str, JSONSchema]] = { + "filename": JSONSchema( + type=JSONSchema.Type.STRING, + description="The name of the file to write.", + ), + "contents": JSONSchema( + type=JSONSchema.Type.STRING, + description="The contents of the file to write.", + ), + } + + def _check_preconditions( + self, filename: str, contents: str + ) -> AbilityResult | None: + message = "" + try: + file_path = self._workspace.get_path(filename) + if file_path.exists(): + message = f"File {filename} already exists." + if len(contents): + message = f"File {filename} was not given any content." + except ValueError as e: + message = str(e) + + if message: + return AbilityResult( + ability_name=self.name(), + ability_args={"filename": filename, "contents": contents}, + success=False, + message=message, + data=None, + ) + + def __call__(self, filename: str, contents: str) -> AbilityResult: + if result := self._check_preconditions(filename, contents): + return result + + file_path = self._workspace.get_path(filename) + try: + directory = os.path.dirname(file_path) + os.makedirs(directory) + with open(filename, "w", encoding="utf-8") as f: + f.write(contents) + success = True + message = f"File {file_path} written successfully." + except IOError as e: + success = False + message = str(e) + + return AbilityResult( + ability_name=self.name(), + ability_args={"filename": filename}, + success=success, + message=message, + ) diff --git a/autogpts/autogpt/autogpt/core/ability/builtins/query_language_model.py b/autogpts/autogpt/autogpt/core/ability/builtins/query_language_model.py new file mode 100644 index 000000000..7a6ae68ee --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/builtins/query_language_model.py @@ -0,0 +1,66 @@ +import logging +from typing import ClassVar + +from autogpt.core.ability.base import Ability, AbilityConfiguration +from autogpt.core.ability.schema import AbilityResult +from autogpt.core.planning.simple import LanguageModelConfiguration +from autogpt.core.plugin.simple import PluginLocation, PluginStorageFormat +from autogpt.core.resource.model_providers import ( + ChatMessage, + ChatModelProvider, + ModelProviderName, + OpenAIModelName, +) +from autogpt.core.utils.json_schema import JSONSchema + + +class QueryLanguageModel(Ability): + default_configuration = AbilityConfiguration( + location=PluginLocation( + storage_format=PluginStorageFormat.INSTALLED_PACKAGE, + storage_route="autogpt.core.ability.builtins.QueryLanguageModel", + ), + language_model_required=LanguageModelConfiguration( + model_name=OpenAIModelName.GPT3, + provider_name=ModelProviderName.OPENAI, + temperature=0.9, + ), + ) + + def __init__( + self, + logger: logging.Logger, + configuration: AbilityConfiguration, + language_model_provider: ChatModelProvider, + ): + self._logger = logger + self._configuration = configuration + self._language_model_provider = language_model_provider + + description: ClassVar[str] = ( + "Query a language model." + " A query should be a question and any relevant context." + ) + + parameters: ClassVar[dict[str, JSONSchema]] = { + "query": JSONSchema( + type=JSONSchema.Type.STRING, + description=( + "A query for a language model. " + "A query should contain a question and any relevant context." + ), + ) + } + + async def __call__(self, query: str) -> AbilityResult: + model_response = await self._language_model_provider.create_chat_completion( + model_prompt=[ChatMessage.user(query)], + functions=[], + model_name=self._configuration.language_model_required.model_name, + ) + return AbilityResult( + ability_name=self.name(), + ability_args={"query": query}, + success=True, + message=model_response.response.content or "", + ) diff --git a/autogpts/autogpt/autogpt/core/ability/schema.py b/autogpts/autogpt/autogpt/core/ability/schema.py new file mode 100644 index 000000000..3d20a7b92 --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/schema.py @@ -0,0 +1,30 @@ +import enum +from typing import Any + +from pydantic import BaseModel + + +class ContentType(str, enum.Enum): + # TBD what these actually are. + TEXT = "text" + CODE = "code" + + +class Knowledge(BaseModel): + content: str + content_type: ContentType + content_metadata: dict[str, Any] + + +class AbilityResult(BaseModel): + """The AbilityResult is a standard response struct for an ability.""" + + ability_name: str + ability_args: dict[str, str] + success: bool + message: str + new_knowledge: Knowledge = None + + def summary(self) -> str: + kwargs = ", ".join(f"{k}={v}" for k, v in self.ability_args.items()) + return f"{self.ability_name}({kwargs}): {self.message}" diff --git a/autogpts/autogpt/autogpt/core/ability/simple.py b/autogpts/autogpt/autogpt/core/ability/simple.py new file mode 100644 index 000000000..962413182 --- /dev/null +++ b/autogpts/autogpt/autogpt/core/ability/simple.py @@ -0,0 +1,97 @@ +import logging + +from autogpt.core.ability.base import Ability, AbilityConfiguration, AbilityRegistry +from autogpt.core.ability.builtins import BUILTIN_ABILITIES +from autogpt.core.ability.schema import AbilityResult +from autogpt.core.configuration import Configurable, SystemConfiguration, SystemSettings +from autogpt.core.memory.base import Memory +from autogpt.core.plugin.simple import SimplePluginService +from autogpt.core.resource.model_providers import ( + ChatModelProvider, + CompletionModelFunction, + ModelProviderName, +) +from autogpt.core.workspace.base import Workspace + + +class AbilityRegistryConfiguration(SystemConfiguration): + """Configuration for the AbilityRegistry subsystem.""" + + abilities: dict[str, AbilityConfiguration] + + +class AbilityRegistrySettings(SystemSettings): + configuration: AbilityRegistryConfiguration + + +class SimpleAbilityRegistry(AbilityRegistry, Configurable): + default_settings = AbilityRegistrySettings( + name="simple_ability_registry", + description="A simple ability registry.", + configuration=AbilityRegistryConfiguration( + abilities={ + ability_name: ability.default_configuration + for ability_name, ability in BUILTIN_ABILITIES.items() + }, + ), + ) + + def __init__( + self, + settings: AbilityRegistrySettings, + logger: logging.Logger, + memory: Memory, + workspace: Workspace, + model_providers: dict[ModelProviderName, ChatModelProvider], + ): + self._configuration = settings.configuration + self._logger = logger + self._memory = memory + self._workspace = workspace + self._model_providers = model_providers + self._abilities: list[Ability] = [] + for ( + ability_name, + ability_configuration, + ) in self._configuration.abilities.items(): + self.register_ability(ability_name, ability_configuration) + + def register_ability( + self, ability_name: str, ability_configuration: AbilityConfiguration + ) -> None: + ability_class = SimplePluginService.get_plugin(ability_configuration.location) + ability_args = { + "logger": self._logger.getChild(ability_name), + "configuration": ability_configuration, + } + if ability_configuration.packages_required: + # TODO: Check packages are installed and maybe install them. + pass + if ability_configuration.memory_provider_required: + ability_args["memory"] = self._memory + if ability_configuration.workspace_required: + ability_args["workspace"] = self._workspace + if ability_configuration.language_model_required: + ability_args["language_model_provider"] = self._model_providers[ + ability_configuration.language_model_required.provider_name + ] + ability = ability_class(**ability_args) + self._abilities.append(ability) + + def list_abilities(self) -> list[str]: + return [ + f"{ability.name()}: {ability.description}" for ability in self._abilities + ] + + def dump_abilities(self) -> list[CompletionModelFunction]: + return [ability.spec for ability in self._abilities] + + def get_ability(self, ability_name: str) -> Ability: + for ability in self._abilities: + if ability.name() == ability_name: + return ability + raise ValueError(f"Ability '{ability_name}' not found.") + + async def perform(self, ability_name: str, **kwargs) -> AbilityResult: + ability = self.get_ability(ability_name) + return await ability(**kwargs) |