From 783c0e64368ff89298f8d83eacdf868ee47f80b5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:39:39 -0300 Subject: [PATCH 01/44] Refactor code to dynamically create classes from strings --- src/backend/langflow/utils/validate.py | 88 ++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/src/backend/langflow/utils/validate.py b/src/backend/langflow/utils/validate.py index dc0244aca..51c4894d5 100644 --- a/src/backend/langflow/utils/validate.py +++ b/src/backend/langflow/utils/validate.py @@ -145,16 +145,46 @@ def create_function(code, function_name): def create_class(code, class_name): + """ + Dynamically create a class from a string of code and a specified class name. + + :param code: String containing the Python code defining the class + :param class_name: Name of the class to be created + :return: A function that, when called, returns an instance of the created class + """ if not hasattr(ast, "TypeIgnore"): - - class TypeIgnore(ast.AST): - _fields = () - - ast.TypeIgnore = TypeIgnore + ast.TypeIgnore = create_type_ignore_class() module = ast.parse(code) - exec_globals = globals().copy() + exec_globals = prepare_global_scope(module) + class_code = extract_class_code(module, class_name) + compiled_class = compile_class_code(class_code) + + return build_class_constructor(compiled_class, exec_globals, class_name) + + +def create_type_ignore_class(): + """ + Create a TypeIgnore class for AST module if it doesn't exist. + + :return: TypeIgnore class + """ + + class TypeIgnore(ast.AST): + _fields = () + + return TypeIgnore + + +def prepare_global_scope(module): + """ + Prepares the global scope with necessary imports from the provided code module. + + :param module: AST parsed module + :return: Dictionary representing the global scope with imported modules + """ + exec_globals = globals().copy() for node in module.body: if isinstance(node, ast.Import): for alias in node.names: @@ -169,17 +199,47 @@ def create_class(code, class_name): exec_globals[alias.name] = getattr(imported_module, alias.name) except ModuleNotFoundError as e: raise ModuleNotFoundError(f"Module {node.module} not found. Please install it and try again.") from e + return exec_globals + +def extract_class_code(module, class_name): + """ + Extracts the AST node for the specified class from the module. + + :param module: AST parsed module + :param class_name: Name of the class to extract + :return: AST node of the specified class + """ class_code = next(node for node in module.body if isinstance(node, ast.ClassDef) and node.name == class_name) class_code.parent = None + return class_code + + +def compile_class_code(class_code): + """ + Compiles the AST node of a class into a code object. + + :param class_code: AST node of the class + :return: Compiled code object of the class + """ code_obj = compile(ast.Module(body=[class_code], type_ignores=[]), "", "exec") - # This suppresses import errors - # with contextlib.suppress(Exception): - exec(code_obj, exec_globals, locals()) + return code_obj + + +def build_class_constructor(compiled_class, exec_globals, class_name): + """ + Builds a constructor function for the dynamically created class. + + :param compiled_class: Compiled code object of the class + :param exec_globals: Global scope with necessary imports + :param class_name: Name of the class + :return: Constructor function for the class + """ + exec(compiled_class, exec_globals, locals()) exec_globals[class_name] = locals()[class_name] # Return a function that imports necessary modules and creates an instance of the target class - def build_my_class(*args, **kwargs): + def build_custom_class(*args, **kwargs): for module_name, module in exec_globals.items(): if isinstance(module, type(importlib)): globals()[module_name] = module @@ -187,9 +247,13 @@ def create_class(code, class_name): instance = exec_globals[class_name](*args, **kwargs) return instance - build_my_class.__globals__.update(exec_globals) + build_custom_class.__globals__.update(exec_globals) + return build_custom_class - return build_my_class + +# Example usage: +# class_builder = create_class("class MyClass: pass", "MyClass") +# my_instance = class_builder() def extract_function_name(code): From 82bd12413965a2d2c5f17b9467ec08d6ed6efa5d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:39:45 -0300 Subject: [PATCH 02/44] Fix import order in test_custom_component.py --- tests/test_custom_component.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index c60247668..b07753b8d 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -1,18 +1,17 @@ import ast -import pytest import types from uuid import uuid4 - +import pytest from fastapi import HTTPException -from langflow.services.database.models.flow import Flow, FlowCreate +from langflow.field_typing.constants import Data from langflow.interface.custom.base import CustomComponent +from langflow.interface.custom.code_parser import CodeParser, CodeSyntaxError from langflow.interface.custom.component import ( Component, ComponentCodeNullError, ) -from langflow.interface.custom.code_parser import CodeParser, CodeSyntaxError - +from langflow.services.database.models.flow import Flow, FlowCreate code_default = """ from langflow import Prompt @@ -229,9 +228,11 @@ def test_custom_component_get_function_entrypoint_return_type(): Test the get_function_entrypoint_return_type property of the CustomComponent class. """ + from langchain.schema import Document + custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type - assert return_type == ["Document"] + assert return_type == [Document] def test_custom_component_get_main_class_name(): @@ -414,7 +415,7 @@ class MyClass(CustomComponent): custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type - assert return_type == [] + assert return_type == [Data] def test_custom_component_get_main_class_name_no_main_class(): From 096f3ae1fc23b6e3477487c992017b4759ace0c9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:39:51 -0300 Subject: [PATCH 03/44] =?UTF-8?q?=F0=9F=94=A8=20refactor(utils.py):=20rena?= =?UTF-8?q?me=20get=5Ffunction=5Fcustom=20to=20eval=5Fcustom=5Fcomponent?= =?UTF-8?q?=5Fcode=20for=20better=20clarity=20and=20consistency=20?= =?UTF-8?q?=E2=9C=A8=20feat(utils.py):=20add=20type=20hinting=20to=20eval?= =?UTF-8?q?=5Fcustom=5Fcomponent=5Fcode=20function=20for=20better=20code?= =?UTF-8?q?=20readability=20and=20maintainability=20=F0=9F=94=A8=20refacto?= =?UTF-8?q?r(utils.py):=20move=20import=20statement=20for=20PromptTemplate?= =?UTF-8?q?=20to=20the=20correct=20location=20for=20better=20organization?= =?UTF-8?q?=20=F0=9F=94=A8=20refactor(utils.py):=20move=20import=20stateme?= =?UTF-8?q?nt=20for=20validate=20to=20the=20correct=20location=20for=20bet?= =?UTF-8?q?ter=20organization=20=F0=9F=94=A8=20refactor(utils.py):=20remov?= =?UTF-8?q?e=20unused=20imports=20and=20reorganize=20import=20statements?= =?UTF-8?q?=20for=20better=20organization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/interface/importing/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index 6b28d792e..fb28bbdc8 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -3,15 +3,16 @@ import importlib from typing import Any, Type -from langchain.prompts import PromptTemplate from langchain.agents import Agent from langchain.base_language import BaseLanguageModel from langchain.chains.base import Chain from langchain.chat_models.base import BaseChatModel +from langchain.prompts import PromptTemplate from langchain.tools import BaseTool + from langflow.interface.custom.custom_component import CustomComponent -from langflow.utils import validate from langflow.interface.wrappers.base import wrapper_creator +from langflow.utils import validate def import_module(module_path: str) -> Any: @@ -180,6 +181,7 @@ def get_function(code): return validate.create_function(code, function_name) -def get_function_custom(code): +def eval_custom_component_code(code: str) -> CustomComponent: + """Evaluate custom component code""" class_name = validate.extract_class_name(code) return validate.create_class(code, class_name) From d7fdea2a89c4749af6ce1cf05a838ec11002479d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:40:14 -0300 Subject: [PATCH 04/44] Refactor loading.py: Import changes and instantiate_custom_component modification --- src/backend/langflow/interface/initialize/loading.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 9f737e133..9839a9062 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -10,8 +10,11 @@ from langchain.chains.base import Chain from langchain.document_loaders.base import BaseLoader from langchain.schema import Document from langchain.vectorstores.base import VectorStore +from loguru import logger +from pydantic import ValidationError + from langflow.interface.custom_lists import CUSTOM_NODES -from langflow.interface.importing.utils import get_function, get_function_custom, import_by_type +from langflow.interface.importing.utils import eval_custom_component_code, get_function, import_by_type from langflow.interface.initialize.llm import initialize_vertexai from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type, handle_partial_variables from langflow.interface.initialize.vector_store import vecstore_initializer @@ -21,8 +24,6 @@ from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.utils import load_file_into_dict from langflow.interface.wrappers.base import wrapper_creator from langflow.utils import validate -from loguru import logger -from pydantic import ValidationError if TYPE_CHECKING: from langflow import CustomComponent @@ -119,7 +120,7 @@ def instantiate_custom_component(node_type, class_object, params, user_id): # we need to make a copy of the params because we will be # modifying it params_copy = params.copy() - class_object: "CustomComponent" = get_function_custom(params_copy.pop("code")) + class_object: "CustomComponent" = eval_custom_component_code(params_copy.pop("code")) custom_component = class_object(user_id=user_id) built_object = custom_component.build(**params_copy) return built_object, {"repr": custom_component.custom_repr()} From e3c35335beec20f3c91517a6fcf8dda3796fa485 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:40:20 -0300 Subject: [PATCH 05/44] Fix import statement in types.py --- src/backend/langflow/interface/types.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index f578bb0bd..f460edf68 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -7,6 +7,8 @@ from typing import Any, List, Optional, Union from uuid import UUID from fastapi import HTTPException +from loguru import logger + from langflow.api.utils import get_new_key from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator @@ -16,7 +18,7 @@ from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.utils import extract_inner_type from langflow.interface.document_loaders.base import documentloader_creator from langflow.interface.embeddings.base import embedding_creator -from langflow.interface.importing.utils import get_function_custom +from langflow.interface.importing.utils import eval_custom_component_code from langflow.interface.llms.base import llm_creator from langflow.interface.memories.base import memory_creator from langflow.interface.output_parsers.base import output_parser_creator @@ -32,7 +34,6 @@ from langflow.template.field.base import TemplateField from langflow.template.frontend_node.constants import CLASSES_TO_REMOVE from langflow.template.frontend_node.custom_components import CustomComponentFrontendNode from langflow.utils.util import get_base_classes -from loguru import logger # Used to get the base_classes list @@ -202,7 +203,7 @@ def build_field_config(custom_component: CustomComponent, user_id: Optional[Unio """Build the field configuration for a custom component""" try: - custom_class = get_function_custom(custom_component.code) + custom_class = eval_custom_component_code(custom_component.code) except Exception as exc: logger.error(f"Error while getting custom function: {str(exc)}") raise HTTPException( From 686b97e69595f826503d10a1799d8f17a2e9add9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 08:40:59 -0300 Subject: [PATCH 06/44] Refactor eval_custom_component_code function signature --- src/backend/langflow/interface/importing/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index fb28bbdc8..f3276d952 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -9,7 +9,6 @@ from langchain.chains.base import Chain from langchain.chat_models.base import BaseChatModel from langchain.prompts import PromptTemplate from langchain.tools import BaseTool - from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.wrappers.base import wrapper_creator from langflow.utils import validate @@ -181,7 +180,7 @@ def get_function(code): return validate.create_function(code, function_name) -def eval_custom_component_code(code: str) -> CustomComponent: +def eval_custom_component_code(code: str) -> Type[CustomComponent]: """Evaluate custom component code""" class_name = validate.extract_class_name(code) return validate.create_class(code, class_name) From 842ba2835a86e53ed9d64b9f5aaf2dbe600e3c7e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 09:36:15 -0300 Subject: [PATCH 07/44] Makes build method async to support async in CC --- poetry.lock | 52 +++++++---- pyproject.toml | 1 + src/backend/langflow/api/v1/chat.py | 23 +++-- src/backend/langflow/graph/graph/base.py | 7 +- src/backend/langflow/graph/vertex/base.py | 46 +++++----- src/backend/langflow/graph/vertex/types.py | 30 +++---- .../interface/custom/custom_component.py | 5 +- .../langflow/interface/initialize/loading.py | 30 ++++--- src/backend/langflow/interface/run.py | 28 ++---- src/backend/langflow/processing/process.py | 26 +++--- .../langflow/services/session/service.py | 5 +- src/backend/langflow/worker.py | 12 +-- tests/test_graph.py | 86 +++++++++---------- tests/test_process.py | 22 +++-- 14 files changed, 189 insertions(+), 184 deletions(-) diff --git a/poetry.lock b/poetry.lock index 988618e1b..e4bce3b3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -411,17 +411,17 @@ files = [ [[package]] name = "boto3" -version = "1.29.5" +version = "1.29.6" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.29.5-py3-none-any.whl", hash = "sha256:030b0f0faf8d44f97e67a5411644243482f33ebf1c45338bb40662239a16dda4"}, - {file = "boto3-1.29.5.tar.gz", hash = "sha256:76fc6a17781c27558c526e899579ccf530df10eb279261fe7800540f0043917e"}, + {file = "boto3-1.29.6-py3-none-any.whl", hash = "sha256:f4d19e01d176c3a5a05e4af733185ff1891b08a3c38d4a439800fa132aa6e9be"}, + {file = "boto3-1.29.6.tar.gz", hash = "sha256:d1d0d979a70bf9b0b13ae3b017f8523708ad953f62d16f39a602d67ee9b25554"}, ] [package.dependencies] -botocore = ">=1.32.5,<1.33.0" +botocore = ">=1.32.6,<1.33.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.7.0,<0.8.0" @@ -430,13 +430,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.32.5" +version = "1.32.6" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.32.5-py3-none-any.whl", hash = "sha256:b8960c955ba275915bf022c54c896c2dac1038289d8a5ace92d1431257c0a439"}, - {file = "botocore-1.32.5.tar.gz", hash = "sha256:75a68f942cd87baff83b3a20dfda11b3aeda48aad32e4dcd6fe8992c0cb0e7db"}, + {file = "botocore-1.32.6-py3-none-any.whl", hash = "sha256:4454f967a4d1a01e3e6205c070455bc4e8fd53b5b0753221581ae679c55a9dfd"}, + {file = "botocore-1.32.6.tar.gz", hash = "sha256:ecec876103783b5efe6099762dda60c2af67e45f7c0ab4568e8265d11c6c449b"}, ] [package.dependencies] @@ -3488,13 +3488,13 @@ zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "langchain" -version = "0.0.339" +version = "0.0.340" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.339-py3-none-any.whl", hash = "sha256:fec250074a6fbb3711a51423d830006d69f34aedb67604df39c642be80852cbb"}, - {file = "langchain-0.0.339.tar.gz", hash = "sha256:34eb4d7987d979663e361da435479c6f0648a170dae3eb1e9f0f7417f033a2c1"}, + {file = "langchain-0.0.340-py3-none-any.whl", hash = "sha256:f80f40b52ef82424e38e894db8b8048b6505da100679e72613316f8d8b0243fb"}, + {file = "langchain-0.0.340.tar.gz", hash = "sha256:1a6bd2511bbb81e42d2a3d7291ee03de180accab851181ee9fdbb7fbaef6c57c"}, ] [package.dependencies] @@ -3559,13 +3559,13 @@ six = "*" [[package]] name = "langfuse" -version = "1.7.4" +version = "1.7.5" description = "A client library for accessing langfuse" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langfuse-1.7.4-py3-none-any.whl", hash = "sha256:f5f1e19eac2d01e9854f567f0946f47dac3be59ee40f335e616355b3545018f3"}, - {file = "langfuse-1.7.4.tar.gz", hash = "sha256:5813d2f43e7ba106ae58f048d81c7091fd681be73b35d87d53ac321f999738ae"}, + {file = "langfuse-1.7.5-py3-none-any.whl", hash = "sha256:ebbcc52f454a9c7cfc9f382e66fddafddb0219f9233598317bbcb66c215b39b6"}, + {file = "langfuse-1.7.5.tar.gz", hash = "sha256:99fc5a30b157a16cc3dcb82e84af13fabc2fd0d192be32ef2ad6d9a7fe27d130"}, ] [package.dependencies] @@ -3936,13 +3936,13 @@ typing-extensions = "*" [[package]] name = "metaphor-python" -version = "0.1.20" +version = "0.1.21" description = "A Python package for the Metaphor API." optional = false python-versions = "*" files = [ - {file = "metaphor-python-0.1.20.tar.gz", hash = "sha256:a1ee7a3b21ff8644553a73bc08a4d475abed182d9a1a0a72c729910837081d50"}, - {file = "metaphor_python-0.1.20-py3-none-any.whl", hash = "sha256:ac06b5bf86f6fb1b2371b8be6589766da8eccd6876d56e94b738654dea9adc9a"}, + {file = "metaphor-python-0.1.21.tar.gz", hash = "sha256:72604c45c7bf447613f9cdf713c6c57612f1790ead2e78b13c65588e5d7aa279"}, + {file = "metaphor_python-0.1.21-py3-none-any.whl", hash = "sha256:b17099f6c37e26a77fbb77a242163fa6b64aad487687264f4ec6c9d16665c5a8"}, ] [package.dependencies] @@ -6143,6 +6143,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "pytest-cov" version = "4.1.0" @@ -9078,4 +9096,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "8335fd767d4ff476b3caf2a9e05fcce1f353d4b3a2e181585080a618099669fa" +content-hash = "62e47482eefda134f0801744360624549d7024861380f51f280f60450d768615" diff --git a/pyproject.toml b/pyproject.toml index d9844d96b..7f8cb136c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ qianfan = "0.0.5" pgvector = "^0.2.3" [tool.poetry.group.dev.dependencies] +pytest-asyncio = "^0.21.1" types-redis = "^4.6.0.5" ipykernel = "^6.21.2" mypy = "^1.1.1" diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 51779b2c9..66e23aa5a 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -8,21 +8,20 @@ from fastapi import ( status, ) from fastapi.responses import StreamingResponse +from loguru import logger +from sqlmodel import Session + from langflow.api.utils import build_input_keys_response from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData - from langflow.graph.graph.base import Graph from langflow.services.auth.utils import ( get_current_active_user, get_current_user_by_jwt, ) -from langflow.services.cache.utils import update_build_status -from loguru import logger -from langflow.services.deps import get_chat_service, get_session, get_cache_service -from sqlmodel import Session -from langflow.services.chat.service import ChatService from langflow.services.cache.service import BaseCacheService - +from langflow.services.cache.utils import update_build_status +from langflow.services.chat.service import ChatService +from langflow.services.deps import get_cache_service, get_chat_service, get_session router = APIRouter(tags=["Chat"]) @@ -164,9 +163,9 @@ async def stream_build( } yield str(StreamData(event="log", data=log_dict)) if vertex.is_task: - vertex = try_running_celery_task(vertex, user_id) + vertex = await try_running_celery_task(vertex, user_id) else: - vertex.build(user_id=user_id) + await vertex.build(user_id=user_id) params = vertex._built_object_repr() valid = True logger.debug(f"Building node {str(vertex.vertex_type)}") @@ -193,7 +192,7 @@ async def stream_build( yield str(StreamData(event="message", data=response)) - langchain_object = graph.build() + langchain_object = await graph.build() # Now we need to check the input_keys to send them to the client if hasattr(langchain_object, "input_keys"): input_keys_response = build_input_keys_response(langchain_object, artifacts) @@ -224,7 +223,7 @@ async def stream_build( raise HTTPException(status_code=500, detail=str(exc)) -def try_running_celery_task(vertex, user_id): +async def try_running_celery_task(vertex, user_id): # Try running the task in celery # and set the task_id to the local vertex # if it fails, run the task locally @@ -236,5 +235,5 @@ def try_running_celery_task(vertex, user_id): except Exception as exc: logger.debug(f"Error running task in celery: {exc}") vertex.task_id = None - vertex.build(user_id=user_id) + await vertex.build(user_id=user_id) return vertex diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 01856d517..e9431a5ec 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -1,6 +1,8 @@ from typing import Dict, Generator, List, Type, Union from langchain.chains.base import Chain +from loguru import logger + from langflow.graph.edge.base import Edge from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.utils import process_flow @@ -8,7 +10,6 @@ from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.types import FileToolVertex, LLMVertex, ToolkitVertex from langflow.interface.tools.constants import FILE_TOOLS from langflow.utils import payload -from loguru import logger class Graph: @@ -116,13 +117,13 @@ class Graph: connected_nodes: List[Vertex] = [edge.source for edge in self.edges if edge.target == node] return connected_nodes - def build(self) -> Chain: + async def build(self) -> Chain: """Builds the graph.""" # Get root node root_node = payload.get_root_node(self) if root_node is None: raise ValueError("No root node found") - return root_node.build() + return await root_node.build() def topological_sort(self) -> List[Vertex]: """ diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 5862ca3ae..5ea645980 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -1,20 +1,18 @@ import ast +import inspect import pickle +import types +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from loguru import logger + from langflow.graph.utils import UnbuiltObject from langflow.graph.vertex.utils import is_basic_type from langflow.interface.initialize import loading from langflow.interface.listing import lazy_load_dict from langflow.utils.constants import DIRECT_TYPES -from loguru import logger from langflow.utils.util import sync_to_async - -import inspect -import types -from typing import Any, Dict, List, Optional -from typing import TYPE_CHECKING - - if TYPE_CHECKING: from langflow.graph.edge.base import Edge @@ -216,18 +214,18 @@ class Vertex: self._raw_params = params self.params = params - def _build(self, user_id=None): + async def _build(self, user_id=None): """ Initiate the build process. """ logger.debug(f"Building {self.vertex_type}") - self._build_each_node_in_params_dict(user_id) - self._get_and_instantiate_class(user_id) + await self._build_each_node_in_params_dict(user_id) + await self._get_and_instantiate_class(user_id) self._validate_built_object() self._built = True - def _build_each_node_in_params_dict(self, user_id=None): + async def _build_each_node_in_params_dict(self, user_id=None): """ Iterates over each node in the params dictionary and builds it. """ @@ -236,9 +234,9 @@ class Vertex: if value == self: del self.params[key] continue - self._build_node_and_update_params(key, value, user_id) + await self._build_node_and_update_params(key, value, user_id) elif isinstance(value, list) and self._is_list_of_nodes(value): - self._build_list_of_nodes_and_update_params(key, value, user_id) + await self._build_list_of_nodes_and_update_params(key, value, user_id) def _is_node(self, value): """ @@ -252,7 +250,7 @@ class Vertex: """ return all(self._is_node(node) for node in value) - def get_result(self, user_id=None, timeout=None) -> Any: + async def get_result(self, user_id=None, timeout=None) -> Any: # Check if the Vertex was built already if self._built: return self._built_object @@ -268,27 +266,27 @@ class Vertex: pass # If there's no task_id, build the vertex locally - self.build(user_id) + await self.build(user_id) return self._built_object - def _build_node_and_update_params(self, key, node, user_id=None): + async def _build_node_and_update_params(self, key, node, user_id=None): """ Builds a given node and updates the params dictionary accordingly. """ - result = node.get_result(user_id) + result = await node.get_result(user_id) self._handle_func(key, result) if isinstance(result, list): self._extend_params_list_with_result(key, result) self.params[key] = result - def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None): + async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None): """ Iterates over a list of nodes, builds each and updates the params dictionary. """ self.params[key] = [] for node in nodes: - built = node.get_result(user_id) + built = await node.get_result(user_id) if isinstance(built, list): if key not in self.params: self.params[key] = [] @@ -318,14 +316,14 @@ class Vertex: if isinstance(self.params[key], list): self.params[key].extend(result) - def _get_and_instantiate_class(self, user_id=None): + async def _get_and_instantiate_class(self, user_id=None): """ Gets the class from a dictionary and instantiates it with the params. """ if self.base_type is None: raise ValueError(f"Base type for node {self.vertex_type} not found") try: - result = loading.instantiate_class( + result = await loading.instantiate_class( node_type=self.vertex_type, base_type=self.base_type, params=self.params, @@ -358,9 +356,9 @@ class Vertex: raise ValueError(message) - def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: + async def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: if not self._built or force: - self._build(user_id, *args, **kwargs) + await self._build(user_id, *args, **kwargs) return self._built_object diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 5fe6f8d31..c288a4b0a 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -1,8 +1,8 @@ import ast from typing import Any, Dict, List, Optional, Union -from langflow.graph.vertex.base import Vertex from langflow.graph.utils import flatten_list +from langflow.graph.vertex.base import Vertex from langflow.interface.utils import extract_input_variables_from_prompt @@ -34,18 +34,18 @@ class AgentVertex(Vertex): elif isinstance(source_node, ChainVertex): self.chains.append(source_node) - def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: + async def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: if not self._built or force: self._set_tools_and_chains() # First, build the tools for tool_node in self.tools: - tool_node.build(user_id=user_id) + await tool_node.build(user_id=user_id) # Next, build the chains and the rest for chain_node in self.chains: - chain_node.build(tools=self.tools, user_id=user_id) + await chain_node.build(tools=self.tools, user_id=user_id) - self._build(user_id=user_id) + await self._build(user_id=user_id) return self._built_object @@ -62,13 +62,13 @@ class LLMVertex(Vertex): def __init__(self, data: Dict, params: Optional[Dict] = None): super().__init__(data, base_type="llms", params=params) - def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: + async def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: # LLM is different because some models might take up too much memory # or time to load. So we only load them when we need them.ß if self.vertex_type == self.built_node_type: return self.class_built_object if not self._built or force: - self._build(user_id=user_id) + await self._build(user_id=user_id) self.built_node_type = self.vertex_type self.class_built_object = self._built_object # Avoid deepcopying the LLM @@ -90,11 +90,11 @@ class WrapperVertex(Vertex): def __init__(self, data: Dict): super().__init__(data, base_type="wrappers") - def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: + async def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any: if not self._built or force: if "headers" in self.params: self.params["headers"] = ast.literal_eval(self.params["headers"]) - self._build(user_id=user_id) + await self._build(user_id=user_id) return self._built_object @@ -193,7 +193,7 @@ class ChainVertex(Vertex): def __init__(self, data: Dict): super().__init__(data, base_type="chains") - def build( + async def build( self, force: bool = False, user_id=None, @@ -212,9 +212,9 @@ class ChainVertex(Vertex): if isinstance(value, PromptVertex): # Build the PromptVertex, passing the tools if available tools = kwargs.get("tools", None) - self.params[key] = value.build(tools=tools, force=force) + self.params[key] = await value.build(tools=tools, force=force) - self._build(user_id=user_id) + await self._build(user_id=user_id) return self._built_object @@ -223,7 +223,7 @@ class PromptVertex(Vertex): def __init__(self, data: Dict): super().__init__(data, base_type="prompts") - def build( + async def build( self, force: bool = False, user_id=None, @@ -236,7 +236,7 @@ class PromptVertex(Vertex): self.params["input_variables"] = [] # Check if it is a ZeroShotPrompt and needs a tool if "ShotPrompt" in self.vertex_type: - tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else [] + tools = [await tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else [] # flatten the list of tools if it is a list of lists # first check if it is a list if tools and isinstance(tools, list) and isinstance(tools[0], list): @@ -257,7 +257,7 @@ class PromptVertex(Vertex): elif isinstance(self.params, dict): self.params.pop("input_variables", None) - self._build(user_id=user_id) + await self._build(user_id=user_id) return self._built_object def _built_object_repr(self): diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index 055766bb0..cb581e182 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -3,7 +3,6 @@ from uuid import UUID import yaml from fastapi import HTTPException - from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component from langflow.interface.custom.directory_reader import DirectoryReader @@ -189,7 +188,7 @@ class CustomComponent(Component): def get_function(self): return validate.create_function(self.code, self.function_entrypoint_name) - def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any: + async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any: from langflow.processing.process import build_sorted_vertices, process_tweaks db_service = get_db_service() @@ -199,7 +198,7 @@ class CustomComponent(Component): raise ValueError(f"Flow {flow_id} not found") if tweaks: graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks) - return build_sorted_vertices(graph_data, self.user_id) + return await build_sorted_vertices(graph_data, self.user_id) def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]: if not self.user_id: diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 9839a9062..3c8bd2d41 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -1,3 +1,4 @@ +import inspect import json from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type @@ -10,9 +11,6 @@ from langchain.chains.base import Chain from langchain.document_loaders.base import BaseLoader from langchain.schema import Document from langchain.vectorstores.base import VectorStore -from loguru import logger -from pydantic import ValidationError - from langflow.interface.custom_lists import CUSTOM_NODES from langflow.interface.importing.utils import eval_custom_component_code, get_function, import_by_type from langflow.interface.initialize.llm import initialize_vertexai @@ -24,6 +22,8 @@ from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.utils import load_file_into_dict from langflow.interface.wrappers.base import wrapper_creator from langflow.utils import validate +from loguru import logger +from pydantic import ValidationError if TYPE_CHECKING: from langflow import CustomComponent @@ -36,7 +36,7 @@ def build_vertex_in_params(params: Dict) -> Dict: return {key: value.build() if isinstance(value, Vertex) else value for key, value in params.items()} -def instantiate_class(node_type: str, base_type: str, params: Dict, user_id=None) -> Any: +async def instantiate_class(node_type: str, base_type: str, params: Dict, user_id=None) -> Any: """Instantiate class from module type and key, and params""" params = convert_params_to_sets(params) params = convert_kwargs(params) @@ -48,7 +48,7 @@ def instantiate_class(node_type: str, base_type: str, params: Dict, user_id=None return custom_node(**params) logger.debug(f"Instantiating {node_type} of type {base_type}") class_object = import_by_type(_type=base_type, name=node_type) - return instantiate_based_on_type(class_object, base_type, node_type, params, user_id=user_id) + return await instantiate_based_on_type(class_object, base_type, node_type, params, user_id=user_id) def convert_params_to_sets(params): @@ -75,7 +75,7 @@ def convert_kwargs(params): return params -def instantiate_based_on_type(class_object, base_type, node_type, params, user_id): +async def instantiate_based_on_type(class_object, base_type, node_type, params, user_id): if base_type == "agents": return instantiate_agent(node_type, class_object, params) elif base_type == "prompts": @@ -109,20 +109,28 @@ def instantiate_based_on_type(class_object, base_type, node_type, params, user_i elif base_type == "memory": return instantiate_memory(node_type, class_object, params) elif base_type == "custom_components": - return instantiate_custom_component(node_type, class_object, params, user_id) + return await instantiate_custom_component(node_type, class_object, params, user_id) elif base_type == "wrappers": return instantiate_wrapper(node_type, class_object, params) else: return class_object(**params) -def instantiate_custom_component(node_type, class_object, params, user_id): - # we need to make a copy of the params because we will be - # modifying it +async def instantiate_custom_component(node_type, class_object, params, user_id): params_copy = params.copy() class_object: "CustomComponent" = eval_custom_component_code(params_copy.pop("code")) custom_component = class_object(user_id=user_id) - built_object = custom_component.build(**params_copy) + + # Determine if the build method is asynchronous + is_async = inspect.iscoroutinefunction(custom_component.build) + + if is_async: + # Await the build method directly if it's async + built_object = await custom_component.build(**params_copy) + else: + # Call the build method directly if it's sync + built_object = custom_component.build(**params_copy) + return built_object, {"repr": custom_component.custom_repr()} diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index 624f250c8..94cd922eb 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -1,10 +1,12 @@ -from typing import Dict, Tuple, Optional, Union -from langflow.graph import Graph -from loguru import logger +from typing import Dict, Optional, Tuple, Union from uuid import UUID +from loguru import logger -def build_sorted_vertices(data_graph, user_id: Optional[Union[str, UUID]] = None) -> Tuple[Graph, Dict]: +from langflow.graph import Graph + + +async def build_sorted_vertices(data_graph, user_id: Optional[Union[str, UUID]] = None) -> Tuple[Graph, Dict]: """ Build langchain object from data_graph. """ @@ -14,28 +16,12 @@ def build_sorted_vertices(data_graph, user_id: Optional[Union[str, UUID]] = None sorted_vertices = graph.topological_sort() artifacts = {} for vertex in sorted_vertices: - vertex.build(user_id=user_id) + await vertex.build(user_id=user_id) if vertex.artifacts: artifacts.update(vertex.artifacts) return graph, artifacts -def build_langchain_object(data_graph): - """ - Build langchain object from data_graph. - """ - - logger.debug("Building langchain object") - nodes = data_graph["nodes"] - # Add input variables - # nodes = payload.extract_input_variables(nodes) - # Nodes, edges and root node - edges = data_graph["edges"] - graph = Graph(nodes, edges) - - return graph.build() - - def get_memory_key(langchain_object): """ Given a LangChain object, this function retrieves the current memory key from the object's memory attribute. diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index 951e196fb..8f0b4b043 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -1,19 +1,15 @@ +import asyncio import json from pathlib import Path -from langchain.schema import AgentAction -from langflow.interface.run import ( - build_sorted_vertices, - get_memory_key, - update_memory_keys, -) +from typing import Any, Dict, List, Optional, Tuple, Union + +from langchain.chains.base import Chain +from langchain.schema import AgentAction, Document +from langchain.vectorstores.base import VectorStore +from langflow.graph import Graph +from langflow.interface.run import build_sorted_vertices, get_memory_key, update_memory_keys from langflow.services.deps import get_session_service from loguru import logger -from langflow.graph import Graph -from langchain.chains.base import Chain -from langchain.vectorstores.base import VectorStore -from typing import Any, Dict, List, Optional, Tuple, Union -from langchain.schema import Document - from pydantic import BaseModel @@ -164,8 +160,8 @@ async def process_graph_cached( if session_id is None: session_id = session_service.generate_key(session_id=session_id, data_graph=data_graph) # Load the graph using SessionService - graph, artifacts = session_service.load_session(session_id, data_graph) - built_object = graph.build() + graph, artifacts = await session_service.load_session(session_id, data_graph) + built_object = await graph.build() processed_inputs = process_inputs(inputs, artifacts) result = generate_result(built_object, processed_inputs) # langchain_object is now updated with the new memory @@ -202,7 +198,7 @@ def load_flow_from_json(flow: Union[Path, str, dict], tweaks: Optional[dict] = N graph = Graph(nodes, edges) if build: - langchain_object = graph.build() + langchain_object = asyncio.run(graph.build()) if hasattr(langchain_object, "verbose"): langchain_object.verbose = True diff --git a/src/backend/langflow/services/session/service.py b/src/backend/langflow/services/session/service.py index 6bdebf6b3..d74b88a54 100644 --- a/src/backend/langflow/services/session/service.py +++ b/src/backend/langflow/services/session/service.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING + from langflow.interface.run import build_sorted_vertices from langflow.services.base import Service from langflow.services.cache.utils import compute_dict_hash @@ -14,7 +15,7 @@ class SessionService(Service): def __init__(self, cache_service): self.cache_service: "BaseCacheService" = cache_service - def load_session(self, key, data_graph): + async def load_session(self, key, data_graph): # Check if the data is cached if key in self.cache_service: return self.cache_service.get(key) @@ -23,7 +24,7 @@ class SessionService(Service): key = self.generate_key(session_id=None, data_graph=data_graph) # If not cached, build the graph and cache it - graph, artifacts = build_sorted_vertices(data_graph) + graph, artifacts = await build_sorted_vertices(data_graph) self.cache_service.set(key, (graph, artifacts)) diff --git a/src/backend/langflow/worker.py b/src/backend/langflow/worker.py index 8f2abcb43..b9d646184 100644 --- a/src/backend/langflow/worker.py +++ b/src/backend/langflow/worker.py @@ -1,13 +1,9 @@ from typing import TYPE_CHECKING, Any, Dict, Optional +from asgiref.sync import async_to_sync from celery.exceptions import SoftTimeLimitExceeded # type: ignore - from langflow.core.celery_app import celery_app -from langflow.processing.process import ( - Result, - generate_result, - process_inputs, -) +from langflow.processing.process import Result, generate_result, process_inputs from langflow.services.deps import get_session_service from langflow.services.manager import initialize_session_service @@ -27,7 +23,7 @@ def build_vertex(self, vertex: "Vertex") -> "Vertex": """ try: vertex.task_id = self.request.id - vertex.build() + async_to_sync(vertex.build)() return vertex except SoftTimeLimitExceeded as e: raise self.retry(exc=SoftTimeLimitExceeded("Task took too long"), countdown=2) from e @@ -47,7 +43,7 @@ def process_graph_cached_task( if session_id is None: session_id = session_service.generate_key(session_id=session_id, data_graph=data_graph) # Load the graph using SessionService - graph, artifacts = session_service.load_session(session_id, data_graph) + graph, artifacts = async_to_sync(session_service.load_session)(session_id, data_graph) built_object = graph.build() processed_inputs = process_inputs(inputs, artifacts) result = generate_result(built_object, processed_inputs) diff --git a/tests/test_graph.py b/tests/test_graph.py index 1c63e62e8..cb69d79d5 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,32 +1,30 @@ import copy import json import os -from pathlib import Path import pickle +from pathlib import Path from typing import Type, Union -from langflow.graph.edge.base import Edge -from langflow.graph.vertex.base import Vertex -from langchain.agents import AgentExecutor + import pytest +from langchain.agents import AgentExecutor from langchain.chains.base import Chain from langchain.llms.fake import FakeListLLM from langflow.graph import Graph -from langflow.graph.vertex.types import ( - FileToolVertex, - LLMVertex, - ToolkitVertex, -) -from langflow.processing.process import get_result_and_thought -from langflow.utils.payload import get_root_node +from langflow.graph.edge.base import Edge from langflow.graph.graph.utils import ( find_last_node, + process_flow, set_new_target_handle, ungroup_node, - process_flow, update_source_handle, update_target_handle, update_template, ) +from langflow.graph.utils import UnbuiltObject +from langflow.graph.vertex.base import Vertex +from langflow.graph.vertex.types import FileToolVertex, LLMVertex, ToolkitVertex +from langflow.processing.process import get_result_and_thought +from langflow.utils.payload import get_root_node # Test cases for the graph module @@ -232,29 +230,32 @@ def test_build_params(basic_graph): assert "memory" in root.params -def test_build(basic_graph): +@pytest.mark.asyncio +async def test_build(basic_graph): """Test Node's build method""" - assert_agent_was_built(basic_graph) + await assert_agent_was_built(basic_graph) -def assert_agent_was_built(graph): +async def assert_agent_was_built(graph): """Assert that the agent was built""" assert isinstance(graph, Graph) # Now we test the build method # Build the Agent - result = graph.build() + result = await graph.build() # The agent should be a AgentExecutor assert isinstance(result, Chain) -def test_llm_node_build(basic_graph): +@pytest.mark.asyncio +async def test_llm_node_build(basic_graph): llm_node = get_node_by_type(basic_graph, LLMVertex) assert llm_node is not None - built_object = llm_node.build() - assert built_object is not None + built_object = await llm_node.build() + assert built_object is not UnbuiltObject() -def test_toolkit_node_build(client, openapi_graph): +@pytest.mark.asyncio +async def test_toolkit_node_build(client, openapi_graph): # Write a file to the disk file_path = "api-with-examples.yaml" with open(file_path, "w") as f: @@ -262,36 +263,31 @@ def test_toolkit_node_build(client, openapi_graph): toolkit_node = get_node_by_type(openapi_graph, ToolkitVertex) assert toolkit_node is not None - built_object = toolkit_node.build() - assert built_object is not None + built_object = await toolkit_node.build() + assert built_object is not UnbuiltObject # Remove the file os.remove(file_path) assert not Path(file_path).exists() -def test_file_tool_node_build(client, openapi_graph): +@pytest.mark.asyncio +async def test_file_tool_node_build(client, openapi_graph): file_path = "api-with-examples.yaml" with open(file_path, "w") as f: f.write("openapi: 3.0.0") assert Path(file_path).exists() file_tool_node = get_node_by_type(openapi_graph, FileToolVertex) - assert file_tool_node is not None - built_object = file_tool_node.build() - assert built_object is not None + assert file_tool_node is not UnbuiltObject + built_object = await file_tool_node.build() + assert built_object is not UnbuiltObject # Remove the file os.remove(file_path) assert not Path(file_path).exists() -# def test_wrapper_node_build(openapi_graph): -# wrapper_node = get_node_by_type(openapi_graph, WrapperVertex) -# assert wrapper_node is not None -# built_object = wrapper_node.build() -# assert built_object is not None - - -def test_get_result_and_thought(basic_graph): +@pytest.mark.asyncio +async def test_get_result_and_thought(basic_graph): """Test the get_result_and_thought method""" responses = [ "Final Answer: I am a response", @@ -303,7 +299,7 @@ def test_get_result_and_thought(basic_graph): assert llm_node is not None llm_node._built_object = FakeListLLM(responses=responses) llm_node._built = True - langchain_object = basic_graph.build() + langchain_object = await basic_graph.build() # assert all nodes are built assert all(node._built for node in basic_graph.nodes) # now build again and check if FakeListLLM was used @@ -486,27 +482,29 @@ def test_update_source_handle(): assert updated_edge["data"]["sourceHandle"]["id"] == "last_node" -def test_pickle_graph(json_vector_store): +@pytest.mark.asyncio +async def test_pickle_graph(json_vector_store): loaded_json = json.loads(json_vector_store) graph = Graph.from_payload(loaded_json) assert isinstance(graph, Graph) - first_result = graph.build() + first_result = await graph.build() assert isinstance(first_result, AgentExecutor) pickled = pickle.dumps(graph) - assert pickled is not None + assert pickled is not UnbuiltObject unpickled = pickle.loads(pickled) - assert unpickled is not None - result = unpickled.build() + assert unpickled is not UnbuiltObject + result = await unpickled.build() assert isinstance(result, AgentExecutor) -def test_pickle_each_vertex(json_vector_store): +@pytest.mark.asyncio +async def test_pickle_each_vertex(json_vector_store): loaded_json = json.loads(json_vector_store) graph = Graph.from_payload(loaded_json) assert isinstance(graph, Graph) for vertex in graph.nodes: - vertex.build() + await vertex.build() pickled = pickle.dumps(vertex) - assert pickled is not None + assert pickled is not UnbuiltObject unpickled = pickle.loads(pickled) - assert unpickled is not None + assert unpickled is not UnbuiltObject diff --git a/tests/test_process.py b/tests/test_process.py index 29ec267a1..c8e4ec9cc 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,3 +1,4 @@ +import pytest from langflow.processing.process import process_tweaks from langflow.services.deps import get_session_service @@ -197,39 +198,42 @@ def test_tweak_not_in_template(): assert result == graph_data -def test_load_langchain_object_with_cached_session(client, basic_graph_data): +@pytest.mark.asyncio +async def test_load_langchain_object_with_cached_session(client, basic_graph_data): # Provide a non-existent session_id session_service = get_session_service() session_id1 = "non-existent-session-id" - graph1, artifacts1 = session_service.load_session(session_id1, basic_graph_data) + graph1, artifacts1 = await session_service.load_session(session_id1, basic_graph_data) # Use the new session_id to get the langchain_object again - graph2, artifacts2 = session_service.load_session(session_id1, basic_graph_data) + graph2, artifacts2 = await session_service.load_session(session_id1, basic_graph_data) assert graph1 == graph2 assert artifacts1 == artifacts2 -def test_load_langchain_object_with_no_cached_session(client, basic_graph_data): +@pytest.mark.asyncio +async def test_load_langchain_object_with_no_cached_session(client, basic_graph_data): # Provide a non-existent session_id session_service = get_session_service() session_id1 = "non-existent-session-id" session_id = session_service.build_key(session_id1, basic_graph_data) - graph1, artifacts1 = session_service.load_session(session_id, basic_graph_data) + graph1, artifacts1 = await session_service.load_session(session_id, basic_graph_data) # Clear the cache session_service.clear_session(session_id) # Use the new session_id to get the langchain_object again - graph2, artifacts2 = session_service.load_session(session_id, basic_graph_data) + graph2, artifacts2 = await session_service.load_session(session_id, basic_graph_data) assert id(graph1) != id(graph2) # Since the cache was cleared, objects should be different -def test_load_langchain_object_without_session_id(client, basic_graph_data): +@pytest.mark.asyncio +async def test_load_langchain_object_without_session_id(client, basic_graph_data): # Provide a non-existent session_id session_service = get_session_service() session_id1 = None - graph1, artifacts1 = session_service.load_session(session_id1, basic_graph_data) + graph1, artifacts1 = await session_service.load_session(session_id1, basic_graph_data) # Use the new session_id to get the langchain_object again - graph2, artifacts2 = session_service.load_session(session_id1, basic_graph_data) + graph2, artifacts2 = await session_service.load_session(session_id1, basic_graph_data) assert graph1 == graph2 From 05d7bd23863631f36a08d6e8f18da87216d41309 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 09:36:24 -0300 Subject: [PATCH 08/44] Update OpenAIConversationalAgent.py --- .../agents/OpenAIConversationalAgent.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/components/agents/OpenAIConversationalAgent.py b/src/backend/langflow/components/agents/OpenAIConversationalAgent.py index eb53a89c0..46a0f1817 100644 --- a/src/backend/langflow/components/agents/OpenAIConversationalAgent.py +++ b/src/backend/langflow/components/agents/OpenAIConversationalAgent.py @@ -1,17 +1,15 @@ -from langflow import CustomComponent from typing import Optional -from langchain.prompts import SystemMessagePromptTemplate -from langchain.tools import Tool -from langchain.schema.memory import BaseMemory -from langchain.chat_models import ChatOpenAI from langchain.agents.agent import AgentExecutor +from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import _get_default_system_message from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent +from langchain.chat_models import ChatOpenAI from langchain.memory.token_buffer import ConversationTokenBufferMemory +from langchain.prompts import SystemMessagePromptTemplate from langchain.prompts.chat import MessagesPlaceholder -from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import ( - _get_default_system_message, -) +from langchain.schema.memory import BaseMemory +from langchain.tools import Tool +from langflow import CustomComponent class ConversationalAgent(CustomComponent): @@ -51,8 +49,8 @@ class ConversationalAgent(CustomComponent): ) -> AgentExecutor: llm = ChatOpenAI( model=model_name, - openai_api_key=openai_api_key, - openai_api_base=openai_api_base, + api_key=openai_api_key, + base_url=openai_api_base, ) if not memory: memory_key = "chat_history" From 0541078e43338e0aa483dccc450b0f5450893e69 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:12 -0300 Subject: [PATCH 09/44] Update typing import in OpenAIConversationalAgent.py --- .../langflow/components/agents/OpenAIConversationalAgent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/components/agents/OpenAIConversationalAgent.py b/src/backend/langflow/components/agents/OpenAIConversationalAgent.py index 46a0f1817..499775747 100644 --- a/src/backend/langflow/components/agents/OpenAIConversationalAgent.py +++ b/src/backend/langflow/components/agents/OpenAIConversationalAgent.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from langchain.agents.agent import AgentExecutor from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import _get_default_system_message @@ -25,7 +25,7 @@ class ConversationalAgent(CustomComponent): "gpt-4-32k", ] return { - "tools": {"is_list": True, "display_name": "Tools"}, + "tools": {"display_name": "Tools"}, "memory": {"display_name": "Memory"}, "system_message": {"display_name": "System Message"}, "max_token_limit": {"display_name": "Max Token Limit"}, @@ -41,7 +41,7 @@ class ConversationalAgent(CustomComponent): self, model_name: str, openai_api_key: str, - tools: Tool, + tools: List[Tool], openai_api_base: Optional[str] = None, memory: Optional[BaseMemory] = None, system_message: Optional[SystemMessagePromptTemplate] = None, From 1dc7053fbf11c3eeadeeb5afa9d2b6499791c593 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:19 -0300 Subject: [PATCH 10/44] Refactor flatten_query_string_lists function --- src/backend/langflow/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index d93bd8004..e78ffcbde 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,7 +6,6 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles - from langflow.api import router from langflow.interface.utils import setup_llm_caching from langflow.services.plugins.langfuse import LangfuseInstance @@ -32,7 +31,7 @@ def create_app(): @app.middleware("http") async def flatten_query_string_lists(request: Request, call_next): - flattened = [] + flattened: list[tuple[str, str]] = [] for key, value in request.query_params.multi_items(): flattened.extend((key, entry) for entry in value.split(",")) @@ -100,7 +99,6 @@ def setup_app(static_files_dir: Optional[Path] = None, backend_only: bool = Fals if __name__ == "__main__": import uvicorn - from langflow.__main__ import get_number_of_workers configure() From fdaca098d95420b7d071d47ea95cc029a8fe6de0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:27 -0300 Subject: [PATCH 11/44] Fix error in evaluating custom component code --- src/backend/langflow/interface/types.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index f460edf68..092b553bd 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -203,9 +203,14 @@ def build_field_config(custom_component: CustomComponent, user_id: Optional[Unio """Build the field configuration for a custom component""" try: - custom_class = eval_custom_component_code(custom_component.code) + if custom_component.code is None: + return {} + 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: - logger.error(f"Error while getting custom function: {str(exc)}") + logger.error(f"Error while evaluating custom component code: {str(exc)}") raise HTTPException( status_code=400, detail={ From 33d2b9d6aa7bc9f7efeaf8f99b6fb8bf143f2597 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:34 -0300 Subject: [PATCH 12/44] Refactor agent imports and remove unused imports --- .../langflow/interface/agents/custom.py | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/backend/langflow/interface/agents/custom.py b/src/backend/langflow/interface/agents/custom.py index b9da48b73..512623ef2 100644 --- a/src/backend/langflow/interface/agents/custom.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -1,13 +1,6 @@ from typing import Any, List, Optional -from langchain.chains.llm import LLMChain -from langchain.agents import ( - AgentExecutor, - Tool, - ZeroShotAgent, - initialize_agent, - AgentType, -) +from langchain.agents import AgentExecutor, AgentType, Tool, ZeroShotAgent, initialize_agent from langchain.agents.agent_toolkits import ( SQLDatabaseToolkit, VectorStoreInfo, @@ -16,25 +9,18 @@ from langchain.agents.agent_toolkits import ( ) from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit -from langchain_experimental.agents.agent_toolkits.pandas.prompt import ( - PREFIX as PANDAS_PREFIX, -) -from langchain_experimental.agents.agent_toolkits.pandas.prompt import ( - SUFFIX_WITH_DF as PANDAS_SUFFIX, -) from langchain.agents.agent_toolkits.sql.prompt import SQL_PREFIX, SQL_SUFFIX -from langchain.agents.agent_toolkits.vectorstore.prompt import ( - PREFIX as VECTORSTORE_PREFIX, -) -from langchain.agents.agent_toolkits.vectorstore.prompt import ( - ROUTER_PREFIX as VECTORSTORE_ROUTER_PREFIX, -) +from langchain.agents.agent_toolkits.vectorstore.prompt import PREFIX as VECTORSTORE_PREFIX +from langchain.agents.agent_toolkits.vectorstore.prompt import ROUTER_PREFIX as VECTORSTORE_ROUTER_PREFIX from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS from langchain.base_language import BaseLanguageModel +from langchain.chains.llm import LLMChain from langchain.memory.chat_memory import BaseChatMemory from langchain.sql_database import SQLDatabase -from langchain_experimental.tools.python.tool import PythonAstREPLTool from langchain.tools.sql_database.prompt import QUERY_CHECKER +from langchain_experimental.agents.agent_toolkits.pandas.prompt import PREFIX as PANDAS_PREFIX +from langchain_experimental.agents.agent_toolkits.pandas.prompt import SUFFIX_WITH_DF as PANDAS_SUFFIX +from langchain_experimental.tools.python.tool import PythonAstREPLTool from langflow.interface.base import CustomAgentExecutor @@ -55,7 +41,7 @@ class JsonAgent(CustomAgentExecutor): @classmethod def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel): tools = toolkit if isinstance(toolkit, list) else toolkit.get_tools() - tool_names = {tool.name for tool in tools} + tool_names = list({tool.name for tool in tools}) prompt = ZeroShotAgent.create_prompt( tools, prefix=JSON_PREFIX, @@ -112,7 +98,7 @@ class CSVAgent(CustomAgentExecutor): llm=llm, prompt=partial_prompt, ) - tool_names = {tool.name for tool in tools} + tool_names = list({tool.name for tool in tools}) agent = ZeroShotAgent( llm_chain=llm_chain, allowed_tools=tool_names, @@ -151,7 +137,7 @@ class VectorStoreAgent(CustomAgentExecutor): llm=llm, prompt=prompt, ) - tool_names = {tool.name for tool in tools} + tool_names = list({tool.name for tool in tools}) agent = ZeroShotAgent( llm_chain=llm_chain, allowed_tools=tool_names, @@ -217,7 +203,7 @@ class SQLAgent(CustomAgentExecutor): llm=llm, prompt=prompt, ) - tool_names = {tool.name for tool in tools} # type: ignore + tool_names = list({tool.name for tool in tools}) # type: ignore agent = ZeroShotAgent( llm_chain=llm_chain, allowed_tools=tool_names, @@ -266,7 +252,7 @@ class VectorStoreRouterAgent(CustomAgentExecutor): llm=llm, prompt=prompt, ) - tool_names = {tool.name for tool in tools} + tool_names = list({tool.name for tool in tools}) agent = ZeroShotAgent( llm_chain=llm_chain, allowed_tools=tool_names, From 49da01345e57fa24b38ede67efe0e8aaccd7b991 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:40 -0300 Subject: [PATCH 13/44] Fix variable type annotation in CodeParser class --- src/backend/langflow/interface/custom/code_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/custom/code_parser.py b/src/backend/langflow/interface/custom/code_parser.py index d1b33a4e2..fd6e80a88 100644 --- a/src/backend/langflow/interface/custom/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser.py @@ -4,6 +4,7 @@ import traceback from typing import Any, Dict, List, Type, Union from fastapi import HTTPException + from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails @@ -106,7 +107,7 @@ class CodeParser: Constructs an evaluation environment with the necessary imports for the return type, taking into account module aliases. """ - eval_env = {} + eval_env: dict = {} for import_entry in self.data["imports"]: if isinstance(import_entry, tuple): # from module import name module, name = import_entry From 1da20fadef8dd0edfb9f5e16d4103071fcc7f6b3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:03:48 -0300 Subject: [PATCH 14/44] Fix async issue in CustomComponent and decrypt_api_key function --- .../langflow/interface/custom/custom_component.py | 5 +++-- src/backend/langflow/services/auth/utils.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index cb581e182..ba77cea8a 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -3,6 +3,7 @@ from uuid import UUID import yaml from fastapi import HTTPException + from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component from langflow.interface.custom.directory_reader import DirectoryReader @@ -212,7 +213,7 @@ class CustomComponent(Component): except Exception as e: raise ValueError("Session is invalid") from e - def get_flow( + async def get_flow( self, *, flow_name: Optional[str] = None, @@ -232,7 +233,7 @@ class CustomComponent(Component): if not flow: raise ValueError(f"Flow {flow_name or flow_id} not found") - return self.load_flow(flow.id, tweaks) + return await self.load_flow(flow.id, tweaks) def build(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index be361b80a..da0ee7396 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -6,12 +6,13 @@ from cryptography.fernet import Fernet from fastapi import Depends, HTTPException, Security, status from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer from jose import JWTError, jwt +from sqlmodel import Session + from langflow.services.database.models.api_key.api_key import ApiKey from langflow.services.database.models.api_key.crud import check_key from langflow.services.database.models.user.crud import get_user_by_id, get_user_by_username, update_user_last_login_at from langflow.services.database.models.user.user import User from langflow.services.deps import get_session, get_settings_service -from sqlmodel import Session oauth2_login = OAuth2PasswordBearer(tokenUrl="api/v1/login", auto_error=False) @@ -323,6 +324,8 @@ def decrypt_api_key(encrypted_api_key: str, settings_service=Depends(get_setting fernet = get_fernet(settings_service) # Two-way decryption if isinstance(encrypted_api_key, str): - encrypted_api_key = encrypted_api_key.encode() - decrypted_key = fernet.decrypt(encrypted_api_key).decode() + encoded_bytes = encrypted_api_key.encode() + else: + encoded_bytes = encrypted_api_key + decrypted_key = fernet.decrypt(encoded_bytes).decode() return decrypted_key From 65c1a1b24186245e7a54e3303f9be997fd9f6e93 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:37:44 -0300 Subject: [PATCH 15/44] Add typing imports to constants.py --- src/backend/langflow/interface/custom/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/custom/constants.py b/src/backend/langflow/interface/custom/constants.py index 298b7a969..36ebf420e 100644 --- a/src/backend/langflow/interface/custom/constants.py +++ b/src/backend/langflow/interface/custom/constants.py @@ -1,5 +1,5 @@ DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent - +from typing import Optional, List, Dict, Union from langflow.field_typing import ( AgentExecutor, BaseChatMemory, From bcd13e18f3b36ba5bb13bbc0f08235cb5aa7c672 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:37:59 -0300 Subject: [PATCH 16/44] Fix missing type for argument in CustomComponent class --- src/backend/langflow/interface/custom/custom_component.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index ba77cea8a..005acbc05 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -3,7 +3,6 @@ from uuid import UUID import yaml from fastapi import HTTPException - from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component from langflow.interface.custom.directory_reader import DirectoryReader @@ -111,7 +110,7 @@ class CustomComponent(Component): ), }, ) - elif not arg.get("type"): + elif not arg.get("type") and arg.get("name") != "self": # Set the type to Data arg["type"] = "Data" return args From f4210f567bfce50ba56b84d5f72c082dc7af0f6a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:57:06 -0300 Subject: [PATCH 17/44] Add caching to CustomComponent class --- .../interface/custom/custom_component.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index 005acbc05..a0f5d6967 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -1,7 +1,9 @@ +import operator from typing import Any, Callable, ClassVar, List, Optional, Union from uuid import UUID import yaml +from cachetools import TTLCache, cachedmethod from fastapi import HTTPException from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component @@ -29,6 +31,7 @@ class CustomComponent(Component): status: Optional[str] = None def __init__(self, **data): + self.cache = TTLCache(maxsize=1024, ttl=60) super().__init__(**data) @property @@ -78,25 +81,10 @@ class CustomComponent(Component): return super().get_code_tree(code) @property - def get_function_entrypoint_args(self) -> str: - if not self.code: - return "" - tree = self.get_code_tree(self.code) - - component_classes = [cls for cls in tree["classes"] if self.code_class_base_inheritance in cls["bases"]] - if not component_classes: - return "" - - # Assume the first Component class is the one we're interested in - component_class = component_classes[0] - build_methods = [ - method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name - ] - - if not build_methods: - return "" - - build_method = build_methods[0] + def get_function_entrypoint_args(self) -> list: + build_method = self.get_build_method() + if not build_method: + return [] args = build_method["args"] for arg in args: @@ -115,8 +103,8 @@ class CustomComponent(Component): arg["type"] = "Data" return args - @property - def get_function_entrypoint_return_type(self) -> List[str]: + @cachedmethod(operator.attrgetter("cache")) + def get_build_method(self): if not self.code: return [] tree = self.get_code_tree(self.code) @@ -134,7 +122,13 @@ class CustomComponent(Component): if not build_methods: return [] - build_method = build_methods[0] + return build_methods[0] + + @property + def get_function_entrypoint_return_type(self) -> List[str]: + build_method = self.get_build_method() + if not build_method: + return build_method return_type = build_method["return_type"] if not return_type: return [] From 946bf0319064b0d52d0bfb3c64ec7b0e0603d479 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:57:11 -0300 Subject: [PATCH 18/44] Add cache functionality to Component class --- src/backend/langflow/interface/custom/component.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/langflow/interface/custom/component.py b/src/backend/langflow/interface/custom/component.py index 5a476ff58..0f503accb 100644 --- a/src/backend/langflow/interface/custom/component.py +++ b/src/backend/langflow/interface/custom/component.py @@ -1,6 +1,8 @@ import ast +import operator from typing import Any, ClassVar, Optional +from cachetools import TTLCache, cachedmethod from fastapi import HTTPException from langflow.interface.custom.code_parser import CodeParser @@ -24,9 +26,11 @@ class Component: field_config: dict = {} def __init__(self, **data): + self.cache = TTLCache(maxsize=1024, ttl=60) for key, value in data.items(): setattr(self, key, value) + @cachedmethod(operator.attrgetter("cache")) def get_code_tree(self, code: str): parser = CodeParser(code) return parser.parse_code() From 48dbbf1dd318061ab3c269e10581a07443d39cd1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:57:15 -0300 Subject: [PATCH 19/44] Add caching for code parsing --- src/backend/langflow/interface/custom/code_parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/custom/code_parser.py b/src/backend/langflow/interface/custom/code_parser.py index fd6e80a88..691bc577e 100644 --- a/src/backend/langflow/interface/custom/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser.py @@ -1,8 +1,10 @@ import ast import inspect +import operator import traceback from typing import Any, Dict, List, Type, Union +from cachetools import TTLCache, cachedmethod from fastapi import HTTPException from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails @@ -27,6 +29,7 @@ class CodeParser: """ Initializes the parser with the provided code. """ + self.cache = TTLCache(maxsize=1024, ttl=60) if isinstance(code, type): if not inspect.isclass(code): raise ValueError("The provided code must be a class.") @@ -123,6 +126,7 @@ class CodeParser: exec(f"import {module} as {alias if alias else module}", eval_env) return eval_env + @cachedmethod(operator.attrgetter("cache")) def parse_callable_details(self, node: ast.FunctionDef) -> Dict[str, Any]: """ Extracts details from a single function or method node. @@ -267,7 +271,7 @@ class CodeParser: elif isinstance(stmt, ast.AnnAssign): if attr := self.parse_ann_assign(stmt): class_details.attributes.append(attr) - elif isinstance(stmt, ast.FunctionDef): + elif isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): method, is_init = self.parse_function_def(stmt) if is_init: class_details.init = method From 14db3c0263f8018ff912a130286174bed1b03a66 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 10:57:23 -0300 Subject: [PATCH 20/44] Refactor function_args check in add_extra_fields() --- src/backend/langflow/interface/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 092b553bd..dde82370d 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -234,7 +234,7 @@ def build_field_config(custom_component: CustomComponent, user_id: Optional[Unio def add_extra_fields(frontend_node, field_config, function_args): """Add extra fields to the frontend node""" - if function_args is None or function_args == "": + if not function_args: return # sort function_args which is a list of dicts From 6fea5897783b5cc298f12af7de71039699163f43 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:09:19 -0300 Subject: [PATCH 21/44] Refactor construct_eval_env method in CodeParser class --- src/backend/langflow/interface/custom/code_parser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/interface/custom/code_parser.py b/src/backend/langflow/interface/custom/code_parser.py index 691bc577e..3f4bf35e4 100644 --- a/src/backend/langflow/interface/custom/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser.py @@ -6,7 +6,6 @@ from typing import Any, Dict, List, Type, Union from cachetools import TTLCache, cachedmethod from fastapi import HTTPException - from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails @@ -105,13 +104,15 @@ class CodeParser: arg_dict["type"] = ast.unparse(arg.annotation) return arg_dict - def construct_eval_env(self, return_type_str: str) -> dict: + @cachedmethod(operator.attrgetter("cache")) + @staticmethod + def construct_eval_env(return_type_str: str, imports) -> dict: """ Constructs an evaluation environment with the necessary imports for the return type, taking into account module aliases. """ eval_env: dict = {} - for import_entry in self.data["imports"]: + for import_entry in imports: if isinstance(import_entry, tuple): # from module import name module, name = import_entry if name in return_type_str: @@ -134,7 +135,7 @@ class CodeParser: return_type = None if node.returns: return_type_str = ast.unparse(node.returns) - eval_env = self.construct_eval_env(return_type_str) + eval_env = self.construct_eval_env(return_type_str, self.data["imports"]) try: return_type = eval(return_type_str, eval_env) From ccfd8e8acd93dd06cf795c4e297289ab8a62386a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:48:06 -0300 Subject: [PATCH 22/44] Refactor file loader component --- .../components/documentloaders/FileLoader.py | 124 +----------------- 1 file changed, 6 insertions(+), 118 deletions(-) diff --git a/src/backend/langflow/components/documentloaders/FileLoader.py b/src/backend/langflow/components/documentloaders/FileLoader.py index 072603a96..eea401b83 100644 --- a/src/backend/langflow/components/documentloaders/FileLoader.py +++ b/src/backend/langflow/components/documentloaders/FileLoader.py @@ -1,119 +1,7 @@ -from langflow import CustomComponent from langchain.schema import Document -from typing import Any, Dict, List -loaders_info: List[Dict[str, Any]] = [ - { - "loader": "AirbyteJSONLoader", - "name": "Airbyte JSON (.jsonl)", - "import": "langchain.document_loaders.AirbyteJSONLoader", - "defaultFor": ["jsonl"], - "allowdTypes": ["jsonl"], - }, - { - "loader": "JSONLoader", - "name": "JSON (.json)", - "import": "langchain.document_loaders.JSONLoader", - "defaultFor": ["json"], - "allowdTypes": ["json"], - }, - { - "loader": "BSHTMLLoader", - "name": "BeautifulSoup4 HTML (.html, .htm)", - "import": "langchain.document_loaders.BSHTMLLoader", - "allowdTypes": ["html", "htm"], - }, - { - "loader": "CSVLoader", - "name": "CSV (.csv)", - "import": "langchain.document_loaders.CSVLoader", - "defaultFor": ["csv"], - "allowdTypes": ["csv"], - }, - { - "loader": "CoNLLULoader", - "name": "CoNLL-U (.conllu)", - "import": "langchain.document_loaders.CoNLLULoader", - "defaultFor": ["conllu"], - "allowdTypes": ["conllu"], - }, - { - "loader": "EverNoteLoader", - "name": "EverNote (.enex)", - "import": "langchain.document_loaders.EverNoteLoader", - "defaultFor": ["enex"], - "allowdTypes": ["enex"], - }, - { - "loader": "FacebookChatLoader", - "name": "Facebook Chat (.json)", - "import": "langchain.document_loaders.FacebookChatLoader", - "allowdTypes": ["json"], - }, - { - "loader": "OutlookMessageLoader", - "name": "Outlook Message (.msg)", - "import": "langchain.document_loaders.OutlookMessageLoader", - "defaultFor": ["msg"], - "allowdTypes": ["msg"], - }, - { - "loader": "PyPDFLoader", - "name": "PyPDF (.pdf)", - "import": "langchain.document_loaders.PyPDFLoader", - "defaultFor": ["pdf"], - "allowdTypes": ["pdf"], - }, - { - "loader": "STRLoader", - "name": "Subtitle (.str)", - "import": "langchain.document_loaders.STRLoader", - "defaultFor": ["str"], - "allowdTypes": ["str"], - }, - { - "loader": "TextLoader", - "name": "Text (.txt)", - "import": "langchain.document_loaders.TextLoader", - "defaultFor": ["txt"], - "allowdTypes": ["txt"], - }, - { - "loader": "UnstructuredEmailLoader", - "name": "Unstructured Email (.eml)", - "import": "langchain.document_loaders.UnstructuredEmailLoader", - "defaultFor": ["eml"], - "allowdTypes": ["eml"], - }, - { - "loader": "UnstructuredHTMLLoader", - "name": "Unstructured HTML (.html, .htm)", - "import": "langchain.document_loaders.UnstructuredHTMLLoader", - "defaultFor": ["html", "htm"], - "allowdTypes": ["html", "htm"], - }, - { - "loader": "UnstructuredMarkdownLoader", - "name": "Unstructured Markdown (.md)", - "import": "langchain.document_loaders.UnstructuredMarkdownLoader", - "defaultFor": ["md"], - "allowdTypes": ["md"], - }, - { - "loader": "UnstructuredPowerPointLoader", - "name": "Unstructured PowerPoint (.pptx)", - "import": "langchain.document_loaders.UnstructuredPowerPointLoader", - "defaultFor": ["pptx"], - "allowdTypes": ["pptx"], - }, - { - "loader": "UnstructuredWordLoader", - "name": "Unstructured Word (.docx)", - "import": "langchain.document_loaders.UnstructuredWordLoader", - "defaultFor": ["docx"], - "allowdTypes": ["docx"], - }, -] +from langflow import CustomComponent +from langflow.utils.constants import LOADERS_INFO class FileLoaderComponent(CustomComponent): @@ -122,12 +10,12 @@ class FileLoaderComponent(CustomComponent): beta = True def build_config(self): - loader_options = ["Automatic"] + [loader_info["name"] for loader_info in loaders_info] + loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO] file_types = [] suffixes = [] - for loader_info in loaders_info: + for loader_info in LOADERS_INFO: if "allowedTypes" in loader_info: file_types.extend(loader_info["allowedTypes"]) suffixes.extend([f".{ext}" for ext in loader_info["allowedTypes"]]) @@ -189,7 +77,7 @@ class FileLoaderComponent(CustomComponent): # Mapeie o nome do loader selecionado para suas informações selected_loader_info = None - for loader_info in loaders_info: + for loader_info in LOADERS_INFO: if loader_info["name"] == loader: selected_loader_info = loader_info break @@ -200,7 +88,7 @@ class FileLoaderComponent(CustomComponent): if loader == "Automatic": # Determine o loader automaticamente com base na extensão do arquivo default_loader_info = None - for info in loaders_info: + for info in LOADERS_INFO: if "defaultFor" in info and file_type in info["defaultFor"]: default_loader_info = info break From 7a521fa6cbfbda5768b52f4aca7139e15b9a0c82 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:48:14 -0300 Subject: [PATCH 23/44] Add new loaders to constants.py --- src/backend/langflow/utils/constants.py | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/backend/langflow/utils/constants.py b/src/backend/langflow/utils/constants.py index 283f44406..e347a8f63 100644 --- a/src/backend/langflow/utils/constants.py +++ b/src/backend/langflow/utils/constants.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, List + OPENAI_MODELS = [ "text-davinci-003", "text-davinci-002", @@ -59,3 +61,117 @@ DIRECT_TYPES = [ "code", "NestedDict", ] + + +LOADERS_INFO: List[Dict[str, Any]] = [ + { + "loader": "AirbyteJSONLoader", + "name": "Airbyte JSON (.jsonl)", + "import": "langchain.document_loaders.AirbyteJSONLoader", + "defaultFor": ["jsonl"], + "allowdTypes": ["jsonl"], + }, + { + "loader": "JSONLoader", + "name": "JSON (.json)", + "import": "langchain.document_loaders.JSONLoader", + "defaultFor": ["json"], + "allowdTypes": ["json"], + }, + { + "loader": "BSHTMLLoader", + "name": "BeautifulSoup4 HTML (.html, .htm)", + "import": "langchain.document_loaders.BSHTMLLoader", + "allowdTypes": ["html", "htm"], + }, + { + "loader": "CSVLoader", + "name": "CSV (.csv)", + "import": "langchain.document_loaders.CSVLoader", + "defaultFor": ["csv"], + "allowdTypes": ["csv"], + }, + { + "loader": "CoNLLULoader", + "name": "CoNLL-U (.conllu)", + "import": "langchain.document_loaders.CoNLLULoader", + "defaultFor": ["conllu"], + "allowdTypes": ["conllu"], + }, + { + "loader": "EverNoteLoader", + "name": "EverNote (.enex)", + "import": "langchain.document_loaders.EverNoteLoader", + "defaultFor": ["enex"], + "allowdTypes": ["enex"], + }, + { + "loader": "FacebookChatLoader", + "name": "Facebook Chat (.json)", + "import": "langchain.document_loaders.FacebookChatLoader", + "allowdTypes": ["json"], + }, + { + "loader": "OutlookMessageLoader", + "name": "Outlook Message (.msg)", + "import": "langchain.document_loaders.OutlookMessageLoader", + "defaultFor": ["msg"], + "allowdTypes": ["msg"], + }, + { + "loader": "PyPDFLoader", + "name": "PyPDF (.pdf)", + "import": "langchain.document_loaders.PyPDFLoader", + "defaultFor": ["pdf"], + "allowdTypes": ["pdf"], + }, + { + "loader": "STRLoader", + "name": "Subtitle (.str)", + "import": "langchain.document_loaders.STRLoader", + "defaultFor": ["str"], + "allowdTypes": ["str"], + }, + { + "loader": "TextLoader", + "name": "Text (.txt)", + "import": "langchain.document_loaders.TextLoader", + "defaultFor": ["txt"], + "allowdTypes": ["txt"], + }, + { + "loader": "UnstructuredEmailLoader", + "name": "Unstructured Email (.eml)", + "import": "langchain.document_loaders.UnstructuredEmailLoader", + "defaultFor": ["eml"], + "allowdTypes": ["eml"], + }, + { + "loader": "UnstructuredHTMLLoader", + "name": "Unstructured HTML (.html, .htm)", + "import": "langchain.document_loaders.UnstructuredHTMLLoader", + "defaultFor": ["html", "htm"], + "allowdTypes": ["html", "htm"], + }, + { + "loader": "UnstructuredMarkdownLoader", + "name": "Unstructured Markdown (.md)", + "import": "langchain.document_loaders.UnstructuredMarkdownLoader", + "defaultFor": ["md"], + "allowdTypes": ["md"], + }, + { + "loader": "UnstructuredPowerPointLoader", + "name": "Unstructured PowerPoint (.pptx)", + "import": "langchain.document_loaders.UnstructuredPowerPointLoader", + "defaultFor": ["pptx"], + "allowdTypes": ["pptx"], + }, + { + "loader": "UnstructuredWordLoader", + "name": "Unstructured Word (.docx)", + "import": "langchain.document_loaders.UnstructuredWordLoader", + "defaultFor": ["docx"], + "allowdTypes": ["docx"], + }, +] From 49c9187e9c397826b719300721e86fc6ddf588e6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:49:04 -0300 Subject: [PATCH 24/44] Update return type extraction to return a list instead of a tuple --- src/backend/langflow/interface/custom/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 11a155a00..e527670a0 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -22,11 +22,11 @@ def extract_inner_type_from_generic_alias(return_type: GenericAlias) -> Any: return return_type -def extract_union_types_from_generic_alias(return_type: GenericAlias) -> tuple: +def extract_union_types_from_generic_alias(return_type: GenericAlias) -> list: """ Extracts the inner type from a type hint that is a Union. """ - return return_type.__args__ + return list(return_type.__args__) def extract_union_types(return_type: str) -> list[str]: From 4d285000cf26c2ae98de1f568f7b9990f61e051d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:49:13 -0300 Subject: [PATCH 25/44] Fix code validation and return type in CustomComponent --- .../langflow/interface/custom/custom_component.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index a0f5d6967..032ed1476 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -5,6 +5,7 @@ from uuid import UUID import yaml from cachetools import TTLCache, cachedmethod from fastapi import HTTPException + from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component from langflow.interface.custom.directory_reader import DirectoryReader @@ -73,8 +74,9 @@ class CustomComponent(Component): "traceback": f"Type hint '{type_hint}' is used but not imported in the code.", } raise HTTPException(status_code=400, detail=error_detail) + return True - def is_check_valid(self) -> bool: + def validate(self) -> bool: return self._class_template_validation(self.code) if self.code else False def get_code_tree(self, code: str): @@ -125,7 +127,7 @@ class CustomComponent(Component): return build_methods[0] @property - def get_function_entrypoint_return_type(self) -> List[str]: + def get_function_entrypoint_return_type(self) -> List[Any]: build_method = self.get_build_method() if not build_method: return build_method @@ -149,13 +151,15 @@ class CustomComponent(Component): @property def get_main_class_name(self): + if not self.code: + return "" tree = self.get_code_tree(self.code) base_name = self.code_class_base_inheritance method_name = self.function_entrypoint_name classes = [] - for item in tree.get("classes"): + for item in tree.get("classes", []): if base_name in item["bases"]: method_names = [method["name"] for method in item["methods"]] if method_name in method_names: @@ -166,11 +170,13 @@ class CustomComponent(Component): @property def build_template_config(self): + if not self.code: + return {} tree = self.get_code_tree(self.code) attributes = [ main_class["attributes"] - for main_class in tree.get("classes") + for main_class in tree.get("classes", []) if main_class["name"] == self.get_main_class_name ] # Get just the first item From 2f6a2f3efb8ae7a558a2a36b629080cc6b461e49 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:49:24 -0300 Subject: [PATCH 26/44] Fix caching bug in get_code_tree method --- src/backend/langflow/interface/custom/component.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/custom/component.py b/src/backend/langflow/interface/custom/component.py index 0f503accb..98ccf8db3 100644 --- a/src/backend/langflow/interface/custom/component.py +++ b/src/backend/langflow/interface/custom/component.py @@ -4,7 +4,6 @@ from typing import Any, ClassVar, Optional from cachetools import TTLCache, cachedmethod from fastapi import HTTPException - from langflow.interface.custom.code_parser import CodeParser from langflow.utils import validate @@ -30,7 +29,7 @@ class Component: for key, value in data.items(): setattr(self, key, value) - @cachedmethod(operator.attrgetter("cache")) + @cachedmethod(cache=operator.attrgetter("cache")) def get_code_tree(self, code: str): parser = CodeParser(code) return parser.parse_code() From 843eeac7ee405b646e7d1eeba7578ca8b0bf8d7a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:49:46 -0300 Subject: [PATCH 27/44] Fix caching bug in CodeParser --- .../langflow/interface/custom/code_parser.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/custom/code_parser.py b/src/backend/langflow/interface/custom/code_parser.py index 3f4bf35e4..696e5e50d 100644 --- a/src/backend/langflow/interface/custom/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser.py @@ -4,8 +4,9 @@ import operator import traceback from typing import Any, Dict, List, Type, Union -from cachetools import TTLCache, cachedmethod +from cachetools import TTLCache, cachedmethod, keys from fastapi import HTTPException + from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails @@ -19,6 +20,13 @@ def get_data_type(): return Data +def imports_key(*args, **kwargs): + imports = kwargs.pop("imports") + key = keys.methodkey(*args, **kwargs) + key += tuple(imports) + return key + + class CodeParser: """ A parser for Python source code, extracting code details. @@ -105,8 +113,7 @@ class CodeParser: return arg_dict @cachedmethod(operator.attrgetter("cache")) - @staticmethod - def construct_eval_env(return_type_str: str, imports) -> dict: + def construct_eval_env(self, return_type_str: str, imports) -> dict: """ Constructs an evaluation environment with the necessary imports for the return type, taking into account module aliases. @@ -127,7 +134,7 @@ class CodeParser: exec(f"import {module} as {alias if alias else module}", eval_env) return eval_env - @cachedmethod(operator.attrgetter("cache")) + @cachedmethod(cache=operator.attrgetter("cache")) def parse_callable_details(self, node: ast.FunctionDef) -> Dict[str, Any]: """ Extracts details from a single function or method node. @@ -135,7 +142,7 @@ class CodeParser: return_type = None if node.returns: return_type_str = ast.unparse(node.returns) - eval_env = self.construct_eval_env(return_type_str, self.data["imports"]) + eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) try: return_type = eval(return_type_str, eval_env) From 2b882bd2768cc757ef9172ada51f51f33b4a25a1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:49:54 -0300 Subject: [PATCH 28/44] Add menu path to component template --- src/backend/langflow/interface/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index dde82370d..0b80aa9f5 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -374,7 +374,7 @@ def build_valid_menu(valid_components): for menu_item in valid_components["menu"]: menu_name = menu_item["name"] valid_menu[menu_name] = {} - + menu_path = menu_item["path"] for component in menu_item["components"]: logger.debug(f"Building component: {component.get('name'), component.get('output_types')}") try: @@ -383,10 +383,12 @@ def build_valid_menu(valid_components): component_output_types = component["output_types"] component_extractor = CustomComponent(code=component_code) - component_extractor.is_check_valid() + component_extractor.validate() component_template = build_langchain_template_custom_component(component_extractor) component_template["output_types"] = component_output_types + full_path = f"{menu_path}/{component.get('file')}" + component_template["full_path"] = full_path if len(component_output_types) == 1: component_name = component_output_types[0] else: From 943ef39050687b5cfe4af25615a228e28423aff1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 11:50:31 -0300 Subject: [PATCH 29/44] Refactor API endpoints and validate custom component code --- src/backend/langflow/api/v1/endpoints.py | 37 ++++++++++-------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index adecd5dbf..17c7c17a0 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -1,33 +1,29 @@ from http import HTTPStatus from typing import Annotated, Optional, Union -from langflow.services.auth.utils import api_key_security, get_current_active_user - -from langflow.services.cache.utils import save_uploaded_file -from langflow.services.database.models.flow import Flow -from langflow.processing.process import process_graph_cached, process_tweaks -from langflow.services.database.models.user.user import User -from langflow.services.deps import ( - get_session_service, - get_settings_service, - get_task_service, -) -from loguru import logger -from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status import sqlalchemy as sa -from langflow.interface.custom.custom_component import CustomComponent - +from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status +from loguru import logger from langflow.api.v1.schemas import ( + CustomComponentCode, ProcessResponse, TaskResponse, TaskStatusResponse, UploadFileResponse, - CustomComponentCode, ) - - -from langflow.services.deps import get_session +from langflow.interface.custom.custom_component import CustomComponent +from langflow.processing.process import process_graph_cached, process_tweaks +from langflow.services.auth.utils import api_key_security, get_current_active_user +from langflow.services.cache.utils import save_uploaded_file +from langflow.services.database.models.flow import Flow +from langflow.services.database.models.user.user import User +from langflow.services.deps import ( + get_session, + get_session_service, + get_settings_service, + get_task_service, +) try: from langflow.worker import process_graph_cached_task @@ -39,7 +35,6 @@ except ImportError: from sqlmodel import Session - from langflow.services.task.service import TaskService # build router @@ -217,6 +212,6 @@ async def custom_component( ) extractor = CustomComponent(code=raw_code.code) - extractor.is_check_valid() + extractor.validate() return build_langchain_template_custom_component(extractor, user_id=user.id) From 19f998a3263817e79fffd95f5c397c34ee965009 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 12:16:28 -0300 Subject: [PATCH 30/44] Add custom component reload endpoint --- src/backend/langflow/api/v1/endpoints.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 17c7c17a0..2d2ebd952 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -13,6 +13,7 @@ from langflow.api.v1.schemas import ( UploadFileResponse, ) from langflow.interface.custom.custom_component import CustomComponent +from langflow.interface.custom.directory_reader import DirectoryReader from langflow.processing.process import process_graph_cached, process_tweaks from langflow.services.auth.utils import api_key_security, get_current_active_user from langflow.services.cache.utils import save_uploaded_file @@ -215,3 +216,22 @@ async def custom_component( extractor.validate() return build_langchain_template_custom_component(extractor, user_id=user.id) + + +@router.post("/custom_component/reload", status_code=HTTPStatus.OK) +async def reload_custom_component(path: str): + from langflow.interface.types import ( + build_langchain_template_custom_component, + ) + + try: + reader = DirectoryReader("") + valid, content = reader.process_file(path) + if not valid: + raise ValueError(content) + + extractor = CustomComponent(code=content) + extractor.validate() + return build_langchain_template_custom_component(extractor, user_id=user.id) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) From 4e5ec2a4de0e53c7f12cfb8d5bc44c744ab6c30c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 12:16:39 -0300 Subject: [PATCH 31/44] Add caching for langchain types dictionary --- src/backend/langflow/interface/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 0b80aa9f5..94c924033 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -6,6 +6,7 @@ import warnings from typing import Any, List, Optional, Union from uuid import UUID +from cachetools import LRUCache, cached from fastapi import HTTPException from loguru import logger @@ -49,6 +50,7 @@ def get_type_list(): return all_types +@cached(LRUCache(maxsize=1)) def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union """Build a dictionary of all langchain types""" all_types = {} From 4580d428e51d027a9b8c2231a0a095cc3d10676a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 15:38:24 -0300 Subject: [PATCH 32/44] Fix import statement in constants.py --- src/backend/langflow/field_typing/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/field_typing/constants.py b/src/backend/langflow/field_typing/constants.py index 646af0bac..76ef3aa5b 100644 --- a/src/backend/langflow/field_typing/constants.py +++ b/src/backend/langflow/field_typing/constants.py @@ -3,11 +3,12 @@ from typing import Callable, Dict, Union from langchain.agents.agent import AgentExecutor from langchain.chains.base import Chain from langchain.document_loaders.base import BaseLoader -from langchain.llms.base import BaseLanguageModel, BaseLLM +from langchain.llms.base import BaseLLM from langchain.memory.chat_memory import BaseChatMemory from langchain.prompts import BasePromptTemplate, ChatPromptTemplate, PromptTemplate from langchain.schema import BaseOutputParser, BaseRetriever, Document from langchain.schema.embeddings import Embeddings +from langchain.schema.language_model import BaseLanguageModel from langchain.schema.memory import BaseMemory from langchain.text_splitter import TextSplitter from langchain.tools import Tool From 29cef91e6ff214d6a415e58bebbcd4dfb12b58d8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 15:38:40 -0300 Subject: [PATCH 33/44] Add AgentInitializerComponent to initialize agents --- .../components/agents/AgentInitializer.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/backend/langflow/components/agents/AgentInitializer.py diff --git a/src/backend/langflow/components/agents/AgentInitializer.py b/src/backend/langflow/components/agents/AgentInitializer.py new file mode 100644 index 000000000..b0268892b --- /dev/null +++ b/src/backend/langflow/components/agents/AgentInitializer.py @@ -0,0 +1,32 @@ +from typing import List + +from langchain.agents import AgentExecutor, AgentType, initialize_agent, types +from langflow import CustomComponent +from langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool + + +class AgentInitializerComponent(CustomComponent): + def build_config(self): + agents = list(types.AGENT_TO_CLASS.keys()) + # field_type and required are optional + return { + "agent": {"options": agents, "value": agents[0], "display_name": "Agent Type"}, + "max_iterations": {"display_name": "Max Iterations", "value": 10}, + "memory": {"display_name": "Memory"}, + "tools": {"display_name": "Tools"}, + "llm": {"display_name": "Language Model"}, + } + + def build( + self, agent: str, llm: BaseLanguageModel, memory: BaseChatMemory, tools: List[Tool], max_iterations: int + ) -> AgentExecutor: + agent = AgentType(agent) + return initialize_agent( + tools=tools, + llm=llm, + agent=agent, + memory=memory, + return_intermediate_steps=True, + handle_parsing_errors=True, + max_iterations=max_iterations, + ) From 8b1895ccf389ba292077460d63b54964d013f85c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 15:55:27 -0300 Subject: [PATCH 34/44] Update AgentInitializerComponent documentation link --- src/backend/langflow/components/agents/AgentInitializer.py | 4 ++++ src/backend/langflow/config.yaml | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/components/agents/AgentInitializer.py b/src/backend/langflow/components/agents/AgentInitializer.py index b0268892b..376717610 100644 --- a/src/backend/langflow/components/agents/AgentInitializer.py +++ b/src/backend/langflow/components/agents/AgentInitializer.py @@ -6,6 +6,10 @@ from langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool class AgentInitializerComponent(CustomComponent): + display_name: str = "Agent Initializer" + description: str = f"Initialize an agent of type: {types.AGENT_TO_CLASS.keys()}" + documentation: str = "https://python.langchain.com/docs/modules/agents/agent_types/" + def build_config(self): agents = list(types.AGENT_TO_CLASS.keys()) # field_type and required are optional diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index d234ce8ea..39bdc1ed6 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -5,8 +5,6 @@ agents: documentation: "https://python.langchain.com/docs/modules/agents/toolkits/openapi" CSVAgent: documentation: "https://python.langchain.com/docs/modules/agents/toolkits/csv" - AgentInitializer: - documentation: "https://python.langchain.com/docs/modules/agents/agent_types/" VectorStoreAgent: documentation: "" VectorStoreRouterAgent: From cdb7c1a695cc6e55121a37d75149723f51daa083 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 15:55:34 -0300 Subject: [PATCH 35/44] Update mypy version to 1.7.1 --- poetry.lock | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/poetry.lock b/poetry.lock index e4bce3b3e..9c8eec516 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4169,38 +4169,38 @@ dill = ">=0.3.7" [[package]] name = "mypy" -version = "1.7.0" +version = "1.7.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"}, - {file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"}, - {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, - {file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"}, - {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, - {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, - {file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"}, - {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, - {file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"}, - {file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, - {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, - {file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"}, - {file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"}, - {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, - {file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"}, - {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, - {file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"}, - {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, - {file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"}, - {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, - {file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"}, - {file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"}, - {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, - {file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"}, - {file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, ] [package.dependencies] From 9db5f90acff2b287b2e2be32829f76a428148766 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 23 Nov 2023 16:38:54 -0300 Subject: [PATCH 36/44] fix(extraSidebarComponent): sort the keys in dataFilter object with a custom sorting function to prioritize "saved_components" key --- .../components/extraSidebarComponent/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index bdc8abfb5..803a239f4 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -305,7 +305,15 @@ export default function ExtraSidebar(): JSX.Element {
{Object.keys(dataFilter) - .sort() + .sort((a, b) => { + if (a.toLowerCase() === "saved_components") { + return -1; + } else if (b.toLowerCase() === "saved_components") { + return 1; + } else { + return a.localeCompare(b); + } + }) .map((SBSectionName: keyof APIObjectType, index) => Object.keys(dataFilter[SBSectionName]).length > 0 ? ( Date: Thu, 23 Nov 2023 16:55:15 -0300 Subject: [PATCH 37/44] fix(sideBarDraggableComponent): fix indentation and closing tag placement for IconComponent in SidebarDraggableComponent --- .../extraSidebarComponent/sideBarDraggableComponent/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx index c30fe56f5..7784f7cab 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx @@ -93,11 +93,11 @@ export default function SidebarDraggableComponent({ > {display_name}
- - + From 61ad8809903a5c6793a66d7551445c72b9d95206 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 23 Nov 2023 17:22:43 -0300 Subject: [PATCH 38/44] fix(shareModal/index.tsx): add conditional check for hasApiKey before calling handleGetTags and handleGetNames to prevent unnecessary API calls when there is no API key available --- src/frontend/src/modals/shareModal/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/modals/shareModal/index.tsx b/src/frontend/src/modals/shareModal/index.tsx index 71ebcba9c..aee242ec2 100644 --- a/src/frontend/src/modals/shareModal/index.tsx +++ b/src/frontend/src/modals/shareModal/index.tsx @@ -15,6 +15,7 @@ import { FlowType } from "../../types/flow"; import { removeApiKeys } from "../../utils/reactflowUtils"; import { getTagsIds } from "../../utils/storeUtils"; import BaseModal from "../baseModal"; +import { StoreContext } from "../../contexts/storeContext"; export default function ShareModal({ component, @@ -30,6 +31,7 @@ export default function ShareModal({ setOpen?: (open: boolean) => void; }): JSX.Element { const { version, addFlow } = useContext(FlowsContext); + const {hasApiKey} = useContext(StoreContext) const { setSuccessData, setErrorData } = useContext(alertContext); const [checked, setChecked] = useState(true); const [name, setName] = useState(component?.name ?? ""); @@ -46,10 +48,12 @@ export default function ShareModal({ useEffect(() => { if (open || internalOpen) { - handleGetTags(); - handleGetNames(); + if(hasApiKey){ + handleGetTags(); + handleGetNames(); + } } - }, [open, internalOpen]); + }, [open, internalOpen,hasApiKey]); function handleGetTags() { setLoadingTags(true); From 595006cb77146dc2de645b35fa936d667bfa0000 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 17:22:41 -0300 Subject: [PATCH 39/44] Update AgentInitializerComponent to initialize a Langchain Agent --- src/backend/langflow/components/agents/AgentInitializer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/components/agents/AgentInitializer.py b/src/backend/langflow/components/agents/AgentInitializer.py index 376717610..2e8a9de2f 100644 --- a/src/backend/langflow/components/agents/AgentInitializer.py +++ b/src/backend/langflow/components/agents/AgentInitializer.py @@ -1,13 +1,14 @@ -from typing import List +from typing import Callable, List, Union from langchain.agents import AgentExecutor, AgentType, initialize_agent, types + from langflow import CustomComponent from langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool class AgentInitializerComponent(CustomComponent): display_name: str = "Agent Initializer" - description: str = f"Initialize an agent of type: {types.AGENT_TO_CLASS.keys()}" + description: str = "Initialize a Langchain Agent." documentation: str = "https://python.langchain.com/docs/modules/agents/agent_types/" def build_config(self): @@ -23,7 +24,7 @@ class AgentInitializerComponent(CustomComponent): def build( self, agent: str, llm: BaseLanguageModel, memory: BaseChatMemory, tools: List[Tool], max_iterations: int - ) -> AgentExecutor: + ) -> Union[AgentExecutor, Callable]: agent = AgentType(agent) return initialize_agent( tools=tools, From 5dc419e25ffc11d92914d183c87d60e4441fe936 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 23 Nov 2023 17:22:51 -0300 Subject: [PATCH 40/44] Fix APIKeyError status code --- src/backend/langflow/services/store/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/store/exceptions.py b/src/backend/langflow/services/store/exceptions.py index 058b21652..df86d59bc 100644 --- a/src/backend/langflow/services/store/exceptions.py +++ b/src/backend/langflow/services/store/exceptions.py @@ -17,7 +17,7 @@ class ForbiddenError(CustomException): class APIKeyError(CustomException): def __init__(self, detail="API key error"): - super().__init__(detail, 401) + super().__init__(detail, 400) #! Should be 401 class FilterError(CustomException): From bd74a16e477ec173f105f2fbd790864eb3262273 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 23 Nov 2023 17:39:28 -0300 Subject: [PATCH 41/44] fix(extraSidebarComponent): change variable name from `hasApiKey` to `hasApiKey` for consistency fix(nodeToolbarComponent): add condition to show Share option only if `hasApiKey` is true --- .../FlowPage/components/extraSidebarComponent/index.tsx | 8 ++++++-- .../FlowPage/components/nodeToolbarComponent/index.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 803a239f4..09286514f 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -23,12 +23,14 @@ import { } from "../../../../utils/utils"; import DisclosureComponent from "../DisclosureComponent"; import SidebarDraggableComponent from "./sideBarDraggableComponent"; +import { StoreContext } from "../../../../contexts/storeContext"; export default function ExtraSidebar(): JSX.Element { const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } = useContext(typesContext); const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } = useContext(FlowsContext); + const {hasApiKey} = useContext(StoreContext) const { setErrorData } = useContext(alertContext); const [dataFilter, setFilterData] = useState(data); const [search, setSearch] = useState(""); @@ -48,6 +50,8 @@ export default function ExtraSidebar(): JSX.Element { event.dataTransfer.setData("nodedata", JSON.stringify(data)); } + + // Handle showing components after use search input function handleSearchInput(e: string) { if (e === "") { @@ -195,9 +199,9 @@ export default function ExtraSidebar(): JSX.Element { () => ( -
+
+
), diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 1fbc07095..c78861133 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -21,6 +21,7 @@ import { updateFlowPosition, } from "../../../../utils/reactflowUtils"; import { classNames } from "../../../../utils/utils"; +import { StoreContext } from "../../../../contexts/storeContext"; export default function NodeToolbarComponent({ data, @@ -50,6 +51,7 @@ export default function NodeToolbarComponent({ ); const updateNodeInternals = useUpdateNodeInternals(); const { getNodeId } = useContext(FlowsContext); + const {hasApiKey} = useContext(StoreContext) function canMinimize() { let countHandles: number = 0; @@ -87,7 +89,7 @@ export default function NodeToolbarComponent({ downloadNode(createFlowComponent(cloneDeep(data), version)); break; case "Share": - setShowconfirmShare(true); + if(hasApiKey) setShowconfirmShare(true); break; case "SaveAll": saveComponent(cloneDeep(data)); @@ -211,7 +213,7 @@ export default function NodeToolbarComponent({ Save{" "}
{" "} - + {hasApiKey &&
{" "} Share{" "}
{" "} -
+
}
Date: Thu, 23 Nov 2023 17:43:22 -0300 Subject: [PATCH 42/44] Add metadata and comp_count variables --- src/backend/langflow/services/store/service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/services/store/service.py b/src/backend/langflow/services/store/service.py index 92cd67447..f8c322355 100644 --- a/src/backend/langflow/services/store/service.py +++ b/src/backend/langflow/services/store/service.py @@ -445,6 +445,8 @@ class StoreService(Service): result: List[ListComponentResponse] = [] authorized = False + metadata = {} + comp_count = 0 try: result, metadata = await self.query_components( api_key=store_api_key, From 0e078efee0e458ed2bac906f7d3271197fd82132 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 23 Nov 2023 18:28:07 -0300 Subject: [PATCH 43/44] fix(shareModal): add support for disabled prop to prevent opening the modal when disabled fix(extraSidebarComponent): pass disabled prop to ShareModal based on hasApiKey value to disable sharing when no API key is available --- src/frontend/src/modals/shareModal/index.tsx | 4 +++- .../pages/FlowPage/components/extraSidebarComponent/index.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/modals/shareModal/index.tsx b/src/frontend/src/modals/shareModal/index.tsx index aee242ec2..c68af6f1c 100644 --- a/src/frontend/src/modals/shareModal/index.tsx +++ b/src/frontend/src/modals/shareModal/index.tsx @@ -23,12 +23,14 @@ export default function ShareModal({ children, open, setOpen, + disabled }: { children?: ReactNode; is_component: boolean; component: FlowType; open?: boolean; setOpen?: (open: boolean) => void; + disabled?: boolean; }): JSX.Element { const { version, addFlow } = useContext(FlowsContext); const {hasApiKey} = useContext(StoreContext) @@ -117,7 +119,7 @@ export default function ShareModal({ return ( {children ? children : <>} diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 09286514f..91dd161c1 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -184,7 +184,7 @@ export default function ExtraSidebar(): JSX.Element { const ModalMemo = useMemo( () => ( - +
@@ -192,7 +192,7 @@ export default function ExtraSidebar(): JSX.Element { ), - [] + [hasApiKey] ); const ExportMemo = useMemo( From 8e47e7c9d7f061d87a2d9d39822d3a565efe94a8 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 23 Nov 2023 18:41:13 -0300 Subject: [PATCH 44/44] fix(PageComponent): change position calculation to use screenToFlowPosition instead of project to fix incorrect node creation position --- .../src/pages/FlowPage/components/PageComponent/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 277740274..71c623abb 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -319,9 +319,9 @@ export default function Page({ ); // Calculate the position where the node should be created - const position = reactFlowInstance!.project({ - x: event.clientX - reactflowBounds!.left, - y: event.clientY - reactflowBounds!.top, + const position = reactFlowInstance!.screenToFlowPosition({ + x: event.clientX, + y: event.clientY }); // Generate a unique node ID