feat(forge): Unbreak forge agent (#7196)

Revert some changes to fix forge agent and enable components support.
- Rename forge `Agent` to `ProtocolAgent`
- Bring back and update `forge/app.py` and `forge/agent/forge_agent.py`
- `ForgeAgent` inherits from `BaseAgent`, supports component execution and runs the same pipelines as autogpt Agent
- Update forge version from 0.1.0 to 0.2.0
- Update code comments
This commit is contained in:
Krzysztof Czerwinski
2024-06-12 13:45:00 +01:00
committed by GitHub
parent 6ec708c771
commit 9f71cd2437
9 changed files with 273 additions and 35 deletions

9
cli.py
View File

@@ -149,10 +149,11 @@ def start(agent_name: str, no_setup: bool):
setup_process.wait()
click.echo()
subprocess.Popen(["./run_benchmark", "serve"], cwd=agent_dir)
click.echo("⌛ (Re)starting benchmark server...")
wait_until_conn_ready(8080)
click.echo()
# FIXME: Doesn't work: Command not found: agbenchmark
# subprocess.Popen(["./run_benchmark", "serve"], cwd=agent_dir)
# click.echo("⌛ (Re)starting benchmark server...")
# wait_until_conn_ready(8080)
# click.echo()
subprocess.Popen(["./run"], cwd=agent_dir)
click.echo(f"⌛ (Re)starting agent '{agent_name}'...")

View File

@@ -27,7 +27,7 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
888 "Y88P" 888 "Y88888 "Y8888
888
Y8b d88P
"Y88P" v0.1.0
"Y88P" v0.2.0
\n"""
if __name__ == "__main__":

View File

@@ -0,0 +1,229 @@
import inspect
import logging
from typing import Any, Optional
from uuid import uuid4
from forge.agent.base import BaseAgent, BaseAgentSettings
from forge.agent.protocols import (
AfterExecute,
CommandProvider,
DirectiveProvider,
MessageProvider,
)
from forge.agent_protocol.agent import ProtocolAgent
from forge.agent_protocol.database.db import AgentDB
from forge.agent_protocol.models.task import (
Step,
StepRequestBody,
Task,
TaskRequestBody,
)
from forge.command.command import Command
from forge.components.system.system import SystemComponent
from forge.config.ai_profile import AIProfile
from forge.file_storage.base import FileStorage
from forge.llm.prompting.schema import ChatPrompt
from forge.llm.prompting.utils import dump_prompt
from forge.llm.providers.schema import AssistantFunctionCall
from forge.llm.providers.utils import function_specs_from_commands
from forge.models.action import (
ActionErrorResult,
ActionProposal,
ActionResult,
ActionSuccessResult,
)
from forge.utils.exceptions import AgentException, AgentTerminated
logger = logging.getLogger(__name__)
class ForgeAgent(ProtocolAgent, BaseAgent):
"""
The goal of the Forge is to take care of the boilerplate code,
so you can focus on agent design.
There is a great paper surveying the agent landscape: https://arxiv.org/abs/2308.11432
Which I would highly recommend reading as it will help you understand the possibilities.
ForgeAgent provides component support; https://docs.agpt.co/forge/components/introduction/
Using Components is a new way of building agents that is more flexible and easier to extend.
Components replace some agent's logic and plugins with a more modular and composable system.
""" # noqa: E501
def __init__(self, database: AgentDB, workspace: FileStorage):
"""
The database is used to store tasks, steps and artifact metadata.
The workspace is used to store artifacts (files).
"""
# An example agent information; you can modify this to suit your needs
state = BaseAgentSettings(
name="Forge Agent",
description="The Forge Agent is a generic agent that can solve tasks.",
agent_id=str(uuid4()),
ai_profile=AIProfile(
ai_name="ForgeAgent", ai_role="Generic Agent", ai_goals=["Solve tasks"]
),
task="Solve tasks",
)
# ProtocolAgent adds the Agent Protocol (API) functionality
ProtocolAgent.__init__(self, database, workspace)
# BaseAgent provides the component handling functionality
BaseAgent.__init__(self, state)
# AGENT COMPONENTS
# Components provide additional functionality to the agent
# There are NO components added by default in the BaseAgent
# You can create your own components or add existing ones
# Built-in components:
# https://docs.agpt.co/forge/components/built-in-components/
# System component provides "finish" command and adds some prompt information
self.system = SystemComponent()
async def create_task(self, task_request: TaskRequestBody) -> Task:
"""
The agent protocol, which is the core of the Forge,
works by creating a task and then executing steps for that task.
This method is called when the agent is asked to create a task.
We are hooking into function to add a custom log message.
Though you can do anything you want here.
"""
task = await super().create_task(task_request)
logger.info(
f"📦 Task created with ID: {task.task_id} and "
f"input: {task.input[:40]}{'...' if len(task.input) > 40 else ''}"
)
return task
async def execute_step(self, task_id: str, step_request: StepRequestBody) -> Step:
"""
Preffered method to add agent logic is to add custom components:
https://docs.agpt.co/forge/components/creating-components/
Outdated tutorial on how to add custom logic:
https://aiedge.medium.com/autogpt-forge-e3de53cc58ec
The agent protocol, which is the core of the Forge, works by creating a task and then
executing steps for that task. This method is called when the agent is asked to execute
a step.
The task that is created contains an input string, for the benchmarks this is the task
the agent has been asked to solve and additional input, which is a dictionary and
could contain anything.
If you want to get the task use:
```
task = await self.db.get_task(task_id)
```
The step request body is essentially the same as the task request and contains an input
string, for the benchmarks this is the task the agent has been asked to solve and
additional input, which is a dictionary and could contain anything.
You need to implement logic that will take in this step input and output the completed step
as a step object. You can do everything in a single step or you can break it down into
multiple steps. Returning a request to continue in the step output, the user can then decide
if they want the agent to continue or not.
""" # noqa: E501
step = await self.db.create_step(
task_id=task_id, input=step_request, is_last=False
)
proposal = await self.propose_action()
output = await self.execute(proposal)
if isinstance(output, ActionSuccessResult):
step.output = str(output.outputs)
elif isinstance(output, ActionErrorResult):
step.output = output.reason
return step
async def propose_action(self) -> ActionProposal:
self.reset_trace()
# Get directives
directives = self.state.directives.copy(deep=True)
directives.resources += await self.run_pipeline(DirectiveProvider.get_resources)
directives.constraints += await self.run_pipeline(
DirectiveProvider.get_constraints
)
directives.best_practices += await self.run_pipeline(
DirectiveProvider.get_best_practices
)
# Get commands
self.commands = await self.run_pipeline(CommandProvider.get_commands)
# Get messages
messages = await self.run_pipeline(MessageProvider.get_messages)
prompt: ChatPrompt = ChatPrompt(
messages=messages, functions=function_specs_from_commands(self.commands)
)
logger.debug(f"Executing prompt:\n{dump_prompt(prompt)}")
# Call the LLM and parse result
# THIS NEEDS TO BE REPLACED WITH YOUR LLM CALL/LOGIC
# Have a look at autogpt/agents/agent.py for an example (complete_and_parse)
proposal = ActionProposal(
thoughts="I cannot solve the task!",
use_tool=AssistantFunctionCall(
name="finish", arguments={"reason": "Unimplemented logic"}
),
)
self.config.cycle_count += 1
return proposal
async def execute(self, proposal: Any, user_feedback: str = "") -> ActionResult:
tool = proposal.use_tool
# Get commands
self.commands = await self.run_pipeline(CommandProvider.get_commands)
# Execute the command
try:
command: Optional[Command] = None
for c in reversed(self.commands):
if tool.name in c.names:
command = c
if command is None:
raise AgentException(f"Command {tool.name} not found")
command_result = command(**tool.arguments)
if inspect.isawaitable(command_result):
command_result = await command_result
result = ActionSuccessResult(outputs=command_result)
except AgentTerminated:
result = ActionSuccessResult(outputs="Agent terminated or finished")
except AgentException as e:
result = ActionErrorResult.from_exception(e)
logger.warning(f"{tool} raised an error: {e}")
await self.run_pipeline(AfterExecute.after_execute, result)
logger.debug("\n".join(self.trace))
return result
async def do_not_execute(
self, denied_proposal: Any, user_feedback: str
) -> ActionResult:
result = ActionErrorResult(reason="Action denied")
await self.run_pipeline(AfterExecute.after_execute, result)
logger.debug("\n".join(self.trace))
return result

View File

@@ -28,7 +28,7 @@ from forge.file_storage.base import FileStorage
logger = logging.getLogger(__name__)
class Agent:
class ProtocolAgent:
def __init__(self, database: AgentDB, workspace: FileStorage):
self.db = database
self.workspace = workspace

View File

@@ -3,17 +3,12 @@ from pathlib import Path
import pytest
from fastapi import UploadFile
from forge.agent_protocol.database.db import AgentDB
from forge.agent_protocol.models.task import (
StepRequestBody,
Task,
TaskListResponse,
TaskRequestBody,
)
from forge.file_storage.base import FileStorageConfiguration
from forge.file_storage.local import LocalFileStorage
from .agent import Agent
from .agent import ProtocolAgent
from .database.db import AgentDB
from .models.task import StepRequestBody, Task, TaskListResponse, TaskRequestBody
@pytest.fixture
@@ -21,7 +16,7 @@ def agent(test_workspace: Path):
db = AgentDB("sqlite:///test.db")
config = FileStorageConfiguration(root=test_workspace)
workspace = LocalFileStorage(config)
return Agent(db, workspace)
return ProtocolAgent(db, workspace)
@pytest.fixture
@@ -33,7 +28,7 @@ def file_upload():
@pytest.mark.asyncio
async def test_create_task(agent: Agent):
async def test_create_task(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -42,7 +37,7 @@ async def test_create_task(agent: Agent):
@pytest.mark.asyncio
async def test_list_tasks(agent: Agent):
async def test_list_tasks(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -52,7 +47,7 @@ async def test_list_tasks(agent: Agent):
@pytest.mark.asyncio
async def test_get_task(agent: Agent):
async def test_get_task(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -63,7 +58,7 @@ async def test_get_task(agent: Agent):
@pytest.mark.xfail(reason="execute_step is not implemented")
@pytest.mark.asyncio
async def test_execute_step(agent: Agent):
async def test_execute_step(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -78,7 +73,7 @@ async def test_execute_step(agent: Agent):
@pytest.mark.xfail(reason="execute_step is not implemented")
@pytest.mark.asyncio
async def test_get_step(agent: Agent):
async def test_get_step(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -92,7 +87,7 @@ async def test_get_step(agent: Agent):
@pytest.mark.asyncio
async def test_list_artifacts(agent: Agent):
async def test_list_artifacts(agent: ProtocolAgent):
tasks = await agent.list_tasks()
assert tasks.tasks, "No tasks in test.db"
@@ -101,7 +96,7 @@ async def test_list_artifacts(agent: Agent):
@pytest.mark.asyncio
async def test_create_artifact(agent: Agent, file_upload: UploadFile):
async def test_create_artifact(agent: ProtocolAgent, file_upload: UploadFile):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -116,7 +111,7 @@ async def test_create_artifact(agent: Agent, file_upload: UploadFile):
@pytest.mark.asyncio
async def test_create_and_get_artifact(agent: Agent, file_upload: UploadFile):
async def test_create_and_get_artifact(agent: ProtocolAgent, file_upload: UploadFile):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)

View File

@@ -24,7 +24,7 @@ from .models import (
)
if TYPE_CHECKING:
from forge.agent.agent import Agent
from .agent import ProtocolAgent
base_router = APIRouter()
logger = logging.getLogger(__name__)
@@ -73,7 +73,7 @@ async def create_agent_task(request: Request, task_request: TaskRequestBody) ->
"artifacts": [],
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
task = await agent.create_task(task_request)
@@ -124,7 +124,7 @@ async def list_agent_tasks(
}
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
tasks = await agent.list_tasks(page, page_size)
return tasks
@@ -185,7 +185,7 @@ async def get_agent_task(request: Request, task_id: str) -> Task:
]
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
task = await agent.get_task(task_id)
return task
@@ -239,7 +239,7 @@ async def list_agent_task_steps(
}
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
steps = await agent.list_steps(task_id, page, page_size)
return steps
@@ -298,7 +298,7 @@ async def execute_agent_task_step(
...
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
# An empty step request represents a yes to continue command
if not step_request:
@@ -337,7 +337,7 @@ async def get_agent_task_step(request: Request, task_id: str, step_id: str) -> S
...
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
step = await agent.get_step(task_id, step_id)
return step
@@ -388,7 +388,7 @@ async def list_agent_task_artifacts(
}
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
artifacts = await agent.list_artifacts(task_id, page, page_size)
return artifacts
@@ -430,7 +430,7 @@ async def upload_agent_task_artifacts(
"file_name": "main.py"
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
if file is None:
raise HTTPException(status_code=400, detail="File must be specified")
@@ -468,7 +468,7 @@ async def download_agent_task_artifact(
Response:
<file_content_of_artifact>
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
return await agent.get_artifact(task_id, artifact_id)
except Exception:

13
forge/forge/app.py Normal file
View File

@@ -0,0 +1,13 @@
import os
from pathlib import Path
from forge.agent.forge_agent import ForgeAgent
from forge.agent_protocol.database.db import AgentDB
from forge.file_storage import FileStorageBackendName, get_storage
database_name = os.getenv("DATABASE_STRING")
workspace = get_storage(FileStorageBackendName.LOCAL, root_path=Path("workspace"))
database = AgentDB(database_name, debug_enabled=False)
agent = ForgeAgent(database=database, workspace=workspace)
app = agent.get_agent_app()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "AutoGPT-Forge"
version = "0.1.0"
version = "0.2.0"
description = ""
authors = ["AutoGPT <support@agpt.co>"]
license = "MIT"

View File

@@ -464,7 +464,7 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
888 "Y88P" 888 "Y88888 "Y8888
888
Y8b d88P
"Y88P" v0.1.0
"Y88P" v0.2.0
[2023-09-27 15:39:07,832] [forge.sdk.agent] [INFO] 📝 Agent server starting on http://localhost:8000