diff --git a/src/backend/base/langflow/components/prototypes/SubFlow.py b/src/backend/base/langflow/components/prototypes/SubFlow.py index f3f0b4623..009822fee 100644 --- a/src/backend/base/langflow/components/prototypes/SubFlow.py +++ b/src/backend/base/langflow/components/prototypes/SubFlow.py @@ -1,29 +1,25 @@ from typing import Any, List, Optional -from langflow.base.flow_processing.utils import build_data_from_result_data -from langflow.custom import CustomComponent -from langflow.graph.graph.base import Graph -from langflow.graph.schema import RunOutputs -from langflow.graph.vertex.base import Vertex -from langflow.helpers.flow import get_flow_inputs -from langflow.schema import Data -from langflow.schema.dotdict import dotdict -from langflow.template.field.base import Input from loguru import logger +from langflow.base.flow_processing.utils import build_data_from_result_data +from langflow.custom import Component +from langflow.graph.graph.base import Graph +from langflow.graph.vertex.base import Vertex +from langflow.helpers.flow import get_flow_inputs +from langflow.io import DropdownInput, Output +from langflow.schema import Data, dotdict -class SubFlowComponent(CustomComponent): + +class SubFlowComponent(Component): display_name = "Sub Flow" - description = ( - "Dynamically Generates a Component from a Flow. The output is a list of data with keys 'result' and 'message'." - ) + description = "Generates a Component from a Flow, with all of its inputs, and " name = "SubFlow" beta: bool = True - field_order = ["flow_name"] def get_flow_names(self) -> List[str]: - flow_datas = self.list_flows() - return [flow_data.data["name"] for flow_data in flow_datas] + flow_data = self.list_flows() + return [flow_data.data["name"] for flow_data in flow_data] def get_flow(self, flow_name: str) -> Optional[Data]: flow_datas = self.list_flows() @@ -33,12 +29,11 @@ class SubFlowComponent(CustomComponent): return None def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): - logger.debug(f"Updating build config with field value {field_value} and field name {field_name}") if field_name == "flow_name": build_config["flow_name"]["options"] = self.get_flow_names() - # Clean up the build config + for key in list(build_config.keys()): - if key not in self.field_order + ["code", "_type", "get_final_results_only"]: + if key not in [x.name for x in self.inputs] + ["code", "_type", "get_final_results_only"]: del build_config[key] if field_value is not None and field_name == "flow_name": try: @@ -55,62 +50,58 @@ class SubFlowComponent(CustomComponent): return build_config - def add_inputs_to_build_config(self, inputs: List[Vertex], build_config: dotdict): - new_fields: list[Input] = [] - for vertex in inputs: - field = Input( - display_name=vertex.display_name, - name=vertex.id, - info=vertex.description, - field_type="str", - value=None, - ) - new_fields.append(field) - logger.debug(new_fields) + def add_inputs_to_build_config(self, inputs_vertex: List[Vertex], build_config: dotdict): + new_fields: list[dotdict] = [] + + for vertex in inputs_vertex: + new_vertex_inputs = [] + field_template = vertex.data["node"]["template"] + for inp in field_template.keys(): + if inp not in ["code", "_type"]: + field_template[inp]["display_name"] = ( + vertex.display_name + " - " + field_template[inp]["display_name"] + ) + field_template[inp]["name"] = vertex.id + "|" + inp + new_vertex_inputs.append(field_template[inp]) + new_fields += new_vertex_inputs for field in new_fields: - build_config[field.name] = field.to_dict() + build_config[field["name"]] = field return build_config - def build_config(self): - return { - "input_value": { - "display_name": "Input Value", - "multiline": True, - }, - "flow_name": { - "display_name": "Flow Name", - "info": "The name of the flow to run.", - "options": [], - "real_time_refresh": True, - "refresh_button": True, - }, - "tweaks": { - "display_name": "Tweaks", - "info": "Tweaks to apply to the flow.", - }, - "get_final_results_only": { - "display_name": "Get Final Results Only", - "info": "If False, the output will contain all outputs from the flow.", - "advanced": True, - }, - } + inputs = [ + DropdownInput( + name="flow_name", + display_name="Flow Name", + info="The name of the flow to run.", + options=[], + refresh_button=True, + real_time_refresh=True, + ), + ] - async def build(self, flow_name: str, get_final_results_only: bool = True, **kwargs) -> List[Data]: - tweaks = {key: {"input_value": value} for key, value in kwargs.items()} - run_outputs: List[Optional[RunOutputs]] = await self.run_flow( + outputs = [Output(name="flow_outputs", display_name="Flow Outputs", method="generate_results")] + + async def generate_results(self) -> List[Data]: + tweaks: dict = {} + for field in self._attributes.keys(): + if field != "flow_name": + [node, name] = field.split("|") + if node not in tweaks.keys(): + tweaks[node] = {} + tweaks[node][name] = self._attributes[field] + + run_outputs = await self.run_flow( tweaks=tweaks, - flow_name=flow_name, + flow_name=self.flow_name, + output_type="all", ) + data: list[Data] = [] if not run_outputs: - return [] + return data run_output = run_outputs[0] - data = [] if run_output is not None: for output in run_output.outputs: if output: - data.extend(build_data_from_result_data(output, get_final_results_only)) - - self.status = data - logger.debug(data) + data.extend(build_data_from_result_data(output)) return data diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index c4a43d4cf..dcda6b508 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -343,7 +343,10 @@ def load_starter_projects() -> list[tuple[Path, dict]]: starter_projects = [] folder = Path(__file__).parent / "starter_projects" for file in folder.glob("*.json"): - project = orjson.loads(file.read_text(encoding="utf-8")) + try: + project = orjson.loads(file.read_text(encoding="utf-8")) + except orjson.JSONDecodeError as e: + raise ValueError(f"Error loading starter project {file}: {e}") starter_projects.append((file, project)) logger.info(f"Loaded starter project {file}") return starter_projects diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index c8023775f..4d616e175 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -226,9 +226,46 @@ class SecretStrInput(BaseInputMixin, DatabaseLoadMixin): field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD password: CoalesceBool = Field(default=True) - input_types: list[str] = [] + input_types: list[str] = ["Message"] load_from_db: CoalesceBool = True + @field_validator("value") + @classmethod + def validate_value(cls, v: Any, _info): + """ + Validates the given value and returns the processed value. + + Args: + v (Any): The value to be validated. + _info: Additional information about the input. + + Returns: + The processed value. + + Raises: + ValueError: If the value is not of a valid type or if the input is missing a required key. + """ + value: str | AsyncIterator | Iterator | None = None + if isinstance(v, str): + value = v + elif isinstance(v, Message): + value = v.text + elif isinstance(v, Data): + if v.text_key in v.data: + value = v.data[v.text_key] + else: + keys = ", ".join(v.data.keys()) + input_name = _info.data["name"] + raise ValueError( + f"The input to '{input_name}' must contain the key '{v.text_key}'." + f"You can set `text_key` to one of the following keys: {keys} or set the value using another Component." + ) + elif isinstance(v, (AsyncIterator, Iterator)): + value = v + else: + raise ValueError(f"Invalid value type {type(v)}") + return value + class IntInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMixin): """ diff --git a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx index d8b3b772e..51b50332e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx @@ -1,7 +1,5 @@ -import { title } from "process"; import { Handle, Position } from "reactflow"; import ShadTooltip from "../../../../components/shadTooltipComponent"; -import { Button } from "../../../../components/ui/button"; import { isValidConnection, scapedJSONStringfy, @@ -37,10 +35,7 @@ export default function HandleRenderComponent({ testIdComplement?: string; }) { return ( - + ); } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 1fcc009d3..611fd79be 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -349,8 +349,8 @@ export default function ParameterComponent({ testIdComplement={`${data?.type?.toLowerCase()}-shownode`} /> )} -
- {data.node?.template[name] !== undefined && ( + {data.node?.template[name] !== undefined && ( +
- )} -
+
+ )} {openOutputModal && ( { options?.setNodeClass && options.setNodeClass(newNodeClass); diff --git a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx index 8289143f6..796f9b768 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx @@ -29,7 +29,7 @@ export function RefreshParameterComponent({ setErrorData, ); return ( -
+
{children} {templateData.refresh_button && (