From c8c57059ebd730e96c0b8baa92fdf9513ae1d06c Mon Sep 17 00:00:00 2001 From: Swifty Date: Tue, 5 Sep 2023 13:13:29 +0200 Subject: [PATCH] Addition of Ability Register and Sample Ability (#27) --- forge/autogpt/sdk/abilities/__init__.py | 0 .../sdk/abilities/file_system/files.py | 35 ++++ forge/autogpt/sdk/abilities/registry.py | 187 ++++++++++++++++++ forge/autogpt/sdk/agent.py | 2 +- forge/autogpt/sdk/ai_profile.py | 4 +- 5 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 forge/autogpt/sdk/abilities/__init__.py create mode 100644 forge/autogpt/sdk/abilities/file_system/files.py create mode 100644 forge/autogpt/sdk/abilities/registry.py diff --git a/forge/autogpt/sdk/abilities/__init__.py b/forge/autogpt/sdk/abilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forge/autogpt/sdk/abilities/file_system/files.py b/forge/autogpt/sdk/abilities/file_system/files.py new file mode 100644 index 0000000..052314d --- /dev/null +++ b/forge/autogpt/sdk/abilities/file_system/files.py @@ -0,0 +1,35 @@ +from typing import List + +from ..registry import ability + + +@ability( + name="list_files", + description="List files in a directory", + parameters=[ + { + "name": "path", + "description": "Path to the directory", + "type": "string", + "required": True, + }, + { + "name": "recursive", + "description": "Recursively list files", + "type": "boolean", + "required": False, + }, + ], + output_type="list[str]", +) +def list_files(agent, path: str, recursive: bool = False) -> List[str]: + """ + List files in a directory + """ + import glob + import os + + if recursive: + return glob.glob(os.path.join(path, "**"), recursive=True) + else: + return os.listdir(path) diff --git a/forge/autogpt/sdk/abilities/registry.py b/forge/autogpt/sdk/abilities/registry.py new file mode 100644 index 0000000..ef3337e --- /dev/null +++ b/forge/autogpt/sdk/abilities/registry.py @@ -0,0 +1,187 @@ +import glob +import importlib +import inspect +import os +from typing import Any, Callable, List + +import pydantic + + +class AbilityParameter(pydantic.BaseModel): + """ + This class represents a parameter for an ability. + + Attributes: + name (str): The name of the parameter. + description (str): A brief description of what the parameter does. + type (str): The type of the parameter. + required (bool): A flag indicating whether the parameter is required or optional. + """ + + name: str + description: str + type: str + required: bool + + +class Ability(pydantic.BaseModel): + """ + This class represents an ability in the system. + + Attributes: + name (str): The name of the ability. + description (str): A brief description of what the ability does. + method (Callable): The method that implements the ability. + parameters (List[AbilityParameter]): A list of parameters that the ability requires. + output_type (str): The type of the output that the ability returns. + """ + + name: str + description: str + method: Callable + parameters: List[AbilityParameter] + output_type: str + category: str | None = None + + def __call__(self, *args: Any, **kwds: Any) -> Any: + """ + This method allows the class instance to be called as a function. + + Args: + *args: Variable length argument list. + **kwds: Arbitrary keyword arguments. + + Returns: + Any: The result of the method call. + """ + return self.method(*args, **kwds) + + def __str__(self) -> str: + """ + This method returns a string representation of the class instance. + + Returns: + str: A string representation of the class instance. + """ + func_summary = f"{self.name}(" + for param in self.parameters: + func_summary += f"{param.name}: {param.type}, " + func_summary = func_summary[:-2] + ")" + func_summary += f" -> {self.output_type}. Usage: {self.description}," + return func_summary + + +def ability( + name: str, description: str, parameters: List[AbilityParameter], output_type: str +): + def decorator(func): + func_params = inspect.signature(func).parameters + param_names = set( + [AbilityParameter.parse_obj(param).name for param in parameters] + ) + param_names.add("agent") + func_param_names = set(func_params.keys()) + if param_names != func_param_names: + raise ValueError( + f"Mismatch in parameter names. Ability Annotation includes {param_names}, but function acatually takes {func_param_names} in function {func.__name__} signature" + ) + func.ability = Ability( + name=name, + description=description, + parameters=parameters, + method=func, + output_type=output_type, + ) + return func + + return decorator + + +class AbilityRegister: + def __init__(self) -> None: + self.abilities = {} + self.register_abilities() + + def register_abilities(self) -> None: + print(os.path.join(os.path.dirname(__file__), "*.py")) + for ability_path in glob.glob( + os.path.join(os.path.dirname(__file__), "**/*.py"), recursive=True + ): + if not os.path.basename(ability_path) in [ + "__init__.py", + "registry.py", + ]: + ability = os.path.relpath( + ability_path, os.path.dirname(__file__) + ).replace("/", ".") + try: + module = importlib.import_module( + f".{ability[:-3]}", package="autogpt.sdk.abilities" + ) + for attr in dir(module): + func = getattr(module, attr) + if hasattr(func, "ability"): + ab = func.ability + + ab.category = ( + ability.split(".")[0].lower().replace("_", " ") + if len(ability.split(".")) > 1 + else "general" + ) + self.abilities[func.ability.name] = func.ability + except Exception as e: + print(f"Error occurred while registering abilities: {str(e)}") + + def list_abilities(self) -> List[Ability]: + return self.abilities + + def abilities_description(self) -> str: + abilities_by_category = {} + for ability in self.abilities.values(): + if ability.category not in abilities_by_category: + abilities_by_category[ability.category] = [] + abilities_by_category[ability.category].append(str(ability)) + + abilities_description = "" + for category, abilities in abilities_by_category.items(): + if abilities_description != "": + abilities_description += "\n" + abilities_description += f"{category}:" + for ability in abilities: + abilities_description += f" {ability}" + + return abilities_description + + def run_ability(self, agent, ability_name: str, *args: Any, **kwds: Any) -> Any: + """ + This method runs a specified ability with the provided arguments and keyword arguments. + + The agent is passed as the first argument to the ability. This allows the ability to access and manipulate + the agent's state as needed. + + Args: + agent: The agent instance. + ability_name (str): The name of the ability to run. + *args: Variable length argument list. + **kwds: Arbitrary keyword arguments. + + Returns: + Any: The result of the ability execution. + + Raises: + Exception: If there is an error in running the ability. + """ + try: + ability = self.abilities[ability_name] + return ability(agent, *args, **kwds) + except Exception: + raise + + +if __name__ == "__main__": + import sys + + sys.path.append("/Users/swifty/dev/forge/forge") + register = AbilityRegister() + print(register.abilities_description()) + print(register.run_ability(None, "list_files", "/Users/swifty/dev/forge/forge")) diff --git a/forge/autogpt/sdk/agent.py b/forge/autogpt/sdk/agent.py index a97bea4..e96cd64 100644 --- a/forge/autogpt/sdk/agent.py +++ b/forge/autogpt/sdk/agent.py @@ -3,8 +3,8 @@ from uuid import uuid4 from fastapi import APIRouter, FastAPI, UploadFile -from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse from hypercorn.asyncio import serve from hypercorn.config import Config diff --git a/forge/autogpt/sdk/ai_profile.py b/forge/autogpt/sdk/ai_profile.py index f509645..b477833 100644 --- a/forge/autogpt/sdk/ai_profile.py +++ b/forge/autogpt/sdk/ai_profile.py @@ -16,10 +16,10 @@ from autogpt.sdk import PromptEngine -class ProfileGenerator: +class ProfileGenerator: def __init__(self, task: str, PromptEngine: PromptEngine): """ Initialize the profile generator with the task to be performed. """ - self.task = task \ No newline at end of file + self.task = task