diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index 2b63b8ad8..fa0c45e7b 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -1,7 +1,6 @@ import operator import re -from typing import Any, ClassVar -from uuid import UUID +from typing import TYPE_CHECKING, Any, ClassVar from cachetools import TTLCache, cachedmethod from fastapi import HTTPException @@ -12,6 +11,9 @@ from langflow.custom.code_parser import CodeParser from langflow.custom.eval import eval_custom_component_code from langflow.utils import validate +if TYPE_CHECKING: + from uuid import UUID + class ComponentCodeNullError(HTTPException): pass @@ -25,15 +27,15 @@ class BaseComponent: ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided." ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided." - _code: str | None = None - """The code of the component. Defaults to None.""" - _function_entrypoint_name: str = "build" - field_config: dict = {} - _user_id: str | UUID | None = None - _template_config: dict = {} - def __init__(self, **data) -> None: + self._code: str | None = None + self._function_entrypoint_name: str = "build" + self.field_config: dict = {} + self._user_id: str | UUID | None = None + self._template_config: dict = {} + self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60) + for key, value in data.items(): if key == "user_id": self._user_id = value @@ -41,8 +43,12 @@ class BaseComponent: setattr(self, key, value) def __setattr__(self, key, value) -> None: - if key == "_user_id" and self._user_id is not None: - logger.warning("user_id is immutable and cannot be changed.") + if key == "_user_id": + try: + if self._user_id is not None: + logger.warning("user_id is immutable and cannot be changed.") + except (KeyError, AttributeError): + pass super().__setattr__(key, value) @cachedmethod(cache=operator.attrgetter("cache")) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index a25a4ef85..4cb0bbb8d 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -93,16 +93,27 @@ class Component(CustomComponent): inputs: list[InputTypes] = [] outputs: list[Output] = [] code_class_base_inheritance: ClassVar[str] = "Component" - _output_logs: dict[str, list[Log]] = {} - _current_output: str = "" - _metadata: dict = {} - _ctx: dict = {} - _code: str | None = None - _logs: list[Log] = [] def __init__(self, **kwargs) -> None: - # if key starts with _ it is a config - # else it is an input + # Initialize instance-specific attributes first + self._output_logs: dict[str, list[Log]] = {} + self._current_output: str = "" + self._metadata: dict = {} + self._ctx: dict = {} + self._code: str | None = None + self._logs: list[Log] = [] + + # Initialize component-specific collections + self._inputs: dict[str, InputTypes] = {} + self._outputs_map: dict[str, Output] = {} + self._results: dict[str, Any] = {} + self._attributes: dict[str, Any] = {} + self._edges: list[EdgeData] = [] + self._components: list[Component] = [] + self._event_manager: EventManager | None = None + self._state_model = None + + # Process input kwargs inputs = {} config = {} for key, value in kwargs.items(): @@ -112,34 +123,35 @@ class Component(CustomComponent): config[key[1:]] = value else: inputs[key] = value - self._inputs: dict[str, InputTypes] = {} - self._outputs_map: dict[str, Output] = {} - self._results: dict[str, Any] = {} - self._attributes: dict[str, Any] = {} + self._parameters = inputs or {} - self._edges: list[EdgeData] = [] - self._components: list[Component] = [] - self._current_output = "" - self._event_manager: EventManager | None = None - self._state_model = None self.set_attributes(self._parameters) - self._output_logs = {} - config = config or {} - if "_id" not in config: - config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} + + # Store original inputs and config for reference self.__inputs = inputs - self.__config = config - self._reset_all_output_values() - super().__init__(**config) + self.__config = config or {} + + # Add unique ID if not provided + if "_id" not in self.__config: + self.__config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} + + # Initialize base class + super().__init__(**self.__config) + + # Post-initialization setup if hasattr(self, "_trace_type"): self.trace_type = self._trace_type if not hasattr(self, "trace_type"): self.trace_type = "chain" + + # Setup inputs and outputs + self._reset_all_output_values() if self.inputs is not None: self.map_inputs(self.inputs) if self.outputs is not None: self.map_outputs(self.outputs) - # Set output types + + # Final setup self._set_output_types(list(self._outputs_map.values())) self.set_class_code() self._set_output_required_inputs() diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index 9e30d5a7d..fed586fb0 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -50,6 +50,9 @@ class CustomComponent(BaseComponent): _tree (Optional[dict]): The code tree of the custom component. """ + # True constants that should be shared (using ClassVar) + _code_class_base_inheritance: ClassVar[str] = "CustomComponent" + function_entrypoint_name: ClassVar[str] = "build" name: str | None = None """The name of the component used to styles. Defaults to None.""" display_name: str | None = None @@ -58,36 +61,6 @@ class CustomComponent(BaseComponent): """The description of the component. Defaults to None.""" icon: str | None = None """The icon of the component. It should be an emoji. Defaults to None.""" - is_input: bool | None = None - """The input state of the component. Defaults to None. - If True, the component must have a field named 'input_value'.""" - add_tool_output: bool | None = False - """Indicates whether the component will be treated as a tool. Defaults to False.""" - is_output: bool | None = None - """The output state of the component. Defaults to None. - If True, the component must have a field named 'input_value'.""" - field_config: dict = {} - """The field configuration of the component. Defaults to an empty dictionary.""" - field_order: list[str] | None = None - """The field order of the component. Defaults to an empty list.""" - frozen: bool | None = False - """The default frozen state of the component. Defaults to False.""" - build_parameters: dict | None = None - """The build parameters of the component. Defaults to None.""" - _vertex: Vertex | None = None - """The edge target parameter of the component. Defaults to None.""" - _code_class_base_inheritance: ClassVar[str] = "CustomComponent" - function_entrypoint_name: ClassVar[str] = "build" - function: Callable | None = None - repr_value: Any | None = "" - status: Any | None = None - """The status of the component. This is displayed on the frontend. Defaults to None.""" - _flows_data: list[Data] | None = None - _outputs: list[OutputValue] = [] - _logs: list[Log] = [] - _output_logs: dict[str, list[Log] | Log] = {} - _tracing_service: TracingService | None = None - _tree: dict | None = None def __init__(self, **data) -> None: """Initializes a new instance of the CustomComponent class. @@ -95,10 +68,33 @@ class CustomComponent(BaseComponent): Args: **data: Additional keyword arguments to initialize the custom component. """ - self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60) + # Initialize instance-specific attributes first + self.is_input: bool | None = None + self.is_output: bool | None = None + self.add_tool_output: bool = False + self.field_config: dict = {} + self.field_order: list[str] | None = None + self.frozen: bool = False + self.build_parameters: dict | None = None + self._vertex: Vertex | None = None + self.function: Callable | None = None + self.repr_value: Any = "" + self.status: Any | None = None + + # Initialize collections with empty defaults + self._flows_data: list[Data] | None = None + self._outputs: list[OutputValue] = [] self._logs: list[Log] = [] + self._output_logs: dict[str, list[Log] | Log] = {} + self._tracing_service: TracingService | None = None + self._tree: dict | None = None + + # Initialize additional instance state + self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60) self._results: dict = {} self._artifacts: dict = {} + + # Call parent's init after setting up our attributes super().__init__(**data) def set_attributes(self, parameters: dict) -> None: diff --git a/src/backend/tests/unit/custom/component/test_component_instance_attributes.py b/src/backend/tests/unit/custom/component/test_component_instance_attributes.py new file mode 100644 index 000000000..0dcbc5a18 --- /dev/null +++ b/src/backend/tests/unit/custom/component/test_component_instance_attributes.py @@ -0,0 +1,118 @@ +import pytest +from langflow.components.inputs.chat import ChatInput +from langflow.schema.message import Message + + +@pytest.fixture +def chat_input_instances(): + """Create two instances of ChatInput for testing.""" + chat1 = ChatInput() + chat2 = ChatInput() + return chat1, chat2 + + +def test_input_value_independence(chat_input_instances): + """Test that input_value is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different input values + chat1.build(input_value="Hello from chat1") + chat2.build(input_value="Hello from chat2") + + # Verify values are different + assert chat1.input_value != chat2.input_value + assert chat1.input_value == "Hello from chat1" + assert chat2.input_value == "Hello from chat2" + + +def test_sender_name_independence(chat_input_instances): + """Test that sender_name is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different sender names + chat1.build(sender_name="Alice") + chat2.build(sender_name="Bob") + + # Verify values are different + assert chat1.sender_name != chat2.sender_name + assert chat1.sender_name == "Alice" + assert chat2.sender_name == "Bob" + + +def test_multiple_attributes_independence(chat_input_instances): + """Test that multiple attributes are independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set multiple attributes for chat1 + chat1.build(input_value="Message 1", sender_name="Alice", background_color="blue", text_color="white") + + # Set different attributes for chat2 + chat2.build(input_value="Message 2", sender_name="Bob", background_color="red", text_color="black") + + # Verify all attributes are independent + assert chat1.input_value != chat2.input_value + assert chat1.sender_name != chat2.sender_name + assert chat1.background_color != chat2.background_color + assert chat1.text_color != chat2.text_color + + +async def test_message_output_independence(chat_input_instances): + """Test that message outputs are independent between instances.""" + chat1, chat2 = chat_input_instances + + # Configure different messages + chat1.build( + input_value="Hello from chat1", + sender_name="Alice", + should_store_message=False, # Prevent actual message storage + ) + chat2.build( + input_value="Hello from chat2", + sender_name="Bob", + should_store_message=False, # Prevent actual message storage + ) + + # Get messages from both instances + message1 = await chat1.message_response() + message2 = await chat2.message_response() + + # Verify messages are different + assert isinstance(message1, Message) + assert isinstance(message2, Message) + assert message1.text != message2.text + assert message1.sender_name != message2.sender_name + + +async def test_status_independence(chat_input_instances): + """Test that status attribute is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Configure and run messages + chat1.build(input_value="Status test 1", sender_name="Alice", should_store_message=False) + chat2.build(input_value="Status test 2", sender_name="Bob", should_store_message=False) + + # Generate messages to update status + await chat1.message_response() + await chat2.message_response() + + # Verify status values are different + assert chat1.status != chat2.status + assert chat1.status.text == "Status test 1" + assert chat2.status.text == "Status test 2" + + +def test_files_independence(chat_input_instances): + """Test that files attribute is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different files + files1 = ["file1.txt", "file2.txt"] + files2 = ["file3.txt", "file4.txt"] + + chat1.build(files=files1) + chat2.build(files=files2) + + # Verify files are independent + assert chat1.files != chat2.files + assert chat1.files == files1 + assert chat2.files == files2