mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
1 Commits
testing-cl
...
cursor/SEC
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b448593671 |
@@ -549,6 +549,97 @@ class AgentToggleInputBlock(AgentInputBlock):
|
||||
)
|
||||
|
||||
|
||||
class AgentTableInputBlock(AgentInputBlock):
|
||||
"""
|
||||
A table input block that allows users to define column headers and input data in a table format.
|
||||
|
||||
The block outputs a list of dictionaries where each dictionary represents a row,
|
||||
with keys being the column headers and values being the cell data.
|
||||
"""
|
||||
|
||||
class Input(AgentInputBlock.Input):
|
||||
value: list[dict[str, Any]] = SchemaField(
|
||||
description="Table data as a list of dictionaries (rows).",
|
||||
default_factory=list,
|
||||
advanced=False,
|
||||
title="Default Table Data",
|
||||
)
|
||||
headers: list[str] = SchemaField(
|
||||
description="Column headers for the table.",
|
||||
default_factory=list,
|
||||
advanced=False,
|
||||
title="Table Headers",
|
||||
)
|
||||
allow_add_rows: bool = SchemaField(
|
||||
description="Whether users can add new rows to the table.",
|
||||
default=True,
|
||||
advanced=True,
|
||||
)
|
||||
allow_edit_headers: bool = SchemaField(
|
||||
description="Whether users can edit the column headers.",
|
||||
default=True,
|
||||
advanced=True,
|
||||
)
|
||||
min_rows: int = SchemaField(
|
||||
description="Minimum number of rows in the table.",
|
||||
default=0,
|
||||
advanced=True,
|
||||
)
|
||||
max_rows: Optional[int] = SchemaField(
|
||||
description="Maximum number of rows in the table.",
|
||||
default=None,
|
||||
advanced=True,
|
||||
)
|
||||
|
||||
class Output(AgentInputBlock.Output):
|
||||
result: list[dict[str, Any]] = SchemaField(
|
||||
description="Table data as a list of dictionaries with headers as keys."
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
description="Block for table input with customizable headers and rows.",
|
||||
disabled=not config.enable_agent_input_subtype_blocks,
|
||||
input_schema=AgentTableInputBlock.Input,
|
||||
output_schema=AgentTableInputBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"value": [
|
||||
{"Name": "John Doe", "Age": "30", "City": "New York"},
|
||||
{"Name": "Jane Smith", "Age": "25", "City": "Los Angeles"},
|
||||
],
|
||||
"headers": ["Name", "Age", "City"],
|
||||
"name": "table_1",
|
||||
"description": "Example table with user data",
|
||||
},
|
||||
{
|
||||
"value": [
|
||||
{"Product": "Laptop", "Price": "999", "Stock": "50"},
|
||||
{"Product": "Mouse", "Price": "25", "Stock": "100"},
|
||||
],
|
||||
"headers": ["Product", "Price", "Stock"],
|
||||
"name": "table_2",
|
||||
"description": "Example table with product data",
|
||||
},
|
||||
],
|
||||
test_output=[
|
||||
("result", [
|
||||
{"Name": "John Doe", "Age": "30", "City": "New York"},
|
||||
{"Name": "Jane Smith", "Age": "25", "City": "Los Angeles"},
|
||||
]),
|
||||
("result", [
|
||||
{"Product": "Laptop", "Price": "999", "Stock": "50"},
|
||||
{"Product": "Mouse", "Price": "25", "Stock": "100"},
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
|
||||
if input_data.value is not None:
|
||||
yield "result", input_data.value
|
||||
|
||||
|
||||
IO_BLOCK_IDs = [
|
||||
AgentInputBlock().id,
|
||||
AgentOutputBlock().id,
|
||||
@@ -560,4 +651,5 @@ IO_BLOCK_IDs = [
|
||||
AgentFileInputBlock().id,
|
||||
AgentDropdownInputBlock().id,
|
||||
AgentToggleInputBlock().id,
|
||||
AgentTableInputBlock().id,
|
||||
]
|
||||
|
||||
@@ -512,6 +512,22 @@ export const NodeGenericInputField: FC<{
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.TABLE:
|
||||
return (
|
||||
<NodeTableInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema as BlockIOObjectSubSchema}
|
||||
tableData={currentValue}
|
||||
errors={errors}
|
||||
connections={connections}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.LONG_TEXT:
|
||||
case DataType.SHORT_TEXT:
|
||||
default:
|
||||
@@ -815,12 +831,14 @@ const NodeKeyValueInput: FC<{
|
||||
placeholder="Key"
|
||||
value={key ?? ""}
|
||||
onChange={(e) =>
|
||||
updateKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
updateKeyValuePairs([
|
||||
...keyValuePairs.slice(0, index),
|
||||
{
|
||||
key: e.target.value,
|
||||
value: value,
|
||||
}),
|
||||
)
|
||||
},
|
||||
...keyValuePairs.slice(index + 1),
|
||||
])
|
||||
}
|
||||
/>
|
||||
<NodeGenericInputField
|
||||
@@ -833,12 +851,14 @@ const NodeKeyValueInput: FC<{
|
||||
connections={connections}
|
||||
displayName={displayName || beautifyString(key)}
|
||||
handleInputChange={(_, newValue) =>
|
||||
updateKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
updateKeyValuePairs([
|
||||
...keyValuePairs.slice(0, index),
|
||||
{
|
||||
key: key,
|
||||
value: newValue,
|
||||
}),
|
||||
)
|
||||
},
|
||||
...keyValuePairs.slice(index + 1),
|
||||
])
|
||||
}
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
@@ -846,7 +866,10 @@ const NodeKeyValueInput: FC<{
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
onClick={() =>
|
||||
updateKeyValuePairs(keyValuePairs.toSpliced(index, 1))
|
||||
updateKeyValuePairs([
|
||||
...keyValuePairs.slice(0, index),
|
||||
...keyValuePairs.slice(index + 1),
|
||||
])
|
||||
}
|
||||
>
|
||||
<Cross2Icon />
|
||||
@@ -971,7 +994,10 @@ const NodeArrayInput: FC<{
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleInputChange(selfKey, entries.toSpliced(index, 1))
|
||||
handleInputChange(selfKey, [
|
||||
...entries.slice(0, index),
|
||||
...entries.slice(index + 1),
|
||||
])
|
||||
}
|
||||
>
|
||||
<Cross2Icon />
|
||||
@@ -1241,6 +1267,191 @@ const NodeBooleanInput: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const NodeTableInput: FC<{
|
||||
nodeId: string;
|
||||
selfKey: string;
|
||||
schema: BlockIOObjectSubSchema;
|
||||
tableData?: { value?: Array<Record<string, any>>; headers?: string[] };
|
||||
errors: { [key: string]: string | undefined };
|
||||
connections: NodeObjectInputTreeProps["connections"];
|
||||
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
|
||||
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
|
||||
className?: string;
|
||||
displayName?: string;
|
||||
}> = ({
|
||||
nodeId,
|
||||
selfKey,
|
||||
schema,
|
||||
tableData,
|
||||
errors,
|
||||
connections,
|
||||
handleInputChange,
|
||||
handleInputClick,
|
||||
className,
|
||||
displayName,
|
||||
}) => {
|
||||
const [headers, setHeaders] = useState<string[]>(tableData?.headers || ["Column 1"]);
|
||||
const [rows, setRows] = useState<Array<Record<string, any>>>(
|
||||
tableData?.value || [{}]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableData?.headers) setHeaders(tableData.headers);
|
||||
if (tableData?.value) setRows(tableData.value);
|
||||
}, [tableData]);
|
||||
|
||||
const updateTableData = useCallback(
|
||||
(newHeaders: string[], newRows: Array<Record<string, any>>) => {
|
||||
setHeaders(newHeaders);
|
||||
setRows(newRows);
|
||||
handleInputChange(selfKey, {
|
||||
headers: newHeaders,
|
||||
value: newRows,
|
||||
});
|
||||
},
|
||||
[selfKey, handleInputChange]
|
||||
);
|
||||
|
||||
const addHeader = () => {
|
||||
const newHeaders = [...headers, `Column ${headers.length + 1}`];
|
||||
const newRows = rows.map(row => ({ ...row, [newHeaders[newHeaders.length - 1]]: "" }));
|
||||
updateTableData(newHeaders, newRows);
|
||||
};
|
||||
|
||||
const updateHeader = (index: number, newHeader: string) => {
|
||||
const oldHeader = headers[index];
|
||||
const newHeaders = [...headers];
|
||||
newHeaders[index] = newHeader;
|
||||
|
||||
const newRows = rows.map(row => {
|
||||
const newRow = { ...row };
|
||||
if (oldHeader in newRow) {
|
||||
newRow[newHeader] = newRow[oldHeader];
|
||||
delete newRow[oldHeader];
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
|
||||
updateTableData(newHeaders, newRows);
|
||||
};
|
||||
|
||||
const removeHeader = (index: number) => {
|
||||
if (headers.length <= 1) return; // Keep at least one header
|
||||
|
||||
const headerToRemove = headers[index];
|
||||
const newHeaders = headers.filter((_, i) => i !== index);
|
||||
const newRows = rows.map(row => {
|
||||
const newRow = { ...row };
|
||||
delete newRow[headerToRemove];
|
||||
return newRow;
|
||||
});
|
||||
|
||||
updateTableData(newHeaders, newRows);
|
||||
};
|
||||
|
||||
const addRow = () => {
|
||||
const newRow = headers.reduce((acc, header) => ({ ...acc, [header]: "" }), {});
|
||||
const newRows = [...rows, newRow];
|
||||
updateTableData(headers, newRows);
|
||||
};
|
||||
|
||||
const updateCell = (rowIndex: number, header: string, value: any) => {
|
||||
const newRows = [...rows];
|
||||
newRows[rowIndex] = { ...newRows[rowIndex], [header]: value };
|
||||
updateTableData(headers, newRows);
|
||||
};
|
||||
|
||||
const removeRow = (index: number) => {
|
||||
if (rows.length <= 1) return; // Keep at least one row
|
||||
|
||||
const newRows = rows.filter((_, i) => i !== index);
|
||||
updateTableData(headers, newRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className, "flex flex-col space-y-2")}>
|
||||
<div className="text-sm font-medium">{displayName || "Table"}</div>
|
||||
|
||||
{/* Headers */}
|
||||
<div className="flex items-center space-x-2 border-b pb-2">
|
||||
{headers.map((header, index) => (
|
||||
<div key={index} className="flex items-center space-x-1">
|
||||
<LocalValuedInput
|
||||
type="text"
|
||||
value={header}
|
||||
onChange={(e) => updateHeader(index, e.target.value)}
|
||||
className="min-w-[100px] text-sm font-medium"
|
||||
placeholder={`Column ${index + 1}`}
|
||||
/>
|
||||
{headers.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeHeader(index)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Cross2Icon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={addHeader}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
<div className="space-y-1">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div key={rowIndex} className="flex items-center space-x-2">
|
||||
{headers.map((header) => (
|
||||
<LocalValuedInput
|
||||
key={header}
|
||||
type="text"
|
||||
value={row[header] || ""}
|
||||
onChange={(e) => updateCell(rowIndex, header, e.target.value)}
|
||||
className="min-w-[100px] text-sm"
|
||||
placeholder={`Enter ${header}`}
|
||||
/>
|
||||
))}
|
||||
{rows.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeRow(rowIndex)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Cross2Icon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Row Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addRow}
|
||||
className="self-start"
|
||||
>
|
||||
<PlusIcon className="mr-1 h-3 w-3" /> Add Row
|
||||
</Button>
|
||||
|
||||
{errors[selfKey] && (
|
||||
<span className="error-message text-sm text-red-500">
|
||||
{errors[selfKey]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeFallbackInput: FC<{
|
||||
selfKey: string;
|
||||
schema?: BlockIOSubSchema;
|
||||
|
||||
@@ -78,6 +78,7 @@ export enum DataType {
|
||||
OBJECT = "object",
|
||||
KEY_VALUE = "key-value",
|
||||
ARRAY = "array",
|
||||
TABLE = "table",
|
||||
}
|
||||
|
||||
export type BlockIOSubSchemaMeta = {
|
||||
@@ -1082,6 +1083,33 @@ export function determineDataType(schema: BlockIOSubSchema): DataType {
|
||||
schema = schema.allOf[0];
|
||||
}
|
||||
|
||||
// Table detection: Check if this is an object with both 'value' (array of objects) and 'headers' (array of strings)
|
||||
if (
|
||||
"type" in schema &&
|
||||
schema.type === "object" &&
|
||||
"properties" in schema &&
|
||||
schema.properties &&
|
||||
"value" in schema.properties &&
|
||||
"headers" in schema.properties
|
||||
) {
|
||||
const valueSchema = schema.properties.value;
|
||||
const headersSchema = schema.properties.headers;
|
||||
|
||||
// Check if value is array of objects and headers is array of strings
|
||||
if (
|
||||
valueSchema.type === "array" &&
|
||||
"items" in valueSchema &&
|
||||
valueSchema.items &&
|
||||
valueSchema.items.type === "object" &&
|
||||
headersSchema.type === "array" &&
|
||||
"items" in headersSchema &&
|
||||
headersSchema.items &&
|
||||
headersSchema.items.type === "string"
|
||||
) {
|
||||
return DataType.TABLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Credentials override
|
||||
if ("credentials_provider" in schema) {
|
||||
return DataType.CREDENTIALS;
|
||||
@@ -1095,14 +1123,14 @@ export function determineDataType(schema: BlockIOSubSchema): DataType {
|
||||
// Handle anyOf => optional types (string|null, number|null, etc.)
|
||||
if ("anyOf" in schema) {
|
||||
// e.g. schema.anyOf might look like [{ type: "string", ... }, { type: "null" }]
|
||||
const types = schema.anyOf.map((sub) =>
|
||||
const types = (schema.anyOf as any[]).map((sub: any) =>
|
||||
"type" in sub ? sub.type : undefined,
|
||||
);
|
||||
|
||||
// (string | null)
|
||||
if (types.includes("string") && types.includes("null")) {
|
||||
const strSchema = schema.anyOf.find(
|
||||
(s) => s.type === "string",
|
||||
const strSchema = (schema.anyOf as any[]).find(
|
||||
(s: any) => s.type === "string",
|
||||
) as BlockIOStringSubSchema;
|
||||
return _handleStringSchema(strSchema);
|
||||
}
|
||||
@@ -1113,8 +1141,8 @@ export function determineDataType(schema: BlockIOSubSchema): DataType {
|
||||
types.includes("null")
|
||||
) {
|
||||
// Just reuse our single-type logic for whichever is not null
|
||||
const numSchema = schema.anyOf.find(
|
||||
(s) => s.type === "number" || s.type === "integer",
|
||||
const numSchema = (schema.anyOf as any[]).find(
|
||||
(s: any) => s.type === "number" || s.type === "integer",
|
||||
);
|
||||
if (numSchema) {
|
||||
return _handleSingleTypeSchema(numSchema);
|
||||
@@ -1124,15 +1152,15 @@ export function determineDataType(schema: BlockIOSubSchema): DataType {
|
||||
|
||||
// (array | null)
|
||||
if (types.includes("array") && types.includes("null")) {
|
||||
const arrSchema = schema.anyOf.find((s) => s.type === "array");
|
||||
const arrSchema = (schema.anyOf as any[]).find((s: any) => s.type === "array");
|
||||
if (arrSchema) return _handleSingleTypeSchema(arrSchema);
|
||||
return DataType.ARRAY;
|
||||
}
|
||||
|
||||
// (object | null)
|
||||
if (types.includes("object") && types.includes("null")) {
|
||||
const objSchema = schema.anyOf.find(
|
||||
(s) => s.type === "object",
|
||||
const objSchema = (schema.anyOf as any[]).find(
|
||||
(s: any) => s.type === "object",
|
||||
) as BlockIOObjectSubSchema;
|
||||
if (objSchema) return _handleSingleTypeSchema(objSchema);
|
||||
return DataType.OBJECT;
|
||||
|
||||
Reference in New Issue
Block a user