Added in agent explination

This commit is contained in:
SwiftyOS
2023-08-30 12:47:29 +02:00
parent 80a8b95b8c
commit 87c6874a54
10 changed files with 132 additions and 442 deletions

3
.gitignore vendored
View File

@@ -168,4 +168,5 @@ agbenchmark
.benchmarks
.mypy_cache
.pytest_cache
.vscode
.vscode
ig_*

View File

@@ -14,13 +14,13 @@ repos:
rev: 5.12.0
hooks:
- id: isort
language_version: python3.10
language_version: python3.11
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.10
language_version: python3.11
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: 'v1.3.0'

View File

@@ -1,12 +1,94 @@
import autogpt.sdk.agent
from autogpt.sdk.db import AgentDB
from autogpt.sdk.schema import Step, StepRequestBody
from autogpt.sdk.workspace import Workspace
class AutoGPTAgent(autogpt.sdk.agent.Agent):
async def create_and_execute_step(
self, task_id: str, step_request: StepRequestBody
) -> Step:
"""
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 possabilities.
Here is a summary of the key components of an agent:
Anatomy of an agent:
- Profile
- Memory
- Planning
- Action
Profile:
Agents typically perform a task by assuming specific roles. For example, a teacher,
a coder, a planner etc. In using the profile in the llm prompt it has been shown to
improve the quality of the output. https://arxiv.org/abs/2305.14688
Additionally baed on the profile selected, the agent could be configured to use a
different llm. The possabilities are endless and the profile can be selected selected
dynamically based on the task at hand.
Memory:
Memory is critical for the agent to acculmulate experiences, self-evolve, and behave
in a more consistent, reasonable, and effective manner. There are many approaches to
memory. However, some thoughts: there is long term and short term or working memory.
You may want different approaches for each. There has also been work exploring the
idea of memory reflection, which is the ability to assess its memories and re-evaluate
them. For example, condensting short term memories into long term memories.
Planning:
When humans face a complex task, they first break it down into simple subtasks and then
solve each subtask one by one. The planning module empowers LLM-based agents with the ability
to think and plan for solving complex tasks, which makes the agent more comprehensive,
powerful, and reliable. The two key methods to consider are: Planning with feedback and planning
without feedback.
Action:
Actions translate the agents decisions into specific outcomes. For example, if the agent
decides to write a file, the action would be to write the file. There are many approaches you
could implement actions.
The Forge has a basic module for each of these areas. However, you are free to implement your own.
This is just a starting point.
"""
def __init__(self, database: AgentDB, workspace: Workspace):
"""
Create a step for the task and execute it.
The database is used to store tasks, steps and artifact metadata. The workspace is used to
store artifacts. The workspace is a directory on the file system.
Feel free to create subclasses of the database and workspace to implement your own storage
"""
return await super().create_and_execute_step(task_id, step_request)
super().__init__(database, workspace)
async def execute_step(self, task_id: str, step_request: StepRequestBody) -> Step:
"""
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 bechmarks 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 essentailly the same as the task request and contains an input
string, for the bechmarks 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.
"""
raise NotImplementedError

View File

