Files
Auto-GPT/autogpt/core/agent/simple.py
2023-07-07 22:51:01 -04:00

392 lines
14 KiB
Python

import logging
from datetime import datetime
from pathlib import Path
from typing import Any
from pydantic import BaseModel
from autogpt.core.ability import (
AbilityRegistrySettings,
AbilityResult,
SimpleAbilityRegistry,
)
from autogpt.core.agent.base import Agent
from autogpt.core.configuration import Configurable, SystemConfiguration, SystemSettings
from autogpt.core.memory import MemorySettings, SimpleMemory
from autogpt.core.planning import PlannerSettings, SimplePlanner, Task, TaskStatus
from autogpt.core.plugin.simple import (
PluginLocation,
PluginStorageFormat,
SimplePluginService,
)
from autogpt.core.resource.model_providers import OpenAIProvider, OpenAISettings
from autogpt.core.workspace.simple import SimpleWorkspace, WorkspaceSettings
class AgentSystems(SystemConfiguration):
ability_registry: PluginLocation
memory: PluginLocation
openai_provider: PluginLocation
planning: PluginLocation
workspace: PluginLocation
class AgentConfiguration(SystemConfiguration):
cycle_count: int
max_task_cycle_count: int
creation_time: str
name: str
role: str
goals: list[str]
systems: AgentSystems
class AgentSystemSettings(SystemSettings):
configuration: AgentConfiguration
class AgentSettings(BaseModel):
agent: AgentSystemSettings
ability_registry: AbilityRegistrySettings
memory: MemorySettings
openai_provider: OpenAISettings
planning: PlannerSettings
workspace: WorkspaceSettings
def update_agent_name_and_goals(self, agent_goals: dict) -> None:
self.agent.configuration.name = agent_goals["agent_name"]
self.agent.configuration.role = agent_goals["agent_role"]
self.agent.configuration.goals = agent_goals["agent_goals"]
class SimpleAgent(Agent, Configurable):
default_settings = AgentSystemSettings(
name="simple_agent",
description="A simple agent.",
configuration=AgentConfiguration(
name="Entrepreneur-GPT",
role=(
"An AI designed to autonomously develop and run businesses with "
"the sole goal of increasing your net worth."
),
goals=[
"Increase net worth",
"Grow Twitter Account",
"Develop and manage multiple businesses autonomously",
],
cycle_count=0,
max_task_cycle_count=3,
creation_time="",
systems=AgentSystems(
ability_registry=PluginLocation(
storage_format=PluginStorageFormat.INSTALLED_PACKAGE,
storage_route="autogpt.core.ability.SimpleAbilityRegistry",
),
memory=PluginLocation(
storage_format=PluginStorageFormat.INSTALLED_PACKAGE,
storage_route="autogpt.core.memory.SimpleMemory",
),
openai_provider=PluginLocation(
storage_format=PluginStorageFormat.INSTALLED_PACKAGE,
storage_route="autogpt.core.resource.model_providers.OpenAIProvider",
),
planning=PluginLocation(
storage_format=PluginStorageFormat.INSTALLED_PACKAGE,
storage_route="autogpt.core.planning.SimplePlanner",
),
workspace=PluginLocation(
storage_format=PluginStorageFormat.INSTALLED_PACKAGE,
storage_route="autogpt.core.workspace.SimpleWorkspace",
),
),
),
)
def __init__(
self,
settings: AgentSystemSettings,
logger: logging.Logger,
ability_registry: SimpleAbilityRegistry,
memory: SimpleMemory,
openai_provider: OpenAIProvider,
planning: SimplePlanner,
workspace: SimpleWorkspace,
):
self._configuration = settings.configuration
self._logger = logger
self._ability_registry = ability_registry
self._memory = memory
# FIXME: Need some work to make this work as a dict of providers
# Getting the construction of the config to work is a bit tricky
self._openai_provider = openai_provider
self._planning = planning
self._workspace = workspace
self._task_queue = []
self._completed_tasks = []
self._current_task = None
self._next_ability = None
@classmethod
def from_workspace(
cls,
workspace_path: Path,
logger: logging.Logger,
) -> "SimpleAgent":
agent_settings = SimpleWorkspace.load_agent_settings(workspace_path)
agent_args = {}
agent_args["settings"] = agent_settings.agent
agent_args["logger"] = logger
agent_args["workspace"] = cls._get_system_instance(
"workspace",
agent_settings,
logger,
)
agent_args["openai_provider"] = cls._get_system_instance(
"openai_provider",
agent_settings,
logger,
)
agent_args["planning"] = cls._get_system_instance(
"planning",
agent_settings,
logger,
model_providers={"openai": agent_args["openai_provider"]},
)
agent_args["memory"] = cls._get_system_instance(
"memory",
agent_settings,
logger,
workspace=agent_args["workspace"],
)
agent_args["ability_registry"] = cls._get_system_instance(
"ability_registry",
agent_settings,
logger,
workspace=agent_args["workspace"],
memory=agent_args["memory"],
model_providers={"openai": agent_args["openai_provider"]},
)
return cls(**agent_args)
async def build_initial_plan(self) -> dict:
plan = await self._planning.make_initial_plan(
agent_name=self._configuration.name,
agent_role=self._configuration.role,
agent_goals=self._configuration.goals,
abilities=self._ability_registry.list_abilities(),
)
tasks = [Task.parse_obj(task) for task in plan.content["task_list"]]
# TODO: Should probably do a step to evaluate the quality of the generated tasks,
# and ensure that they have actionable ready and acceptance criteria
self._task_queue.extend(tasks)
self._task_queue.sort(key=lambda t: t.priority, reverse=True)
self._task_queue[-1].context.status = TaskStatus.READY
return plan.content
async def determine_next_ability(self, *args, **kwargs):
if not self._task_queue:
return {"response": "I don't have any tasks to work on right now."}
self._configuration.cycle_count += 1
task = self._task_queue.pop()
self._logger.info(f"Working on task: {task}")
task = await self._evaluate_task_and_add_context(task)
next_ability = await self._choose_next_ability(
task,
self._ability_registry.dump_abilities(),
)
self._current_task = task
self._next_ability = next_ability.content
return self._current_task, self._next_ability
async def execute_next_ability(self, user_input: str, *args, **kwargs):
if user_input == "y":
ability = self._ability_registry.get_ability(
self._next_ability["next_ability"]
)
ability_response = await ability(**self._next_ability["ability_arguments"])
await self._update_tasks_and_memory(ability_response)
if self._current_task.context.status == TaskStatus.DONE:
self._completed_tasks.append(self._current_task)
else:
self._task_queue.append(self._current_task)
self._current_task = None
self._next_ability = None
return ability_response.dict()
else:
raise NotImplementedError
async def _evaluate_task_and_add_context(self, task: Task) -> Task:
"""Evaluate the task and add context to it."""
if task.context.status == TaskStatus.IN_PROGRESS:
# Nothing to do here
return task
else:
self._logger.debug(f"Evaluating task {task} and adding relevant context.")
# TODO: Look up relevant memories (need working memory system)
# TODO: Evaluate whether there is enough information to start the task (language model call).
task.context.enough_info = True
task.context.status = TaskStatus.IN_PROGRESS
return task
async def _choose_next_ability(self, task: Task, ability_schema: list[dict]):
"""Choose the next ability to use for the task."""
self._logger.debug(f"Choosing next ability for task {task}.")
if task.context.cycle_count > self._configuration.max_task_cycle_count:
# Don't hit the LLM, just set the next action as "breakdown_task" with an appropriate reason
raise NotImplementedError
elif not task.context.enough_info:
# Don't ask the LLM, just set the next action as "breakdown_task" with an appropriate reason
raise NotImplementedError
else:
next_ability = await self._planning.determine_next_ability(
task, ability_schema
)
return next_ability
async def _update_tasks_and_memory(self, ability_result: AbilityResult):
self._current_task.context.cycle_count += 1
self._current_task.context.prior_actions.append(ability_result)
# TODO: Summarize new knowledge
# TODO: store knowledge and summaries in memory and in relevant tasks
# TODO: evaluate whether the task is complete
def __repr__(self):
return "SimpleAgent()"
################################################################
# Factory interface for agent bootstrapping and initialization #
################################################################
@classmethod
def build_user_configuration(cls) -> dict[str, Any]:
"""Build the user's configuration."""
configuration_dict = {
"agent": cls.get_user_config(),
}
system_locations = configuration_dict["agent"]["configuration"]["systems"]
for system_name, system_location in system_locations.items():
system_class = SimplePluginService.get_plugin(system_location)
configuration_dict[system_name] = system_class.get_user_config()
configuration_dict = _prune_empty_dicts(configuration_dict)
return configuration_dict
@classmethod
def compile_settings(
cls, logger: logging.Logger, user_configuration: dict
) -> AgentSettings:
"""Compile the user's configuration with the defaults."""
logger.debug("Processing agent system configuration.")
configuration_dict = {
"agent": cls.build_agent_configuration(
user_configuration.get("agent", {})
).dict(),
}
system_locations = configuration_dict["agent"]["configuration"]["systems"]
# Build up default configuration
for system_name, system_location in system_locations.items():
logger.debug(f"Compiling configuration for system {system_name}")
system_class = SimplePluginService.get_plugin(system_location)
configuration_dict[system_name] = system_class.build_agent_configuration(
user_configuration.get(system_name, {})
).dict()
return AgentSettings.parse_obj(configuration_dict)
@classmethod
async def determine_agent_name_and_goals(
cls,
user_objective: str,
agent_settings: AgentSettings,
logger: logging.Logger,
) -> dict:
logger.debug("Loading OpenAI provider.")
provider: OpenAIProvider = cls._get_system_instance(
"openai_provider",
agent_settings,
logger=logger,
)
logger.debug("Loading agent planner.")
agent_planner: SimplePlanner = cls._get_system_instance(
"planning",
agent_settings,
logger=logger,
model_providers={"openai": provider},
)
logger.debug("determining agent name and goals.")
model_response = await agent_planner.decide_name_and_goals(
user_objective,
)
return model_response.content
@classmethod
def provision_agent(
cls,
agent_settings: AgentSettings,
logger: logging.Logger,
):
agent_settings.agent.configuration.creation_time = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)
workspace: SimpleWorkspace = cls._get_system_instance(
"workspace",
agent_settings,
logger=logger,
)
return workspace.setup_workspace(agent_settings, logger)
@classmethod
def _get_system_instance(
cls,
system_name: str,
agent_settings: AgentSettings,
logger: logging.Logger,
*args,
**kwargs,
):
system_locations = agent_settings.agent.configuration.systems.dict()
system_settings = getattr(agent_settings, system_name)
system_class = SimplePluginService.get_plugin(system_locations[system_name])
system_instance = system_class(
system_settings,
*args,
logger=logger.getChild(system_name),
**kwargs,
)
return system_instance
def _prune_empty_dicts(d: dict) -> dict:
"""
Prune branches from a nested dictionary if the branch only contains empty dictionaries at the leaves.
Args:
d: The dictionary to prune.
Returns:
The pruned dictionary.
"""
pruned = {}
for key, value in d.items():
if isinstance(value, dict):
pruned_value = _prune_empty_dicts(value)
if (
pruned_value
): # if the pruned dictionary is not empty, add it to the result
pruned[key] = pruned_value
else:
pruned[key] = value
return pruned