From 58c43695469963b7d0e73e508d75eeb41f47b3bb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:32:02 -0300 Subject: [PATCH 001/100] refactor: update code references to use _code instead of code --- src/backend/base/langflow/api/v1/endpoints.py | 4 +- .../custom/custom_component/base_component.py | 11 ++-- .../custom_component/custom_component.py | 10 ++-- .../directory_reader/directory_reader.py | 2 +- src/backend/base/langflow/custom/utils.py | 18 +++---- .../tests/unit/test_custom_component.py | 50 +++++++++---------- .../tests/unit/test_helper_components.py | 2 +- 7 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index fadd1d7b313..296bfbba264 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -552,7 +552,7 @@ async def custom_component( raw_code: CustomComponentRequest, user: User = Depends(get_current_active_user), ): - component = Component(code=raw_code.code) + component = Component(_code=raw_code.code) built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id) if raw_code.frontend_node is not None: @@ -582,7 +582,7 @@ async def custom_component_update( """ try: - component = Component(code=code_request.code) + component = Component(_code=code_request.code) component_node, cc_instance = build_custom_component_template( component, diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index 098942dd418..ce1c5c445b4 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -23,7 +23,8 @@ class BaseComponent: ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided." ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided." - code: Optional[str] = None + _code: Optional[str] = None + """The code of the component. Defaults to None.""" _function_entrypoint_name: str = "build" field_config: dict = {} _user_id: Optional[str] @@ -47,7 +48,7 @@ def get_code_tree(self, code: str): return parser.parse_code() def get_function(self): - if not self.code: + if not self._code: raise ComponentCodeNullError( status_code=400, detail={"error": self.ERROR_CODE_NULL, "traceback": ""}, @@ -62,7 +63,7 @@ def get_function(self): }, ) - return validate.create_function(self.code, self._function_entrypoint_name) + return validate.create_function(self._code, self._function_entrypoint_name) def build_template_config(self) -> dict: """ @@ -71,10 +72,10 @@ def build_template_config(self) -> dict: Returns: A dictionary representing the template configuration. """ - if not self.code: + if not self._code: return {} - cc_class = eval_custom_component_code(self.code) + cc_class = eval_custom_component_code(self._code) component_instance = cc_class() template_config = {} 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 c5e10932f72..95c10022f03 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -64,8 +64,6 @@ class CustomComponent(BaseComponent): is_output: Optional[bool] = None """The output state of the component. Defaults to None. If True, the component must have a field named 'input_value'.""" - code: Optional[str] = None - """The code of the component. Defaults to None.""" field_config: dict = {} """The field configuration of the component. Defaults to an empty dictionary.""" field_order: Optional[List[str]] = None @@ -226,7 +224,7 @@ def tree(self): Returns: dict: The code tree of the custom component. """ - return self.get_code_tree(self.code or "") + return self.get_code_tree(self._code or "") def to_data(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Data]: """ @@ -326,7 +324,7 @@ def get_method(self, method_name: str): Returns: dict: The build method for the custom component. """ - if not self.code: + if not self._code: return {} component_classes = [ @@ -379,7 +377,7 @@ def get_main_class_name(self): Returns: str: The main class name of the custom component. """ - if not self.code: + if not self._code: return "" base_name = self.code_class_base_inheritance @@ -468,7 +466,7 @@ def get_function(self): Returns: Callable: The function associated with the custom component. """ - return validate.create_function(self.code, self.function_entrypoint_name) + return validate.create_function(self._code, self.function_entrypoint_name) async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph": if not self._user_id: 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 1fb4ca93e5a..18f39b5eb03 100644 --- a/src/backend/base/langflow/custom/directory_reader/directory_reader.py +++ b/src/backend/base/langflow/custom/directory_reader/directory_reader.py @@ -373,7 +373,7 @@ def get_output_types_from_code(code: str) -> list: """ Get the output types from the code. """ - custom_component = CustomComponent(code=code) + custom_component = CustomComponent(_code=code) types_list = custom_component.get_function_entrypoint_return_type # Get the name of types classes diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index d721bc275dc..84aef23994f 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -266,10 +266,10 @@ def run_build_inputs( def get_component_instance(custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None): try: - if custom_component.code is None: + if custom_component._code is None: raise ValueError("Code is None") - elif isinstance(custom_component.code, str): - custom_class = eval_custom_component_code(custom_component.code) + elif isinstance(custom_component._code, str): + custom_class = eval_custom_component_code(custom_component._code) else: raise ValueError("Invalid code type") except Exception as exc: @@ -300,10 +300,10 @@ def run_build_config( """Build the field configuration for a custom component""" try: - if custom_component.code is None: + if custom_component._code is None: raise ValueError("Code is None") - elif isinstance(custom_component.code, str): - custom_class = eval_custom_component_code(custom_component.code) + elif isinstance(custom_component._code, str): + custom_class = eval_custom_component_code(custom_component._code) else: raise ValueError("Invalid code type") except Exception as exc: @@ -363,7 +363,7 @@ def build_custom_component_template_from_inputs( # The List of Inputs fills the role of the build_config and the entrypoint_args field_config = custom_component.template_config frontend_node = ComponentFrontendNode.from_inputs(**field_config) - frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) + frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) # But we now need to calculate the return_type of the methods in the outputs for output in frontend_node.outputs: if output.types: @@ -407,7 +407,7 @@ def build_custom_component_template( add_extra_fields(frontend_node, field_config, entrypoint_args) - frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) + frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) @@ -432,7 +432,7 @@ def create_component_template(component): component_code = component["code"] component_output_types = component["output_types"] - component_extractor = Component(code=component_code) + component_extractor = Component(_code=component_code) component_template, component_instance = build_custom_component_template(component_extractor) if not component_template["output_types"] and component_output_types: diff --git a/src/backend/tests/unit/test_custom_component.py b/src/backend/tests/unit/test_custom_component.py index a0e5525cb57..d582f293345 100644 --- a/src/backend/tests/unit/test_custom_component.py +++ b/src/backend/tests/unit/test_custom_component.py @@ -16,7 +16,7 @@ def code_component_with_multiple_outputs(): with open("src/backend/tests/data/component_multiple_outputs.py", "r") as f: code = f.read() - return Component(code=code) + return Component(_code=code) code_default = """ @@ -72,8 +72,8 @@ def test_component_init(): """ Test the initialization of the Component class. """ - component = BaseComponent(code=code_default, function_entrypoint_name="build") - assert component.code == code_default + component = BaseComponent(_code=code_default, function_entrypoint_name="build") + assert component._code == code_default assert component.function_entrypoint_name == "build" @@ -81,8 +81,8 @@ def test_component_get_code_tree(): """ Test the get_code_tree method of the Component class. """ - component = BaseComponent(code=code_default, function_entrypoint_name="build") - tree = component.get_code_tree(component.code) + component = BaseComponent(_code=code_default, function_entrypoint_name="build") + tree = component.get_code_tree(component._code) assert "imports" in tree @@ -91,7 +91,7 @@ def test_component_code_null_error(): Test the get_function method raises the ComponentCodeNullError when the code is empty. """ - component = BaseComponent(code="", function_entrypoint_name="") + component = BaseComponent(_code="", function_entrypoint_name="") with pytest.raises(ComponentCodeNullError): component.get_function() @@ -102,8 +102,8 @@ def test_custom_component_init(): """ function_entrypoint_name = "build" - custom_component = CustomComponent(code=code_default, function_entrypoint_name=function_entrypoint_name) - assert custom_component.code == code_default + custom_component = CustomComponent(_code=code_default, function_entrypoint_name=function_entrypoint_name) + assert custom_component._code == code_default assert custom_component.function_entrypoint_name == function_entrypoint_name @@ -111,7 +111,7 @@ def test_custom_component_build_template_config(): """ Test the build_template_config property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") config = custom_component.build_template_config() assert isinstance(config, dict) @@ -120,7 +120,7 @@ def test_custom_component_get_function(): """ Test the get_function property of the CustomComponent class. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = custom_component.get_function() assert isinstance(my_function, types.FunctionType) @@ -195,7 +195,7 @@ def test_component_get_function_valid(): Test the get_function method of the Component class with valid code and function_entrypoint_name. """ - component = BaseComponent(code="def build(): pass", function_entrypoint_name="build") + component = BaseComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = component.get_function() assert callable(my_function) @@ -205,7 +205,7 @@ def test_custom_component_get_function_entrypoint_args(): Test the get_function_entrypoint_args property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") args = custom_component.get_function_entrypoint_args assert len(args) == 3 assert args[0]["name"] == "self" @@ -219,7 +219,7 @@ def test_custom_component_get_function_entrypoint_return_type(): property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type assert return_type == [Document] @@ -228,7 +228,7 @@ def test_custom_component_get_main_class_name(): """ Test the get_main_class_name property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") class_name = custom_component.get_main_class_name assert class_name == "YourComponent" @@ -238,7 +238,7 @@ def test_custom_component_get_function_valid(): Test the get_function property of the CustomComponent class with valid code and function_entrypoint_name. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = custom_component.get_function assert callable(my_function) @@ -352,9 +352,9 @@ def test_component_get_code_tree_syntax_error(): Test the get_code_tree method of the Component class raises the CodeSyntaxError when given incorrect syntax. """ - component = BaseComponent(code="import os as", function_entrypoint_name="build") + component = BaseComponent(_code="import os as", function_entrypoint_name="build") with pytest.raises(CodeSyntaxError): - component.get_code_tree(component.code) + component.get_code_tree(component._code) def test_custom_component_class_template_validation_no_code(): @@ -362,7 +362,7 @@ def test_custom_component_class_template_validation_no_code(): Test the _class_template_validation method of the CustomComponent class raises the HTTPException when the code is None. """ - custom_component = CustomComponent(code=None, function_entrypoint_name="build") + custom_component = CustomComponent(_code=None, function_entrypoint_name="build") with pytest.raises(TypeError): custom_component.get_function() @@ -372,9 +372,9 @@ def test_custom_component_get_code_tree_syntax_error(): Test the get_code_tree method of the CustomComponent class raises the CodeSyntaxError when given incorrect syntax. """ - custom_component = CustomComponent(code="import os as", function_entrypoint_name="build") + custom_component = CustomComponent(_code="import os as", function_entrypoint_name="build") with pytest.raises(CodeSyntaxError): - custom_component.get_code_tree(custom_component.code) + custom_component.get_code_tree(custom_component._code) def test_custom_component_get_function_entrypoint_args_no_args(): @@ -387,7 +387,7 @@ class MyMainClass(CustomComponent): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") args = custom_component.get_function_entrypoint_args assert len(args) == 0 @@ -402,7 +402,7 @@ class MyClass(CustomComponent): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type assert return_type == [] @@ -416,7 +416,7 @@ def test_custom_component_get_main_class_name_no_main_class(): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") class_name = custom_component.get_main_class_name assert class_name == "" @@ -426,13 +426,13 @@ def test_custom_component_build_not_implemented(): Test the build method of the CustomComponent class raises the NotImplementedError. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") with pytest.raises(NotImplementedError): custom_component.build() def test_build_config_no_code(): - component = CustomComponent(code=None) + component = CustomComponent(_code=None) assert component.get_function_entrypoint_args == [] assert component.get_function_entrypoint_return_type == [] diff --git a/src/backend/tests/unit/test_helper_components.py b/src/backend/tests/unit/test_helper_components.py index dc07c583549..75395b8620f 100644 --- a/src/backend/tests/unit/test_helper_components.py +++ b/src/backend/tests/unit/test_helper_components.py @@ -32,7 +32,7 @@ def test_uuid_generator_component(): # Arrange uuid_generator_component = helpers.IDGeneratorComponent() - uuid_generator_component.code = open(helpers.IDGenerator.__file__, "r").read() + uuid_generator_component._code = open(helpers.IDGenerator.__file__, "r").read() frontend_node, _ = build_custom_component_template(uuid_generator_component) From 1f96febbb9dede439e000c99d42313d5ad18dfd1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:39:04 -0300 Subject: [PATCH 002/100] refactor: add backwards compatible attributes to Component class --- .../base/langflow/custom/custom_component/component.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 2aafd92109c..b3bef4fd348 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -14,6 +14,8 @@ from .custom_component import CustomComponent +BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"] + class Component(CustomComponent): inputs: List[InputTypes] = [] @@ -39,6 +41,8 @@ def __getattr__(self, name: str) -> Any: return self.__dict__["_attributes"][name] if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: return self.__dict__["_inputs"][name].value + if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: + return self.__dict__[f"_{name}"] raise AttributeError(f"{name} not found in {self.__class__.__name__}") def map_inputs(self, inputs: List[InputTypes]): From 290f685d4edfd02290a9cc5bee2f31a456a8835a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:40:29 -0300 Subject: [PATCH 003/100] refactor: update Component constructor to pass config params with underscore Refactored the `Component` class in `component.py` to handle inputs and outputs. Added a new method `map_outputs` to map a list of outputs to the component. Also updated the `__init__` method to properly initialize the inputs, outputs, and other attributes. This change improves the flexibility and extensibility of the `Component` class. Co-authored-by: Gabriel Luiz Freitas Almeida --- .../custom/custom_component/component.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index b3bef4fd348..1134918553d 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -23,18 +23,36 @@ class Component(CustomComponent): code_class_base_inheritance: ClassVar[str] = "Component" _output_logs: dict[str, Log] = {} - def __init__(self, **data): + def __init__(self, **kwargs): + # if key starts with _ it is a config + # else it is an input + inputs = {} + config = {} + for key, value in kwargs.items(): + if key.startswith("_"): + config[key] = value + else: + inputs[key] = value self._inputs: dict[str, InputTypes] = {} + self._outputs: dict[str, Output] = {} self._results: dict[str, Any] = {} self._attributes: dict[str, Any] = {} - self._parameters: dict[str, Any] = {} + self._parameters = inputs or {} + self._components: list[Component] = [] + self.set_attributes(self._parameters) self._output_logs = {} - super().__init__(**data) + config = config or {} + if "_id" not in config: + config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} + super().__init__(**config) + if hasattr(self, "_trace_type"): + self.trace_type = self._trace_type if not hasattr(self, "trace_type"): self.trace_type = "chain" if self.inputs is not None: self.map_inputs(self.inputs) - self.set_attributes(self._parameters) + if self.outputs is not None: + self.map_outputs(self.outputs) def __getattr__(self, name: str) -> Any: if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: @@ -52,6 +70,22 @@ def map_inputs(self, inputs: List[InputTypes]): raise ValueError("Input name cannot be None.") self._inputs[input_.name] = input_ + def map_outputs(self, outputs: List[Output]): + """ + Maps the given list of outputs to the component. + Args: + outputs (List[Output]): The list of outputs to be mapped. + Raises: + ValueError: If the output name is None. + Returns: + None + """ + self.outputs = outputs + for output in outputs: + if output.name is None: + raise ValueError("Output name cannot be None.") + self._outputs[output.name] = output + def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() From ca1aa328227835c66357d191c7ff6c862d854112 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:42:32 -0300 Subject: [PATCH 004/100] refactor: change attribute to use underscore --- .../base/langflow/base/agents/crewai/crew.py | 6 +-- .../base/langflow/base/models/model.py | 6 +-- .../langflow/components/prototypes/Listen.py | 6 +-- .../langflow/components/prototypes/Notify.py | 6 +-- .../custom/custom_component/component.py | 19 ++++---- .../custom_component/custom_component.py | 44 ++++++++----------- .../base/langflow/services/tracing/service.py | 6 +-- 7 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/backend/base/langflow/base/agents/crewai/crew.py b/src/backend/base/langflow/base/agents/crewai/crew.py index 8326c3965ef..359b87591fd 100644 --- a/src/backend/base/langflow/base/agents/crewai/crew.py +++ b/src/backend/base/langflow/base/agents/crewai/crew.py @@ -51,8 +51,8 @@ def get_task_callback( self, ) -> Callable: def task_callback(task_output: TaskOutput): - if self.vertex: - vertex_id = self.vertex.id + if self._vertex: + vertex_id = self._vertex.id else: vertex_id = self.display_name or self.__class__.__name__ self.log(task_output.model_dump(), name=f"Task (Agent: {task_output.agent}) - {vertex_id}") @@ -63,7 +63,7 @@ def get_step_callback( self, ) -> Callable: def step_callback(agent_output: Union[AgentFinish, List[Tuple[AgentAction, str]]]): - _id = self.vertex.id if self.vertex else self.display_name + _id = self._vertex.id if self._vertex else self.display_name if isinstance(agent_output, AgentFinish): messages = agent_output.messages self.log(cast(dict, messages[0].to_json()), name=f"Finish (Agent: {_id})") diff --git a/src/backend/base/langflow/base/models/model.py b/src/backend/base/langflow/base/models/model.py index 6a2aeda1d53..9a1057c6276 100644 --- a/src/backend/base/langflow/base/models/model.py +++ b/src/backend/base/langflow/base/models/model.py @@ -1,7 +1,7 @@ import json import warnings from abc import abstractmethod -from typing import Optional, Union, List +from typing import List, Optional, Union from langchain_core.language_models.llms import LLM from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage @@ -10,7 +10,7 @@ from langflow.custom import Component from langflow.field_typing import LanguageModel from langflow.inputs import MessageInput, MessageTextInput -from langflow.inputs.inputs import InputTypes, BoolInput +from langflow.inputs.inputs import BoolInput, InputTypes from langflow.schema.message import Message from langflow.template.field.base import Output @@ -164,7 +164,7 @@ def get_chat_result( inputs: Union[list, dict] = messages or {} try: runnable = runnable.with_config( # type: ignore - {"run_name": self.display_name, "project_name": self.tracing_service.project_name} # type: ignore + {"run_name": self.display_name, "project_name": self._tracing_service.project_name} # type: ignore ) if stream: return runnable.stream(inputs) # type: ignore diff --git a/src/backend/base/langflow/components/prototypes/Listen.py b/src/backend/base/langflow/components/prototypes/Listen.py index 6e5de723a87..e75ec070b8a 100644 --- a/src/backend/base/langflow/components/prototypes/Listen.py +++ b/src/backend/base/langflow/components/prototypes/Listen.py @@ -23,6 +23,6 @@ def build(self, name: str) -> Data: return state def _set_successors_ids(self): - self.vertex.is_state = True - successors = self.vertex.graph.successor_map.get(self.vertex.id, []) - return successors + self.vertex.graph.activated_vertices + self._vertex.is_state = True + successors = self._vertex.graph.successor_map.get(self._vertex.id, []) + return successors + self._vertex.graph.activated_vertices diff --git a/src/backend/base/langflow/components/prototypes/Notify.py b/src/backend/base/langflow/components/prototypes/Notify.py index b83331e3a91..72287a25509 100644 --- a/src/backend/base/langflow/components/prototypes/Notify.py +++ b/src/backend/base/langflow/components/prototypes/Notify.py @@ -43,6 +43,6 @@ def build(self, name: str, data: Optional[Data] = None, append: bool = False) -> return data def _set_successors_ids(self): - self.vertex.is_state = True - successors = self.vertex.graph.successor_map.get(self.vertex.id, []) - return successors + self.vertex.graph.activated_vertices + self._vertex.is_state = True + successors = self._vertex.graph.successor_map.get(self._vertex.id, []) + return successors + self._vertex.graph.activated_vertices diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 1134918553d..b51552c791e 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -2,6 +2,7 @@ from typing import Any, Callable, ClassVar, List, Optional, Union from uuid import UUID +import nanoid import yaml from pydantic import BaseModel @@ -144,9 +145,9 @@ def get_trace_as_metadata(self): async def _build_with_tracing(self): inputs = self.get_trace_as_inputs() metadata = self.get_trace_as_metadata() - async with self.tracing_service.trace_context(self, self.trace_name, inputs, metadata): + async with self._tracing_service.trace_context(self, self.trace_name, inputs, metadata): _results, _artifacts = await self._build_results() - self.tracing_service.set_outputs(self.trace_name, _results) + self._tracing_service.set_outputs(self.trace_name, _results) return _results, _artifacts @@ -154,7 +155,7 @@ async def _build_without_tracing(self): return await self._build_results() async def build_results(self): - if self.tracing_service: + if self._tracing_service: return await self._build_with_tracing() return await self._build_without_tracing() @@ -162,11 +163,11 @@ async def _build_results(self): _results = {} _artifacts = {} if hasattr(self, "outputs"): - self._set_outputs(self.vertex.outputs) + self._set_outputs(self._vertex.outputs) for output in self.outputs: # Build the output if it's connected to some other vertex # or if it's not connected to any vertex - if not self.vertex.outgoing_edges or output.name in self.vertex.edges_source_names: + if not self._vertex.outgoing_edges or output.name in self._vertex.edges_source_names: if output.method is None: raise ValueError(f"Output {output.name} does not have a method defined.") method: Callable = getattr(self, output.method) @@ -180,9 +181,9 @@ async def _build_results(self): if ( isinstance(result, Message) and result.flow_id is None - and self.vertex.graph.flow_id is not None + and self._vertex.graph.flow_id is not None ): - result.set_flow_id(self.vertex.graph.flow_id) + result.set_flow_id(self._vertex.graph.flow_id) _results[output.name] = result output.value = result custom_repr = self.custom_repr() @@ -214,8 +215,8 @@ async def _build_results(self): self._logs = [] self._artifacts = _artifacts self._results = _results - if self.tracing_service: - self.tracing_service.set_outputs(self.trace_name, _results) + if self._tracing_service: + self._tracing_service.set_outputs(self.trace_name, _results) return _results, _artifacts def custom_repr(self): 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 95c10022f03..a0452ca9e04 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -14,7 +14,7 @@ from langflow.schema.dotdict import dotdict from langflow.schema.log import LoggableType from langflow.schema.schema import OutputValue -from langflow.services.deps import get_storage_service, get_tracing_service, get_variable_service, session_scope +from langflow.services.deps import get_storage_service, get_variable_service, session_scope from langflow.services.storage.service import StorageService from langflow.services.tracing.schema import Log from langflow.template.utils import update_frontend_node_with_template_values @@ -72,20 +72,20 @@ class CustomComponent(BaseComponent): """The default frozen state of the component. Defaults to False.""" build_parameters: Optional[dict] = None """The build parameters of the component. Defaults to None.""" - vertex: Optional["Vertex"] = None + _vertex: Optional["Vertex"] = None """The edge target parameter of the component. Defaults to None.""" code_class_base_inheritance: ClassVar[str] = "CustomComponent" function_entrypoint_name: ClassVar[str] = "build" function: Optional[Callable] = None repr_value: Optional[Any] = "" - user_id: Optional[Union[UUID, str]] = None + _user_id: Optional[Union[UUID, str]] = None status: Optional[Any] = None """The status of the component. This is displayed on the frontend. Defaults to None.""" _flows_data: Optional[List[Data]] = None _outputs: List[OutputValue] = [] _logs: List[Log] = [] _output_logs: dict[str, Log] = {} - tracing_service: Optional["TracingService"] = None + _tracing_service: Optional["TracingService"] = None def set_attributes(self, parameters: dict): pass @@ -94,51 +94,43 @@ def set_parameters(self, parameters: dict): self._parameters = parameters self.set_attributes(self._parameters) - @classmethod - def initialize(cls, **kwargs): - user_id = kwargs.pop("user_id", None) - vertex = kwargs.pop("vertex", None) - tracing_service = kwargs.pop("tracing_service", get_tracing_service()) - params_copy = kwargs.copy() - return cls(user_id=user_id, _parameters=params_copy, vertex=vertex, tracing_service=tracing_service) - @property def trace_name(self): - return f"{self.display_name} ({self.vertex.id})" + return f"{self.display_name} ({self._vertex.id})" def update_state(self, name: str, value: Any): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) + self._vertex.graph.update_state(name=name, record=value, caller=self._vertex.id) except Exception as e: raise ValueError(f"Error updating state: {e}") def stop(self, output_name: str | None = None): - if not output_name and self.vertex and len(self.vertex.outputs) == 1: - output_name = self.vertex.outputs[0]["name"] + if not output_name and self._vertex and len(self._vertex.outputs) == 1: + output_name = self._vertex.outputs[0]["name"] elif not output_name: raise ValueError("You must specify an output name to call stop") - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.graph.mark_branch(vertex_id=self.vertex.id, output_name=output_name, state="INACTIVE") + self.graph.mark_branch(vertex_id=self._vertex.id, output_name=output_name, state="INACTIVE") except Exception as e: raise ValueError(f"Error stopping {self.display_name}: {e}") def append_state(self, name: str, value: Any): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) + self._vertex.graph.append_state(name=name, record=value, caller=self._vertex.id) except Exception as e: raise ValueError(f"Error appending state: {e}") def get_state(self, name: str): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - return self.vertex.graph.get_state(name=name) + return self._vertex.graph.get_state(name=name) except Exception as e: raise ValueError(f"Error getting state: {e}") @@ -176,7 +168,7 @@ def get_full_path(self, path: str) -> str: @property def graph(self): - return self.vertex.graph + return self._vertex.graph def _get_field_order(self): return self.field_order or list(self.field_config.keys()) @@ -522,8 +514,8 @@ def log(self, message: LoggableType | list[LoggableType], name: Optional[str] = name = f"Log {len(self._logs) + 1}" log = Log(message=message, type=get_artifact_type(message), name=name) self._logs.append(log) - if self.tracing_service and self.vertex: - self.tracing_service.add_log(trace_name=self.trace_name, log=log) + if self._tracing_service and self._vertex: + self._tracing_service.add_log(trace_name=self.trace_name, log=log) def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict): """ diff --git a/src/backend/base/langflow/services/tracing/service.py b/src/backend/base/langflow/services/tracing/service.py index feb74e3a234..6a3b7ac987c 100644 --- a/src/backend/base/langflow/services/tracing/service.py +++ b/src/backend/base/langflow/services/tracing/service.py @@ -194,8 +194,8 @@ async def trace_context( metadata: Optional[Dict[str, Any]] = None, ): trace_id = trace_name - if component.vertex: - trace_id = component.vertex.id + if component._vertex: + trace_id = component._vertex.id trace_type = component.trace_type self._start_traces( trace_id, @@ -203,7 +203,7 @@ async def trace_context( trace_type, self._cleanup_inputs(inputs), metadata, - component.vertex, + component._vertex, ) try: yield self From 81391c57687dcb357018871ba6f89f622a3644a2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:42:41 -0300 Subject: [PATCH 005/100] refactor: update CustomComponent initialization parameters Refactored the `instantiate_class` function in `loading.py` to update the initialization parameters for the `CustomComponent` class. Changed the parameter names from `user_id`, `parameters`, `vertex`, and `tracing_service` to `_user_id`, `_parameters`, `_vertex`, and `_tracing_service` respectively. This change ensures consistency and improves code readability. Co-authored-by: Gabriel Luiz Freitas Almeida --- .../langflow/interface/initialize/loading.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backend/base/langflow/interface/initialize/loading.py b/src/backend/base/langflow/interface/initialize/loading.py index f090febf3a8..ea2858100ac 100644 --- a/src/backend/base/langflow/interface/initialize/loading.py +++ b/src/backend/base/langflow/interface/initialize/loading.py @@ -33,11 +33,11 @@ async def instantiate_class( custom_params = get_params(vertex.params) code = custom_params.pop("code") class_object: Type["CustomComponent" | "Component"] = eval_custom_component_code(code) - custom_component: "CustomComponent" | "Component" = class_object.initialize( - user_id=user_id, - parameters=custom_params, - vertex=vertex, - tracing_service=get_tracing_service(), + custom_component: "CustomComponent" | "Component" = class_object( + _user_id=user_id, + _parameters=custom_params, + _vertex=vertex, + _tracing_service=get_tracing_service(), ) return custom_component, custom_params @@ -186,9 +186,9 @@ async def build_custom_component(params: dict, custom_component: "CustomComponen raw = post_process_raw(raw, artifact_type) artifact = {"repr": custom_repr, "raw": raw, "type": artifact_type} - if custom_component.vertex is not None: - custom_component._artifacts = {custom_component.vertex.outputs[0].get("name"): artifact} - custom_component._results = {custom_component.vertex.outputs[0].get("name"): build_result} + if custom_component._vertex is not None: + custom_component._artifacts = {custom_component._vertex.outputs[0].get("name"): artifact} + custom_component._results = {custom_component._vertex.outputs[0].get("name"): build_result} return custom_component, build_result, artifact raise ValueError("Custom component does not have a vertex") From 6d01edad358d5b7d9ec6c470540ab3e0d2ef011f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:47:34 -0300 Subject: [PATCH 006/100] refactor: update BaseComponent to accept UUID for _user_id Updated the `BaseComponent` class in `base_component.py` to accept a `UUID` type for the `_user_id` attribute. This change improves the type safety and ensures consistency with the usage of `_user_id` throughout the codebase. --- .../base/langflow/custom/custom_component/base_component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index ce1c5c445b4..9e3342970bc 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -1,6 +1,7 @@ import operator import warnings from typing import Any, ClassVar, Optional +from uuid import UUID from cachetools import TTLCache, cachedmethod from fastapi import HTTPException @@ -27,7 +28,7 @@ class BaseComponent: """The code of the component. Defaults to None.""" _function_entrypoint_name: str = "build" field_config: dict = {} - _user_id: Optional[str] + _user_id: Optional[str | UUID] def __init__(self, **data): self.cache = TTLCache(maxsize=1024, ttl=60) From 49349ff86c10d83ebe48d12085613348a38f5218 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:47:40 -0300 Subject: [PATCH 007/100] refactor: import nanoid with type annotation The `nanoid` import in `component.py` has been updated to include a type annotation `# type: ignore`. This change ensures that the type checker ignores any errors related to the `nanoid` import. --- src/backend/base/langflow/custom/custom_component/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index b51552c791e..07a73ab8796 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -2,7 +2,7 @@ from typing import Any, Callable, ClassVar, List, Optional, Union from uuid import UUID -import nanoid +import nanoid # type: ignore import yaml from pydantic import BaseModel From 1e2f5175d1ee2b933c5eb7e97cdb3725fe32c723 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:47:55 -0300 Subject: [PATCH 008/100] fix(custom_component.py): convert _user_id to string before passing to functions to ensure compatibility with function signatures --- .../langflow/custom/custom_component/custom_component.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 a0452ca9e04..c341762744f 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -1,4 +1,5 @@ from pathlib import Path +from turtle import st from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Sequence, Union from uuid import UUID @@ -78,7 +79,6 @@ class CustomComponent(BaseComponent): function_entrypoint_name: ClassVar[str] = "build" function: Optional[Callable] = None repr_value: Optional[Any] = "" - _user_id: Optional[Union[UUID, str]] = None status: Optional[Any] = None """The status of the component. This is displayed on the frontend. Defaults to None.""" _flows_data: Optional[List[Data]] = None @@ -463,7 +463,7 @@ def get_function(self): async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph": if not self._user_id: raise ValueError("Session is invalid") - return await load_flow(user_id=self._user_id, flow_id=flow_id, tweaks=tweaks) + return await load_flow(user_id=str(self._user_id), flow_id=flow_id, tweaks=tweaks) async def run_flow( self, @@ -479,14 +479,14 @@ async def run_flow( flow_id=flow_id, flow_name=flow_name, tweaks=tweaks, - user_id=self._user_id, + user_id=str(self._user_id), ) def list_flows(self) -> List[Data]: if not self._user_id: raise ValueError("Session is invalid") try: - return list_flows(user_id=self._user_id) + return list_flows(user_id=str(self._user_id)) except Exception as e: raise ValueError(f"Error listing flows: {e}") From fde96870bf549d7c1dd54f25ea0a428b83cef0b4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:50:30 -0300 Subject: [PATCH 009/100] feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components --- .../custom/custom_component/component.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 07a73ab8796..a5373b449fd 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -1,11 +1,12 @@ import inspect -from typing import Any, Callable, ClassVar, List, Optional, Union +from typing import Any, Callable, ClassVar, List, Optional, Union, get_type_hints from uuid import UUID import nanoid # type: ignore import yaml from pydantic import BaseModel +from langflow.helpers.custom import format_type from langflow.inputs.inputs import InputTypes from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.schema.data import Data @@ -54,6 +55,7 @@ def __init__(self, **kwargs): self.map_inputs(self.inputs) if self.outputs is not None: self.map_outputs(self.outputs) + self._set_output_types() def __getattr__(self, name: str) -> Any: if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: @@ -91,6 +93,27 @@ def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() + def _set_output_types(self): + for output in self.outputs: + return_types = self._get_method_return_type(output.method) + output.add_types(return_types) + output.set_selected() + + def _get_method_return_type(self, method_name: str) -> List[str]: + method = getattr(self, method_name) + return_type = get_type_hints(method)["return"] + extracted_return_types = self._extract_return_type(return_type) + return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types] + + def _get_output_by_method(self, method: Callable): + # method is a callable and output.method is a string + # we need to find the output that has the same method + output = next((output for output in self.outputs if output.method == method.__name__), None) + if output is None: + method_name = method.__name__ if hasattr(method, "__name__") else str(method) + raise ValueError(f"Output with method {method_name} not found") + return output + def _validate_outputs(self): # Raise Error if some rule isn't met pass From 7f679a4de4e3182f1c82c5a9a98120d0ced4dcdd Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:55:16 -0300 Subject: [PATCH 010/100] refactor: extract method to get method return type in CustomComponent --- .../custom/custom_component/custom_component.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 c341762744f..b73cca405d3 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -1,7 +1,5 @@ from pathlib import Path -from turtle import st from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Sequence, Union -from uuid import UUID import yaml from cachetools import TTLCache @@ -269,6 +267,14 @@ def to_data(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bo return data_objects + def get_method_return_type(self, method_name: str): + build_method = self.get_method(method_name) + if not build_method or not build_method.get("has_return"): + return [] + return_type = build_method["return_type"] + + return self._extract_return_type(return_type) + def create_references_from_data(self, data: List[Data], include_data: bool = False) -> str: """ Create references from a list of data. @@ -341,12 +347,7 @@ def get_function_entrypoint_return_type(self) -> List[Any]: """ return self.get_method_return_type(self.function_entrypoint_name) - def get_method_return_type(self, method_name: str): - build_method = self.get_method(method_name) - if not build_method or not build_method.get("has_return"): - return [] - return_type = build_method["return_type"] - + def _extract_return_type(self, return_type: str): if hasattr(return_type, "__origin__") and return_type.__origin__ in [ list, List, From 485e5e238474a2a5b163eac38cc68c0ddb42e87b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:58:29 -0300 Subject: [PATCH 011/100] refactor(utils.py): refactor code to use _user_id instead of user_id for consistency and clarity perf(utils.py): optimize code by reusing cc_instance instead of calling get_component_instance multiple times --- src/backend/base/langflow/custom/utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index 84aef23994f..498fd8a1ab6 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -283,7 +283,7 @@ def get_component_instance(custom_component: CustomComponent, user_id: Optional[ ) from exc try: - custom_instance = custom_class(user_id=user_id) + custom_instance = custom_class(_user_id=user_id) return custom_instance except Exception as exc: logger.error(f"Error while instantiating custom component: {str(exc)}") @@ -317,7 +317,7 @@ def run_build_config( ) from exc try: - custom_instance = custom_class(user_id=user_id) + custom_instance = custom_class(_user_id=user_id) build_config: Dict = custom_instance.build_config() for field_name, field in build_config.copy().items(): @@ -361,14 +361,15 @@ def build_custom_component_template_from_inputs( custom_component: Union[Component, CustomComponent], user_id: Optional[Union[str, UUID]] = None ): # The List of Inputs fills the role of the build_config and the entrypoint_args - field_config = custom_component.template_config + cc_instance = get_component_instance(custom_component, user_id=user_id) + field_config = cc_instance.get_template_config(cc_instance) frontend_node = ComponentFrontendNode.from_inputs(**field_config) frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) # But we now need to calculate the return_type of the methods in the outputs for output in frontend_node.outputs: if output.types: continue - return_types = custom_component.get_method_return_type(output.method) + return_types = cc_instance.get_method_return_type(output.method) return_types = [format_type(return_type) for return_type in return_types] output.add_types(return_types) output.set_selected() @@ -376,8 +377,8 @@ def build_custom_component_template_from_inputs( frontend_node.validate_component() # ! This should be removed when we have a better way to handle this frontend_node.set_base_classes_from_outputs() - reorder_fields(frontend_node, custom_component._get_field_order()) - cc_instance = get_component_instance(custom_component, user_id=user_id) + reorder_fields(frontend_node, cc_instance._get_field_order()) + return frontend_node.to_dict(add_name=False), cc_instance @@ -414,7 +415,7 @@ def build_custom_component_template( reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(add_name=False), custom_instance + return frontend_node.to_dict(keep_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc From 2c389dc9d18ad78c19fd91a4c395929de71e878e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 13:59:48 -0300 Subject: [PATCH 012/100] refactor(utils.py, base.py): change parameter name 'add_name' to 'keep_name' for clarity and consistency in codebase --- src/backend/base/langflow/custom/utils.py | 4 ++-- src/backend/base/langflow/template/frontend_node/base.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index 84aef23994f..b5f31a551a6 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -378,7 +378,7 @@ def build_custom_component_template_from_inputs( frontend_node.set_base_classes_from_outputs() reorder_fields(frontend_node, custom_component._get_field_order()) cc_instance = get_component_instance(custom_component, user_id=user_id) - return frontend_node.to_dict(add_name=False), cc_instance + return frontend_node.to_dict(keep_name=False), cc_instance def build_custom_component_template( @@ -414,7 +414,7 @@ def build_custom_component_template( reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(add_name=False), custom_instance + return frontend_node.to_dict(keep_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 77b6d1f69ae..9aeb0b97dd9 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -90,10 +90,10 @@ def serialize_model(self, handler): return {name: result} # For backwards compatibility - def to_dict(self, add_name=True) -> dict: + 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 add_name: + if not keep_name: return dump.pop(self.name) return dump From 53255418ea1b16b8167aaf5f12a5516e8641c68f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:01:09 +0000 Subject: [PATCH 013/100] [autofix.ci] apply automated fixes --- .../base/langflow/custom/custom_component/custom_component.py | 2 -- 1 file changed, 2 deletions(-) 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 c341762744f..14b5240f65e 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -1,7 +1,5 @@ from pathlib import Path -from turtle import st from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Sequence, Union -from uuid import UUID import yaml from cachetools import TTLCache From 4a455db4cbcef5cf314ffde950f1d842dbc8dd94 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:04:51 -0300 Subject: [PATCH 014/100] refactor: update schema.py to include Edge related typres The `schema.py` file in the `src/backend/base/langflow/graph/edge` directory has been updated to include the `TargetHandle` and `SourceHandle` models. These models define the structure and attributes of the target and source handles used in the edge data. This change improves the clarity and consistency of the codebase. --- .../base/langflow/graph/edge/schema.py | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/graph/edge/schema.py b/src/backend/base/langflow/graph/edge/schema.py index 628073d1326..6d9c95dd15e 100644 --- a/src/backend/base/langflow/graph/edge/schema.py +++ b/src/backend/base/langflow/graph/edge/schema.py @@ -1,5 +1,7 @@ -from typing import Optional, Any, List -from pydantic import BaseModel +from typing import Any, List, Optional + +from pydantic import BaseModel, Field, field_validator +from typing_extensions import TypedDict class ResultPair(BaseModel): @@ -32,3 +34,55 @@ def format(self, sep: str = "\n") -> str: for result_pair in self.result_pairs[:-1] ] ) + + +class TargetHandle(BaseModel): + fieldName: str = Field(..., description="Field name for the target handle.") + id: str = Field(..., description="Unique identifier for the target handle.") + inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.") + type: str = Field(..., description="Type of the target handle.") + + +class SourceHandle(BaseModel): + baseClasses: list[str] = Field(default_factory=list, description="List of base classes for the source handle.") + dataType: str = Field(..., description="Data type for the source handle.") + id: str = Field(..., description="Unique identifier for the source handle.") + name: Optional[str] = Field(None, description="Name of the source handle.") + output_types: List[str] = Field(default_factory=list, description="List of output types for the source handle.") + + @field_validator("name", mode="before") + @classmethod + def validate_name(cls, v, _info): + if _info.data["dataType"] == "GroupNode": + # 'OpenAIModel-u4iGV_text_output' + splits = v.split("_", 1) + if len(splits) != 2: + raise ValueError(f"Invalid source handle name {v}") + v = splits[1] + return v + + +class SourceHandleDict(TypedDict, total=False): + baseClasses: list[str] + dataType: str + id: str + name: Optional[str] + output_types: List[str] + + +class TargetHandleDict(TypedDict): + fieldName: str + id: str + inputTypes: Optional[List[str]] + type: str + + +class EdgeDataDetails(TypedDict): + sourceHandle: SourceHandleDict + targetHandle: TargetHandleDict + + +class EdgeData(TypedDict, total=False): + source: str + target: str + data: EdgeDataDetails From 86bba19e953f6ba79d043311ebe97435745d9ba3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:09:06 -0300 Subject: [PATCH 015/100] refactor: update BaseInputMixin to handle invalid field types gracefully The `BaseInputMixin` class in `input_mixin.py` has been updated to handle invalid field types gracefully. Instead of raising an exception, it now returns `FieldTypes.OTHER` for any invalid field type. This change improves the robustness and reliability of the codebase. --- src/backend/base/langflow/inputs/input_mixin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index fe7f54f5674..ac446ac3b90 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -78,9 +78,10 @@ def to_dict(self): @field_validator("field_type", mode="before") @classmethod def validate_field_type(cls, v): - if v not in FieldTypes: + try: + return FieldTypes(v) + except ValueError: return FieldTypes.OTHER - return FieldTypes(v) @model_serializer(mode="wrap") def serialize_model(self, handler): From d495989b7dcaddc292ff458bf216af438150ad23 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:09:27 -0300 Subject: [PATCH 016/100] refactor: update file_types field alias in FileMixin The `file_types` field in the `FileMixin` class of `input_mixin.py` has been updated to use the `alias` parameter instead of `serialization_alias`. This change ensures consistency and improves the clarity of the codebase. --- src/backend/base/langflow/inputs/input_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index ac446ac3b90..9b20a8b8f4c 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -102,7 +102,7 @@ class MetadataTraceMixin(BaseModel): # Mixin for input fields that can be listable class ListableInputMixin(BaseModel): - is_list: bool = Field(default=False, serialization_alias="list") + is_list: bool = Field(default=False, alias="list") # Specific mixin for fields needing database interaction @@ -113,7 +113,7 @@ class DatabaseLoadMixin(BaseModel): # Specific mixin for fields needing file interaction class FileMixin(BaseModel): file_path: Optional[str] = Field(default="") - file_types: list[str] = Field(default=[], serialization_alias="fileTypes") + file_types: list[str] = Field(default=[], alias="fileTypes") @field_validator("file_types") @classmethod From da5889209ab6302eb2f5ce43b926f0207af6af39 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:09:58 -0300 Subject: [PATCH 017/100] refactor(inputs): update field_type declarations in various input classes to use SerializableFieldTypes enum for better type safety and clarity --- .../base/langflow/inputs/input_mixin.py | 2 +- src/backend/base/langflow/inputs/inputs.py | 54 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 9b20a8b8f4c..7dfa4f96615 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -29,7 +29,7 @@ class FieldTypes(str, Enum): class BaseInputMixin(BaseModel, validate_assignment=True): model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") - field_type: Optional[SerializableFieldTypes] = Field(default=FieldTypes.TEXT) + field_type: SerializableFieldTypes = Field(default=FieldTypes.TEXT) required: bool = False """Specifies if the field is required. Defaults to False.""" diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index c8023775f24..452dd28f2e9 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -24,7 +24,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMixin): - field_type: Optional[SerializableFieldTypes] = FieldTypes.TABLE + field_type: SerializableFieldTypes = FieldTypes.TABLE is_list: bool = True @field_validator("value") @@ -50,11 +50,11 @@ class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin): Attributes: input_types (list[str]): A list of input types. - field_type (Optional[SerializableFieldTypes]): The field type of the input. + field_type (SerializableFieldTypes): The field type of the input. """ input_types: list[str] = Field(default_factory=list) - field_type: Optional[SerializableFieldTypes] = FieldTypes.OTHER + field_type: SerializableFieldTypes = FieldTypes.OTHER class DataInput(HandleInput, InputTraceMixin): @@ -69,12 +69,12 @@ class DataInput(HandleInput, InputTraceMixin): class PromptInput(BaseInputMixin, ListableInputMixin, InputTraceMixin): - field_type: Optional[SerializableFieldTypes] = FieldTypes.PROMPT + field_type: SerializableFieldTypes = FieldTypes.PROMPT # Applying mixins to a specific input type class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin, MetadataTraceMixin): - field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT + field_type: SerializableFieldTypes = FieldTypes.TEXT load_from_db: CoalesceBool = False """Defines if the field will allow the user to open a text editor. Default is False.""" @@ -190,11 +190,11 @@ class MultilineInput(MessageTextInput, MultilineMixin, InputTraceMixin): Represents a multiline input field. Attributes: - field_type (Optional[SerializableFieldTypes]): The type of the field. Defaults to FieldTypes.TEXT. + field_type (SerializableFieldTypes): The type of the field. Defaults to FieldTypes.TEXT. multiline (CoalesceBool): Indicates whether the input field should support multiple lines. Defaults to True. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT + field_type: SerializableFieldTypes = FieldTypes.TEXT multiline: CoalesceBool = True @@ -203,11 +203,11 @@ class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin): Represents a multiline input field. Attributes: - field_type (Optional[SerializableFieldTypes]): The type of the field. Defaults to FieldTypes.TEXT. + field_type (SerializableFieldTypes): The type of the field. Defaults to FieldTypes.TEXT. multiline (CoalesceBool): Indicates whether the input field should support multiple lines. Defaults to True. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD + field_type: SerializableFieldTypes = FieldTypes.PASSWORD multiline: CoalesceBool = True password: CoalesceBool = Field(default=True) @@ -219,12 +219,12 @@ class SecretStrInput(BaseInputMixin, DatabaseLoadMixin): This class inherits from `BaseInputMixin` and `DatabaseLoadMixin`. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to `FieldTypes.PASSWORD`. + field_type (SerializableFieldTypes): The field type of the input. Defaults to `FieldTypes.PASSWORD`. password (CoalesceBool): A boolean indicating whether the input is a password. Defaults to `True`. input_types (list[str]): A list of input types associated with this input. Defaults to an empty list. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD + field_type: SerializableFieldTypes = FieldTypes.PASSWORD password: CoalesceBool = Field(default=True) input_types: list[str] = [] load_from_db: CoalesceBool = True @@ -238,10 +238,10 @@ class IntInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMixi It inherits from the `BaseInputMixin`, `ListableInputMixin`, and `RangeMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.INTEGER. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.INTEGER. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.INTEGER + field_type: SerializableFieldTypes = FieldTypes.INTEGER class FloatInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMixin): @@ -252,10 +252,10 @@ class FloatInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMi It inherits from the `BaseInputMixin`, `ListableInputMixin`, and `RangeMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.FLOAT. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.FLOAT. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.FLOAT + field_type: SerializableFieldTypes = FieldTypes.FLOAT class BoolInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin): @@ -266,11 +266,11 @@ class BoolInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin): It inherits from the `BaseInputMixin` and `ListableInputMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.BOOLEAN. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.BOOLEAN. value (CoalesceBool): The value of the boolean input. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.BOOLEAN + field_type: SerializableFieldTypes = FieldTypes.BOOLEAN value: CoalesceBool = False @@ -282,11 +282,11 @@ class NestedDictInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, In It inherits from the `BaseInputMixin` and `ListableInputMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.NESTED_DICT. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.NESTED_DICT. value (Optional[dict]): The value of the input. Defaults to an empty dictionary. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.NESTED_DICT + field_type: SerializableFieldTypes = FieldTypes.NESTED_DICT value: Optional[dict | Data] = {} @@ -298,11 +298,11 @@ class DictInput(BaseInputMixin, ListableInputMixin, InputTraceMixin): It inherits from the `BaseInputMixin` and `ListableInputMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.DICT. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.DICT. value (Optional[dict]): The value of the dictionary input. Defaults to an empty dictionary. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.DICT + field_type: SerializableFieldTypes = FieldTypes.DICT value: Optional[dict] = {} @@ -314,12 +314,12 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin): It inherits from the `BaseInputMixin` and `DropDownMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.TEXT. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.TEXT. options (Optional[Union[list[str], Callable]]): List of options for the field. Default is None. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT + field_type: SerializableFieldTypes = FieldTypes.TEXT options: list[str] = Field(default_factory=list) combobox: CoalesceBool = False @@ -332,12 +332,12 @@ class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, Metada It inherits from the `BaseInputMixin`, `ListableInputMixin` and `DropDownMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.TEXT. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.TEXT. options (Optional[Union[list[str], Callable]]): List of options for the field. Only used when is_list=True. Default is None. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT + field_type: SerializableFieldTypes = FieldTypes.TEXT options: list[str] = Field(default_factory=list) is_list: bool = Field(default=True, serialization_alias="list") combobox: CoalesceBool = False @@ -362,10 +362,10 @@ class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixi It inherits from the `BaseInputMixin`, `ListableInputMixin`, and `FileMixin` classes. Attributes: - field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.FILE. + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.FILE. """ - field_type: Optional[SerializableFieldTypes] = FieldTypes.FILE + field_type: SerializableFieldTypes = FieldTypes.FILE InputTypes = Union[ From 62c9ec485907d99e08a11ca7ddaebdaa583d3fe3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:11:03 -0300 Subject: [PATCH 018/100] refactor(inputs): convert dict to Message object in _validate_value method --- src/backend/base/langflow/inputs/inputs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 452dd28f2e9..f67e2606002 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -129,6 +129,8 @@ class MessageInput(StrInput, InputTraceMixin): @staticmethod def _validate_value(v: Any, _info): # If v is a instance of Message, then its fine + if isinstance(v, dict): + return Message(**v) if isinstance(v, Message): return v if isinstance(v, str): From 1d6491a26cf1ef35c1e890aca6a01d2ce77a0c88 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:12:16 -0300 Subject: [PATCH 019/100] refactor(inputs): convert dict to Message object in _validate_value method --- src/backend/base/langflow/inputs/inputs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index f67e2606002..7466b3aabc4 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -166,6 +166,8 @@ def _validate_value(v: Any, _info): 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, dict): + v = Message(**v) if isinstance(v, str): value = v elif isinstance(v, Message): From 5657101e8a751ce979bc663ac8b3a96a58161328 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:21:29 -0300 Subject: [PATCH 020/100] refactor(inputs): update model_config in BaseInputMixin to enable populating by name The `model_config` attribute in the `BaseInputMixin` class of `input_mixin.py` has been updated to include the `populate_by_name=True` parameter. This change allows the model configuration to be populated by name, improving the flexibility and usability of the codebase. --- src/backend/base/langflow/inputs/input_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 7dfa4f96615..5b1f112d0fc 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -27,7 +27,7 @@ class FieldTypes(str, Enum): # Base mixin for common input field attributes and methods class BaseInputMixin(BaseModel, validate_assignment=True): - model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid", populate_by_name=True) field_type: SerializableFieldTypes = Field(default=FieldTypes.TEXT) From 1fe81266eb391352f66b6003d51ae6fa1582c602 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:24:39 -0300 Subject: [PATCH 021/100] refactor: update _extract_return_type method in CustomComponent to accept Any type The _extract_return_type method in CustomComponent has been updated to accept the Any type as the return_type parameter. This change improves the flexibility and compatibility of the method, allowing it to handle a wider range of return types. --- .../base/langflow/custom/custom_component/custom_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b73cca405d3..135ae350b24 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -347,7 +347,7 @@ def get_function_entrypoint_return_type(self) -> List[Any]: """ return self.get_method_return_type(self.function_entrypoint_name) - def _extract_return_type(self, return_type: str): + def _extract_return_type(self, return_type: Any): if hasattr(return_type, "__origin__") and return_type.__origin__ in [ list, List, From 5ce27d2efd03715563ec805348770fa58e8b7a4d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:27:00 -0300 Subject: [PATCH 022/100] refactor(component): add get_input and get_output methods for easier access to input and output values The `Component` class in `component.py` has been updated to include the `get_input` and `get_output` methods. These methods allow for easier retrieval of input and output values by name, improving the usability and readability of the codebase. --- .../custom/custom_component/component.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index a5373b449fd..00513cdca6e 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -89,6 +89,34 @@ def map_outputs(self, outputs: List[Output]): raise ValueError("Output name cannot be None.") self._outputs[output.name] = output + def get_input(self, name: str) -> Any: + """ + Retrieves the value of the input with the specified name. + Args: + name (str): The name of the input. + Returns: + Any: The value of the input. + Raises: + ValueError: If the input with the specified name is not found. + """ + if name in self._inputs: + return self._inputs[name] + raise ValueError(f"Input {name} not found in {self.__class__.__name__}") + + def get_output(self, name: str) -> Any: + """ + Retrieves the output with the specified name. + Args: + name (str): The name of the output to retrieve. + Returns: + Any: The output value. + Raises: + ValueError: If the output with the specified name is not found. + """ + if name in self._outputs: + return self._outputs[name] + raise ValueError(f"Output {name} not found in {self.__class__.__name__}") + def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() From aab3fd62f8b140d222937b2218e5fa7aa83803a4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:27:05 -0300 Subject: [PATCH 023/100] refactor(vertex): add get_input and get_output methods for easier access to input and output values --- src/backend/base/langflow/graph/vertex/types.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/graph/vertex/types.py b/src/backend/base/langflow/graph/vertex/types.py index c36487dd912..2078e341c08 100644 --- a/src/backend/base/langflow/graph/vertex/types.py +++ b/src/backend/base/langflow/graph/vertex/types.py @@ -9,11 +9,12 @@ from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, serialize_field from langflow.graph.vertex.base import Vertex +from langflow.inputs.inputs import InputTypes from langflow.schema import Data from langflow.schema.artifact import ArtifactType from langflow.schema.message import Message from langflow.schema.schema import INPUT_FIELD_NAME -from langflow.template.field.base import UNDEFINED +from langflow.template.field.base import UNDEFINED, Output from langflow.utils.schemas import ChatOutputResponse, DataOutputResponse from langflow.utils.util import unescape_string @@ -57,6 +58,16 @@ def _update_built_object_and_artifacts(self, result): for key, value in self._built_object.items(): self.add_result(key, value) + def get_input(self, name: str) -> InputTypes: + if self._custom_component is None: + raise ValueError(f"Vertex {self.id} does not have a component instance.") + return self._custom_component.get_input(name) + + def get_output(self, name: str) -> Output: + if self._custom_component is None: + raise ValueError(f"Vertex {self.id} does not have a component instance.") + return self._custom_component.get_output(name) + def get_edge_with_target(self, target_id: str) -> Generator["ContractEdge", None, None]: """ Get the edge with the target id. From 62acf55ffaeaa8d21f5bf289c7164f18db74794e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:33:01 -0300 Subject: [PATCH 024/100] refactor(component): add set_output_value method for easier modification of output values The `Component` class in `component.py` has been updated to include the `set_output_value` method. This method allows for easier modification of output values by name, improving the usability and flexibility of the codebase. --- .../base/langflow/custom/custom_component/component.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 00513cdca6e..4af5f65b8d3 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -117,6 +117,12 @@ def get_output(self, name: str) -> Any: return self._outputs[name] raise ValueError(f"Output {name} not found in {self.__class__.__name__}") + def set_output_value(self, name: str, value: Any): + if name in self._outputs: + self._outputs[name].value = value + else: + raise ValueError(f"Output {name} not found in {self.__class__.__name__}") + def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() From efd1592fa267e585e64c59aba62a0efc3f91d5ad Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:37:18 -0300 Subject: [PATCH 025/100] feat: add run_until_complete and run_in_thread functions for handling asyncio tasks The `async_helpers.py` file in the `src/backend/base/langflow/utils` directory has been added. This file includes the `run_until_complete` and `run_in_thread` functions, which provide a way to handle asyncio tasks in different scenarios. The `run_until_complete` function checks if an event loop is already running and either runs the coroutine in a separate event loop in a new thread or creates a new event loop and runs the coroutine. The `run_in_thread` function runs the coroutine in a separate thread and returns the result or raises an exception if one occurs. These functions improve the flexibility and usability of the codebase. --- .../base/langflow/utils/async_helpers.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/backend/base/langflow/utils/async_helpers.py diff --git a/src/backend/base/langflow/utils/async_helpers.py b/src/backend/base/langflow/utils/async_helpers.py new file mode 100644 index 00000000000..25ce544510a --- /dev/null +++ b/src/backend/base/langflow/utils/async_helpers.py @@ -0,0 +1,35 @@ +import asyncio +import threading + + +def run_until_complete(coro): + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + # Run the coroutine in a separate event loop in a new thread + return run_in_thread(coro) + else: + return loop.run_until_complete(coro) + except RuntimeError: + # If there's no event loop, create a new one and run the coroutine + return asyncio.run(coro) + + +def run_in_thread(coro): + result = None + exception = None + + def target(): + nonlocal result, exception + try: + result = asyncio.run(coro) + except Exception as e: + exception = e + + thread = threading.Thread(target=target) + thread.start() + thread.join() + + if exception: + raise exception + return result From fc54f356abfbb662152dcd52ff0e8b33132d1228 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:38:24 -0300 Subject: [PATCH 026/100] refactor(component): add _edges attribute to Component class for managing edges The `Component` class in `component.py` has been updated to include the `_edges` attribute. This attribute is a list of `EdgeData` objects and is used for managing edges in the component. This change improves the functionality and organization of the codebase. --- src/backend/base/langflow/custom/custom_component/component.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 4af5f65b8d3..1abfbe5b052 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -6,6 +6,7 @@ import yaml from pydantic import BaseModel +from langflow.graph.edge.schema import EdgeData from langflow.helpers.custom import format_type from langflow.inputs.inputs import InputTypes from langflow.schema.artifact import get_artifact_type, post_process_raw @@ -40,6 +41,7 @@ def __init__(self, **kwargs): self._results: dict[str, Any] = {} self._attributes: dict[str, Any] = {} self._parameters = inputs or {} + self._edges: list[EdgeData] = [] self._components: list[Component] = [] self.set_attributes(self._parameters) self._output_logs = {} From 1954edf9f962ff8916cf893a71d8bcfaf69a6959 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:39:51 -0300 Subject: [PATCH 027/100] fix(component.py): fix conditional statement to check if self._vertex is not None before accessing its attributes --- .../custom/custom_component/component.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 1abfbe5b052..c276cdc3fc4 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -57,6 +57,7 @@ def __init__(self, **kwargs): self.map_inputs(self.inputs) if self.outputs is not None: self.map_outputs(self.outputs) + # Set output types self._set_output_types() def __getattr__(self, name: str) -> Any: @@ -94,10 +95,13 @@ def map_outputs(self, outputs: List[Output]): def get_input(self, name: str) -> Any: """ Retrieves the value of the input with the specified name. + Args: name (str): The name of the input. + Returns: Any: The value of the input. + Raises: ValueError: If the input with the specified name is not found. """ @@ -108,10 +112,13 @@ def get_input(self, name: str) -> Any: def get_output(self, name: str) -> Any: """ Retrieves the output with the specified name. + Args: name (str): The name of the output to retrieve. + Returns: Any: The output value. + Raises: ValueError: If the output with the specified name is not found. """ @@ -161,6 +168,7 @@ def _validate_inputs(self, params: dict): continue input_ = self._inputs[key] # BaseInputMixin has a `validate_assignment=True` + input_.value = value params[input_.name] = input_.value @@ -168,7 +176,7 @@ def set_attributes(self, params: dict): self._validate_inputs(params) _attributes = {} for key, value in params.items(): - if key in self.__dict__: + if key in self.__dict__ and value != getattr(self, key): raise ValueError( f"{self.__class__.__name__} defines an input parameter named '{key}' " f"that is a reserved word and cannot be used." @@ -222,11 +230,16 @@ async def _build_results(self): _results = {} _artifacts = {} if hasattr(self, "outputs"): - self._set_outputs(self._vertex.outputs) + if self._vertex: + self._set_outputs(self._vertex.outputs) for output in self.outputs: # Build the output if it's connected to some other vertex # or if it's not connected to any vertex - if not self._vertex.outgoing_edges or output.name in self._vertex.edges_source_names: + if ( + not self._vertex + or not self._vertex.outgoing_edges + or output.name in self._vertex.edges_source_names + ): if output.method is None: raise ValueError(f"Output {output.name} does not have a method defined.") method: Callable = getattr(self, output.method) @@ -238,7 +251,8 @@ async def _build_results(self): if inspect.iscoroutinefunction(method): result = await result if ( - isinstance(result, Message) + self._vertex is not None + and isinstance(result, Message) and result.flow_id is None and self._vertex.graph.flow_id is not None ): From 25b4dc536308222e1ee63065ccac72b3e8100eb8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:40:00 -0300 Subject: [PATCH 028/100] refactor(component): add _get_fallback_input method for handling fallback input The `Component` class in `component.py` has been updated to include the `_get_fallback_input` method. This method returns an `Input` object with the provided keyword arguments, which is used as a fallback input when needed. This change improves the flexibility and readability of the codebase. --- src/backend/base/langflow/custom/custom_component/component.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index c276cdc3fc4..9ca90eca4f1 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -330,3 +330,6 @@ def _get_field_order(self): def build(self, **kwargs): self.set_attributes(kwargs) + + def _get_fallback_input(self, **kwargs): + return Input(**kwargs) From 213a80380967dd2cfcb323744c3fa995c4738f1e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:40:21 -0300 Subject: [PATCH 029/100] refactor(component): add TYPE_CHECKING import for Vertex in component.py --- .../base/langflow/custom/custom_component/component.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 9ca90eca4f1..e6bc3acb6df 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -1,5 +1,5 @@ import inspect -from typing import Any, Callable, ClassVar, List, Optional, Union, get_type_hints +from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Union, get_type_hints from uuid import UUID import nanoid # type: ignore @@ -17,6 +17,9 @@ from .custom_component import CustomComponent +if TYPE_CHECKING: + from langflow.graph.vertex.base import Vertex + BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"] From 5ab8fedb4f04f2f0386dad8e3fec2534669b2a52 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:41:08 -0300 Subject: [PATCH 030/100] refactor(component): add _map_parameters_on_frontend_node and _map_parameters_on_template and other methods The `Component` class in `component.py` has been refactored to include the `_map_parameters_on_frontend_node` and `_map_parameters_on_template` methods. These methods are responsible for mapping the parameters of the component onto the frontend node and template, respectively. This change improves the organization and maintainability of the codebase. --- .../custom/custom_component/component.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index e6bc3acb6df..d1c95ddbf10 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -145,12 +145,6 @@ def _set_output_types(self): output.add_types(return_types) output.set_selected() - def _get_method_return_type(self, method_name: str) -> List[str]: - method = getattr(self, method_name) - return_type = get_type_hints(method)["return"] - extracted_return_types = self._extract_return_type(return_type) - return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types] - def _get_output_by_method(self, method: Callable): # method is a callable and output.method is a string # we need to find the output that has the same method @@ -164,6 +158,54 @@ def _validate_outputs(self): # Raise Error if some rule isn't met pass + def _map_parameters_on_frontend_node(self, frontend_node: ComponentFrontendNode): + for name, value in self._parameters.items(): + frontend_node.set_field_value_in_template(name, value) + + def _map_parameters_on_template(self, template: dict): + for name, value in self._parameters.items(): + template[name]["value"] = value + + def _get_method_return_type(self, method_name: str) -> List[str]: + method = getattr(self, method_name) + return_type = get_type_hints(method)["return"] + extracted_return_types = self._extract_return_type(return_type) + return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types] + + def _update_template(self, frontend_node: dict): + return frontend_node + + def to_frontend_node(self): + #! This part here is clunky but we need it like this for + #! backwards compatibility. We can change how prompt component + #! works and then update this later + field_config = self.get_template_config(self) + frontend_node = ComponentFrontendNode.from_inputs(**field_config) + self._map_parameters_on_frontend_node(frontend_node) + + frontend_node_dict = frontend_node.to_dict(keep_name=False) + frontend_node_dict = self._update_template(frontend_node_dict) + self._map_parameters_on_template(frontend_node_dict["template"]) + + frontend_node = ComponentFrontendNode.from_dict(frontend_node_dict) + + for output in frontend_node.outputs: + if output.types: + continue + return_types = self._get_method_return_type(output.method) + output.add_types(return_types) + output.set_selected() + + frontend_node.validate_component() + frontend_node.set_base_classes_from_outputs() + data = { + "data": { + "node": frontend_node.to_dict(keep_name=False), + "type": self.__class__.__name__, + } + } + return data + def _validate_inputs(self, params: dict): # Params keys are the `name` attribute of the Input objects for key, value in params.copy().items(): From ea8ac9f8b846dfb094aebee3903935e353c72fc0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:41:54 -0300 Subject: [PATCH 031/100] refactor(component): Add map_inputs and map_outputs methods for mapping inputs and outputs The `Component` class in `component.py` has been updated to include the `map_inputs` and `map_outputs` methods. These methods allow for mapping the given inputs and outputs to the component, improving the functionality and organization of the codebase. --- .../custom/custom_component/component.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index d1c95ddbf10..6ed38a268d2 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -135,7 +135,53 @@ def set_output_value(self, name: str, value: Any): else: raise ValueError(f"Output {name} not found in {self.__class__.__name__}") + def map_outputs(self, outputs: List[Output]): + """ + Maps the given list of outputs to the component. + + Args: + outputs (List[Output]): The list of outputs to be mapped. + + Raises: + ValueError: If the output name is None. + + Returns: + None + """ + self.outputs = outputs + for output in outputs: + if output.name is None: + raise ValueError("Output name cannot be None.") + self._outputs[output.name] = output + + def map_inputs(self, inputs: List[InputTypes]): + """ + Maps the given inputs to the component. + + Args: + inputs (List[InputTypes]): A list of InputTypes objects representing the inputs. + + Raises: + ValueError: If the input name is None. + + """ + self.inputs = inputs + for input_ in inputs: + if input_.name is None: + raise ValueError("Input name cannot be None.") + self._inputs[input_.name] = input_ + def validate(self, params: dict): + """ + Validates the component parameters. + + Args: + params (dict): A dictionary containing the component parameters. + + Raises: + ValueError: If the inputs are not valid. + ValueError: If the outputs are not valid. + """ self._validate_inputs(params) self._validate_outputs() From 13c63f0afdb3afc4eb7443edff222c61ad305ab5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:43:21 -0300 Subject: [PATCH 032/100] refactor(component): Add Input, Output, and ComponentFrontendNode imports and run_until_complete function This commit refactors the `component.py` file in the `src/backend/base/langflow/custom/custom_component` directory. It adds the `Input`, `Output`, and `ComponentFrontendNode` imports, as well as the `run_until_complete` function from the `async_helpers.py` file. These changes improve the functionality and organization of the codebase. --- .../base/langflow/custom/custom_component/component.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 6ed38a268d2..a10784f9935 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -13,7 +13,9 @@ from langflow.schema.data import Data from langflow.schema.message import Message from langflow.services.tracing.schema import Log -from langflow.template.field.base import UNDEFINED, Output +from langflow.template.field.base import UNDEFINED, Input, Output +from langflow.template.frontend_node.custom_components import ComponentFrontendNode +from langflow.utils.async_helpers import run_until_complete from .custom_component import CustomComponent From 564af7ecbc89db047de82fe843a998bd1bb104a0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:44:08 -0300 Subject: [PATCH 033/100] refactor(component): Add map_inputs and map_outputs methods for mapping inputs and outputs --- .../custom/custom_component/component.py | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index a10784f9935..287c97887f5 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -65,37 +65,54 @@ def __init__(self, **kwargs): # Set output types self._set_output_types() - def __getattr__(self, name: str) -> Any: - if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: - return self.__dict__["_attributes"][name] - if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: - return self.__dict__["_inputs"][name].value - if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: - return self.__dict__[f"_{name}"] - raise AttributeError(f"{name} not found in {self.__class__.__name__}") + def set(self, **kwargs): + """ + Connects the component to other components or sets parameters and attributes. - def map_inputs(self, inputs: List[InputTypes]): - self.inputs = inputs - for input_ in inputs: - if input_.name is None: - raise ValueError("Input name cannot be None.") - self._inputs[input_.name] = input_ + Args: + **kwargs: Keyword arguments representing the connections, parameters, and attributes. - def map_outputs(self, outputs: List[Output]): + Returns: + None + + Raises: + KeyError: If the specified input name does not exist. """ - Maps the given list of outputs to the component. + for key, value in kwargs.items(): + self._process_connection_or_parameter(key, value) + + def list_inputs(self): + """ + Returns a list of input names. + """ + return [_input.name for _input in self.inputs] + + def list_outputs(self): + """ + Returns a list of output names. + """ + return [_output.name for _output in self.outputs] + + async def run(self): + """ + Executes the component's logic and returns the result. + + Returns: + The result of executing the component's logic. + """ + return await self._run() + + def set_vertex(self, vertex: "Vertex"): + """ + Sets the vertex for the component. + Args: - outputs (List[Output]): The list of outputs to be mapped. - Raises: - ValueError: If the output name is None. + vertex (Vertex): The vertex to set. + Returns: None """ - self.outputs = outputs - for output in outputs: - if output.name is None: - raise ValueError("Output name cannot be None.") - self._outputs[output.name] = output + self._vertex = vertex def get_input(self, name: str) -> Any: """ From f32419926d2f03a4c65a8326bf54869d7b22fb34 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:44:24 -0300 Subject: [PATCH 034/100] refactor(component): Add _process_connection_or_parameter method for handling connections and parameters The `Component` class in `component.py` has been updated to include the `_process_connection_or_parameter` method. This method is responsible for handling connections and parameters based on the provided key and value. It checks if the value is callable and connects it to the component, otherwise it sets the parameter or attribute. This change improves the functionality and organization of the codebase. --- .../custom/custom_component/component.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 287c97887f5..c734ad2e27f 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -219,6 +219,93 @@ def _get_output_by_method(self, method: Callable): raise ValueError(f"Output with method {method_name} not found") return output + def _process_connection_or_parameter(self, key, value): + _input = self._get_or_create_input(key) + if callable(value): + self._connect_to_component(key, value, _input) + else: + self._set_parameter_or_attribute(key, value) + + def _get_or_create_input(self, key): + try: + return self._inputs[key] + except KeyError: + _input = self._get_fallback_input(name=key, display_name=key) + self._inputs[key] = _input + self.inputs.append(_input) + return _input + + def _connect_to_component(self, key, value, _input): + component = value.__self__ + self._components.append(component) + output = component._get_output_by_method(value) + self._add_edge(component, key, output, _input) + + def _add_edge(self, component, key, output, _input): + self._edges.append( + { + "source": component._id, + "target": self._id, + "data": { + "sourceHandle": { + "dataType": self.name, + "id": component._id, + "name": output.name, + "output_types": output.types, + }, + "targetHandle": { + "fieldName": key, + "id": self._id, + "inputTypes": _input.input_types, + "type": _input.field_type, + }, + }, + } + ) + + def _set_parameter_or_attribute(self, key, value): + self._parameters[key] = value + self._attributes[key] = value + + def __call__(self, **kwargs): + self.set(**kwargs) + + return run_until_complete(self.run()) + + async def _run(self): + # Resolve callable inputs + for key, _input in self._inputs.items(): + if callable(_input.value): + result = _input.value() + if inspect.iscoroutine(result): + result = await result + self._inputs[key].value = result + + self.set_attributes({}) + + return await self.build_results() + + def __getattr__(self, name: str) -> Any: + if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: + return self.__dict__["_attributes"][name] + if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: + return self.__dict__["_inputs"][name].value + if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: + return self.__dict__[f"_{name}"] + raise AttributeError(f"{name} not found in {self.__class__.__name__}") + + def _set_input_value(self, name: str, value: Any): + if name in self._inputs: + input_value = self._inputs[name].value + if callable(input_value): + raise ValueError( + f"Input {name} is connected to {input_value.__self__.display_name}.{input_value.__name__}" + ) + self._inputs[name].value = value + self._attributes[name] = value + else: + raise ValueError(f"Input {name} not found in {self.__class__.__name__}") + def _validate_outputs(self): # Raise Error if some rule isn't met pass From e082ce6aaff1a1a30c4b40d6cb17cd866ffdb762 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:45:27 -0300 Subject: [PATCH 035/100] refactor(frontend_node): Add set_field_value_in_template method for updating field values The `FrontendNode` class in `base.py` has been updated to include the `set_field_value_in_template` method. This method allows for updating the value of a specific field in the template of the frontend node. It iterates through the fields and sets the value of the field with the provided name. This change improves the flexibility and functionality of the codebase. --- src/backend/base/langflow/template/frontend_node/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 9aeb0b97dd9..632598c42cb 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -173,3 +173,9 @@ def from_inputs(cls, **kwargs): template = Template(type_name="Component", fields=inputs) kwargs["template"] = template return cls(**kwargs) + + def set_field_value_in_template(self, field_name, value): + for field in self.template.fields: + if field.name == field_name: + field.value = value + break From 810855596338e168ccf0ec85cbd3b2d6c73f97d4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:49:05 -0300 Subject: [PATCH 036/100] refactor(inputs): Add DefaultPromptField class for default prompt inputs The `inputs.py` file in the `src/backend/base/langflow/inputs` directory has been refactored to include the `DefaultPromptField` class. This class represents a default prompt input with customizable properties such as name, display name, field type, advanced flag, multiline flag, input types, and value. This change improves the flexibility and functionality of the codebase. --- src/backend/base/langflow/inputs/inputs.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 7466b3aabc4..a9b5b2a5490 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -6,6 +6,7 @@ from langflow.inputs.validators import CoalesceBool from langflow.schema.data import Data from langflow.schema.message import Message +from langflow.template.field.base import Input from .input_mixin import ( BaseInputMixin, @@ -57,7 +58,7 @@ class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin): field_type: SerializableFieldTypes = FieldTypes.OTHER -class DataInput(HandleInput, InputTraceMixin): +class DataInput(HandleInput, InputTraceMixin, ListableInputMixin): """ Represents an Input that has a Handle that receives a Data object. @@ -372,7 +373,23 @@ class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixi field_type: SerializableFieldTypes = FieldTypes.FILE +DEFAULT_PROMPT_INTUT_TYPES = ["Message", "Text"] + + +class DefaultPromptField(Input): + name: str + display_name: Optional[str] = None + field_type: str = "str" + + advanced: bool = False + multiline: bool = True + input_types: list[str] = DEFAULT_PROMPT_INTUT_TYPES + value: str = "" # Set the value to empty string + + InputTypes = Union[ + Input, + DefaultPromptField, BoolInput, DataInput, DictInput, @@ -398,6 +415,9 @@ class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixi def _instantiate_input(input_type: str, data: dict) -> InputTypes: input_type_class = InputTypesMap.get(input_type) + if "type" in data: + # Replate with field_type + data["field_type"] = data.pop("type") if input_type_class: return input_type_class(**data) else: From 3d76d6cbcd2997e6ae89606b55172cff5e031893 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:49:25 -0300 Subject: [PATCH 037/100] feat: Add Template.from_dict method for creating Template objects from dictionaries This commit adds the `from_dict` class method to the `Template` class in `base.py`. This method allows for creating `Template` objects from dictionaries by converting the dictionary keys and values into the appropriate `Template` attributes. This change improves the flexibility and functionality of the codebase. --- .../base/langflow/template/template/base.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/template/base.py b/src/backend/base/langflow/template/template/base.py index 7c0c7fa0faf..d0372b0b654 100644 --- a/src/backend/base/langflow/template/template/base.py +++ b/src/backend/base/langflow/template/template/base.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field, model_serializer -from langflow.inputs.inputs import InputTypes +from langflow.inputs.inputs import InputTypes, _instantiate_input from langflow.template.field.base import Input from langflow.utils.constants import DIRECT_TYPES @@ -35,6 +35,28 @@ def serialize_model(self, handler): return result + @classmethod + def from_dict(cls, data: dict) -> "Template": + for key, value in data.copy().items(): + if key == "_type": + data["type_name"] = value + del data[key] + else: + value["name"] = key + if "fields" not in data: + data["fields"] = [] + input_type = value.pop("_input_type", None) + if input_type: + try: + _input = _instantiate_input(input_type, value) + except Exception as e: + raise ValueError(f"Error instantiating input {input_type}: {e}") + else: + _input = Input(**value) + + data["fields"].append(_input) + return cls(**data) + # For backwards compatibility def to_dict(self, format_field_func=None): self.process_fields(format_field_func) From 24529352e3de53ffd8d3088dbbbdc87df4ad358e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 14:49:30 -0300 Subject: [PATCH 038/100] refactor(frontend_node): Add from_dict method for creating FrontendNode objects from dictionaries --- src/backend/base/langflow/template/frontend_node/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 632598c42cb..199233ee9b6 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -89,6 +89,12 @@ def serialize_model(self, handler): 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.""" From 6ab4c35e50bbc4bb33833eeba7b9d78049bcfb7a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:29:11 -0300 Subject: [PATCH 039/100] refactor: update BaseComponent to use get_template_config method Refactored the `BaseComponent` class in `base_component.py` to use the `get_template_config` method instead of duplicating the code. This change improves code readability and reduces redundancy. --- .../custom/custom_component/base_component.py | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index 9e3342970bc..7ccfc712da8 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -28,7 +28,7 @@ class BaseComponent: """The code of the component. Defaults to None.""" _function_entrypoint_name: str = "build" field_config: dict = {} - _user_id: Optional[str | UUID] + _user_id: Optional[str | UUID] = None def __init__(self, **data): self.cache = TTLCache(maxsize=1024, ttl=60) @@ -39,7 +39,7 @@ def __init__(self, **data): setattr(self, key, value) def __setattr__(self, key, value): - if key == "_user_id" and hasattr(self, "_user_id"): + if key == "_user_id" and hasattr(self, "_user_id") and getattr(self, "_user_id") is not None: warnings.warn("user_id is immutable and cannot be changed.") super().__setattr__(key, value) @@ -66,23 +66,16 @@ def get_function(self): return validate.create_function(self._code, self._function_entrypoint_name) - def build_template_config(self) -> dict: + @staticmethod + def get_template_config(component): """ - Builds the template configuration for the custom component. - - Returns: - A dictionary representing the template configuration. + Gets the template configuration for the custom component itself. """ - if not self._code: - return {} - - cc_class = eval_custom_component_code(self._code) - component_instance = cc_class() template_config = {} for attribute, func in ATTR_FUNC_MAPPING.items(): - if hasattr(component_instance, attribute): - value = getattr(component_instance, attribute) + if hasattr(component, attribute): + value = getattr(component, attribute) if value is not None: template_config[attribute] = func(value=value) @@ -92,5 +85,20 @@ def build_template_config(self) -> dict: return template_config + def build_template_config(self) -> dict: + """ + Builds the template configuration for the custom component. + + Returns: + A dictionary representing the template configuration. + """ + if not self._code: + return {} + + cc_class = eval_custom_component_code(self._code) + component_instance = cc_class() + template_config = self.get_template_config(component_instance) + return template_config + def build(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError From f1e03fa2d97d062df8d95c141e2ed2e33a59602e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:44:20 -0300 Subject: [PATCH 040/100] refactor(graph): Add EdgeData import and update add_nodes_and_edges method signature The `Graph` class in `base.py` has been updated to include the `EdgeData` import and modify the signature of the `add_nodes_and_edges` method. The `add_nodes_and_edges` method now accepts a list of dictionaries representing `EdgeData` objects instead of a list of dictionaries with string keys and values. This change improves the type safety and clarity of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 35db271c272..bc9278256eb 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -10,6 +10,7 @@ from langflow.exceptions.component import ComponentBuildException from langflow.graph.edge.base import ContractEdge +from langflow.graph.edge.schema import EdgeData from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager from langflow.graph.graph.state_manager import GraphStateManager @@ -73,7 +74,7 @@ def __init__( logger.error(f"Error getting tracing service: {exc}") self.tracing_service = None - def add_nodes_and_edges(self, nodes: List[Dict], edges: List[Dict[str, str]]): + def add_nodes_and_edges(self, nodes: List[Dict], edges: List[EdgeData]): self._vertices = nodes self._edges = edges self.raw_graph_data = {"nodes": nodes, "edges": edges} From 939ae63b9ecfd1ab90c48331d264d5cf6a8d1253 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:44:28 -0300 Subject: [PATCH 041/100] refactor(graph): Add first_layer property to Graph class The `Graph` class in `base.py` has been updated to include the `first_layer` property. This property returns the first layer of the graph and throws a `ValueError` if the graph is not prepared. This change improves the functionality and organization of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index bc9278256eb..8a6a305c68c 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -221,6 +221,12 @@ def validate_stream(self): "are connected and both have stream or streaming set to True" ) + @property + def first_layer(self): + if self._first_layer is None: + raise ValueError("Graph not prepared. Call prepare() first.") + return self._first_layer + @property def run_id(self): """ From 91276c646130f70ac0553a294d8343dc3b2e79a2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:44:37 -0300 Subject: [PATCH 042/100] refactor(graph): Update Graph class instantiation in base.py The `Graph` class in `base.py` has been updated to use keyword arguments when instantiating the class. This change improves the readability and maintainability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 8a6a305c68c..1888aab2d70 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -653,7 +653,7 @@ def from_payload( try: vertices = payload["nodes"] edges = payload["edges"] - graph = cls(flow_id, flow_name, user_id) + graph = cls(flow_id=flow_id, flow_name=flow_name, user_id=user_id) graph.add_nodes_and_edges(vertices, edges) return graph except KeyError as exc: From 805f9fee2738e8325fcf97ce0bdfff20f18c5384 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:44:44 -0300 Subject: [PATCH 043/100] refactor(graph): Add prepare method to Graph class The `Graph` class in `base.py` has been updated to include the `prepare` method. This method prepares the graph for execution by validating the stream, building edges, and sorting vertices. It also adds the first layer of vertices to the run manager and sets the run queue. This change improves the functionality and organization of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 1888aab2d70..13bfa644718 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1224,6 +1224,26 @@ def _create_vertex(self, vertex: dict): vertex_instance.set_top_level(self.top_level_vertices) return vertex_instance + def prepare(self, stop_component_id: Optional[str] = None, start_component_id: Optional[str] = None): + if stop_component_id and start_component_id: + raise ValueError("You can only provide one of stop_component_id or start_component_id") + self.validate_stream() + self.edges = self._build_edges() + if stop_component_id or start_component_id: + try: + first_layer = self.sort_vertices(stop_component_id, start_component_id) + except Exception as exc: + logger.error(exc) + first_layer = self.sort_vertices() + else: + first_layer = self.sort_vertices() + + for vertex_id in first_layer: + self.run_manager.add_to_vertices_being_run(vertex_id) + self._run_queue = deque(first_layer) + self._prepared = True + return self + def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]: """Returns the children of a vertex based on the vertex type.""" children = [] From b706aeaf70507c7ccd3352ca25df67e64f759cf5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:44:52 -0300 Subject: [PATCH 044/100] refactor(graph): Improve graph preparation in retrieve_vertices_order function The `retrieve_vertices_order` function in `chat.py` has been updated to improve the graph preparation process. Instead of manually sorting vertices and adding them to the run manager, the function now calls the `prepare` method of the `Graph` class. This method validates the stream, builds edges, and sets the first layer of vertices. This change improves the functionality and organization of the codebase. --- src/backend/base/langflow/api/v1/chat.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/backend/base/langflow/api/v1/chat.py b/src/backend/base/langflow/api/v1/chat.py index 8121e888929..dc5a604e56c 100644 --- a/src/backend/base/langflow/api/v1/chat.py +++ b/src/backend/base/langflow/api/v1/chat.py @@ -95,18 +95,7 @@ async def retrieve_vertices_order( graph = await build_and_cache_graph_from_data( flow_id=flow_id_str, graph_data=data.model_dump(), chat_service=chat_service ) - graph.validate_stream() - if stop_component_id or start_component_id: - try: - first_layer = graph.sort_vertices(stop_component_id, start_component_id) - except Exception as exc: - logger.error(exc) - first_layer = graph.sort_vertices() - else: - first_layer = graph.sort_vertices() - - for vertex_id in first_layer: - graph.run_manager.add_to_vertices_being_run(vertex_id) + graph = graph.prepare(stop_component_id, start_component_id) # Now vertices is a list of lists # We need to get the id of each vertex @@ -122,7 +111,7 @@ async def retrieve_vertices_order( playgroundSuccess=True, ), ) - return VerticesOrderResponse(ids=first_layer, run_id=graph._run_id, vertices_to_run=vertices_to_run) + return VerticesOrderResponse(ids=graph.first_layer, run_id=graph.run_id, vertices_to_run=vertices_to_run) except Exception as exc: background_tasks.add_task( telemetry_service.log_package_playground, From 4b04dd74b678f7f7a956e90151c1e0786d6f8be6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:54:25 -0300 Subject: [PATCH 045/100] refactor: Add GetCache and SetCache protocols for caching functionality --- src/backend/base/langflow/services/chat/schema.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/backend/base/langflow/services/chat/schema.py diff --git a/src/backend/base/langflow/services/chat/schema.py b/src/backend/base/langflow/services/chat/schema.py new file mode 100644 index 00000000000..51cf32e225c --- /dev/null +++ b/src/backend/base/langflow/services/chat/schema.py @@ -0,0 +1,10 @@ +import asyncio +from typing import Any, Protocol + + +class GetCache(Protocol): + async def __call__(self, key: str, lock: asyncio.Lock | None = None) -> Any: ... + + +class SetCache(Protocol): + async def __call__(self, key: str, data: Any, lock: asyncio.Lock | None = None) -> bool: ... From 2d9a64119b9f728befab28e1d1666bae294ac5b8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:54:54 -0300 Subject: [PATCH 046/100] refactor(graph): Add VertexBuildResult class for representing vertex build results --- src/backend/base/langflow/graph/graph/schema.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/backend/base/langflow/graph/graph/schema.py diff --git a/src/backend/base/langflow/graph/graph/schema.py b/src/backend/base/langflow/graph/graph/schema.py new file mode 100644 index 00000000000..30d67255fd9 --- /dev/null +++ b/src/backend/base/langflow/graph/graph/schema.py @@ -0,0 +1,13 @@ +from typing import TYPE_CHECKING, NamedTuple + +if TYPE_CHECKING: + from langflow.graph.schema import ResultData + from langflow.graph.vertex.base import Vertex + + +class VertexBuildResult(NamedTuple): + result_dict: "ResultData" + params: str + valid: bool + artifacts: dict + vertex: "Vertex" From edbbad16c593bdaaf544c2278ab5aa6b2b63afbf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:55:31 -0300 Subject: [PATCH 047/100] refactor(chat.py, base.py): update build_vertex method in chat.py and base.py --- src/backend/base/langflow/api/v1/chat.py | 15 ++++++------ src/backend/base/langflow/graph/graph/base.py | 23 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/backend/base/langflow/api/v1/chat.py b/src/backend/base/langflow/api/v1/chat.py index dc5a604e56c..9c0fc881a4e 100644 --- a/src/backend/base/langflow/api/v1/chat.py +++ b/src/backend/base/langflow/api/v1/chat.py @@ -178,19 +178,18 @@ async def build_vertex( try: lock = chat_service._async_cache_locks[flow_id_str] - ( - result_dict, - params, - valid, - artifacts, - vertex, - ) = await graph.build_vertex( - chat_service=chat_service, + vertex_build_result = await graph.build_vertex( vertex_id=vertex_id, user_id=current_user.id, inputs_dict=inputs.model_dump() if inputs else {}, files=files, + get_cache=chat_service.get_cache, + set_cache=chat_service.set_cache, ) + result_dict = vertex_build_result.result_dict + params = vertex_build_result.params + valid = vertex_build_result.valid + artifacts = vertex_build_result.artifacts next_runnable_vertices = await graph.get_next_runnable_vertices(lock, vertex=vertex, cache=False) top_level_vertices = graph.get_top_level_vertices(next_runnable_vertices) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 13bfa644718..2cfa122d6de 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -13,6 +13,7 @@ from langflow.graph.edge.schema import EdgeData from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager +from langflow.graph.graph.schema import VertexBuildResult from langflow.graph.graph.state_manager import GraphStateManager from langflow.graph.graph.utils import find_start_component_id, process_flow, sort_up_to_vertex from langflow.graph.schema import InterfaceComponentTypes, RunOutputs @@ -21,7 +22,7 @@ from langflow.schema import Data from langflow.schema.schema import INPUT_FIELD_NAME, InputType from langflow.services.cache.utils import CacheMiss -from langflow.services.chat.service import ChatService +from langflow.services.chat.schema import GetCache, SetCache from langflow.services.deps import get_chat_service, get_tracing_service if TYPE_CHECKING: @@ -858,13 +859,14 @@ def get_root_of_group_node(self, vertex_id: str) -> Vertex: async def build_vertex( self, - chat_service: ChatService, vertex_id: str, + get_cache: GetCache, + set_cache: SetCache, inputs_dict: Optional[Dict[str, str]] = None, files: Optional[list[str]] = None, user_id: Optional[str] = None, fallback_to_env_vars: bool = False, - ): + ) -> VertexBuildResult: """ Builds a vertex in the graph. @@ -888,12 +890,12 @@ async def build_vertex( params = "" if vertex.frozen: # Check the cache for the vertex - cached_result = await chat_service.get_cache(key=vertex.id) + cached_result = await get_cache(key=vertex.id) if isinstance(cached_result, CacheMiss): await vertex.build( user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars, files=files ) - await chat_service.set_cache(key=vertex.id, data=vertex) + await set_cache(key=vertex.id, data=vertex) else: cached_vertex = cached_result["result"] # Now set update the vertex with the cached vertex @@ -910,7 +912,7 @@ async def build_vertex( await vertex.build( user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars, files=files ) - await chat_service.set_cache(key=vertex.id, data=vertex) + await set_cache(key=vertex.id, data=vertex) if vertex.result is not None: params = f"{vertex._built_object_repr()}{params}" @@ -919,7 +921,11 @@ async def build_vertex( artifacts = vertex.artifacts else: raise ValueError(f"No result found for vertex {vertex_id}") - return result_dict, params, valid, artifacts, vertex + + vertex_build_result = VertexBuildResult( + result_dict=result_dict, params=params, valid=valid, artifacts=artifacts, vertex=vertex + ) + return vertex_build_result except Exception as exc: if not isinstance(exc, ComponentBuildException): logger.exception(f"Error building Component: \n\n{exc}") @@ -973,11 +979,12 @@ async def process(self, fallback_to_env_vars: bool, start_component_id: Optional vertex = self.get_vertex(vertex_id) task = asyncio.create_task( self.build_vertex( - chat_service=chat_service, vertex_id=vertex_id, user_id=self.user_id, inputs_dict={}, fallback_to_env_vars=fallback_to_env_vars, + get_cache=chat_service.get_cache, + set_cache=chat_service.set_cache, ), name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}", ) From 92da5048e49b3a0d1464d36b33ae2a04b0bd8add Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:58:53 -0300 Subject: [PATCH 048/100] refactor(graph): Update Edge and ContractEdge constructors to use EdgeData type The constructors of the `Edge` and `ContractEdge` classes in `base.py` have been updated to use the `EdgeData` type for the `edge` and `raw_edge` parameters, respectively. This change improves the type safety and clarity of the codebase. --- src/backend/base/langflow/graph/edge/base.py | 9 +++++---- src/backend/base/langflow/graph/graph/base.py | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/graph/edge/base.py b/src/backend/base/langflow/graph/edge/base.py index 78ad68d45a3..1cd003b658b 100644 --- a/src/backend/base/langflow/graph/edge/base.py +++ b/src/backend/base/langflow/graph/edge/base.py @@ -3,6 +3,7 @@ from loguru import logger from pydantic import BaseModel, Field, field_validator +from langflow.graph.edge.schema import EdgeData from langflow.schema.schema import INPUT_FIELD_NAME if TYPE_CHECKING: @@ -36,7 +37,7 @@ class TargetHandle(BaseModel): class Edge: - def __init__(self, source: "Vertex", target: "Vertex", edge: dict): + def __init__(self, source: "Vertex", target: "Vertex", edge: EdgeData): self.source_id: str = source.id if source else "" self.target_id: str = target.id if target else "" if data := edge.get("data", {}): @@ -50,8 +51,8 @@ def __init__(self, source: "Vertex", target: "Vertex", edge: dict): else: # Logging here because this is a breaking change logger.error("Edge data is empty") - self._source_handle = edge.get("sourceHandle", "") - self._target_handle = edge.get("targetHandle", "") + self._source_handle = edge.get("sourceHandle", "") # type: ignore + self._target_handle = edge.get("targetHandle", "") # type: ignore # 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD' # target_param is documents self.target_param = self._target_handle.split("|")[1] @@ -182,7 +183,7 @@ def __eq__(self, __o: object) -> bool: class ContractEdge(Edge): - def __init__(self, source: "Vertex", target: "Vertex", raw_edge: dict): + def __init__(self, source: "Vertex", target: "Vertex", raw_edge: EdgeData): super().__init__(source, target, raw_edge) self.is_fulfilled = False # Whether the contract has been fulfilled. self.result: Any = None diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 2cfa122d6de..f027dc281e4 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -93,8 +93,7 @@ def add_nodes_and_edges(self, nodes: List[Dict], edges: List[EdgeData]): def add_node(self, node: dict): self._vertices.append(node) - # TODO: Create a TypedDict to represente the edge - def add_edge(self, edge: dict): + def add_edge(self, edge: EdgeData): self._edges.append(edge) def initialize(self): From 397893c1252f3b302f0034508e931f18d939c310 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 16:15:17 -0300 Subject: [PATCH 049/100] feat: add BaseModel class with model_config attribute A new `BaseModel` class has been added to the `base_model.py` file. This class extends the `PydanticBaseModel` and includes a `model_config` attribute of type `ConfigDict`. This change improves the codebase by providing a base model with a configuration dictionary for models. Co-authored-by: Gabriel Luiz Freitas Almeida --- src/backend/base/langflow/helpers/base_model.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/backend/base/langflow/helpers/base_model.py diff --git a/src/backend/base/langflow/helpers/base_model.py b/src/backend/base/langflow/helpers/base_model.py new file mode 100644 index 00000000000..c81fd99d2c6 --- /dev/null +++ b/src/backend/base/langflow/helpers/base_model.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel as PydanticBaseModel +from pydantic import ConfigDict + + +class BaseModel(PydanticBaseModel): + model_config = ConfigDict(populate_by_name=True) From b59a6cc896ef9eca5ac7fd95134d4259237a9d98 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 16:15:26 -0300 Subject: [PATCH 050/100] refactor: update langflow.graph.edge.schema.py Refactor the `langflow.graph.edge.schema.py` file to include the `TargetHandle` and `SourceHandle` models. This change improves the clarity and consistency of the codebase. Co-authored-by: Gabriel Luiz Freitas Almeida --- src/backend/base/langflow/graph/edge/schema.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/graph/edge/schema.py b/src/backend/base/langflow/graph/edge/schema.py index 6d9c95dd15e..d8ae9963c18 100644 --- a/src/backend/base/langflow/graph/edge/schema.py +++ b/src/backend/base/langflow/graph/edge/schema.py @@ -1,8 +1,10 @@ from typing import Any, List, Optional -from pydantic import BaseModel, Field, field_validator +from pydantic import Field, field_validator from typing_extensions import TypedDict +from langflow.helpers.base_model import BaseModel + class ResultPair(BaseModel): result: Any @@ -37,15 +39,19 @@ def format(self, sep: str = "\n") -> str: class TargetHandle(BaseModel): - fieldName: str = Field(..., description="Field name for the target handle.") + fieldName: str = Field(..., alias="fieldName", description="Field name for the target handle.") id: str = Field(..., description="Unique identifier for the target handle.") - inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.") + input_types: List[str] = Field( + default_factory=list, alias="inputTypes", description="List of input types for the target handle." + ) type: str = Field(..., description="Type of the target handle.") class SourceHandle(BaseModel): - baseClasses: list[str] = Field(default_factory=list, description="List of base classes for the source handle.") - dataType: str = Field(..., description="Data type for the source handle.") + base_classes: list[str] = Field( + default_factory=list, alias="baseClasses", description="List of base classes for the source handle." + ) + data_type: str = Field(..., alias="dataType", description="Data type for the source handle.") id: str = Field(..., description="Unique identifier for the source handle.") name: Optional[str] = Field(None, description="Name of the source handle.") output_types: List[str] = Field(default_factory=list, description="List of output types for the source handle.") @@ -53,7 +59,7 @@ class SourceHandle(BaseModel): @field_validator("name", mode="before") @classmethod def validate_name(cls, v, _info): - if _info.data["dataType"] == "GroupNode": + if _info.data["data_type"] == "GroupNode": # 'OpenAIModel-u4iGV_text_output' splits = v.split("_", 1) if len(splits) != 2: From 4b33c218cf902dd4106fc2c2486bdd2bd1eb8e80 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 16:20:51 -0300 Subject: [PATCH 051/100] refactor(base): Update target_param assignment in Edge class The `target_param` assignment in the `Edge` class of `base.py` has been updated to use the `cast` function for type hinting. This change improves the type safety and clarity of the codebase. --- src/backend/base/langflow/graph/edge/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/graph/edge/base.py b/src/backend/base/langflow/graph/edge/base.py index 1cd003b658b..d7ae82de4d0 100644 --- a/src/backend/base/langflow/graph/edge/base.py +++ b/src/backend/base/langflow/graph/edge/base.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, cast from loguru import logger from pydantic import BaseModel, Field, field_validator @@ -55,7 +55,7 @@ def __init__(self, source: "Vertex", target: "Vertex", edge: EdgeData): self._target_handle = edge.get("targetHandle", "") # type: ignore # 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD' # target_param is documents - self.target_param = self._target_handle.split("|")[1] + self.target_param = cast(str, self._target_handle).split("|")[1] # Validate in __init__ to fail fast self.validate_edge(source, target) From 0de9b8bf114bc325cd85d3c21c0bdb3c6990ed0f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 16:37:42 -0300 Subject: [PATCH 052/100] refactor(base): Add check for existing type in add_types method --- src/backend/base/langflow/template/field/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index e825762301c..97b6d6a816f 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -1,5 +1,6 @@ from enum import Enum -from typing import Any, Callable, GenericAlias, Optional, Union, _GenericAlias, _UnionGenericAlias # type: ignore +from typing import Optional # type: ignore +from typing import Any, Callable, GenericAlias, Union, _GenericAlias, _UnionGenericAlias from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer, model_validator @@ -182,6 +183,8 @@ def to_dict(self): def add_types(self, _type: list[Any]): for type_ in _type: + if type_ in self.types: + continue if self.types is None: self.types = [] self.types.append(type_) From 8cc7700d86cea6c5cb5405a288b00475647fda0f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 18:17:39 -0300 Subject: [PATCH 053/100] refactor: update build_custom_component_template to use add_name instead of keep_name Refactor the `build_custom_component_template` function in `utils.py` to use the `add_name` parameter instead of the deprecated `keep_name` parameter. This change ensures consistency with the updated method signature and improves code clarity. --- src/backend/base/langflow/custom/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index 498fd8a1ab6..5d8a9c664b3 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -415,7 +415,7 @@ def build_custom_component_template( reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(keep_name=False), custom_instance + return frontend_node.to_dict(add_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc From a88ff42eba77f20e1d82fa2c12c7d5ea07afbf2f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 18:45:35 -0300 Subject: [PATCH 054/100] feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components (#3115) * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components * refactor: extract method to get method return type in CustomComponent * refactor: update _extract_return_type method in CustomComponent to accept Any type The _extract_return_type method in CustomComponent has been updated to accept the Any type as the return_type parameter. This change improves the flexibility and compatibility of the method, allowing it to handle a wider range of return types. --- .../custom/custom_component/component.py | 25 ++++++++++++++++++- .../custom_component/custom_component.py | 15 ++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 07a73ab8796..a5373b449fd 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -1,11 +1,12 @@ import inspect -from typing import Any, Callable, ClassVar, List, Optional, Union +from typing import Any, Callable, ClassVar, List, Optional, Union, get_type_hints from uuid import UUID import nanoid # type: ignore import yaml from pydantic import BaseModel +from langflow.helpers.custom import format_type from langflow.inputs.inputs import InputTypes from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.schema.data import Data @@ -54,6 +55,7 @@ def __init__(self, **kwargs): self.map_inputs(self.inputs) if self.outputs is not None: self.map_outputs(self.outputs) + self._set_output_types() def __getattr__(self, name: str) -> Any: if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: @@ -91,6 +93,27 @@ def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() + def _set_output_types(self): + for output in self.outputs: + return_types = self._get_method_return_type(output.method) + output.add_types(return_types) + output.set_selected() + + def _get_method_return_type(self, method_name: str) -> List[str]: + method = getattr(self, method_name) + return_type = get_type_hints(method)["return"] + extracted_return_types = self._extract_return_type(return_type) + return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types] + + def _get_output_by_method(self, method: Callable): + # method is a callable and output.method is a string + # we need to find the output that has the same method + output = next((output for output in self.outputs if output.method == method.__name__), None) + if output is None: + method_name = method.__name__ if hasattr(method, "__name__") else str(method) + raise ValueError(f"Output with method {method_name} not found") + return output + def _validate_outputs(self): # Raise Error if some rule isn't met pass 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 14b5240f65e..135ae350b24 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -267,6 +267,14 @@ def to_data(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bo return data_objects + def get_method_return_type(self, method_name: str): + build_method = self.get_method(method_name) + if not build_method or not build_method.get("has_return"): + return [] + return_type = build_method["return_type"] + + return self._extract_return_type(return_type) + def create_references_from_data(self, data: List[Data], include_data: bool = False) -> str: """ Create references from a list of data. @@ -339,12 +347,7 @@ def get_function_entrypoint_return_type(self) -> List[Any]: """ return self.get_method_return_type(self.function_entrypoint_name) - def get_method_return_type(self, method_name: str): - build_method = self.get_method(method_name) - if not build_method or not build_method.get("has_return"): - return [] - return_type = build_method["return_type"] - + def _extract_return_type(self, return_type: Any): if hasattr(return_type, "__origin__") and return_type.__origin__ in [ list, List, From a8c356464008056532225bfda4415707d3499c55 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 18:46:10 -0300 Subject: [PATCH 055/100] refactor: add _template_config property to BaseComponent Add a new `_template_config` property to the `BaseComponent` class in `base_component.py`. This property is used to store the template configuration for the custom component. If the `_template_config` property is empty, it is populated by calling the `build_template_config` method. This change improves the efficiency of accessing the template configuration and ensures that it is only built when needed. --- .../base/langflow/custom/custom_component/base_component.py | 1 + .../base/langflow/custom/custom_component/custom_component.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index 7ccfc712da8..58d4daa4622 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -29,6 +29,7 @@ class BaseComponent: _function_entrypoint_name: str = "build" field_config: dict = {} _user_id: Optional[str | UUID] = None + _template_config: dict = {} def __init__(self, **data): self.cache = TTLCache(maxsize=1024, ttl=60) 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 135ae350b24..d99fd6561ba 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -394,7 +394,9 @@ def template_config(self): Returns: dict: The template configuration for the custom component. """ - return self.build_template_config() + if not self._template_config: + self._template_config = self.build_template_config() + return self._template_config @property def variables(self): From 1ed4812f093d544dccadb9f06f59a1937afdac85 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 18:51:23 -0300 Subject: [PATCH 056/100] refactor: add type checking for Output types in add_types method Improve type checking in the `add_types` method of the `Output` class in `base.py`. Check if the `type_` already exists in the `types` list before adding it. This change ensures that duplicate types are not added to the list. --- src/backend/base/langflow/template/field/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index e825762301c..eca0dc805b1 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -1,5 +1,13 @@ from enum import Enum -from typing import Any, Callable, GenericAlias, Optional, Union, _GenericAlias, _UnionGenericAlias # type: ignore +from typing import ( + Any, + Callable, + GenericAlias, + Optional, # type: ignore + Union, + _GenericAlias, + _UnionGenericAlias, +) from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer, model_validator @@ -182,6 +190,8 @@ def to_dict(self): def add_types(self, _type: list[Any]): for type_ in _type: + if type_ in self.types: + continue if self.types is None: self.types = [] self.types.append(type_) From 7acc1dd24fe6b9003c34018934bdf2231ec3484c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 18:58:06 -0300 Subject: [PATCH 057/100] update starter projects --- .../initial_setup/starter_projects/Complex Agent.json | 6 ++++-- .../initial_setup/starter_projects/Hierarchical Agent.json | 6 ++++-- .../initial_setup/starter_projects/Sequential Agent.json | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json index 628ba2045b7..08d6086151d 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json @@ -4037,7 +4037,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -4048,7 +4049,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json index 8a0e18ee230..7a48edba9ae 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json @@ -2615,7 +2615,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -2626,7 +2627,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json index b1a70afc683..5364b52fd01 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json @@ -2953,7 +2953,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -2964,7 +2965,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } From 4be549f43bde8399576cc1e4b8295b0348a2167e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 19:12:08 -0300 Subject: [PATCH 058/100] refactor: optimize imports in base.py Optimize imports in the `base.py` file by removing unused imports and organizing the remaining imports. This change improves code readability and reduces unnecessary clutter. --- src/backend/base/langflow/template/field/base.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index eca0dc805b1..6395e680af8 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -1,13 +1,8 @@ from enum import Enum -from typing import ( - Any, - Callable, - GenericAlias, - Optional, # type: ignore - Union, - _GenericAlias, - _UnionGenericAlias, -) +from typing import GenericAlias # type: ignore +from typing import _GenericAlias # type: ignore +from typing import _UnionGenericAlias # type: ignore +from typing import Any, Callable, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer, model_validator From 61bdbb1e574ed42a9facb314f94789d9d527b888 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 19:12:19 -0300 Subject: [PATCH 059/100] fix(base.py): fix condition to check if self.types is not None before checking if type_ is in self.types --- src/backend/base/langflow/template/field/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index 6395e680af8..79ca2d3d9a6 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -185,7 +185,7 @@ def to_dict(self): def add_types(self, _type: list[Any]): for type_ in _type: - if type_ in self.types: + if self.types and type_ in self.types: continue if self.types is None: self.types = [] From cb4bb932edcfb460af94c50a3cfcba385053400e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 19:48:26 -0300 Subject: [PATCH 060/100] refactor: update build_custom_component_template to use add_name instead of keep_name --- src/backend/base/langflow/custom/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index f309ee97026..48beb3de7a0 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -415,7 +415,7 @@ def build_custom_component_template( reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(add_name=False), custom_instance + return frontend_node.to_dict(keep_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc From 941bcc43ed84a0d59c365fa4b23389e7ffdb4656 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:04:07 -0300 Subject: [PATCH 061/100] refactor(prompts): Update PromptComponent to support custom fields and template updates The `PromptComponent` class in `Prompt.py` has been updated to support custom fields and template updates. The `_update_template` method has been added to update the prompt template with custom fields. The `post_code_processing` method has been modified to update the template and improve backwards compatibility. The `_get_fallback_input` method has been added to provide a default prompt field. These changes improve the functionality and flexibility of the codebase. --- .../base/langflow/components/prompts/Prompt.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/backend/base/langflow/components/prompts/Prompt.py b/src/backend/base/langflow/components/prompts/Prompt.py index 400bbfc49a1..83d590ecf3d 100644 --- a/src/backend/base/langflow/components/prompts/Prompt.py +++ b/src/backend/base/langflow/components/prompts/Prompt.py @@ -2,6 +2,7 @@ from langflow.custom import Component from langflow.io import Output, PromptInput from langflow.schema.message import Message +from langflow.template.field.prompt import DefaultPromptField from langflow.template.utils import update_template_values @@ -27,12 +28,25 @@ async def build_prompt( self.status = prompt.text return prompt + def _update_template(self, frontend_node: dict): + prompt_template = frontend_node["template"]["template"]["value"] + custom_fields = frontend_node["custom_fields"] + frontend_node_template = frontend_node["template"] + _ = process_prompt_template( + template=prompt_template, + name="template", + custom_fields=custom_fields, + frontend_node_template=frontend_node_template, + ) + return frontend_node + def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict): """ This function is called after the code validation is done. """ frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node) template = frontend_node["template"]["template"]["value"] + # Kept it duplicated for backwards compatibility _ = process_prompt_template( template=template, name="template", @@ -43,3 +57,6 @@ def post_code_processing(self, new_frontend_node: dict, current_frontend_node: d # and update the frontend_node with those values update_template_values(new_template=frontend_node, previous_template=current_frontend_node["template"]) return frontend_node + + def _get_fallback_input(self, **kwargs): + return DefaultPromptField(**kwargs) From 9648ee47100d61274ef9bf496bb6d7a4fe3b55ea Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:09:36 -0300 Subject: [PATCH 062/100] refactor(base): Add DefaultPromptField to langflow.io The `DefaultPromptField` class has been added to the `langflow.io` module. This class provides a default prompt field for the `TableInput` class. This change improves the functionality and flexibility of the codebase. --- src/backend/base/langflow/inputs/__init__.py | 2 ++ src/backend/base/langflow/io/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index 0afcacb0678..00842931cfe 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -17,6 +17,7 @@ SecretStrInput, StrInput, TableInput, + DefaultPromptField, ) __all__ = [ @@ -38,4 +39,5 @@ "StrInput", "MessageTextInput", "TableInput", + "DefaultPromptField", ] diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index 063f030ffa5..8d26e7ed75e 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -17,6 +17,7 @@ SecretStrInput, StrInput, TableInput, + DefaultPromptField, ) from langflow.template import Output @@ -40,4 +41,5 @@ "MessageTextInput", "Output", "TableInput", + "DefaultPromptField", ] From 64573c9af8598b4dbf08fc8507e0d1c81044c892 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:10:06 -0300 Subject: [PATCH 063/100] refactor(prompts): Update PromptComponent to support custom fields and template updates --- src/backend/base/langflow/components/prompts/Prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/prompts/Prompt.py b/src/backend/base/langflow/components/prompts/Prompt.py index 83d590ecf3d..512398b0d38 100644 --- a/src/backend/base/langflow/components/prompts/Prompt.py +++ b/src/backend/base/langflow/components/prompts/Prompt.py @@ -1,8 +1,8 @@ from langflow.base.prompts.api_utils import process_prompt_template from langflow.custom import Component +from langflow.inputs.inputs import DefaultPromptField from langflow.io import Output, PromptInput from langflow.schema.message import Message -from langflow.template.field.prompt import DefaultPromptField from langflow.template.utils import update_template_values From 79ec97ee268cf8f38f425043c35bbcb4d5fe6c7a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:13:18 -0300 Subject: [PATCH 064/100] refactor(base): Update langflow.template.field.prompt.py for backwards compatibility --- .../base/langflow/template/field/prompt.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/backend/base/langflow/template/field/prompt.py b/src/backend/base/langflow/template/field/prompt.py index 6ae396cf5be..8592bc2e1d6 100644 --- a/src/backend/base/langflow/template/field/prompt.py +++ b/src/backend/base/langflow/template/field/prompt.py @@ -1,16 +1,3 @@ -from typing import Optional - -from langflow.template.field.base import Input - -DEFAULT_PROMPT_INTUT_TYPES = ["Message", "Text"] - - -class DefaultPromptField(Input): - name: str - display_name: Optional[str] = None - field_type: str = "str" - - advanced: bool = False - multiline: bool = True - input_types: list[str] = DEFAULT_PROMPT_INTUT_TYPES - value: str = "" # Set the value to empty string +# This file is for backwards compatibility +from langflow.inputs import DEFAULT_PROMPT_INTUT_TYPES # noqa +from langflow.inputs import DefaultPromptField # noqa From 846ba0e4e7511e1c92ccfc620d488e7dda2331b1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:17:26 -0300 Subject: [PATCH 065/100] refactor(base): Update langflow.components.__init__.py to import the prompts module This change adds the prompts module to the list of imports in the __init__.py file of the langflow.components package. This ensures that the prompts module is available for use in the codebase. --- .../base/langflow/components/__init__.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/backend/base/langflow/components/__init__.py b/src/backend/base/langflow/components/__init__.py index 4b64a59e68b..7b30052ec6c 100644 --- a/src/backend/base/langflow/components/__init__.py +++ b/src/backend/base/langflow/components/__init__.py @@ -1,9 +1,31 @@ +from . import ( + agents, + chains, + documentloaders, + embeddings, + helpers, + inputs, + memories, + models, + outputs, + prompts, + prototypes, + retrievers, + textsplitters, + toolkits, + tools, + vectorstores, +) + __all__ = [ "agents", "chains", "documentloaders", "embeddings", + "prompts", "prototypes", + "models", + "helpers", "inputs", "memories", "outputs", From 051a4c3360e873958b509685c8ae0d57af838140 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:42:57 -0300 Subject: [PATCH 066/100] refactor(base): Update langflow.template.field.prompt.py for backwards compatibility --- src/backend/base/langflow/template/field/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/field/prompt.py b/src/backend/base/langflow/template/field/prompt.py index 8592bc2e1d6..5ad43946ebb 100644 --- a/src/backend/base/langflow/template/field/prompt.py +++ b/src/backend/base/langflow/template/field/prompt.py @@ -1,3 +1,3 @@ # This file is for backwards compatibility -from langflow.inputs import DEFAULT_PROMPT_INTUT_TYPES # noqa +from langflow.inputs.inputs import DEFAULT_PROMPT_INTUT_TYPES # noqa from langflow.inputs import DefaultPromptField # noqa From 4a97dcc7f650f507820752dbaedc1bfa4a910a30 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:25:05 -0300 Subject: [PATCH 067/100] refactor(graph): Update VertexTypesDict to import vertex types lazily The VertexTypesDict class in constants.py has been updated to import vertex types lazily. This change improves the performance of the codebase by deferring the import until it is actually needed. --- .../base/langflow/graph/graph/constants.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/constants.py b/src/backend/base/langflow/graph/graph/constants.py index ca04e81c6fd..740a7e44321 100644 --- a/src/backend/base/langflow/graph/graph/constants.py +++ b/src/backend/base/langflow/graph/graph/constants.py @@ -1,11 +1,25 @@ from langflow.graph.schema import CHAT_COMPONENTS -from langflow.graph.vertex import types from langflow.utils.lazy_load import LazyLoadDictBase +class Finish: + def __bool__(self): + return True + + def __eq__(self, other): + return isinstance(other, Finish) + + +def _import_vertex_types(): + from langflow.graph.vertex import types + + return types + + class VertexTypesDict(LazyLoadDictBase): def __init__(self): self._all_types_dict = None + self._types = _import_vertex_types() @property def VERTEX_TYPE_MAP(self): @@ -20,13 +34,13 @@ def _build_dict(self): 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}, + **{t: self._types.CustomComponentVertex for t in ["CustomComponent"]}, + **{t: self._types.ComponentVertex for t in ["Component"]}, + **{t: self._types.InterfaceVertex for t in CHAT_COMPONENTS}, } def get_custom_component_vertex_type(self): - return types.CustomComponentVertex + return self._types.CustomComponentVertex lazy_load_vertex_dict = VertexTypesDict() From 252ca15d87e50b0f9efca923459cf879cbd27caa Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:25:42 -0300 Subject: [PATCH 068/100] refactor(graph): Add missing attributes and lock to Graph class The Graph class in base.py has been updated to add missing attributes and a lock. This change ensures that the necessary attributes are initialized and provides thread safety with the addition of a lock. It improves the functionality and reliability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index f027dc281e4..ce255a730c1 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -69,6 +69,17 @@ def __init__( self.vertices: List[Vertex] = [] self.run_manager = RunnableVerticesManager() self.state_manager = GraphStateManager() + self._vertices: List[dict] = [] + self._edges: List[EdgeData] = [] + self.top_level_vertices: List[str] = [] + self.vertex_map: Dict[str, Vertex] = {} + self.predecessor_map: Dict[str, List[str]] = defaultdict(list) + self.successor_map: Dict[str, List[str]] = defaultdict(list) + self.in_degree_map: Dict[str, int] = defaultdict(int) + self.parent_child_map: Dict[str, List[str]] = defaultdict(list) + self._run_queue: deque[str] = deque() + self._first_layer: List[str] = [] + self._lock = asyncio.Lock() try: self.tracing_service: "TracingService" | None = get_tracing_service() except Exception as exc: From 306ee86eb2641792148fda71ff351d4982771461 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:27:58 -0300 Subject: [PATCH 069/100] refactor(graph): Add method to set inputs in Graph class The `_set_inputs` method has been added to the Graph class in base.py. This method allows for setting inputs for specific vertices based on input components, inputs, and input type. It improves the functionality and flexibility of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index ce255a730c1..4fe3e7466a9 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -313,6 +313,20 @@ def define_vertices_lists(self): if getattr(vertex, attribute): getattr(self, f"_{attribute}_vertices").append(vertex.id) + def _set_inputs(self, input_components: list[str], inputs: Dict[str, str], input_type: InputType | None): + for vertex_id in self._is_input_vertices: + vertex = self.get_vertex(vertex_id) + # If the vertex is not in the input_components list + if input_components and (vertex_id not in input_components and vertex.display_name not in input_components): + continue + # If the input_type is not any and the input_type is not in the vertex id + # Example: input_type = "chat" and vertex.id = "OpenAI-19ddn" + elif input_type is not None and input_type != "any" and input_type not in vertex.id.lower(): + continue + if vertex is None: + raise ValueError(f"Vertex {vertex_id} not found") + vertex.update_raw_params(inputs, overwrite=True) + async def _run( self, inputs: Dict[str, str], From 23d1546cc9f520f2eb0644c756926f0ff8f83496 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:28:11 -0300 Subject: [PATCH 070/100] refactor(graph): Set inputs for specific vertices in Graph class The `_set_inputs` method has been added to the Graph class in base.py. This method allows for setting inputs for specific vertices based on input components, inputs, and input type. It improves the functionality and flexibility of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 4fe3e7466a9..2f8ae5bd56f 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -359,20 +359,7 @@ async def _run( if not isinstance(inputs.get(INPUT_FIELD_NAME, ""), str): raise ValueError(f"Invalid input value: {inputs.get(INPUT_FIELD_NAME)}. Expected string") if inputs: - for vertex_id in self._is_input_vertices: - vertex = self.get_vertex(vertex_id) - # If the vertex is not in the input_components list - if input_components and ( - vertex_id not in input_components and vertex.display_name not in input_components - ): - continue - # If the input_type is not any and the input_type is not in the vertex id - # Example: input_type = "chat" and vertex.id = "OpenAI-19ddn" - elif input_type is not None and input_type != "any" and input_type not in vertex.id.lower(): - continue - if vertex is None: - raise ValueError(f"Vertex {vertex_id} not found") - vertex.update_raw_params(inputs, overwrite=True) + self._set_inputs(input_components, inputs, input_type) # Update all the vertices with the session_id for vertex_id in self._has_session_id_vertices: vertex = self.get_vertex(vertex_id) From d191d1cfcd71ab437a246a92745f25baefdfc016 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:28:24 -0300 Subject: [PATCH 071/100] refactor(graph): Update Graph class to set cache using flow_id The `Graph` class in `base.py` has been updated to set the cache using the `flow_id` attribute. This change ensures that the cache is properly set when `cache` is enabled and `flow_id` is not None. It improves the functionality and reliability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 2f8ae5bd56f..f64dd551430 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1038,9 +1038,9 @@ async def get_next_runnable_vertices(self, lock: asyncio.Lock, vertex: "Vertex", next_runnable_vertices.remove(v_id) else: self.run_manager.add_to_vertices_being_run(next_v_id) - if cache and self.flow_id: - set_cache_coro = partial(get_chat_service().set_cache, self.flow_id) - await set_cache_coro(self, lock) + if cache and self.flow_id is not None: + set_cache_coro = partial(get_chat_service().set_cache, key=self.flow_id) + await set_cache_coro(data=self, lock=lock) return next_runnable_vertices async def _execute_tasks(self, tasks: List[asyncio.Task], lock: asyncio.Lock) -> List[str]: From 4d34a89f5580aa633d8fbf900956459707cda5a0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:29:17 -0300 Subject: [PATCH 072/100] refactor(graph): Refactor Graph class to improve edge building The `Graph` class in `base.py` has been refactored to improve the process of building edges. The `build_edge` method has been added to encapsulate the logic of creating a `ContractEdge` object from the given `EdgeData`. This change enhances the readability and maintainability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index f64dd551430..63374996ce7 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1186,19 +1186,22 @@ def _build_edges(self) -> List[ContractEdge]: edges: set[ContractEdge] = set() for edge in self._edges: - source = self.get_vertex(edge["source"]) - target = self.get_vertex(edge["target"]) - - if source is None: - raise ValueError(f"Source vertex {edge['source']} not found") - if target is None: - raise ValueError(f"Target vertex {edge['target']} not found") - new_edge = ContractEdge(source, target, edge) - + new_edge = self.build_edge(edge) edges.add(new_edge) return list(edges) + def build_edge(self, edge: EdgeData) -> ContractEdge: + source = self.get_vertex(edge["source"]) + target = self.get_vertex(edge["target"]) + + if source is None: + raise ValueError(f"Source vertex {edge['source']} not found") + if target is None: + raise ValueError(f"Target vertex {edge['target']} not found") + new_edge = ContractEdge(source, target, edge) + return new_edge + def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]: """Returns the node class based on the node type.""" # First we check for the node_base_type From e0e46251d9fa50cd363a99cf7390b5ca42e22aa0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:29:45 -0300 Subject: [PATCH 073/100] refactor(graph): Update _create_vertex method parameter name for clarity The `_create_vertex` method in the `Graph` class of `base.py` has been updated to change the parameter name from `vertex` to `frontend_data` for improved clarity. This change enhances the readability and maintainability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 63374996ce7..5637219e097 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1232,8 +1232,8 @@ def _build_vertices(self) -> List[Vertex]: return vertices - def _create_vertex(self, vertex: dict): - vertex_data = vertex["data"] + def _create_vertex(self, frontend_data: dict): + vertex_data = frontend_data["data"] vertex_type: str = vertex_data["type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore if "id" not in vertex_data: @@ -1241,7 +1241,7 @@ def _create_vertex(self, vertex: dict): VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"]) - vertex_instance = VertexClass(vertex, graph=self) + vertex_instance = VertexClass(frontend_data, graph=self) vertex_instance.set_top_level(self.top_level_vertices) return vertex_instance From 46cd367cf1bf1e94a3e8e2acc06fa112ca600661 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:30:03 -0300 Subject: [PATCH 074/100] refactor(graph): Update Graph class to return first layer in sort_interface_components_first The `sort_interface_components_first` method in the `Graph` class of `base.py` has been updated to return just the first layer of vertices. This change improves the functionality of the codebase by providing a more focused and efficient sorting mechanism. --- src/backend/base/langflow/graph/graph/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 5637219e097..02b6b948bee 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1459,6 +1459,7 @@ def sort_vertices( self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)} self.build_run_map() # Return just the first layer + self._first_layer = first_layer return first_layer def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]: From 1b900b22f52dd88b1557a984c26ee1c550bc392c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:30:26 -0300 Subject: [PATCH 075/100] refactor(graph): Update Graph class to use get_vertex method for building vertices The _build_vertices method in the Graph class of base.py has been updated to use the get_vertex method instead of creating a new vertex instance. This change improves the efficiency and maintainability of the codebase. --- src/backend/base/langflow/graph/graph/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 02b6b948bee..fabbc723edd 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1226,8 +1226,11 @@ def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) - def _build_vertices(self) -> List[Vertex]: """Builds the vertices of the graph.""" vertices: List[Vertex] = [] - for vertex in self._vertices: - vertex_instance = self._create_vertex(vertex) + for frontend_data in self._vertices: + try: + vertex_instance = self.get_vertex(frontend_data["id"]) + except ValueError: + vertex_instance = self._create_vertex(frontend_data) vertices.append(vertex_instance) return vertices From 9017a8185469ee1dc1cf95fd53787bd24e0f6289 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:31:00 -0300 Subject: [PATCH 076/100] refactor(graph): Update Graph class to use astep method for asynchronous execution The `Graph` class in `base.py` has been updated to use the `astep` method for asynchronous execution of vertices. This change improves the efficiency and maintainability of the codebase by leveraging asyncio and allowing for concurrent execution of vertices. --- src/backend/base/langflow/graph/graph/base.py | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index fabbc723edd..bc234de76fe 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -868,6 +868,70 @@ def get_root_of_group_node(self, vertex_id: str) -> Vertex: return vertex raise ValueError(f"Vertex {vertex_id} is not a top level vertex or no root vertex found") + async def astep( + self, + inputs: Optional["InputValueRequest"] = None, + files: Optional[list[str]] = None, + user_id: Optional[str] = None, + ): + if not self._prepared: + raise ValueError("Graph not prepared. Call prepare() first.") + if not self._run_queue: + asyncio.create_task(self.end_all_traces()) + return Finish() + vertex_id = self._run_queue.popleft() + chat_service = get_chat_service() + vertex_build_result = await self.build_vertex( + vertex_id=vertex_id, + user_id=user_id, + inputs_dict=inputs.model_dump() if inputs else {}, + files=files, + get_cache=chat_service.get_cache, + set_cache=chat_service.set_cache, + ) + + next_runnable_vertices = await self.get_next_runnable_vertices( + self._lock, vertex=vertex_build_result.vertex, cache=False + ) + if self.stop_vertex and self.stop_vertex in next_runnable_vertices: + next_runnable_vertices = [self.stop_vertex] + self._run_queue.extend(next_runnable_vertices) + self.reset_inactivated_vertices() + self.reset_activated_vertices() + + await chat_service.set_cache(str(self.flow_id or self._run_id), self) + return vertex_build_result + + def step( + self, + inputs: Optional["InputValueRequest"] = None, + files: Optional[list[str]] = None, + user_id: Optional[str] = None, + ): + # Call astep but synchronously + loop = asyncio.get_event_loop() + return loop.run_until_complete(self.astep(inputs, files, user_id)) + + def prepare(self, stop_component_id: Optional[str] = None, start_component_id: Optional[str] = None): + if stop_component_id and start_component_id: + raise ValueError("You can only provide one of stop_component_id or start_component_id") + self.validate_stream() + self.edges = self._build_edges() + if stop_component_id or start_component_id: + try: + first_layer = self.sort_vertices(stop_component_id, start_component_id) + except Exception as exc: + logger.error(exc) + first_layer = self.sort_vertices() + else: + first_layer = self.sort_vertices() + + for vertex_id in first_layer: + self.run_manager.add_to_vertices_being_run(vertex_id) + self._run_queue = deque(first_layer) + self._prepared = True + return self + async def build_vertex( self, vertex_id: str, @@ -1248,26 +1312,6 @@ def _create_vertex(self, frontend_data: dict): vertex_instance.set_top_level(self.top_level_vertices) return vertex_instance - def prepare(self, stop_component_id: Optional[str] = None, start_component_id: Optional[str] = None): - if stop_component_id and start_component_id: - raise ValueError("You can only provide one of stop_component_id or start_component_id") - self.validate_stream() - self.edges = self._build_edges() - if stop_component_id or start_component_id: - try: - first_layer = self.sort_vertices(stop_component_id, start_component_id) - except Exception as exc: - logger.error(exc) - first_layer = self.sort_vertices() - else: - first_layer = self.sort_vertices() - - for vertex_id in first_layer: - self.run_manager.add_to_vertices_being_run(vertex_id) - self._run_queue = deque(first_layer) - self._prepared = True - return self - def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]: """Returns the children of a vertex based on the vertex type.""" children = [] From c901a2120415c5a63675bc59314d5edf0da28118 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:33:14 -0300 Subject: [PATCH 077/100] feat(base.py): implement methods to add components and component edges in the Graph class --- src/backend/base/langflow/graph/graph/base.py | 114 +++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index bc234de76fe..c5f6defaddc 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -11,14 +11,14 @@ from langflow.exceptions.component import ComponentBuildException from langflow.graph.edge.base import ContractEdge from langflow.graph.edge.schema import EdgeData -from langflow.graph.graph.constants import lazy_load_vertex_dict +from langflow.graph.graph.constants import Finish, lazy_load_vertex_dict from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager from langflow.graph.graph.schema import VertexBuildResult from langflow.graph.graph.state_manager import GraphStateManager from langflow.graph.graph.utils import find_start_component_id, process_flow, sort_up_to_vertex from langflow.graph.schema import InterfaceComponentTypes, RunOutputs from langflow.graph.vertex.base import Vertex, VertexStates -from langflow.graph.vertex.types import InterfaceVertex, StateVertex +from langflow.graph.vertex.types import ComponentVertex, InterfaceVertex, StateVertex from langflow.schema import Data from langflow.schema.schema import INPUT_FIELD_NAME, InputType from langflow.services.cache.utils import CacheMiss @@ -26,6 +26,8 @@ from langflow.services.deps import get_chat_service, get_tracing_service if TYPE_CHECKING: + from langflow.api.v1.schemas import InputValueRequest + from langflow.custom.custom_component.component import Component from langflow.graph.schema import ResultData from langflow.services.tracing.service import TracingService @@ -35,6 +37,8 @@ class Graph: def __init__( self, + start: Optional["Component"] = None, + end: Optional["Component"] = None, flow_id: Optional[str] = None, flow_name: Optional[str] = None, user_id: Optional[str] = None, @@ -47,6 +51,7 @@ def __init__( edges (List[Dict[str, str]]): A list of dictionaries representing the edges of the graph. flow_id (Optional[str], optional): The ID of the flow. Defaults to None. """ + self._prepared = False self._runs = 0 self._updates = 0 self.flow_id = flow_id @@ -85,6 +90,11 @@ def __init__( except Exception as exc: logger.error(f"Error getting tracing service: {exc}") self.tracing_service = None + if start is not None and end is not None: + self._set_start_and_end(start, end) + self.prepare() + if (start is not None and end is None) or (start is None and end is not None): + raise ValueError("You must provide both input and output components") def add_nodes_and_edges(self, nodes: List[Dict], edges: List[EdgeData]): self._vertices = nodes @@ -100,11 +110,111 @@ def add_nodes_and_edges(self, nodes: List[Dict], edges: List[EdgeData]): self._edges = self._graph_data["edges"] self.initialize() + def add_component(self, _id: str, component: "Component"): + if _id in self.vertex_map: + return + frontend_node = component.to_frontend_node() + frontend_node["data"]["id"] = _id + frontend_node["id"] = _id + self._vertices.append(frontend_node) + vertex = self._create_vertex(frontend_node) + vertex.add_component_instance(component) + self.vertices.append(vertex) + self.vertex_map[_id] = vertex + + if component._edges: + for edge in component._edges: + self._add_edge(edge) + + if component._components: + for _component in component._components: + self.add_component(_component._id, _component) + + def _set_start_and_end(self, start: "Component", end: "Component"): + if not hasattr(start, "to_frontend_node"): + raise TypeError(f"start must be a Component. Got {type(start)}") + if not hasattr(end, "to_frontend_node"): + raise TypeError(f"end must be a Component. Got {type(end)}") + self.add_component(start._id, start) + self.add_component(end._id, end) + + def add_component_edge(self, source_id: str, output_input_tuple: Tuple[str, str], target_id: str): + source_vertex = self.get_vertex(source_id) + if not isinstance(source_vertex, ComponentVertex): + raise ValueError(f"Source vertex {source_id} is not a component vertex.") + target_vertex = self.get_vertex(target_id) + if not isinstance(target_vertex, ComponentVertex): + raise ValueError(f"Target vertex {target_id} is not a component vertex.") + output_name, input_name = output_input_tuple + edge_data: EdgeData = { + "source": source_id, + "target": target_id, + "data": { + "sourceHandle": { + "dataType": source_vertex.base_name, + "id": source_vertex.id, + "name": output_name, + "output_types": source_vertex.get_output(output_name).types, + }, + "targetHandle": { + "fieldName": input_name, + "id": target_vertex.id, + "inputTypes": target_vertex.get_input(input_name).input_types, + "type": str(target_vertex.get_input(input_name).field_type), + }, + }, + } + self._add_edge(edge_data) + + async def async_start(self, inputs: Optional[List[dict]] = None): + if not self._prepared: + raise ValueError("Graph not prepared. Call prepare() first.") + # The idea is for this to return a generator that yields the result of + # each step call and raise StopIteration when the graph is done + for _input in inputs or []: + for key, value in _input.items(): + vertex = self.get_vertex(key) + vertex.set_input_value(key, value) + while True: + result = await self.astep() + yield result + if isinstance(result, Finish): + return + + def start(self, inputs: Optional[List[dict]] = None) -> Generator: + #! Change this soon + nest_asyncio.apply() + loop = asyncio.get_event_loop() + async_gen = self.async_start(inputs) + async_gen_task = asyncio.ensure_future(async_gen.__anext__()) + + while True: + try: + result = loop.run_until_complete(async_gen_task) + yield result + if isinstance(result, Finish): + return + async_gen_task = asyncio.ensure_future(async_gen.__anext__()) + except StopAsyncIteration: + break + + def _add_edge(self, edge: EdgeData): + self.add_edge(edge) + source_id = edge["data"]["sourceHandle"]["id"] + target_id = edge["data"]["targetHandle"]["id"] + self.predecessor_map[target_id].append(source_id) + self.successor_map[source_id].append(target_id) + self.in_degree_map[target_id] += 1 + self.parent_child_map[source_id].append(target_id) + # TODO: Create a TypedDict to represente the node def add_node(self, node: dict): self._vertices.append(node) def add_edge(self, edge: EdgeData): + # Check if the edge already exists + if edge in self._edges: + return self._edges.append(edge) def initialize(self): From cd8dd2c30e11add2b86ef160d76391e5138eeaaa Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:33:20 -0300 Subject: [PATCH 078/100] refactor(graph): Import nest_asyncio for asynchronous execution in Graph class --- src/backend/base/langflow/graph/graph/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index c5f6defaddc..8a8836b8dfd 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -6,6 +6,7 @@ from itertools import chain from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple, Type, Union +import nest_asyncio from loguru import logger from langflow.exceptions.component import ComponentBuildException From 2424bdc5671d15c7508282b7c8d42a1cf0da7563 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:56:55 -0300 Subject: [PATCH 079/100] refactor(base.py): Update Vertex class to handle parameter dictionaries in build_params The `build_params` method in the `Vertex` class of `base.py` has been updated to handle parameter dictionaries correctly. If the `param_dict` is empty or has more than one key, the method now sets the parameter value to the vertex that is the source of the edge. Otherwise, it sets the parameter value to a dictionary with keys corresponding to the keys in the `param_dict` and values as the vertex that is the source of the edge. This change improves the functionality and maintainability of the codebase. --- src/backend/base/langflow/graph/vertex/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index c12bdd88d84..f5f09ac4ac9 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -289,12 +289,13 @@ def _build_params(self): # we don't know the key of the dict but we need to set the value # to the vertex that is the source of the edge param_dict = template_dict[param_key]["value"] - if param_dict: + if not param_dict or len(param_dict) != 1: + params[param_key] = self.graph.get_vertex(edge.source_id) + else: params[param_key] = { key: self.graph.get_vertex(edge.source_id) for key in param_dict.keys() } - else: - params[param_key] = self.graph.get_vertex(edge.source_id) + else: params[param_key] = self.graph.get_vertex(edge.source_id) From fbe5f3fbb45e9a07559eb05269928b79edb78936 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:57:10 -0300 Subject: [PATCH 080/100] refactor(base.py): Add methods to set input values and add component instances in Vertex class The `Vertex` class in `base.py` has been refactored to include two new methods: `set_input_value` and `add_component_instance`. The `set_input_value` method allows setting input values for a vertex by name, while the `add_component_instance` method adds a component instance to the vertex. These changes enhance the functionality and maintainability of the codebase. --- src/backend/base/langflow/graph/vertex/base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index f5f09ac4ac9..357e9dc2b90 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -96,6 +96,15 @@ def __init__( self.build_times: List[float] = [] self.state = VertexStates.ACTIVE + def set_input_value(self, name: str, value: Any): + if self._custom_component is None: + raise ValueError(f"Vertex {self.id} does not have a component instance.") + self._custom_component._set_input_value(name, value) + + def add_component_instance(self, component_instance: "Component"): + component_instance.set_vertex(self) + self._custom_component = component_instance + def add_result(self, name: str, result: Any): self.results[name] = result From 800c75934aad63dcd582f9e8bb7183763e783372 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:57:40 -0300 Subject: [PATCH 081/100] refactor(message.py): Update _timestamp_to_str to handle datetime or str input The `_timestamp_to_str` function in `message.py` has been updated to handle both `datetime` and `str` input. If the input is a `datetime` object, it will be formatted as a string using the "%Y-%m-%d %H:%M:%S" format. If the input is already a string, it will be returned as is. This change improves the flexibility and usability of the function. --- src/backend/base/langflow/schema/message.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/schema/message.py b/src/backend/base/langflow/schema/message.py index f62d4a9abe8..49d096c1a35 100644 --- a/src/backend/base/langflow/schema/message.py +++ b/src/backend/base/langflow/schema/message.py @@ -23,8 +23,10 @@ ) -def _timestamp_to_str(timestamp: datetime) -> str: - return timestamp.strftime("%Y-%m-%d %H:%M:%S") +def _timestamp_to_str(timestamp: datetime | str) -> str: + if isinstance(timestamp, datetime): + return timestamp.strftime("%Y-%m-%d %H:%M:%S") + return timestamp class Message(Data): From c97b8c78851b3cf028cac73abaae479ac668f920 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 21:57:53 -0300 Subject: [PATCH 082/100] refactor(test_base.py): Add unit tests for Graph class Unit tests have been added to the `test_base.py` file to test the functionality of the `Graph` class. These tests ensure that the graph is prepared correctly, components are added and connected properly, and the graph executes as expected. This change improves the reliability and maintainability of the codebase. --- .../tests/unit/graph/graph/test_base.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/backend/tests/unit/graph/graph/test_base.py diff --git a/src/backend/tests/unit/graph/graph/test_base.py b/src/backend/tests/unit/graph/graph/test_base.py new file mode 100644 index 00000000000..68ecdff7a67 --- /dev/null +++ b/src/backend/tests/unit/graph/graph/test_base.py @@ -0,0 +1,131 @@ +from collections import deque + +import pytest + +from langflow.components.inputs.ChatInput import ChatInput +from langflow.components.outputs.ChatOutput import ChatOutput +from langflow.components.outputs.TextOutput import TextOutputComponent +from langflow.graph.graph.base import Graph +from langflow.graph.graph.constants import Finish + + +@pytest.fixture +def client(): + pass + + +@pytest.mark.asyncio +async def test_graph_not_prepared(): + chat_input = ChatInput() + chat_output = ChatOutput() + graph = Graph() + graph.add_component("chat_input", chat_input) + graph.add_component("chat_output", chat_output) + graph.add_component_edge("chat_input", (chat_input.outputs[0].name, chat_input.inputs[0].name), "chat_output") + with pytest.raises(ValueError): + await graph.astep() + + +@pytest.mark.asyncio +async def test_graph(): + chat_input = ChatInput() + chat_output = ChatOutput() + graph = Graph() + graph.add_component("chat_input", chat_input) + graph.add_component("chat_output", chat_output) + graph.add_component_edge("chat_input", (chat_input.outputs[0].name, chat_input.inputs[0].name), "chat_output") + graph.prepare() + assert graph._run_queue == deque(["chat_input"]) + await graph.astep() + assert graph._run_queue == deque(["chat_output"]) + + assert graph.vertices[0].id == "chat_input" + assert graph.vertices[1].id == "chat_output" + assert graph.edges[0].source_id == "chat_input" + assert graph.edges[0].target_id == "chat_output" + + +@pytest.mark.asyncio +async def test_graph_functional(): + chat_input = ChatInput(_id="chat_input") + chat_output = ChatOutput(input_value="test", _id="chat_output") + chat_output.set(sender_name=chat_input.message_response) + graph = Graph(chat_input, chat_output) + assert graph._run_queue == deque(["chat_input"]) + await graph.astep() + assert graph._run_queue == deque(["chat_output"]) + + assert graph.vertices[0].id == "chat_input" + assert graph.vertices[1].id == "chat_output" + assert graph.edges[0].source_id == "chat_input" + assert graph.edges[0].target_id == "chat_output" + + +@pytest.mark.asyncio +async def test_graph_functional_async_start(): + chat_input = ChatInput(_id="chat_input") + chat_output = ChatOutput(input_value="test", _id="chat_output") + chat_output.set(sender_name=chat_input.message_response) + graph = Graph(chat_input, chat_output) + # Now iterate through the graph + # and check that the graph is running + # correctly + ids = ["chat_input", "chat_output"] + results = [] + async for result in graph.async_start(): + results.append(result) + + assert len(results) == 3 + assert all(result.vertex.id in ids for result in results if hasattr(result, "vertex")) + assert results[-1] == Finish() + + +def test_graph_functional_start(): + chat_input = ChatInput(_id="chat_input") + chat_output = ChatOutput(input_value="test", _id="chat_output") + chat_output.set(sender_name=chat_input.message_response) + graph = Graph(chat_input, chat_output) + graph.prepare() + # Now iterate through the graph + # and check that the graph is running + # correctly + ids = ["chat_input", "chat_output"] + results = [] + for result in graph.start(): + results.append(result) + + assert len(results) == 3 + assert all(result.vertex.id in ids for result in results if hasattr(result, "vertex")) + assert results[-1] == Finish() + + +def test_graph_functional_start_end(): + chat_input = ChatInput(_id="chat_input") + text_output = TextOutputComponent(_id="text_output") + text_output.set(input_value=chat_input.message_response) + chat_output = ChatOutput(input_value="test", _id="chat_output") + chat_output.set(input_value=text_output.text_response) + graph = Graph(chat_input, text_output) + graph.prepare() + # Now iterate through the graph + # and check that the graph is running + # correctly + ids = ["chat_input", "text_output"] + results = [] + for result in graph.start(): + results.append(result) + + assert len(results) == len(ids) + 1 + assert all(result.vertex.id in ids for result in results if hasattr(result, "vertex")) + assert results[-1] == Finish() + # Now, using the same components but different start and end components + graph = Graph(chat_input, chat_output) + graph.prepare() + ids = ["chat_input", "chat_output", "text_output"] + results = [] + for result in graph.start(): + results.append(result) + + assert len(results) == len(ids) + 1 + assert all(result.vertex.id in ids for result in results if hasattr(result, "vertex")) + assert results[-1] == Finish() From a08cfdd0435950d852277e372f968060407bf8b5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:30:13 -0300 Subject: [PATCH 083/100] refactor(initialize/loading.py): Refactor get_instance_results function The `get_instance_results` function in `initialize/loading.py` has been refactored to simplify the logic for building custom components and components. The previous implementation had separate checks for `CustomComponent` and `Component` types, but the refactored version combines these checks into a single condition based on the `base_type` parameter. This change improves the readability and maintainability of the codebase. --- src/backend/base/langflow/interface/initialize/loading.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/interface/initialize/loading.py b/src/backend/base/langflow/interface/initialize/loading.py index ea2858100ac..f0ae915adc3 100644 --- a/src/backend/base/langflow/interface/initialize/loading.py +++ b/src/backend/base/langflow/interface/initialize/loading.py @@ -7,13 +7,13 @@ from loguru import logger from pydantic import PydanticDeprecatedSince20 -from langflow.custom import Component, CustomComponent from langflow.custom.eval import eval_custom_component_code from langflow.schema import Data from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.services.deps import get_tracing_service if TYPE_CHECKING: + from langflow.custom import Component, CustomComponent from langflow.graph.vertex.base import Vertex @@ -54,9 +54,9 @@ async def get_instance_results( ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20) - if base_type == "custom_components" and isinstance(custom_component, CustomComponent): + if base_type == "custom_components": return await build_custom_component(params=custom_params, custom_component=custom_component) - elif base_type == "component" and isinstance(custom_component, Component): + elif base_type == "component": return await build_component(params=custom_params, custom_component=custom_component) else: raise ValueError(f"Base type {base_type} not found.") From 73a1d25041798d57f76e551d948438dc03b1b5ea Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:42:03 -0300 Subject: [PATCH 084/100] refactor(component.py): Add set_input_value method to Component class The `set_input_value` method has been added to the `Component` class in `component.py`. This method allows setting the value of an input by name, and also updates the `load_from_db` attribute if applicable. This change enhances the functionality and maintainability of the codebase. --- .../base/langflow/custom/custom_component/component.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index c734ad2e27f..0ecbc2387ab 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -154,6 +154,14 @@ def set_output_value(self, name: str, value: Any): else: raise ValueError(f"Output {name} not found in {self.__class__.__name__}") + def set_input_value(self, name: str, value: Any): + if name in self._inputs: + self._inputs[name].value = value + if hasattr(self._inputs[name], "load_from_db"): + self._inputs[name].load_from_db = False + else: + raise ValueError(f"Input {name} not found in {self.__class__.__name__}") + def map_outputs(self, outputs: List[Output]): """ Maps the given list of outputs to the component. From cc23367c8a7e38b79a3018aa08fe9d9de62bb714 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:42:11 -0300 Subject: [PATCH 085/100] refactor(component.py): Set input value in _set_parameter_or_attribute method The `_set_parameter_or_attribute` method in the `Component` class now sets the input value using the `set_input_value` method. This change improves the clarity and consistency of the codebase. --- src/backend/base/langflow/custom/custom_component/component.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 0ecbc2387ab..1bbfe41656e 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -272,6 +272,7 @@ def _add_edge(self, component, key, output, _input): ) def _set_parameter_or_attribute(self, key, value): + self.set_input_value(key, value) self._parameters[key] = value self._attributes[key] = value From 9b5a761d969bd825319b81e8d334f96c9c604cbf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:42:19 -0300 Subject: [PATCH 086/100] refactor(inputs.py): Improve error message for invalid value type The `SecretStrInput` class in `inputs.py` has been updated to improve the error message when an invalid value type is encountered. Instead of a generic error message, the new message includes the specific value type and the name of the input. This change enhances the clarity and usability of the codebase. --- src/backend/base/langflow/inputs/inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index b9f5d9ca51a..01f332f749b 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -268,7 +268,7 @@ def validate_value(cls, v: Any, _info): elif isinstance(v, (AsyncIterator, Iterator)): value = v else: - raise ValueError(f"Invalid value type {type(v)}") + raise ValueError(f"Invalid value type `{type(v)}` for input `{_info.data['name']}`") return value From 7a002a4dd22c0b3de94944a8c187fdd51a960756 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:42:24 -0300 Subject: [PATCH 087/100] feat: Add unit test for memory chatbot functionality --- .../starter_projects/test_memory_chatbot.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py diff --git a/src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py b/src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py new file mode 100644 index 00000000000..3fd135de288 --- /dev/null +++ b/src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py @@ -0,0 +1,41 @@ +from collections import deque + +from langflow.components.helpers.Memory import MemoryComponent +from langflow.components.inputs.ChatInput import ChatInput +from langflow.components.models.OpenAIModel import OpenAIModelComponent +from langflow.components.outputs.ChatOutput import ChatOutput +from langflow.components.prompts.Prompt import PromptComponent +from langflow.graph import Graph +from langflow.graph.graph.constants import Finish + + +def test_memory_chatbot(): + session_id = "test_session_id" + template = """{context} + +User: {user_message} +AI: """ + memory_component = MemoryComponent(_id="chat_memory") + memory_component.set(session_id=session_id) + chat_input = ChatInput(_id="chat_input") + prompt_component = PromptComponent(_id="prompt") + prompt_component.set( + template=template, user_message=chat_input.message_response, context=memory_component.retrieve_messages_as_text + ) + openai_component = OpenAIModelComponent(_id="openai") + openai_component.set( + input_value=prompt_component.build_prompt, max_tokens=100, temperature=0.1, api_key="test_api_key" + ) + openai_component.get_output("text_output").value = "Mock response" + + chat_output = ChatOutput(_id="chat_output") + chat_output.set(input_value=openai_component.text_response) + + graph = Graph(chat_input, chat_output) + # Now we run step by step + expected_order = deque(["chat_input", "chat_memory", "prompt", "openai", "chat_output"]) + for step in expected_order: + result = graph.step() + if isinstance(result, Finish): + break + assert step == result.vertex.id From b821b221ff6b9361cba2a315679db25c44eec4d9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:58:28 -0300 Subject: [PATCH 088/100] refactor(base.py): Update __repr__ method in ContractEdge class The `__repr__` method in the `ContractEdge` class of `base.py` has been updated to include the source handle and target handle information when available. This change improves the readability and clarity of the representation of the edge in the codebase. --- src/backend/base/langflow/graph/edge/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/base/langflow/graph/edge/base.py b/src/backend/base/langflow/graph/edge/base.py index d7ae82de4d0..5653c5bc9e2 100644 --- a/src/backend/base/langflow/graph/edge/base.py +++ b/src/backend/base/langflow/graph/edge/base.py @@ -227,4 +227,6 @@ async def get_result_from_source(self, source: "Vertex", target: "Vertex"): return self.result def __repr__(self) -> str: + if self.source_handle and self.target_handle: + return f"{self.source_id} -[{self.source_handle.name}->{self.target_handle.fieldName}]-> {self.target_id}" return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" From 0f873ac21ee26e894a6ca40f77a43489d209defe Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:59:10 -0300 Subject: [PATCH 089/100] refactor(component.py): Update set method to return self The `set` method in the `Component` class of `component.py` has been updated to return `self` after processing the connection or parameter. This change improves the chaining capability of the method and enhances the readability and consistency of the codebase. --- src/backend/base/langflow/custom/custom_component/component.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 1bbfe41656e..b6fa164c911 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -80,6 +80,7 @@ def set(self, **kwargs): """ for key, value in kwargs.items(): self._process_connection_or_parameter(key, value) + return self def list_inputs(self): """ From 32452d05194c06e706607a7587dd8ba99a157ba4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 22:59:18 -0300 Subject: [PATCH 090/100] refactor(starter_projects): Add unit test for vector store RAG A unit test has been added to the `test_vector_store_rag.py` file in the `starter_projects` directory. This test ensures that the vector store RAG graph is set up correctly and produces the expected results. This change improves the reliability and maintainability of the codebase. --- .../starter_projects/test_vector_store_rag.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py diff --git a/src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py b/src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py new file mode 100644 index 00000000000..1482ed55621 --- /dev/null +++ b/src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py @@ -0,0 +1,89 @@ +from textwrap import dedent + +from langflow.components.data.File import FileComponent +from langflow.components.embeddings.OpenAIEmbeddings import OpenAIEmbeddingsComponent +from langflow.components.helpers.ParseData import ParseDataComponent +from langflow.components.helpers.SplitText import SplitTextComponent +from langflow.components.inputs.ChatInput import ChatInput +from langflow.components.models.OpenAIModel import OpenAIModelComponent +from langflow.components.outputs.ChatOutput import ChatOutput +from langflow.components.prompts.Prompt import PromptComponent +from langflow.components.vectorstores.AstraDB import AstraVectorStoreComponent +from langflow.graph.graph.base import Graph +from langflow.graph.graph.constants import Finish +from langflow.schema.data import Data + + +def test_vector_store_rag(): + # Ingestion Graph + file_component = FileComponent(_id="file-123") + file_component.set(path="test.txt") + text_splitter = SplitTextComponent(_id="text-splitter-123") + text_splitter.set(data_inputs=file_component.load_file) + openai_embeddings = OpenAIEmbeddingsComponent(_id="openai-embeddings-123") + openai_embeddings.set( + openai_api_key="sk-123", openai_api_base="https://api.openai.com/v1", openai_api_type="openai" + ) + vector_store = AstraVectorStoreComponent(_id="vector-store-123") + vector_store.set( + embedding=openai_embeddings.build_embeddings, + ingest_data=text_splitter.split_text, + api_endpoint="https://astra.example.com", + token="token", + ) + + # RAG Graph + chat_input = ChatInput(_id="chatinput-123") + chat_input.get_output("message").value = "What is the meaning of life?" + rag_vector_store = AstraVectorStoreComponent(_id="rag-vector-store-123") + rag_vector_store.set( + search_input=chat_input.message_response, + api_endpoint="https://astra.example.com", + token="token", + embedding=openai_embeddings.build_embeddings, + ) + # Mock search_documents + rag_vector_store.get_output("search_results").value = [ + Data(data={"text": "Hello, world!"}), + Data(data={"text": "Goodbye, world!"}), + ] + parse_data = ParseDataComponent(_id="parse-data-123") + parse_data.set(data=rag_vector_store.search_documents) + prompt_component = PromptComponent(_id="prompt-123") + prompt_component.set( + template=dedent("""Given the following context, answer the question. + Context:{context} + + Question: {question} + Answer:"""), + context=parse_data.parse_data, + question=chat_input.message_response, + ) + + openai_component = OpenAIModelComponent(_id="openai-123") + openai_component.set(api_key="sk-123", openai_api_base="https://api.openai.com/v1") + openai_component.set_output_value("text_output", "Hello, world!") + openai_component.set(input_value=prompt_component.build_prompt) + + chat_output = ChatOutput(_id="chatoutput-123") + chat_output.set(input_value=openai_component.text_response) + + graph = Graph(start=chat_input, end=chat_output) + assert graph is not None + ids = [ + "chatinput-123", + "chatoutput-123", + "openai-123", + "parse-data-123", + "prompt-123", + "rag-vector-store-123", + "openai-embeddings-123", + ] + results = [] + for result in graph.start(): + results.append(result) + + assert len(results) == 8 + vids = [result.vertex.id for result in results if hasattr(result, "vertex")] + assert all(vid in ids for vid in vids), f"Diff: {set(vids) - set(ids)}" + assert results[-1] == Finish() From 39eae1bf8512831404da8a012a9e82385ec23463 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 08:22:05 -0300 Subject: [PATCH 091/100] refactor: remove unused prepare method in Graph class --- src/backend/base/langflow/graph/graph/base.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index bbce6edbee0..e48519cb4c2 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1023,26 +1023,6 @@ def step( loop = asyncio.get_event_loop() return loop.run_until_complete(self.astep(inputs, files, user_id)) - def prepare(self, stop_component_id: Optional[str] = None, start_component_id: Optional[str] = None): - if stop_component_id and start_component_id: - raise ValueError("You can only provide one of stop_component_id or start_component_id") - self.validate_stream() - self.edges = self._build_edges() - if stop_component_id or start_component_id: - try: - first_layer = self.sort_vertices(stop_component_id, start_component_id) - except Exception as exc: - logger.error(exc) - first_layer = self.sort_vertices() - else: - first_layer = self.sort_vertices() - - for vertex_id in first_layer: - self.run_manager.add_to_vertices_being_run(vertex_id) - self._run_queue = deque(first_layer) - self._prepared = True - return self - async def build_vertex( self, vertex_id: str, From a21dac27a9b83c954eec5d803e19321de76e63b2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 08:22:13 -0300 Subject: [PATCH 092/100] refactor: update Output class to use list[str] for types field --- src/backend/base/langflow/template/field/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index 79ca2d3d9a6..f3583e466da 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -158,7 +158,7 @@ def validate_type(cls, v): class Output(BaseModel): - types: Optional[list[str]] = Field(default=[]) + types: list[str] = Field(default=[]) """List of output types for the field.""" selected: Optional[str] = Field(default=None) From 7b6b629929853d25441eaf99b4c2d935f4cdf9d3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 09:30:50 -0300 Subject: [PATCH 093/100] refactor: add name validation to BaseInputMixin --- src/backend/base/langflow/inputs/input_mixin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index f772a256ac0..747287541ac 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -75,6 +75,13 @@ class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore def to_dict(self): return self.model_dump(exclude_none=True, by_alias=True) + @field_validator("name", mode="before") + @classmethod + def validate_name(cls, v): + if not v: + raise ValueError("name must be set") + return v + @field_validator("field_type", mode="before") @classmethod def validate_field_type(cls, v): From 1bebf86f5dff3d7ade9a70f9e30d60933e69f7e8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 09:31:01 -0300 Subject: [PATCH 094/100] refactor: update ContractEdge __repr__ method for improved readability and consistency --- src/backend/base/langflow/graph/edge/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/graph/edge/base.py b/src/backend/base/langflow/graph/edge/base.py index 29534aba5f3..549b3f0950f 100644 --- a/src/backend/base/langflow/graph/edge/base.py +++ b/src/backend/base/langflow/graph/edge/base.py @@ -227,6 +227,8 @@ async def get_result_from_source(self, source: "Vertex", target: "Vertex"): return self.result def __repr__(self) -> str: - if self.source_handle and self.target_handle: + if (hasattr(self, "source_handle") and self.source_handle) and ( + hasattr(self, "target_handle") and self.target_handle + ): return f"{self.source_id} -[{self.source_handle.name}->{self.target_handle.fieldName}]-> {self.target_id}" return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" From 3bd2b43b9f6278b320dbd32faf0bb1c8e11ffdc1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:01:03 -0300 Subject: [PATCH 095/100] refactor: update BaseInputMixin to ensure name field is required with appropriate description --- src/backend/base/langflow/inputs/input_mixin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 747287541ac..8785e199661 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -40,12 +40,12 @@ class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore show: bool = True """Should the field be shown. Defaults to True.""" + name: str = Field(description="Name of the field.") + """Name of the field. Default is an empty string.""" + value: Any = "" """The value of the field. Default is an empty string.""" - name: Optional[str] = None - """Name of the field. Default is an empty string.""" - display_name: Optional[str] = None """Display name of the field. Defaults to None.""" From 869158820925bd3e07b506c68956f83efa5acfc9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:01:50 -0300 Subject: [PATCH 096/100] refactor: remove name validation from BaseInputMixin --- src/backend/base/langflow/inputs/input_mixin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 8785e199661..7b7f7c2b193 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -75,13 +75,6 @@ class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore def to_dict(self): return self.model_dump(exclude_none=True, by_alias=True) - @field_validator("name", mode="before") - @classmethod - def validate_name(cls, v): - if not v: - raise ValueError("name must be set") - return v - @field_validator("field_type", mode="before") @classmethod def validate_field_type(cls, v): From b131d6952744c3b153a004c7d20c2b292b7e14f0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:01:59 -0300 Subject: [PATCH 097/100] refactor: update input tests to include 'name' field in all input types for better validation and clarity --- src/backend/tests/unit/inputs/test_inputs.py | 92 +++++++++----------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/backend/tests/unit/inputs/test_inputs.py b/src/backend/tests/unit/inputs/test_inputs.py index 535de0cbf48..902d8142231 100644 --- a/src/backend/tests/unit/inputs/test_inputs.py +++ b/src/backend/tests/unit/inputs/test_inputs.py @@ -31,51 +31,44 @@ def client(): def test_table_input_valid(): - # Test with a valid list of dictionaries - data = TableInput(value=[{"key": "value"}, {"key2": "value2"}]) + data = TableInput(name="valid_table", value=[{"key": "value"}, {"key2": "value2"}]) assert data.value == [{"key": "value"}, {"key2": "value2"}] def test_table_input_invalid(): with pytest.raises(ValidationError): - # Test with an invalid value - TableInput(value="invalid") + TableInput(name="invalid_table", value="invalid") with pytest.raises(ValidationError): - # Test with a list containing invalid item - TableInput(value=[{"key": "value"}, "invalid"]) + TableInput(name="invalid_table", value=[{"key": "value"}, "invalid"]) def test_str_input_valid(): - data = StrInput(value="This is a string") + data = StrInput(name="valid_str", value="This is a string") assert data.value == "This is a string" def test_str_input_invalid(): with pytest.warns(UserWarning): - # Test with an invalid value - StrInput(value=1234) + StrInput(name="invalid_str", value=1234) def test_message_text_input_valid(): - # Test with a valid string - data = MessageTextInput(value="This is a message") + data = MessageTextInput(name="valid_msg", value="This is a message") assert data.value == "This is a message" - # Test with a valid Message object msg = Message(text="This is a message") - data = MessageTextInput(value=msg) + data = MessageTextInput(name="valid_msg", value=msg) assert data.value == "This is a message" def test_message_text_input_invalid(): with pytest.raises(ValidationError): - # Test with an invalid value - MessageTextInput(value=1234) + MessageTextInput(name="invalid_msg", value=1234) def test_instantiate_input_valid(): - data = {"value": "This is a string"} + data = {"name": "valid_input", "value": "This is a string"} input_instance = _instantiate_input("StrInput", data) assert isinstance(input_instance, StrInput) assert input_instance.value == "This is a string" @@ -83,146 +76,145 @@ def test_instantiate_input_valid(): def test_instantiate_input_invalid(): with pytest.raises(ValueError): - # Test with an invalid input type - _instantiate_input("InvalidInput", {"value": "This is a string"}) + _instantiate_input("InvalidInput", {"name": "invalid_input", "value": "This is a string"}) def test_handle_input_valid(): - data = HandleInput(input_types=["BaseLanguageModel"]) + data = HandleInput(name="valid_handle", input_types=["BaseLanguageModel"]) assert data.input_types == ["BaseLanguageModel"] def test_handle_input_invalid(): with pytest.raises(ValidationError): - HandleInput(input_types="BaseLanguageModel") # should be a list, not a string + HandleInput(name="invalid_handle", input_types="BaseLanguageModel") def test_data_input_valid(): - data_input = DataInput(input_types=["Data"]) + data_input = DataInput(name="valid_data", input_types=["Data"]) assert data_input.input_types == ["Data"] def test_prompt_input_valid(): - prompt_input = PromptInput(value="Enter your name") + prompt_input = PromptInput(name="valid_prompt", value="Enter your name") assert prompt_input.value == "Enter your name" def test_multiline_input_valid(): - multiline_input = MultilineInput(value="This is a\nmultiline input") + multiline_input = MultilineInput(name="valid_multiline", value="This is a\nmultiline input") assert multiline_input.value == "This is a\nmultiline input" assert multiline_input.multiline is True def test_multiline_input_invalid(): with pytest.raises(ValidationError): - MultilineInput(value=1234) # should be a string, not an integer + MultilineInput(name="invalid_multiline", value=1234) def test_multiline_secret_input_valid(): - multiline_secret_input = MultilineSecretInput(value="secret") + multiline_secret_input = MultilineSecretInput(name="valid_multiline_secret", value="secret") assert multiline_secret_input.value == "secret" assert multiline_secret_input.password is True def test_multiline_secret_input_invalid(): with pytest.raises(ValidationError): - MultilineSecretInput(value=1234) # should be a string, not an integer + MultilineSecretInput(name="invalid_multiline_secret", value=1234) def test_secret_str_input_valid(): - secret_str_input = SecretStrInput(value="supersecret") + secret_str_input = SecretStrInput(name="valid_secret_str", value="supersecret") assert secret_str_input.value == "supersecret" assert secret_str_input.password is True def test_secret_str_input_invalid(): with pytest.raises(ValidationError): - SecretStrInput(value=1234) # should be a string, not an integer + SecretStrInput(name="invalid_secret_str", value=1234) def test_int_input_valid(): - int_input = IntInput(value=10) + int_input = IntInput(name="valid_int", value=10) assert int_input.value == 10 def test_int_input_invalid(): with pytest.raises(ValidationError): - IntInput(value="not_an_int") # should be an integer, not a string + IntInput(name="invalid_int", value="not_an_int") def test_float_input_valid(): - float_input = FloatInput(value=10.5) + float_input = FloatInput(name="valid_float", value=10.5) assert float_input.value == 10.5 def test_float_input_invalid(): with pytest.raises(ValidationError): - FloatInput(value="not_a_float") # should be a float, not a string + FloatInput(name="invalid_float", value="not_a_float") def test_bool_input_valid(): - bool_input = BoolInput(value=True) + bool_input = BoolInput(name="valid_bool", value=True) assert bool_input.value is True def test_bool_input_invalid(): with pytest.raises(ValidationError): - BoolInput(value="not_a_bool") # should be a bool, not a string + BoolInput(name="invalid_bool", value="not_a_bool") def test_nested_dict_input_valid(): - nested_dict_input = NestedDictInput(value={"key": "value"}) + nested_dict_input = NestedDictInput(name="valid_nested_dict", value={"key": "value"}) assert nested_dict_input.value == {"key": "value"} def test_nested_dict_input_invalid(): with pytest.raises(ValidationError): - NestedDictInput(value="not_a_dict") # should be a dict, not a string + NestedDictInput(name="invalid_nested_dict", value="not_a_dict") def test_dict_input_valid(): - dict_input = DictInput(value={"key": "value"}) + dict_input = DictInput(name="valid_dict", value={"key": "value"}) assert dict_input.value == {"key": "value"} def test_dict_input_invalid(): with pytest.raises(ValidationError): - DictInput(value="not_a_dict") # should be a dict, not a string + DictInput(name="invalid_dict", value="not_a_dict") def test_dropdown_input_valid(): - dropdown_input = DropdownInput(options=["option1", "option2"]) + dropdown_input = DropdownInput(name="valid_dropdown", options=["option1", "option2"]) assert dropdown_input.options == ["option1", "option2"] def test_dropdown_input_invalid(): with pytest.raises(ValidationError): - DropdownInput(options="option1") # should be a list, not a string + DropdownInput(name="invalid_dropdown", options="option1") def test_multiselect_input_valid(): - multiselect_input = MultiselectInput(value=["option1", "option2"]) + multiselect_input = MultiselectInput(name="valid_multiselect", value=["option1", "option2"]) assert multiselect_input.value == ["option1", "option2"] def test_multiselect_input_invalid(): with pytest.raises(ValidationError): - MultiselectInput(value="option1") # should be a list, not a string + MultiselectInput(name="invalid_multiselect", value="option1") def test_file_input_valid(): - file_input = FileInput(value=["/path/to/file"]) + file_input = FileInput(name="valid_file", value=["/path/to/file"]) assert file_input.value == ["/path/to/file"] def test_instantiate_input_comprehensive(): valid_data = { - "StrInput": {"value": "A string"}, - "IntInput": {"value": 10}, - "FloatInput": {"value": 10.5}, - "BoolInput": {"value": True}, - "DictInput": {"value": {"key": "value"}}, - "MultiselectInput": {"value": ["option1", "option2"]}, + "StrInput": {"name": "str_input", "value": "A string"}, + "IntInput": {"name": "int_input", "value": 10}, + "FloatInput": {"name": "float_input", "value": 10.5}, + "BoolInput": {"name": "bool_input", "value": True}, + "DictInput": {"name": "dict_input", "value": {"key": "value"}}, + "MultiselectInput": {"name": "multiselect_input", "value": ["option1", "option2"]}, } for input_type, data in valid_data.items(): @@ -230,4 +222,4 @@ def test_instantiate_input_comprehensive(): assert isinstance(input_instance, InputTypesMap[input_type]) with pytest.raises(ValueError): - _instantiate_input("InvalidInput", {"value": "Invalid"}) # Invalid input type + _instantiate_input("InvalidInput", {"name": "invalid_input", "value": "Invalid"}) From 75b2a67fc9a997e56352b0b35eeff3aa76df2e0b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:20:17 -0300 Subject: [PATCH 098/100] refactor: enhance Component class with methods to validate callable outputs and inheritance checks for better robustness --- .../custom/custom_component/component.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index d0f9b7197a0..30da391150d 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -220,9 +220,32 @@ def _get_output_by_method(self, method: Callable): raise ValueError(f"Output with method {method_name} not found") return output + def _inherits_from_component(self, method: Callable): + # check if the method is a method from a class that inherits from Component + # and that it is an output of that class + inherits_from_component = hasattr(method, "__self__") and isinstance(method.__self__, Component) + return inherits_from_component + + def _method_is_valid_output(self, method: Callable): + # check if the method is a method from a class that inherits from Component + # and that it is an output of that class + method_is_output = ( + hasattr(method, "__self__") + and isinstance(method.__self__, Component) + and method.__self__._get_output_by_method(method) + ) + return method_is_output + def _process_connection_or_parameter(self, key, value): _input = self._get_or_create_input(key) - if callable(value): + # We need to check if callable AND if it is a method from a class that inherits from Component + if callable(value) and self._inherits_from_component(value): + try: + self._method_is_valid_output(value) + except ValueError: + raise ValueError( + f"Method {value.__name__} is not a valid output of {value.__self__.__class__.__name__}" + ) self._connect_to_component(key, value, _input) else: self._set_parameter_or_attribute(key, value) From 73e02fc305d13c967ef63a09f72f07f53c95d043 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:20:30 -0300 Subject: [PATCH 099/100] refactor: disable load_from_db for inputs in Component class to improve input handling logic and prevent unwanted database loading --- src/backend/base/langflow/custom/custom_component/component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 30da391150d..7c495e82374 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -327,7 +327,8 @@ def _set_input_value(self, name: str, value: Any): f"Input {name} is connected to {input_value.__self__.display_name}.{input_value.__name__}" ) self._inputs[name].value = value - self._attributes[name] = value + if hasattr(self._inputs[name], "load_from_db"): + self._inputs[name].load_from_db = False else: raise ValueError(f"Input {name} not found in {self.__class__.__name__}") From 5a7e46f2949b1f5c14c357c365f22356dabbae72 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 5 Aug 2024 10:36:37 -0300 Subject: [PATCH 100/100] refactor: add test for setting invalid output in test_component.py --- .../custom/custom_component/test_component.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/backend/tests/unit/custom/custom_component/test_component.py diff --git a/src/backend/tests/unit/custom/custom_component/test_component.py b/src/backend/tests/unit/custom/custom_component/test_component.py new file mode 100644 index 00000000000..f28ce861b4c --- /dev/null +++ b/src/backend/tests/unit/custom/custom_component/test_component.py @@ -0,0 +1,16 @@ +import pytest + +from langflow.components.inputs.ChatInput import ChatInput +from langflow.components.outputs import ChatOutput + + +@pytest.fixture +def client(): + pass + + +def test_set_invalid_output(): + chatinput = ChatInput() + chatoutput = ChatOutput() + with pytest.raises(ValueError): + chatoutput.set(input_value=chatinput.build_config)