@@ -15,7 +15,6 @@ from .middlewares import AgentMiddleware
from .routes.agent_protocol import base_router
from .schema import *
from .tracing import setup_tracing
from .utils import run
from .workspace import Workspace
LOG = CustomLogger(__name__)
@@ -102,54 +101,11 @@ class Agent:
except Exception as e:
raise
async def create_and_execute_step(
self, task_id: str, step_request: StepRequestBody
) -> Step:
async def execute_step(self, task_id: str, step_request: StepRequestBody) -> Step:
"""
Create a step for the task.
"""
if step_request.input != "y":
step = await self.db.create_step(
task_id=task_id,
input=step_request,
additional_input=step_request.additional_input,
)
# utils.run
artifacts = run(step.input)
for artifact in artifacts:
art = await self.db.create_artifact(
task_id=step.task_id,
file_name=artifact["file_name"],
uri=artifact["uri"],
agent_created=True,
step_id=step.step_id,
)
assert isinstance(
art, Artifact
), f"Artifact not instance of Artifact {type(art)}"
step.artifacts.append(art)
step.status = "completed"
else:
steps, steps_pagination = await self.db.list_steps(
task_id, page=1, per_page=100
)
# Find the latest step that has not been completed
step = next((s for s in reversed(steps) if s.status != "completed"), None)
if step is None:
# If all steps have been completed, create a new placeholder step
step = await self.db.create_step(
task_id=task_id,
input="y",
additional_input={},
)
step.status = "completed"
step.is_last = True
step.output = "No more steps to run."
step = await self.db.update_step(step)
if isinstance(step.status, Status):
step.status = step.status.value
step.output = "Done some work"
return step
raise NotImplementedError
async def get_step(self, task_id: str, step_id: str) -> Step:
"""

View File

@@ -371,7 +371,7 @@ async def execute_agent_task_step(
"""
agent = request["agent"]
try:
step = await agent.create_and_execute_step(task_id, step)
step = await agent.execute_step(task_id, step)
return Response(
content=step.json(),
status_code=200,
@@ -539,7 +539,6 @@ async def upload_agent_task_artifacts(
"""
agent = request["agent"]
if file is None:
return Response(
content=json.dumps({"error": "File must be specified"}),

View File

@@ -8,7 +8,6 @@ from datetime import datetime
from enum import Enum
from typing import List, Optional
from fastapi import UploadFile
from pydantic import BaseModel, Field

View File

@@ -1,179 +0,0 @@
"""
TEMPORARY FILE FOR TESTING PURPOSES ONLY WILL BE REMOVED SOON!
-------------------------------------------------------------
PLEASE IGNORE
-------------------------------------------------------------
"""
import glob
import os
import typing
from pathlib import Path
import dotenv
from .forge_log import CustomLogger
LOG = CustomLogger(__name__)
dotenv.load_dotenv()
import openai
import requests
from tenacity import retry, stop_after_attempt, wait_random_exponential
PROJECT_DIR = Path().resolve()
workspace = os.path.join(PROJECT_DIR, "agbenchmark/workspace")
@retry(wait=wait_random_exponential(min=1, max=40), stop=stop_after_attempt(3))
def chat_completion_request(
messages: typing.List[typing.Dict[str, str]],
functions: typing.List[typing.Dict[str, str]] | None = None,
function_call: typing.Optional[str] = None,
model: str = "gpt-3.5-turbo",
temperature: float = 0,
) -> typing.Union[typing.Dict[str, typing.Any], Exception]:
"""Generate a response to a list of messages using OpenAI's API"""
try:
return openai.ChatCompletion.create(
model=model,
messages=messages,
user="TheForge",
temperature=temperature,
)
except Exception as e:
LOG.info("Unable to generate ChatCompletion response")
LOG.info(f"Exception: {e}")
exit()
def run(task: str):
"""Runs the agent for benchmarking"""
LOG.info("Running agent")
steps = plan(task)
execute_plan(steps)
# check for artifacts in workspace
items = glob.glob(os.path.join(workspace, "*"))
if items:
artifacts = []
LOG.info(f"Found {len(items)} artifacts in workspace")
for item in items:
with open(item, "r") as f:
item_contents = f.read()
path_within_workspace = os.path.relpath(item, workspace)
artifacts.append(
{
"file_name": os.path.basename(item),
"uri": f"file://{path_within_workspace}",
"contents": item_contents,
}
)
return artifacts
def execute_plan(plan: typing.List[str]) -> None:
"""Each step is valid python, join the steps together into a python script and execute it"""
script = "\n".join(plan)
LOG.info(f"Executing script: \n{script}")
exec(script)
def plan(task: str) -> typing.List[str]:
"""Returns a list of tasks that needs to be executed to complete the task"""
abilities = """
write_file(contents='The content you want to write', filepath='file_to_write.txt')
read_file(filepath='file_to_write.txt')
"""
json_format = """
{
"steps": [
"write_file(contents='The capital is xxx', filepath='answer.txt')",
"read_file(filepath='file_to_read.txt')",
]
}
"""
planning_prompt = f"""Answer in json format:
Determine the steps needed to complete the following task :
{task}
---
Possible steps:
{abilities}
---
Example answer:
{json_format}
---
As you can see, we only use hard coded values when calling the functions.
Please write your answer below:
"""
messages = [{"role": "user", "content": planning_prompt}]
response = chat_completion_request(messages=messages)
import json
plan = json.loads(response.choices[0].message.content)
return plan["steps"]
def append_to_file(contents: str, filepath: str, to_start: bool) -> bool:
"""Reads in a file then writes the file out with the contents appended to the end or start"""
if workspace not in filepath:
filepath = os.path.join(workspace, filepath)
file_contents = read_file(filepath)
if file_contents is None:
file_contents = ""
if to_start:
contents += file_contents
else:
contents = file_contents + contents
return write_file(contents, filepath)
def write_file(contents: str, filepath: str) -> bool:
"""Creates directory for the file if it doesn't exist, then writes the file"""
if workspace not in filepath:
filepath = os.path.join(workspace, filepath)
success = False
directory = os.path.dirname(filepath)
os.makedirs(directory, exist_ok=True)
try:
with open(filepath, "w") as f:
f.write(contents)
success = True
except Exception as e:
LOG.info(f"Unable to write file: {e}")
return success
def read_file(filepath: str) -> typing.Optional[str]:
"""Reads in the contents of a file"""
if workspace not in filepath:
filepath = os.path.join(workspace, filepath)
contents = None
try:
with open(filepath, "r") as f:
contents = f.read()
except Exception as e:
LOG.info(f"Unable to read file: {e}")
return contents
def read_webpage(url: str) -> typing.Optional[str]:
"""Checks if the url is valid then reads the contents of the webpage"""
contents = None
try:
response = requests.get(url)
if response.status_code == 200:
contents = response.text
except Exception as e:
LOG.info(f"Unable to read webpage: {e}")
return contents
if __name__ == "__main__":
test_messages = [{"role": "user", "content": "Hello, how are you?"}]
response = chat_completion_request(test_messages)
LOG.info(response)

View File

@@ -3,9 +3,6 @@ import os
import typing
from pathlib import Path
import aiohttp
from fastapi import Response
class Workspace(abc.ABC):
@abc.abstractclassmethod

240
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ pytest = "^7.4.0"
pytest-asyncio = "^0.21.1"
watchdog = "^3.0.0"
mock = "^5.1.0"
autoflake = "^2.2.0"
[tool.poetry.group.ui.dependencies]