feat(platform): Introduced Agent Execution Block (#8533)
parent
5ee909f687
commit
1e872406ca
|
@ -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
|
|
@ -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=[
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]):
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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)}`}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue