From befcaae232fb472ac3f72aa996173f3f2da78c9d Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Fri, 6 Feb 2026 18:28:14 +0100 Subject: [PATCH] Strip down `GraphMeta` and introduce `GraphModelWithoutNodes` --- .../backend/api/features/chat/tools/utils.py | 11 +- .../backend/api/features/library/db.py | 2 +- .../backend/backend/api/features/store/db.py | 27 +- .../backend/api/features/store/image_gen.py | 37 +-- .../backend/api/features/store/routes.py | 2 +- .../backend/backend/data/graph.py | 201 ++++++++------- .../AgentOnboardingCredentials.tsx | 4 +- .../AgentOnboardingCredentials/helpers.ts | 4 +- .../(no-navbar)/onboarding/5-run/helpers.ts | 6 +- .../legacy-builder/BlocksControl.tsx | 48 +++- .../legacy-builder/RunnerInputUI.tsx | 4 +- .../legacy-builder/RunnerUIWrapper.tsx | 4 +- .../build/hooks/useSubAgentUpdate/helpers.ts | 4 +- .../build/hooks/useSubAgentUpdate/types.ts | 13 +- .../useSubAgentUpdate/useSubAgentUpdate.ts | 40 ++- .../app/(platform)/build/stores/graphStore.ts | 2 +- .../components/agent-run-draft-view.tsx | 4 +- .../agent-schedule-details-view.tsx | 4 +- .../frontend/src/app/api/openapi.json | 234 +++++++++++------- .../src/lib/autogpt-server-api/types.ts | 30 +-- 20 files changed, 409 insertions(+), 272 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/utils.py b/autogpt_platform/backend/backend/api/features/chat/tools/utils.py index 690106acf3..cda0914809 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/utils.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/utils.py @@ -6,7 +6,6 @@ from typing import Any from backend.api.features.library import db as library_db from backend.api.features.library import model as library_model from backend.api.features.store import db as store_db -from backend.data import graph as graph_db from backend.data.graph import GraphModel from backend.data.model import ( CredentialsFieldInfo, @@ -44,14 +43,8 @@ async def fetch_graph_from_store_slug( return None, None # Get the graph from store listing version - graph_meta = await store_db.get_available_graph( - store_agent.store_listing_version_id - ) - graph = await graph_db.get_graph( - graph_id=graph_meta.id, - version=graph_meta.version, - user_id=None, # Public access - include_subgraphs=True, + graph = await store_db.get_available_graph( + store_agent.store_listing_version_id, hide_nodes=False ) return graph, store_agent diff --git a/autogpt_platform/backend/backend/api/features/library/db.py b/autogpt_platform/backend/backend/api/features/library/db.py index 6bebfb573c..32479c18a3 100644 --- a/autogpt_platform/backend/backend/api/features/library/db.py +++ b/autogpt_platform/backend/backend/api/features/library/db.py @@ -374,7 +374,7 @@ async def get_library_agent_by_graph_id( async def add_generated_agent_image( - graph: graph_db.BaseGraph, + graph: graph_db.GraphBaseMeta, user_id: str, library_agent_id: str, ) -> Optional[prisma.models.LibraryAgent]: diff --git a/autogpt_platform/backend/backend/api/features/store/db.py b/autogpt_platform/backend/backend/api/features/store/db.py index 850a2bc3e9..87b72d6a9c 100644 --- a/autogpt_platform/backend/backend/api/features/store/db.py +++ b/autogpt_platform/backend/backend/api/features/store/db.py @@ -1,7 +1,7 @@ import asyncio import logging from datetime import datetime, timezone -from typing import Any, Literal +from typing import Any, Literal, overload import fastapi import prisma.enums @@ -11,8 +11,8 @@ import prisma.types from backend.data.db import transaction from backend.data.graph import ( - GraphMeta, GraphModel, + GraphModelWithoutNodes, get_graph, get_graph_as_admin, get_sub_graphs, @@ -334,7 +334,22 @@ async def get_store_agent_details( raise DatabaseError("Failed to fetch agent details") from e -async def get_available_graph(store_listing_version_id: str) -> GraphMeta: +@overload +async def get_available_graph( + store_listing_version_id: str, hide_nodes: Literal[False] +) -> GraphModel: ... + + +@overload +async def get_available_graph( + store_listing_version_id: str, hide_nodes: Literal[True] = True +) -> GraphModelWithoutNodes: ... + + +async def get_available_graph( + store_listing_version_id: str, + hide_nodes: bool = True, +) -> GraphModelWithoutNodes | GraphModel: try: # Get avaialble, non-deleted store listing version store_listing_version = ( @@ -344,7 +359,7 @@ async def get_available_graph(store_listing_version_id: str) -> GraphMeta: "isAvailable": True, "isDeleted": False, }, - include={"AgentGraph": {"include": {"Nodes": True}}}, + include={"AgentGraph": {"include": AGENT_GRAPH_INCLUDE}}, ) ) @@ -354,7 +369,9 @@ async def get_available_graph(store_listing_version_id: str) -> GraphMeta: detail=f"Store listing version {store_listing_version_id} not found", ) - return GraphModel.from_db(store_listing_version.AgentGraph).meta() + return (GraphModelWithoutNodes if hide_nodes else GraphModel).from_db( + store_listing_version.AgentGraph + ) except Exception as e: logger.error(f"Error getting agent: {e}") diff --git a/autogpt_platform/backend/backend/api/features/store/image_gen.py b/autogpt_platform/backend/backend/api/features/store/image_gen.py index 87b7b601df..31d19f2ac7 100644 --- a/autogpt_platform/backend/backend/api/features/store/image_gen.py +++ b/autogpt_platform/backend/backend/api/features/store/image_gen.py @@ -16,7 +16,7 @@ from backend.blocks.ideogram import ( StyleType, UpscaleOption, ) -from backend.data.graph import BaseGraph +from backend.data.graph import GraphBaseMeta from backend.data.model import CredentialsMetaInput, ProviderName from backend.integrations.credentials_store import ideogram_credentials from backend.util.request import Requests @@ -34,14 +34,14 @@ class ImageStyle(str, Enum): DIGITAL_ART = "digital art" -async def generate_agent_image(agent: BaseGraph | AgentGraph) -> io.BytesIO: +async def generate_agent_image(agent: GraphBaseMeta | AgentGraph) -> io.BytesIO: if settings.config.use_agent_image_generation_v2: return await generate_agent_image_v2(graph=agent) else: return await generate_agent_image_v1(agent=agent) -async def generate_agent_image_v2(graph: BaseGraph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v2(graph: GraphBaseMeta | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Ideogram model. Returns: @@ -54,14 +54,17 @@ async def generate_agent_image_v2(graph: BaseGraph | AgentGraph) -> io.BytesIO: description = f"{name} ({graph.description})" if graph.description else name prompt = ( - f"Create a visually striking retro-futuristic vector pop art illustration prominently featuring " - f'"{name}" in bold typography. The image clearly and literally depicts a {description}, ' - f"along with recognizable objects directly associated with the primary function of a {name}. " - f"Ensure the imagery is concrete, intuitive, and immediately understandable, clearly conveying the " - f"purpose of a {name}. Maintain vibrant, limited-palette colors, sharp vector lines, geometric " - f"shapes, flat illustration techniques, and solid colors without gradients or shading. Preserve a " - f"retro-futuristic aesthetic influenced by mid-century futurism and 1960s psychedelia, " - f"prioritizing clear visual storytelling and thematic clarity above all else." + "Create a visually striking retro-futuristic vector pop art illustration " + f'prominently featuring "{name}" in bold typography. The image clearly and ' + f"literally depicts a {description}, along with recognizable objects directly " + f"associated with the primary function of a {name}. " + f"Ensure the imagery is concrete, intuitive, and immediately understandable, " + f"clearly conveying the purpose of a {name}. " + "Maintain vibrant, limited-palette colors, sharp vector lines, " + "geometric shapes, flat illustration techniques, and solid colors " + "without gradients or shading. Preserve a retro-futuristic aesthetic " + "influenced by mid-century futurism and 1960s psychedelia, " + "prioritizing clear visual storytelling and thematic clarity above all else." ) custom_colors = [ @@ -99,12 +102,12 @@ async def generate_agent_image_v2(graph: BaseGraph | AgentGraph) -> io.BytesIO: return io.BytesIO(response.content) -async def generate_agent_image_v1(agent: BaseGraph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v1(agent: GraphBaseMeta | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Flux model via Replicate API. Args: - agent (Graph): The agent to generate an image for + agent (_GraphBaseMeta | AgentGraph): The agent to generate an image for Returns: io.BytesIO: The generated image as bytes @@ -114,7 +117,13 @@ async def generate_agent_image_v1(agent: BaseGraph | AgentGraph) -> io.BytesIO: raise ValueError("Missing Replicate API key in settings") # Construct prompt from agent details - prompt = f"Create a visually engaging app store thumbnail for the AI agent that highlights what it does in a clear and captivating way:\n- **Name**: {agent.name}\n- **Description**: {agent.description}\nFocus on showcasing its core functionality with an appealing design." + prompt = ( + "Create a visually engaging app store thumbnail for the AI agent " + "that highlights what it does in a clear and captivating way:\n" + f"- **Name**: {agent.name}\n" + f"- **Description**: {agent.description}\n" + f"Focus on showcasing its core functionality with an appealing design." + ) # Set up Replicate client client = ReplicateClient(api_token=settings.secrets.replicate_api_key) diff --git a/autogpt_platform/backend/backend/api/features/store/routes.py b/autogpt_platform/backend/backend/api/features/store/routes.py index 2f3c7bfb04..d93fe60f15 100644 --- a/autogpt_platform/backend/backend/api/features/store/routes.py +++ b/autogpt_platform/backend/backend/api/features/store/routes.py @@ -278,7 +278,7 @@ async def get_agent( ) async def get_graph_meta_by_store_listing_version_id( store_listing_version_id: str, -) -> backend.data.graph.GraphMeta: +) -> backend.data.graph.GraphModelWithoutNodes: """ Get Agent Graph from Store Listing Version ID. """ diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 23475d7f05..1790466fab 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -111,10 +111,12 @@ class Link(BaseDbModel): class Node(BaseDbModel): block_id: str - input_default: BlockInput = {} # dict[input_name, default_value] - metadata: dict[str, Any] = {} - input_links: list[Link] = [] - output_links: list[Link] = [] + input_default: BlockInput = Field( # dict[input_name, default_value] + default_factory=dict + ) + metadata: dict[str, Any] = Field(default_factory=dict) + input_links: list[Link] = Field(default_factory=list) + output_links: list[Link] = Field(default_factory=list) @property def credentials_optional(self) -> bool: @@ -219,18 +221,33 @@ class NodeModel(Node): return result -class BaseGraph(BaseDbModel): +class GraphBaseMeta(BaseDbModel): + """ + Shared base for `GraphModelMeta` and `BaseGraph`, with core graph metadata fields. + """ + version: int = 1 is_active: bool = True name: str description: str instructions: str | None = None recommended_schedule_cron: str | None = None - nodes: list[Node] = [] - links: list[Link] = [] forked_from_id: str | None = None forked_from_version: int | None = None + +class BaseGraph(GraphBaseMeta): + """ + Graph with nodes, links, and computed I/O schema fields. + + Used to represent sub-graphs within a `Graph`. Contains the full graph + structure including nodes and links, plus computed fields for schemas + and trigger info. Does NOT include user_id or created_at (see GraphModel). + """ + + nodes: list[Node] = Field(default_factory=list) + links: list[Link] = Field(default_factory=list) + @computed_field @property def input_schema(self) -> dict[str, Any]: @@ -359,7 +376,73 @@ class GraphTriggerInfo(BaseModel): class Graph(BaseGraph): - sub_graphs: list[BaseGraph] = [] # Flattened sub-graphs + """Creatable graph model used in API create/update endpoints.""" + + sub_graphs: list[BaseGraph] = Field(default_factory=list) # Flattened sub-graphs + + +class GraphMeta(GraphBaseMeta): + """ + Lightweight graph metadata model representing an existing graph from the database, + for use in listings and summaries. + + Lacks `GraphModel`'s nodes, links, and expensive computed fields. + Use for list endpoints where full graph data is not needed and performance matters. + """ + + id: str # type: ignore + version: int # type: ignore + user_id: str + created_at: datetime + + @staticmethod + def from_db(graph: "AgentGraph") -> "GraphMeta": + return GraphMeta( + id=graph.id, + version=graph.version, + is_active=graph.isActive, + name=graph.name or "", + description=graph.description or "", + instructions=graph.instructions, + recommended_schedule_cron=graph.recommendedScheduleCron, + forked_from_id=graph.forkedFromId, + forked_from_version=graph.forkedFromVersion, + user_id=graph.userId, + created_at=graph.createdAt, + ) + + +class GraphModel(Graph, GraphMeta): + """ + Full graph model representing an existing graph from the database. + + This is the primary model for working with persisted graphs. Includes all + graph data (nodes, links, sub_graphs) plus user ownership and timestamps. + Provides computed fields (input_schema, output_schema, etc.) used during + set-up (frontend) and execution (backend). + + Inherits from: + - `Graph`: provides structure (nodes, links, sub_graphs) and computed schemas + - `GraphMeta`: provides user_id, created_at for database records + """ + + nodes: list[NodeModel] = Field(default_factory=list) # type: ignore + + @property + def starting_nodes(self) -> list[NodeModel]: + outbound_nodes = {link.sink_id for link in self.links} + input_nodes = { + node.id for node in self.nodes if node.block.block_type == BlockType.INPUT + } + return [ + node + for node in self.nodes + if node.id not in outbound_nodes or node.id in input_nodes + ] + + @property + def webhook_input_node(self) -> NodeModel | None: # type: ignore + return cast(NodeModel, super().webhook_input_node) @computed_field @property @@ -521,36 +604,6 @@ class Graph(BaseGraph): for key, (field_info, node_field_pairs) in combined.items() } - -class GraphModel(Graph): - user_id: str - nodes: list[NodeModel] = [] # type: ignore - - created_at: datetime - - @property - def starting_nodes(self) -> list[NodeModel]: - outbound_nodes = {link.sink_id for link in self.links} - input_nodes = { - node.id for node in self.nodes if node.block.block_type == BlockType.INPUT - } - return [ - node - for node in self.nodes - if node.id not in outbound_nodes or node.id in input_nodes - ] - - @property - def webhook_input_node(self) -> NodeModel | None: # type: ignore - return cast(NodeModel, super().webhook_input_node) - - def meta(self) -> "GraphMeta": - """ - Returns a GraphMeta object with metadata about the graph. - This is used to return metadata about the graph without exposing nodes and links. - """ - return GraphMeta.from_graph(self) - def reassign_ids(self, user_id: str, reassign_graph_id: bool = False): """ Reassigns all IDs in the graph to new UUIDs. @@ -865,56 +918,28 @@ class GraphModel(Graph): ], ) + def hide_nodes(self) -> "GraphModelWithoutNodes": + """ + Returns a copy of the `GraphModel` with nodes, links, and sub-graphs hidden + (excluded from serialization). They are still present in the model instance + so all computed fields (e.g. `credentials_input_schema`) still work. + """ + return GraphModelWithoutNodes.model_validate(self, from_attributes=True) -class GraphMeta(BaseModel): + +class GraphModelWithoutNodes(GraphModel): """ - Graph metadata without nodes/links, used for list endpoints. + GraphModel variant that excludes nodes, links, and sub-graphs from serialization. - This is a flat, lightweight model (not inheriting from Graph) to avoid recomputing - expensive computed fields. Values are copied from GraphModel. + Used in contexts like the store where exposing internal graph structure + is not desired. Inherits all computed fields from GraphModel but marks + nodes and links as excluded from JSON output. """ - id: str - version: int = 1 - is_active: bool = True - name: str - description: str - instructions: str | None = None - recommended_schedule_cron: str | None = None - forked_from_id: str | None = None - forked_from_version: int | None = None - user_id: str + nodes: list[NodeModel] = Field(default_factory=list, exclude=True) + links: list[Link] = Field(default_factory=list, exclude=True) - input_schema: dict[str, Any] - output_schema: dict[str, Any] - credentials_input_schema: dict[str, Any] - has_external_trigger: bool - has_human_in_the_loop: bool - has_sensitive_action: bool - trigger_setup_info: Optional["GraphTriggerInfo"] - - @staticmethod - def from_graph(graph: "GraphModel") -> "GraphMeta": - return GraphMeta( - id=graph.id, - version=graph.version, - is_active=graph.is_active, - name=graph.name, - description=graph.description, - instructions=graph.instructions, - recommended_schedule_cron=graph.recommended_schedule_cron, - forked_from_id=graph.forked_from_id, - forked_from_version=graph.forked_from_version, - user_id=graph.user_id, - # Pre-computed values (were @computed_field on Graph) - input_schema=graph.input_schema, - output_schema=graph.output_schema, - has_external_trigger=graph.has_external_trigger, - has_human_in_the_loop=graph.has_human_in_the_loop, - has_sensitive_action=graph.has_sensitive_action, - trigger_setup_info=graph.trigger_setup_info, - credentials_input_schema=graph.credentials_input_schema, - ) + sub_graphs: list[BaseGraph] = Field(default_factory=list, exclude=True) class GraphsPaginated(BaseModel): @@ -985,21 +1010,11 @@ async def list_graphs_paginated( where=where_clause, distinct=["id"], order={"version": "desc"}, - include=AGENT_GRAPH_INCLUDE, skip=offset, take=page_size, ) - graph_models: list[GraphMeta] = [] - for graph in graphs: - try: - # GraphMeta.from_graph() accesses all computed fields on the GraphModel, - # which validates that the graph is well formed (e.g. no unknown block_ids). - graph_meta = GraphModel.from_db(graph).meta() - graph_models.append(graph_meta) - except Exception as e: - logger.error(f"Error processing graph {graph.id}: {e}") - continue + graph_models = [GraphMeta.from_db(graph) for graph in graphs] return GraphsPaginated( graphs=graph_models, diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/AgentOnboardingCredentials.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/AgentOnboardingCredentials.tsx index f0bb652a06..a8efa344a2 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/AgentOnboardingCredentials.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/AgentOnboardingCredentials.tsx @@ -1,5 +1,5 @@ import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput"; -import { GraphMeta } from "@/app/api/__generated__/models/graphMeta"; +import { GraphModel } from "@/app/api/__generated__/models/graphModel"; import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput"; import { useState } from "react"; import { getSchemaDefaultCredentials } from "../../helpers"; @@ -9,7 +9,7 @@ type Credential = CredentialsMetaInput | undefined; type Credentials = Record; type Props = { - agent: GraphMeta | null; + agent: GraphModel | null; siblingInputs?: Record; onCredentialsChange: ( credentials: Record, diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/helpers.ts b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/helpers.ts index 7a456d63e4..a4947015c4 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/helpers.ts +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/components/AgentOnboardingCredentials/helpers.ts @@ -1,9 +1,9 @@ import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput"; -import { GraphMeta } from "@/app/api/__generated__/models/graphMeta"; +import { GraphModel } from "@/app/api/__generated__/models/graphModel"; import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types"; export function getCredentialFields( - agent: GraphMeta | null, + agent: GraphModel | null, ): AgentCredentialsFields { if (!agent) return {}; diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/helpers.ts b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/helpers.ts index 62f5c564ff..ff1f8d452c 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/helpers.ts +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/helpers.ts @@ -3,10 +3,10 @@ import type { CredentialsMetaInput, } from "@/lib/autogpt-server-api/types"; import type { InputValues } from "./types"; -import { GraphMeta } from "@/app/api/__generated__/models/graphMeta"; +import { GraphModel } from "@/app/api/__generated__/models/graphModel"; export function computeInitialAgentInputs( - agent: GraphMeta | null, + agent: GraphModel | null, existingInputs?: InputValues | null, ): InputValues { const properties = agent?.input_schema?.properties || {}; @@ -29,7 +29,7 @@ export function computeInitialAgentInputs( } type IsRunDisabledParams = { - agent: GraphMeta | null; + agent: GraphModel | null; isRunning: boolean; agentInputs: InputValues | null | undefined; }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx index f5451e6d4d..99b66fe1dc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/BlocksControl.tsx @@ -30,6 +30,8 @@ import { } from "@/components/atoms/Tooltip/BaseTooltip"; import { GraphMeta } from "@/lib/autogpt-server-api"; import jaro from "jaro-winkler"; +import { getV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs"; +import { okData } from "@/app/api/helpers"; type _Block = Omit & { uiKey?: string; @@ -107,6 +109,8 @@ export function BlocksControl({ .filter((b) => b.uiType !== BlockUIType.AGENT) .sort((a, b) => a.name.localeCompare(b.name)); + // Agent blocks are created from GraphMeta which doesn't include schemas. + // Schemas will be fetched on-demand when the block is actually added. const agentBlockList = flows .map((flow): _Block => { return { @@ -116,8 +120,9 @@ export function BlocksControl({ `Ver.${flow.version}` + (flow.description ? ` | ${flow.description}` : ""), categories: [{ category: "AGENT", description: "" }], - inputSchema: flow.input_schema, - outputSchema: flow.output_schema, + // Empty schemas - will be populated when block is added + inputSchema: { type: "object", properties: {} }, + outputSchema: { type: "object", properties: {} }, staticOutput: false, uiType: BlockUIType.AGENT, costs: [], @@ -125,8 +130,7 @@ export function BlocksControl({ hardcodedValues: { graph_id: flow.id, graph_version: flow.version, - input_schema: flow.input_schema, - output_schema: flow.output_schema, + // Schemas will be fetched on-demand when block is added }, }; }) @@ -182,6 +186,37 @@ export function BlocksControl({ setSelectedCategory(null); }, []); + // Handler to add a block, fetching graph data on-demand for agent blocks + const handleAddBlock = useCallback( + async (block: _Block & { notAvailable: string | null }) => { + if (block.notAvailable) return; + + // For agent blocks, fetch the full graph to get schemas + if (block.uiType === BlockUIType.AGENT && block.hardcodedValues) { + const graphID = block.hardcodedValues.graph_id as string; + const graphVersion = block.hardcodedValues.graph_version as number; + const graphData = okData( + await getV1GetSpecificGraph(graphID, { version: graphVersion }), + ); + + if (graphData) { + addBlock(block.id, block.name, { + ...block.hardcodedValues, + input_schema: graphData.input_schema, + output_schema: graphData.output_schema, + }); + } else { + // Fallback: add without schemas (will be incomplete) + console.error("Failed to fetch graph data for agent block"); + addBlock(block.id, block.name, block.hardcodedValues || {}); + } + } else { + addBlock(block.id, block.name, block.hardcodedValues || {}); + } + }, + [addBlock], + ); + // Extract unique categories from blocks const categories = useMemo(() => { return Array.from( @@ -303,10 +338,7 @@ export function BlocksControl({ }), ); }} - onClick={() => - !block.notAvailable && - addBlock(block.id, block.name, block?.hardcodedValues || {}) - } + onClick={() => handleAddBlock(block)} title={block.notAvailable ?? undefined} >
void; - graph: GraphMeta; + graph: Graph; doRun?: ( inputs: Record, credentialsInputs: Record, diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/RunnerUIWrapper.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/RunnerUIWrapper.tsx index a9af065a5d..b1d40fb919 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/RunnerUIWrapper.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/RunnerUIWrapper.tsx @@ -9,13 +9,13 @@ import { CustomNodeData } from "@/app/(platform)/build/components/legacy-builder import { BlockUIType, CredentialsMetaInput, - GraphMeta, + Graph, } from "@/lib/autogpt-server-api/types"; import RunnerOutputUI, { OutputNodeInfo } from "./RunnerOutputUI"; import { RunnerInputDialog } from "./RunnerInputUI"; interface RunnerUIWrapperProps { - graph: GraphMeta; + graph: Graph; nodes: Node[]; graphExecutionError?: string | null; saveAndRun: ( diff --git a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/helpers.ts index aece7e9811..69593a142b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/helpers.ts @@ -1,5 +1,5 @@ import { GraphInputSchema } from "@/lib/autogpt-server-api"; -import { GraphMetaLike, IncompatibilityInfo } from "./types"; +import { GraphLike, IncompatibilityInfo } from "./types"; // Helper type for schema properties - the generated types are too loose type SchemaProperties = Record; @@ -36,7 +36,7 @@ export function getSchemaRequired(schema: unknown): SchemaRequired { */ export function createUpdatedAgentNodeInputs( currentInputs: Record, - latestSubGraphVersion: GraphMetaLike, + latestSubGraphVersion: GraphLike, ): Record { return { ...currentInputs, diff --git a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/types.ts b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/types.ts index 83f83155db..6c115f20a3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/types.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/types.ts @@ -1,7 +1,11 @@ -import type { GraphMeta as LegacyGraphMeta } from "@/lib/autogpt-server-api"; +import type { + Graph as LegacyGraph, + GraphMeta as LegacyGraphMeta, +} from "@/lib/autogpt-server-api"; +import type { GraphModel as GeneratedGraph } from "@/app/api/__generated__/models/graphModel"; import type { GraphMeta as GeneratedGraphMeta } from "@/app/api/__generated__/models/graphMeta"; -export type SubAgentUpdateInfo = { +export type SubAgentUpdateInfo = { hasUpdate: boolean; currentVersion: number; latestVersion: number; @@ -10,7 +14,10 @@ export type SubAgentUpdateInfo = { incompatibilities: IncompatibilityInfo | null; }; -// Union type for GraphMeta that works with both legacy and new builder +// Union type for Graph (with schemas) that works with both legacy and new builder +export type GraphLike = LegacyGraph | GeneratedGraph; + +// Union type for GraphMeta (without schemas) for version detection export type GraphMetaLike = LegacyGraphMeta | GeneratedGraphMeta; export type IncompatibilityInfo = { diff --git a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/useSubAgentUpdate.ts b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/useSubAgentUpdate.ts index 315e337cd6..7ad10ea697 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/useSubAgentUpdate.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/hooks/useSubAgentUpdate/useSubAgentUpdate.ts @@ -1,5 +1,11 @@ import { useMemo } from "react"; -import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api"; +import type { + GraphInputSchema, + GraphOutputSchema, +} from "@/lib/autogpt-server-api"; +import type { GraphModel } from "@/app/api/__generated__/models/graphModel"; +import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs"; +import { okData } from "@/app/api/helpers"; import { getEffectiveType } from "@/lib/utils"; import { EdgeLike, getSchemaProperties, getSchemaRequired } from "./helpers"; import { @@ -11,26 +17,38 @@ import { /** * Checks if a newer version of a sub-agent is available and determines compatibility */ -export function useSubAgentUpdate( +export function useSubAgentUpdate( nodeID: string, graphID: string | undefined, graphVersion: number | undefined, currentInputSchema: GraphInputSchema | undefined, currentOutputSchema: GraphOutputSchema | undefined, connections: EdgeLike[], - availableGraphs: T[], -): SubAgentUpdateInfo { + availableGraphs: GraphMetaLike[], +): SubAgentUpdateInfo { // Find the latest version of the same graph - const latestGraph = useMemo(() => { + const latestGraphInfo = useMemo(() => { if (!graphID) return null; return availableGraphs.find((graph) => graph.id === graphID) || null; }, [graphID, availableGraphs]); - // Check if there's an update available + // Check if there's a newer version available const hasUpdate = useMemo(() => { - if (!latestGraph || graphVersion === undefined) return false; - return latestGraph.version! > graphVersion; - }, [latestGraph, graphVersion]); + if (!latestGraphInfo || graphVersion === undefined) return false; + return latestGraphInfo.version! > graphVersion; + }, [latestGraphInfo, graphVersion]); + + // Fetch full graph IF an update is detected + const { data: latestGraph } = useGetV1GetSpecificGraph( + graphID ?? "", + { version: latestGraphInfo?.version }, + { + query: { + enabled: hasUpdate && !!graphID && !!latestGraphInfo?.version, + select: okData, + }, + }, + ); // Get connected input and output handles for this specific node const connectedHandles = useMemo(() => { @@ -152,8 +170,8 @@ export function useSubAgentUpdate( return { hasUpdate, currentVersion: graphVersion || 0, - latestVersion: latestGraph?.version || 0, - latestGraph, + latestVersion: latestGraphInfo?.version || 0, + latestGraph: latestGraph || null, isCompatible: compatibilityResult.isCompatible, incompatibilities: compatibilityResult.incompatibilities, }; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/graphStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/graphStore.ts index 6961884732..c1eba556d2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/stores/graphStore.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/graphStore.ts @@ -18,7 +18,7 @@ interface GraphStore { outputSchema: Record | null, ) => void; - // Available graphs; used for sub-graph updates + // Available graphs; used for sub-graph updated version detection availableSubGraphs: GraphMeta[]; setAvailableSubGraphs: (graphs: GraphMeta[]) => void; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx index 0147c19a5c..b0c3a6ff7b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx @@ -10,8 +10,8 @@ import React, { import { CredentialsMetaInput, CredentialsType, + Graph, GraphExecutionID, - GraphMeta, LibraryAgentPreset, LibraryAgentPresetID, LibraryAgentPresetUpdatable, @@ -69,7 +69,7 @@ export function AgentRunDraftView({ className, recommendedScheduleCron, }: { - graph: GraphMeta; + graph: Graph; agentActions?: ButtonAction[]; recommendedScheduleCron?: string | null; doRun?: ( diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx index 61161088fc..30b0a82e65 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useMemo } from "react"; import { + Graph, GraphExecutionID, - GraphMeta, Schedule, ScheduleID, } from "@/lib/autogpt-server-api"; @@ -35,7 +35,7 @@ export function AgentScheduleDetailsView({ onForcedRun, doDeleteSchedule, }: { - graph: GraphMeta; + graph: Graph; schedule: Schedule; agentActions: ButtonAction[]; onForcedRun: (runID: GraphExecutionID) => void; diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 00980da6b0..0e9020272d 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -5629,7 +5629,9 @@ "description": "Successful Response", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/GraphMeta" } + "schema": { + "$ref": "#/components/schemas/GraphModelWithoutNodes" + } } } }, @@ -6495,18 +6497,6 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Recommended Schedule Cron" }, - "nodes": { - "items": { "$ref": "#/components/schemas/Node" }, - "type": "array", - "title": "Nodes", - "default": [] - }, - "links": { - "items": { "$ref": "#/components/schemas/Link" }, - "type": "array", - "title": "Links", - "default": [] - }, "forked_from_id": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Forked From Id" @@ -6514,11 +6504,22 @@ "forked_from_version": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Forked From Version" + }, + "nodes": { + "items": { "$ref": "#/components/schemas/Node" }, + "type": "array", + "title": "Nodes" + }, + "links": { + "items": { "$ref": "#/components/schemas/Link" }, + "type": "array", + "title": "Links" } }, "type": "object", "required": ["name", "description"], - "title": "BaseGraph" + "title": "BaseGraph", + "description": "Graph with nodes, links, and computed I/O schema fields.\n\nUsed to represent sub-graphs within a `Graph`. Contains the full graph\nstructure including nodes and links, plus computed fields for schemas\nand trigger info. Does NOT include user_id or created_at (see GraphModel)." }, "BaseGraph-Output": { "properties": { @@ -6539,18 +6540,6 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Recommended Schedule Cron" }, - "nodes": { - "items": { "$ref": "#/components/schemas/Node" }, - "type": "array", - "title": "Nodes", - "default": [] - }, - "links": { - "items": { "$ref": "#/components/schemas/Link" }, - "type": "array", - "title": "Links", - "default": [] - }, "forked_from_id": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Forked From Id" @@ -6559,6 +6548,16 @@ "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Forked From Version" }, + "nodes": { + "items": { "$ref": "#/components/schemas/Node" }, + "type": "array", + "title": "Nodes" + }, + "links": { + "items": { "$ref": "#/components/schemas/Link" }, + "type": "array", + "title": "Links" + }, "input_schema": { "additionalProperties": true, "type": "object", @@ -6605,7 +6604,8 @@ "has_sensitive_action", "trigger_setup_info" ], - "title": "BaseGraph" + "title": "BaseGraph", + "description": "Graph with nodes, links, and computed I/O schema fields.\n\nUsed to represent sub-graphs within a `Graph`. Contains the full graph\nstructure including nodes and links, plus computed fields for schemas\nand trigger info. Does NOT include user_id or created_at (see GraphModel)." }, "BlockCategoryResponse": { "properties": { @@ -7399,18 +7399,6 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Recommended Schedule Cron" }, - "nodes": { - "items": { "$ref": "#/components/schemas/Node" }, - "type": "array", - "title": "Nodes", - "default": [] - }, - "links": { - "items": { "$ref": "#/components/schemas/Link" }, - "type": "array", - "title": "Links", - "default": [] - }, "forked_from_id": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Forked From Id" @@ -7419,16 +7407,26 @@ "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Forked From Version" }, + "nodes": { + "items": { "$ref": "#/components/schemas/Node" }, + "type": "array", + "title": "Nodes" + }, + "links": { + "items": { "$ref": "#/components/schemas/Link" }, + "type": "array", + "title": "Links" + }, "sub_graphs": { "items": { "$ref": "#/components/schemas/BaseGraph-Input" }, "type": "array", - "title": "Sub Graphs", - "default": [] + "title": "Sub Graphs" } }, "type": "object", "required": ["name", "description"], - "title": "Graph" + "title": "Graph", + "description": "Creatable graph model used in API create/update endpoints." }, "GraphExecution": { "properties": { @@ -7778,6 +7776,52 @@ "description": "Response schema for paginated graph executions." }, "GraphMeta": { + "properties": { + "id": { "type": "string", "title": "Id" }, + "version": { "type": "integer", "title": "Version" }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "default": true + }, + "name": { "type": "string", "title": "Name" }, + "description": { "type": "string", "title": "Description" }, + "instructions": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Instructions" + }, + "recommended_schedule_cron": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Recommended Schedule Cron" + }, + "forked_from_id": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Forked From Id" + }, + "forked_from_version": { + "anyOf": [{ "type": "integer" }, { "type": "null" }], + "title": "Forked From Version" + }, + "user_id": { "type": "string", "title": "User Id" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "version", + "name", + "description", + "user_id", + "created_at" + ], + "title": "GraphMeta", + "description": "Lightweight graph metadata model representing an existing graph from the database,\nfor use in listings and summaries.\n\nLacks `GraphModel`'s nodes, links, and expensive computed fields.\nUse for list endpoints where full graph data is not needed and performance matters." + }, + "GraphModel": { "properties": { "id": { "type": "string", "title": "Id" }, "version": { "type": "integer", "title": "Version", "default": 1 }, @@ -7805,58 +7849,85 @@ "title": "Forked From Version" }, "user_id": { "type": "string", "title": "User Id" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "nodes": { + "items": { "$ref": "#/components/schemas/NodeModel" }, + "type": "array", + "title": "Nodes" + }, + "links": { + "items": { "$ref": "#/components/schemas/Link" }, + "type": "array", + "title": "Links" + }, + "sub_graphs": { + "items": { "$ref": "#/components/schemas/BaseGraph-Output" }, + "type": "array", + "title": "Sub Graphs" + }, "input_schema": { "additionalProperties": true, "type": "object", - "title": "Input Schema" + "title": "Input Schema", + "readOnly": true }, "output_schema": { "additionalProperties": true, "type": "object", - "title": "Output Schema" - }, - "credentials_input_schema": { - "additionalProperties": true, - "type": "object", - "title": "Credentials Input Schema" + "title": "Output Schema", + "readOnly": true }, "has_external_trigger": { "type": "boolean", - "title": "Has External Trigger" + "title": "Has External Trigger", + "readOnly": true }, "has_human_in_the_loop": { "type": "boolean", - "title": "Has Human In The Loop" + "title": "Has Human In The Loop", + "readOnly": true }, "has_sensitive_action": { "type": "boolean", - "title": "Has Sensitive Action" + "title": "Has Sensitive Action", + "readOnly": true }, "trigger_setup_info": { "anyOf": [ { "$ref": "#/components/schemas/GraphTriggerInfo" }, { "type": "null" } - ] + ], + "readOnly": true + }, + "credentials_input_schema": { + "additionalProperties": true, + "type": "object", + "title": "Credentials Input Schema", + "readOnly": true } }, "type": "object", "required": [ - "id", "name", "description", "user_id", + "created_at", "input_schema", "output_schema", - "credentials_input_schema", "has_external_trigger", "has_human_in_the_loop", "has_sensitive_action", - "trigger_setup_info" + "trigger_setup_info", + "credentials_input_schema" ], - "title": "GraphMeta", - "description": "Graph metadata without nodes/links, used for list endpoints.\n\nThis is a flat, lightweight model (not inheriting from Graph) to avoid recomputing\nexpensive computed fields. Values are copied from GraphModel." + "title": "GraphModel", + "description": "Full graph model representing an existing graph from the database.\n\nThis is the primary model for working with persisted graphs. Includes all\ngraph data (nodes, links, sub_graphs) plus user ownership and timestamps.\nProvides computed fields (input_schema, output_schema, etc.) used during\nset-up (frontend) and execution (backend).\n\nInherits from:\n- `Graph`: provides structure (nodes, links, sub_graphs) and computed schemas\n- `GraphMeta`: provides user_id, created_at for database records" }, - "GraphModel": { + "GraphModelWithoutNodes": { "properties": { "id": { "type": "string", "title": "Id" }, "version": { "type": "integer", "title": "Version", "default": 1 }, @@ -7875,18 +7946,6 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Recommended Schedule Cron" }, - "nodes": { - "items": { "$ref": "#/components/schemas/NodeModel" }, - "type": "array", - "title": "Nodes", - "default": [] - }, - "links": { - "items": { "$ref": "#/components/schemas/Link" }, - "type": "array", - "title": "Links", - "default": [] - }, "forked_from_id": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Forked From Id" @@ -7895,12 +7954,6 @@ "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Forked From Version" }, - "sub_graphs": { - "items": { "$ref": "#/components/schemas/BaseGraph-Output" }, - "type": "array", - "title": "Sub Graphs", - "default": [] - }, "user_id": { "type": "string", "title": "User Id" }, "created_at": { "type": "string", @@ -7962,7 +8015,8 @@ "trigger_setup_info", "credentials_input_schema" ], - "title": "GraphModel" + "title": "GraphModelWithoutNodes", + "description": "GraphModel variant that excludes nodes, links, and sub-graphs from serialization.\n\nUsed in contexts like the store where exposing internal graph structure\nis not desired. Inherits all computed fields from GraphModel but marks\nnodes and links as excluded from JSON output." }, "GraphSettings": { "properties": { @@ -8602,26 +8656,22 @@ "input_default": { "additionalProperties": true, "type": "object", - "title": "Input Default", - "default": {} + "title": "Input Default" }, "metadata": { "additionalProperties": true, "type": "object", - "title": "Metadata", - "default": {} + "title": "Metadata" }, "input_links": { "items": { "$ref": "#/components/schemas/Link" }, "type": "array", - "title": "Input Links", - "default": [] + "title": "Input Links" }, "output_links": { "items": { "$ref": "#/components/schemas/Link" }, "type": "array", - "title": "Output Links", - "default": [] + "title": "Output Links" } }, "type": "object", @@ -8701,26 +8751,22 @@ "input_default": { "additionalProperties": true, "type": "object", - "title": "Input Default", - "default": {} + "title": "Input Default" }, "metadata": { "additionalProperties": true, "type": "object", - "title": "Metadata", - "default": {} + "title": "Metadata" }, "input_links": { "items": { "$ref": "#/components/schemas/Link" }, "type": "array", - "title": "Input Links", - "default": [] + "title": "Input Links" }, "output_links": { "items": { "$ref": "#/components/schemas/Link" }, "type": "array", - "title": "Output Links", - "default": [] + "title": "Output Links" }, "graph_id": { "type": "string", "title": "Graph Id" }, "graph_version": { "type": "integer", "title": "Graph Version" }, diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index 74855f5e28..44fb25dbfc 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -362,25 +362,14 @@ export type GraphMeta = { user_id: UserID; version: number; is_active: boolean; + created_at: Date; name: string; description: string; instructions?: string | null; recommended_schedule_cron: string | null; forked_from_id?: GraphID | null; forked_from_version?: number | null; - input_schema: GraphInputSchema; - output_schema: GraphOutputSchema; - credentials_input_schema: CredentialsInputSchema; -} & ( - | { - has_external_trigger: true; - trigger_setup_info: GraphTriggerInfo; - } - | { - has_external_trigger: false; - trigger_setup_info: null; - } -); +}; export type GraphID = Brand; @@ -447,11 +436,22 @@ export type GraphTriggerInfo = { /* Mirror of backend/data/graph.py:Graph */ export type Graph = GraphMeta & { - created_at: Date; nodes: Node[]; links: Link[]; sub_graphs: Omit[]; // Flattened sub-graphs -}; + input_schema: GraphInputSchema; + output_schema: GraphOutputSchema; + credentials_input_schema: CredentialsInputSchema; +} & ( + | { + has_external_trigger: true; + trigger_setup_info: GraphTriggerInfo; + } + | { + has_external_trigger: false; + trigger_setup_info: null; + } + ); export type GraphUpdateable = Omit< Graph,