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):
# 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 (

View file

@ -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)

View file

@ -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)

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.
"""
my_code = """
from langflow.custom import CustomComponent
class MyMainClass(CustomComponent):
def build():
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
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