Add OpenAI function call support (#4683)

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
This commit is contained in:
Erik Peterson
2023-06-21 19:52:44 -07:00
committed by GitHub
parent 32038c9f5b
commit 857d26d101
23 changed files with 416 additions and 180 deletions

View File

@@ -1,7 +1,9 @@
import pytest
from autogpt.agent.agent_manager import AgentManager
from autogpt.llm import ChatModelResponse
from autogpt.llm.chat import create_chat_completion
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS
@pytest.fixture
@@ -27,12 +29,16 @@ def model():
@pytest.fixture(autouse=True)
def mock_create_chat_completion(mocker):
def mock_create_chat_completion(mocker, config):
mock_create_chat_completion = mocker.patch(
"autogpt.agent.agent_manager.create_chat_completion",
wraps=create_chat_completion,
)
mock_create_chat_completion.return_value = "irrelevant"
mock_create_chat_completion.return_value = ChatModelResponse(
model_info=OPEN_AI_CHAT_MODELS[config.fast_llm_model],
content="irrelevant",
function_call={},
)
return mock_create_chat_completion

View File

@@ -5,10 +5,13 @@ from pathlib import Path
import pytest
from autogpt.models.command import Command
from autogpt.models.command import Command, CommandParameter
from autogpt.models.command_registry import CommandRegistry
SIGNATURE = "(arg1: int, arg2: str) -> str"
PARAMETERS = [
CommandParameter("arg1", "int", description="Argument 1", required=True),
CommandParameter("arg2", "str", description="Argument 2", required=False),
]
class TestCommand:
@@ -26,13 +29,16 @@ class TestCommand:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
assert cmd.name == "example"
assert cmd.description == "Example command"
assert cmd.method == self.example_command_method
assert cmd.signature == "(arg1: int, arg2: str) -> str"
assert (
str(cmd)
== "example: Example command, params: (arg1: int, arg2: Optional[str])"
)
def test_command_call(self):
"""Test that Command(*args) calls and returns the result of method(*args)."""
@@ -41,13 +47,14 @@ class TestCommand:
name="example",
description="Example command",
method=self.example_command_method,
signature={
"prompt": {
"type": "string",
"description": "The prompt used to generate the image",
"required": True,
},
},
parameters=[
CommandParameter(
name="prompt",
type="string",
description="The prompt used to generate the image",
required=True,
),
],
)
result = cmd(arg1=1, arg2="test")
assert result == "1 - test"
@@ -58,22 +65,11 @@ class TestCommand:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
with pytest.raises(TypeError):
cmd(arg1="invalid", does_not_exist="test")
def test_command_custom_signature(self):
custom_signature = "custom_arg1: int, custom_arg2: str"
cmd = Command(
name="example",
description="Example command",
method=self.example_command_method,
signature=custom_signature,
)
assert cmd.signature == custom_signature
class TestCommandRegistry:
@staticmethod
@@ -87,7 +83,7 @@ class TestCommandRegistry:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
registry.register(cmd)
@@ -102,7 +98,7 @@ class TestCommandRegistry:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
registry.register(cmd)
@@ -117,7 +113,7 @@ class TestCommandRegistry:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
registry.register(cmd)
@@ -139,7 +135,7 @@ class TestCommandRegistry:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
registry.register(cmd)
@@ -161,13 +157,13 @@ class TestCommandRegistry:
name="example",
description="Example command",
method=self.example_command_method,
signature=SIGNATURE,
parameters=PARAMETERS,
)
registry.register(cmd)
command_prompt = registry.command_prompt()
assert f"(arg1: int, arg2: str)" in command_prompt
assert f"(arg1: int, arg2: Optional[str])" in command_prompt
def test_import_mock_commands_module(self):
"""Test that the registry can import a module with mock command plugins."""

View File

