diff --git a/rnd/autogpt_builder/.env.example b/rnd/autogpt_builder/.env.example new file mode 100644 index 0000000000..a70a8847a2 --- /dev/null +++ b/rnd/autogpt_builder/.env.example @@ -0,0 +1 @@ +AGPT_SERVER_URL=http://localhost:8000 \ No newline at end of file diff --git a/rnd/autogpt_builder/package.json b/rnd/autogpt_builder/package.json index 67b793bbad..941e6ca669 100644 --- a/rnd/autogpt_builder/package.json +++ b/rnd/autogpt_builder/package.json @@ -9,18 +9,21 @@ "lint": "next lint" }, "dependencies": { + "next": "14.2.4", "react": "^18", "react-dom": "^18", - "next": "14.2.4" + "react-modal": "^3.16.1", + "reactflow": "^11.11.4" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-modal": "^3.16.3", + "eslint": "^8", + "eslint-config-next": "14.2.4", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.4" + "typescript": "^5" } } diff --git a/rnd/autogpt_builder/src/app/page.tsx b/rnd/autogpt_builder/src/app/page.tsx index 916ce244e8..63cefb866f 100644 --- a/rnd/autogpt_builder/src/app/page.tsx +++ b/rnd/autogpt_builder/src/app/page.tsx @@ -1,97 +1,40 @@ import Image from "next/image"; +import Flow from '../components/Flow'; export default function Home() { return ( -
-
-

- Get started by adding a  - node -

-
- - By{" "} - AutoGPT Logo - -
-
+
+
+

+ Get started by adding a  + node +

+
+ + By{" "} + AutoGPT Logo + +
+
-
- AutoGPT Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find all the information about AutoGPT's features. -

-
- - - -

- Runner{" "} - - -> - -

-

- Run your agent! -

-
- - -

- Deploy{" "} - - -> - -

-

- Deploy your agent! -

