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
parent
833944e228
commit
6456285753
|
@ -8,6 +8,12 @@ type Schema = {
|
|||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
items?: Schema;
|
||||
additionalProperties?: { type: string };
|
||||
allOf?: any[];
|
||||
anyOf?: any[];
|
||||
oneOf?: any[];
|
||||
};
|
||||
|
||||
type CustomNodeData = {
|
||||
|
@ -25,6 +31,9 @@ type CustomNodeData = {
|
|||
|
||||
const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
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 [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
const [modalValue, setModalValue] = useState<string>('');
|
||||
|
@ -76,41 +85,24 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
};
|
||||
|
||||
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);
|
||||
data.setHardcodedValues(newValues);
|
||||
setErrors((prevErrors) => ({ ...prevErrors, [key]: null }));
|
||||
};
|
||||
|
||||
const validateInput = (key: string, value: any, schema: any) => {
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
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 getValue = (key: string) => {
|
||||
const keys = key.split('.');
|
||||
return keys.reduce((acc, k) => (acc && acc[k] !== undefined) ? acc[k] : '', data.hardcodedValues);
|
||||
};
|
||||
|
||||
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) => {
|
||||
setActiveKey(key);
|
||||
setModalValue(data.hardcodedValues[key] || '');
|
||||
const value = getValue(key);
|
||||
setModalValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalSave = (value: string) => {
|
||||
if (activeKey) {
|
||||
handleInputChange(activeKey, value);
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
handleInputChange(activeKey, parsedValue);
|
||||
} catch (error) {
|
||||
handleInputChange(activeKey, value);
|
||||
}
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setActiveKey(null);
|
||||
};
|
||||
|
||||
const addArrayItem = (key: string) => {
|
||||
const currentValues = data.hardcodedValues[key] || [];
|
||||
handleInputChange(key, [...currentValues, '']);
|
||||
};
|
||||
const renderInputField = (key: string, schema: any, parentKey: string = ''): JSX.Element => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const error = errors[fullKey];
|
||||
const value = getValue(fullKey);
|
||||
|
||||
const removeArrayItem = (key: string, index: number) => {
|
||||
const currentValues = data.hardcodedValues[key] || [];
|
||||
currentValues.splice(index, 1);
|
||||
handleInputChange(key, [...currentValues]);
|
||||
};
|
||||
if (isHandleConnected(fullKey)) {
|
||||
return <div className="connected-input">Connected</div>;
|
||||
}
|
||||
|
||||
const handleArrayItemChange = (key: string, index: number, value: string) => {
|
||||
const currentValues = data.hardcodedValues[key] || [];
|
||||
currentValues[index] = value;
|
||||
handleInputChange(key, [...currentValues]);
|
||||
};
|
||||
const renderClickableInput = (displayValue: string) => (
|
||||
<div className="clickable-input" onClick={() => handleInputClick(fullKey)}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
|
||||
const addDynamicTextInput = () => {
|
||||
const dynamicKeyPrefix = 'texts_$_';
|
||||
const currentKeys = Object.keys(data.hardcodedValues).filter(key => key.startsWith(dynamicKeyPrefix));
|
||||
const nextIndex = currentKeys.length + 1;
|
||||
const newKey = `${dynamicKeyPrefix}${nextIndex}`;
|
||||
handleInputChange(newKey, '');
|
||||
};
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{key}:</strong>
|
||||
{Object.entries(schema.properties).map(([propKey, propSchema]: [string, any]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
{renderInputField(propKey, propSchema, fullKey)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const removeDynamicTextInput = (key: string) => {
|
||||
const newValues = { ...data.hardcodedValues };
|
||||
delete newValues[key];
|
||||
data.setHardcodedValues(newValues);
|
||||
};
|
||||
if (schema.type === 'object' && schema.additionalProperties) {
|
||||
const objectValue = value || {};
|
||||
return (
|
||||
<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">
|
||||
×
|
||||
</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) => {
|
||||
handleInputChange(key, value);
|
||||
};
|
||||
if (schema.anyOf) {
|
||||
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) {
|
||||
case 'string':
|
||||
return schema.enum ? (
|
||||
<div key={key} className="input-container">
|
||||
<div key={fullKey} className="input-container">
|
||||
<select
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
value={value || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {key}</option>
|
||||
{schema.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
|
@ -192,44 +298,34 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
) : (
|
||||
<div key={key} className="input-container">
|
||||
<div className="clickable-input" onClick={() => handleInputClick(key)}>
|
||||
{data.hardcodedValues[key] || `Enter ${key}`}
|
||||
</div>
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value || `Enter ${key}`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
value="true"
|
||||
checked={data.hardcodedValues[key] === true}
|
||||
onChange={() => handleInputChange(key, true)}
|
||||
/>
|
||||
True
|
||||
</label>
|
||||
<label className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
value="false"
|
||||
checked={data.hardcodedValues[key] === false}
|
||||
onChange={() => handleInputChange(key, false)}
|
||||
/>
|
||||
False
|
||||
</label>
|
||||
<div key={fullKey} className="input-container">
|
||||
<select
|
||||
value={value === undefined ? '' : value.toString()}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {key}</option>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<div key={fullKey} className="input-container">
|
||||
<input
|
||||
type="number"
|
||||
value={data.hardcodedValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, parseFloat(e.target.value))}
|
||||
value={value || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
|
||||
className="number-input"
|
||||
/>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
|
@ -237,23 +333,23 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
);
|
||||
case 'array':
|
||||
if (schema.items && schema.items.type === 'string') {
|
||||
const arrayValues = data.hardcodedValues[key] || [];
|
||||
const arrayValues = value || [];
|
||||
return (
|
||||
<div key={key} className="input-container">
|
||||
<div key={fullKey} className="input-container">
|
||||
{arrayValues.map((item: string, index: number) => (
|
||||
<div key={`${key}-${index}`} className="array-item-container">
|
||||
<div key={`${fullKey}.${index}`} className="array-item-container">
|
||||
<input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => handleArrayItemChange(key, index, e.target.value)}
|
||||
onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
|
||||
className="array-item-input"
|
||||
/>
|
||||
<button onClick={() => removeArrayItem(key, index)} className="array-item-remove">
|
||||
<button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => addArrayItem(key)} className="array-item-add">
|
||||
<button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
|
||||
Add Item
|
||||
</button>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
|
@ -262,52 +358,33 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
}
|
||||
return null;
|
||||
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">
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const validateInputs = () => {
|
||||
const newErrors: { [key: string]: string | null } = {};
|
||||
Object.keys(data.inputSchema.properties).forEach((key) => {
|
||||
const value = data.hardcodedValues[key];
|
||||
const schema = data.inputSchema.properties[key];
|
||||
const error = validateInput(key, value, schema);
|
||||
if (error) {
|
||||
newErrors[key] = error;
|
||||
}
|
||||
});
|
||||
const validateRecursive = (schema: any, parentKey: string = '') => {
|
||||
Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const value = getValue(fullKey);
|
||||
|
||||
if (propSchema.type === 'object' && propSchema.properties) {
|
||||
validateRecursive(propSchema, fullKey);
|
||||
} else {
|
||||
if (propSchema.required && !value) {
|
||||
newErrors[fullKey] = `${fullKey} is required`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
validateRecursive(data.inputSchema);
|
||||
setErrors(newErrors);
|
||||
return Object.values(newErrors).every((error) => error === null);
|
||||
};
|
||||
|
@ -320,9 +397,8 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="custom-node">
|
||||
<div className={`custom-node ${data.status === 'RUNNING' ? 'running' : data.status === 'COMPLETED' ? 'completed' : ''}`}>
|
||||
<div className="node-header">
|
||||
<div className="node-title">{data.blockType || data.title}</div>
|
||||
<button onClick={toggleProperties} className="toggle-button">
|
||||
|
@ -332,38 +408,18 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
<div className="node-content">
|
||||
<div className="input-section">
|
||||
{data.inputSchema &&
|
||||
Object.keys(data.inputSchema.properties).map((key) => (
|
||||
Object.entries(data.inputSchema.properties).map(([key, schema]) => (
|
||||
<div key={key}>
|
||||
{key !== 'texts' ? (
|
||||
<div>
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</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 className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%' }}
|
||||
/>
|
||||
<span className="handle-label">{key}</span>
|
||||
</div>
|
||||
{renderInputField(key, schema)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -397,4 +453,4 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
export default memo(CustomNode);
|
||||
|
|
|
@ -19,6 +19,7 @@ import './flow.css';
|
|||
type Schema = {
|
||||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
additionalProperties?: { type: string };
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
|
@ -44,12 +45,6 @@ type AvailableNode = {
|
|||
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}> =
|
||||
({isOpen, availableNodes, addNode}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
@ -61,28 +56,17 @@ const Sidebar: React.FC<{isOpen: boolean, availableNodes: AvailableNode[], addNo
|
|||
);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '250px',
|
||||
backgroundColor: '#333',
|
||||
padding: '20px',
|
||||
zIndex: 4,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<h3 style={{color: '#fff'}}>Nodes</h3>
|
||||
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
|
||||
<h3>Nodes</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search nodes..."
|
||||
style={{width: '100%', marginBottom: '10px', padding: '5px'}}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{filteredNodes.map((node) => (
|
||||
<div key={node.id} style={{marginBottom: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||
<span style={{color: '#fff'}}>{node.name}</span>
|
||||
<div key={node.id} className="sidebarNodeRowStyle">
|
||||
<span>{node.name}</span>
|
||||
<button onClick={() => addNode(node.id, node.name)}>Add</button>
|
||||
</div>
|
||||
))}
|
||||
|
@ -183,38 +167,61 @@ const Flow: React.FC = () => {
|
|||
};
|
||||
|
||||
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) {
|
||||
console.error(`Schema not found for block ID: ${node.data.block_id}`);
|
||||
return {};
|
||||
if (!blockSchema) {
|
||||
console.error(`Schema not found for block ID: ${node.data.block_id}`);
|
||||
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;
|
||||
};
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
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 = {
|
||||
id: agentId || '',
|
||||
name: 'Agent Name',
|
||||
description: 'Agent Description',
|
||||
nodes: formattedNodes,
|
||||
links: links // Ensure this field is included
|
||||
};
|
||||
|
||||
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 newAgentId = createData.id;
|
||||
setAgentId(newAgentId);
|
||||
|
||||
console.log('Response from the API:', JSON.stringify(createData, null, 2));
|
||||
|
||||
const executeResponse = await fetch(`${apiUrl}/graphs/${newAgentId}/execute`, {
|
||||
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -290,14 +303,15 @@ const Flow: React.FC = () => {
|
|||
const executeData = await executeResponse.json();
|
||||
const runId = executeData.id;
|
||||
|
||||
|
||||
const pollExecution = async () => {
|
||||
const response = await fetch(`${apiUrl}/graphs/${newAgentId}/executions/${runId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
data.forEach(updateNodeData);
|
||||
updateNodesWithExecutionData(data);
|
||||
|
||||
if (data.every((node: any) => node.status === 'COMPLETED')) {
|
||||
console.log('All nodes completed execution');
|
||||
} else {
|
||||
|
@ -312,47 +326,29 @@ const Flow: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const updateNodesWithExecutionData = (executionData: any[]) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
const nodeExecution = executionData.find((exec) => exec.node_id === node.id);
|
||||
if (nodeExecution) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
status: nodeExecution.status,
|
||||
output_data: nodeExecution.output_data,
|
||||
isPropertiesOpen: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateNodesWithExecutionData = (executionData: any[]) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
const nodeExecution = executionData.find((exec) => exec.node_id === node.id);
|
||||
if (nodeExecution) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
status: nodeExecution.status,
|
||||
output_data: nodeExecution.output_data,
|
||||
isPropertiesOpen: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{ height: '100vh', width: '100%' }}>
|
||||
<button
|
||||
|
@ -384,4 +380,4 @@ const Flow: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Flow;
|
||||
export default Flow;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
.custom-node {
|
||||
padding: 20px;
|
||||
padding: 15px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 20px;
|
||||
background: #333;
|
||||
border-radius: 12px;
|
||||
background: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
width: 250px;
|
||||
width: 300px; /* Adjust width for better layout */
|
||||
box-sizing: border-box; /* Ensure padding doesn't affect overall width */
|
||||
}
|
||||
|
||||
.node-header {
|
||||
|
@ -28,9 +29,8 @@
|
|||
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-direction: column; /* Use column to stack elements */
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
|
@ -137,3 +137,49 @@
|
|||
font-size: 12px;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/graphs': 'http://localhost:8000'
|
||||
}
|
||||
}
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
import praw
|
||||
from typing import Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
|
@ -87,8 +88,9 @@ class RedditGetPostsBlock(Block):
|
|||
class RedditPostCommentBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
creds: RedditCredentials = Field(description="Reddit credentials")
|
||||
post_id: str = Field(description="Reddit post ID")
|
||||
comment: str = Field(description="Comment text")
|
||||
data: Any = Field(description="Reddit post")
|
||||
# post_id: str = Field(description="Reddit post ID")
|
||||
# comment: str = Field(description="Comment text")
|
||||
|
||||
class Output(BlockSchema):
|
||||
comment_id: str
|
||||
|
@ -102,6 +104,6 @@ class RedditPostCommentBlock(Block):
|
|||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
client = get_praw(input_data.creds)
|
||||
submission = client.submission(id=input_data.post_id)
|
||||
comment = submission.reply(input_data.comment)
|
||||
submission = client.submission(id=input_data.data["post_id"])
|
||||
comment = submission.reply(input_data.data["comment"])
|
||||
yield "comment_id", comment.id
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
import json
|
||||
|
||||
from typing import Any
|
||||
from pydantic import Field
|
||||
|
@ -7,7 +8,7 @@ from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
|||
|
||||
class TextMatcherBlock(Block):
|
||||
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")
|
||||
data: Any = Field(description="Data to be forwarded to output")
|
||||
case_sensitive: bool = Field(description="Case sensitive match", default=True)
|
||||
|
@ -26,7 +27,7 @@ class TextMatcherBlock(Block):
|
|||
def run(self, input_data: Input) -> BlockOutput:
|
||||
output = input_data.data or input_data.text
|
||||
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
|
||||
else:
|
||||
yield "negative", output
|
||||
|
|
|
@ -6,6 +6,7 @@ import uvicorn
|
|||
|
||||
from contextlib import asynccontextmanager
|
||||
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.graph import (
|
||||
|
@ -43,6 +44,14 @@ class AgentServer(AppProcess):
|
|||
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
|
||||
router = APIRouter()
|
||||
router.add_api_route(
|
||||
|
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
|||
from pkgutil import iter_modules
|
||||
from typing import Union
|
||||
|
||||
from cx_Freeze import Executable, setup # type: ignore
|
||||
from cx_Freeze import Executable, setup # type: ignore
|
||||
|
||||
packages = [
|
||||
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"
|
||||
txt_to_rtf("../../LICENSE", license_file)
|
||||
|
||||
|
||||
setup(
|
||||
name="AutoGPT Server",
|
||||
url="https://agpt.co",
|
||||
|
@ -102,7 +101,7 @@ setup(
|
|||
"applications_shortcut": True,
|
||||
"volume_label": "AutoGPTServer",
|
||||
"background": "builtin-arrow",
|
||||
|
||||
|
||||
"license": {
|
||||
"default-language": "en_US",
|
||||
"licenses": {"en_US": license_file},
|
||||
|
|
Loading…
Reference in New Issue