@@ -7,7 +7,7 @@ import pytest
from autogpt.agent import Agent
from autogpt.config import AIConfig
from autogpt.config.config import Config
from autogpt.llm.base import ChatSequence, Message
from autogpt.llm.base import ChatModelResponse, ChatSequence, Message
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS
from autogpt.llm.utils import count_string_tokens
from autogpt.memory.message_history import MessageHistory
@@ -45,10 +45,14 @@ def test_message_history_batch_summary(mocker, agent, config):
message_count = 0
# Setting the mock output and inputs
mock_summary_text = "I executed browse_website command for each of the websites returned from Google search, but none of them have any job openings."
mock_summary_response = ChatModelResponse(
model_info=OPEN_AI_CHAT_MODELS[model],
content="I executed browse_website command for each of the websites returned from Google search, but none of them have any job openings.",
function_call={},
)
mock_summary = mocker.patch(
"autogpt.memory.message_history.create_chat_completion",
return_value=mock_summary_text,
return_value=mock_summary_response,
)
system_prompt = 'You are AIJobSearcher, an AI designed to search for job openings for software engineer role\nYour decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.\n\nGOALS:\n\n1. Find any job openings for software engineers online\n2. Go through each of the websites and job openings to summarize their requirements and URL, and skip that if you already visit the website\n\nIt takes money to let you run. Your API budget is $5.000\n\nConstraints:\n1. ~4000 word limit for short term memory. Your short term memory is short, so immediately save important information to files.\n2. If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.\n3. No user assistance\n4. Exclusively use the commands listed in double quotes e.g. "command name"\n\nCommands:\n1. google_search: Google Search, args: "query": "<query>"\n2. browse_website: Browse Website, args: "url": "<url>", "question": "<what_you_want_to_find_on_website>"\n3. task_complete: Task Complete (Shutdown), args: "reason": "<reason>"\n\nResources:\n1. Internet access for searches and information gathering.\n2. Long Term memory management.\n3. GPT-3.5 powered Agents for delegation of simple tasks.\n4. File output.\n\nPerformance Evaluation:\n1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.\n2. Constructively self-criticize your big-picture behavior constantly.\n3. Reflect on past decisions and strategies to refine your approach.\n4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.\n5. Write all code to a file.\n\nYou should only respond in JSON format as described below \nResponse Format: \n{\n "thoughts": {\n "text": "thought",\n "reasoning": "reasoning",\n "plan": "- short bulleted\\n- list that conveys\\n- long-term plan",\n "criticism": "constructive self-criticism",\n "speak": "thoughts summary to say to user"\n },\n "command": {\n "name": "command name",\n "args": {\n "arg name": "value"\n }\n }\n} \nEnsure the response can be parsed by Python json.loads'
@@ -139,6 +143,6 @@ def test_message_history_batch_summary(mocker, agent, config):
assert new_summary_message == Message(
role="system",
content="This reminds you of these events from your past: \n"
+ mock_summary_text,
+ mock_summary_response.content,
type=None,
)

View File

