From 85370c81b6e3678d6d7b613eb77c28d36486cda2 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Sun, 3 Nov 2024 22:21:56 +0700 Subject: [PATCH 1/7] refactor(backend): Merge GraphMeta & Graph, remove subgraph --- .../backend/backend/blocks/branching.py | 25 +- .../backend/backend/data/execution.py | 41 +-- .../backend/backend/data/graph.py | 270 +++++------------- .../backend/backend/data/includes.py | 29 ++ .../backend/backend/executor/manager.py | 39 +-- .../backend/backend/server/rest_api.py | 44 +-- .../migration.sql | 11 + autogpt_platform/backend/schema.prisma | 5 - .../backend/test/data/test_graph.py | 145 +++------- 9 files changed, 203 insertions(+), 406 deletions(-) create mode 100644 autogpt_platform/backend/backend/data/includes.py create mode 100644 autogpt_platform/backend/migrations/20241103133307_remove_subgraph/migration.sql diff --git a/autogpt_platform/backend/backend/blocks/branching.py b/autogpt_platform/backend/backend/blocks/branching.py index d651b595ce23..93be02e8f925 100644 --- a/autogpt_platform/backend/backend/blocks/branching.py +++ b/autogpt_platform/backend/backend/blocks/branching.py @@ -71,11 +71,12 @@ def __init__(self): ) def run(self, input_data: Input, **kwargs) -> BlockOutput: - value1 = input_data.value1 operator = input_data.operator - value2 = input_data.value2 + # cast value 1 and value 2 to float or int if possible + value1 = float(input_data.value1) + value2 = float(input_data.value2) yes_value = input_data.yes_value if input_data.yes_value is not None else value1 - no_value = input_data.no_value if input_data.no_value is not None else value1 + no_value = input_data.no_value if input_data.no_value is not None else value2 comparison_funcs = { ComparisonOperator.EQUAL: lambda a, b: a == b, @@ -86,17 +87,11 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: ComparisonOperator.LESS_THAN_OR_EQUAL: lambda a, b: a <= b, } - try: - result = comparison_funcs[operator](value1, value2) + result = comparison_funcs[operator](value1, value2) - yield "result", result + yield "result", result - if result: - yield "yes_output", yes_value - else: - yield "no_output", no_value - - except Exception: - yield "result", None - yield "yes_output", None - yield "no_output", None + if result: + yield "yes_output", yes_value + else: + yield "no_output", no_value diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py index aaf06ffef7f4..5b7d34a3b272 100644 --- a/autogpt_platform/backend/backend/data/execution.py +++ b/autogpt_platform/backend/backend/data/execution.py @@ -9,14 +9,11 @@ AgentNodeExecution, AgentNodeExecutionInputOutput, ) -from prisma.types import ( - AgentGraphExecutionInclude, - AgentGraphExecutionWhereInput, - AgentNodeExecutionInclude, -) +from prisma.types import AgentGraphExecutionWhereInput from pydantic import BaseModel from backend.data.block import BlockData, BlockInput, CompletedBlockOutput +from backend.data.includes import EXECUTION_RESULT_INCLUDE, GRAPH_EXECUTION_INCLUDE from backend.util import json, mock @@ -110,24 +107,6 @@ def from_db(execution: AgentNodeExecution): # --------------------- Model functions --------------------- # -EXECUTION_RESULT_INCLUDE: AgentNodeExecutionInclude = { - "Input": True, - "Output": True, - "AgentNode": True, - "AgentGraphExecution": True, -} - -GRAPH_EXECUTION_INCLUDE: AgentGraphExecutionInclude = { - "AgentNodeExecutions": { - "include": { - "Input": True, - "Output": True, - "AgentNode": True, - "AgentGraphExecution": True, - } - } -} - async def create_graph_execution( graph_id: str, @@ -268,21 +247,9 @@ async def update_graph_execution_start_time(graph_exec_id: str): async def update_graph_execution_stats( graph_exec_id: str, - error: Exception | None, - wall_time: float, - cpu_time: float, - node_count: int, + stats: dict[str, Any], ): - status = ExecutionStatus.FAILED if error else ExecutionStatus.COMPLETED - stats = ( - { - "walltime": wall_time, - "cputime": cpu_time, - "nodecount": node_count, - "error": str(error) if error else None, - }, - ) - + status = ExecutionStatus.FAILED if stats.get("error") else ExecutionStatus.COMPLETED await AgentGraphExecution.prisma().update( where={"id": graph_exec_id}, data={ diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index b4f8f8aeb739..a6ecab65c2f8 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -6,14 +6,13 @@ import prisma.types from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink -from prisma.types import AgentGraphInclude from pydantic import BaseModel -from pydantic_core import PydanticUndefinedType +from pydantic.fields import computed_field -from backend.blocks.basic import AgentInputBlock, AgentOutputBlock -from backend.data.block import BlockInput, get_block, get_blocks +from backend.data.block import BlockInput, BlockType, get_block, get_blocks from backend.data.db import BaseDbModel, transaction from backend.data.execution import ExecutionStatus +from backend.data.includes import AGENT_GRAPH_INCLUDE, AGENT_NODE_INCLUDE from backend.util import json logger = logging.getLogger(__name__) @@ -21,8 +20,15 @@ class InputSchemaItem(BaseModel): node_id: str + title: str | None = None description: str | None = None + default: Any | None = None + + +class OutputSchemaItem(BaseModel): + node_id: str title: str | None = None + description: str | None = None class Link(BaseDbModel): @@ -69,7 +75,7 @@ def from_db(node: AgentNode): return obj -class ExecutionMeta(BaseDbModel): +class GraphExecution(BaseDbModel): execution_id: str started_at: datetime ended_at: datetime @@ -78,20 +84,19 @@ class ExecutionMeta(BaseDbModel): status: ExecutionStatus @staticmethod - def from_agent_graph_execution(execution: AgentGraphExecution): + def from_db(execution: AgentGraphExecution): now = datetime.now(timezone.utc) start_time = execution.startedAt or execution.createdAt end_time = execution.updatedAt or now duration = (end_time - start_time).total_seconds() + total_run_time = duration - total_run_time = 0 - if execution.AgentNodeExecutions: - for node_execution in execution.AgentNodeExecutions: - node_start = node_execution.startedTime or now - node_end = node_execution.endedTime or now - total_run_time += (node_end - node_start).total_seconds() + if execution.stats: + stats = json.loads(execution.stats) + duration = stats.get("walltime", duration) + total_run_time = stats.get("nodes_walltime", total_run_time) - return ExecutionMeta( + return GraphExecution( id=execution.id, execution_id=execution.id, started_at=start_time, @@ -102,39 +107,46 @@ def from_agent_graph_execution(execution: AgentGraphExecution): ) -class GraphMeta(BaseDbModel): +class Graph(BaseDbModel): version: int = 1 is_active: bool = True is_template: bool = False name: str description: str - executions: list[ExecutionMeta] | None = None - - @staticmethod - def from_db(graph: AgentGraph): - if graph.AgentGraphExecution: - executions = [ - ExecutionMeta.from_agent_graph_execution(execution) - for execution in graph.AgentGraphExecution - ] - else: - executions = None - - return GraphMeta( - id=graph.id, - version=graph.version, - is_active=graph.isActive, - is_template=graph.isTemplate, - name=graph.name or "", - description=graph.description or "", - executions=executions, - ) + executions: list[GraphExecution] = [] + nodes: list[Node] = [] + links: list[Link] = [] + @computed_field + @property + def input_schema(self) -> dict[str, InputSchemaItem]: + return { + node.input_default["name"]: InputSchemaItem( + node_id=node.id, + title=node.input_default.get("title"), + description=node.input_default.get("description"), + default=node.input_default.get("value"), + ) + for node in self.nodes + if (b := get_block(node.block_id)) + and b.block_type == BlockType.INPUT + and "name" in node.input_default + } -class Graph(GraphMeta): - nodes: list[Node] - links: list[Link] - subgraphs: dict[str, list[str]] = {} # subgraph_id -> [node_id] + @computed_field + @property + def output_schema(self) -> dict[str, OutputSchemaItem]: + return { + node.input_default["name"]: OutputSchemaItem( + node_id=node.id, + title=node.input_default.get("title"), + description=node.input_default.get("description"), + ) + for node in self.nodes + if (b := get_block(node.block_id)) + and b.block_type == BlockType.OUTPUT + and "name" in node.input_default + } @property def starting_nodes(self) -> list[Node]: @@ -142,7 +154,7 @@ def starting_nodes(self) -> list[Node]: input_nodes = { v.id for v in self.nodes - if isinstance(get_block(v.block_id), AgentInputBlock) + if (b := get_block(v.block_id)) and b.block_type == BlockType.INPUT } return [ node @@ -150,28 +162,6 @@ def starting_nodes(self) -> list[Node]: if node.id not in outbound_nodes or node.id in input_nodes ] - @property - def ending_nodes(self) -> list[Node]: - return [ - v for v in self.nodes if isinstance(get_block(v.block_id), AgentOutputBlock) - ] - - @property - def subgraph_map(self) -> dict[str, str]: - """ - Returns a mapping of node_id to subgraph_id. - A node in the main graph will be mapped to the graph's id. - """ - subgraph_map = { - node_id: subgraph_id - for subgraph_id, node_ids in self.subgraphs.items() - for node_id in node_ids - } - subgraph_map.update( - {node.id: self.id for node in self.nodes if node.id not in subgraph_map} - ) - return subgraph_map - def reassign_ids(self, reassign_graph_id: bool = False): """ Reassigns all IDs in the graph to new UUIDs. @@ -179,11 +169,7 @@ def reassign_ids(self, reassign_graph_id: bool = False): """ self.validate_graph() - id_map = { - **{node.id: str(uuid.uuid4()) for node in self.nodes}, - **{subgraph_id: str(uuid.uuid4()) for subgraph_id in self.subgraphs}, - } - + id_map = {node.id: str(uuid.uuid4()) for node in self.nodes} if reassign_graph_id: self.id = str(uuid.uuid4()) @@ -194,11 +180,6 @@ def reassign_ids(self, reassign_graph_id: bool = False): link.source_id = id_map[link.source_id] link.sink_id = id_map[link.sink_id] - self.subgraphs = { - id_map[subgraph_id]: [id_map[node_id] for node_id in node_ids] - for subgraph_id, node_ids in self.subgraphs.items() - } - def validate_graph(self, for_run: bool = False): def sanitize(name): return name.split("_#_")[0].split("_@_")[0].split("_$_")[0] @@ -217,8 +198,9 @@ def sanitize(name): + [sanitize(link.sink_name) for link in node.input_links] ) for name in block.input_schema.get_required_fields(): - if name not in provided_inputs and not isinstance( - block, AgentInputBlock + if ( + name not in provided_inputs + and not block.block_type == BlockType.INPUT ): raise ValueError( f"Node {block.name} #{node.id} required input missing: `{name}`" @@ -230,18 +212,6 @@ def is_static_output_block(nid: str) -> bool: b = get_block(bid) return b.static_output if b else False - def is_input_output_block(nid: str) -> bool: - bid = node_map[nid].block_id - b = get_block(bid) - return isinstance(b, AgentInputBlock) or isinstance(b, AgentOutputBlock) - - # subgraphs: all nodes in subgraph must be present in the graph. - for subgraph_id, node_ids in self.subgraphs.items(): - for node_id in node_ids: - if node_id not in node_map: - raise ValueError(f"Subgraph {subgraph_id}'s node {node_id} invalid") - subgraph_map = self.subgraph_map - # Links: links are connected and the connected pin data type are compatible. for link in self.links: source = (link.source_id, link.source_name) @@ -270,66 +240,27 @@ def is_input_output_block(nid: str) -> bool: if sanitized_name not in fields: raise ValueError(f"{suffix}, `{name}` invalid, {fields}") - if ( - subgraph_map.get(link.source_id) != subgraph_map.get(link.sink_id) - and not is_input_output_block(link.source_id) - and not is_input_output_block(link.sink_id) - ): - raise ValueError(f"{suffix}, Connecting nodes from different subgraph.") - if is_static_output_block(link.source_id): link.is_static = True # Each value block output should be static. # TODO: Add type compatibility check here. - def get_input_schema(self) -> list[InputSchemaItem]: - """ - Walks the graph and returns all the inputs that are either not: - - static - - provided by parent node - """ - input_schema = [] - for node in self.nodes: - block = get_block(node.block_id) - if not block: - continue - - for input_name, input_schema_item in ( - block.input_schema.jsonschema().get("properties", {}).items() - ): - # Check if the input is not static and not provided by a parent node - if ( - input_name not in node.input_default - and not any( - link.sink_name == input_name for link in node.input_links - ) - and isinstance( - block.input_schema.model_fields.get(input_name).default, - PydanticUndefinedType, - ) - ): - input_schema.append( - InputSchemaItem( - node_id=node.id, - description=input_schema_item.get("description"), - title=input_schema_item.get("title"), - ) - ) - - return input_schema - @staticmethod def from_db(graph: AgentGraph, hide_credentials: bool = False): - nodes = [ - *(graph.AgentNodes or []), - *( - node - for subgraph in graph.AgentSubGraphs or [] - for node in subgraph.AgentNodes or [] - ), + executions = [ + GraphExecution.from_db(execution) + for execution in graph.AgentGraphExecution or [] ] + nodes = graph.AgentNodes or [] + return Graph( - **GraphMeta.from_db(graph).model_dump(), + id=graph.id, + version=graph.version, + is_active=graph.isActive, + is_template=graph.isTemplate, + name=graph.name or "", + description=graph.description or "", + executions=executions, nodes=[Graph._process_node(node, hide_credentials) for node in nodes], links=list( { @@ -338,10 +269,6 @@ def from_db(graph: AgentGraph, hide_credentials: bool = False): for link in (node.Input or []) + (node.Output or []) } ), - subgraphs={ - subgraph.id: [node.id for node in subgraph.AgentNodes or []] - for subgraph in graph.AgentSubGraphs or [] - }, ) @staticmethod @@ -370,20 +297,6 @@ def _hide_credentials_in_input(input_data: dict[str, Any]) -> dict[str, Any]: return result -AGENT_NODE_INCLUDE: prisma.types.AgentNodeInclude = { - "Input": True, - "Output": True, - "AgentBlock": True, -} - -__SUBGRAPH_INCLUDE = {"AgentNodes": {"include": AGENT_NODE_INCLUDE}} - -AGENT_GRAPH_INCLUDE: prisma.types.AgentGraphInclude = { - **__SUBGRAPH_INCLUDE, - "AgentSubGraphs": {"include": __SUBGRAPH_INCLUDE}, # type: ignore -} - - # --------------------- Model functions --------------------- # @@ -395,11 +308,12 @@ async def get_node(node_id: str) -> Node: return Node.from_db(node) -async def get_graphs_meta( +async def get_graphs( user_id: str, include_executions: bool = False, + include_nodes: bool = False, filter_by: Literal["active", "template"] | None = "active", -) -> list[GraphMeta]: +) -> list[Graph]: """ Retrieves graph metadata objects. Default behaviour is to get all currently active graphs. @@ -410,7 +324,7 @@ async def get_graphs_meta( user_id: The ID of the user that owns the graph. Returns: - list[GraphMeta]: A list of objects representing the retrieved graph metadata. + list[Graph]: A list of objects representing the retrieved graph metadata. """ where_clause: prisma.types.AgentGraphWhereInput = {} @@ -421,23 +335,17 @@ async def get_graphs_meta( where_clause["userId"] = user_id + graph_include = AGENT_GRAPH_INCLUDE + graph_include["AgentGraphExecution"] = include_executions + graphs = await AgentGraph.prisma().find_many( where=where_clause, distinct=["id"], order={"version": "desc"}, - include=( - AgentGraphInclude( - AgentGraphExecution={"include": {"AgentNodeExecutions": True}} - ) - if include_executions - else None - ), + include=graph_include, ) - if not graphs: - return [] - - return [GraphMeta.from_db(graph) for graph in graphs] + return [Graph.from_db(graph) for graph in graphs] async def get_graph( @@ -463,7 +371,7 @@ async def get_graph( elif not template: where_clause["isActive"] = True - if user_id and not template: + if user_id is not None and not template: where_clause["userId"] = user_id graph = await AgentGraph.prisma().find_first( @@ -549,33 +457,13 @@ async def __create_graph(tx, graph: Graph, user_id: str): } ) - await asyncio.gather( - *[ - AgentGraph.prisma(tx).create( - data={ - "id": subgraph_id, - "agentGraphParentId": graph.id, - "version": graph.version, - "name": f"SubGraph of {graph.name}", - "description": f"Sub-Graph of {graph.id}", - "isTemplate": graph.is_template, - "isActive": graph.is_active, - "userId": user_id, - } - ) - for subgraph_id in graph.subgraphs - ] - ) - - subgraph_map = graph.subgraph_map - await asyncio.gather( *[ AgentNode.prisma(tx).create( { "id": node.id, "agentBlockId": node.block_id, - "agentGraphId": subgraph_map.get(node.id, graph.id), + "agentGraphId": graph.id, "agentGraphVersion": graph.version, "constantInput": json.dumps(node.input_default), "metadata": json.dumps(node.metadata), diff --git a/autogpt_platform/backend/backend/data/includes.py b/autogpt_platform/backend/backend/data/includes.py new file mode 100644 index 000000000000..371d87ec5d24 --- /dev/null +++ b/autogpt_platform/backend/backend/data/includes.py @@ -0,0 +1,29 @@ +import prisma + +AGENT_NODE_INCLUDE: prisma.types.AgentNodeInclude = { + "Input": True, + "Output": True, + "AgentBlock": True, +} + +AGENT_GRAPH_INCLUDE: prisma.types.AgentGraphInclude = { + "AgentNodes": {"include": AGENT_NODE_INCLUDE} # type: ignore +} + +EXECUTION_RESULT_INCLUDE: prisma.types.AgentNodeExecutionInclude = { + "Input": True, + "Output": True, + "AgentNode": True, + "AgentGraphExecution": True, +} + +GRAPH_EXECUTION_INCLUDE: prisma.types.AgentGraphExecutionInclude = { + "AgentNodeExecutions": { + "include": { + "Input": True, + "Output": True, + "AgentNode": True, + "AgentGraphExecution": True, + } + } +} diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 102de62c1698..6e98f54723d0 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -473,7 +473,7 @@ def on_node_execution( cls, q: ExecutionQueue[NodeExecution], node_exec: NodeExecution, - ): + ) -> dict[str, Any]: log_metadata = LogMetadata( user_id=node_exec.user_id, graph_eid=node_exec.graph_exec_id, @@ -493,6 +493,7 @@ def on_node_execution( cls.db_client.update_node_execution_stats( node_exec.node_exec_id, execution_stats ) + return execution_stats @classmethod @time_measured @@ -556,16 +557,15 @@ def on_graph_execution(cls, graph_exec: GraphExecution, cancel: threading.Event) node_eid="*", block_name="-", ) - timing_info, (node_count, error) = cls._on_graph_execution( + timing_info, (exec_stats, error) = cls._on_graph_execution( graph_exec, cancel, log_metadata ) - + exec_stats["walltime"] = timing_info.wall_time + exec_stats["cputime"] = timing_info.cpu_time + exec_stats["error"] = str(error) if error else None cls.db_client.update_graph_execution_stats( graph_exec_id=graph_exec.graph_exec_id, - error=error, - wall_time=timing_info.wall_time, - cpu_time=timing_info.cpu_time, - node_count=node_count, + stats=exec_stats, ) @classmethod @@ -575,14 +575,18 @@ def _on_graph_execution( graph_exec: GraphExecution, cancel: threading.Event, log_metadata: LogMetadata, - ) -> tuple[int, Exception | None]: + ) -> tuple[dict[str, Any], Exception | None]: """ Returns: - The number of node executions completed. + The execution statistics of the graph execution. The error that occurred during the execution. """ log_metadata.info(f"Start graph execution {graph_exec.graph_exec_id}") - n_node_executions = 0 + exec_stats = { + "nodes_walltime": 0, + "nodes_cputime": 0, + "node_count": 0, + } error = None finished = False @@ -608,17 +612,20 @@ def cancel_handler(): def make_exec_callback(exec_data: NodeExecution): node_id = exec_data.node_id - def callback(_): + def callback(result: object): running_executions.pop(node_id) - nonlocal n_node_executions - n_node_executions += 1 + nonlocal exec_stats + if isinstance(result, dict): + exec_stats["node_count"] += 1 + exec_stats["nodes_cputime"] += result.get("cputime", 0) + exec_stats["nodes_walltime"] += result.get("walltime", 0) return callback while not queue.empty(): if cancel.is_set(): error = RuntimeError("Execution is cancelled") - return n_node_executions, error + return exec_stats, error exec_data = queue.get() @@ -649,7 +656,7 @@ def callback(_): for node_id, execution in list(running_executions.items()): if cancel.is_set(): error = RuntimeError("Execution is cancelled") - return n_node_executions, error + return exec_stats, error if not queue.empty(): break # yield to parent loop to execute new queue items @@ -668,7 +675,7 @@ def callback(_): finished = True cancel.set() cancel_thread.join() - return n_node_executions, error + return exec_stats, error class ExecutionManager(AppService): diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 0d91e9c622f5..bd28b2984cf4 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -10,8 +10,8 @@ from autogpt_libs.auth.middleware import auth_middleware from autogpt_libs.utils.cache import thread_cached from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request -from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse +from starlette.middleware.cors import CORSMiddleware from typing_extensions import TypedDict from backend.data import block, db @@ -72,14 +72,6 @@ def run_service(self): f"FastAPI CORS allow origins: {Config().backend_cors_allow_origins}" ) - app.add_middleware( - CORSMiddleware, - allow_origins=Config().backend_cors_allow_origins, - allow_credentials=True, - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers - ) - health_router = APIRouter() health_router.add_api_route( path="/health", @@ -207,12 +199,6 @@ def run_service(self): methods=["PUT"], tags=["graphs"], ) - api_router.add_api_route( - path="/graphs/{graph_id}/input_schema", - endpoint=self.get_graph_input_schema, - methods=["GET"], - tags=["graphs"], - ) api_router.add_api_route( path="/graphs/{graph_id}/execute", endpoint=self.execute_graph, @@ -273,6 +259,14 @@ def run_service(self): app.include_router(api_router) app.include_router(health_router) + app = CORSMiddleware( + app=app, + allow_origins=Config().backend_cors_allow_origins, + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers + ) + uvicorn.run( app, host=Config().agent_api_host, @@ -358,16 +352,16 @@ async def get_graphs( cls, user_id: Annotated[str, Depends(get_user_id)], with_runs: bool = False, - ) -> list[graph_db.GraphMeta]: - return await graph_db.get_graphs_meta( + ) -> list[graph_db.Graph]: + return await graph_db.get_graphs( include_executions=with_runs, filter_by="active", user_id=user_id ) @classmethod async def get_templates( cls, user_id: Annotated[str, Depends(get_user_id)] - ) -> list[graph_db.GraphMeta]: - return await graph_db.get_graphs_meta(filter_by="template", user_id=user_id) + ) -> list[graph_db.Graph]: + return await graph_db.get_graphs(filter_by="template", user_id=user_id) @classmethod async def get_graph( @@ -550,18 +544,6 @@ async def stop_graph_run( # Retrieve & return canceled graph execution in its final state return await execution_db.get_execution_results(graph_exec_id) - @classmethod - async def get_graph_input_schema( - cls, - graph_id: str, - user_id: Annotated[str, Depends(get_user_id)], - ) -> list[graph_db.InputSchemaItem]: - try: - graph = await graph_db.get_graph(graph_id, user_id=user_id) - return graph.get_input_schema() if graph else [] - except Exception: - raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.") - @classmethod async def list_graph_runs( cls, diff --git a/autogpt_platform/backend/migrations/20241103133307_remove_subgraph/migration.sql b/autogpt_platform/backend/migrations/20241103133307_remove_subgraph/migration.sql new file mode 100644 index 000000000000..58caf9b2b1b9 --- /dev/null +++ b/autogpt_platform/backend/migrations/20241103133307_remove_subgraph/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `agentGraphParentId` on the `AgentGraph` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "AgentGraph" DROP CONSTRAINT "AgentGraph_agentGraphParentId_version_fkey"; + +-- AlterTable +ALTER TABLE "AgentGraph" DROP COLUMN "agentGraphParentId"; diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 7b16a5e652f4..3fedcb37171c 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -52,11 +52,6 @@ model AgentGraph { AgentGraphExecution AgentGraphExecution[] AgentGraphExecutionSchedule AgentGraphExecutionSchedule[] - // All sub-graphs are defined within this 1-level depth list (even if it's a nested graph). - AgentSubGraphs AgentGraph[] @relation("AgentSubGraph") - agentGraphParentId String? - AgentGraphParent AgentGraph? @relation("AgentSubGraph", fields: [agentGraphParentId, version], references: [id, version], onDelete: Cascade) - @@id(name: "graphVersionId", [id, version]) } diff --git a/autogpt_platform/backend/test/data/test_graph.py b/autogpt_platform/backend/test/data/test_graph.py index b902360c3048..bc1d87c9d178 100644 --- a/autogpt_platform/backend/test/data/test_graph.py +++ b/autogpt_platform/backend/test/data/test_graph.py @@ -2,7 +2,7 @@ import pytest -from backend.blocks.basic import AgentInputBlock, StoreValueBlock +from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock from backend.data.graph import Graph, Link, Node from backend.data.user import DEFAULT_USER_ID from backend.server.model import CreateGraph @@ -15,9 +15,8 @@ async def test_graph_creation(server: SpinTestServer): Test the creation of a graph with nodes and links. This test ensures that: - 1. Nodes from different subgraphs cannot be directly connected. - 2. A graph can be successfully created with valid connections. - 3. The created graph has the correct structure and properties. + 1. A graph can be successfully created with valid connections. + 2. The created graph has the correct structure and properties. Args: server (SpinTestServer): The test server instance. @@ -37,23 +36,13 @@ async def test_graph_creation(server: SpinTestServer): links=[ Link( source_id="node_1", - sink_id="node_3", + sink_id="node_2", source_name="output", sink_name="input", ), ], - subgraphs={"subgraph_1": ["node_2", "node_3"]}, ) create_graph = CreateGraph(graph=graph) - - try: - await server.agent_server.create_graph(create_graph, False, DEFAULT_USER_ID) - assert False, "Should not be able to connect nodes from different subgraphs" - except ValueError as e: - assert "different subgraph" in str(e) - - # Change node_1 <-> node_3 link to node_1 <-> node_2 (input for subgraph_1) - graph.links[0].sink_id = "node_2" created_graph = await server.agent_server.create_graph( create_graph, False, DEFAULT_USER_ID ) @@ -73,9 +62,6 @@ async def test_graph_creation(server: SpinTestServer): assert links[0].source_id in {nodes[0].id, nodes[1].id, nodes[2].id} assert links[0].sink_id in {nodes[0].id, nodes[1].id, nodes[2].id} - assert len(created_graph.subgraphs) == 1 - assert len(created_graph.subgraph_map) == len(created_graph.nodes) == 3 - @pytest.mark.asyncio(scope="session") async def test_get_input_schema(server: SpinTestServer): @@ -91,90 +77,40 @@ async def test_get_input_schema(server: SpinTestServer): server (SpinTestServer): The test server instance. """ value_block = StoreValueBlock().id + input_block = AgentInputBlock().id + output_block = AgentOutputBlock().id graph = Graph( name="TestInputSchema", description="Test input schema", nodes=[ + Node( + id="node_0", + block_id=input_block, + input_default={"name": "in_key", "title": "Input Key"}, + ), Node(id="node_1", block_id=value_block), - ], - links=[], - ) - - create_graph = CreateGraph(graph=graph) - created_graph = await server.agent_server.create_graph( - create_graph, False, DEFAULT_USER_ID - ) - - input_schema = created_graph.get_input_schema() - - assert len(input_schema) == 1 - - assert input_schema[0].title == "Input" - assert input_schema[0].node_id == created_graph.nodes[0].id - - -@pytest.mark.asyncio(scope="session") -async def test_get_input_schema_none_required(server: SpinTestServer): - """ - Test the get_input_schema method when no inputs are required. - - This test ensures that: - 1. A graph can be created with a node that has a default input value. - 2. The input schema of the created graph is empty when all inputs have default values. - - Args: - server (SpinTestServer): The test server instance. - """ - value_block = StoreValueBlock().id - - graph = Graph( - name="TestInputSchema", - description="Test input schema", - nodes=[ - Node(id="node_1", block_id=value_block, input_default={"input": "value"}), - ], - links=[], - ) - - create_graph = CreateGraph(graph=graph) - created_graph = await server.agent_server.create_graph( - create_graph, False, DEFAULT_USER_ID - ) - - input_schema = created_graph.get_input_schema() - - assert input_schema == [] - - -@pytest.mark.asyncio(scope="session") -async def test_get_input_schema_with_linked_blocks(server: SpinTestServer): - """ - Test the get_input_schema method with linked blocks. - - This test ensures that: - 1. A graph can be created with multiple nodes and links between them. - 2. The input schema correctly identifies required inputs for linked blocks. - 3. Inputs that are satisfied by links are not included in the input schema. - - Args: - server (SpinTestServer): The test server instance. - """ - value_block = StoreValueBlock().id - - graph = Graph( - name="TestInputSchemaLinkedBlocks", - description="Test input schema with linked blocks", - nodes=[ - Node(id="node_1", block_id=value_block), - Node(id="node_2", block_id=value_block), + Node( + id="node_2", + block_id=output_block, + input_default={ + "name": "out_key", + "description": "This is an output key", + }, + ), ], links=[ + Link( + source_id="node_0", + sink_id="node_1", + source_name="output", + sink_name="input", + ), Link( source_id="node_1", sink_id="node_2", source_name="output", - sink_name="data", + sink_name="input", ), ], ) @@ -184,25 +120,12 @@ async def test_get_input_schema_with_linked_blocks(server: SpinTestServer): create_graph, False, DEFAULT_USER_ID ) - input_schema = created_graph.get_input_schema() - - assert len(input_schema) == 2 - - node_1_input = next( - (item for item in input_schema if item.node_id == created_graph.nodes[0].id), - None, - ) - node_2_input = next( - (item for item in input_schema if item.node_id == created_graph.nodes[1].id), - None, - ) - - assert node_1_input is not None - assert node_2_input is not None - assert node_1_input.title == "Input" - assert node_2_input.title == "Input" + input_schema = created_graph.input_schema + assert len(input_schema) == 1 + assert input_schema["in_key"].node_id == created_graph.nodes[0].id + assert input_schema["in_key"].title == "Input Key" - assert not any( - item.title == "data" and item.node_id == created_graph.nodes[1].id - for item in input_schema - ) + output_schema = created_graph.output_schema + assert len(output_schema) == 1 + assert output_schema["out_key"].node_id == created_graph.nodes[2].id + assert output_schema["out_key"].description == "This is an output key" From 08fc2579d764cfb7c550b9abbfcfc6cdf05d56b8 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Mon, 4 Nov 2024 07:52:20 +0700 Subject: [PATCH 2/7] Cleanup --- autogpt_platform/backend/backend/blocks/branching.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/branching.py b/autogpt_platform/backend/backend/blocks/branching.py index 93be02e8f925..65a01c977268 100644 --- a/autogpt_platform/backend/backend/blocks/branching.py +++ b/autogpt_platform/backend/backend/blocks/branching.py @@ -72,9 +72,15 @@ def __init__(self): def run(self, input_data: Input, **kwargs) -> BlockOutput: operator = input_data.operator - # cast value 1 and value 2 to float or int if possible - value1 = float(input_data.value1) - value2 = float(input_data.value2) + + value1 = input_data.value1 + if isinstance(value1, str): + value1 = float(value1.strip()) + + value2 = input_data.value2 + if isinstance(value2, str): + value2 = float(value2.strip()) + yes_value = input_data.yes_value if input_data.yes_value is not None else value1 no_value = input_data.no_value if input_data.no_value is not None else value2 From 76b581774cdcb1ce8c914bc21c42d9c251cf3163 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 5 Nov 2024 17:44:42 +0700 Subject: [PATCH 3/7] Merge conflict --- autogpt_platform/backend/backend/data/graph.py | 7 +++++-- autogpt_platform/backend/test/data/test_graph.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 341610a45c19..f481c3867b93 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -192,7 +192,11 @@ def sanitize(name): provided_inputs = set( [sanitize(name) for name in node.input_default] - + [sanitize(link.sink_name) for link in node.input_links] + + [ + sanitize(link.sink_name) + for link in self.links + if link.sink_id == node.id + ] ) for name in block.input_schema.get_required_fields(): if name not in provided_inputs and ( @@ -309,7 +313,6 @@ async def get_node(node_id: str) -> Node: async def get_graphs( user_id: str, include_executions: bool = False, - include_nodes: bool = False, filter_by: Literal["active", "template"] | None = "active", ) -> list[Graph]: """ diff --git a/autogpt_platform/backend/test/data/test_graph.py b/autogpt_platform/backend/test/data/test_graph.py index ede336373fcc..a311f1d2f8cf 100644 --- a/autogpt_platform/backend/test/data/test_graph.py +++ b/autogpt_platform/backend/test/data/test_graph.py @@ -110,7 +110,7 @@ async def test_get_input_schema(server: SpinTestServer): source_id="node_1", sink_id="node_2", source_name="output", - sink_name="input", + sink_name="value", ), ], ) From 57b2373dadfcf85c3b32a143080ec4f383b8da65 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 5 Nov 2024 17:50:43 +0700 Subject: [PATCH 4/7] Cleanup --- autogpt_platform/backend/backend/data/graph.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index f481c3867b93..ed00198330fe 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -184,7 +184,11 @@ def validate_graph(self, for_run: bool = False): def sanitize(name): return name.split("_#_")[0].split("_@_")[0].split("_$_")[0] - # Nodes: required fields are filled or connected, except for InputBlock. + input_links = {node.id: [] for node in self.nodes} + for link in self.links: + input_links[link.sink_id].append(link) + + # Nodes: required fields are filled or connected for node in self.nodes: block = get_block(node.block_id) if block is None: @@ -192,11 +196,7 @@ def sanitize(name): provided_inputs = set( [sanitize(name) for name in node.input_default] - + [ - sanitize(link.sink_name) - for link in self.links - if link.sink_id == node.id - ] + + [sanitize(link.sink_name) for link in input_links.get(node.id, [])] ) for name in block.input_schema.get_required_fields(): if name not in provided_inputs and ( @@ -207,6 +207,7 @@ def sanitize(name): raise ValueError( f"Node {block.name} #{node.id} required input missing: `{name}`" ) + node_map = {v.id: v for v in self.nodes} def is_static_output_block(nid: str) -> bool: From 2a96c235596483a33f032f5bb01aab4a6112a8d1 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 5 Nov 2024 17:54:58 +0700 Subject: [PATCH 5/7] Cleanup --- autogpt_platform/backend/backend/data/graph.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index ed00198330fe..3f7fc00a52f2 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -1,11 +1,12 @@ import asyncio import logging import uuid +from collections import defaultdict from datetime import datetime, timezone from typing import Any, Literal -import prisma.types from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink +from prisma.types import AgentGraphWhereInput from pydantic import BaseModel from pydantic.fields import computed_field @@ -184,7 +185,7 @@ def validate_graph(self, for_run: bool = False): def sanitize(name): return name.split("_#_")[0].split("_@_")[0].split("_$_")[0] - input_links = {node.id: [] for node in self.nodes} + input_links = defaultdict(list) for link in self.links: input_links[link.sink_id].append(link) @@ -328,7 +329,7 @@ async def get_graphs( Returns: list[Graph]: A list of objects representing the retrieved graph metadata. """ - where_clause: prisma.types.AgentGraphWhereInput = {} + where_clause: AgentGraphWhereInput = {} if filter_by == "active": where_clause["isActive"] = True @@ -364,7 +365,7 @@ async def get_graph( Returns `None` if the record is not found. """ - where_clause: prisma.types.AgentGraphWhereInput = { + where_clause: AgentGraphWhereInput = { "id": graph_id, "isTemplate": template, } From 1569e231a0dc1a75cb072638e68d01f6b615a2a3 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 5 Nov 2024 18:24:08 +0700 Subject: [PATCH 6/7] Merge input & output schema --- .../backend/backend/blocks/basic.py | 58 ++++++++---- .../backend/backend/data/graph.py | 91 +++++++++++-------- .../backend/test/data/test_graph.py | 50 +++++++--- 3 files changed, 129 insertions(+), 70 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 4acbaf5a58f8..6e8a2906d7f8 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -148,9 +148,12 @@ class Input(BlockSchema): description="The value to be passed as input.", default=None, ) - description: str = SchemaField( + title: str | None = SchemaField( + description="The title of the input.", default=None, advanced=True + ) + description: str | None = SchemaField( description="The description of the input.", - default="", + default=None, advanced=True, ) placeholder_values: List[Any] = SchemaField( @@ -163,6 +166,16 @@ class Input(BlockSchema): default=False, advanced=True, ) + advanced: bool = SchemaField( + description="Whether to show the input in the advanced section, if the field is not required.", + default=False, + advanced=True, + ) + secret: bool = SchemaField( + description="Whether the input should be treated as a secret.", + default=False, + advanced=True, + ) class Output(BlockSchema): result: Any = SchemaField(description="The value passed as input.") @@ -195,6 +208,7 @@ def __init__(self): ], categories={BlockCategory.INPUT, BlockCategory.BASIC}, block_type=BlockType.INPUT, + static_output=True, ) def run(self, input_data: Input, **kwargs) -> BlockOutput: @@ -205,28 +219,25 @@ class AgentOutputBlock(Block): """ Records the output of the graph for users to see. - Attributes: - recorded_value: The value to be recorded as output. - name: The name of the output. - description: The description of the output. - fmt_string: The format string to be used to format the recorded_value. - - Outputs: - output: The formatted recorded_value if fmt_string is provided and the recorded_value - can be formatted, otherwise the raw recorded_value. - Behavior: - If fmt_string is provided and the recorded_value is of a type that can be formatted, - the block attempts to format the recorded_value using the fmt_string. - If formatting fails or no fmt_string is provided, the raw recorded_value is output. + If `format` is provided and the `value` is of a type that can be formatted, + the block attempts to format the recorded_value using the `format`. + If formatting fails or no `format` is provided, the raw `value` is output. """ class Input(BlockSchema): - value: Any = SchemaField(description="The value to be recorded as output.") + value: Any = SchemaField( + description="The value to be recorded as output.", + default=None, + advanced=False, + ) name: str = SchemaField(description="The name of the output.") - description: str = SchemaField( + title: str | None = SchemaField( + description="The title of the input.", default=None, advanced=True + ) + description: str | None = SchemaField( description="The description of the output.", - default="", + default=None, advanced=True, ) format: str = SchemaField( @@ -234,6 +245,16 @@ class Input(BlockSchema): default="", advanced=True, ) + advanced: bool = SchemaField( + description="Whether to treat the output as advanced.", + default=False, + advanced=True, + ) + secret: bool = SchemaField( + description="Whether the output should be treated as a secret.", + default=False, + advanced=True, + ) class Output(BlockSchema): output: Any = SchemaField(description="The value recorded as output.") @@ -271,6 +292,7 @@ def __init__(self): ], categories={BlockCategory.OUTPUT, BlockCategory.BASIC}, block_type=BlockType.OUTPUT, + static_output=True, ) def run(self, input_data: Input, **kwargs) -> BlockOutput: diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 3f7fc00a52f2..d490208262a3 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -3,13 +3,13 @@ import uuid from collections import defaultdict from datetime import datetime, timezone -from typing import Any, Literal +from typing import Any, Literal, Type from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink from prisma.types import AgentGraphWhereInput -from pydantic import BaseModel from pydantic.fields import computed_field +from backend.blocks.basic import AgentInputBlock, AgentOutputBlock from backend.data.block import BlockInput, BlockType, get_block, get_blocks from backend.data.db import BaseDbModel, transaction from backend.data.execution import ExecutionStatus @@ -19,19 +19,6 @@ logger = logging.getLogger(__name__) -class InputSchemaItem(BaseModel): - node_id: str - title: str | None = None - description: str | None = None - default: Any | None = None - - -class OutputSchemaItem(BaseModel): - node_id: str - title: str | None = None - description: str | None = None - - class Link(BaseDbModel): source_id: str sink_id: str @@ -118,36 +105,60 @@ class Graph(BaseDbModel): nodes: list[Node] = [] links: list[Link] = [] - @computed_field - @property - def input_schema(self) -> dict[str, InputSchemaItem]: + @staticmethod + def _generate_schema( + type_class: Type[AgentInputBlock.Input] | Type[AgentOutputBlock.Input], + data: list[dict], + ) -> dict[str, Any]: + props = [] + for p in data: + try: + props.append(type_class(**p)) + except Exception as e: + logger.warning(f"Invalid {type_class}: {p}, {e}") + return { - node.input_default["name"]: InputSchemaItem( - node_id=node.id, - title=node.input_default.get("title"), - description=node.input_default.get("description"), - default=node.input_default.get("value"), - ) - for node in self.nodes - if (b := get_block(node.block_id)) - and b.block_type == BlockType.INPUT - and "name" in node.input_default + "type": "object", + "properties": { + p.name: { + "secret": p.secret, + "advanced": p.advanced, + "title": p.title or p.name, + **({"description": p.description} if p.description else {}), + **({"default": p.value} if p.value is not None else {}), + } + for p in props + }, + "required": [p.name for p in props if p.value is None], } @computed_field @property - def output_schema(self) -> dict[str, OutputSchemaItem]: - return { - node.input_default["name"]: OutputSchemaItem( - node_id=node.id, - title=node.input_default.get("title"), - description=node.input_default.get("description"), - ) - for node in self.nodes - if (b := get_block(node.block_id)) - and b.block_type == BlockType.OUTPUT - and "name" in node.input_default - } + def input_schema(self) -> dict[str, Any]: + return self._generate_schema( + AgentInputBlock.Input, + [ + node.input_default + for node in self.nodes + if (b := get_block(node.block_id)) + and b.block_type == BlockType.INPUT + and "name" in node.input_default + ], + ) + + @computed_field + @property + def output_schema(self) -> dict[str, Any]: + return self._generate_schema( + AgentOutputBlock.Input, + [ + node.input_default + for node in self.nodes + if (b := get_block(node.block_id)) + and b.block_type == BlockType.OUTPUT + and "name" in node.input_default + ], + ) @property def starting_nodes(self) -> list[Node]: diff --git a/autogpt_platform/backend/test/data/test_graph.py b/autogpt_platform/backend/test/data/test_graph.py index a311f1d2f8cf..050e20fdc04b 100644 --- a/autogpt_platform/backend/test/data/test_graph.py +++ b/autogpt_platform/backend/test/data/test_graph.py @@ -1,9 +1,12 @@ +from typing import Any from uuid import UUID import pytest from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock +from backend.data.block import BlockSchema from backend.data.graph import Graph, Link, Node +from backend.data.model import SchemaField from backend.data.user import DEFAULT_USER_ID from backend.server.model import CreateGraph from backend.util.test import SpinTestServer @@ -38,7 +41,7 @@ async def test_graph_creation(server: SpinTestServer): source_id="node_1", sink_id="node_2", source_name="output", - sink_name="input", + sink_name="name", ), ], ) @@ -85,11 +88,18 @@ async def test_get_input_schema(server: SpinTestServer): description="Test input schema", nodes=[ Node( - id="node_0", + id="node_0_a", block_id=input_block, - input_default={"name": "in_key", "title": "Input Key"}, + input_default={"name": "in_key_a", "title": "Key A", "value": "A"}, + metadata={"id": "node_0_a"}, ), - Node(id="node_1", block_id=value_block), + Node( + id="node_0_b", + block_id=input_block, + input_default={"name": "in_key_b", "advanced": True}, + metadata={"id": "node_0_b"}, + ), + Node(id="node_1", block_id=value_block, metadata={"id": "node_1"}), Node( id="node_2", block_id=output_block, @@ -97,13 +107,20 @@ async def test_get_input_schema(server: SpinTestServer): "name": "out_key", "description": "This is an output key", }, + metadata={"id": "node_2"}, ), ], links=[ Link( - source_id="node_0", + source_id="node_0_a", sink_id="node_1", - source_name="output", + source_name="result", + sink_name="input", + ), + Link( + source_id="node_0_b", + sink_id="node_1", + source_name="result", sink_name="input", ), Link( @@ -120,12 +137,21 @@ async def test_get_input_schema(server: SpinTestServer): create_graph, DEFAULT_USER_ID ) + class ExpectedInputSchema(BlockSchema): + in_key_a: Any = SchemaField(title="Key A", default="A", advanced=False) + in_key_b: Any = SchemaField(title="in_key_b", advanced=True) + + class ExpectedOutputSchema(BlockSchema): + out_key: Any = SchemaField( + description="This is an output key", + title="out_key", + advanced=False, + ) + input_schema = created_graph.input_schema - assert len(input_schema) == 1 - assert input_schema["in_key"].node_id == created_graph.nodes[0].id - assert input_schema["in_key"].title == "Input Key" + input_schema["title"] = "ExpectedInputSchema" + assert input_schema == ExpectedInputSchema.jsonschema() output_schema = created_graph.output_schema - assert len(output_schema) == 1 - assert output_schema["out_key"].node_id == created_graph.nodes[2].id - assert output_schema["out_key"].description == "This is an output key" + output_schema["title"] = "ExpectedOutputSchema" + assert output_schema == ExpectedOutputSchema.jsonschema() From a7fa5890a33fd126ca196cbde283fc2d2b91d44e Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 6 Nov 2024 22:13:28 +0700 Subject: [PATCH 7/7] Add migration to convert stats from list into object --- .../20241103144418_graph_exec_stats_list_to_obj/migration.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 autogpt_platform/backend/migrations/20241103144418_graph_exec_stats_list_to_obj/migration.sql diff --git a/autogpt_platform/backend/migrations/20241103144418_graph_exec_stats_list_to_obj/migration.sql b/autogpt_platform/backend/migrations/20241103144418_graph_exec_stats_list_to_obj/migration.sql new file mode 100644 index 000000000000..ad9f45c1422e --- /dev/null +++ b/autogpt_platform/backend/migrations/20241103144418_graph_exec_stats_list_to_obj/migration.sql @@ -0,0 +1,4 @@ +-- This migration converts the stats column from a list to an object. +UPDATE "AgentGraphExecution" +SET "stats" = (stats::jsonb -> 0)::text +WHERE stats IS NOT NULL AND jsonb_typeof(stats::jsonb) = 'array';