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:
parent
1e334335d1
commit
3643d962b9
5 changed files with 28 additions and 24 deletions
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue