From be8fd0f891733d183f5cbf10353446724229113f Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Mon, 2 Dec 2024 16:56:05 +0530 Subject: [PATCH 1/2] feat: Add mutually exclusive input handling in NodeGenericInputField ### Changes - Introduced `mutually_exclusive` parameter in `SchemaField` to manage input exclusivity. - Implemented logic in `NodeGenericInputField` to disable inputs based on mutual exclusivity. - Updated related components to support the new `disabled` state for inputs. - Enhanced `BlockIOSubSchemaMeta` to include `mutually_exclusive` property. --- .../backend/blocks/test_mutual_exclusive.py | 80 +++++++++++++++++ .../backend/backend/data/model.py | 2 + .../src/components/node-input-components.tsx | 87 +++++++++++++++---- .../src/lib/autogpt-server-api/types.ts | 1 + 4 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 autogpt_platform/backend/backend/blocks/test_mutual_exclusive.py diff --git a/autogpt_platform/backend/backend/blocks/test_mutual_exclusive.py b/autogpt_platform/backend/backend/blocks/test_mutual_exclusive.py new file mode 100644 index 000000000000..ec7065bc2074 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/test_mutual_exclusive.py @@ -0,0 +1,80 @@ +from typing import Optional, Literal +from pydantic import BaseModel + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +class TestMutualExclusiveBlock(Block): + class Input(BlockSchema): + text_input_1: Optional[str] = SchemaField( + title="Text Input 1", + description="First mutually exclusive input", + ) + + text_input_2: Optional[str] = SchemaField( + title="Text Input 2", + description="Second mutually exclusive input", + ) + + text_input_3: Optional[str] = SchemaField( + title="Text Input 3", + description="Third mutually exclusive input", + ) + + number_input_1: Optional[int] = SchemaField( + title="Number Input 1", + description="First number input (mutually exclusive)", + mutually_exclusive="group2" + ) + + number_input_2: Optional[int] = SchemaField( + title="Number Input 2", + description="Second number input (mutually exclusive)", + mutually_exclusive="group2" + ) + + independent_input: str = SchemaField( + title="Independent Input", + description="This input is not mutually exclusive with others", + default="This can be filled anytime" + ) + + class Output(BlockSchema): + result: str = SchemaField( + description="Shows which inputs were filled" + ) + + def __init__(self): + super().__init__( + id="b7faa910-b074-11ef-bee7-477f51db4711", + description="A test block to demonstrate mutually exclusive inputs", + categories={BlockCategory.BASIC}, + input_schema=TestMutualExclusiveBlock.Input, + output_schema=TestMutualExclusiveBlock.Output, + test_input={ + "text_input_1": "Test input 1", + "independent_input": "Independent test" + }, + test_output=[ + ("result", "Text Input 1: Test input 1\nIndependent Input: Independent test") + ] + ) + + def run(self, input_data: Input, **kwargs) -> BlockOutput: + filled_inputs = [] + + if input_data.text_input_1: + filled_inputs.append(f"Text Input 1: {input_data.text_input_1}") + if input_data.text_input_2: + filled_inputs.append(f"Text Input 2: {input_data.text_input_2}") + if input_data.text_input_3: + filled_inputs.append(f"Text Input 3: {input_data.text_input_3}") + + if input_data.number_input_1: + filled_inputs.append(f"Number Input 1: {input_data.number_input_1}") + if input_data.number_input_2: + filled_inputs.append(f"Number Input 2: {input_data.number_input_2}") + + filled_inputs.append(f"Independent Input: {input_data.independent_input}") + + yield "result", "\n".join(filled_inputs) diff --git a/autogpt_platform/backend/backend/data/model.py b/autogpt_platform/backend/backend/data/model.py index f8b6781e0ce1..4382bac71f74 100644 --- a/autogpt_platform/backend/backend/data/model.py +++ b/autogpt_platform/backend/backend/data/model.py @@ -124,6 +124,7 @@ def SchemaField( secret: bool = False, exclude: bool = False, hidden: Optional[bool] = None, + mutually_exclusive: Optional[str] = None, **kwargs, ) -> T: json_extra = { @@ -133,6 +134,7 @@ def SchemaField( "secret": secret, "advanced": advanced, "hidden": hidden, + "mutually_exclusive": mutually_exclusive, }.items() if v is not None } diff --git a/autogpt_platform/frontend/src/components/node-input-components.tsx b/autogpt_platform/frontend/src/components/node-input-components.tsx index b93a894b9e92..bfce480b845c 100644 --- a/autogpt_platform/frontend/src/components/node-input-components.tsx +++ b/autogpt_platform/frontend/src/components/node-input-components.tsx @@ -32,6 +32,7 @@ import { LocalValuedInput } from "./ui/input"; import NodeHandle from "./NodeHandle"; import { ConnectionData } from "./CustomNode"; import { CredentialsInput } from "./integrations/credentials-input"; +import { useNodes } from "@xyflow/react"; type NodeObjectInputTreeProps = { nodeId: string; @@ -116,6 +117,42 @@ export const NodeGenericInputField: FC<{ className, displayName, }) => { + const nodes = useNodes(); + + const isDisabledByMutualExclusion = useCallback(() => { + if (!propSchema.mutually_exclusive) return false; + + // Find all inputs in the same node with the same mutuallyExclusive group + const node = nodes.find((n) => n.id === nodeId); + if (!node) return false; + const inputSchema = node.data.inputSchema as { + properties?: Record; + }; + if (!inputSchema?.properties) return false; + + // Check if any other input in the same group has a value + return Object.entries(inputSchema.properties).some(([key, schema]) => { + const otherSchema = schema; + return ( + key !== propKey && + otherSchema.mutually_exclusive === propSchema.mutually_exclusive && + (node.data.hardcodedValues as Record)[key] !== + undefined && + (node.data.hardcodedValues as Record)[key] !== "" + ); + }); + }, [nodeId, propKey, propSchema, nodes]); + + const isDisabled = isDisabledByMutualExclusion(); + + const handleMutualExclusiveInputChange = (key: string, value: any) => { + if (isDisabled) { + console.warn("Input is disabled due to mutual exclusivity"); + return; + } + handleInputChange(key, value); + }; + className = cn(className, "my-2"); displayName ||= propSchema.title || beautifyString(propKey); @@ -219,7 +256,8 @@ export const NodeGenericInputField: FC<{ error={errors[propKey]} className={className} displayName={displayName} - handleInputChange={handleInputChange} + disabled={isDisabled} + handleInputChange={handleMutualExclusiveInputChange} handleInputClick={handleInputClick} /> ); @@ -244,7 +282,8 @@ export const NodeGenericInputField: FC<{ error={errors[propKey]} className={className} displayName={displayName} - handleInputChange={handleInputChange} + disabled={isDisabled} + handleInputChange={handleMutualExclusiveInputChange} handleInputClick={handleInputClick} /> ); @@ -443,7 +482,7 @@ const NodeKeyValueInput: FC<{ >
{keyValuePairs.map(({ key, value }, index) => ( - /* + /* The `index` is used as a DOM key instead of the actual `key` because the `key` can change with each input, causing the input to lose focus. */ @@ -693,6 +732,7 @@ const NodeStringInput: FC<{ handleInputClick: NodeObjectInputTreeProps["handleInputClick"]; className?: string; displayName: string; + disabled?: boolean; }> = ({ selfKey, schema, @@ -702,6 +742,7 @@ const NodeStringInput: FC<{ handleInputClick, className, displayName, + disabled, }) => { if (!value) { value = schema.default || ""; @@ -718,8 +759,11 @@ const NodeStringInput: FC<{