Compare commits

...

4 Commits

Author SHA1 Message Date
SwiftyOS
5f7822d5b7 Added test for async bug 2024-08-21 10:35:42 +02:00
Zamil Majdy
2c1275040f Fix buggy changes 2024-08-21 09:17:24 +02:00
SwiftyOS
e627ad5a64 Update test_input in WaitBlock to include "data" field 2024-08-20 17:12:03 +02:00
SwiftyOS
fc70862f76 Added new blocks and updated old ones 2024-08-20 16:53:43 +02:00
5 changed files with 660 additions and 8 deletions

View File

@@ -1,3 +1,4 @@
import json
from abc import ABC, abstractmethod
from typing import Any, Generic, List, TypeVar
@@ -123,8 +124,10 @@ class ObjectLookupBase(Block, ABC, Generic[T]):
)
def run(self, input_data: ObjectLookupBaseInput[T]) -> BlockOutput:
obj = input_data.input
key = input_data.key
obj = input_data.input
if isinstance(obj, str):
obj = json.loads(obj)
if isinstance(obj, dict) and key in obj:
yield "output", obj[key]
@@ -169,7 +172,7 @@ class OutputBlock(ObjectLookupBase[Any]):
class DictionaryAddEntryBlock(Block):
class Input(BlockSchema):
dictionary: dict | None = SchemaField(
dictionary: dict | None | str = SchemaField(
default=None,
description="The dictionary to add the entry to. If not provided, a new dictionary will be created.",
placeholder='{"key1": "value1", "key2": "value2"}',
@@ -216,6 +219,8 @@ class DictionaryAddEntryBlock(Block):
# If no dictionary is provided, create a new one
if input_data.dictionary is None:
updated_dict = {}
elif isinstance(input_data.dictionary, str):
updated_dict = json.loads(input_data.dictionary)
else:
# Create a copy of the input dictionary to avoid modifying the original
updated_dict = input_data.dictionary.copy()
@@ -300,3 +305,41 @@ class ListAddEntryBlock(Block):
yield "updated_list", updated_list
except Exception as e:
yield "error", f"Failed to add entry to list: {str(e)}"
class CreateListBlock(Block):
class Input(BlockSchema):
items: List[Any] = Field(
description="Items to be added to the list", default=[]
)
class Output(BlockSchema):
list: List[Any] = SchemaField(description="The list with the new entry added.")
error: str = SchemaField(description="Error message if the operation failed.")
def __init__(self):
super().__init__(
id="aeb08fc1-2fc1-4141-bc8e-f758f183a812",
description="Adds a new entry to a list. The entry can be of any type. If no list is provided, a new one is created.",
categories={BlockCategory.BASIC},
input_schema=CreateListBlock.Input,
output_schema=CreateListBlock.Output,
test_input=[
{
"items": [1, "string", {"existing_key": "existing_value"}],
},
],
test_output=[
(
"list",
[
1,
"string",
{"existing_key": "existing_value"},
],
),
],
)
def run(self, input_data: Input) -> BlockOutput:
yield "list", input_data.items

View File

@@ -1,7 +1,7 @@
from typing import Any, List, Tuple
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from autogpt_server.data.model import SchemaField
from autogpt_server.data.model import Field, SchemaField
class ForEachBlock(Block):
@@ -10,9 +10,13 @@ class ForEachBlock(Block):
description="The list of items to iterate over",
placeholder="[1, 2, 3, 4, 5]",
)
return_index: bool = Field(
description="If it should just yield the item or the item and index",
default=True,
)
class Output(BlockSchema):
item: Tuple[int, Any] = SchemaField(
item: Tuple[int, Any] | Any = SchemaField(
description="A tuple with the index and current item in the iteration"
)
@@ -33,4 +37,7 @@ class ForEachBlock(Block):
def run(self, input_data: Input) -> BlockOutput:
for index, item in enumerate(input_data.items):
yield "item", (index, item)
if input_data.return_index:
yield "item", (index, item)
else:
yield "item", item

View File

@@ -1,6 +1,6 @@
import time
from datetime import datetime, timedelta
from typing import Union
from typing import Any, Union
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
@@ -137,3 +137,41 @@ class TimerBlock(Block):
time.sleep(total_seconds)
yield "message", "timer finished"
class WaitBlock(Block):
class Input(BlockSchema):
data: Any
seconds: Union[int, str] = 0
minutes: Union[int, str] = 0
hours: Union[int, str] = 0
days: Union[int, str] = 0
class Output(BlockSchema):
data: Any
message: str
def __init__(self):
super().__init__(
id="d67a9c52-5e4e-11e2-bcfd-0770200c9c61",
description="This block waits a given time period, then returns the input data",
categories={BlockCategory.TEXT},
input_schema=WaitBlock.Input,
output_schema=WaitBlock.Output,
test_input=[{"seconds": 1, "data": "something"}],
test_output=[
("data", "something"),
],
)
def run(self, input_data: Input) -> BlockOutput:
seconds = int(input_data.seconds)
minutes = int(input_data.minutes)
hours = int(input_data.hours)
days = int(input_data.days)
total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400
time.sleep(total_seconds)
yield "data", input_data.data

View File

@@ -0,0 +1,319 @@
{
"id": "dd6eee5f-1ec9-46a9-840a-98d652bfeaef",
"version": 27,
"is_active": true,
"is_template": false,
"name": "Async Bug Graph",
"description": "Agent Description",
"nodes": [
{
"id": "ee1aa2d8-8ae9-49c5-b949-c8bc1f64c1b1",
"block_id": "b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
"input_default": {
"key": "item"
},
"metadata": {
"position": {
"x": 115.43720926499259,
"y": 111.1650147372838
}
}
},
{
"id": "672dc715-1c4a-4845-9ca4-cdea58392389",
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
"input_default": {
"texts": [],
"format": "{path_string} same as {should_be_same}"
},
"metadata": {
"position": {
"x": 2761.2885335656524,
"y": 381.88199866097335
}
}
},
{
"id": "b875a2ee-cc1c-43e5-bfa2-6181c1c267aa",
"block_id": "aeb08fc1-2fc1-4141-bc8e-f758f183a812",
"input_default": {
"items": [
"{\"item\": \"one\"}",
"{\"item\": \"two\"}",
"{\"item\": \"three\"}",
"{\"item\": \"four\"}"
]
},
"metadata": {
"position": {
"x": -907.0675129350209,
"y": -158.6404843200935
}
}
},
{
"id": "8629f421-1792-4cc9-9ff9-2619d330208d",
"block_id": "f3b1c1b2-4c4f-4f0d-8d2f-4c4f0d8d2f4c",
"input_default": {},
"metadata": {
"position": {
"x": 3356.6302763437966,
"y": 365.69194750094584
}
}
},
{
"id": "a796407e-e2aa-4a78-8349-74e8fcf29acd",
"block_id": "31d1064e-7446-4693-a7d4-65e5ca1180d1",
"input_default": {
"key": "path_string"
},
"metadata": {
"position": {
"x": 2022.5433773817927,
"y": 341.365624355902
}
}
},
{
"id": "b88a15a0-b206-4fb7-87d9-6c0855f30706",
"block_id": "31d1064e-7446-4693-a7d4-65e5ca1180d1",
"input_default": {
"key": "should_be_same"
},
"metadata": {
"position": {
"x": 2378.4218596312107,
"y": -662.6702903841343
}
}
},
{
"id": "8c784897-9989-42b0-b46c-f0ff2c552ccf",
"block_id": "f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l",
"input_default": {
"return_index": false
},
"metadata": {
"position": {
"x": -307.80980510121657,
"y": -183.40632582304875
}
}
},
{
"id": "7920e629-a792-4931-bbda-f7e8f48a7f10",
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
"input_default": {
"texts": [],
"format": "{path_string} same as {should_be_same}"
},
"metadata": {
"position": {
"x": 3237.363889239568,
"y": -338.7056564591866
}
}
},
{
"id": "1bf74f10-9c11-4302-9b25-1454c4aa8c1f",
"block_id": "d67a9c52-5e4e-11e2-bcfd-0770200c9c61",
"input_default": {
"seconds": "3"
},
"metadata": {
"position": {
"x": 1423.4009426673902,
"y": 412.93463171958416
}
}
},
{
"id": "d1b1e337-c214-4586-9f35-f297fee7373b",
"block_id": "31d1064e-7446-4693-a7d4-65e5ca1180d1",
"input_default": {
"key": "should_be_same"
},
"metadata": {
"position": {
"x": 2109.5446580301345,
"y": 860.7214483654554
}
}
},
{
"id": "b7eb124c-9e3f-42de-9c51-957f1fdba448",
"block_id": "31d1064e-7446-4693-a7d4-65e5ca1180d1",
"input_default": {
"key": "path_string"
},
"metadata": {
"position": {
"x": 1568.9031528685712,
"y": -160.68552495576077
}
}
},
{
"id": "e6f6208e-528f-4465-a0be-2665d5fa9a77",
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
"input_default": {
"format": "Fast Path Item is {item}"
},
"metadata": {
"position": {
"x": 792.1126241529676,
"y": -556.8424043502238
}
}
},
{
"id": "a8ee5748-55d7-41d1-9e3a-922da1011e20",
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
"input_default": {
"texts": [],
"format": "Slow Path Item is {item}"
},
"metadata": {
"position": {
"x": 740.520265186629,
"y": 84.492978274699
}
}
},
{
"id": "352967f4-8490-4203-b413-a332904521b1",
"block_id": "f3b1c1b2-4c4f-4f0d-8d2f-4c4f0d8d2f4c",
"input_default": {},
"metadata": {
"position": {
"x": 3952.194366925888,
"y": -245.2362702707926
}
}
}
],
"links": [
{
"id": "67516fd1-4c28-4e67-8ade-07d8e78a91f8",
"source_id": "a8ee5748-55d7-41d1-9e3a-922da1011e20",
"sink_id": "1bf74f10-9c11-4302-9b25-1454c4aa8c1f",
"source_name": "output",
"sink_name": "data",
"is_static": false
},
{
"id": "72d5ee64-d4ba-4cf8-a414-345babb1810f",
"source_id": "d1b1e337-c214-4586-9f35-f297fee7373b",
"sink_id": "672dc715-1c4a-4845-9ca4-cdea58392389",
"source_name": "updated_dictionary",
"sink_name": "named_texts",
"is_static": false
},
{
"id": "a73cfe8a-f60b-4fb9-9bd8-cb11b8be5316",
"source_id": "1bf74f10-9c11-4302-9b25-1454c4aa8c1f",
"sink_id": "a796407e-e2aa-4a78-8349-74e8fcf29acd",
"source_name": "data",
"sink_name": "value",
"is_static": false
},
{
"id": "cc213a85-e649-44da-999a-fe8d2bb022a7",
"source_id": "e6f6208e-528f-4465-a0be-2665d5fa9a77",
"sink_id": "b7eb124c-9e3f-42de-9c51-957f1fdba448",
"source_name": "output",
"sink_name": "value",
"is_static": false
},
{
"id": "7f9959af-145a-4fe7-b897-317e51b8f70e",
"source_id": "b88a15a0-b206-4fb7-87d9-6c0855f30706",
"sink_id": "7920e629-a792-4931-bbda-f7e8f48a7f10",
"source_name": "updated_dictionary",
"sink_name": "named_texts",
"is_static": false
},
{
"id": "6eb53905-d954-4236-aec6-fd38c3a18872",
"source_id": "ee1aa2d8-8ae9-49c5-b949-c8bc1f64c1b1",
"sink_id": "b88a15a0-b206-4fb7-87d9-6c0855f30706",
"source_name": "output",
"sink_name": "value",
"is_static": false
},
{
"id": "5a4c2d4a-5914-4855-a72e-1a559ae33e4b",
"source_id": "8c784897-9989-42b0-b46c-f0ff2c552ccf",
"sink_id": "a8ee5748-55d7-41d1-9e3a-922da1011e20",
"source_name": "item",
"sink_name": "named_texts",
"is_static": false
},
{
"id": "05a0fd2c-fe23-4395-a550-b7bc8446ab9f",
"source_id": "7920e629-a792-4931-bbda-f7e8f48a7f10",
"sink_id": "352967f4-8490-4203-b413-a332904521b1",
"source_name": "output",
"sink_name": "text",
"is_static": false
},
{
"id": "1451c239-fcb3-4c7a-8d1c-d668995f7cfd",
"source_id": "ee1aa2d8-8ae9-49c5-b949-c8bc1f64c1b1",
"sink_id": "d1b1e337-c214-4586-9f35-f297fee7373b",
"source_name": "output",
"sink_name": "value",
"is_static": false
},
{
"id": "4be8734c-009c-43ed-a5f8-1e0943df8419",
"source_id": "8c784897-9989-42b0-b46c-f0ff2c552ccf",
"sink_id": "e6f6208e-528f-4465-a0be-2665d5fa9a77",
"source_name": "item",
"sink_name": "named_texts",
"is_static": false
},
{
"id": "1d02f828-acd6-46ca-a84c-0dc17e4890e1",
"source_id": "672dc715-1c4a-4845-9ca4-cdea58392389",
"sink_id": "8629f421-1792-4cc9-9ff9-2619d330208d",
"source_name": "output",
"sink_name": "text",
"is_static": false
},
{
"id": "101f20e5-ec8a-46fc-883d-f01c135e457d",
"source_id": "a796407e-e2aa-4a78-8349-74e8fcf29acd",
"sink_id": "d1b1e337-c214-4586-9f35-f297fee7373b",
"source_name": "updated_dictionary",
"sink_name": "dictionary",
"is_static": false
},
{
"id": "1e41f028-4682-46f7-940e-e0dc6e593d1e",
"source_id": "b7eb124c-9e3f-42de-9c51-957f1fdba448",
"sink_id": "b88a15a0-b206-4fb7-87d9-6c0855f30706",
"source_name": "updated_dictionary",
"sink_name": "dictionary",
"is_static": false
},
{
"id": "c80961ee-166d-4523-95d8-5cceb3975793",
"source_id": "b875a2ee-cc1c-43e5-bfa2-6181c1c267aa",
"sink_id": "8c784897-9989-42b0-b46c-f0ff2c552ccf",
"source_name": "list",
"sink_name": "items",
"is_static": false
},
{
"id": "1685867f-d1b6-46e3-9676-404e9be4fe6d",
"source_id": "8c784897-9989-42b0-b46c-f0ff2c552ccf",
"sink_id": "ee1aa2d8-8ae9-49c5-b949-c8bc1f64c1b1",
"source_name": "item",
"sink_name": "input",
"is_static": false
}
],
"subgraphs": {}
}

View File

@@ -1,7 +1,17 @@
import pytest
from prisma.models import User
from autogpt_server.blocks.basic import ObjectLookupBlock, ValueBlock
from autogpt_server.blocks.basic import (
ObjectLookupBlock,
ValueBlock,
CreateListBlock,
ObjectLookupBase,
DictionaryAddEntryBlock,
PrintingBlock,
)
from autogpt_server.blocks.iteration import ForEachBlock
from autogpt_server.blocks.text import TextFormatterBlock
from autogpt_server.blocks.time_blocks import WaitBlock
from autogpt_server.blocks.maths import MathsBlock, Operation
from autogpt_server.data import execution, graph
from autogpt_server.executor import ExecutionManager
@@ -17,6 +27,7 @@ async def execute_graph(
test_user: User,
input_data: dict,
num_execs: int = 4,
timeout: int = 20,
) -> str:
# --- Test adding new executions --- #
response = await agent_server.execute_graph(test_graph.id, input_data, test_user.id)
@@ -24,7 +35,7 @@ async def execute_graph(
# Execution queue should be empty
assert await wait_execution(
test_manager, test_user.id, test_graph.id, graph_exec_id, num_execs
test_manager, test_user.id, test_graph.id, graph_exec_id, num_execs, timeout=timeout
)
return graph_exec_id
@@ -239,3 +250,237 @@ async def test_static_input_link_on_graph(server):
for exec_data in executions[-3:]:
assert exec_data.status == execution.ExecutionStatus.COMPLETED
assert exec_data.output_data == {"result": [9]}
@pytest.mark.asyncio(scope="session")
async def test_async_bug_graph_behavior(server):
"""
This test is asserting the behaviour of the Async Bug Graph.
Test scenario:
The graph has multiple nodes performing object lookups, formatting texts, and processing lists.
The graph links them in a specific sequence to test asynchronous operations and dependencies.
"""
nodes = [
graph.Node( # Node 0 - executed once
block_id=CreateListBlock().id,
input_default={
"items": [
'{"item": "one"}',
'{"item": "two"}',
'{"item": "three"}',
'{"item": "four"}',
]
},
),
graph.Node( # Node 1 - executed once
block_id=ForEachBlock().id,
input_default={"return_index": False},
),
graph.Node( # Node 2 - executed once per loop
block_id=ObjectLookupBlock().id,
input_default={"key": "item", "input": {}},
),
graph.Node( # Node 3 (TOP) - executed once per loop
block_id=TextFormatterBlock().id,
input_default={"format": "Fast Path Item is {item}"},
),
graph.Node( # Node 4 (BOTTOM) - executed once per loop
block_id=TextFormatterBlock().id,
input_default={"format": "Slow Path Item is {item}"},
),
graph.Node( # Node 5 (TOP) - executed once per loop
block_id=DictionaryAddEntryBlock().id,
input_default={"key": "path_string"},
),
graph.Node( # Node 6 (BOTTOM) - executed once per loop
block_id=WaitBlock().id,
input_default={"seconds": 1},
),
graph.Node( # Node 7 (TOP) - executed once per loop
block_id=DictionaryAddEntryBlock().id,
input_default={"key": "should_be_same"},
),
graph.Node( # Node 8 (BOTTOM) - executed once per loop
block_id=DictionaryAddEntryBlock().id,
input_default={"key": "path_string"},
),
graph.Node( # Node 9 (BOTTOM) - executed once per loop
block_id=DictionaryAddEntryBlock().id,
input_default={"key": "should_be_same"},
),
graph.Node( # Node 10 (TOP) - executed once per loop
block_id=TextFormatterBlock().id,
input_default={"format": "{path_string} same as {should_be_same}"},
),
graph.Node( # Node 11 (BOTTOM) - executed once per loop
block_id=TextFormatterBlock().id,
input_default={"format": "{path_string} same as {should_be_same}"},
),
graph.Node( # Node 12 (TOP) - executed once per loop
block_id=PrintingBlock().id,
),
graph.Node( # Node 13 (BOTTOM) - executed once per loop
block_id=PrintingBlock().id,
),
]
# num execs = 2 initial + 9 per loop = 2 + 9*3 = 29
links = [
graph.Link(
source_id=nodes[0].id,
sink_id=nodes[1].id,
source_name="list",
sink_name="items",
is_static=False,
),
# ForEachBlock needs to be connected to 2x text formmater blocks and object lookup block
graph.Link(
source_id=nodes[1].id, # ForEachBlock
sink_id=nodes[2].id, # ObjectLookupBlock
source_name="item",
sink_name="input",
is_static=False,
),
graph.Link(
source_id=nodes[1].id, # ForEachBlock
sink_id=nodes[3].id, # TextFormatterBlock
source_name="item",
sink_name="named_texts",
is_static=False,
),
graph.Link(
source_id=nodes[1].id, # ForEachBlock
sink_id=nodes[4].id, # TextFormatterBlock
source_name="item",
sink_name="named_texts",
is_static=False,
),
# Top Execution Path
graph.Link(
source_id=nodes[3].id, # TextFormatterBlock
sink_id=nodes[5].id, # DictionaryAddEntryBlock
source_name="output",
sink_name="value",
is_static=False,
),
graph.Link(
source_id=nodes[5].id, # DictionaryAddEntryBlock
sink_id=nodes[7].id, # DictionaryAddEntryBlock
source_name="updated_dictionary",
sink_name="dictionary",
is_static=False,
),
graph.Link(
source_id=nodes[7].id, # DictionaryAddEntryBlock
sink_id=nodes[10].id, # TextFormatterBlock
source_name="updated_dictionary",
sink_name="named_texts",
is_static=False,
),
graph.Link(
source_id=nodes[10].id, # TextFormatterBlock
sink_id=nodes[12].id, # PrintingBlock
source_name="output",
sink_name="text",
is_static=False,
),
# Object Lookup Block needs to be connected to the DictionaryAddEntryBlock
graph.Link(
source_id=nodes[2].id, # ObjectLookupBlock
sink_id=nodes[7].id, # DictionaryAddEntryBlock
source_name="output",
sink_name="value",
),
graph.Link(
source_id=nodes[10].id, # TextFormatterBlock
sink_id=nodes[12].id, # PrintingBlock
source_name="output",
sink_name="text",
is_static=False,
),
# Bottom Execution Path
graph.Link(
source_id=nodes[4].id, # TextFormatterBlock
sink_id=nodes[6].id, # WaitBlock
source_name="output",
sink_name="data",
is_static=False,
),
graph.Link(
source_id=nodes[6].id, # WaitBlock
sink_id=nodes[8].id, # DictionaryAddEntryBlock
source_name="data",
sink_name="value",
is_static=False,
),
graph.Link(
source_id=nodes[8].id, # DictionaryAddEntryBlock
sink_id=nodes[9].id, # DictionaryAddEntryBlock
source_name="updated_dictionary",
sink_name="dictionary",
is_static=False,
),
# Object Lookup Block needs to be connected to the DictionaryAddEntryBlock
graph.Link(
source_id=nodes[2].id, # ObjectLookupBlock
sink_id=nodes[9].id, # DictionaryAddEntryBlock
source_name="output",
sink_name="value",
),
graph.Link(
source_id=nodes[9].id, # DictionaryAddEntryBlock
sink_id=nodes[11].id, # TextFormatterBlock
source_name="updated_dictionary",
sink_name="named_texts",
is_static=False,
),
graph.Link(
source_id=nodes[11].id, # TextFormatterBlock
sink_id=nodes[13].id, # PrintingBlock
source_name="output",
sink_name="text",
is_static=False,
),
]
test_graph = graph.Graph(
name="Async Bug Graph",
description="Agent Description",
nodes=nodes,
links=links,
)
test_user = await create_test_user()
test_graph = await graph.create_graph(test_graph, user_id=test_user.id)
graph_exec_id = await execute_graph(
server.agent_server, server.exec_manager, test_graph, test_user, {}, 54
)
executions = await server.agent_server.get_run_execution_results(
test_graph.id, graph_exec_id, test_user.id
)
assert len(executions) == 54
expected_ouputs = set(
[
"Fast Path Item is one should be the same as one",
"Fast Path Item is two should be the same as two",
"Fast Path Item is three should be the same as three",
"Fast Path Item is four should be the same as four",
"Slow Path Item is one should be the same as one",
"Slow Path Item is two should be the same as two",
"Slow Path Item is three should be the same as three",
"Slow Path Item is four should be the same as four",
]
)
actaul_outputs = set()
for exec_data in executions:
if "text" in exec_data.input_data:
output = exec_data.input_data["text"]
actaul_outputs.add(output)
assert expected_ouputs.isdisjoint(actaul_outputs), f"Actual: {actaul_outputs}"