diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index 0982848e0..382c40cc4 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -73,7 +73,7 @@ The CustomComponent class serves as the foundation for creating custom component | _`required: bool`_ | Makes the field required. | | _`info: str`_ | Adds a tooltip to the field. | | _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is _file_. Defines which file types will be accepted. For example, _json_, _yaml_ or _yml_. | - + | _`range_spec: langflow.field_typing.RangeSpec`_ | This is a requirement if the _`field_type`_ is _`float`_. Defines the range of values accepted and the step size. If none is defined, the default is _`[-1, 1, 0.1]`_. | - The CustomComponent class also provides helpful methods for specific tasks (e.g., to load and use other flows from the Langflow platform): | Method Name | Description | diff --git a/src/backend/langflow/field_typing/__init__.py b/src/backend/langflow/field_typing/__init__.py index 30a8d8abe..6fc932e6f 100644 --- a/src/backend/langflow/field_typing/__init__.py +++ b/src/backend/langflow/field_typing/__init__.py @@ -24,6 +24,7 @@ from .constants import ( Tool, VectorStore, ) +from .range_spec import RangeSpec __all__ = [ "NestedDict", @@ -48,5 +49,6 @@ __all__ = [ "BasePromptTemplate", "ChatPromptTemplate", "Prompt", + "RangeSpec", "TemplateField", ] diff --git a/src/backend/langflow/field_typing/range_spec.py b/src/backend/langflow/field_typing/range_spec.py new file mode 100644 index 000000000..036d21fa9 --- /dev/null +++ b/src/backend/langflow/field_typing/range_spec.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, field_validator + + +class RangeSpec(BaseModel): + min: float = -1.0 + max: float = 1.0 + step: float = 0.1 + + @field_validator("max") + @classmethod + def max_must_be_greater_than_min(cls, v, values, **kwargs): + if "min" in values.data and v <= values.data["min"]: + raise ValueError("max must be greater than min") + return v + + @field_validator("step") + @classmethod + def step_must_be_positive(cls, v): + if v <= 0: + raise ValueError("step must be positive") + return v diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 08c00afbb..41c6ed350 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -8,6 +8,9 @@ from uuid import UUID from cachetools import LRUCache, cached from fastapi import HTTPException +from loguru import logger + +from langflow.field_typing.range_spec import RangeSpec from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator from langflow.interface.custom.custom_component import CustomComponent @@ -31,7 +34,6 @@ from langflow.template.field.base import TemplateField from langflow.template.frontend_node.constants import CLASSES_TO_REMOVE from langflow.template.frontend_node.custom_components import CustomComponentFrontendNode from langflow.utils.util import get_base_classes -from loguru import logger # Used to get the base_classes list @@ -272,6 +274,10 @@ def update_field_dict(field_dict): field_dict["value"] = field_dict["value"](field_dict.get("options", [])) field_dict["refresh"] = True + # Let's check if "range_spec" is a RangeSpec object + if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): + field_dict["rangeSpec"] = field_dict["rangeSpec"].model_dump() + def add_extra_fields(frontend_node, field_config, function_args): """Add extra fields to the frontend node""" diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index aa39da213..7571f5209 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -3,6 +3,8 @@ from typing import Any, Callable, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_serializer +from langflow.field_typing.range_spec import RangeSpec + class TemplateField(BaseModel): model_config = ConfigDict() @@ -60,6 +62,9 @@ class TemplateField(BaseModel): refresh: Optional[bool] = None """Specifies if the field should be refreshed. Defaults to False.""" + range_spec: Optional[RangeSpec] = Field(None, serialization_alias="rangeSpec") + """Range specification for the field. Defaults to None.""" + def to_dict(self): return self.model_dump(by_alias=True, exclude_none=True) @@ -68,3 +73,11 @@ class TemplateField(BaseModel): if self.field_type == "file": return value return "" + + @field_serializer("field_type") + def serialize_field_type(self, value, _info): + if value == "float": + # check if range_spec is set + if self.range_spec is None: + self.range_spec = RangeSpec() + return value diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 160f8c051..b08b92bb9 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -457,6 +457,7 @@ export default function ParameterComponent({ diff --git a/src/frontend/src/components/codeTabsComponent/index.tsx b/src/frontend/src/components/codeTabsComponent/index.tsx index 2ff5bd591..bf9f4ff74 100644 --- a/src/frontend/src/components/codeTabsComponent/index.tsx +++ b/src/frontend/src/components/codeTabsComponent/index.tsx @@ -472,6 +472,11 @@ export default function CodeTabsComponent({ templateField ].value } + rangeSpec={ + node.data.node.template[ + templateField + ].rangeSpec + } onChange={(target) => { setData((old) => { let newInputList = diff --git a/src/frontend/src/components/floatComponent/index.tsx b/src/frontend/src/components/floatComponent/index.tsx index 7e57361f0..9b3376499 100644 --- a/src/frontend/src/components/floatComponent/index.tsx +++ b/src/frontend/src/components/floatComponent/index.tsx @@ -7,11 +7,14 @@ export default function FloatComponent({ value, onChange, disabled, + rangeSpec, editNode = false, }: FloatComponentType): JSX.Element { - const step = 0.1; - const min = -2; - const max = 2; + const step = rangeSpec?.step ?? 0.1; + const min = rangeSpec?.min ?? -2; + const max = rangeSpec?.max ?? 2; + console.log("FloatComponent", value, disabled, rangeSpec, editNode); + console.log("FloatComponent", step, min, max); // Clear component state useEffect(() => { @@ -40,7 +43,9 @@ export default function FloatComponent({ disabled={disabled} className={editNode ? "input-edit-node" : ""} placeholder={ - editNode ? "Number -2 to 2" : "Type a number from minus two to two" + editNode + ? `Enter a value between ${min} and ${max}` + : `Enter a value between ${min} and ${max}` } onChange={(event) => { onChange(event.target.value); diff --git a/src/frontend/src/components/intComponent/index.tsx b/src/frontend/src/components/intComponent/index.tsx index 56fec347e..633d7bd8d 100644 --- a/src/frontend/src/components/intComponent/index.tsx +++ b/src/frontend/src/components/intComponent/index.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { FloatComponentType } from "../../types/components"; +import { IntComponentType } from "../../types/components"; import { handleKeyDown } from "../../utils/reactflowUtils"; import { Input } from "../ui/input"; @@ -9,7 +9,7 @@ export default function IntComponent({ disabled, editNode = false, id = "", -}: FloatComponentType): JSX.Element { +}: IntComponentType): JSX.Element { const min = 0; // Clear component state diff --git a/src/frontend/src/modals/EditNodeModal/index.tsx b/src/frontend/src/modals/EditNodeModal/index.tsx index e48816f5e..325fce195 100644 --- a/src/frontend/src/modals/EditNodeModal/index.tsx +++ b/src/frontend/src/modals/EditNodeModal/index.tsx @@ -360,6 +360,10 @@ const EditNodeModal = forwardRef( void; + editNode?: boolean; + id?: string; +}; + export type FloatComponentType = { value: string; disabled?: boolean; onChange: (value: string) => void; + rangeSpec: RangeSpecType; editNode?: boolean; id?: string; }; diff --git a/tests/test_llms_template.py b/tests/test_llms_template.py index 352056de7..30a15c932 100644 --- a/tests/test_llms_template.py +++ b/tests/test_llms_template.py @@ -88,6 +88,7 @@ def test_openai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["max_tokens"] == { @@ -118,6 +119,7 @@ def test_openai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["frequency_penalty"] == { @@ -133,6 +135,7 @@ def test_openai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["presence_penalty"] == { @@ -148,6 +151,7 @@ def test_openai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["n"] == { @@ -237,6 +241,7 @@ def test_openai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["logit_bias"] == { @@ -359,6 +364,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["model_kwargs"] == { @@ -403,6 +409,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers): "list": False, "advanced": False, "info": "", + "rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1}, "fileTypes": [], } assert template["max_retries"] == {