feat: Add ComponentFrontendNode to CustomComponent

This commit adds the `ComponentFrontendNode` class to the `CustomComponent` module. The `ComponentFrontendNode` class defines a new frontend node for the `Component` type. It includes a template with a code input field. This change enhances the functionality and flexibility of the `CustomComponent` module.
This commit is contained in:
ogabrielluiz 2024-05-31 09:25:48 -03:00
commit b7de1ff3bd
7 changed files with 105 additions and 31 deletions

View file

@ -21,6 +21,7 @@ class VertexTypesDict(LazyLoadDictBase):
def get_type_dict(self):
return {
**{t: types.CustomComponentVertex for t in ["CustomComponent"]},
**{t: types.ComponentVertex for t in ["Component"]},
**{t: types.InterfaceVertex for t in CHAT_COMPONENTS},
}

View file

@ -24,6 +24,15 @@ class CustomComponentVertex(Vertex):
return self.artifacts["repr"] or super()._built_object_repr()
class ComponentVertex(Vertex):
def __init__(self, data: Dict, graph):
super().__init__(data, graph=graph, base_type="component")
def _built_object_repr(self):
if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr()
class InterfaceVertex(Vertex):
def __init__(self, data: Dict, graph):
super().__init__(data, graph=graph, base_type="custom_components", is_task=True)

View file

@ -27,10 +27,25 @@ async def instantiate_class(
params = convert_params_to_sets(params)
params = convert_kwargs(params)
logger.debug(f"Instantiating {vertex_type} of type {base_type}")
if not base_type:
raise ValueError("No base type provided for vertex")
params_copy = params.copy()
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
custom_component: "CustomComponent" = class_object(
user_id=user_id,
parameters=params_copy,
vertex=vertex,
selected_output_type=vertex.selected_output_type,
)
params_copy = update_params_with_load_from_db_fields(
custom_component, params_copy, vertex.load_from_db_fields, fallback_to_env_vars
)
if base_type == "custom_components":
return await instantiate_custom_component(params, user_id, vertex, fallback_to_env_vars=fallback_to_env_vars)
return await build_custom_component(params=params, custom_component=custom_component)
elif base_type == "component":
return await build_component(params=params, custom_component=custom_component)
else:
raise ValueError(f"Base type {base_type} not found.")
@ -94,26 +109,37 @@ def update_params_with_load_from_db_fields(
return params
async def instantiate_custom_component(
params: dict, user_id: str, vertex: "Vertex", fallback_to_env_vars: bool = False
async def build_component(
params: dict,
custom_component: "CustomComponent",
vertex: "Vertex",
):
params_copy = params.copy()
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
custom_component: "CustomComponent" = class_object(
user_id=user_id,
parameters=params_copy,
vertex=vertex,
selected_output_type=vertex.selected_output_type,
)
params_copy = update_params_with_load_from_db_fields(
custom_component, params_copy, vertex.load_from_db_fields, fallback_to_env_vars
)
# Now set the params as attributes of the custom_component
custom_component.set_attributes(params_copy)
custom_component.set_attributes(params)
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
params_copy["retriever"] = params_copy["retriever"].as_retriever()
build_result = {}
if hasattr(custom_component, "outputs"):
for output in custom_component.outputs:
# Build the output if it's connected to some other vertex
# or if it's not connected to any vertex
if not vertex.edges or output.name in vertex.edges:
method: Callable | Awaitable = getattr(custom_component, output.method)
result = method()
# If the method is asynchronous, we need to await it
if inspect.iscoroutinefunction(method):
result = await result
build_result[output.name] = result
custom_repr = custom_component.custom_repr()
if custom_repr is None and isinstance(build_result, (dict, Record, str)):
custom_repr = build_result
if not isinstance(custom_repr, str):
custom_repr = str(custom_repr)
return custom_component, build_result, {"repr": custom_repr}
async def build_custom_component(params: dict, custom_component: "CustomComponent"):
if "retriever" in params and hasattr(params["retriever"], "as_retriever"):
params["retriever"] = params["retriever"].as_retriever()
# Determine if the build method is asynchronous
is_async = inspect.iscoroutinefunction(custom_component.build)
@ -124,22 +150,12 @@ async def instantiate_custom_component(
# the methods don't require any params because they are already set in the custom_component
# so we can just call them
if hasattr(custom_component, "outputs"):
for output in custom_component.outputs:
if output.name in vertex.edges:
method: Callable | Awaitable = getattr(custom_component, output.method)
result = method()
# If the method is asynchronous, we need to await it
if inspect.iscoroutinefunction(method):
result = await result
vertex.add_result(output.name, result)
if is_async:
# Await the build method directly if it's async
build_result = await custom_component.build(**params_copy)
build_result = await custom_component.build(**params)
else:
# Call the build method directly if it's sync
build_result = custom_component.build(**params_copy)
build_result = custom_component.build(**params)
custom_repr = custom_component.custom_repr()
if custom_repr is None and isinstance(build_result, (dict, Record, str)):
custom_repr = build_result

View file

@ -5,6 +5,9 @@ CUSTOM_NODES: dict[str, dict[str, frontend_node.base.FrontendNode]] = {
"custom_components": {
"CustomComponent": frontend_node.custom_components.CustomComponentFrontendNode(),
},
"component": {
"Component": frontend_node.custom_components.ComponentFrontendNode(),
},
}

View file

@ -67,3 +67,28 @@ class CustomComponentFrontendNode(FrontendNode):
)
description: Optional[str] = None
base_classes: list[str] = []
class ComponentFrontendNode(FrontendNode):
_format_template: bool = False
name: str = "Component"
display_name: Optional[str] = "Component"
beta: bool = False
template: Template = Template(
type_name="Component",
fields=[
Input(
field_type="code",
required=True,
placeholder="",
is_list=False,
show=True,
value=DEFAULT_CUSTOM_COMPONENT_CODE,
name="code",
advanced=False,
dynamic=True,
)
],
)
description: Optional[str] = None
base_classes: list[str] = []

View file

@ -8,7 +8,7 @@ class MultipleOutputsComponent(CustomComponent):
Input(display_name="Number", name="number", field_type=int),
]
outputs = [
Output(display_name="Certain Output", method="certain_output", name="certain_output"),
Output(name="Certain Output", method="certain_output"),
Output(name="Other Output", method="other_output"),
]

View file

@ -0,0 +1,20 @@
from langflow.custom import CustomComponent
from langflow.template.field.base import Input, Output
from random import randint
class MultipleOutputsComponent(CustomComponent):
inputs = [
Input(display_name="Input", name="input", field_type=str),
Input(display_name="Number", name="number", field_type=int),
]
outputs = [
Output(name="Certain Output", method="certain_output"),
Output(name="Other Output", method="other_output"),
]
def certain_output(self) -> int:
return randint(0, self.number)
def other_output(self) -> int:
return self.certain_output()