mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
committed by
GitHub
parent
6ec708c771
commit
9f71cd2437
9
cli.py
9
cli.py
@@ -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}'...")
|
||||
|
||||
@@ -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__":
|
||||
|
||||
229
forge/forge/agent/forge_agent.py
Normal file
229
forge/forge/agent/forge_agent.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
)
|
||||
@@ -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
13
forge/forge/app.py
Normal 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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user