-
-
-
+
+
+ +
+
+
); } diff --git a/rnd/autogpt_builder/src/components/CustomNode.tsx b/rnd/autogpt_builder/src/components/CustomNode.tsx new file mode 100644 index 0000000000..522cd78635 --- /dev/null +++ b/rnd/autogpt_builder/src/components/CustomNode.tsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect, FC, memo } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; +import 'reactflow/dist/style.css'; + +type Schema = { + properties: { [key: string]: any }; +}; + +const CustomNode: FC = ({ data }) => { + const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false); + + // Automatically open properties when output_data or status is updated + useEffect(() => { + if (data.output_data || data.status) { + setIsPropertiesOpen(true); + } + }, [data.output_data, data.status]); + + const toggleProperties = () => { + setIsPropertiesOpen(!isPropertiesOpen); + }; + + const generateHandles = (schema: Schema, type: 'source' | 'target') => { + if (!schema?.properties) return null; + const keys = Object.keys(schema.properties); + return keys.map((key) => ( +
+ {type === 'target' && ( + <> + + {key} + + )} + {type === 'source' && ( + <> + {key} + + + )} +
+ )); + }; + + const handleInputChange = (key: string, value: any) => { + const newValues = { ...data.hardcodedValues, [key]: value }; + data.setHardcodedValues(newValues); + }; + + const isHandleConnected = (key: string) => { + return data.connections.some((conn: string) => { + const [, target] = conn.split(' -> '); + return target.includes(key) && target.includes(data.title); + }); + }; + + const hasDisconnectedHandle = (key: string) => { + return !isHandleConnected(key); + }; + + return ( +
+
+
+ {data?.title.replace(/\d+/g, '')} +
+ +
+
+
+ {data.inputSchema && generateHandles(data.inputSchema, 'target')} + {data.inputSchema && Object.keys(data.inputSchema.properties).map(key => ( + hasDisconnectedHandle(key) && ( +
+ handleInputChange(key, e.target.value)} + style={{ width: '100%', padding: '5px', borderRadius: '4px', border: '1px solid #555', background: '#444', color: '#e0e0e0' }} + /> +
+ ) + ))} +
+
+ {data.outputSchema && generateHandles(data.outputSchema, 'source')} +
+
+ {isPropertiesOpen && ( +
+

Node Output

+

Status: {typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}

+

Output Data: {typeof data.output_data === 'object' ? JSON.stringify(data.output_data) : data.output_data || 'N/A'}

+
+ )} +
+ ); +}; + +export default memo(CustomNode); diff --git a/rnd/autogpt_builder/src/components/Flow.tsx b/rnd/autogpt_builder/src/components/Flow.tsx new file mode 100644 index 0000000000..8a0191b3b1 --- /dev/null +++ b/rnd/autogpt_builder/src/components/Flow.tsx @@ -0,0 +1,590 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from 'react'; +import ReactFlow, { + addEdge, + applyNodeChanges, + applyEdgeChanges, + Node, + Edge, + OnNodesChange, + OnEdgesChange, + OnConnect, + NodeTypes, + EdgeRemoveChange, +} from 'reactflow'; +import 'reactflow/dist/style.css'; +import Modal from 'react-modal'; +import CustomNode from './CustomNode'; +import './flow.css'; + +const initialNodes: Node[] = []; +const initialEdges: Edge[] = []; +const nodeTypes: NodeTypes = { + custom: CustomNode, +}; + +interface AvailableNode { + id: string; + name: string; + description: string; + inputSchema?: { properties: { [key: string]: any }; required?: string[] }; + outputSchema?: { properties: { [key: string]: any } }; +} + +interface ExecData { + node_id: string; + status: string; + output_data: any; +} + +const Flow: React.FC = () => { + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const [nodeId, setNodeId] = useState(1); + const [modalIsOpen, setModalIsOpen] = useState(false); + const [selectedNode, setSelectedNode] = useState(null); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [variableName, setVariableName] = useState(''); + const [variableValue, setVariableValue] = useState(''); + const [printVariable, setPrintVariable] = useState(''); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [availableNodes, setAvailableNodes] = useState([]); + const [loadingStatus, setLoadingStatus] = useState<'loading' | 'failed' | 'loaded'>('loading'); + const [agentId, setAgentId] = useState(null); + + const apiUrl = 'http://localhost:8000' + + useEffect(() => { + fetch(`${apiUrl}/blocks`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + setAvailableNodes(data.map((node: AvailableNode) => ({ + ...node, + description: typeof node.description === 'object' ? JSON.stringify(node.description) : node.description, + }))); + setLoadingStatus('loaded'); + }) + .catch(error => { + console.error('Error fetching nodes:', error); + setLoadingStatus('failed'); + }); + }, []); + + const onNodesChange: OnNodesChange = useCallback( + (changes) => setNodes((nds) => applyNodeChanges(changes, nds).map(node => ({ + ...node, + data: { + ...node.data, + metadata: { + ...node.data.metadata, + position: node.position + } + } + }))), + [] + ); + + const onEdgesChange: OnEdgesChange = useCallback( + (changes) => { + const removedEdges = changes.filter((change): change is EdgeRemoveChange => change.type === 'remove'); + setEdges((eds) => applyEdgeChanges(changes, eds)); + + if (removedEdges.length > 0) { + setNodes((nds) => + nds.map((node) => { + const updatedConnections = node.data.connections.filter( + (conn: string) => + !removedEdges.some((edge) => edge.id && conn.includes(edge.id)) + ); + return { ...node, data: { ...node.data, connections: updatedConnections } }; + }) + ); + } + }, + [] + ); + + const onConnect: OnConnect = useCallback( + (connection) => { + setEdges((eds) => addEdge(connection, eds)); + setNodes((nds) => + nds.map((node) => { + if (node.id === connection.source) { + const connections = node.data.connections || []; + connections.push(`${node.data.title} ${connection.sourceHandle} -> ${connection.targetHandle}`); + return { ...node, data: { ...node.data, connections } }; + } + if (node.id === connection.target) { + const connections = node.data.connections || []; + connections.push(`${connection.sourceHandle} -> ${node.data.title} ${connection.targetHandle}`); + return { ...node, data: { ...node.data, connections } }; + } + return node; + }) + ); + }, + [setEdges, setNodes] + ); + + const addNode = (type: string, label: string, description: string) => { + const nodeSchema = availableNodes.find(node => node.name === label); + const position = { x: Math.random() * 400, y: Math.random() * 400 }; + + const newNode: Node = { + id: nodeId.toString(), + type: 'custom', + data: { + label: label, + title: `${type} ${nodeId}`, + description: `${description}`, + inputSchema: nodeSchema?.inputSchema, + outputSchema: nodeSchema?.outputSchema, + connections: [], + variableName: '', + variableValue: '', + printVariable: '', + setVariableName, + setVariableValue, + setPrintVariable, + hardcodedValues: {}, + setHardcodedValues: (values: { [key: string]: any }) => { + setNodes((nds) => nds.map((node) => + node.id === nodeId.toString() + ? { ...node, data: { ...node.data, hardcodedValues: values } } + : node + )); + }, + block_id: nodeSchema?.id || '', + metadata: { + position // Store position in metadata + } + }, + position, + }; + setNodes((nds) => [...nds, newNode]); + setNodeId((id) => id + 1); + }; + + const closeModal = () => { + setModalIsOpen(false); + setSelectedNode(null); + }; + + const saveNodeData = () => { + if (selectedNode) { + setNodes((nds) => + nds.map((node) => + node.id === selectedNode.id + ? { + ...node, + data: { + ...node.data, + title, + description, + label: title, + variableName, + variableValue: typeof variableValue === 'object' ? JSON.stringify(variableValue) : variableValue, + printVariable: typeof printVariable === 'object' ? JSON.stringify(printVariable) : printVariable, + }, + } + : node + ) + ); + closeModal(); + } + }; + + const toggleSidebar = () => { + setIsSidebarOpen(!isSidebarOpen); + }; + + const filteredNodes = availableNodes.filter(node => node.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const prepareNodeInputData = (node: Node, allNodes: Node[], allEdges: Edge[]) => { + const nodeSchema = availableNodes.find(n => n.id === node.data.block_id); + if (!nodeSchema || !nodeSchema.inputSchema) return {}; + + let inputData: { [key: string]: any } = {}; + const inputProperties = nodeSchema.inputSchema.properties; + const requiredProperties = nodeSchema.inputSchema.required || []; + + // Initialize inputData with default values for all required properties + requiredProperties.forEach(prop => { + inputData[prop] = node.data.hardcodedValues[prop] || ''; + }); + + Object.keys(inputProperties).forEach(prop => { + const inputEdge = allEdges.find(edge => edge.target === node.id && edge.targetHandle === prop); + if (inputEdge) { + const sourceNode = allNodes.find(n => n.id === inputEdge.source); + inputData[prop] = sourceNode?.data.output_data || sourceNode?.data.hardcodedValues[prop] || ''; + } else if (node.data.hardcodedValues && node.data.hardcodedValues[prop]) { + inputData[prop] = node.data.hardcodedValues[prop]; + } + }); + + return inputData; + }; + + const updateNodeData = (execData: ExecData) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === execData.node_id) { + return { + ...node, + data: { + ...node.data, + status: execData.status, + output_data: execData.output_data, + isPropertiesOpen: true, // Open the properties + }, + }; + } + return node; + }) + ); + }; + + const runAgent = async () => { + try { + const formattedNodes = nodes.map(node => ({ + id: node.id, + block_id: node.data.block_id, + input_default: prepareNodeInputData(node, nodes, edges), + input_nodes: edges.filter(edge => edge.target === node.id).reduce((acc, edge) => { + if (edge.targetHandle) { + acc[edge.targetHandle] = edge.source; + } + return acc; + }, {} as { [key: string]: string }), + output_nodes: edges.filter(edge => edge.source === node.id).reduce((acc, edge) => { + if (edge.sourceHandle) { + acc[edge.sourceHandle] = edge.target; + } + return acc; + }, {} as { [key: string]: string }), + metadata: node.data.metadata, + connections: node.data.connections // Ensure connections are preserved + })); + + const payload = { + id: '', + name: 'Agent Name', + description: 'Agent Description', + nodes: formattedNodes, + }; + + const createResponse = await fetch(`${apiUrl}/agents`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!createResponse.ok) { + throw new Error(`HTTP error! Status: ${createResponse.status}`); + } + + const createData = await createResponse.json(); + const agentId = createData.id; + setAgentId(agentId); + + const responseNodes = createData.nodes.map((node: any) => { + const block = availableNodes.find(n => n.id === node.block_id); + const connections = edges.filter(edge => edge.source === node.id || edge.target === node.id).map(edge => ({ + id: edge.id, + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle + })); + return { + id: node.id, + type: 'custom', + position: node.metadata.position, + data: { + label: block?.name || 'Unknown', + title: `${block?.name || 'Unknown'}`, + description: `${block?.description || ''}`, + inputSchema: block?.inputSchema, + outputSchema: block?.outputSchema, + connections: connections.map(c => `${c.source}-${c.sourceHandle} -> ${c.target}-${c.targetHandle}`), + variableName: '', + variableValue: '', + printVariable: '', + setVariableName, + setVariableValue, + setPrintVariable, + hardcodedValues: node.input_default, + setHardcodedValues: (values: { [key: string]: any }) => { + setNodes((nds) => nds.map((n) => + n.id === node.id + ? { ...n, data: { ...n.data, hardcodedValues: values } } + : n + )); + }, + block_id: node.block_id, + metadata: node.metadata + }, + }; + }); + + const newEdges = createData.nodes.flatMap((node: any) => { + return Object.entries(node.output_nodes).map(([sourceHandle, targetNodeId]) => ({ + id: `${node.id}-${sourceHandle}-${targetNodeId}`, + source: node.id, + sourceHandle: sourceHandle, + target: targetNodeId, + targetHandle: Object.keys(node.input_nodes).find(key => node.input_nodes[key] === targetNodeId) || '', + })); + }); + + setNodes(responseNodes); + setEdges(newEdges); + + const initialNodeInput = nodes.reduce((acc, node) => { + acc[node.id] = prepareNodeInputData(node, nodes, edges); + return acc; + }, {} as { [key: string]: any }); + + const nodeInputForExecution = Object.keys(initialNodeInput).reduce((acc, key) => { + const blockId = nodes.find(node => node.id === key)?.data.block_id; + const nodeSchema = availableNodes.find(n => n.id === blockId); + if (nodeSchema && nodeSchema.inputSchema) { + Object.keys(nodeSchema.inputSchema.properties).forEach(prop => { + acc[prop] = initialNodeInput[key][prop]; + }); + } + return acc; + }, {} as { [key: string]: any }); + + const executeResponse = await fetch(`${apiUrl}/agents/${agentId}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(nodeInputForExecution), + }); + + if (!executeResponse.ok) { + throw new Error(`HTTP error! Status: ${executeResponse.status}`); + } + + const executeData = await executeResponse.json(); + const runId = executeData.run_id; + + const startPolling = () => { + const endTime = Date.now() + 60000; + + const poll = async () => { + if (Date.now() >= endTime) { + console.log('Polling timeout reached.'); + return; + } + + try { + const response = await fetch(`${apiUrl}/agents/${agentId}/executions/${runId}`); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + data.forEach(updateNodeData); + + const allCompleted = data.every((exec: any) => exec.status === 'COMPLETED'); + if (allCompleted) { + console.log('All nodes are completed.'); + return; + } + + setTimeout(poll, 100); + } catch (error) { + console.error('Error during polling:', error); + setTimeout(poll, 100); + } + }; + + poll(); + }; + + startPolling(); + } catch (error) { + console.error('Error running agent:', error); + } + }; + + return ( +
+
+ + + {agentId && ( + + Agent ID: {agentId} + + )} +
+
+ +
+ {selectedNode && ( + +

Edit Node

+
{ + e.preventDefault(); + saveNodeData(); + }} + > +
+ +
+
+