From daedaae820e7e3c7f0e1c431d3d923acac9bef30 Mon Sep 17 00:00:00 2001 From: ogabrielluiz Date: Mon, 3 Jun 2024 13:49:03 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20(text.py):=20Rename=20CustomComp?= =?UTF-8?q?onent=20to=20Component=20for=20better=20clarity=20and=20consist?= =?UTF-8?q?ency=20=F0=9F=93=9D=20(ChatInput.py,=20TextInput.py,=20ChatOutp?= =?UTF-8?q?ut.py,=20RecordsOutput.py,=20TextOutput.py):=20Refactor=20code?= =?UTF-8?q?=20to=20use=20Input=20and=20Output=20classes=20for=20defining?= =?UTF-8?q?=20inputs=20and=20outputs=20=F0=9F=93=9D=20(CustomComponent.py)?= =?UTF-8?q?:=20Update=20method=20to=20get=20the=20build=20method=20for=20c?= =?UTF-8?q?ustom=20components=20to=20consider=20classes=20inheriting=20fro?= =?UTF-8?q?m=20Component=20or=20CustomComponent=20=F0=9F=93=9D=20(Director?= =?UTF-8?q?yReader.py):=20Remove=20check=20for=20missing=20build=20functio?= =?UTF-8?q?n=20as=20it=20is=20no=20longer=20necessary=20=F0=9F=93=9D=20(ba?= =?UTF-8?q?se.py):=20Add=20properties=20to=20easily=20access=20outgoing=20?= =?UTF-8?q?edges,=20incoming=20edges,=20and=20source=20names=20of=20edges?= =?UTF-8?q?=20in=20a=20Vertex=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (types.py): Refactor code to improve readability and maintainability by adding type hinting and organizing imports 📝 (types.py): Add missing documentation and comments to clarify the purpose of methods and classes ♻️ (loading.py): Refactor code to remove redundant code and improve code structure for better maintainability 📝 (loading.py): Add comments to explain the purpose of functions and the flow of the code ♻️ (base.py): Refactor code to improve consistency and readability by updating class names and method calls to match the intended functionality --- src/backend/base/langflow/base/io/text.py | 4 +- .../langflow/components/inputs/ChatInput.py | 7 +- .../langflow/components/inputs/TextInput.py | 45 ++++++------- .../langflow/components/outputs/ChatOutput.py | 49 +++++++++----- .../components/outputs/RecordsOutput.py | 16 +++-- .../langflow/components/outputs/TextOutput.py | 41 +++++++----- .../custom_component/custom_component.py | 8 +-- .../directory_reader/directory_reader.py | 2 - .../base/langflow/graph/vertex/base.py | 12 ++++ .../base/langflow/graph/vertex/types.py | 66 ++++++++++++++++++- .../langflow/interface/initialize/loading.py | 22 +++++-- .../langflow/template/frontend_node/base.py | 2 +- 12 files changed, 192 insertions(+), 82 deletions(-) diff --git a/src/backend/base/langflow/base/io/text.py b/src/backend/base/langflow/base/io/text.py index 5ecfea11a..84ef001cf 100644 --- a/src/backend/base/langflow/base/io/text.py +++ b/src/backend/base/langflow/base/io/text.py @@ -1,12 +1,12 @@ from typing import Optional -from langflow.custom import CustomComponent +from langflow.custom import Component from langflow.field_typing import Text from langflow.helpers.record import records_to_text from langflow.schema.schema import Record -class TextComponent(CustomComponent): +class TextComponent(Component): display_name = "Text Component" description = "Used to pass text to the next component." diff --git a/src/backend/base/langflow/components/inputs/ChatInput.py b/src/backend/base/langflow/components/inputs/ChatInput.py index 9264f85cb..e8a4ccc21 100644 --- a/src/backend/base/langflow/components/inputs/ChatInput.py +++ b/src/backend/base/langflow/components/inputs/ChatInput.py @@ -21,7 +21,7 @@ class ChatInput(ChatComponent): ] def text_response(self) -> Text: - result = self.message + result = self.input_value if self.session_id and isinstance(result, (Record, str)): self.store_message(result, self.session_id, self.sender, self.sender_name) return result @@ -29,11 +29,12 @@ class ChatInput(ChatComponent): def record_response(self) -> Record: record = Record( data={ - "message": self.message, + "message": self.input_value, "sender": self.sender, "sender_name": self.sender_name, "session_id": self.session_id, - } + }, + text_key="message", ) if self.session_id and isinstance(record, (Record, str)): self.store_message(record, self.session_id, self.sender, self.sender_name) diff --git a/src/backend/base/langflow/components/inputs/TextInput.py b/src/backend/base/langflow/components/inputs/TextInput.py index b2317678e..2e3a65ee1 100644 --- a/src/backend/base/langflow/components/inputs/TextInput.py +++ b/src/backend/base/langflow/components/inputs/TextInput.py @@ -1,7 +1,6 @@ -from typing import Optional - from langflow.base.io.text import TextComponent from langflow.field_typing import Text +from langflow.template import Input, Output class TextInput(TextComponent): @@ -9,24 +8,26 @@ class TextInput(TextComponent): description = "Get text inputs from the Playground." icon = "type" - def build_config(self): - return { - "input_value": { - "display_name": "Value", - "input_types": ["Record", "Text"], - "info": "Text or Record to be passed as input.", - }, - "record_template": { - "display_name": "Record Template", - "multiline": True, - "info": "Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.", - "advanced": True, - }, - } + inputs = [ + Input( + name="input_value", + type=str, + display_name="Value", + info="Text or Record to be passed as input.", + input_types=["Record", "Text"], + ), + Input( + name="record_template", + type=str, + display_name="Record Template", + multiline=True, + info="Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.", + advanced=True, + ), + ] + outputs = [ + Output(name="Text", method="text_response"), + ] - def build( - self, - input_value: Optional[Text] = "", - record_template: Optional[str] = "", - ) -> Text: - return super().build(input_value=input_value, record_template=record_template) + def text_response(self) -> Text: + return self.input_value if self.input_value else "" diff --git a/src/backend/base/langflow/components/outputs/ChatOutput.py b/src/backend/base/langflow/components/outputs/ChatOutput.py index 7994c9ded..3e44d38aa 100644 --- a/src/backend/base/langflow/components/outputs/ChatOutput.py +++ b/src/backend/base/langflow/components/outputs/ChatOutput.py @@ -1,8 +1,7 @@ -from typing import Optional, Union - from langflow.base.io.chat import ChatComponent from langflow.field_typing import Text from langflow.schema import Record +from langflow.template import Input, Output class ChatOutput(ChatComponent): @@ -10,20 +9,34 @@ class ChatOutput(ChatComponent): description = "Display a chat message in the Playground." icon = "ChatOutput" - def build( - self, - sender: Optional[str] = "Machine", - sender_name: Optional[str] = "AI", - input_value: Optional[str] = None, - session_id: Optional[str] = None, - return_record: Optional[bool] = False, - record_template: Optional[str] = "{text}", - ) -> Union[Text, Record]: - return super().build_with_record( - sender=sender, - sender_name=sender_name, - input_value=input_value, - session_id=session_id, - return_record=return_record, - record_template=record_template or "", + inputs = [ + Input(name="input_value", type=str, display_name="Message", multiline=True), + Input(name="sender", type=str, display_name="Sender Type", options=["Machine", "AI"]), + Input(name="sender_name", type=str, display_name="Sender Name"), + Input(name="session_id", type=str, display_name="Session ID"), + Input(name="record_template", type=str, display_name="Record Template", default="{text}"), + ] + outputs = [ + Output(name="Message", method="text_response"), + Output(name="Record", method="record_response"), + ] + + def text_response(self) -> Text: + result = self.input_value + if self.session_id and isinstance(result, (Record, str)): + self.store_message(result, self.session_id, self.sender, self.sender_name) + return result + + def record_response(self) -> Record: + record = Record( + data={ + "message": self.input_value, + "sender": self.sender, + "sender_name": self.sender_name, + "session_id": self.session_id, + "template": self.record_template or "", + } ) + if self.session_id and isinstance(record, (Record, str)): + self.store_message(record, self.session_id, self.sender, self.sender_name) + return record diff --git a/src/backend/base/langflow/components/outputs/RecordsOutput.py b/src/backend/base/langflow/components/outputs/RecordsOutput.py index 25eae862e..7af2a0c7e 100644 --- a/src/backend/base/langflow/components/outputs/RecordsOutput.py +++ b/src/backend/base/langflow/components/outputs/RecordsOutput.py @@ -1,10 +1,18 @@ -from langflow.custom import CustomComponent +from langflow.custom import Component from langflow.schema import Record +from langflow.template import Input, Output -class RecordsOutput(CustomComponent): +class RecordsOutput(Component): display_name = "Records Output" description = "Display Records as a Table" - def build(self, input_value: Record) -> Record: - return input_value + inputs = [ + Input(name="input_value", type=Record, display_name="Record Input"), + ] + outputs = [ + Output(name="Record", method="record_response"), + ] + + def record_response(self) -> Record: + return self.input_value diff --git a/src/backend/base/langflow/components/outputs/TextOutput.py b/src/backend/base/langflow/components/outputs/TextOutput.py index 0d55621b2..2459192be 100644 --- a/src/backend/base/langflow/components/outputs/TextOutput.py +++ b/src/backend/base/langflow/components/outputs/TextOutput.py @@ -1,7 +1,6 @@ -from typing import Optional - from langflow.base.io.text import TextComponent from langflow.field_typing import Text +from langflow.template import Input, Output class TextOutput(TextComponent): @@ -9,20 +8,26 @@ class TextOutput(TextComponent): description = "Display a text output in the Playground." icon = "type" - def build_config(self): - return { - "input_value": { - "display_name": "Value", - "input_types": ["Record", "Text"], - "info": "Text or Record to be passed as output.", - }, - "record_template": { - "display_name": "Record Template", - "multiline": True, - "info": "Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.", - "advanced": True, - }, - } + inputs = [ + Input( + name="input_value", + type=str, + display_name="Value", + info="Text or Record to be passed as output.", + input_types=["Record", "Text"], + ), + Input( + name="record_template", + type=str, + display_name="Record Template", + multiline=True, + info="Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.", + advanced=True, + ), + ] + outputs = [ + Output(name="Text", method="text_response"), + ] - def build(self, input_value: Optional[Text] = "", record_template: Optional[str] = "") -> Text: - return super().build(input_value=input_value, record_template=record_template) + def text_response(self) -> Text: + return self.input_value if self.input_value else "" diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index ce01f9db6..d51b4d5d8 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -1,10 +1,9 @@ -import operator from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Sequence, Union from uuid import UUID import yaml -from cachetools import TTLCache, cachedmethod +from cachetools import TTLCache from langchain_core.documents import Document from pydantic import BaseModel @@ -299,7 +298,6 @@ class CustomComponent(BaseComponent): arg["type"] = "Data" return args - @cachedmethod(operator.attrgetter("cache")) def get_method(self, method_name: str): """ Gets the build method for the custom component. @@ -310,7 +308,9 @@ class CustomComponent(BaseComponent): if not self.code: return {} - component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] + component_classes = [ + cls for cls in self.tree["classes"] if "Component" in cls["bases"] or "CustomComponent" in cls["bases"] + ] if not component_classes: return {} diff --git a/src/backend/base/langflow/custom/directory_reader/directory_reader.py b/src/backend/base/langflow/custom/directory_reader/directory_reader.py index 4d7b33bfc..dc9dfa51c 100644 --- a/src/backend/base/langflow/custom/directory_reader/directory_reader.py +++ b/src/backend/base/langflow/custom/directory_reader/directory_reader.py @@ -301,8 +301,6 @@ class DirectoryReader: return False, "Empty file" elif not self.validate_code(file_content): return False, "Syntax error" - elif not self.validate_build(file_content): - return False, "Missing build function" elif self._is_type_hint_used_in_args("Optional", file_content) and not self._is_type_hint_imported( "Optional", file_content ): diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index e9f09f9aa..8863bd5aa 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -136,6 +136,18 @@ class Vertex: def edges(self) -> List["ContractEdge"]: return self.graph.get_vertex_edges(self.id) + @property + def outgoing_edges(self) -> List["ContractEdge"]: + return [edge for edge in self.edges if edge.source_id == self.id] + + @property + def incoming_edges(self) -> List["ContractEdge"]: + return [edge for edge in self.edges if edge.target_id == self.id] + + @property + def edges_source_names(self) -> List[str]: + return {edge.source_handle.name for edge in self.edges} + @property def predecessors(self) -> List["Vertex"]: return self.graph.get_predecessors(self) diff --git a/src/backend/base/langflow/graph/vertex/types.py b/src/backend/base/langflow/graph/vertex/types.py index 79eafd8e5..e1180fef9 100644 --- a/src/backend/base/langflow/graph/vertex/types.py +++ b/src/backend/base/langflow/graph/vertex/types.py @@ -1,19 +1,24 @@ import json -from typing import AsyncIterator, Dict, Iterator, List +from typing import Any, AsyncIterator, Dict, Iterator, List import yaml +from git import TYPE_CHECKING from langchain_core.messages import AIMessage from loguru import logger from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes from langflow.graph.utils import UnbuiltObject, serialize_field from langflow.graph.vertex.base import Vertex +from langflow.graph.vertex.utils import log_transaction from langflow.schema import Record from langflow.schema.schema import INPUT_FIELD_NAME from langflow.services.monitor.utils import log_vertex_build from langflow.utils.schemas import ChatOutputResponse, RecordOutputResponse from langflow.utils.util import unescape_string +if TYPE_CHECKING: + from langflow.graph.edge.base import ContractEdge + class CustomComponentVertex(Vertex): def __init__(self, data: Dict, graph): @@ -32,10 +37,65 @@ class ComponentVertex(Vertex): if self.artifacts and "repr" in self.artifacts: return self.artifacts["repr"] or super()._built_object_repr() + def _update_built_object_and_artifacts(self, result): + """ + Updates the built object and its artifacts. + """ + if isinstance(result, tuple): + if len(result) == 2: + self._built_object, self.artifacts = result + elif len(result) == 3: + self._custom_component, self._built_object, self.artifacts = result + else: + self._built_object = result -class InterfaceVertex(Vertex): + for key, value in self._built_object.items(): + self.add_result(key, value) + + def get_edge_with_target(self, target_id: str) -> "ContractEdge": + """ + Get the edge with the target id. + + Args: + target_id: The target id of the edge. + + Returns: + The edge with the target id. + """ + for edge in self.edges: + if edge.target_id == target_id: + return edge + return None + + async def _get_result(self, requester: "Vertex") -> Any: + """ + Retrieves the result of the built component. + + If the component has not been built yet, a ValueError is raised. + + Returns: + The built result if use_result is True, else the built object. + """ + if not self._built: + log_transaction(source=self, target=requester, flow_id=self.graph.flow_id, status="error") + raise ValueError(f"Component {self.display_name} has not been built yet") + + if requester is None: + raise ValueError("Requester Vertex is None") + + edge = self.get_edge_with_target(requester.id) + if edge is None: + raise ValueError(f"Edge not found between {self.display_name} and {requester.display_name}") + + result = self.results[edge.source_handle.name] + + log_transaction(source=self, target=requester, flow_id=self.graph.flow_id, status="success") + return result + + +class InterfaceVertex(ComponentVertex): def __init__(self, data: Dict, graph): - super().__init__(data, graph=graph, base_type="custom_components", is_task=True) + super().__init__(data, graph=graph) self.steps = [self._build, self._run] def build_stream_url(self): diff --git a/src/backend/base/langflow/interface/initialize/loading.py b/src/backend/base/langflow/interface/initialize/loading.py index 239bf5cc3..c5cec3b55 100644 --- a/src/backend/base/langflow/interface/initialize/loading.py +++ b/src/backend/base/langflow/interface/initialize/loading.py @@ -4,13 +4,15 @@ import os from typing import TYPE_CHECKING, Any, Awaitable, Callable, Type import orjson +import yaml from loguru import logger +from pydantic import BaseModel from langflow.custom.eval import eval_custom_component_code from langflow.schema.schema import Record if TYPE_CHECKING: - from langflow.custom import CustomComponent + from langflow.custom import Component, CustomComponent from langflow.graph.vertex.base import Vertex @@ -32,6 +34,7 @@ async def instantiate_class( raise ValueError("No base type provided for vertex") params_copy = params.copy() + # Remove code from params class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code")) custom_component: "CustomComponent" = class_object( user_id=user_id, @@ -43,9 +46,9 @@ async def instantiate_class( custom_component, params_copy, vertex.load_from_db_fields, fallback_to_env_vars ) if base_type == "custom_components": - return await build_custom_component(params=params, custom_component=custom_component) + return await build_custom_component(params=params_copy, custom_component=custom_component) elif base_type == "component": - return await build_component(params=params, custom_component=custom_component) + return await build_component(params=params_copy, custom_component=custom_component, vertex=vertex) else: raise ValueError(f"Base type {base_type} not found.") @@ -111,7 +114,7 @@ def update_params_with_load_from_db_fields( async def build_component( params: dict, - custom_component: "CustomComponent", + custom_component: "Component", vertex: "Vertex", ): # Now set the params as attributes of the custom_component @@ -122,7 +125,7 @@ async def build_component( 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: + if not vertex.outgoing_edges or output.name in vertex.edges_source_names: method: Callable | Awaitable = getattr(custom_component, output.method) result = method() # If the method is asynchronous, we need to await it @@ -130,6 +133,15 @@ async def build_component( result = await result build_result[output.name] = result custom_repr = custom_component.custom_repr() + + # ! Temporary REPR + # Since all are dict, yaml.dump them + if isinstance(build_result, dict): + _build_result = { + key: value.model_dump() if isinstance(value, BaseModel) else value for key, value in build_result.items() + } + custom_repr = yaml.dump(_build_result) + if custom_repr is None and isinstance(build_result, (dict, Record, str)): custom_repr = build_result if not isinstance(custom_repr, str): diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 5eca25e9d..310c7149c 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -124,6 +124,6 @@ class FrontendNode(BaseModel): if "inputs" not in kwargs: raise ValueError("Missing 'inputs' argument.") inputs = kwargs.pop("inputs") - template = Template(type_name="CustomComponent", fields=inputs) + template = Template(type_name="Component", fields=inputs) kwargs["template"] = template return cls(**kwargs)