diff --git a/.github/actions/poetry_caching/action.yml b/.github/actions/poetry_caching/action.yml new file mode 100644 index 000000000..fb76b1723 --- /dev/null +++ b/.github/actions/poetry_caching/action.yml @@ -0,0 +1,94 @@ +# An action for setting up poetry install with caching. +# Using a custom action since the default action does not +# take poetry install groups into account. +# Action code from: +# https://github.com/actions/setup-python/issues/505#issuecomment-1273013236 +# Copy of https://github.com/langchain-ai/langchain/blob/2f8dd1a1619f25daa4737df4d378b1acd6ff83c4/.github/actions/poetry_setup/action.yml +name: poetry-install-with-caching +description: Poetry install with support for caching of dependency groups. + +inputs: + python-version: + description: Python version, supporting MAJOR.MINOR only + required: true + + poetry-version: + description: Poetry version + required: true + + cache-key: + description: Cache key to use for manual handling of caching + required: true + + working-directory: + description: Directory whose poetry.lock file should be cached + required: true + +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + name: Setup python ${{ inputs.python-version }} + id: setup-python + with: + python-version: ${{ inputs.python-version }} + + - uses: actions/cache@v4 + id: cache-bin-poetry + name: Cache Poetry binary - Python ${{ inputs.python-version }} + env: + SEGMENT_DOWNLOAD_TIMEOUT_MIN: "1" + with: + path: | + /opt/pipx/venvs/poetry + # This step caches the poetry installation, so make sure it's keyed on the poetry version as well. + key: bin-poetry-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-${{ inputs.poetry-version }} + + - name: Refresh shell hashtable and fixup softlinks + if: steps.cache-bin-poetry.outputs.cache-hit == 'true' + shell: bash + env: + POETRY_VERSION: ${{ inputs.poetry-version }} + PYTHON_VERSION: ${{ inputs.python-version }} + run: | + set -eux + + # Refresh the shell hashtable, to ensure correct `which` output. + hash -r + + # `actions/cache@v3` doesn't always seem able to correctly unpack softlinks. + # Delete and recreate the softlinks pipx expects to have. + rm /opt/pipx/venvs/poetry/bin/python + cd /opt/pipx/venvs/poetry/bin + ln -s "$(which "python$PYTHON_VERSION")" python + chmod +x python + cd /opt/pipx_bin/ + ln -s /opt/pipx/venvs/poetry/bin/poetry poetry + chmod +x poetry + + # Ensure everything got set up correctly. + /opt/pipx/venvs/poetry/bin/python --version + /opt/pipx_bin/poetry --version + + - name: Install poetry + if: steps.cache-bin-poetry.outputs.cache-hit != 'true' + shell: bash + env: + POETRY_VERSION: ${{ inputs.poetry-version }} + PYTHON_VERSION: ${{ inputs.python-version }} + # Install poetry using the python version installed by setup-python step. + run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose + + - name: Restore pip and poetry cached dependencies + uses: actions/cache@v4 + env: + SEGMENT_DOWNLOAD_TIMEOUT_MIN: "4" + WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }} + with: + path: | + ~/.cache/pip + ~/.cache/pypoetry/virtualenvs + ~/.cache/pypoetry/cache + ~/.cache/pypoetry/artifacts + ${{ env.WORKDIR }}/.venv + key: py-deps-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-poetry-${{ inputs.poetry-version }}-${{ inputs.cache-key }}-${{ hashFiles(format('{0}/**/poetry.lock', env.WORKDIR)) }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c78ba4c71..e1b427322 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,20 +26,24 @@ jobs: - "3.11" steps: - uses: actions/checkout@v4 - - name: Install poetry - run: | - pipx install poetry==$POETRY_VERSION - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - id: setup-python + - name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_caching" with: python-version: ${{ matrix.python-version }} - cache: poetry + poetry-version: ${{ env.POETRY_VERSION }} + cache-key: ${{ runner.os }}-poetry-${{ env.POETRY_VERSION }}-${{ hashFiles('**/poetry.lock') }} - name: Install Python dependencies run: | poetry env use ${{ matrix.python-version }} poetry install - if: ${{ steps.setup-python.outputs.cache-hit != 'true' }} - - name: Analysing the code with our lint + - name: Get .mypy_cache to speed up mypy + uses: actions/cache@v4 + env: + SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2" + with: + path: | + ./.mypy_cache + key: ${{ runner.os }}-mypy-${{ hashFiles('**/pyproject.toml') }} + - name: Lint check run: | make lint diff --git a/.github/workflows/python_test.yml b/.github/workflows/python_test.yml index a04a208f3..8a6615f8f 100644 --- a/.github/workflows/python_test.yml +++ b/.github/workflows/python_test.yml @@ -29,19 +29,16 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry==$POETRY_VERSION - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - id: setup-python + - name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }} + uses: "./.github/actions/poetry_caching" with: python-version: ${{ matrix.python-version }} - cache: "poetry" + poetry-version: ${{ env.POETRY_VERSION }} + cache-key: ${{ runner.os }}-poetry-${{ env.POETRY_VERSION }}-${{ hashFiles('**/poetry.lock') }} - name: Install Python dependencies run: | poetry env use ${{ matrix.python-version }} poetry install - if: ${{ steps.setup-python.outputs.cache-hit != 'true' }} - name: Run unit tests run: | make tests diff --git a/.gitignore b/.gitignore index 0d74c2208..03eed1556 100644 --- a/.gitignore +++ b/.gitignore @@ -264,4 +264,5 @@ scratchpad* chroma*/* stuff/* src/frontend/playwright-report/index.html -*.bak \ No newline at end of file +*.bak +prof/* \ No newline at end of file diff --git a/Makefile b/Makefile index 965086f99..6d279e62f 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,14 @@ env ?= .env open_browser ?= true path = src/backend/base/langflow/frontend +codespell: + @poetry install --with spelling + poetry run codespell --toml pyproject.toml + +fix_codespell: + @poetry install --with spelling + poetry run codespell --toml pyproject.toml --write + setup_poetry: pipx install poetry @@ -39,20 +47,16 @@ coverage: # allow passing arguments to pytest tests: - @make install_backend - poetry run pytest tests --instafail $(args) # Use like: format: - poetry run ruff . --fix + poetry run ruff check . --fix poetry run ruff format . cd src/frontend && npm run format lint: - make install_backend poetry run mypy --namespace-packages -p "langflow" - poetry run ruff . --fix install_frontend: cd src/frontend && npm install @@ -129,12 +133,12 @@ frontendc: make run_frontend install_backend: - @echo 'Setting up the environment' - @make setup_env @echo 'Installing backend dependencies' - @poetry install --extras deploy + @poetry install backend: + @echo 'Setting up the environment' + @make setup_env make install_backend @-kill -9 `lsof -t -i:7860` ifdef login diff --git a/base.Dockerfile b/base.Dockerfile index 936943cd1..3946af99d 100644 --- a/base.Dockerfile +++ b/base.Dockerfile @@ -57,7 +57,7 @@ RUN apt-get update \ # install poetry - respects $POETRY_VERSION & $POETRY_HOME # The --mount will mount the buildx cache directory to where -# Poetry and Pip store their cache so that they can re-use it +# Poetry and Pip store their cache so that they can reuse it RUN --mount=type=cache,target=/root/.cache \ curl -sSL https://install.python-poetry.org | python3 - diff --git a/deploy/base.Dockerfile b/deploy/base.Dockerfile index 58fae3dab..84b448e2d 100644 --- a/deploy/base.Dockerfile +++ b/deploy/base.Dockerfile @@ -56,7 +56,7 @@ RUN apt-get update \ # install poetry - respects $POETRY_VERSION & $POETRY_HOME # The --mount will mount the buildx cache directory to where -# Poetry and Pip store their cache so that they can re-use it +# Poetry and Pip store their cache so that they can reuse it RUN --mount=type=cache,target=/root/.cache \ curl -sSL https://install.python-poetry.org | python3 - diff --git a/poetry.lock b/poetry.lock index c992bc5e7..f56329f9a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1130,6 +1130,23 @@ prompt-toolkit = ">=3.0.36" [package.extras] testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] +[[package]] +name = "codespell" +version = "2.2.6" +description = "Codespell" +optional = false +python-versions = ">=3.8" +files = [ + {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"}, + {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"}, +] + +[package.extras] +dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] +types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] + [[package]] name = "cohere" version = "5.2.5" @@ -2659,6 +2676,17 @@ files = [ httpx = ">=0.23,<0.28" pydantic = ">=1.10,<3" +[[package]] +name = "gprof2dot" +version = "2022.7.29" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=2.7" +files = [ + {file = "gprof2dot-2022.7.29-py2.py3-none-any.whl", hash = "sha256:f165b3851d3c52ee4915eb1bd6cca571e5759823c2cd0f71a79bda93c2dc85d6"}, + {file = "gprof2dot-2022.7.29.tar.gz", hash = "sha256:45b4d298bd36608fccf9511c3fd88a773f7a1abc04d6cd39445b11ba43133ec5"}, +] + [[package]] name = "greenlet" version = "3.0.3" @@ -3930,13 +3958,13 @@ url = "src/backend/base" [[package]] name = "langfuse" -version = "2.23.2" +version = "2.24.0" description = "A client library for accessing langfuse" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langfuse-2.23.2-py3-none-any.whl", hash = "sha256:591811d36fe42b26451615686ea91c04d96afd247ac023818b1ca72861e537a6"}, - {file = "langfuse-2.23.2.tar.gz", hash = "sha256:11df6c6ff6ca5f6a079836f2530d5f7dcbe11d7058ae16efcafbf8275b0e14b6"}, + {file = "langfuse-2.24.0-py3-none-any.whl", hash = "sha256:356e8260f093470f09ed8aabad769bae6e9eb1e8a8682f53f1d135c69e07d0d9"}, + {file = "langfuse-2.24.0.tar.gz", hash = "sha256:c2f9f4c66578ea70da20c2f1bf6ac468b349471b9f2644f95add57aa00817827"}, ] [package.dependencies] @@ -3986,13 +4014,13 @@ regex = ["regex"] [[package]] name = "litellm" -version = "1.34.40" +version = "1.34.41" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" files = [ - {file = "litellm-1.34.40-py3-none-any.whl", hash = "sha256:ad6d321eec9819d1697223f5c528ca5c5f2476944bb2afc691cf8517804777e7"}, - {file = "litellm-1.34.40.tar.gz", hash = "sha256:99faea82cc8bfa1ca6e3063adebfd9e65ecc60b00f4de9a47bda0559d6c9f637"}, + {file = "litellm-1.34.41-py3-none-any.whl", hash = "sha256:ec6b1fb7f27178ad36f3be64e2f0fb73a637c0d7c128d5aa1d845414cc21be66"}, + {file = "litellm-1.34.41.tar.gz", hash = "sha256:e8ad1a0b9cf4abea0471394db8c95f479e94e271bc1ce3d98204e1a6ed917e93"}, ] [package.dependencies] @@ -7185,6 +7213,25 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-profiling" +version = "1.7.0" +description = "Profiling plugin for py.test" +optional = false +python-versions = "*" +files = [ + {file = "pytest-profiling-1.7.0.tar.gz", hash = "sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29"}, + {file = "pytest_profiling-1.7.0-py2.py3-none-any.whl", hash = "sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019"}, +] + +[package.dependencies] +gprof2dot = "*" +pytest = "*" +six = "*" + +[package.extras] +tests = ["pytest-virtualenv"] + [[package]] name = "pytest-sugar" version = "1.0.0" @@ -7999,28 +8046,28 @@ msg-parse = ["extract-msg (>=0.27)"] [[package]] name = "ruff" -version = "0.2.2" +version = "0.3.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, + {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"}, + {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"}, + {file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"}, + {file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"}, + {file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"}, + {file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"}, ] [[package]] @@ -10256,11 +10303,10 @@ test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [extras] -all = [] deploy = ["celery", "flower", "redis"] local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "0d658da1ee75640f74d7335685760091df2a452e84eef81dcda2590dd4044e92" +content-hash = "9dd152b30031767c522c77e2ad5fc4597a8d1590b13968af143bd382e056b2a1" diff --git a/pyproject.toml b/pyproject.toml index d5705ade5..709ae103f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,10 +85,10 @@ langchain-openai = "^0.1.1" [tool.poetry.group.dev.dependencies] types-redis = "^4.6.0.5" ipykernel = "^6.29.0" -mypy = "^1.8.0" -ruff = "^0.2.1" +mypy = "^1.9.0" +ruff = "^0.3.5" httpx = "*" -pytest = "^8.0.0" +pytest = "^8.1.0" types-requests = "^2.31.0" requests = "^2.31.0" pytest-cov = "^4.1.0" @@ -106,11 +106,23 @@ pytest-sugar = "^1.0.0" respx = "^0.21.1" pytest-instafail = "^0.5.0" pytest-asyncio = "^0.23.0" +pytest-profiling = "^1.7.0" [tool.poetry.extras] deploy = ["celery", "redis", "flower"] local = ["llama-cpp-python", "sentence-transformers", "ctransformers"] -all = ["deploy", "local"] + + +[tool.poetry.group.spelling] +optional = true + +[tool.poetry.group.spelling.dependencies] +codespell = "^2.2.6" + +[tool.codespell] +skip = '.git,*.pdf,*.svg,*.pdf,*.yaml,*.ipynb,poetry.lock,*.min.js,*.css,package-lock.json,*.trig' +# Ignore latin etc +ignore-regex = '.*(Stati Uniti|Tense=Pres).*' [tool.pytest.ini_options] diff --git a/src/backend/base/langflow/alembic/versions/e3bc869fa272_fix_nullable.py b/src/backend/base/langflow/alembic/versions/e3bc869fa272_fix_nullable.py index f4ea219b0..dbcf7fd9f 100644 --- a/src/backend/base/langflow/alembic/versions/e3bc869fa272_fix_nullable.py +++ b/src/backend/base/langflow/alembic/versions/e3bc869fa272_fix_nullable.py @@ -5,6 +5,7 @@ Revises: 1a110b568907 Create Date: 2024-04-10 19:17:22.820455 """ + from typing import Sequence, Union import sqlalchemy as sa diff --git a/src/backend/base/langflow/components/chains/LLMChain.py b/src/backend/base/langflow/components/chains/LLMChain.py index 9c2b3eae2..9fba051db 100644 --- a/src/backend/base/langflow/components/chains/LLMChain.py +++ b/src/backend/base/langflow/components/chains/LLMChain.py @@ -1,9 +1,10 @@ from typing import Optional -from langchain.chains import LLMChain +from langchain.chains.llm import LLMChain -from langflow.field_typing import BaseLanguageModel, BaseMemory, BasePromptTemplate, Text +from langflow.field_typing import BaseLanguageModel, BaseMemory, Text from langflow.interface.custom.custom_component import CustomComponent +from langchain_core.prompts import PromptTemplate class LLMChainComponent(CustomComponent): @@ -19,10 +20,11 @@ class LLMChainComponent(CustomComponent): def build( self, - prompt: BasePromptTemplate, + template: Text, llm: BaseLanguageModel, memory: Optional[BaseMemory] = None, ) -> Text: + prompt = PromptTemplate.from_template(template) runnable = LLMChain(prompt=prompt, llm=llm, memory=memory) result_dict = runnable.invoke({}) output_key = runnable.output_key diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 4b55d8162..cccb1739e 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -918,7 +918,7 @@ class Graph: return ChatVertex elif node_name in ["ShouldRunNext"]: return RoutingVertex - elif node_name in ["SharedState", "Notify", "GetNotified"]: + elif node_name in ["SharedState", "Notify", "Listen"]: return StateVertex elif node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP: return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_base_type] @@ -1130,6 +1130,34 @@ class Graph: return vertices_layers + def sort_layer_by_dependency(self, vertices_layers: List[List[str]]) -> List[List[str]]: + """Sorts the vertices in each layer by dependency, ensuring no vertex depends on a subsequent vertex.""" + sorted_layers = [] + + for layer in vertices_layers: + sorted_layer = self._sort_single_layer_by_dependency(layer) + sorted_layers.append(sorted_layer) + + return sorted_layers + + def _sort_single_layer_by_dependency(self, layer: List[str]) -> List[str]: + """Sorts a single layer by dependency using a stable sorting method.""" + # Build a map of each vertex to its index in the layer for quick lookup. + index_map = {vertex: index for index, vertex in enumerate(layer)} + # Create a sorted copy of the layer based on dependency order. + sorted_layer = sorted(layer, key=lambda vertex: self._max_dependency_index(vertex, index_map), reverse=True) + + return sorted_layer + + def _max_dependency_index(self, vertex_id: str, index_map: Dict[str, int]) -> int: + """Finds the highest index a given vertex's dependencies occupy in the same layer.""" + vertex = self.get_vertex(vertex_id) + max_index = -1 + for successor in vertex.successors: # Assuming vertex.successors is a list of successor vertex identifiers. + if successor.id in index_map: + max_index = max(max_index, index_map[successor.id]) + return max_index + def sort_vertices( self, stop_component_id: Optional[str] = None, @@ -1151,6 +1179,9 @@ class Graph: vertices_layers = self.layered_topological_sort(vertices) vertices_layers = self.sort_by_avg_build_time(vertices_layers) # vertices_layers = self.sort_chat_inputs_first(vertices_layers) + # Now we should sort each layer in a way that we make sure + # vertex V does not depend on vertex V+1 + vertices_layers = self.sort_layer_by_dependency(vertices_layers) self.increment_run_count() self._sorted_vertices_layers = vertices_layers first_layer = vertices_layers[0] diff --git a/src/backend/base/langflow/interface/run.py b/src/backend/base/langflow/interface/run.py index aec602ac7..2327b8725 100644 --- a/src/backend/base/langflow/interface/run.py +++ b/src/backend/base/langflow/interface/run.py @@ -5,17 +5,6 @@ from loguru import logger from langflow.graph import Graph -async def build_sorted_vertices(data_graph, flow_id: str) -> Tuple[Graph, Dict]: - """ - Build langchain object from data_graph. - """ - - logger.debug("Building langchain object") - graph = Graph.from_payload(data_graph, flow_id=flow_id) - - return graph, {} - - 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/base/langflow/services/deps.py b/src/backend/base/langflow/services/deps.py index 23a78e320..beafdec30 100644 --- a/src/backend/base/langflow/services/deps.py +++ b/src/backend/base/langflow/services/deps.py @@ -1,7 +1,7 @@ from contextlib import contextmanager from typing import TYPE_CHECKING, Generator -from langflow.services import ServiceType, service_manager +from langflow.services.schema import ServiceType if TYPE_CHECKING: from sqlmodel import Session @@ -21,7 +21,7 @@ if TYPE_CHECKING: from langflow.services.variable.service import VariableService -def get_service(service_type: ServiceType): +def get_service(service_type: ServiceType, default=None): """ Retrieves the service instance for the given service type. @@ -32,7 +32,13 @@ def get_service(service_type: ServiceType): Any: The service instance. """ - return service_manager.get(service_type) # type: ignore + from langflow.services.manager import service_manager + + if not service_manager.factories: + #! This is a workaround to ensure that the service manager is initialized + #! Not optimal, but it works for now + service_manager.register_factories() + return service_manager.get(service_type, default) # type: ignore def get_state_service() -> "StateService": @@ -42,7 +48,9 @@ def get_state_service() -> "StateService": Returns: The StateService instance. """ - return service_manager.get(ServiceType.STATE_SERVICE) # type: ignore + from langflow.services.state.factory import StateServiceFactory + + return get_service(ServiceType.STATE_SERVICE, StateServiceFactory()) # type: ignore def get_socket_service() -> "SocketIOService": @@ -52,7 +60,7 @@ def get_socket_service() -> "SocketIOService": Returns: SocketIOService: The SocketIOService instance. """ - return service_manager.get(ServiceType.SOCKETIO_SERVICE) # type: ignore + return get_service(ServiceType.SOCKETIO_SERVICE) # type: ignore def get_storage_service() -> "StorageService": @@ -62,7 +70,9 @@ def get_storage_service() -> "StorageService": Returns: The storage service instance. """ - return service_manager.get(ServiceType.STORAGE_SERVICE) # type: ignore + from langflow.services.storage.factory import StorageServiceFactory + + return get_service(ServiceType.STORAGE_SERVICE, default=StorageServiceFactory()) # type: ignore def get_variable_service() -> "VariableService": @@ -73,7 +83,9 @@ def get_variable_service() -> "VariableService": The VariableService instance. """ - return service_manager.get(ServiceType.VARIABLE_SERVICE) # type: ignore + from langflow.services.variable.factory import VariableServiceFactory + + return get_service(ServiceType.VARIABLE_SERVICE, VariableServiceFactory()) # type: ignore def get_plugins_service() -> "PluginService": @@ -83,7 +95,7 @@ def get_plugins_service() -> "PluginService": Returns: PluginService: The PluginService instance. """ - return service_manager.get(ServiceType.PLUGIN_SERVICE) # type: ignore + return get_service(ServiceType.PLUGIN_SERVICE) # type: ignore def get_settings_service() -> "SettingsService": @@ -98,14 +110,9 @@ def get_settings_service() -> "SettingsService": Raises: ValueError: If the service cannot be retrieved or initialized. """ - try: - return service_manager.get(ServiceType.SETTINGS_SERVICE) # type: ignore - except ValueError: - # initialize settings service - from langflow.services.manager import initialize_settings_service + from langflow.services.settings.factory import SettingsServiceFactory - initialize_settings_service() - return service_manager.get(ServiceType.SETTINGS_SERVICE) # type: ignore + return get_service(ServiceType.SETTINGS_SERVICE, SettingsServiceFactory()) # type: ignore def get_db_service() -> "DatabaseService": @@ -116,7 +123,9 @@ def get_db_service() -> "DatabaseService": The DatabaseService instance. """ - return service_manager.get(ServiceType.DATABASE_SERVICE) # type: ignore + from langflow.services.database.factory import DatabaseServiceFactory + + return get_service(ServiceType.DATABASE_SERVICE, DatabaseServiceFactory()) # type: ignore def get_session() -> Generator["Session", None, None]: @@ -165,7 +174,9 @@ def get_cache_service() -> "CacheService": Returns: The cache service instance. """ - return service_manager.get(ServiceType.CACHE_SERVICE) # type: ignore + from langflow.services.cache.factory import CacheServiceFactory + + return get_service(ServiceType.CACHE_SERVICE, CacheServiceFactory()) # type: ignore def get_session_service() -> "SessionService": @@ -175,7 +186,9 @@ def get_session_service() -> "SessionService": Returns: The session service instance. """ - return service_manager.get(ServiceType.SESSION_SERVICE) # type: ignore + from langflow.services.session.factory import SessionServiceFactory + + return get_service(ServiceType.SESSION_SERVICE, SessionServiceFactory()) # type: ignore def get_monitor_service() -> "MonitorService": @@ -185,7 +198,9 @@ def get_monitor_service() -> "MonitorService": Returns: MonitorService: The MonitorService instance. """ - return service_manager.get(ServiceType.MONITOR_SERVICE) # type: ignore + from langflow.services.monitor.factory import MonitorServiceFactory + + return get_service(ServiceType.MONITOR_SERVICE, MonitorServiceFactory()) # type: ignore def get_task_service() -> "TaskService": @@ -196,7 +211,9 @@ def get_task_service() -> "TaskService": The TaskService instance. """ - return service_manager.get(ServiceType.TASK_SERVICE) # type: ignore + from langflow.services.task.factory import TaskServiceFactory + + return get_service(ServiceType.TASK_SERVICE, TaskServiceFactory()) # type: ignore def get_chat_service() -> "ChatService": @@ -206,7 +223,7 @@ def get_chat_service() -> "ChatService": Returns: ChatService: The chat service instance. """ - return service_manager.get(ServiceType.CHAT_SERVICE) # type: ignore + return get_service(ServiceType.CHAT_SERVICE) # type: ignore def get_store_service() -> "StoreService": @@ -216,4 +233,4 @@ def get_store_service() -> "StoreService": Returns: StoreService: The StoreService instance. """ - return service_manager.get(ServiceType.STORE_SERVICE) # type: ignore + return get_service(ServiceType.STORE_SERVICE) # type: ignore diff --git a/src/backend/base/langflow/services/manager.py b/src/backend/base/langflow/services/manager.py index 20186f7da..cd27754d1 100644 --- a/src/backend/base/langflow/services/manager.py +++ b/src/backend/base/langflow/services/manager.py @@ -1,13 +1,20 @@ -from typing import TYPE_CHECKING, Dict +import importlib +import inspect +from typing import TYPE_CHECKING, Dict, Optional from loguru import logger if TYPE_CHECKING: from langflow.services.base import Service + from langflow.services.factory import ServiceFactory from langflow.services.schema import ServiceType +class NoFactoryRegisteredError(Exception): + pass + + class ServiceManager: """ Manages the creation of different services. @@ -16,6 +23,15 @@ class ServiceManager: def __init__(self): self.services: Dict[str, "Service"] = {} self.factories = {} + self.register_factories() + + def register_factories(self): + for factory in self.get_factories(): + try: + self.register_factory(factory) + except Exception as exc: + logger.exception(exc) + logger.error(f"Error initializing {factory}: {exc}") def register_factory( self, @@ -28,24 +44,28 @@ class ServiceManager: service_name = service_factory.service_class.name self.factories[service_name] = service_factory - def get(self, service_name: "ServiceType") -> "Service": + def get(self, service_name: "ServiceType", default: Optional["ServiceFactory"] = None) -> "Service": """ Get (or create) a service by its name. """ + if service_name not in self.services: - self._create_service(service_name) + self._create_service(service_name, default) return self.services[service_name] - def _create_service(self, service_name: "ServiceType"): + def _create_service(self, service_name: "ServiceType", default: Optional["ServiceFactory"] = None): """ Create a new service given its name, handling dependencies. """ logger.debug(f"Create service {service_name}") - self._validate_service_creation(service_name) + self._validate_service_creation(service_name, default) # Create dependencies first factory = self.factories.get(service_name) + if factory is None and default is not None: + self.register_factory(default) + factory = default for dependency in factory.dependencies: if dependency not in self.services: self._create_service(dependency) @@ -57,12 +77,12 @@ class ServiceManager: self.services[service_name] = self.factories[service_name].create(**dependent_services) self.services[service_name].set_ready() - def _validate_service_creation(self, service_name: "ServiceType"): + def _validate_service_creation(self, service_name: "ServiceType", default: Optional["ServiceFactory"] = None): """ Validate whether the service can be created. """ - if service_name not in self.factories: - raise ValueError(f"No factory registered for the service class '{service_name.name}'") + if service_name not in self.factories and default is None: + raise NoFactoryRegisteredError(f"No factory registered for the service class '{service_name.name}'") def update(self, service_name: "ServiceType"): """ @@ -88,6 +108,34 @@ class ServiceManager: self.services = {} self.factories = {} + @staticmethod + def get_factories(): + from langflow.services.factory import ServiceFactory + from langflow.services.schema import ServiceType + + service_names = [ServiceType(service_type).value.replace("_service", "") for service_type in ServiceType] + base_module = "langflow.services" + factories = [] + + for name in service_names: + try: + module_name = f"{base_module}.{name}.factory" + module = importlib.import_module(module_name) + + # Find all classes in the module that are subclasses of ServiceFactory + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, ServiceFactory) and obj is not ServiceFactory: + factories.append(obj()) + break + + except Exception as exc: + logger.exception(exc) + raise RuntimeError( + f"Could not initialize services. Please check your settings. Error in {name}." + ) from exc + + return factories + service_manager = ServiceManager() @@ -106,9 +154,7 @@ def initialize_session_service(): Initialize the session manager. """ from langflow.services.cache import factory as cache_factory - from langflow.services.session import ( - factory as session_service_factory, - ) # type: ignore + from langflow.services.session import factory as session_service_factory # type: ignore initialize_settings_service() diff --git a/src/backend/base/langflow/services/schema.py b/src/backend/base/langflow/services/schema.py index fe0d7022e..511e395d2 100644 --- a/src/backend/base/langflow/services/schema.py +++ b/src/backend/base/langflow/services/schema.py @@ -19,5 +19,5 @@ class ServiceType(str, Enum): VARIABLE_SERVICE = "variable_service" STORAGE_SERVICE = "storage_service" MONITOR_SERVICE = "monitor_service" - SOCKETIO_SERVICE = "socket_service" + # SOCKETIO_SERVICE = "socket_service" STATE_SERVICE = "state_service" diff --git a/src/backend/base/langflow/services/session/service.py b/src/backend/base/langflow/services/session/service.py index 0c925db19..e0feea4e0 100644 --- a/src/backend/base/langflow/services/session/service.py +++ b/src/backend/base/langflow/services/session/service.py @@ -1,6 +1,5 @@ from typing import Coroutine, Optional -from langflow.interface.run import build_sorted_vertices from langflow.services.base import Service from langflow.services.cache.base import CacheService from langflow.services.session.utils import compute_dict_hash, session_id_generator @@ -25,8 +24,10 @@ class SessionService(Service): if data_graph is None: return (None, None) # If not cached, build the graph and cache it - graph, artifacts = await build_sorted_vertices(data_graph, flow_id) + from langflow.graph.graph.base import Graph + graph = Graph.from_payload(data_graph, flow_id=flow_id) + artifacts: dict = {} await self.cache_service.set(key, (graph, artifacts)) return graph, artifacts diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index 531b62bf7..15bc82b7e 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -100,9 +100,9 @@ class Settings(BaseSettings): STORE: Optional[bool] = True STORE_URL: Optional[str] = "https://api.langflow.store" - DOWNLOAD_WEBHOOK_URL: Optional[ - str - ] = "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" + DOWNLOAD_WEBHOOK_URL: Optional[str] = ( + "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" + ) LIKE_WEBHOOK_URL: Optional[str] = "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da" STORAGE_TYPE: str = "local" diff --git a/src/backend/base/langflow/services/utils.py b/src/backend/base/langflow/services/utils.py index 10c1a359e..34b3415ad 100644 --- a/src/backend/base/langflow/services/utils.py +++ b/src/backend/base/langflow/services/utils.py @@ -1,41 +1,13 @@ -import importlib -import inspect - from loguru import logger from sqlmodel import Session, select from langflow.services.auth.utils import create_super_user, verify_password +from langflow.services.cache.factory import CacheServiceFactory from langflow.services.database.utils import initialize_database -from langflow.services.factory import ServiceFactory -from langflow.services.manager import service_manager from langflow.services.schema import ServiceType from langflow.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD -from langflow.services.socket.utils import set_socketio_server -from .deps import get_db_service, get_session, get_settings_service - - -def get_factories(): - service_names = [ServiceType(service_type).value.replace("_service", "") for service_type in ServiceType] - base_module = "langflow.services" - factories = [] - - for name in service_names: - try: - module_name = f"{base_module}.{name}.factory" - module = importlib.import_module(module_name) - - # Find all classes in the module that are subclasses of ServiceFactory - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, ServiceFactory) and obj is not ServiceFactory: - factories.append(obj()) - break - - except Exception as exc: - logger.exception(exc) - raise RuntimeError(f"Could not initialize services. Please check your settings. Error in {name}.") from exc - - return factories +from .deps import get_db_service, get_service, get_session, get_settings_service def get_or_create_super_user(session: Session, username, password, is_default): @@ -145,6 +117,8 @@ def teardown_services(): except Exception as exc: logger.exception(exc) try: + from langflow.services.manager import service_manager + service_manager.teardown() except Exception as exc: logger.exception(exc) @@ -156,7 +130,7 @@ def initialize_settings_service(): """ from langflow.services.settings import factory as settings_factory - service_manager.register_factory(settings_factory.SettingsServiceFactory()) + get_service(ServiceType.SETTINGS_SERVICE, settings_factory.SettingsServiceFactory()) def initialize_session_service(): @@ -168,11 +142,13 @@ def initialize_session_service(): initialize_settings_service() - service_manager.register_factory( + get_service( + ServiceType.CACHE_SERVICE, cache_factory.CacheServiceFactory(), ) - service_manager.register_factory( + get_service( + ServiceType.SESSION_SERVICE, session_service_factory.SessionServiceFactory(), ) @@ -181,27 +157,17 @@ def initialize_services(fix_migration: bool = False, socketio_server=None): """ Initialize all the services needed. """ - for factory in get_factories(): - try: - service_manager.register_factory(factory) - except Exception as exc: - logger.exception(exc) - logger.error(f"Error initializing {factory}: {exc}") - # Test cache connection - service_manager.get(ServiceType.CACHE_SERVICE) + get_service(ServiceType.CACHE_SERVICE, default=CacheServiceFactory()) # Setup the superuser try: initialize_database(fix_migration=fix_migration) except Exception as exc: logger.error(exc) raise exc - setup_superuser(service_manager.get(ServiceType.SETTINGS_SERVICE), next(get_session())) + setup_superuser(get_service(ServiceType.SETTINGS_SERVICE), next(get_session())) try: get_db_service().migrate_flows_if_auto_login() except Exception as exc: logger.error(f"Error migrating flows: {exc}") raise RuntimeError("Error migrating flows") from exc - - # Initialize the SocketIO service - set_socketio_server(socketio_server) diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index d31b8c74d..0bc938e74 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -62,30 +62,6 @@ cryptography = "^42.0.5" asyncer = "^0.0.5" -[tool.poetry.group.dev.dependencies] -pytest-asyncio = "^0.23.1" -types-redis = "^4.6.0.5" -ipykernel = "^6.26.0" -mypy = "^1.7.1" -ruff = "^0.3.5" -httpx = "*" -pytest = "^8.1.1" -types-requests = "^2.31.0" -requests = "^2.31.0" -pytest-cov = "^5.0.0" -pandas-stubs = "^2.2.1.230412" -types-pillow = "^10.2.0.0" -types-pyyaml = "^6.0.12.8" -types-python-jose = "^3.3.4.8" -types-passlib = "^1.7.7.13" -locust = "^2.24.1" -pytest-mock = "^3.14.0" -pytest-xdist = "^3.5.0" -types-pywin32 = "^306.0.0.4" -types-google-cloud-ndb = "^2.3.0.0" -pytest-sugar = "^1.0.0" - - [tool.poetry.extras] deploy = ["celery", "redis", "flower"] local = ["llama-cpp-python", "sentence-transformers", "ctransformers"] diff --git a/tests/conftest.py b/tests/conftest.py index a09e7de0f..03cefb686 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import orjson import pytest from fastapi.testclient import TestClient from httpx import AsyncClient +from sqlmodel import Session, SQLModel, create_engine, select from langflow.graph.graph.base import Graph from langflow.initial_setup.setup import STARTER_FOLDER_NAME from langflow.services.auth.utils import get_password_hash @@ -27,25 +28,39 @@ if TYPE_CHECKING: def pytest_configure(): - pytest.BASIC_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "basic_example.json" - pytest.COMPLEX_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_example.json" - pytest.COMPLEX_DEPS_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_deps_example.json" - pytest.OPENAPI_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "Openapi.json" - pytest.GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "grouped_chat.json" - pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "one_group_chat.json" - pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "vector_store_grouped.json" + data_path = Path(__file__).parent.absolute() / "data" - pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = ( - Path(__file__).parent.absolute() / "data" / "BasicChatWithPromptAndHistory.json" - ) - pytest.CHAT_INPUT = Path(__file__).parent.absolute() / "data" / "ChatInputTest.json" - pytest.TWO_OUTPUTS = Path(__file__).parent.absolute() / "data" / "TwoOutputsTest.json" - pytest.VECTOR_STORE_PATH = Path(__file__).parent.absolute() / "data" / "Vector_store.json" + pytest.BASIC_EXAMPLE_PATH = data_path / "basic_example.json" + pytest.COMPLEX_EXAMPLE_PATH = data_path / "complex_example.json" + pytest.OPENAPI_EXAMPLE_PATH = data_path / "Openapi.json" + pytest.GROUPED_CHAT_EXAMPLE_PATH = data_path / "grouped_chat.json" + pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = data_path / "one_group_chat.json" + pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = data_path / "vector_store_grouped.json" + + pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = data_path / "BasicChatwithPromptandHistory.json" + pytest.CHAT_INPUT = data_path / "ChatInputTest.json" + pytest.TWO_OUTPUTS = data_path / "TwoOutputsTest.json" + pytest.VECTOR_STORE_PATH = data_path / "Vector_store.json" pytest.CODE_WITH_SYNTAX_ERROR = """ def get_text(): retun "Hello World" """ + # validate that all the paths are correct and the files exist + for path in [ + pytest.BASIC_EXAMPLE_PATH, + pytest.COMPLEX_EXAMPLE_PATH, + pytest.OPENAPI_EXAMPLE_PATH, + pytest.GROUPED_CHAT_EXAMPLE_PATH, + pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH, + pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH, + pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, + pytest.CHAT_INPUT, + pytest.TWO_OUTPUTS, + pytest.VECTOR_STORE_PATH, + ]: + assert path.exists(), f"File {path} does not exist. Available files: {list(data_path.iterdir())}" + @pytest.fixture(autouse=True) def check_openai_api_key_in_environment_variables(): @@ -192,16 +207,6 @@ def json_vector_store(): return f.read() -@pytest.fixture -def complex_graph_with_groups(): - with open(pytest.COMPLEX_DEPS_EXAMPLE_PATH, "r") as f: - flow_graph = json.load(f) - data_graph = flow_graph["data"] - nodes = data_graph["nodes"] - edges = data_graph["edges"] - return Graph(nodes, edges) - - @pytest.fixture(name="client", autouse=True) def client_fixture(session: Session, monkeypatch, request): # Set the database url to a test database diff --git a/tests/test_cache_manager.py b/tests/test_cache_manager.py deleted file mode 100644 index bbc94d9fb..000000000 --- a/tests/test_cache_manager.py +++ /dev/null @@ -1,82 +0,0 @@ -from io import StringIO - -import pandas as pd -import pytest -from langflow.services.chat.cache import CacheService -from PIL import Image - - -@pytest.fixture -def cache_service(): - return CacheService() - - -def test_cache_service_attach_detach_notify(cache_service): - observer_called = False - - def observer(): - nonlocal observer_called - observer_called = True - - cache_service.attach(observer) - cache_service.notify() - - assert observer_called - - observer_called = False - cache_service.detach(observer) - cache_service.notify() - - assert not observer_called - - -def test_cache_service_client_context(cache_service): - with cache_service.set_client_id("client1"): - cache_service.add("foo", "bar", "string") - assert cache_service.get("foo") == { - "obj": "bar", - "type": "string", - "extension": "str", - } - - with cache_service.set_client_id("client2"): - cache_service.add("baz", "qux", "string") - assert cache_service.get("baz") == { - "obj": "qux", - "type": "string", - "extension": "str", - } - - with pytest.raises(KeyError): - cache_service.get("foo") - - -def test_cache_service_add_pandas(cache_service): - df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) - - with cache_service.set_client_id("client1"): - cache_service.add_pandas("test_df", df) - cached_df = cache_service.get("test_df") - assert cached_df["type"] == "pandas" - assert cached_df["extension"] == "csv" - read_df = pd.read_csv(StringIO(cached_df["obj"]), index_col=0) - pd.testing.assert_frame_equal(df, read_df) - - -def test_cache_service_add_image(cache_service): - img = Image.new("RGB", (50, 50), color="red") - - with cache_service.set_client_id("client1"): - cache_service.add_image("test_image", img) - cached_img = cache_service.get("test_image") - assert cached_img["type"] == "image" - assert cached_img["extension"] == "png" - assert isinstance(cached_img["obj"], Image.Image) - - -def test_cache_service_get_last(cache_service): - with cache_service.set_client_id("client1"): - cache_service.add("foo", "bar", "string") - cache_service.add("baz", "qux", "string") - last_item = cache_service.get_last() - assert last_item == {"obj": "qux", "type": "string", "extension": "str"} diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 3a9f42360..23d965b7d 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -514,7 +514,7 @@ def test_successful_run_with_output_type_any(client, starter_project, created_ap def test_successful_run_with_output_type_debug(client, starter_project, created_api_key): # This one should return outputs for all components - # Let's just check the amount of outputs(there hsould be 7) + # Let's just check the amount of outputs(there should be 7) headers = {"x-api-key": created_api_key.api_key} flow_id = starter_project["id"] payload = {