@@ -1,115 +1,152 @@
from unittest import TestCase
from autogpt.prompts.generator import PromptGenerator
class TestPromptGenerator(TestCase):
def test_add_constraint():
"""
Test cases for the PromptGenerator class, which is responsible for generating
prompts for the AI with constraints, commands, resources, and performance evaluations.
Test if the add_constraint() method adds a constraint to the generator's constraints list.
"""
constraint = "Constraint1"
generator = PromptGenerator()
generator.add_constraint(constraint)
assert constraint in generator.constraints
def test_add_command():
"""
Test if the add_command() method adds a command to the generator's commands list.
"""
command_label = "Command Label"
command_name = "command_name"
args = {"arg1": "value1", "arg2": "value2"}
generator = PromptGenerator()
generator.add_command(command_label, command_name, args)
command = {
"label": command_label,
"name": command_name,
"args": args,
"function": None,
}
assert command in generator.commands
def test_add_resource():
"""
Test if the add_resource() method adds a resource to the generator's resources list.
"""
resource = "Resource1"
generator = PromptGenerator()
generator.add_resource(resource)
assert resource in generator.resources
def test_add_performance_evaluation():
"""
Test if the add_performance_evaluation() method adds an evaluation to the generator's
performance_evaluation list.
"""
evaluation = "Evaluation1"
generator = PromptGenerator()
generator.add_performance_evaluation(evaluation)
assert evaluation in generator.performance_evaluation
def test_generate_prompt_string(config):
"""
Test if the generate_prompt_string() method generates a prompt string with all the added
constraints, commands, resources, and evaluations.
"""
@classmethod
def setUpClass(cls):
"""
Set up the initial state for each test method by creating an instance of PromptGenerator.
"""
cls.generator = PromptGenerator()
# Define the test data
constraints = ["Constraint1", "Constraint2"]
commands = [
{
"label": "Command1",
"name": "command_name1",
"args": {"arg1": "value1"},
},
{
"label": "Command2",
"name": "command_name2",
"args": {},
},
]
resources = ["Resource1", "Resource2"]
evaluations = ["Evaluation1", "Evaluation2"]
# Test whether the add_constraint() method adds a constraint to the generator's constraints list
def test_add_constraint(self):
"""
Test if the add_constraint() method adds a constraint to the generator's constraints list.
"""
constraint = "Constraint1"
self.generator.add_constraint(constraint)
self.assertIn(constraint, self.generator.constraints)
# Add test data to the generator
generator = PromptGenerator()
for constraint in constraints:
generator.add_constraint(constraint)
for command in commands:
generator.add_command(command["label"], command["name"], command["args"])
for resource in resources:
generator.add_resource(resource)
for evaluation in evaluations:
generator.add_performance_evaluation(evaluation)
# Test whether the add_command() method adds a command to the generator's commands list
def test_add_command(self):
"""
Test if the add_command() method adds a command to the generator's commands list.
"""
command_label = "Command Label"
command_name = "command_name"
args = {"arg1": "value1", "arg2": "value2"}
self.generator.add_command(command_label, command_name, args)
command = {
"label": command_label,
"name": command_name,
"args": args,
"function": None,
}
self.assertIn(command, self.generator.commands)
# Generate the prompt string and verify its correctness
prompt_string = generator.generate_prompt_string(config)
assert prompt_string is not None
def test_add_resource(self):
"""
Test if the add_resource() method adds a resource to the generator's resources list.
"""
resource = "Resource1"
self.generator.add_resource(resource)
self.assertIn(resource, self.generator.resources)
# Check if all constraints, commands, resources, and evaluations are present in the prompt string
for constraint in constraints:
assert constraint in prompt_string
for command in commands:
assert command["name"] in prompt_string
for key, value in command["args"].items():
assert f'"{key}": "{value}"' in prompt_string
for resource in resources:
assert resource in prompt_string
for evaluation in evaluations:
assert evaluation in prompt_string
def test_add_performance_evaluation(self):
"""
Test if the add_performance_evaluation() method adds an evaluation to the generator's
performance_evaluation list.
"""
evaluation = "Evaluation1"
self.generator.add_performance_evaluation(evaluation)
self.assertIn(evaluation, self.generator.performance_evaluation)
def test_generate_prompt_string(self):
"""
Test if the generate_prompt_string() method generates a prompt string with all the added
constraints, commands, resources, and evaluations.
"""
# Define the test data
constraints = ["Constraint1", "Constraint2"]
commands = [
{
"label": "Command1",
"name": "command_name1",
"args": {"arg1": "value1"},
},
{
"label": "Command2",
"name": "command_name2",
"args": {},
},
]
resources = ["Resource1", "Resource2"]
evaluations = ["Evaluation1", "Evaluation2"]
def test_generate_prompt_string(config):
"""
Test if the generate_prompt_string() method generates a prompt string with all the added
constraints, commands, resources, and evaluations.
"""
# Add test data to the generator
for constraint in constraints:
self.generator.add_constraint(constraint)
for command in commands:
self.generator.add_command(
command["label"], command["name"], command["args"]
)
for resource in resources:
self.generator.add_resource(resource)
for evaluation in evaluations:
self.generator.add_performance_evaluation(evaluation)
# Define the test data
constraints = ["Constraint1", "Constraint2"]
commands = [
{
"label": "Command1",
"name": "command_name1",
"args": {"arg1": "value1"},
},
{
"label": "Command2",
"name": "command_name2",
"args": {},
},
]
resources = ["Resource1", "Resource2"]
evaluations = ["Evaluation1", "Evaluation2"]
# Generate the prompt string and verify its correctness
prompt_string = self.generator.generate_prompt_string()
self.assertIsNotNone(prompt_string)
# Add test data to the generator
generator = PromptGenerator()
for constraint in constraints:
generator.add_constraint(constraint)
for command in commands:
generator.add_command(command["label"], command["name"], command["args"])
for resource in resources:
generator.add_resource(resource)
for evaluation in evaluations:
generator.add_performance_evaluation(evaluation)
# Check if all constraints, commands, resources, and evaluations are present in the prompt string
for constraint in constraints:
self.assertIn(constraint, prompt_string)
for command in commands:
self.assertIn(command["name"], prompt_string)
for key, value in command["args"].items():
self.assertIn(f'"{key}": "{value}"', prompt_string)
for resource in resources:
self.assertIn(resource, prompt_string)
for evaluation in evaluations:
self.assertIn(evaluation, prompt_string)
# Generate the prompt string and verify its correctness
prompt_string = generator.generate_prompt_string(config)
assert prompt_string is not None
self.assertIn("constraints", prompt_string.lower())
self.assertIn("commands", prompt_string.lower())
self.assertIn("resources", prompt_string.lower())
self.assertIn("performance evaluation", prompt_string.lower())
# Check if all constraints, commands, resources, and evaluations are present in the prompt string
for constraint in constraints:
assert constraint in prompt_string
for command in commands:
assert command["name"] in prompt_string
for key, value in command["args"].items():
assert f'"{key}": "{value}"' in prompt_string
for resource in resources:
assert resource in prompt_string
for evaluation in evaluations:
assert evaluation in prompt_string