feat(platform): Introduced Agent Execution Block (#8533)

pull/8627/merge
Zamil Majdy 2024-11-12 13:03:15 +07:00 committed by GitHub
parent 5ee909f687
commit 1e872406ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 324 additions and 157 deletions

View File

@ -0,0 +1,100 @@
import logging
from autogpt_libs.utils.cache import thread_cached
from backend.data.block import (
Block,
BlockCategory,
BlockInput,
BlockOutput,
BlockSchema,
BlockType,
get_block,
)
from backend.data.execution import ExecutionStatus
from backend.data.model import SchemaField
logger = logging.getLogger(__name__)
@thread_cached
def get_executor_manager_client():
from backend.executor import ExecutionManager
from backend.util.service import get_service_client
return get_service_client(ExecutionManager)
@thread_cached
def get_event_bus():
from backend.data.queue import RedisExecutionEventBus
return RedisExecutionEventBus()
class AgentExecutorBlock(Block):
class Input(BlockSchema):
user_id: str = SchemaField(description="User ID")
graph_id: str = SchemaField(description="Graph ID")
graph_version: int = SchemaField(description="Graph Version")
data: BlockInput = SchemaField(description="Input data for the graph")
input_schema: dict = SchemaField(description="Input schema for the graph")
output_schema: dict = SchemaField(description="Output schema for the graph")
class Output(BlockSchema):
pass
def __init__(self):
super().__init__(
id="e189baac-8c20-45a1-94a7-55177ea42565",
description="Executes an existing agent inside your agent",
input_schema=AgentExecutorBlock.Input,
output_schema=AgentExecutorBlock.Output,
block_type=BlockType.AGENT,
categories={BlockCategory.AGENT},
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
executor_manager = get_executor_manager_client()
event_bus = get_event_bus()
graph_exec = executor_manager.add_execution(
graph_id=input_data.graph_id,
graph_version=input_data.graph_version,
user_id=input_data.user_id,
data=input_data.data,
)
log_id = f"Graph #{input_data.graph_id}-V{input_data.graph_version}, exec-id: {graph_exec.graph_exec_id}"
logger.info(f"Starting execution of {log_id}")
for event in event_bus.listen(
graph_id=graph_exec.graph_id, graph_exec_id=graph_exec.graph_exec_id
):
logger.info(
f"Execution {log_id} produced input {event.input_data} output {event.output_data}"
)
if not event.node_id:
if event.status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED]:
logger.info(f"Execution {log_id} ended with status {event.status}")
break
else:
continue
if not event.block_id:
logger.warning(f"{log_id} received event without block_id {event}")
continue
block = get_block(event.block_id)
if not block or block.block_type != BlockType.OUTPUT:
continue
output_name = event.input_data.get("name")
if not output_name:
logger.warning(f"{log_id} produced an output with no name {event}")
continue
for output_data in event.output_data.get("output", []):
logger.info(f"Execution {log_id} produced {output_name}: {output_data}")
yield output_name, output_data

View File

@ -233,7 +233,9 @@ class AgentOutputBlock(Block):
)
name: str = SchemaField(description="The name of the output.")
title: str | None = SchemaField(
description="The title of the input.", default=None, advanced=True
description="The title of the output.",
default=None,
advanced=True,
)
description: str | None = SchemaField(
description="The description of the output.",
@ -262,7 +264,7 @@ class AgentOutputBlock(Block):
def __init__(self):
super().__init__(
id="363ae599-353e-4804-937e-b2ee3cef3da4",
description=("Stores the output of the graph for users to see."),
description="Stores the output of the graph for users to see.",
input_schema=AgentOutputBlock.Input,
output_schema=AgentOutputBlock.Output,
test_input=[

View File

@ -34,6 +34,7 @@ class BlockType(Enum):
INPUT = "Input"
OUTPUT = "Output"
NOTE = "Note"
AGENT = "Agent"
class BlockCategory(Enum):
@ -48,6 +49,7 @@ class BlockCategory(Enum):
COMMUNICATION = "Block that interacts with communication platforms."
DEVELOPER_TOOLS = "Developer tools such as GitHub blocks."
DATA = "Block that interacts with structured data."
AGENT = "Block that interacts with other agents."
def dict(self) -> dict[str, str]:
return {"category": self.name, "description": self.value}
@ -299,7 +301,9 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
):
if output_name == "error":
raise RuntimeError(output_data)
if error := self.output_schema.validate_field(output_name, output_data):
if self.block_type == BlockType.STANDARD and (
error := self.output_schema.validate_field(output_name, output_data)
):
raise ValueError(f"Block produced an invalid output data: {error}")
yield output_name, output_data

View File

@ -64,6 +64,7 @@ class ExecutionResult(BaseModel):
graph_exec_id: str
node_exec_id: str
node_id: str
block_id: str
status: ExecutionStatus
input_data: BlockInput
output_data: CompletedBlockOutput
@ -72,6 +73,26 @@ class ExecutionResult(BaseModel):
start_time: datetime | None
end_time: datetime | None
@staticmethod
def from_graph(graph: AgentGraphExecution):
return ExecutionResult(
graph_id=graph.agentGraphId,
graph_version=graph.agentGraphVersion,
graph_exec_id=graph.id,
node_exec_id="",
node_id="",
block_id="",
status=graph.executionStatus,
# TODO: Populate input_data & output_data from AgentNodeExecutions
# Input & Output comes AgentInputBlock & AgentOutputBlock.
input_data={},
output_data={},
add_time=graph.createdAt,
queue_time=graph.createdAt,
start_time=graph.startedAt,
end_time=graph.updatedAt,
)
@staticmethod
def from_db(execution: AgentNodeExecution):
if execution.executionData:
@ -93,9 +114,10 @@ class ExecutionResult(BaseModel):
graph_id=graph_execution.agentGraphId if graph_execution else "",
graph_version=graph_execution.agentGraphVersion if graph_execution else 0,
graph_exec_id=execution.agentGraphExecutionId,
block_id=execution.AgentNode.agentBlockId if execution.AgentNode else "",
node_exec_id=execution.id,
node_id=execution.agentNodeId,
status=ExecutionStatus(execution.executionStatus),
status=execution.executionStatus,
input_data=input_data,
output_data=output_data,
add_time=execution.addedTime,
@ -248,15 +270,20 @@ async def update_graph_execution_start_time(graph_exec_id: str):
async def update_graph_execution_stats(
graph_exec_id: str,
stats: dict[str, Any],
):
) -> ExecutionResult:
status = ExecutionStatus.FAILED if stats.get("error") else ExecutionStatus.COMPLETED
await AgentGraphExecution.prisma().update(
res = await AgentGraphExecution.prisma().update(
where={"id": graph_exec_id},
data={
"executionStatus": status,
"stats": json.dumps(stats),
},
)
if not res:
raise ValueError(f"Execution {graph_exec_id} not found.")
return ExecutionResult.from_graph(res)
async def update_node_execution_stats(node_exec_id: str, stats: dict[str, Any]):

View File

@ -9,6 +9,7 @@ from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeL
from prisma.types import AgentGraphWhereInput
from pydantic.fields import computed_field
from backend.blocks.agent import AgentExecutorBlock
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
@ -174,24 +175,35 @@ class Graph(BaseDbModel):
if node.id not in outbound_nodes or node.id in input_nodes
]
def reassign_ids(self, reassign_graph_id: bool = False):
def reassign_ids(self, user_id: str, reassign_graph_id: bool = False):
"""
Reassigns all IDs in the graph to new UUIDs.
This method can be used before storing a new graph to the database.
"""
self.validate_graph()
# Reassign Graph ID
id_map = {node.id: str(uuid.uuid4()) for node in self.nodes}
if reassign_graph_id:
self.id = str(uuid.uuid4())
# Reassign Node IDs
for node in self.nodes:
node.id = id_map[node.id]
# Reassign Link IDs
for link in self.links:
link.source_id = id_map[link.source_id]
link.sink_id = id_map[link.sink_id]
# Reassign User IDs for agent blocks
for node in self.nodes:
if node.block_id != AgentExecutorBlock().id:
continue
node.input_default["user_id"] = user_id
node.input_default.setdefault("data", {})
self.validate_graph()
def validate_graph(self, for_run: bool = False):
def sanitize(name):
return name.split("_#_")[0].split("_@_")[0].split("_$_")[0]
@ -215,6 +227,7 @@ class Graph(BaseDbModel):
for_run # Skip input completion validation, unless when executing.
or block.block_type == BlockType.INPUT
or block.block_type == BlockType.OUTPUT
or block.block_type == BlockType.AGENT
):
raise ValueError(
f"Node {block.name} #{node.id} required input missing: `{name}`"
@ -248,18 +261,26 @@ class Graph(BaseDbModel):
)
sanitized_name = sanitize(name)
vals = node.input_default
if i == 0:
fields = f"Valid output fields: {block.output_schema.get_fields()}"
fields = (
block.output_schema.get_fields()
if block.block_type != BlockType.AGENT
else vals.get("output_schema", {}).get("properties", {}).keys()
)
else:
fields = f"Valid input fields: {block.input_schema.get_fields()}"
fields = (
block.input_schema.get_fields()
if block.block_type != BlockType.AGENT
else vals.get("input_schema", {}).get("properties", {}).keys()
)
if sanitized_name not in fields:
raise ValueError(f"{suffix}, `{name}` invalid, {fields}")
fields_msg = f"Allowed fields: {fields}"
raise ValueError(f"{suffix}, `{name}` invalid, {fields_msg}")
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.
@staticmethod
def from_db(graph: AgentGraph, hide_credentials: bool = False):
executions = [

View File

@ -41,8 +41,8 @@ class DatabaseManager(AppService):
return Config().database_api_port
@expose
def send_execution_update(self, execution_result_dict: dict[Any, Any]):
self.event_queue.publish(ExecutionResult(**execution_result_dict))
def send_execution_update(self, execution_result: ExecutionResult):
self.event_queue.publish(execution_result)
@staticmethod
def exposed_run_and_wait(

View File

@ -125,7 +125,7 @@ def execute_node(
def update_execution(status: ExecutionStatus) -> ExecutionResult:
exec_update = db_client.update_execution_status(node_exec_id, status)
db_client.send_execution_update(exec_update.model_dump())
db_client.send_execution_update(exec_update)
return exec_update
node = db_client.get_node(node_id)
@ -251,7 +251,7 @@ def _enqueue_next_nodes(
exec_update = db_client.update_execution_status(
node_exec_id, ExecutionStatus.QUEUED, data
)
db_client.send_execution_update(exec_update.model_dump())
db_client.send_execution_update(exec_update)
return NodeExecution(
user_id=user_id,
graph_exec_id=graph_exec_id,
@ -572,10 +572,11 @@ class Executor:
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(
result = cls.db_client.update_graph_execution_stats(
graph_exec_id=graph_exec.graph_exec_id,
stats=exec_stats,
)
cls.db_client.send_execution_update(result)
@classmethod
@time_measured
@ -729,7 +730,7 @@ class ExecutionManager(AppService):
)
self.active_graph_runs[graph_exec_id] = (future, cancel_event)
future.add_done_callback(
lambda _: self.active_graph_runs.pop(graph_exec_id)
lambda _: self.active_graph_runs.pop(graph_exec_id, None)
)
def cleanup(self):
@ -744,11 +745,17 @@ class ExecutionManager(AppService):
@expose
def add_execution(
self, graph_id: str, data: BlockInput, user_id: str
) -> dict[str, Any]:
graph: Graph | None = self.db_client.get_graph(graph_id, user_id=user_id)
self,
graph_id: str,
data: BlockInput,
user_id: str,
graph_version: int | None = None,
) -> GraphExecution:
graph: Graph | None = self.db_client.get_graph(
graph_id=graph_id, user_id=user_id, version=graph_version
)
if not graph:
raise Exception(f"Graph #{graph_id} not found.")
raise ValueError(f"Graph #{graph_id} not found.")
graph.validate_graph(for_run=True)
self._validate_node_input_credentials(graph, user_id)
@ -770,7 +777,7 @@ class ExecutionManager(AppService):
input_data, error = validate_exec(node, input_data)
if input_data is None:
raise Exception(error)
raise ValueError(error)
else:
nodes_input.append((node.id, input_data))
@ -796,7 +803,7 @@ class ExecutionManager(AppService):
exec_update = self.db_client.update_execution_status(
node_exec.node_exec_id, ExecutionStatus.QUEUED, node_exec.input_data
)
self.db_client.send_execution_update(exec_update.model_dump())
self.db_client.send_execution_update(exec_update)
graph_exec = GraphExecution(
user_id=user_id,
@ -806,7 +813,7 @@ class ExecutionManager(AppService):
)
self.queue.add(graph_exec)
return graph_exec.model_dump()
return graph_exec
@expose
def cancel_execution(self, graph_exec_id: str) -> None:
@ -843,7 +850,7 @@ class ExecutionManager(AppService):
exec_update = self.db_client.update_execution_status(
node_exec.node_exec_id, ExecutionStatus.FAILED
)
self.db_client.send_execution_update(exec_update.model_dump())
self.db_client.send_execution_update(exec_update)
def _validate_node_input_credentials(self, graph: Graph, user_id: str):
"""Checks all credentials for all nodes of the graph"""

View File

@ -209,7 +209,7 @@ async def update_graph(
400, detail="Changing is_template on an existing graph is forbidden"
)
graph.is_active = not graph.is_template
graph.reassign_ids()
graph.reassign_ids(user_id=user_id)
new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
@ -265,7 +265,7 @@ async def execute_graph(
graph_exec = execution_manager_client().add_execution(
graph_id, node_input, user_id=user_id
)
return {"id": graph_exec["graph_exec_id"]}
return {"id": graph_exec.graph_exec_id}
except Exception as e:
msg = e.__str__().encode().decode("unicode_escape")
raise HTTPException(status_code=400, detail=msg)
@ -403,7 +403,7 @@ async def do_create_graph(
graph.is_template = is_template
graph.is_active = not is_template
graph.reassign_ids(reassign_graph_id=True)
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
return await graph_db.create_graph(graph, user_id=user_id)

View File

@ -72,6 +72,7 @@ async def test_send_execution_result(
graph_exec_id="test_exec_id",
node_exec_id="test_node_exec_id",
node_id="test_node_id",
block_id="test_block_id",
status=ExecutionStatus.COMPLETED,
input_data={"input1": "value1"},
output_data={"output1": ["result1"]},
@ -102,6 +103,7 @@ async def test_send_execution_result_no_subscribers(
graph_exec_id="test_exec_id",
node_exec_id="test_node_exec_id",
node_id="test_node_id",
block_id="test_block_id",
status=ExecutionStatus.COMPLETED,
input_data={"input1": "value1"},
output_data={"output1": ["result1"]},

View File

@ -93,6 +93,12 @@ export function CustomNode({
const isInitialSetup = useRef(true);
const flowContext = useContext(FlowContext);
if (data.uiType === BlockUIType.AGENT) {
// Display the graph's schema instead AgentExecutorBlock's schema.
data.inputSchema = data.hardcodedValues?.input_schema || {};
data.outputSchema = data.hardcodedValues?.output_schema || {};
}
if (!flowContext) {
throw new Error("FlowContext consumer must be inside FlowEditor component");
}
@ -163,38 +169,6 @@ export function CustomNode({
if (!schema?.properties) return null;
let keys = Object.entries(schema.properties);
switch (nodeType) {
case BlockUIType.INPUT:
// For INPUT blocks, dont include connection handles
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
return (
(isRequired || isAdvancedOpen || !isAdvanced) && (
<div key={propKey} data-id={`input-handle-${propKey}`}>
<span className="text-m green mb-0 text-gray-900">
{propSchema.title || beautifyString(propKey)}
</span>
<div key={propKey}>
{!isConnected && (
<NodeGenericInputField
nodeId={id}
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
)}
</div>
</div>
)
);
});
case BlockUIType.NOTE:
// For NOTE blocks, don't render any input handles
const [noteKey, noteSchema] = keys[0];
@ -213,59 +187,25 @@ export function CustomNode({
</div>
);
case BlockUIType.OUTPUT:
// For OUTPUT blocks, only show the 'value' property
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
return (
(isRequired || isAdvancedOpen || !isAdvanced) && (
<div key={propKey} data-id={`output-handle-${propKey}`}>
{propKey !== "value" ? (
<span className="text-m green mb-0 text-gray-900">
{propSchema.title || beautifyString(propKey)}
</span>
) : (
<NodeHandle
keyName={propKey}
isConnected={isConnected}
isRequired={isRequired}
schema={propSchema}
side="left"
/>
)}
{!isConnected && (
<NodeGenericInputField
nodeId={id}
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
)}
</div>
)
);
});
default:
const getInputPropKey = (key: string) =>
nodeType == BlockUIType.AGENT ? `data.${key}` : key;
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
const isConnectable =
// No input connection handles for credentials
propKey !== "credentials" &&
// No input connection handles on INPUT blocks
nodeType !== BlockUIType.INPUT &&
// For OUTPUT blocks, only show the 'value' (hides 'name') input connection handle
!(nodeType == BlockUIType.OUTPUT && propKey == "name");
return (
(isRequired || isAdvancedOpen || isConnected || !isAdvanced) && (
<div key={propKey} data-id={`input-handle-${propKey}`}>
{"credentials_provider" in propSchema ? (
<span className="text-m green mb-0 text-gray-900">
Credentials
</span>
) : (
{isConnectable ? (
<NodeHandle
keyName={propKey}
isConnected={isConnected}
@ -273,13 +213,22 @@ export function CustomNode({
schema={propSchema}
side="left"
/>
) : (
<span
className="text-m green mb-0 text-gray-900"
title={propSchema.description}
>
{propKey == "credentials"
? "Credentials"
: propSchema.title || beautifyString(propKey)}
</span>
)}
{!isConnected && (
<NodeGenericInputField
nodeId={id}
propKey={propKey}
propKey={getInputPropKey(propKey)}
propSchema={propSchema}
currentValue={getValue(propKey)}
currentValue={getValue(getInputPropKey(propKey))}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
@ -318,8 +267,6 @@ export function CustomNode({
current[lastKey.key] = value;
}
// console.log(`Updating hardcoded values for node ${id}:`, newValues);
if (!isInitialSetup.current) {
history.push({
type: "UPDATE_INPUT",

View File

@ -27,11 +27,7 @@ import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { BlockUIType, Link } from "@/lib/autogpt-server-api";
import {
getTypeColor,
filterBlocksByType,
findNewlyAddedBlockCoordinates,
} from "@/lib/utils";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
@ -48,7 +44,6 @@ import RunnerUIWrapper, {
} from "@/components/RunnerUIWrapper";
import PrimaryActionBar from "@/components/PrimaryActionButton";
import { useToast } from "@/components/ui/use-toast";
import { forceLoad } from "@sentry/nextjs";
import { useCopyPaste } from "../hooks/useCopyPaste";
// This is for the history, this is the minimum distance a block must move before it is logged
@ -86,8 +81,6 @@ const FlowEditor: React.FC<{
setViewport,
} = useReactFlow<CustomNode, CustomEdge>();
const [nodeId, setNodeId] = useState<number>(1);
const [copiedNodes, setCopiedNodes] = useState<CustomNode[]>([]);
const [copiedEdges, setCopiedEdges] = useState<CustomEdge[]>([]);
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false);
const [visualizeBeads, setVisualizeBeads] = useState<
"no" | "static" | "animate"
@ -99,6 +92,7 @@ const FlowEditor: React.FC<{
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,
@ -417,7 +411,7 @@ const FlowEditor: React.FC<{
const { x, y, zoom } = useViewport();
const addNode = useCallback(
(blockId: string, nodeType: string) => {
(blockId: string, nodeType: string, hardcodedValues: any = {}) => {
const nodeSchema = availableNodes.find((node) => node.id === blockId);
if (!nodeSchema) {
console.error(`Schema not found for block ID: ${blockId}`);
@ -462,7 +456,7 @@ const FlowEditor: React.FC<{
categories: nodeSchema.categories,
inputSchema: nodeSchema.inputSchema,
outputSchema: nodeSchema.outputSchema,
hardcodedValues: {},
hardcodedValues: hardcodedValues,
connections: [],
isOutputOpen: false,
block_id: blockId,
@ -618,6 +612,7 @@ const FlowEditor: React.FC<{
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
blocks={availableNodes}
addBlock={addNode}
flows={availableFlows}
/>
}
botChildren={

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
@ -10,7 +10,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Block } from "@/lib/autogpt-server-api";
import { Block, BlockUIType } from "@/lib/autogpt-server-api";
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
import { IconToyBrick } from "@/components/ui/icons";
import { getPrimaryCategoryColor } from "@/lib/utils";
@ -19,11 +19,17 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { GraphMeta } from "@/lib/autogpt-server-api";
interface BlocksControlProps {
blocks: Block[];
addBlock: (id: string, name: string) => void;
addBlock: (
id: string,
name: string,
hardcodedValues: Record<string, any>,
) => void;
pinBlocksPopover: boolean;
flows: GraphMeta[];
}
/**
@ -39,29 +45,42 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
blocks,
addBlock,
pinBlocksPopover,
flows,
}) => {
const blockList = blocks.sort((a, b) => a.name.localeCompare(b.name));
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [filteredBlocks, setFilteredBlocks] = useState<Block[]>(blockList);
const resetFilters = React.useCallback(() => {
setSearchQuery("");
setSelectedCategory(null);
setFilteredBlocks(blockList);
}, [blockList]);
const getFilteredBlockList = (): Block[] => {
const blockList = blocks
.filter((b) => b.uiType !== BlockUIType.AGENT)
.sort((a, b) => a.name.localeCompare(b.name));
const agentList = flows.map(
(flow) =>
({
id: "e189baac-8c20-45a1-94a7-55177ea42565", // TODO: fetch this programmatically.
name: flow.name,
description:
`Ver.${flow.version}` +
(flow.description ? ` | ${flow.description}` : ""),
categories: [{ category: "AGENT", description: "" }],
inputSchema: flow.input_schema,
outputSchema: flow.output_schema,
staticOutput: false,
uiType: BlockUIType.AGENT,
uiKey: flow.id,
costs: [],
hardcodedValues: {
graph_id: flow.id,
graph_version: flow.version,
input_schema: flow.input_schema,
output_schema: flow.output_schema,
},
}) as Block,
);
// Extract unique categories from blocks
const categories = Array.from(
new Set([
null,
...blocks.flatMap((block) => block.categories.map((cat) => cat.category)),
]),
);
React.useEffect(() => {
setFilteredBlocks(
blockList.filter(
return blockList
.concat(agentList)
.filter(
(block: Block) =>
(block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
beautifyString(block.name)
@ -69,9 +88,23 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
.includes(searchQuery.toLowerCase())) &&
(!selectedCategory ||
block.categories.some((cat) => cat.category === selectedCategory)),
),
);
}, [blockList, searchQuery, selectedCategory]);
);
};
const resetFilters = React.useCallback(() => {
setSearchQuery("");
setSelectedCategory(null);
}, []);
// Extract unique categories from blocks
const categories = Array.from(
new Set([
null,
...blocks
.flatMap((block) => block.categories.map((cat) => cat.category))
.sort(),
]),
);
return (
<Popover
@ -150,12 +183,14 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
className="h-[60vh] w-fit w-full"
data-id="blocks-control-scroll-area"
>
{filteredBlocks.map((block) => (
{getFilteredBlockList().map((block) => (
<Card
key={block.id}
key={block.uiKey || block.id}
className="m-2 my-4 flex h-20 cursor-pointer shadow-none hover:shadow-lg"
data-id={`block-card-${block.id}`}
onClick={() => addBlock(block.id, block.name)}
onClick={() =>
addBlock(block.id, block.name, block?.hardcodedValues || {})
}
>
<div
className={`-ml-px h-full w-3 rounded-l-xl ${getPrimaryCategoryColor(block.categories)}`}

View File

@ -53,7 +53,6 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
object ||= ("default" in schema ? schema.default : null) ?? {};
return (
<div className={cn(className, "w-full flex-col")}>
{displayName && <strong>{displayName}</strong>}
{Object.entries(schema.properties).map(([propKey, propSchema]) => {
const childKey = selfKey ? `${selfKey}.${propKey}` : propKey;
@ -519,7 +518,6 @@ const NodeArrayInput: FC<{
typeof errors[selfKey] === "string" ? errors[selfKey] : undefined;
return (
<div className={cn(className, "flex flex-col")}>
{displayName && <strong>{displayName}</strong>}
{entries.map((entry: any, index: number) => {
const entryKey = `${selfKey}_$_${index}`;
const isConnected =

View File

@ -3,6 +3,7 @@ import { CustomNode } from "@/components/CustomNode";
import AutoGPTServerAPI, {
Block,
BlockIOSubSchema,
BlockUIType,
Graph,
Link,
NodeExecutionResult,
@ -18,6 +19,7 @@ import Ajv from "ajv";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
import { GraphMeta } from "@/lib/autogpt-server-api";
const ajv = new Ajv({ strict: false, allErrors: true });
@ -36,6 +38,7 @@ export default function useAgentGraph(
const [agentDescription, setAgentDescription] = useState<string>("");
const [agentName, setAgentName] = useState<string>("");
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
const [availableFlows, setAvailableFlows] = useState<GraphMeta[]>([]);
const [updateQueue, setUpdateQueue] = useState<NodeExecutionResult[]>([]);
const processedUpdates = useRef<NodeExecutionResult[]>([]);
/**
@ -93,12 +96,17 @@ export default function useAgentGraph(
};
}, [api]);
// Load available blocks
// Load available blocks & flows
useEffect(() => {
api
.getBlocks()
.then((blocks) => setAvailableNodes(blocks))
.catch();
api
.listGraphs()
.then((flows) => setAvailableFlows(flows))
.catch();
}, [api]);
//TODO to utils? repeated in Flow
@ -118,7 +126,7 @@ export default function useAgentGraph(
const outputSchema = node.data.outputSchema;
if (!outputSchema) return "unknown";
const outputHandle = outputSchema.properties[handleId];
const outputHandle = outputSchema.properties[handleId] || {};
if (!("type" in outputHandle)) return "unknown";
return outputHandle.type;
},
@ -137,6 +145,12 @@ export default function useAgentGraph(
const block = availableNodes.find(
(block) => block.id === node.block_id,
)!;
const flow =
block.uiType == BlockUIType.AGENT
? availableFlows.find(
(flow) => flow.id === node.input_default.graph_id,
)
: null;
const newNode: CustomNode = {
id: node.id,
type: "custom",
@ -146,7 +160,7 @@ export default function useAgentGraph(
},
data: {
block_id: block.id,
blockType: block.name,
blockType: flow?.name || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
@ -200,7 +214,7 @@ export default function useAgentGraph(
return newNodes;
});
},
[availableNodes, formatEdgeID, getOutputType],
[availableNodes, availableFlows, formatEdgeID, getOutputType],
);
const getFrontendId = useCallback(
@ -270,6 +284,7 @@ export default function useAgentGraph(
const updateNodesWithExecutionData = useCallback(
(executionData: NodeExecutionResult) => {
if (!executionData.node_id) return;
if (passDataToBeads) {
updateEdgeBeads(executionData);
}
@ -672,8 +687,8 @@ export default function useAgentGraph(
const payload = {
id: savedAgent?.id!,
name: agentName || "Agent Name",
description: agentDescription || "Agent Description",
name: agentName || `New Agent ${new Date().toISOString()}`,
description: agentDescription || "",
nodes: formattedNodes,
links: links,
};
@ -850,6 +865,7 @@ export default function useAgentGraph(
setAgentDescription,
savedAgent,
availableNodes,
availableFlows,
getOutputType,
requestSave,
requestSaveAndRun,

View File

@ -25,7 +25,9 @@ export type Block = {
outputSchema: BlockIORootSchema;
staticOutput: boolean;
uiType: BlockUIType;
uiKey?: string;
costs: BlockCost[];
hardcodedValues: { [key: string]: any } | null;
};
export type BlockIORootSchema = {
@ -190,6 +192,8 @@ export type GraphMeta = {
is_template: boolean;
name: string;
description: string;
input_schema: BlockIOObjectSubSchema;
output_schema: BlockIOObjectSubSchema;
};
export type GraphMetaWithRuns = GraphMeta & {
@ -204,12 +208,19 @@ export type Graph = GraphMeta & {
export type GraphUpdateable = Omit<
Graph,
"version" | "is_active" | "is_template" | "links"
| "version"
| "is_active"
| "is_template"
| "links"
| "input_schema"
| "output_schema"
> & {
version?: number;
is_active?: boolean;
is_template?: boolean;
links: Array<LinkCreatable>;
input_schema?: BlockIOObjectSubSchema;
output_schema?: BlockIOObjectSubSchema;
};
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string };
@ -299,6 +310,7 @@ export enum BlockUIType {
INPUT = "Input",
OUTPUT = "Output",
NOTE = "Note",
AGENT = "Agent",
}
export type AnalyticsMetrics = {

View File

@ -211,6 +211,7 @@ export const categoryColorMap: Record<string, string> = {
OUTPUT: "bg-red-300",
LOGIC: "bg-teal-300",
DEVELOPER_TOOLS: "bg-fuchsia-300",
AGENT: "bg-lime-300",
};
export function getPrimaryCategoryColor(categories: Category[]): string {