diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index f41c5f845..382c40cc4 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -56,7 +56,7 @@ The CustomComponent class serves as the foundation for creating custom component - **build_config**: Used to define the configuration fields of the component (if applicable). It should always return a dictionary with specific keys representing the field names and corresponding configurations. This method is called when the code is processed (i.e., when you click _Check and Save_ in the code editor). It must follow the format described below: - Top-level keys are field names. - - Their values are also of type _`dict`_. They specify the behavior of the generated fields. + - Their values are can be of type _`langflow.field_typing.TemplateField`_ or _`dict`_. They specify the behavior of the generated fields. Below are the available keys used to configure component fields: diff --git a/src/backend/langflow/field_typing/__init__.py b/src/backend/langflow/field_typing/__init__.py index f25f1f840..6fc932e6f 100644 --- a/src/backend/langflow/field_typing/__init__.py +++ b/src/backend/langflow/field_typing/__init__.py @@ -1,3 +1,5 @@ +from langflow.template.field.base import TemplateField + from .constants import ( AgentExecutor, BaseChatMemory, @@ -48,4 +50,5 @@ __all__ = [ "ChatPromptTemplate", "Prompt", "RangeSpec", + "TemplateField", ] diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 8718579db..41c6ed350 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -129,7 +129,7 @@ def add_new_custom_field( advanced=field_advanced, placeholder=placeholder, display_name=display_name, - **field_config, + **sanitize_field_config(field_config), ) template.get("template")[field_name] = new_field.model_dump(by_alias=True, exclude_none=True) template.get("custom_fields")[field_name] = None @@ -137,6 +137,13 @@ def add_new_custom_field( return template +def sanitize_field_config(field_config: Dict): + # If any of the already existing keys are in field_config, remove them + for key in ["name", "field_type", "value", "required", "placeholder", "display_name", "advanced", "show"]: + field_config.pop(key, None) + return field_config + + # TODO: Move to correct place def add_code_field(template, raw_code, field_config): # Field with the Python code to allow update @@ -224,7 +231,10 @@ def build_field_config( try: build_config: Dict = custom_class(user_id=user_id).build_config() - for field_name, field_dict in build_config.items(): + for field_name, field in build_config.items(): + # Allow user to build TemplateField as well + # as a dict with the same keys as TemplateField + field_dict = get_field_dict(field) if update_field is not None and field_name != update_field: continue try: @@ -246,6 +256,13 @@ def build_field_config( ) from exc +def get_field_dict(field): + """Get the field dictionary from a TemplateField or a dict""" + if isinstance(field, TemplateField): + return field.model_dump(by_alias=True, exclude_none=True) + return field + + def update_field_dict(field_dict): """Update the field dictionary by calling options() or value() if they are callable""" if "options" in field_dict and callable(field_dict["options"]): diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index c8e1235cc..7571f5209 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -1,4 +1,5 @@ -from typing import Any, Optional +from abc import ABC +from typing import Any, Callable, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_serializer @@ -37,7 +38,7 @@ class TemplateField(BaseModel): password: bool = False """Specifies if the field is a password. Defaults to False.""" - options: Optional[list[str]] = None + options: Union[list[str], Callable] = [] """List of options for the field. Only used when is_list=True. Default is an empty list.""" name: str = "" diff --git a/tests/conftest.py b/tests/conftest.py index ce279162f..fdc896266 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -308,3 +308,11 @@ def test_component_code(): # load the content as a string with open(path, "r") as f: return f.read() + + +@pytest.fixture +def test_component_with_templatefield_code(): + path = Path(__file__).parent.absolute() / "data" / "component_with_templatefield.py" + # load the content as a string + with open(path, "r") as f: + return f.read() diff --git a/tests/data/component_with_templatefield.py b/tests/data/component_with_templatefield.py new file mode 100644 index 000000000..6b2ce011b --- /dev/null +++ b/tests/data/component_with_templatefield.py @@ -0,0 +1,17 @@ +import random + +from langflow import CustomComponent +from langflow.field_typing import TemplateField + + +class TestComponent(CustomComponent): + def refresh_values(self): + # This is a function that will be called every time the component is updated + # and should return a list of random strings + return [f"Random {random.randint(1, 100)}" for _ in range(5)] + + def build_config(self): + return {"param": TemplateField(display_name="Param", options=self.refresh_values)} + + def build(self, param: int): + return param diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index 3be85ddd2..007374763 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -549,3 +549,17 @@ def test_build_langchain_template_custom_component_valid_code(test_component_cod frontend_node = build_custom_component_template(component, update_field="param") new_param_options = frontend_node["template"]["param"]["options"] assert param_options != new_param_options + + +def test_build_langchain_template_custom_component_templatefield(test_component_with_templatefield_code): + component = create_and_validate_component(test_component_with_templatefield_code) + frontend_node = build_custom_component_template(component) + assert isinstance(frontend_node, dict) + template = frontend_node["template"] + assert isinstance(template, dict) + assert "param" in template + param_options = template["param"]["options"] + # Now run it again with an update field + frontend_node = build_custom_component_template(component, update_field="param") + new_param_options = frontend_node["template"]["param"]["options"] + assert param_options != new_param_options