Tai Truong
fix readme
d202ada
from collections import defaultdict
from pydantic import BaseModel, field_serializer, model_serializer
from langflow.template.field.base import Output
from langflow.template.template.base import Template
class FrontendNode(BaseModel):
_format_template: bool = True
template: Template
"""Template for the frontend node."""
description: str | None = None
"""Description of the frontend node."""
icon: str | None = None
"""Icon of the frontend node."""
is_input: bool | None = None
"""Whether the frontend node is used as an input when processing the Graph.
If True, there should be a field named 'input_value'."""
is_output: bool | None = None
"""Whether the frontend node is used as an output when processing the Graph.
If True, there should be a field named 'input_value'."""
is_composition: bool | None = None
"""Whether the frontend node is used for composition."""
base_classes: list[str]
"""List of base classes for the frontend node."""
name: str = ""
"""Name of the frontend node."""
display_name: str | None = ""
"""Display name of the frontend node."""
documentation: str = ""
"""Documentation of the frontend node."""
custom_fields: dict | None = defaultdict(list)
"""Custom fields of the frontend node."""
output_types: list[str] = []
"""List of output types for the frontend node."""
full_path: str | None = None
"""Full path of the frontend node."""
pinned: bool = False
"""Whether the frontend node is pinned."""
conditional_paths: list[str] = []
"""List of conditional paths for the frontend node."""
frozen: bool = False
"""Whether the frontend node is frozen."""
outputs: list[Output] = []
"""List of output fields for the frontend node."""
field_order: list[str] = []
"""Order of the fields in the frontend node."""
beta: bool = False
"""Whether the frontend node is in beta."""
legacy: bool = False
"""Whether the frontend node is legacy."""
error: str | None = None
"""Error message for the frontend node."""
edited: bool = False
"""Whether the frontend node has been edited."""
metadata: dict = {}
"""Metadata for the component node."""
tool_mode: bool = False
"""Whether the frontend node is in tool mode."""
def set_documentation(self, documentation: str) -> None:
"""Sets the documentation of the frontend node."""
self.documentation = documentation
@field_serializer("base_classes")
def process_base_classes(self, base_classes: list[str]) -> list[str]:
"""Removes unwanted base classes from the list of base classes."""
return sorted(set(base_classes), key=lambda x: x.lower())
@field_serializer("display_name")
def process_display_name(self, display_name: str) -> str:
"""Sets the display name of the frontend node."""
return display_name or self.name
@model_serializer(mode="wrap")
def serialize_model(self, handler):
result = handler(self)
if hasattr(self, "template") and hasattr(self.template, "to_dict"):
result["template"] = self.template.to_dict()
name = result.pop("name")
# Migrate base classes to outputs
if "output_types" in result and not result.get("outputs"):
for base_class in result["output_types"]:
output = Output(
display_name=base_class,
name=base_class.lower(),
types=[base_class],
selected=base_class,
)
result["outputs"].append(output.model_dump())
return {name: result}
@classmethod
def from_dict(cls, data: dict) -> "FrontendNode":
if "template" in data:
data["template"] = Template.from_dict(data["template"])
return cls(**data)
# For backwards compatibility
def to_dict(self, *, keep_name=True) -> dict:
"""Returns a dict representation of the frontend node."""
dump = self.model_dump(by_alias=True, exclude_none=True)
if not keep_name:
return dump.pop(self.name)
return dump
def add_extra_fields(self) -> None:
pass
def add_extra_base_classes(self) -> None:
pass
def set_base_classes_from_outputs(self) -> None:
self.base_classes = [output_type for output in self.outputs for output_type in output.types]
def validate_component(self) -> None:
self.validate_name_overlap()
self.validate_attributes()
def validate_name_overlap(self) -> None:
# Check if any of the output names overlap with the any of the inputs
output_names = [output.name for output in self.outputs]
input_names = [input_.name for input_ in self.template.fields]
overlap = set(output_names).intersection(input_names)
if overlap:
overlap_str = ", ".join(f"'{x}'" for x in overlap)
msg = f"There should be no overlap between input and output names. Names {overlap_str} are duplicated."
raise ValueError(msg)
def validate_attributes(self) -> None:
# None of inputs, outputs, _artifacts, _results, logs, status, vertex, graph, display_name, description,
# documentation, icon should be present in outputs or input names
output_names = [output.name for output in self.outputs]
input_names = [input_.name for input_ in self.template.fields]
attributes = [
"inputs",
"outputs",
"_artifacts",
"_results",
"logs",
"status",
"vertex",
"graph",
"display_name",
"description",
"documentation",
"icon",
]
output_overlap = set(output_names).intersection(attributes)
input_overlap = set(input_names).intersection(attributes)
error_message = ""
if output_overlap:
output_overlap_str = ", ".join(f"'{x}'" for x in output_overlap)
error_message += f"Output names {output_overlap_str} are reserved attributes.\n"
if input_overlap:
input_overlap_str = ", ".join(f"'{x}'" for x in input_overlap)
error_message += f"Input names {input_overlap_str} are reserved attributes."
def add_base_class(self, base_class: str | list[str]) -> None:
"""Adds a base class to the frontend node."""
if isinstance(base_class, str):
self.base_classes.append(base_class)
elif isinstance(base_class, list):
self.base_classes.extend(base_class)
def add_output_type(self, output_type: str | list[str]) -> None:
"""Adds an output type to the frontend node."""
if isinstance(output_type, str):
self.output_types.append(output_type)
elif isinstance(output_type, list):
self.output_types.extend(output_type)
@classmethod
def from_inputs(cls, **kwargs):
"""Create a frontend node from inputs."""
if "inputs" not in kwargs:
msg = "Missing 'inputs' argument."
raise ValueError(msg)
if "_outputs_map" in kwargs:
kwargs["outputs"] = kwargs.pop("_outputs_map")
inputs = kwargs.pop("inputs")
template = Template(type_name="Component", fields=inputs)
kwargs["template"] = template
return cls(**kwargs)
def set_field_value_in_template(self, field_name, value) -> None:
for field in self.template.fields:
if field.name == field_name:
field.value = value
break
def set_field_load_from_db_in_template(self, field_name, value) -> None:
for field in self.template.fields:
if field.name == field_name and hasattr(field, "load_from_db"):
field.load_from_db = value
break