feat: add new Tab input (#7032)

* Added TabMixin

* Added Tab component on inputs

* Added Tab component to initializations

* Added tests for tab input

* Added Tab Component type

* Added options and active tab to input field type

* Added tab component on frontend

* Instantiate tab component

* Update package lock

* Refactor input classes and imports for consistency

- Reordered imports to maintain consistency across files.
- Simplified class definitions by removing unnecessary line breaks.
- Updated the `__all__` list in `__init__.py` files to include `TableInput` consistently.
- Adjusted test cases for cleaner syntax without altering functionality.

* Add constants for tab options limits in input mixin

- Introduced `MAX_TAB_OPTIONS` and `MAX_TAB_OPTION_LENGTH` constants for better maintainability.
- Updated validation logic in `TabMixin` to use these constants for clearer and more flexible error messages.

* Refactor tab input validation tests for improved clarity

- Replaced individual test cases for invalid tab inputs with a parameterized test function.
- Enhanced test coverage by including cases for too many options, exceeding character limits, and non-string values.
- Improved documentation within the test function for better understanding of validation scenarios.

* Enhance tab input validation tests with parameterization

- Refactored `test_tab_input_valid` to use `pytest.mark.parametrize` for improved test coverage and clarity.
- Included multiple scenarios for valid tab inputs, such as standard, fewer options, and empty options.
- Updated assertions to reflect the expected outcomes based on parameterized inputs.

* Enhance TabInput validation to ensure value is a string and one of the specified options

- Updated the `validate_value` method to enforce that the input value is a string.
- Added a check to validate that the value is among the allowed options, raising a ValueError with a descriptive message if not.
- Improved error handling for better user feedback on invalid inputs.

* Fix optional chaining in error handling within CodeAreaModal

- Updated the error check in the `delayedFunction` to use optional chaining for safer access to the error detail.
- This change ensures that the code handles cases where `detail` may be undefined, improving robustness.

* Add 'TAB' field type to schema and update direct types list

- Included 'TAB' as a valid field type in the schema conversion dictionary.
- Updated the DIRECT_TYPES list to include 'tab', ensuring consistency across type definitions.
- These changes enhance the flexibility of the input handling for tab components.

* Add unit test

* Re-added noqa

* fix: unit tests

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Lucas Oliveira 2025-03-17 11:10:48 -03:00 committed by GitHub
commit 95ceb52fa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 408 additions and 7 deletions

View file

@ -22,6 +22,7 @@ from .inputs import (
SecretStrInput,
SliderInput,
StrInput,
TabInput,
TableInput,
)
@ -52,5 +53,6 @@ __all__ = [
"SecretStrInput",
"SliderInput",
"StrInput",
"TabInput",
"TableInput",
]

View file

@ -0,0 +1,2 @@
MAX_TAB_OPTIONS = 3
MAX_TAB_OPTION_LENGTH = 20

View file

@ -11,6 +11,7 @@ from pydantic import (
)
from langflow.field_typing.range_spec import RangeSpec
from langflow.inputs.constants import MAX_TAB_OPTION_LENGTH, MAX_TAB_OPTIONS
from langflow.inputs.validators import CoalesceBool
from langflow.schema.table import Column, TableOptions, TableSchema
@ -30,6 +31,7 @@ class FieldTypes(str, Enum):
TABLE = "table"
LINK = "link"
SLIDER = "slider"
TAB = "tab"
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
@ -188,6 +190,31 @@ class DropDownMixin(BaseModel):
"""Dictionary of dialog inputs for the field. Default is an empty object."""
class TabMixin(BaseModel):
"""Mixin for tab input fields that allows a maximum of 3 values, each with a maximum of 20 characters."""
options: list[str] = Field(default_factory=list, max_length=3)
"""List of tab options. Maximum of 3 values allowed."""
@field_validator("options")
@classmethod
def validate_options(cls, v):
"""Validate that there are at most 3 tab values and each value has at most 20 characters."""
if len(v) > MAX_TAB_OPTIONS:
msg = f"Maximum of {MAX_TAB_OPTIONS} tab values allowed. Got {len(v)} values."
raise ValueError(msg)
for i, value in enumerate(v):
if len(value) > MAX_TAB_OPTION_LENGTH:
msg = (
f"Tab value at index {i} exceeds maximum length of {MAX_TAB_OPTION_LENGTH} "
f"characters. Got {len(value)} characters."
)
raise ValueError(msg)
return v
class MultilineMixin(BaseModel):
multiline: CoalesceBool = True

View file

@ -3,7 +3,7 @@ from collections.abc import AsyncIterator, Iterator
from typing import Any, TypeAlias, get_args
from pandas import DataFrame
from pydantic import Field, field_validator
from pydantic import Field, field_validator, model_validator
from langflow.inputs.validators import CoalesceBool
from langflow.schema.data import Data
@ -26,6 +26,7 @@ from .input_mixin import (
SerializableFieldTypes,
SliderMixin,
TableMixin,
TabMixin,
ToolModeMixin,
)
@ -54,7 +55,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi
"- A single dictionary (will become a one-row table)\n"
"- A Data object (Langflow's internal data structure)\n"
)
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
raise ValueError(msg) # noqa: TRY004
# Ensure each item in the list is either a dict or a Data instance.
for i, item in enumerate(v):
if not isinstance(item, dict | Data):
@ -64,7 +65,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi
"- A Data object (Langflow's internal data structure for passing data between components)\n"
f"Instead, got a {type(item).__name__}. Please check the format of your input data."
)
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
raise ValueError(msg) # noqa: TRY004
return v
@ -105,7 +106,13 @@ class CodeInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeMix
# Applying mixins to a specific input type
class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin, MetadataTraceMixin, ToolModeMixin):
class StrInput(
BaseInputMixin,
ListableInputMixin,
DatabaseLoadMixin,
MetadataTraceMixin,
ToolModeMixin,
):
field_type: SerializableFieldTypes = FieldTypes.TEXT
load_from_db: CoalesceBool = False
"""Defines if the field will allow the user to open a text editor. Default is False."""
@ -399,7 +406,13 @@ class BoolInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolMode
value: CoalesceBool = False
class NestedDictInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, InputTraceMixin, ToolModeMixin):
class NestedDictInput(
BaseInputMixin,
ListableInputMixin,
MetadataTraceMixin,
InputTraceMixin,
ToolModeMixin,
):
"""Represents a nested dictionary field.
This class represents a nested dictionary input and provides functionality for handling dictionary values.
@ -451,6 +464,39 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeM
dialog_inputs: dict[str, Any] = Field(default_factory=dict)
class TabInput(BaseInputMixin, TabMixin, MetadataTraceMixin, ToolModeMixin):
"""Represents a tab input field.
This class represents a tab input field that allows a maximum of 3 values, each with a maximum of 20 characters.
It inherits from the `BaseInputMixin` and `TabMixin` classes.
Attributes:
field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.TAB.
options (list[str]): List of tab options. Maximum of 3 values allowed, each with a maximum of 20 characters.
active_tab (int): Index of the currently active tab. Defaults to 0.
"""
field_type: SerializableFieldTypes = FieldTypes.TAB
options: list[str] = Field(default_factory=list)
@model_validator(mode="after")
@classmethod
def validate_value(cls, values):
"""Validates the value to ensure it's one of the tab values."""
options = values.options # Agora temos certeza de que options está disponível
value = values.value
if not isinstance(value, str):
msg = f"TabInput value must be a string. Got {type(value).__name__}."
raise TypeError(msg)
if value not in options and value != "":
msg = f"TabInput value must be one of the following: {options}. Got: '{value}'"
raise ValueError(msg)
return values
class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeMixin):
"""Represents a multiselect input field.
@ -541,6 +587,7 @@ InputTypes: TypeAlias = (
| LinkInput
| SliderInput
| DataFrameInput
| TabInput
)
InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)}

View file

@ -22,6 +22,7 @@ from langflow.inputs import (
SecretStrInput,
SliderInput,
StrInput,
TabInput,
TableInput,
)
from langflow.template import Output
@ -52,5 +53,6 @@ __all__ = [
"SecretStrInput",
"SliderInput",
"StrInput",
"TabInput",
"TableInput",
]

View file

@ -17,6 +17,7 @@ _convert_field_type_to_type: dict[FieldTypes, type] = {
FieldTypes.PROMPT: str,
FieldTypes.CODE: str,
FieldTypes.OTHER: str,
FieldTypes.TAB: str,
}
if TYPE_CHECKING:

View file

@ -52,7 +52,7 @@ def python_function(text: str) -> str:
PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider"]
DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider", "tab"]
LOADERS_INFO: list[dict[str, Any]] = [

View file

@ -19,6 +19,7 @@ from langflow.inputs.inputs import (
SecretStrInput,
SliderInput,
StrInput,
TabInput,
TableInput,
)
from langflow.inputs.utils import instantiate_input
@ -213,6 +214,81 @@ def test_file_input_valid():
assert file_input.value == ["/path/to/file"]
@pytest.mark.parametrize(
("test_id", "options", "value", "expected_options", "expected_value"),
[
(
"standard_valid",
["Tab1", "Tab2", "Tab3"],
"Tab1",
["Tab1", "Tab2", "Tab3"],
"Tab1",
),
(
"fewer_options",
["Tab1", "Tab2"],
"Tab2",
["Tab1", "Tab2"],
"Tab2",
),
(
"empty_options",
[],
"",
[],
"",
),
],
)
def test_tab_input_valid(test_id, options, value, expected_options, expected_value):
"""Test TabInput validation with valid inputs."""
data = TabInput(
name=f"valid_tab_{test_id}",
options=options,
value=value,
)
assert data.options == expected_options
assert data.value == expected_value
@pytest.mark.parametrize(
("test_id", "options", "value", "error_expected"),
[
(
"too_many_options",
["Tab1", "Tab2", "Tab3", "Tab4"],
"Tab1",
ValidationError,
),
(
"option_too_long",
[
"Tab1",
"ThisTabValueIsTooLongAndExceedsTwentyCharacters",
"Tab3",
],
"Tab1",
ValidationError,
),
(
"non_string_value",
["Tab1", "Tab2", "Tab3"],
123,
TypeError,
),
],
)
def test_tab_input_invalid(test_id, options, value, error_expected):
"""Test TabInput validation with invalid inputs."""
if error_expected:
with pytest.raises(error_expected):
TabInput(
name=f"invalid_tab_{test_id}",
options=options,
value=value,
)
def test_instantiate_input_comprehensive():
valid_data = {
"StrInput": {"name": "str_input", "value": "A string"},
@ -224,6 +300,11 @@ def test_instantiate_input_comprehensive():
"name": "multiselect_input",
"value": ["option1", "option2"],
},
"TabInput": {
"name": "tab_input",
"options": ["Tab1", "Tab2", "Tab3"],
"value": "Tab1",
},
}
for input_type, data in valid_data.items():