diff --git a/src/backend/base/langflow/components/prototypes/CreateData.py b/src/backend/base/langflow/components/prototypes/CreateData.py index 5ece555ed..aca8f6096 100644 --- a/src/backend/base/langflow/components/prototypes/CreateData.py +++ b/src/backend/base/langflow/components/prototypes/CreateData.py @@ -1,7 +1,7 @@ from typing import Any from langflow.custom import Component -from langflow.inputs.inputs import IntInput, MessageTextInput, DictInput +from langflow.inputs.inputs import IntInput, MessageTextInput, DictInput, BoolInput from langflow.io import Output from langflow.field_typing.range_spec import RangeSpec @@ -20,10 +20,21 @@ class CreateDataComponent(Component): display_name="Number of Fields", info="Number of fields to be added to the record.", real_time_refresh=True, - value=0, + value=1, range_spec=RangeSpec(min=1, max=15, step=1, step_type="int"), ), - MessageTextInput(name="text_key", display_name="Text Key", info="Key to be used as text.", advanced=True), + MessageTextInput( + name="text_key", + display_name="Text Key", + info="Key that identifies the field to be used as the text content.", + advanced=True, + ), + BoolInput( + name="text_key_validator", + display_name="Text Key Validator", + advanced=True, + info="If enabled, checks if the given 'Text Key' is present in the given 'Data'.", + ), ] outputs = [ @@ -32,7 +43,7 @@ class CreateDataComponent(Component): def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): if field_name == "number_of_fields": - default_keys = ["code", "_type", "number_of_fields", "text_key"] + default_keys = ["code", "_type", "number_of_fields", "text_key", "text_key_validator"] try: field_value_int = int(field_value) except ValueError: @@ -65,6 +76,15 @@ class CreateDataComponent(Component): return build_config async def build_data(self) -> Data: + data = self.get_data() + return_data = Data(data=data, text_key=self.text_key) + self.status = return_data + if self.text_key_validator: + self.validate_text_key() + return return_data + + def get_data(self): + """Function to get the Data from the attributes""" data = {} for value_dict in self._attributes.values(): if isinstance(value_dict, dict): @@ -73,17 +93,11 @@ class CreateDataComponent(Component): key: value.get_text() if isinstance(value, Data) else value for key, value in value_dict.items() } data.update(value_dict) - return_data = Data(data=data, text_key=self.text_key) - self.status = return_data - return return_data + return data - def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict): - """ - This function is called after the code validation is done. - """ - frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node) - frontend_node["template"] = self.update_build_config( - frontend_node["template"], frontend_node["template"]["number_of_fields"]["value"], "number_of_fields" - ) - frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node) - return frontend_node + def validate_text_key(self): + """This function validates that the Text Key is one of the keys in the Data""" + data_keys = self.get_data().keys() + if self.text_key not in data_keys and self.text_key != "": + formatted_data_keys = ", ".join(data_keys) + raise ValueError(f"Text Key: '{self.text_key}' not found in the Data keys: '{formatted_data_keys}'") diff --git a/src/backend/base/langflow/components/prototypes/SelectData.py b/src/backend/base/langflow/components/prototypes/SelectData.py new file mode 100644 index 000000000..0f6a14bb7 --- /dev/null +++ b/src/backend/base/langflow/components/prototypes/SelectData.py @@ -0,0 +1,47 @@ +from langflow.custom import Component +from langflow.inputs.inputs import DataInput, IntInput +from langflow.io import Output + +from langflow.field_typing.range_spec import RangeSpec +from langflow.schema import Data + + +class SelectDataComponent(Component): + display_name: str = "Select Data" + description: str = "Select a single data from a list of data." + name: str = "SelectData" + icon = "prototypes" + + inputs = [ + DataInput( + name="data_list", + display_name="Data List", + info="List of data to select from.", + is_list=True, # Specify that this input takes a list of Data objects + ), + IntInput( + name="data_index", + display_name="Data Index", + info="Index of the data to select.", + value=0, # Will be populated dynamically based on the length of data_list + range_spec=RangeSpec(min=0, max=15, step=1, step_type="int"), + ), + ] + + outputs = [ + Output(display_name="Selected Data", name="selected_data", method="select_data"), + ] + + async def select_data(self) -> Data: + # Retrieve the selected index from the dropdown + selected_index = int(self.data_index) + # Get the data list + + # Validate that the selected index is within bounds + if selected_index < 0 or selected_index >= len(self.data_list): + raise ValueError(f"Selected index {selected_index} is out of range.") + + # Return the selected Data object + selected_data = self.data_list[selected_index] + self.status = selected_data # Update the component status to reflect the selected data + return selected_data diff --git a/src/backend/base/langflow/components/prototypes/UpdateData.py b/src/backend/base/langflow/components/prototypes/UpdateData.py index 10355d2df..a4c94ae33 100644 --- a/src/backend/base/langflow/components/prototypes/UpdateData.py +++ b/src/backend/base/langflow/components/prototypes/UpdateData.py @@ -1,40 +1,109 @@ -from langflow.custom import CustomComponent +from typing import Any + +from langflow.custom import Component +from langflow.inputs.inputs import IntInput, MessageTextInput, DictInput, DataInput, BoolInput +from langflow.io import Output + +from langflow.field_typing.range_spec import RangeSpec from langflow.schema import Data +from langflow.schema.dotdict import dotdict -class UpdateDataComponent(CustomComponent): - display_name = "Update Data" - description = "Update Data with text-based key/value pairs, similar to updating a Python dictionary." - name = "UpdateData" +class UpdateDataComponent(Component): + display_name: str = "Update data" + description: str = "Dynamically update or append data with the specified fields." + name: str = "UpdateData" - def build_config(self): - return { - "data": { - "display_name": "Data", - "info": "The record to update.", - }, - "new_data": { - "display_name": "New Data", - "info": "The new data to update the record with.", - "input_types": ["Text"], - }, - } + inputs = [ + DataInput( + name="old_data", + display_name="Data", + info="The record to update.", + is_list=False, + ), + IntInput( + name="number_of_fields", + display_name="Number of Fields", + info="Number of fields to be added to the record.", + real_time_refresh=True, + value=0, + range_spec=RangeSpec(min=1, max=15, step=1, step_type="int"), + ), + MessageTextInput( + name="text_key", + display_name="Text Key", + info="Key that identifies the field to be used as the text content.", + advanced=True, + ), + BoolInput( + name="text_key_validator", + display_name="Text Key Validator", + advanced=True, + info="If enabled, checks if the given 'Text Key' is present in the given 'Data'.", + ), + ] - def build( - self, - data: Data, - new_data: dict, - ) -> Data: - """ - Updates a record with new data. + outputs = [ + Output(display_name="Data", name="data", method="build_data"), + ] - Args: - record (Data): The record to update. - new_data (dict): The new data to update the record with. + def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): + if field_name == "number_of_fields": + default_keys = ["code", "_type", "number_of_fields", "text_key", "old_data", "text_key_validator"] + try: + field_value_int = int(field_value) + except ValueError: + return build_config + existing_fields = {} + if field_value_int > 15: + build_config["number_of_fields"]["value"] = 15 + raise ValueError("Number of fields cannot exceed 15. Try using a Component to combine two Data.") + if len(build_config) > len(default_keys): + # back up the existing template fields + for key in build_config.copy(): + if key not in default_keys: + existing_fields[key] = build_config.pop(key) - Returns: - Data: The updated record. - """ - data.data.update(new_data) - self.status = data + for i in range(1, field_value_int + 1): + key = f"field_{i}_key" + if key in existing_fields: + field = existing_fields[key] + build_config[key] = field + else: + field = DictInput( + display_name=f"Field {i}", + name=key, + info=f"Key for field {i}.", + input_types=["Text", "Data"], + ) + build_config[field.name] = field.to_dict() + + build_config["number_of_fields"]["value"] = field_value_int + return build_config + + async def build_data(self) -> Data: + new_data = self.get_data() + self.old_data.data.update(new_data) + if self.text_key: + self.old_data.text_key = self.text_key + self.status = self.old_data + self.validate_text_key(self.old_data) + return self.old_data + + def get_data(self): + """Function to get the Data from the attributes""" + data = {} + for value_dict in self._attributes.values(): + if isinstance(value_dict, dict): + # Check if the value of the value_dict is a Data + value_dict = { + key: value.get_text() if isinstance(value, Data) else value for key, value in value_dict.items() + } + data.update(value_dict) return data + + def validate_text_key(self, data: Data): + """This function validates that the Text Key is one of the keys in the Data""" + data_keys = data.data.keys() + if self.text_key not in data_keys and self.text_key != "": + raise ValueError(f"Text Key: {self.text_key} not found in the Data keys: {','.join(data_keys)}") diff --git a/src/backend/tests/unit/components/prototypes/test_create_data_component.py b/src/backend/tests/unit/components/prototypes/test_create_data_component.py new file mode 100644 index 000000000..d5dc01a5c --- /dev/null +++ b/src/backend/tests/unit/components/prototypes/test_create_data_component.py @@ -0,0 +1,117 @@ +import pytest +from langflow.components.prototypes.CreateData import CreateDataComponent +from langflow.schema import Data + + +@pytest.fixture +def create_data_component(): + return CreateDataComponent() + + +def test_update_build_config(create_data_component): + build_config = { + "number_of_fields": { + "type": "int", + "value": 2, + }, + "text_key": { + "type": "str", + "value": "", + }, + "text_key_validator": { + "type": "bool", + "value": False, + }, + } + updated_config = create_data_component.update_build_config( + build_config=build_config, field_value=3, field_name="number_of_fields" + ) + + assert "field_1_key" in updated_config + assert "field_2_key" in updated_config + assert "field_3_key" in updated_config + assert updated_config["number_of_fields"]["value"] == 3 + + +def test_update_build_config_exceed_limit(create_data_component): + build_config = { + "number_of_fields": { + "type": "int", + "value": 2, + }, + "text_key": { + "type": "str", + "value": "", + }, + "text_key_validator": { + "type": "bool", + "value": False, + }, + } + with pytest.raises( + ValueError, match="Number of fields cannot exceed 15. Try using a Component to combine two Data." + ): + create_data_component.update_build_config(build_config, 16, "number_of_fields") + + +@pytest.mark.asyncio +async def test_build_data(create_data_component): + create_data_component._attributes = { + "field_1_key": {"key1": "value1"}, + "field_2_key": {"key2": "value2"}, + } + create_data_component.text_key = "key1" + create_data_component.text_key_validator = False + + result = await create_data_component.build_data() + + assert isinstance(result, Data) + assert result.data == {"key1": "value1", "key2": "value2"} + assert result.text_key == "key1" + + +def test_get_data(create_data_component): + create_data_component._attributes = { + "field_1_key": {"key1": "value1"}, + "field_2_key": {"key2": "value2"}, + } + + result = create_data_component.get_data() + + assert result == {"key1": "value1", "key2": "value2"} + + +def test_validate_text_key_valid(create_data_component): + # Arrange + create_data_component._attributes = { + "field_1_key": {"key1": "value1"}, + "field_2_key": {"key2": "value2"}, + } + create_data_component.text_key = "key1" + + # Act & Assert + try: + create_data_component.validate_text_key() + except ValueError: + pytest.fail("validate_text_key() raised ValueError unexpectedly!") + + # Additional assertions + assert create_data_component.text_key == "key1" + assert "key1" in create_data_component.get_data() + + +def test_validate_text_key_invalid(create_data_component): + # Arrange + create_data_component._attributes = { + "field_1_key": {"key1": "value1"}, + "field_2_key": {"key2": "value2"}, + } + create_data_component.text_key = "invalid_key" + + # Act & Assert + with pytest.raises(ValueError) as exc_info: + create_data_component.validate_text_key() + + # Check for the exact error message + expected_error_message = f"Text Key: '{create_data_component.text_key}' not found in the Data keys: '{', '.join(create_data_component.get_data().keys())}'" + assert str(exc_info.value) == expected_error_message diff --git a/src/backend/tests/unit/components/prototypes/test_update_data_component.py b/src/backend/tests/unit/components/prototypes/test_update_data_component.py new file mode 100644 index 000000000..36f44edd5 --- /dev/null +++ b/src/backend/tests/unit/components/prototypes/test_update_data_component.py @@ -0,0 +1,105 @@ +import pytest +from langflow.components.prototypes.UpdateData import UpdateDataComponent +from langflow.schema import Data + + +@pytest.fixture +def update_data_component(): + return UpdateDataComponent() + + +def test_update_build_config(update_data_component): + build_config = { + "number_of_fields": { + "type": "int", + "value": 2, + }, + "text_key": { + "type": "str", + "value": "", + }, + "text_key_validator": { + "type": "bool", + "value": False, + }, + } + updated_config = update_data_component.update_build_config( + build_config=build_config, field_value=3, field_name="number_of_fields" + ) + + assert "field_1_key" in updated_config + assert "field_2_key" in updated_config + assert "field_3_key" in updated_config + assert updated_config["number_of_fields"]["value"] == 3 + + +def test_update_build_config_exceed_limit(update_data_component): + build_config = { + "number_of_fields": { + "type": "int", + "value": 2, + }, + "text_key": { + "type": "str", + "value": "", + }, + "text_key_validator": { + "type": "bool", + "value": False, + }, + } + with pytest.raises( + ValueError, match="Number of fields cannot exceed 15. Try using a Component to combine two Data." + ): + update_data_component.update_build_config(build_config, 16, "number_of_fields") + + +@pytest.mark.asyncio +async def test_build_data(update_data_component): + update_data_component._attributes = { + "field_1_key": {"key1": "new_value1"}, + "field_2_key": {"key3": "value3"}, + } + update_data_component.text_key = "key1" + update_data_component.text_key_validator = False + update_data_component.old_data = Data(data={"key1": "old_value1", "key2": "value2"}, text_key="key2") + + result = await update_data_component.build_data() + + assert isinstance(result, Data) + assert result.data == {"key1": "new_value1", "key2": "value2", "key3": "value3"} + assert result.text_key == "key1" + + +def test_get_data(update_data_component): + update_data_component._attributes = { + "field_1_key": {"key1": "value1"}, + "field_2_key": {"key2": "value2"}, + } + + result = update_data_component.get_data() + + assert result == {"key1": "value1", "key2": "value2"} + + +def test_validate_text_key_valid(update_data_component): + data = Data(data={"key1": "value1", "key2": "value2"}, text_key="key1") + update_data_component.text_key = "key1" + + try: + update_data_component.validate_text_key(data) + except ValueError: + pytest.fail("validate_text_key() raised ValueError unexpectedly!") + + +def test_validate_text_key_invalid(update_data_component): + data = Data(data={"key1": "value1", "key2": "value2"}, text_key="key1") + update_data_component.text_key = "invalid_key" + + with pytest.raises(ValueError) as exc_info: + update_data_component.validate_text_key(data) + + expected_error_message = ( + f"Text Key: {update_data_component.text_key} not found in the Data keys: {','.join(data.data.keys())}" + ) + assert str(exc_info.value) == expected_error_message