feat(autogpt_builder) Update custom node to handle deeply nested structures (#7319)

* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* Fix reddit block

* Fix serialization

* Eliminate deprecated class property

* Remove RedditCredentialsBlock

* Cache jsonschema computation, add dictionary construction

* Add dict_split and list_split to output, add more blocks

* Add objc_split for completeness, int both input and output

* Update reddit block

* Add reddit test (untested)

* Resolved json issue on pydantic

* Add creds check on client

* Add dict <--> pydantic object flexibility

* Fix error retry

* Skip reddit test

* Code cleanup

* Chang prompt

* Make this work

* Fix linting

* Hide input_links and output_links from Node

* Add docs

* updating UI to handle deeply nested data structures for reddit usecase

* changing expected key in reddit post to comment

---------

Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
pull/7328/head
Aarushi 2024-07-04 17:54:41 +01:00 committed by GitHub
parent 833944e228
commit 6456285753
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 393 additions and 277 deletions

View File

@ -8,6 +8,12 @@ type Schema = {
type: string; type: string;
properties: { [key: string]: any }; properties: { [key: string]: any };
required?: string[]; required?: string[];
enum?: string[];
items?: Schema;
additionalProperties?: { type: string };
allOf?: any[];
anyOf?: any[];
oneOf?: any[];
}; };
type CustomNodeData = { type CustomNodeData = {
@ -25,6 +31,9 @@ type CustomNodeData = {
const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => { const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false); const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
const [keyValuePairs, setKeyValuePairs] = useState<{ key: string, value: string }[]>([]);
const [newKey, setNewKey] = useState<string>('');
const [newValue, setNewValue] = useState<string>('');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null); const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>(''); const [modalValue, setModalValue] = useState<string>('');
@ -76,41 +85,24 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
}; };
const handleInputChange = (key: string, value: any) => { const handleInputChange = (key: string, value: any) => {
const newValues = { ...data.hardcodedValues, [key]: value }; const keys = key.split('.');
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
let current = newValues;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
console.log(`Updating hardcoded values for node ${id}:`, newValues); console.log(`Updating hardcoded values for node ${id}:`, newValues);
data.setHardcodedValues(newValues); data.setHardcodedValues(newValues);
setErrors((prevErrors) => ({ ...prevErrors, [key]: null })); setErrors((prevErrors) => ({ ...prevErrors, [key]: null }));
}; };
const validateInput = (key: string, value: any, schema: any) => { const getValue = (key: string) => {
switch (schema.type) { const keys = key.split('.');
case 'string': return keys.reduce((acc, k) => (acc && acc[k] !== undefined) ? acc[k] : '', data.hardcodedValues);
if (schema.enum && !schema.enum.includes(value)) {
return `Invalid value for ${key}`;
}
break;
case 'boolean':
if (typeof value !== 'boolean') {
return `Invalid value for ${key}`;
}
break;
case 'number':
if (typeof value !== 'number') {
return `Invalid value for ${key}`;
}
break;
case 'array':
if (!Array.isArray(value) || value.some((item: any) => typeof item !== 'string')) {
return `Invalid value for ${key}`;
}
if (schema.minItems && value.length < schema.minItems) {
return `${key} requires at least ${schema.minItems} items`;
}
break;
default:
return null;
}
return null;
}; };
const isHandleConnected = (key: string) => { const isHandleConnected = (key: string) => {
@ -123,66 +115,180 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
}); });
}; };
const handleAddProperty = () => {
if (newKey && newValue) {
const newPairs = [...keyValuePairs, { key: newKey, value: newValue }];
setKeyValuePairs(newPairs);
setNewKey('');
setNewValue('');
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}
};
const handleInputClick = (key: string) => { const handleInputClick = (key: string) => {
setActiveKey(key); setActiveKey(key);
setModalValue(data.hardcodedValues[key] || ''); const value = getValue(key);
setModalValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
setIsModalOpen(true); setIsModalOpen(true);
}; };
const handleModalSave = (value: string) => { const handleModalSave = (value: string) => {
if (activeKey) { if (activeKey) {
handleInputChange(activeKey, value); try {
const parsedValue = JSON.parse(value);
handleInputChange(activeKey, parsedValue);
} catch (error) {
handleInputChange(activeKey, value);
}
} }
setIsModalOpen(false); setIsModalOpen(false);
setActiveKey(null); setActiveKey(null);
}; };
const addArrayItem = (key: string) => { const renderInputField = (key: string, schema: any, parentKey: string = ''): JSX.Element => {
const currentValues = data.hardcodedValues[key] || []; const fullKey = parentKey ? `${parentKey}.${key}` : key;
handleInputChange(key, [...currentValues, '']); const error = errors[fullKey];
}; const value = getValue(fullKey);
const removeArrayItem = (key: string, index: number) => { if (isHandleConnected(fullKey)) {
const currentValues = data.hardcodedValues[key] || []; return <div className="connected-input">Connected</div>;
currentValues.splice(index, 1); }
handleInputChange(key, [...currentValues]);
};
const handleArrayItemChange = (key: string, index: number, value: string) => { const renderClickableInput = (displayValue: string) => (
const currentValues = data.hardcodedValues[key] || []; <div className="clickable-input" onClick={() => handleInputClick(fullKey)}>
currentValues[index] = value; {displayValue}
handleInputChange(key, [...currentValues]); </div>
}; );
const addDynamicTextInput = () => { if (schema.type === 'object' && schema.properties) {
const dynamicKeyPrefix = 'texts_$_'; return (
const currentKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix)); <div key={fullKey} className="object-input">
const nextIndex = currentKeys.length + 1; <strong>{key}:</strong>
const newKey = `${dynamicKeyPrefix}${nextIndex}`; {Object.entries(schema.properties).map(([propKey, propSchema]: [string, any]) => (
handleInputChange(newKey, ''); <div key={`${fullKey}.${propKey}`} className="nested-input">
}; {renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
const removeDynamicTextInput = (key: string) => { if (schema.type === 'object' && schema.additionalProperties) {
const newValues = { ...data.hardcodedValues }; const objectValue = value || {};
delete newValues[key]; return (
data.setHardcodedValues(newValues); <div key={fullKey} className="object-input">
}; <strong>{key}:</strong>
{Object.entries(objectValue).map(([propKey, propValue]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<div className="clickable-input" onClick={() => handleInputClick(`${fullKey}.${propKey}`)}>
{propKey}: {typeof propValue === 'object' ? JSON.stringify(propValue, null, 2) : propValue}
</div>
<button onClick={() => handleInputChange(`${fullKey}.${propKey}`, undefined)} className="array-item-remove">
&times;
</button>
</div>
))}
{key === 'expected_format' && (
<div className="nested-input">
{keyValuePairs.map((pair, index) => (
<div key={index} className="key-value-input">
<input
type="text"
placeholder="Key"
value={pair.key}
onChange={(e) => {
const newPairs = [...keyValuePairs];
newPairs[index].key = e.target.value;
setKeyValuePairs(newPairs);
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}}
/>
<input
type="text"
placeholder="Value"
value={pair.value}
onChange={(e) => {
const newPairs = [...keyValuePairs];
newPairs[index].value = e.target.value;
setKeyValuePairs(newPairs);
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}}
/>
</div>
))}
<div className="key-value-input">
<input
type="text"
placeholder="Key"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
/>
<input
type="text"
placeholder="Value"
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
/>
</div>
<button onClick={handleAddProperty}>Add Property</button>
</div>
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
const handleDynamicTextInputChange = (key: string, value: string) => { if (schema.anyOf) {
handleInputChange(key, value); const types = schema.anyOf.map((s: any) => s.type);
}; if (types.includes('string') && types.includes('null')) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value || `Enter ${key} (optional)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if (schema.allOf) {
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{schema.allOf[0].properties && Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
{renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
if (schema.oneOf) {
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{schema.oneOf[0].properties && Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
{renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
const renderInputField = (key: string, schema: any) => {
const error = errors[key];
switch (schema.type) { switch (schema.type) {
case 'string': case 'string':
return schema.enum ? ( return schema.enum ? (
<div key={key} className="input-container"> <div key={fullKey} className="input-container">
<select <select
value={data.hardcodedValues[key] || ''} value={value || ''}
onChange={(e) => handleInputChange(key, e.target.value)} onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input" className="select-input"
> >
<option value="">Select {key}</option>
{schema.enum.map((option: string) => ( {schema.enum.map((option: string) => (
<option key={option} value={option}> <option key={option} value={option}>
{option} {option}
@ -192,44 +298,34 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
{error && <span className="error-message">{error}</span>} {error && <span className="error-message">{error}</span>}
</div> </div>
) : ( ) : (
<div key={key} className="input-container"> <div key={fullKey} className="input-container">
<div className="clickable-input" onClick={() => handleInputClick(key)}> {renderClickableInput(value || `Enter ${key}`)}
{data.hardcodedValues[key] || `Enter ${key}`}
</div>
{error && <span className="error-message">{error}</span>} {error && <span className="error-message">{error}</span>}
</div> </div>
); );
case 'boolean': case 'boolean':
return ( return (
<div key={key} className="input-container"> <div key={fullKey} className="input-container">
<label className="radio-label"> <select
<input value={value === undefined ? '' : value.toString()}
type="radio" onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
value="true" className="select-input"
checked={data.hardcodedValues[key] === true} >
onChange={() => handleInputChange(key, true)} <option value="">Select {key}</option>
/> <option value="true">True</option>
True <option value="false">False</option>
</label> </select>
<label className="radio-label">
<input
type="radio"
value="false"
checked={data.hardcodedValues[key] === false}
onChange={() => handleInputChange(key, false)}
/>
False
</label>
{error && <span className="error-message">{error}</span>} {error && <span className="error-message">{error}</span>}
</div> </div>
); );
case 'number': case 'number':
case 'integer':
return ( return (
<div key={key} className="input-container"> <div key={fullKey} className="input-container">
<input <input
type="number" type="number"
value={data.hardcodedValues[key] || ''} value={value || ''}
onChange={(e) => handleInputChange(key, parseFloat(e.target.value))} onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
className="number-input" className="number-input"
/> />
{error && <span className="error-message">{error}</span>} {error && <span className="error-message">{error}</span>}
@ -237,23 +333,23 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
); );
case 'array': case 'array':
if (schema.items && schema.items.type === 'string') { if (schema.items && schema.items.type === 'string') {
const arrayValues = data.hardcodedValues[key] || []; const arrayValues = value || [];
return ( return (
<div key={key} className="input-container"> <div key={fullKey} className="input-container">
{arrayValues.map((item: string, index: number) => ( {arrayValues.map((item: string, index: number) => (
<div key={`${key}-${index}`} className="array-item-container"> <div key={`${fullKey}.${index}`} className="array-item-container">
<input <input
type="text" type="text"
value={item} value={item}
onChange={(e) => handleArrayItemChange(key, index, e.target.value)} onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
className="array-item-input" className="array-item-input"
/> />
<button onClick={() => removeArrayItem(key, index)} className="array-item-remove"> <button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
&times; &times;
</button> </button>
</div> </div>
))} ))}
<button onClick={() => addArrayItem(key)} className="array-item-add"> <button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
Add Item Add Item
</button> </button>
{error && <span className="error-message">{error}</span>} {error && <span className="error-message">{error}</span>}
@ -262,52 +358,33 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
} }
return null; return null;
default: default:
return null; return (
<div key={fullKey} className="input-container">
{renderClickableInput(value ? `${key} (Complex)` : `Enter ${key} (Complex)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
} }
}; };
const renderDynamicTextFields = () => {
const dynamicKeyPrefix = 'texts_$_';
const dynamicKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix));
return dynamicKeys.map((key, index) => (
<div key={key} className="input-container">
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span className="handle-label">{key}</span>
{!isHandleConnected(key) && (
<>
<input
type="text"
value={data.hardcodedValues[key]}
onChange={(e) => handleDynamicTextInputChange(key, e.target.value)}
className="dynamic-text-input"
/>
<button onClick={() => removeDynamicTextInput(key)} className="array-item-remove">
&times;
</button>
</>
)}
</div>
</div>
));
};
const validateInputs = () => { const validateInputs = () => {
const newErrors: { [key: string]: string | null } = {}; const newErrors: { [key: string]: string | null } = {};
Object.keys(data.inputSchema.properties).forEach((key) => { const validateRecursive = (schema: any, parentKey: string = '') => {
const value = data.hardcodedValues[key]; Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
const schema = data.inputSchema.properties[key]; const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = validateInput(key, value, schema); const value = getValue(fullKey);
if (error) {
newErrors[key] = error; if (propSchema.type === 'object' && propSchema.properties) {
} validateRecursive(propSchema, fullKey);
}); } else {
if (propSchema.required && !value) {
newErrors[fullKey] = `${fullKey} is required`;
}
}
});
};
validateRecursive(data.inputSchema);
setErrors(newErrors); setErrors(newErrors);
return Object.values(newErrors).every((error) => error === null); return Object.values(newErrors).every((error) => error === null);
}; };
@ -320,9 +397,8 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
} }
}; };
return ( return (
<div className="custom-node"> <div className={`custom-node ${data.status === 'RUNNING' ? 'running' : data.status === 'COMPLETED' ? 'completed' : ''}`}>
<div className="node-header"> <div className="node-header">
<div className="node-title">{data.blockType || data.title}</div> <div className="node-title">{data.blockType || data.title}</div>
<button onClick={toggleProperties} className="toggle-button"> <button onClick={toggleProperties} className="toggle-button">
@ -332,38 +408,18 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
<div className="node-content"> <div className="node-content">
<div className="input-section"> <div className="input-section">
{data.inputSchema && {data.inputSchema &&
Object.keys(data.inputSchema.properties).map((key) => ( Object.entries(data.inputSchema.properties).map(([key, schema]) => (
<div key={key}> <div key={key}>
{key !== 'texts' ? ( <div className="handle-container">
<div> <Handle
<div className="handle-container"> type="target"
<Handle position={Position.Left}
type="target" id={key}
position={Position.Left} style={{ background: '#555', borderRadius: '50%' }}
id={key} />
style={{ background: '#555', borderRadius: '50%' }} <span className="handle-label">{key}</span>
/> </div>
<span className="handle-label">{key}</span> {renderInputField(key, schema)}
</div>
{!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])}
</div>
) : (
<div key={key} className="input-container">
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span className="handle-label">{key}</span>
</div>
{renderDynamicTextFields()}
<button onClick={addDynamicTextInput} className="array-item-add">
Add Text Input
</button>
</div>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -19,6 +19,7 @@ import './flow.css';
type Schema = { type Schema = {
type: string; type: string;
properties: { [key: string]: any }; properties: { [key: string]: any };
additionalProperties?: { type: string };
required?: string[]; required?: string[];
}; };
@ -44,12 +45,6 @@ type AvailableNode = {
outputSchema: Schema; outputSchema: Schema;
}; };
interface ExecData {
node_id: string;
status: string;
output_data: any;
}
const Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNode: (id: string, name: string) => void}> = const Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNode: (id: string, name: string) => void}> =
({isOpen, availableNodes, addNode}) => { ({isOpen, availableNodes, addNode}) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@ -61,28 +56,17 @@ const Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNo
); );
return ( return (
<div style={{ <div className={`sidebar ${isOpen ? 'open' : ''}`}>
position: 'absolute', <h3>Nodes</h3>
left: 0,
top: 0,
bottom: 0,
width: '250px',
backgroundColor: '#333',
padding: '20px',
zIndex: 4,
overflowY: 'auto'
}}>
<h3 style={{color: '#fff'}}>Nodes</h3>
<input <input
type="text" type="text"
placeholder="Search nodes..." placeholder="Search nodes..."
style={{width: '100%', marginBottom: '10px', padding: '5px'}}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
{filteredNodes.map((node) => ( {filteredNodes.map((node) => (
<div key={node.id} style={{marginBottom: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}> <div key={node.id} className="sidebarNodeRowStyle">
<span style={{color: '#fff'}}>{node.name}</span> <span>{node.name}</span>
<button onClick={() => addNode(node.id, node.name)}>Add</button> <button onClick={() => addNode(node.id, node.name)}>Add</button>
</div> </div>
))} ))}
@ -183,38 +167,61 @@ const Flow: React.FC = () => {
}; };
const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge[]) => { const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge[]) => {
console.log("Preparing input data for node:", node.id, node.data.blockType); console.log("Preparing input data for node:", node.id, node.data.blockType);
const blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema; const blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema;
if (!blockSchema) { if (!blockSchema) {
console.error(`Schema not found for block ID: ${node.data.block_id}`); console.error(`Schema not found for block ID: ${node.data.block_id}`);
return {}; return {};
}
const getNestedData = (schema: Schema, values: { [key: string]: any }): { [key: string]: any } => {
let inputData: { [key: string]: any } = {};
if (schema.properties) {
Object.keys(schema.properties).forEach((key) => {
if (values[key] !== undefined) {
if (schema.properties[key].type === 'object') {
inputData[key] = getNestedData(schema.properties[key], values[key]);
} else {
inputData[key] = values[key];
}
}
});
} }
let inputData: { [key: string]: any } = { ...node.data.hardcodedValues }; if (schema.additionalProperties) {
inputData = { ...inputData, ...values };
}
// Get data from connected nodes
const incomingEdges = allEdges.filter(edge => edge.target === node.id);
incomingEdges.forEach(edge => {
const sourceNode = allNodes.find(n => n.id === edge.source);
if (sourceNode && sourceNode.data.output_data) {
const outputKey = Object.keys(sourceNode.data.output_data)[0]; // Assuming single output
inputData[edge.targetHandle as string] = sourceNode.data.output_data[outputKey];
}
});
// Filter out any inputs that are not in the block's schema
Object.keys(inputData).forEach(key => {
if (!blockSchema.properties[key]) {
delete inputData[key];
}
});
console.log(`Final prepared input for ${node.data.blockType} (${node.id}):`, inputData);
return inputData; return inputData;
}; };
let inputData = getNestedData(blockSchema, node.data.hardcodedValues);
// Get data from connected nodes
const incomingEdges = allEdges.filter(edge => edge.target === node.id);
incomingEdges.forEach(edge => {
const sourceNode = allNodes.find(n => n.id === edge.source);
if (sourceNode && sourceNode.data.output_data) {
const outputKey = Object.keys(sourceNode.data.output_data)[0]; // Assuming single output
inputData[edge.targetHandle as string] = sourceNode.data.output_data[outputKey];
}
});
// Filter out any inputs that are not in the block's schema
Object.keys(inputData).forEach(key => {
if (!blockSchema.properties[key]) {
delete inputData[key];
}
});
console.log(`Final prepared input for ${node.data.blockType} (${node.id}):`, inputData);
return inputData;
};
const runAgent = async () => { const runAgent = async () => {
try { try {
console.log("All nodes before formatting:", nodes); console.log("All nodes before formatting:", nodes);
@ -246,11 +253,19 @@ const Flow: React.FC = () => {
}; };
}); });
const links = edges.map(edge => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || '',
sink_name: edge.targetHandle || ''
}));
const payload = { const payload = {
id: agentId || '', id: agentId || '',
name: 'Agent Name', name: 'Agent Name',
description: 'Agent Description', description: 'Agent Description',
nodes: formattedNodes, nodes: formattedNodes,
links: links // Ensure this field is included
}; };
console.log("Payload being sent to the API:", JSON.stringify(payload, null, 2)); console.log("Payload being sent to the API:", JSON.stringify(payload, null, 2));
@ -268,14 +283,12 @@ const Flow: React.FC = () => {
} }
const createData = await createResponse.json(); const createData = await createResponse.json();
const newAgentId = createData.id; const newAgentId = createData.id;
setAgentId(newAgentId); setAgentId(newAgentId);
console.log('Response from the API:', JSON.stringify(createData, null, 2)); console.log('Response from the API:', JSON.stringify(createData, null, 2));
const executeResponse = await fetch(`${apiUrl}/graphs/${newAgentId}/execute`, { const executeResponse = await fetch(`${apiUrl}/graphs/${newAgentId}/execute`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -290,14 +303,15 @@ const Flow: React.FC = () => {
const executeData = await executeResponse.json(); const executeData = await executeResponse.json();
const runId = executeData.id; const runId = executeData.id;
const pollExecution = async () => { const pollExecution = async () => {
const response = await fetch(`${apiUrl}/graphs/${newAgentId}/executions/${runId}`); const response = await fetch(`${apiUrl}/graphs/${newAgentId}/executions/${runId}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
data.forEach(updateNodeData); updateNodesWithExecutionData(data);
if (data.every((node: any) => node.status === 'COMPLETED')) { if (data.every((node: any) => node.status === 'COMPLETED')) {
console.log('All nodes completed execution'); console.log('All nodes completed execution');
} else { } else {
@ -312,47 +326,29 @@ const Flow: React.FC = () => {
} }
}; };
const updateNodesWithExecutionData = (executionData: any[]) => {
setNodes((nds) => const updateNodesWithExecutionData = (executionData: any[]) => {
nds.map((node) => { setNodes((nds) =>
const nodeExecution = executionData.find((exec) => exec.node_id === node.id); nds.map((node) => {
if (nodeExecution) { const nodeExecution = executionData.find((exec) => exec.node_id === node.id);
return { if (nodeExecution) {
...node, return {
data: { ...node,
...node.data, data: {
status: nodeExecution.status, ...node.data,
output_data: nodeExecution.output_data, status: nodeExecution.status,
isPropertiesOpen: true, output_data: nodeExecution.output_data,
}, isPropertiesOpen: true,
}; },
} };
return node; }
}) return node;
); })
}; );
};
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
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;
})
);
};
return ( return (
<div style={{ height: '100vh', width: '100%' }}> <div style={{ height: '100vh', width: '100%' }}>
<button <button

View File

@ -1,10 +1,11 @@
.custom-node { .custom-node {
padding: 20px; padding: 15px;
border: 2px solid #fff; border: 2px solid #fff;
border-radius: 20px; border-radius: 12px;
background: #333; background: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
width: 250px; width: 300px; /* Adjust width for better layout */
box-sizing: border-box; /* Ensure padding doesn't affect overall width */
} }
.node-header { .node-header {
@ -28,9 +29,8 @@
.node-content { .node-content {
display: flex; display: flex;
flex-direction: row; flex-direction: column; /* Use column to stack elements */
justify-content: space-between; gap: 10px;
gap: 20px;
} }
.input-section { .input-section {
@ -137,3 +137,49 @@
font-size: 12px; font-size: 12px;
margin-top: 5px; margin-top: 5px;
} }
.object-input {
margin-left: 10px;
border-left: 1px solid #555;
padding-left: 10px;
}
.nested-input {
margin-top: 5px;
}
.key-value-input {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
}
.key-value-input input {
flex-grow: 1;
}
@keyframes runningAnimation {
0% { background-color: #f39c12; }
50% { background-color: #e67e22; }
100% { background-color: #f39c12; }
}
.running {
animation: runningAnimation 1s infinite;
}
/* Animation for completed status */
@keyframes completedAnimation {
0% { background-color: #27ae60; }
100% { background-color: #2ecc71; }
}
.completed {
animation: completedAnimation 1s infinite;
}
/* Add more styles for better look */
.custom-node {
transition: all 0.3s ease-in-out;
}

View File

@ -0,0 +1,7 @@
module.exports = {
devServer: {
proxy: {
'/graphs': 'http://localhost:8000'
}
}
};

View File

@ -3,6 +3,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import praw import praw
from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from autogpt_server.data.block import Block, BlockOutput, BlockSchema from autogpt_server.data.block import Block, BlockOutput, BlockSchema
@ -87,8 +88,9 @@ class RedditGetPostsBlock(Block):
class RedditPostCommentBlock(Block): class RedditPostCommentBlock(Block):
class Input(BlockSchema): class Input(BlockSchema):
creds: RedditCredentials = Field(description="Reddit credentials") creds: RedditCredentials = Field(description="Reddit credentials")
post_id: str = Field(description="Reddit post ID") data: Any = Field(description="Reddit post")
comment: str = Field(description="Comment text") # post_id: str = Field(description="Reddit post ID")
# comment: str = Field(description="Comment text")
class Output(BlockSchema): class Output(BlockSchema):
comment_id: str comment_id: str
@ -102,6 +104,6 @@ class RedditPostCommentBlock(Block):
def run(self, input_data: Input) -> BlockOutput: def run(self, input_data: Input) -> BlockOutput:
client = get_praw(input_data.creds) client = get_praw(input_data.creds)
submission = client.submission(id=input_data.post_id) submission = client.submission(id=input_data.data["post_id"])
comment = submission.reply(input_data.comment) comment = submission.reply(input_data.data["comment"])
yield "comment_id", comment.id yield "comment_id", comment.id

View File

@ -1,4 +1,5 @@
import re import re
import json
from typing import Any from typing import Any
from pydantic import Field from pydantic import Field
@ -7,7 +8,7 @@ from autogpt_server.data.block import Block, BlockOutput, BlockSchema
class TextMatcherBlock(Block): class TextMatcherBlock(Block):
class Input(BlockSchema): class Input(BlockSchema):
text: str = Field(description="Text to match") text: Any = Field(description="Text to match")
match: str = Field(description="Pattern (Regex) to match") match: str = Field(description="Pattern (Regex) to match")
data: Any = Field(description="Data to be forwarded to output") data: Any = Field(description="Data to be forwarded to output")
case_sensitive: bool = Field(description="Case sensitive match", default=True) case_sensitive: bool = Field(description="Case sensitive match", default=True)
@ -26,7 +27,7 @@ class TextMatcherBlock(Block):
def run(self, input_data: Input) -> BlockOutput: def run(self, input_data: Input) -> BlockOutput:
output = input_data.data or input_data.text output = input_data.data or input_data.text
case = 0 if input_data.case_sensitive else re.IGNORECASE case = 0 if input_data.case_sensitive else re.IGNORECASE
if re.search(input_data.match, input_data.text, case): if re.search(input_data.match, json.dumps(input_data.text), case):
yield "positive", output yield "positive", output
else: else:
yield "negative", output yield "negative", output

View File

@ -6,6 +6,7 @@ import uvicorn
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import APIRouter, Body, FastAPI, HTTPException from fastapi import APIRouter, Body, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from autogpt_server.data import db, execution, block from autogpt_server.data import db, execution, block
from autogpt_server.data.graph import ( from autogpt_server.data.graph import (
@ -43,6 +44,14 @@ class AgentServer(AppProcess):
lifespan=self.lifespan, lifespan=self.lifespan,
) )
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
# Define the API routes # Define the API routes
router = APIRouter() router = APIRouter()
router.add_api_route( router.add_api_route(

View File

@ -3,7 +3,7 @@ from pathlib import Path
from pkgutil import iter_modules from pkgutil import iter_modules
from typing import Union from typing import Union
from cx_Freeze import Executable, setup # type: ignore from cx_Freeze import Executable, setup # type: ignore
packages = [ packages = [
m.name m.name
@ -57,7 +57,6 @@ def txt_to_rtf(input_file: Union[str, Path], output_file: Union[str, Path]) -> N
license_file = "LICENSE.rtf" license_file = "LICENSE.rtf"
txt_to_rtf("../../LICENSE", license_file) txt_to_rtf("../../LICENSE", license_file)
setup( setup(
name="AutoGPT Server", name="AutoGPT Server",
url="https://agpt.co", url="https://agpt.co",