diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 354fcffb4..814bdb3b9 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -42,7 +42,6 @@ class Component(CustomComponent): def __init__(self, **kwargs): # if key starts with _ it is a config # else it is an input - self._reset_all_output_values() inputs = {} config = {} for key, value in kwargs.items(): @@ -53,7 +52,7 @@ class Component(CustomComponent): else: inputs[key] = value self._inputs: dict[str, "InputTypes"] = {} - self._outputs: dict[str, Output] = {} + self._outputs_map: dict[str, Output] = {} self._results: dict[str, Any] = {} self._attributes: dict[str, Any] = {} self._parameters = inputs or {} @@ -69,6 +68,7 @@ class Component(CustomComponent): config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} self.__inputs = inputs self.__config = config + self._reset_all_output_values() super().__init__(**config) if hasattr(self, "_trace_type"): self.trace_type = self._trace_type @@ -86,8 +86,9 @@ class Component(CustomComponent): self._event_manager = event_manager def _reset_all_output_values(self): - for output in self.outputs: - setattr(output, "value", UNDEFINED) + if isinstance(self._outputs_map, dict): + for output in self._outputs_map.values(): + setattr(output, "value", UNDEFINED) def _build_state_model(self): if self._state_model: @@ -95,7 +96,7 @@ class Component(CustomComponent): name = self.name or self.__class__.__name__ model_name = f"{name}StateModel" fields = {} - for output in self.outputs: + for output in self._outputs_map.values(): fields[output.name] = getattr(self, output.method) self._state_model = create_state_model(model_name=model_name, **fields) return self._state_model @@ -116,7 +117,7 @@ class Component(CustomComponent): kwargs["inputs"] = deepcopy(self.__inputs) new_component = type(self)(**kwargs) new_component._code = self._code - new_component._outputs = self._outputs + new_component._outputs_map = self._outputs_map new_component._inputs = self._inputs new_component._edges = self._edges new_component._components = self._components @@ -167,7 +168,7 @@ class Component(CustomComponent): """ Returns a list of output names. """ - return [_output.name for _output in self.outputs] + return [_output.name for _output in self._outputs_map.values()] async def run(self): """ @@ -220,8 +221,8 @@ class Component(CustomComponent): Raises: ValueError: If the output with the specified name is not found. """ - if name in self._outputs: - return self._outputs[name] + if name in self._outputs_map: + return self._outputs_map[name] raise ValueError(f"Output {name} not found in {self.__class__.__name__}") def set_on_output(self, name: str, **kwargs): @@ -232,8 +233,8 @@ class Component(CustomComponent): setattr(output, key, value) def set_output_value(self, name: str, value: Any): - if name in self._outputs: - self._outputs[name].value = value + if name in self._outputs_map: + self._outputs_map[name].value = value else: raise ValueError(f"Output {name} not found in {self.__class__.__name__}") @@ -250,11 +251,12 @@ class Component(CustomComponent): Returns: None """ - self.outputs = outputs for output in outputs: if output.name is None: raise ValueError("Output name cannot be None.") - self._outputs[output.name] = output + # Deepcopy is required to avoid modifying the original component; + # allows each instance of each component to modify its own output + self._outputs_map[output.name] = deepcopy(output) def map_inputs(self, inputs: list["InputTypes"]): """ @@ -267,7 +269,6 @@ class Component(CustomComponent): ValueError: If the input name is None. """ - self.inputs = inputs for input_ in inputs: if input_.name is None: raise ValueError("Input name cannot be None.") @@ -288,7 +289,7 @@ class Component(CustomComponent): self._validate_outputs() def _set_output_types(self): - for output in self.outputs: + for output in self._outputs_map.values(): return_types = self._get_method_return_type(output.method) output.add_types(return_types) output.set_selected() @@ -296,7 +297,7 @@ class Component(CustomComponent): def get_output_by_method(self, method: Callable): # method is a callable and output.method is a string # we need to find the output that has the same method - output = next((output for output in self.outputs if output.method == method.__name__), None) + output = next((output for output in self._outputs_map.values() if output.method == method.__name__), None) if output is None: method_name = method.__name__ if hasattr(method, "__name__") else str(method) raise ValueError(f"Output with method {method_name} not found") @@ -326,7 +327,7 @@ class Component(CustomComponent): def _find_matching_output_method(self, value: "Component"): # get all outputs of the value component - outputs = value.outputs + outputs = value._outputs_map.values() # check if the any of the types in the output.types matches ONLY one input in the current component matching_pairs = [] for output in outputs: @@ -442,8 +443,8 @@ class Component(CustomComponent): return self.__dict__["_attributes"][name] if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: return self.__dict__["_inputs"][name].value - if "_outputs" in self.__dict__ and name in self.__dict__["_outputs"]: - return self.__dict__["_outputs"][name] + if "_outputs_map" in self.__dict__ and name in self.__dict__["_outputs_map"]: + return self.__dict__["_outputs_map"][name] if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: return self.__dict__[f"_{name}"] if name.startswith("_") and name[1:] in BACKWARDS_COMPATIBLE_ATTRIBUTES: @@ -578,7 +579,7 @@ class Component(CustomComponent): self.outputs = [Output(**output) for output in outputs] for output in self.outputs: setattr(self, output.name, output) - self._outputs[output.name] = output + self._outputs_map[output.name] = output def get_trace_as_inputs(self): predefined_inputs = { @@ -620,7 +621,7 @@ class Component(CustomComponent): _results = {} _artifacts = {} if hasattr(self, "outputs"): - for output in self.outputs: + for output in self._outputs_map.values(): # Build the output if it's connected to some other vertex # or if it's not connected to any vertex if ( diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 546a6f82f..1066e7e53 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -319,7 +319,7 @@ class Graph: for vertex in self.vertices: if vertex._custom_component is None: continue - for output in vertex._custom_component.outputs: + for output in vertex._custom_component._outputs_map.values(): for key, value in config["output"].items(): setattr(output, key, value) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index 5686dcfe8..569183a44 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -178,6 +178,7 @@ class Output(BaseModel): """The method to use for the output.""" value: Any | None = Field(default=UNDEFINED) + """The result of the Output. Dynamically updated as execution occurs.""" cache: bool = Field(default=True) diff --git a/src/backend/tests/unit/test_custom_component.py b/src/backend/tests/unit/test_custom_component.py index 71b9d5d34..a43cc4020 100644 --- a/src/backend/tests/unit/test_custom_component.py +++ b/src/backend/tests/unit/test_custom_component.py @@ -400,6 +400,7 @@ def test_custom_component_get_function_entrypoint_args_no_args(): the CustomComponent class with a build method with no arguments. """ my_code = """ +from langflow.custom import CustomComponent class MyMainClass(CustomComponent): def build(): pass""" diff --git a/src/backend/tests/unit/test_database.py b/src/backend/tests/unit/test_database.py index f3f10bed4..829b00e4d 100644 --- a/src/backend/tests/unit/test_database.py +++ b/src/backend/tests/unit/test_database.py @@ -73,10 +73,11 @@ def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_h assert len(response.json()) > 0 -def test_read_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): +def test_read_flow(client: TestClient, json_flow: str, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] - flow = FlowCreate(name="Test Flow", description="description", data=data) + unique_name = str(uuid4()) + flow = FlowCreate(name=unique_name, description="description", data=data) response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] # flow_id should be a UUID but is a string # turn it into a UUID