fix: deepcopy component output to avoid overriding previous outputs (#3698)

* Fixes copy of component outputs

* Fix issue with copying outputs in custom components

- Update loop to iterate over `_outputs.values()` instead of `outputs` to ensure correct attribute setting.

* Refactor output handling in custom component to use dictionary values

- Updated methods to iterate over `_outputs.values()` instead of `outputs`.
- Ensured consistent access to output values across various methods.
- Fixed potential issues with output value resetting and state model building.

* Fix incorrect attribute reference in set_output_value method

* Add missing import for CustomComponent in test_custom_component.py

* Refactor `test_read_flow` to generate unique flow names for each test run

* Refactor: Rename `_outputs` to `_outputs_map` in custom component

- Updated all references of `_outputs` to `_outputs_map` in `component.py` to improve clarity and consistency.
- Adjusted related methods and attributes in `base.py` to align with the new naming convention.

* Fix output handling in custom component

- Moved `_reset_all_output_values` call to ensure outputs are reset after initialization.
- Updated attribute access to use `_outputs_map` instead of `_outputs` for consistency.

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
Jordan Frazier 2024-09-06 05:23:13 -07:00 • committed by GitHub
commit 3643d962b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 28 additions and 24 deletions

View file

@ -42,7 +42,6 @@ class Component(CustomComponent):
def __init__(self, **kwargs): def __init__(self, **kwargs):
# if key starts with _ it is a config # if key starts with _ it is a config
# else it is an input # else it is an input
self._reset_all_output_values()
inputs = {} inputs = {}
config = {} config = {}
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -53,7 +52,7 @@ class Component(CustomComponent):
else: else:
inputs[key] = value inputs[key] = value
self._inputs: dict[str, "InputTypes"] = {} self._inputs: dict[str, "InputTypes"] = {}
self._outputs: dict[str, Output] = {} self._outputs_map: dict[str, Output] = {}
self._results: dict[str, Any] = {} self._results: dict[str, Any] = {}
self._attributes: dict[str, Any] = {} self._attributes: dict[str, Any] = {}
self._parameters = inputs or {} self._parameters = inputs or {}
@ -69,6 +68,7 @@ class Component(CustomComponent):
config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"}
self.__inputs = inputs self.__inputs = inputs
self.__config = config self.__config = config
self._reset_all_output_values()
super().__init__(**config) super().__init__(**config)
if hasattr(self, "_trace_type"): if hasattr(self, "_trace_type"):
self.trace_type = self._trace_type self.trace_type = self._trace_type
@ -86,8 +86,9 @@ class Component(CustomComponent):
self._event_manager = event_manager self._event_manager = event_manager
def _reset_all_output_values(self): def _reset_all_output_values(self):
for output in self.outputs: if isinstance(self._outputs_map, dict):
setattr(output, "value", UNDEFINED) for output in self._outputs_map.values():
setattr(output, "value", UNDEFINED)
def _build_state_model(self): def _build_state_model(self):
if self._state_model: if self._state_model:
@ -95,7 +96,7 @@ class Component(CustomComponent):
name = self.name or self.__class__.__name__ name = self.name or self.__class__.__name__
model_name = f"{name}StateModel" model_name = f"{name}StateModel"
fields = {} fields = {}
for output in self.outputs: for output in self._outputs_map.values():
fields[output.name] = getattr(self, output.method) fields[output.name] = getattr(self, output.method)
self._state_model = create_state_model(model_name=model_name, **fields) self._state_model = create_state_model(model_name=model_name, **fields)
return self._state_model return self._state_model
@ -116,7 +117,7 @@ class Component(CustomComponent):
kwargs["inputs"] = deepcopy(self.__inputs) kwargs["inputs"] = deepcopy(self.__inputs)
new_component = type(self)(**kwargs) new_component = type(self)(**kwargs)
new_component._code = self._code new_component._code = self._code
new_component._outputs = self._outputs new_component._outputs_map = self._outputs_map
new_component._inputs = self._inputs new_component._inputs = self._inputs
new_component._edges = self._edges new_component._edges = self._edges
new_component._components = self._components new_component._components = self._components
@ -167,7 +168,7 @@ class Component(CustomComponent):
""" """
Returns a list of output names. 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): async def run(self):
""" """
@ -220,8 +221,8 @@ class Component(CustomComponent):
Raises: Raises:
ValueError: If the output with the specified name is not found. ValueError: If the output with the specified name is not found.
""" """
if name in self._outputs: if name in self._outputs_map:
return self._outputs[name] return self._outputs_map[name]
raise ValueError(f"Output {name} not found in {self.__class__.__name__}") raise ValueError(f"Output {name} not found in {self.__class__.__name__}")
def set_on_output(self, name: str, **kwargs): def set_on_output(self, name: str, **kwargs):
@ -232,8 +233,8 @@ class Component(CustomComponent):
setattr(output, key, value) setattr(output, key, value)
def set_output_value(self, name: str, value: Any): def set_output_value(self, name: str, value: Any):
if name in self._outputs: if name in self._outputs_map:
self._outputs[name].value = value self._outputs_map[name].value = value
else: else:
raise ValueError(f"Output {name} not found in {self.__class__.__name__}") raise ValueError(f"Output {name} not found in {self.__class__.__name__}")
@ -250,11 +251,12 @@ class Component(CustomComponent):
Returns: Returns:
None None
""" """
self.outputs = outputs
for output in outputs: for output in outputs:
if output.name is None: if output.name is None:
raise ValueError("Output name cannot be 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"]): def map_inputs(self, inputs: list["InputTypes"]):
""" """
@ -267,7 +269,6 @@ class Component(CustomComponent):
ValueError: If the input name is None. ValueError: If the input name is None.
""" """
self.inputs = inputs
for input_ in inputs: for input_ in inputs:
if input_.name is None: if input_.name is None:
raise ValueError("Input name cannot be None.") raise ValueError("Input name cannot be None.")
@ -288,7 +289,7 @@ class Component(CustomComponent):
self._validate_outputs() self._validate_outputs()
def _set_output_types(self): 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) return_types = self._get_method_return_type(output.method)
output.add_types(return_types) output.add_types(return_types)
output.set_selected() output.set_selected()
@ -296,7 +297,7 @@ class Component(CustomComponent):
def get_output_by_method(self, method: Callable): def get_output_by_method(self, method: Callable):
# method is a callable and output.method is a string # method is a callable and output.method is a string
# we need to find the output that has the same method # 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: if output is None:
method_name = method.__name__ if hasattr(method, "__name__") else str(method) method_name = method.__name__ if hasattr(method, "__name__") else str(method)
raise ValueError(f"Output with method {method_name} not found") 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"): def _find_matching_output_method(self, value: "Component"):
# get all outputs of the 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 # check if the any of the types in the output.types matches ONLY one input in the current component
matching_pairs = [] matching_pairs = []
for output in outputs: for output in outputs:
@ -442,8 +443,8 @@ class Component(CustomComponent):
return self.__dict__["_attributes"][name] return self.__dict__["_attributes"][name]
if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]:
return self.__dict__["_inputs"][name].value return self.__dict__["_inputs"][name].value
if "_outputs" in self.__dict__ and name in self.__dict__["_outputs"]: if "_outputs_map" in self.__dict__ and name in self.__dict__["_outputs_map"]:
return self.__dict__["_outputs"][name] return self.__dict__["_outputs_map"][name]
if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: if name in BACKWARDS_COMPATIBLE_ATTRIBUTES:
return self.__dict__[f"_{name}"] return self.__dict__[f"_{name}"]
if name.startswith("_") and name[1:] in BACKWARDS_COMPATIBLE_ATTRIBUTES: 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] self.outputs = [Output(**output) for output in outputs]
for output in self.outputs: for output in self.outputs:
setattr(self, output.name, output) setattr(self, output.name, output)
self._outputs[output.name] = output self._outputs_map[output.name] = output
def get_trace_as_inputs(self): def get_trace_as_inputs(self):
predefined_inputs = { predefined_inputs = {
@ -620,7 +621,7 @@ class Component(CustomComponent):
_results = {} _results = {}
_artifacts = {} _artifacts = {}
if hasattr(self, "outputs"): 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 # Build the output if it's connected to some other vertex
# or if it's not connected to any vertex # or if it's not connected to any vertex
if ( if (

View file

@ -319,7 +319,7 @@ class Graph:
for vertex in self.vertices: for vertex in self.vertices:
if vertex._custom_component is None: if vertex._custom_component is None:
continue continue
for output in vertex._custom_component.outputs: for output in vertex._custom_component._outputs_map.values():
for key, value in config["output"].items(): for key, value in config["output"].items():
setattr(output, key, value) setattr(output, key, value)

View file

@ -178,6 +178,7 @@ class Output(BaseModel):
"""The method to use for the output.""" """The method to use for the output."""
value: Any | None = Field(default=UNDEFINED) value: Any | None = Field(default=UNDEFINED)
"""The result of the Output. Dynamically updated as execution occurs."""
cache: bool = Field(default=True) cache: bool = Field(default=True)

View file

@ -400,6 +400,7 @@ def test_custom_component_get_function_entrypoint_args_no_args():
the CustomComponent class with a build method with no arguments. the CustomComponent class with a build method with no arguments.
""" """
my_code = """ my_code = """
from langflow.custom import CustomComponent
class MyMainClass(CustomComponent): class MyMainClass(CustomComponent):
def build(): def build():
pass""" pass"""

View file

@ -73,10 +73,11 @@ def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_h
assert len(response.json()) > 0 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) flow = orjson.loads(json_flow)
data = flow["data"] 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) 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 flow_id = response.json()["id"] # flow_id should be a UUID but is a string
# turn it into a UUID # turn it into a UUID