fix: flow experimental components (#3093)
* refactor(utils.py): simplify data processing logic in build_data_from_result_data function for better readability and maintainability * feat: set default output type to "chat" in run_flow function * refactor(FlowTool.py): refactor FlowToolComponent class to inherit from LCToolComponent * Fixed Flow as Tool component * Fixed refresh button not appearing at the end * Added way of connecting SecretStrInput to messages * Added real_time_refresh when field has button update too * Refactored SubFlow component * Fixed FlowTool to only output tool and removed async function def * fix: two statements in the same line * [autofix.ci] apply automated fixes * Fixed lint issues * fixed dataobj with wrong name * changed tweaks dict type * Fixed margin appearing in output too * Fixed useless button that made styling worse on handlerendercomponent --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
7276f699fc
commit
5076f2014e
7 changed files with 106 additions and 81 deletions
|
|
@ -1,29 +1,25 @@
|
||||||
from typing import Any, List, Optional
|
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 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"
|
display_name = "Sub Flow"
|
||||||
description = (
|
description = "Generates a Component from a Flow, with all of its inputs, and "
|
||||||
"Dynamically Generates a Component from a Flow. The output is a list of data with keys 'result' and 'message'."
|
|
||||||
)
|
|
||||||
name = "SubFlow"
|
name = "SubFlow"
|
||||||
beta: bool = True
|
beta: bool = True
|
||||||
field_order = ["flow_name"]
|
|
||||||
|
|
||||||
def get_flow_names(self) -> List[str]:
|
def get_flow_names(self) -> List[str]:
|
||||||
flow_datas = self.list_flows()
|
flow_data = self.list_flows()
|
||||||
return [flow_data.data["name"] for flow_data in flow_datas]
|
return [flow_data.data["name"] for flow_data in flow_data]
|
||||||
|
|
||||||
def get_flow(self, flow_name: str) -> Optional[Data]:
|
def get_flow(self, flow_name: str) -> Optional[Data]:
|
||||||
flow_datas = self.list_flows()
|
flow_datas = self.list_flows()
|
||||||
|
|
@ -33,12 +29,11 @@ class SubFlowComponent(CustomComponent):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = 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":
|
if field_name == "flow_name":
|
||||||
build_config["flow_name"]["options"] = self.get_flow_names()
|
build_config["flow_name"]["options"] = self.get_flow_names()
|
||||||
# Clean up the build config
|
|
||||||
for key in list(build_config.keys()):
|
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]
|
del build_config[key]
|
||||||
if field_value is not None and field_name == "flow_name":
|
if field_value is not None and field_name == "flow_name":
|
||||||
try:
|
try:
|
||||||
|
|
@ -55,62 +50,58 @@ class SubFlowComponent(CustomComponent):
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def add_inputs_to_build_config(self, inputs: List[Vertex], build_config: dotdict):
|
def add_inputs_to_build_config(self, inputs_vertex: List[Vertex], build_config: dotdict):
|
||||||
new_fields: list[Input] = []
|
new_fields: list[dotdict] = []
|
||||||
for vertex in inputs:
|
|
||||||
field = Input(
|
for vertex in inputs_vertex:
|
||||||
display_name=vertex.display_name,
|
new_vertex_inputs = []
|
||||||
name=vertex.id,
|
field_template = vertex.data["node"]["template"]
|
||||||
info=vertex.description,
|
for inp in field_template.keys():
|
||||||
field_type="str",
|
if inp not in ["code", "_type"]:
|
||||||
value=None,
|
field_template[inp]["display_name"] = (
|
||||||
)
|
vertex.display_name + " - " + field_template[inp]["display_name"]
|
||||||
new_fields.append(field)
|
)
|
||||||
logger.debug(new_fields)
|
field_template[inp]["name"] = vertex.id + "|" + inp
|
||||||
|
new_vertex_inputs.append(field_template[inp])
|
||||||
|
new_fields += new_vertex_inputs
|
||||||
for field in new_fields:
|
for field in new_fields:
|
||||||
build_config[field.name] = field.to_dict()
|
build_config[field["name"]] = field
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def build_config(self):
|
inputs = [
|
||||||
return {
|
DropdownInput(
|
||||||
"input_value": {
|
name="flow_name",
|
||||||
"display_name": "Input Value",
|
display_name="Flow Name",
|
||||||
"multiline": True,
|
info="The name of the flow to run.",
|
||||||
},
|
options=[],
|
||||||
"flow_name": {
|
refresh_button=True,
|
||||||
"display_name": "Flow Name",
|
real_time_refresh=True,
|
||||||
"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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def build(self, flow_name: str, get_final_results_only: bool = True, **kwargs) -> List[Data]:
|
outputs = [Output(name="flow_outputs", display_name="Flow Outputs", method="generate_results")]
|
||||||
tweaks = {key: {"input_value": value} for key, value in kwargs.items()}
|
|
||||||
run_outputs: List[Optional[RunOutputs]] = await self.run_flow(
|
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,
|
tweaks=tweaks,
|
||||||
flow_name=flow_name,
|
flow_name=self.flow_name,
|
||||||
|
output_type="all",
|
||||||
)
|
)
|
||||||
|
data: list[Data] = []
|
||||||
if not run_outputs:
|
if not run_outputs:
|
||||||
return []
|
return data
|
||||||
run_output = run_outputs[0]
|
run_output = run_outputs[0]
|
||||||
|
|
||||||
data = []
|
|
||||||
if run_output is not None:
|
if run_output is not None:
|
||||||
for output in run_output.outputs:
|
for output in run_output.outputs:
|
||||||
if output:
|
if output:
|
||||||
data.extend(build_data_from_result_data(output, get_final_results_only))
|
data.extend(build_data_from_result_data(output))
|
||||||
|
|
||||||
self.status = data
|
|
||||||
logger.debug(data)
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,10 @@ def load_starter_projects() -> list[tuple[Path, dict]]:
|
||||||
starter_projects = []
|
starter_projects = []
|
||||||
folder = Path(__file__).parent / "starter_projects"
|
folder = Path(__file__).parent / "starter_projects"
|
||||||
for file in folder.glob("*.json"):
|
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))
|
starter_projects.append((file, project))
|
||||||
logger.info(f"Loaded starter project {file}")
|
logger.info(f"Loaded starter project {file}")
|
||||||
return starter_projects
|
return starter_projects
|
||||||
|
|
|
||||||
|
|
@ -226,9 +226,46 @@ class SecretStrInput(BaseInputMixin, DatabaseLoadMixin):
|
||||||
|
|
||||||
field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD
|
field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD
|
||||||
password: CoalesceBool = Field(default=True)
|
password: CoalesceBool = Field(default=True)
|
||||||
input_types: list[str] = []
|
input_types: list[str] = ["Message"]
|
||||||
load_from_db: CoalesceBool = True
|
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):
|
class IntInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMixin):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { title } from "process";
|
|
||||||
import { Handle, Position } from "reactflow";
|
import { Handle, Position } from "reactflow";
|
||||||
import ShadTooltip from "../../../../components/shadTooltipComponent";
|
import ShadTooltip from "../../../../components/shadTooltipComponent";
|
||||||
import { Button } from "../../../../components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
scapedJSONStringfy,
|
scapedJSONStringfy,
|
||||||
|
|
@ -37,10 +35,7 @@ export default function HandleRenderComponent({
|
||||||
testIdComplement?: string;
|
testIdComplement?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div>
|
||||||
unstyled
|
|
||||||
className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted"
|
|
||||||
>
|
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||||
delayDuration={1000}
|
delayDuration={1000}
|
||||||
|
|
@ -100,6 +95,6 @@ export default function HandleRenderComponent({
|
||||||
left ? "-left-[4px] -ml-0.5" : "-right-[4px] -mr-0.5",
|
left ? "-left-[4px] -ml-0.5" : "-right-[4px] -mr-0.5",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -349,8 +349,8 @@ export default function ParameterComponent({
|
||||||
testIdComplement={`${data?.type?.toLowerCase()}-shownode`}
|
testIdComplement={`${data?.type?.toLowerCase()}-shownode`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 w-full">
|
{data.node?.template[name] !== undefined && (
|
||||||
{data.node?.template[name] !== undefined && (
|
<div className="mt-2 w-full">
|
||||||
<ParameterRenderComponent
|
<ParameterRenderComponent
|
||||||
handleOnNewValue={handleOnNewValue}
|
handleOnNewValue={handleOnNewValue}
|
||||||
name={name}
|
name={name}
|
||||||
|
|
@ -362,8 +362,8 @@ export default function ParameterComponent({
|
||||||
nodeClass={data.node!}
|
nodeClass={data.node!}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
{openOutputModal && (
|
{openOutputModal && (
|
||||||
<OutputModal
|
<OutputModal
|
||||||
open={openOutputModal}
|
open={openOutputModal}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,7 @@ const useHandleOnNewValue = ({
|
||||||
if (value !== undefined) parameter[key] = value;
|
if (value !== undefined) parameter[key] = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldUpdate =
|
const shouldUpdate = parameter.real_time_refresh;
|
||||||
parameter.real_time_refresh && !parameter.refresh_button;
|
|
||||||
|
|
||||||
const setNodeClass = (newNodeClass: APIClassType) => {
|
const setNodeClass = (newNodeClass: APIClassType) => {
|
||||||
options?.setNodeClass && options.setNodeClass(newNodeClass);
|
options?.setNodeClass && options.setNodeClass(newNodeClass);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function RefreshParameterComponent({
|
||||||
setErrorData,
|
setErrorData,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
{children}
|
{children}
|
||||||
{templateData.refresh_button && (
|
{templateData.refresh_button && (
|
||||||
<div className="shrink-0 flex-col">
|
<div className="shrink-0 flex-col">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue