diff --git a/poetry.lock b/poetry.lock index 988618e1b..9c8eec516 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] @@ -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] @@ -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/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index adecd5dbf..2d2ebd952 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -1,33 +1,30 @@ 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.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 +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 +36,6 @@ except ImportError: from sqlmodel import Session - from langflow.services.task.service import TaskService # build router @@ -217,6 +213,25 @@ 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) + + +@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)) diff --git a/src/backend/langflow/components/agents/AgentInitializer.py b/src/backend/langflow/components/agents/AgentInitializer.py new file mode 100644 index 000000000..2e8a9de2f --- /dev/null +++ b/src/backend/langflow/components/agents/AgentInitializer.py @@ -0,0 +1,37 @@ +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 = "Initialize a Langchain Agent." + 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 + 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 + ) -> Union[AgentExecutor, Callable]: + 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, + ) diff --git a/src/backend/langflow/components/agents/OpenAIConversationalAgent.py b/src/backend/langflow/components/agents/OpenAIConversationalAgent.py index eb53a89c0..499775747 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 typing import List, Optional 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): @@ -27,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"}, @@ -43,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, @@ -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" 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 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: 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 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/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, diff --git a/src/backend/langflow/interface/custom/code_parser.py b/src/backend/langflow/interface/custom/code_parser.py index d1b33a4e2..696e5e50d 100644 --- a/src/backend/langflow/interface/custom/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser.py @@ -1,9 +1,12 @@ import ast import inspect +import operator import traceback from typing import Any, Dict, List, Type, Union +from cachetools import TTLCache, cachedmethod, keys from fastapi import HTTPException + from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails @@ -17,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. @@ -26,6 +36,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.") @@ -101,13 +112,14 @@ 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")) + 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. """ - eval_env = {} - for import_entry in self.data["imports"]: + eval_env: dict = {} + for import_entry in imports: if isinstance(import_entry, tuple): # from module import name module, name = import_entry if name in return_type_str: @@ -122,6 +134,7 @@ class CodeParser: exec(f"import {module} as {alias if alias else module}", eval_env) return eval_env + @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. @@ -129,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) + eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) try: return_type = eval(return_type_str, eval_env) @@ -266,7 +279,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 diff --git a/src/backend/langflow/interface/custom/component.py b/src/backend/langflow/interface/custom/component.py index 5a476ff58..98ccf8db3 100644 --- a/src/backend/langflow/interface/custom/component.py +++ b/src/backend/langflow/interface/custom/component.py @@ -1,8 +1,9 @@ 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 from langflow.utils import validate @@ -24,9 +25,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(cache=operator.attrgetter("cache")) def get_code_tree(self, code: str): parser = CodeParser(code) return parser.parse_code() 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, diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index 055766bb0..032ed1476 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 @@ -30,6 +32,7 @@ class CustomComponent(Component): status: Optional[str] = None def __init__(self, **data): + self.cache = TTLCache(maxsize=1024, ttl=60) super().__init__(**data) @property @@ -71,33 +74,19 @@ 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): 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: @@ -111,13 +100,13 @@ 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 - @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) @@ -135,7 +124,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[Any]: + build_method = self.get_build_method() + if not build_method: + return build_method return_type = build_method["return_type"] if not return_type: return [] @@ -156,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: @@ -173,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 @@ -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: @@ -213,7 +212,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, @@ -233,7 +232,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/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]: diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index 6b28d792e..f3276d952 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -3,15 +3,15 @@ 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 +180,7 @@ def get_function(code): return validate.create_function(code, function_name) -def get_function_custom(code): +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) diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 9f737e133..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 @@ -11,7 +12,7 @@ from langchain.document_loaders.base import BaseLoader from langchain.schema import Document from langchain.vectorstores.base import VectorStore 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 @@ -35,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) @@ -47,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): @@ -74,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": @@ -108,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" = 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) + + # 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/interface/types.py b/src/backend/langflow/interface/types.py index f578bb0bd..94c924033 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -6,7 +6,10 @@ 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 + 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 +19,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 +35,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 @@ -48,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 = {} @@ -202,9 +205,14 @@ 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) + 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={ @@ -228,7 +236,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 @@ -368,7 +376,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: @@ -377,10 +385,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: 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() 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/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 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/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): 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, 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"], + }, +] 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): 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/src/frontend/src/modals/shareModal/index.tsx b/src/frontend/src/modals/shareModal/index.tsx index 71ebcba9c..c68af6f1c 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, @@ -22,14 +23,17 @@ 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) const { setSuccessData, setErrorData } = useContext(alertContext); const [checked, setChecked] = useState(true); const [name, setName] = useState(component?.name ?? ""); @@ -46,10 +50,12 @@ export default function ShareModal({ useEffect(() => { if (open || internalOpen) { - handleGetTags(); - handleGetNames(); + if(hasApiKey){ + handleGetTags(); + handleGetNames(); + } } - }, [open, internalOpen]); + }, [open, internalOpen,hasApiKey]); function handleGetTags() { setLoadingTags(true); @@ -113,7 +119,7 @@ export default function ShareModal({ return ( {children ? children : <>} diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 872d3d37c..4104ca1c1 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -322,9 +322,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 diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index bdc8abfb5..91dd161c1 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 === "") { @@ -180,7 +184,7 @@ export default function ExtraSidebar(): JSX.Element { const ModalMemo = useMemo( () => ( - +
@@ -188,16 +192,16 @@ export default function ExtraSidebar(): JSX.Element { ), - [] + [hasApiKey] ); const ExportMemo = useMemo( () => ( -
+
+
), @@ -305,7 +309,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 ? ( {display_name}
- - + 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{" "}
{" "} -
+
}