Release: Chat and Cache Improvements, Websocket Integration, and Dark Mode Enhancements 🌙 (#220)

This release brings a series of new features and improvements,
including:
- 💬 Chat and Cache Improvements:
- Refactored cache-related functions and moved them to a new base.py
module
  - Simplified the Chat component and added chat history support
  - Implemented the ability to send file responses in chat
  - Real-time Node validation for improved user experience
- CacheManager was added to share data between tools and display them in
the chat.
- 🌐 Websocket Integration:
  - Implemented websocket connection for the chat (WIP)
- 🌙 Dark Mode Enhancements:
  - Fixed dark mode for dropdown components
  - Improved dark mode styling for the chat interface
  - Updated thought icon for dark mode
- 🚀 Other Improvements:
  - Migrated chat logic to chat modal
  - Implemented unique IDs for flow management
  - Sorted sidebar items for better organization
  - Removed unused imports and optimized codebase

This release enhances the overall user experience and streamlines chat
and cache functionalities, ensuring a smoother and more efficient
workflow. 🎉
This commit is contained in:
anovazzi1 2023-04-29 14:03:41 -03:00 committed by GitHub
commit 5c3f7acd7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 3773 additions and 1814 deletions

View file

@ -0,0 +1,15 @@
# LangFlow Demo Codespace Readme
These instructions will walk you through the process of running a LangFlow demo via GitHub Codespaces.
## Setup
### Create a Codespace in GitHub
To setup the demo, simply navigate to the Langflow repo, click the "+" button, and select "Create new Codespace". This will automatically create a new codespace in your browser, which you can use for the demo.
### Wait for everything to install
After the codespace is opened, you should see a new Terminal window in VS Code where langflow is installed. Once the install completes, `langflow` will launch the webserver and your application will be available via devcontainer port.
Note: VS Code should prompt you with a button to push once the port is available.

View file

@ -0,0 +1,32 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
{
"name": "LangChain Demo Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.10",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {},
"ghcr.io/devcontainers/features/node": {}
},
"customizations": {
"vscode": {
"extensions": [
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"eamodio.gitlens"
]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pipx install 'langflow>=0.0.33' && langflow --host 0.0.0.0"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -1,11 +1,12 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
{
"name": "Default Linux Universal",
"name": "LangChain Dev Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {}
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}
},
"customizations": {
"vscode": {"extensions": [
@ -15,7 +16,7 @@
"sourcery.sourcery",
"eamodio.gitlens"
]}
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
@ -24,7 +25,7 @@
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "uname -a",
"postCreateCommand": "poetry install"
// Configure tool-specific properties.
// "customizations": {},

View file

@ -26,8 +26,15 @@ install_frontend:
run_frontend:
cd src/frontend && npm start
run_backend:
poetry run uvicorn langflow.main:app --port 5003 --reload
frontend:
make install_frontend
make run_frontend
install_backend:
poetry install
backend:
poetry run uvicorn langflow.main:app --port 7860 --reload --log-level debug
build_frontend:
cd src/frontend && CI='' npm run build

View file

@ -1,11 +0,0 @@
#! /bin/bash
cd src/frontend
docker build -t logspace/frontend_build -f build.Dockerfile .
cd ../backend
docker build -t logspace/backend_build -f build.Dockerfile .
cd ../../
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker push ibiscp/langflow:$VERSION

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "reactFlow",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

488
poetry.lock generated
View file

@ -551,77 +551,77 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "clickhouse-connect"
version = "0.5.20"
version = "0.5.22"
description = "ClickHouse core driver, SqlAlchemy, and Superset libraries"
category = "main"
optional = false
python-versions = "~=3.7"
files = [
{file = "clickhouse-connect-0.5.20.tar.gz", hash = "sha256:5fc9a84849f3c3b6f6928b45a0df17fa63ebcf4e518b3a48ec70720957e18683"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c29cf8b2c90eed6b83366c13ab5ad471ff6ef2e334f35818729330854b9747ac"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c03ded1b006fa2cf8f7d823f0ff9c6d294e442a123c96ca2a9ebc4b293bfb7f"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb0024160412d9c6079fa6982cb29abda4db8412b4f63918de7a1bde1dcb7aa"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:170bd258d21bc828557f8a55f23affe22cc4e671c93f645a6316ef874e359f8e"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc70fee875fdba42c0a6f519fa376659a08253fd36d188b8b304f4ccda572177"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:18837e06846797db475b6aee13f03928fb169f64d0efb268e2bb04e015990b5b"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:76f7a7d2d41377e6f382a7ada825be594c2d316481f3194bfffd025727633258"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3bac453f1199af29ec7292d2fd2a8cb0cc0e6692bec9c9da50ce5aec10ff0339"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-win32.whl", hash = "sha256:14983562d2687b18d03a35f27b4e7f28cf013c280ff4fee726501e03bae7528d"},
{file = "clickhouse_connect-0.5.20-cp310-cp310-win_amd64.whl", hash = "sha256:3d618a9c15ee4d2facc7a79e59a646262da64e6ec39d2a1ac6a68167d52266bf"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bdfb74ba2bf5157230f576e16c7d708f20ffa7e4b19c54288d7db2b55ebcd17"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fce7e54ad14b732479c5630948324f7088c3092a74a2442bf015a7cab4bc0a41"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e6a2b6d123f5de362d49f079c509a0a43cfbaecae0130c860706ef738af12b7"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a9391128387013755de8e420bb7e17c6c809f77ca3233fdc966a1df023fa85d"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1df976816913675b46134e8dd9dee2cf315cc4bf42e258211f8036099b8fc280"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f1ddeb651bc75b87ec5fa1fbe17fe3a589d00f42cad76d6e64918067f5025798"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:caf60b4bfb7214d80455137eee45ca0943a370885d65f4298fafde0d431e837a"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c0bdcb72607244dc920f543ee6363a6094e836770aaac07f20556936af85813"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-win32.whl", hash = "sha256:cc3f77df2b1cab2aa99b59f529aead2cc96beac1639ed18f7fd8dba392957623"},
{file = "clickhouse_connect-0.5.20-cp311-cp311-win_amd64.whl", hash = "sha256:e44c3b7e40402ce0650f69cbc31f2f503073e2bec9f2b31befbd823150f2431d"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ba78e7d270d78f9559e4a836c6c4f55ab54d9f2b6505c0d05db6260e8e2a4f6a"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e8924824cd19b739cc920d867bf291a31a5da406637e0c575f6eb961cfb0557"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:672c260c471fd18a87a4f5130e6d72590cd4f57289669c58feff5be934810d28"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69887898f8f5ea6e70c30aa51c756f8a752ef0eb1df747d4aec7b7d10de5e103"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c4da55465a52e0e440772e289e6959cc6acbb2efa0561a7ea4f9a7108159958d"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2087b64ab47969e603cd9735e7c0433bdf15c6d83025abd00c50ca9a617ed39b"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:28b72cabb1d4fc3f04392ed1f654bd925b6c950305869971186f73b2d13d835a"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-win32.whl", hash = "sha256:a481e13216de227aa624449f5f6ead9e51fe7c8f18bbd783c41e4b396919fa08"},
{file = "clickhouse_connect-0.5.20-cp37-cp37m-win_amd64.whl", hash = "sha256:c1dc77bdc15240d6d4d375e098c77403aeabbc6f8b1c2ce524f4389a5d8c6d74"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4fe527b6b4306cad58dde934493d5f018166f78f5914f6abf6ed93750ca7ecbd"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c07b9ca21d302e843aa8c031ef15f85c86280c5730858edfe4eeb952d3991d1d"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e427b3cd1f611bcb8315ea9bc17f0329329ca21043f1a5ef068e2903457b9b"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9319037b437c8d1297b00d8bc3f92239cc2296db409b5bfc2ff22b05c5f3a26f"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c3c533fd2baff653dc40e7b88ca86ce9b8d0923c34fb33ce5ce1d1b7370fe6"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c850bc0cf5a00bd144202a6926b646baa60fb4e6c449b62d46c230c548ec760a"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:632922c90cd71fcb8e1b7e6e2a9b4487dee2e67b91846dc1778cfd9d5198d047"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a6c7733b5754ea048bd7928b0cce6625d71c709570c97f1819ba36054850d915"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-win32.whl", hash = "sha256:738b35e061a3c665e9a099a3b5cb50338bed89a6eee3ce29190cd525a1bc1892"},
{file = "clickhouse_connect-0.5.20-cp38-cp38-win_amd64.whl", hash = "sha256:58da16eac95126d441f106d27c8e3ae931fcc784f263d7d916b5a8086bdcf757"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9c57f6958021ec0b22eabaa02e567df3ff5f85fdfd9d052e3aface655bdf3d1"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9c9a2de183a85fc32ef70973cfad5c9af2a8d73733aa30b9523c1400b813c13"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50fd663b132c4edc1fc5dae33c5cbd2538dd2e0c94bd9fff5e98ca3ca12059a2"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a98b165fa2c8420e5219db244f0790b13f401a0932c6a7d5e5c1a959a26b80"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9686bd02a16e3b6cbf976b2476e54bc7caaf1a95fd129fd44b2692d082dfcef6"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d01a51871dde0cd0d24efafd61ab27c57293a0456a26ec7e8a5a585623239ab1"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2c1096ebad10964fcdd646f41228accf182d24b066cefd18d9b33f021e3017cd"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1f0407cc9ea9d2cf51edfe59993c536c256ae54c40c6b36fb7f738edd48f51b5"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-win32.whl", hash = "sha256:184f7c119c9725b25ecaa3011420de8dc06530999653508a983b27c90894146c"},
{file = "clickhouse_connect-0.5.20-cp39-cp39-win_amd64.whl", hash = "sha256:f7d2cbde4543cccddef8465afed221f81095eec3d3b763d7570c22ae99819ab4"},
{file = "clickhouse_connect-0.5.20-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f83a6e61b9832fc9184bf67e3f7bc041f3b940c066b8162bfadf02aa484b1c4"},
{file = "clickhouse_connect-0.5.20-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61b22a7038553813a8f5432cd3b1e57b6d94c629d599d775f57c64c4700a5df"},
{file = "clickhouse_connect-0.5.20-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbae752fadbd9fa9390f2246c5ce6e75a91225d03adb3451beb49bd3f1ea48f0"},
{file = "clickhouse_connect-0.5.20-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9da5c94be2255d6e07e255899411a5e009723f331d90359e5b21c66e8007630"},
{file = "clickhouse_connect-0.5.20-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:205a3dc992548891150d42856e418398d135d9dfa5f30f53bb7c3633d6b449d0"},
{file = "clickhouse_connect-0.5.20-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5e0c42adc692f2fb285f5f898d166cf4ed9b5779e5f3effab8f612cd3362f004"},
{file = "clickhouse_connect-0.5.20-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8a2d9dfbfd7c3075f5d1c7011e32b5b62853000d16f93684fa69d8b8979a04"},
{file = "clickhouse_connect-0.5.20-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f8bb09db27aba694193073137bd69f8404e53c2ee80f2dbf41c829c081175a"},
{file = "clickhouse_connect-0.5.20-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e07d91e3bcaf3989d698a4d9ad9b36f1dcf357673cc4c44a6663ab78581066"},
{file = "clickhouse_connect-0.5.20-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7832b2c4c4c4b316258bd078b54a82c84aeccd62c917eb986059de738b13b56b"},
{file = "clickhouse_connect-0.5.20-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e7dad00ce8df847f896c50aa9644c685259a995a15823fec788348e736fb893"},
{file = "clickhouse_connect-0.5.20-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34b6c4f16d8b4c5c458504da64e87fb2ec1390640ed7345bf051cfbba18526f4"},
{file = "clickhouse_connect-0.5.20-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ce3896158cbac451253bc3632140920a57bb775a82d68370de9ace97ce96a8"},
{file = "clickhouse_connect-0.5.20-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f1e552c4efdab1937ff824f062561fe0b6901044ea06b373a35c8a1a679cea"},
{file = "clickhouse_connect-0.5.20-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05d1cfd70fd90b5d7cdb4e93d603d34f74d34327811e8f573fbbd87838cfd4a3"},
{file = "clickhouse-connect-0.5.22.tar.gz", hash = "sha256:cc8f01ff88d30b118cf2efc33ac34c89a1e332900396da249df7d0f36ac199d7"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09b461047dfc18c9b6ebfc94fb6022c5fc0ee7a343e1e691ee70294dce532909"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ac953ecd231d311f0c1a0576f164c1e6034358eb28cddcf5a2304d570a211d"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02629304d233df14cd7df5dfb37dd5f87feaa95a7506f984a565e3b8fb185210"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327452ed2622c1b6433ab6a1032813b3bf14a33959d47a2b896b2ab5e1a58e07"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7e9f5f9332c5ae51a0f1aecb99117222cc739a8b430b1961261e2cc9dca215"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5ad201756d2d2d7269c747835ba4f24da3d744bc2801e813f8b33fec897ffe10"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c577bcc2b7e1f71073cf4f87600fd6f06ebfc9cd6ae8a98b28ebed9ebcfb80dd"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83b4efc0f7db5b02348a97dba2b2fcd089d6eb45457049d49ed795a084dd7832"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-win32.whl", hash = "sha256:83b613dcab009ff1b18d5bd3cb9e8c2fe2cdd6b67cd1309389c04f11d3621713"},
{file = "clickhouse_connect-0.5.22-cp310-cp310-win_amd64.whl", hash = "sha256:6954b98df0c2624f9933530eabdc1ad3bf143633e2c5328a3f1e167344f8a9c8"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45972e1a37269b3d9ecbcd3eee4f7cbed4a1311d867ce45a6a30a707b31cb6b6"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436eadecdcf8685a5d1f8654b1a7f689f3933db2eae799f1ec6f183e91f2a5a9"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9610d103f42a45362d1c95dc7ef356c5da86359538f9d30e6298f83d407bb643"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25996d9868b79a4f1442ca4200a44882f192466399c767f010e169ca4328cd80"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7562e357debe460dd29c8b5bc457fab9e3c1ab5ff20531aa2a43a0f05d429ed4"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8a3ad2a4d216572067611add9ba285b1aaaf464cd67d57a1d0980c2491bde361"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6c6beacd96bf7eb0a0eb9e33c1d92b8b9dec642916a630332cc599f6c7441064"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c82b5ea7a2814e19b01c192a32ae705d462be7c27fb8759623b6c03e117d4635"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-win32.whl", hash = "sha256:1cc5fde42bbad4739a6c308bc75fba6d9b817529149447f4393c4092bce01aca"},
{file = "clickhouse_connect-0.5.22-cp311-cp311-win_amd64.whl", hash = "sha256:f82269d18fdc25a2d92378ffd5ecb90658cb0748d14819f129a501c660cbdbe8"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0d135ae71723b28f3e250ff030583fd6f5f16af19b7d14bf6bec98be630c2e3b"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8fc12861461a5b88fd8826c500300ef48b18fa820b9c94be9233198475de67a"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c83e8c0b7596ddb255b98e01a3bc9f7ed9f4bde4d072794be217a73d8c6e7c6"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92a903b8ee52dc65da422ecb2b23b27b469cd076c313a2dd69237f06f642e719"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4beb12c6ca9bd5cbdf6661750d7d20b532abb360cd9e93d684114a2e4c0f9d"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4dcda1e33e64537d7b3969c0dec15bdf67a56a5f0801e85f7fd7cb0717a4ce45"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9b1627800be5148ceee872de67b6483f89c89557b9032ea3c09b68b5fa5e1422"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-win32.whl", hash = "sha256:c0cc27d041c994d61b3a5065734b65c0fe9d578fb6dcf614e46f1893b4259e9f"},
{file = "clickhouse_connect-0.5.22-cp37-cp37m-win_amd64.whl", hash = "sha256:5ea78955374abe704c10f0fcb012a954087d45513e3abceefd8d50311b91e72f"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f96ad0d9ab681b2c9d5b1ea622f1446c30267639dce452cf09733af8b70cc565"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c3bc819b930755599d9ef9d58f353f1b6e4013115b793870768de8027738846"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7125ad73427597fb0aecf9983eb69870242788dfb134dfec2e5bc24795b757ce"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995567a46488b51b68070320c90c4dede2e1315b3ae99f8b519889ecff1e0497"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e593a63bc6d081b2e11714de14a8b7509fb86e721489349034cf98412225f0"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:621e2e49af7d8e62dca25eb0663db493af4b26fd25e7c3a4b30a4fdf51b08c87"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00354463177fcf15ce075205b54ae2f8321f72fa5511e4894bfcc4aab430e6f4"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8d93b85eb6bc809a82b592bbe1244b63c2708478eb751a65f250203e6afa72e7"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-win32.whl", hash = "sha256:56fdf98ccdd1a7536b3066479b832a339285e0c806c056e7192547d31d14cbb2"},
{file = "clickhouse_connect-0.5.22-cp38-cp38-win_amd64.whl", hash = "sha256:ef0e233fcccd48398d378e631ca4582725e816e1cd87e38352703969da72815f"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e00a47fb565ed0f24e048457c67b4ab63676be5b0fe2334a7c43938e6d0a4b9"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:44dc02467f3a9c0d91174d09fcdc4c33ed03aa43cdacbb34fd5bb16db3d1b380"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f020238fbb7e9e0800ef0e3661dc220ef7427a5942732797b189fb5c563274"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251ce0491f06e0bb50f8bb006296fbe30efa111748b276b654fc39cb4fd6e4b4"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88abb8ef9e7d48258c17f02d62f0d9a29f09073a0a1628c6024f68ed1670606b"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:24292e0bb742d328c66f38838c3fe1654552e2817d470f8ad040a985b8d1a799"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4c7baf4b535e9cb063568616aaac183c8bb611575ed515d1b55794122feda60b"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7f413cf98d7d99087be993573e8e0d441131fb8d30b62a50af5b6a75a6f7af66"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-win32.whl", hash = "sha256:76087148e9f7632321b39d582f13050820290d8d233721413ebd131d12e70e76"},
{file = "clickhouse_connect-0.5.22-cp39-cp39-win_amd64.whl", hash = "sha256:6b69c9bd695e8129452bb9f0456f6e16caf12592eec3d4f677e626bdde42724a"},
{file = "clickhouse_connect-0.5.22-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:719fb63118147ae5c03db85bc5fa0b6b30ab1e51da7eb64bcbf5b7d03dc405b5"},
{file = "clickhouse_connect-0.5.22-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a04b409b7907cc0f9e70329f6f5ff42fb3ad5a345db9b56a05f55194071d0f1"},
{file = "clickhouse_connect-0.5.22-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77fa7929b7859300be57a2e182aed51efcdba39c8995a19600c746c749d72e00"},
{file = "clickhouse_connect-0.5.22-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311921fb37625f885fa0c957490cd3189afdf2d9ea314d3fad61e26d8932d61b"},
{file = "clickhouse_connect-0.5.22-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:13ed25c3173af67642cc1553e8bc6583a870227b685bbea5897efc3bc78addfd"},
{file = "clickhouse_connect-0.5.22-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4f745384cd6d02d00c5186f9c36d2cd27634fd41d3de4705a7de59d47d6976d0"},
{file = "clickhouse_connect-0.5.22-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b9cb52f22bb438326503d20e54d68b378076e842abc5345da0b1822a01a309b"},
{file = "clickhouse_connect-0.5.22-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fceb764177f24b9808039ef75e2b2f31e93971cf8d2bc9b8249a268a742190a"},
{file = "clickhouse_connect-0.5.22-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03ec3990efac7e11dcb66dff64456c255e5f2eaad7f12ae111c97915dd51a84c"},
{file = "clickhouse_connect-0.5.22-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a924e87f568eeac25dc39b97bba09ecf9fca2e57bf414bc3c5f2282358af7a67"},
{file = "clickhouse_connect-0.5.22-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:89a0f649eb49398708c632336bcbc2f762e26e258a693a844570c003407d9869"},
{file = "clickhouse_connect-0.5.22-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8abf10e0f5cb913df5d80c609cb95850aae436d31095208c3d938df2c4d0524a"},
{file = "clickhouse_connect-0.5.22-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06ee767384ab7c84f96a1825cdc4dd5dcd41c25dd69bf4a56233e2bbc05ee7b"},
{file = "clickhouse_connect-0.5.22-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed4e041398246a474c295e55c10ccda42df6a2af9922cffe775c3c0dd93a50c8"},
{file = "clickhouse_connect-0.5.22-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2f444b4af4327747e43fe8abdd31af64e9fef425c7554a6d04ebca4acd114c71"},
]
[package.dependencies]
@ -701,6 +701,73 @@ lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (
test = ["pytest"]
typing = ["mypy (>=0.990)"]
[[package]]
name = "coverage"
version = "7.2.4"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "coverage-7.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e5eedde6e6e241ec3816f05767cc77e7456bf5ec6b373fb29917f0990e2078f"},
{file = "coverage-7.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c6c6e3b8fb6411a2035da78d86516bfcfd450571d167304911814407697fb7a"},
{file = "coverage-7.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7668a621afc52db29f6867e0e9c72a1eec9f02c94a7c36599119d557cf6e471"},
{file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfb53bef4b2739ff747ebbd76d6ac5384371fd3c7a8af08899074eba034d483"},
{file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c4f2e44a2ae15fa6883898e756552db5105ca4bd918634cbd5b7c00e19e8a1"},
{file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:700bc9fb1074e0c67c09fe96a803de66663830420781df8dc9fb90d7421d4ccb"},
{file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ac4861241e693e21b280f07844ae0e0707665e1dfcbf9466b793584984ae45c4"},
{file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d6f3c5b6738a494f17c73b4aa3aa899865cc33a74aa85e3b5695943b79ad3ce"},
{file = "coverage-7.2.4-cp310-cp310-win32.whl", hash = "sha256:437da7d2fcc35bf45e04b7e9cfecb7c459ec6f6dc17a8558ed52e8d666c2d9ab"},
{file = "coverage-7.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:1d3893f285fd76f56651f04d1efd3bdce251c32992a64c51e5d6ec3ba9e3f9c9"},
{file = "coverage-7.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a17bf32e9e3333d78606ac1073dd20655dc0752d5b923fa76afd3bc91674ab4"},
{file = "coverage-7.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f7ffdb3af2a01ce91577f84fc0faa056029fe457f3183007cffe7b11ea78b23c"},
{file = "coverage-7.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89e63b38c7b888e00fd42ce458f838dccb66de06baea2da71801b0fc9070bfa0"},
{file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4522dd9aeb9cc2c4c54ce23933beb37a4e106ec2ba94f69138c159024c8a906a"},
{file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c7d88468f01a75231797173b52dc66d20a8d91b8bb75c88fc5861268578f52"},
{file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bc47015fc0455753e8aba1f38b81b731aaf7f004a0c390b404e0fcf1d6c1d72f"},
{file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c122d120c11a236558c339a59b4b60947b38ac9e3ad30a0e0e02540b37bf536"},
{file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:50fda3d33b705b9c01e3b772cfa7d14de8aec2ec2870e4320992c26d057fde12"},
{file = "coverage-7.2.4-cp311-cp311-win32.whl", hash = "sha256:ab08af91cf4d847a6e15d7d5eeae5fead1487caf16ff3a2056dbe64d058fd246"},
{file = "coverage-7.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:876e4ef3eff00b50787867c5bae84857a9af4c369a9d5b266cd9b19f61e48ef7"},
{file = "coverage-7.2.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3fc9cde48de956bfbacea026936fbd4974ff1dc2f83397c6f1968f0142c9d50b"},
{file = "coverage-7.2.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12bc9127c8aca2f7c25c9acca53da3db6799b2999b40f28c2546237b7ea28459"},
{file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2857894c22833d3da6e113623a9b7440159b2295280b4e0d954cadbfa724b85a"},
{file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4db4e6c115d869cd5397d3d21fd99e4c7053205c33a4ae725c90d19dcd178af"},
{file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f37ae1804596f13d811e0247ffc8219f5261b3565bdf45fcbb4fc091b8e9ff35"},
{file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdee9a77fd0ce000781680b6a1f4b721c567f66f2f73a49be1843ff439d634f3"},
{file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b65a6a5484b7f2970393d6250553c05b2ede069e0e18abe907fdc7f3528252e"},
{file = "coverage-7.2.4-cp37-cp37m-win32.whl", hash = "sha256:1a3e8697cb40f28e5bcfb6f4bda7852d96dbb6f6fd7cc306aba4ae690c9905ab"},
{file = "coverage-7.2.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4078939c4b7053e14e87c65aa68dbed7867e326e450f94038bfe1a1b22078ff9"},
{file = "coverage-7.2.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:603a2b172126e3b08c11ca34200143089a088cd0297d4cfc4922d2c1c3a892f9"},
{file = "coverage-7.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72751d117ceaad3b1ea3bcb9e85f5409bbe9fb8a40086e17333b994dbccc0718"},
{file = "coverage-7.2.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f19ba9301e6fb0b94ba71fda9a1b02d11f0aab7f8e2455122a4e2921b6703c2f"},
{file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d784177a7fb9d0f58d24d3e60638c8b729c3693963bf67fa919120f750db237"},
{file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d2a9180beff1922b09bd7389e23454928e108449e646c26da5c62e29b0bf4e3"},
{file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:39747afc854a7ee14e5e132da7db179d6281faf97dc51e6d7806651811c47538"},
{file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60feb703abc8d78e9427d873bcf924c9e30cf540a21971ef5a17154da763b60f"},
{file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2becddfcbf3d994a8f4f9dd2b6015cae3a3eff50dedc6e4a17c3cccbe8f93d4"},
{file = "coverage-7.2.4-cp38-cp38-win32.whl", hash = "sha256:56a674ad18d6b04008283ca03c012be913bf89d91c0803c54c24600b300d9e51"},
{file = "coverage-7.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:ab08e03add2cf5793e66ac1bbbb24acfa90c125476f5724f5d44c56eeec1d635"},
{file = "coverage-7.2.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92b565c51732ea2e7e541709ccce76391b39f4254260e5922e08e00971e88e33"},
{file = "coverage-7.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8769a67e8816c7e94d5bf446fc0501641fde78fdff362feb28c2c64d45d0e9b1"},
{file = "coverage-7.2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d74d6fbd5a98a5629e8467b719b0abea9ca01a6b13555d125c84f8bf4ea23d"},
{file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9f770c6052d9b5c9b0e824fd8c003fe33276473b65b4f10ece9565ceb62438e"},
{file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3023ce23e41a6f006c09f7e6d62b6c069c36bdc9f7de16a5ef823acc02e6c63"},
{file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fabd1f4d12dfd6b4f309208c2f31b116dc5900e0b42dbafe4ee1bc7c998ffbb0"},
{file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e41a7f44e73b37c6f0132ecfdc1c8b67722f42a3d9b979e6ebc150c8e80cf13a"},
{file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:864e36947289be05abd83267c4bade35e772526d3e9653444a9dc891faf0d698"},
{file = "coverage-7.2.4-cp39-cp39-win32.whl", hash = "sha256:ea534200efbf600e60130c48552f99f351cae2906898a9cd924c1c7f2fb02853"},
{file = "coverage-7.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:00f8fd8a5fe1ffc3aef78ea2dbf553e5c0f4664324e878995e38d41f037eb2b3"},
{file = "coverage-7.2.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:856bcb837e96adede31018a0854ce7711a5d6174db1a84e629134970676c54fa"},
{file = "coverage-7.2.4.tar.gz", hash = "sha256:7283f78d07a201ac7d9dc2ac2e4faaea99c4d302f243ee5b4e359f3e170dc008"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli"]
[[package]]
name = "dataclasses-json"
version = "0.5.7"
@ -1506,14 +1573,14 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio"
[[package]]
name = "ipython"
version = "8.12.0"
version = "8.13.1"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "ipython-8.12.0-py3-none-any.whl", hash = "sha256:1c183bf61b148b00bcebfa5d9b39312733ae97f6dad90d7e9b4d86c8647f498c"},
{file = "ipython-8.12.0.tar.gz", hash = "sha256:a950236df04ad75b5bc7f816f9af3d74dc118fd42f2ff7e80e8e60ca1f182e2d"},
{file = "ipython-8.13.1-py3-none-any.whl", hash = "sha256:1c80d08f04144a1994cda25569eab07fbdc4989bd8d8793e3a4ff643065ccb51"},
{file = "ipython-8.13.1.tar.gz", hash = "sha256:9c8487ac18f330c8a683fc50ab6d7bc0fcf9ef1d7a9f6ce7926938261067b81f"},
]
[package.dependencies]
@ -1641,32 +1708,38 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "langchain"
version = "0.0.131"
version = "0.0.152"
description = "Building applications with LLMs through composability"
category = "main"
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
{file = "langchain-0.0.131-py3-none-any.whl", hash = "sha256:3564a759e85095c9d71a78817da9cec1e2a8a0cda1bdd94ef8ac7008e432717a"},
{file = "langchain-0.0.131.tar.gz", hash = "sha256:61baf67fbec561ce38d187915a46e1c41139270826453600951760fde1a5d98a"},
{file = "langchain-0.0.152-py3-none-any.whl", hash = "sha256:f4ef223fc5783c34bc375ab5544a2b14c8ab5680bb8f0c8175176d1e8e19558c"},
{file = "langchain-0.0.152.tar.gz", hash = "sha256:bf0ccaf87139e404e2fd68c5144d5321ac7e5b4354e83a818e4a09edfc7d665f"},
]
[package.dependencies]
aiohttp = ">=3.8.3,<4.0.0"
async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
dataclasses-json = ">=0.5.7,<0.6.0"
numexpr = ">=2.8.4,<3.0.0"
numpy = ">=1,<2"
openapi-schema-pydantic = ">=1.2,<2.0"
pydantic = ">=1,<2"
PyYAML = ">=5.4.1"
requests = ">=2,<3"
SQLAlchemy = ">=1,<2"
SQLAlchemy = ">1.3,<3"
tenacity = ">=8.1.0,<9.0.0"
tqdm = ">=4.48.0"
[package.extras]
all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.4,<0.3.0)", "beautifulsoup4 (>=4,<5)", "boto3 (>=1.26.96,<2.0.0)", "cohere (>=3,<4)", "deeplake (>=3.2.9,<4.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "qdrant-client (>=1.1.1,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<2)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.6,<0.3.0)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=3,<4)", "deeplake (>=3.3.0,<4.0.0)", "duckduckgo-search (>=2.8.6,<3.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "lancedb (>=0.1,<0.2)", "lark (>=1.1.5,<2.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "qdrant-client (>=1.1.2,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
azure = ["azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "openai (>=0,<1)"]
cohere = ["cohere (>=3,<4)"]
llms = ["anthropic (>=0.2.4,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "torch (>=1,<2)", "transformers (>=4,<5)"]
embeddings = ["sentence-transformers (>=2,<3)"]
llms = ["anthropic (>=0.2.6,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "torch (>=1,<3)", "transformers (>=4,<5)"]
openai = ["openai (>=0,<1)"]
qdrant = ["qdrant-client (>=1.1.1,<2.0.0)"]
qdrant = ["qdrant-client (>=1.1.2,<2.0.0)"]
[[package]]
name = "lit"
@ -2250,6 +2323,49 @@ plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]]
name = "numexpr"
version = "2.8.4"
description = "Fast numerical expression evaluator for NumPy"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "numexpr-2.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a75967d46b6bd56455dd32da6285e5ffabe155d0ee61eef685bbfb8dafb2e484"},
{file = "numexpr-2.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db93cf1842f068247de631bfc8af20118bf1f9447cd929b531595a5e0efc9346"},
{file = "numexpr-2.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bca95f4473b444428061d4cda8e59ac564dc7dc6a1dea3015af9805c6bc2946"},
{file = "numexpr-2.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e34931089a6bafc77aaae21f37ad6594b98aa1085bb8b45d5b3cd038c3c17d9"},
{file = "numexpr-2.8.4-cp310-cp310-win32.whl", hash = "sha256:f3a920bfac2645017110b87ddbe364c9c7a742870a4d2f6120b8786c25dc6db3"},
{file = "numexpr-2.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:6931b1e9d4f629f43c14b21d44f3f77997298bea43790cfcdb4dd98804f90783"},
{file = "numexpr-2.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9400781553541f414f82eac056f2b4c965373650df9694286b9bd7e8d413f8d8"},
{file = "numexpr-2.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ee9db7598dd4001138b482342b96d78110dd77cefc051ec75af3295604dde6a"},
{file = "numexpr-2.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff5835e8af9a212e8480003d731aad1727aaea909926fd009e8ae6a1cba7f141"},
{file = "numexpr-2.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655d84eb09adfee3c09ecf4a89a512225da153fdb7de13c447404b7d0523a9a7"},
{file = "numexpr-2.8.4-cp311-cp311-win32.whl", hash = "sha256:5538b30199bfc68886d2be18fcef3abd11d9271767a7a69ff3688defe782800a"},
{file = "numexpr-2.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:3f039321d1c17962c33079987b675fb251b273dbec0f51aac0934e932446ccc3"},
{file = "numexpr-2.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c867cc36cf815a3ec9122029874e00d8fbcef65035c4a5901e9b120dd5d626a2"},
{file = "numexpr-2.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:059546e8f6283ccdb47c683101a890844f667fa6d56258d48ae2ecf1b3875957"},
{file = "numexpr-2.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:845a6aa0ed3e2a53239b89c1ebfa8cf052d3cc6e053c72805e8153300078c0b1"},
{file = "numexpr-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:a38664e699526cb1687aefd9069e2b5b9387da7feac4545de446141f1ef86f46"},
{file = "numexpr-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eaec59e9bf70ff05615c34a8b8d6c7bd042bd9f55465d7b495ea5436f45319d0"},
{file = "numexpr-2.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b318541bf3d8326682ebada087ba0050549a16d8b3fa260dd2585d73a83d20a7"},
{file = "numexpr-2.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b076db98ca65eeaf9bd224576e3ac84c05e451c0bd85b13664b7e5f7b62e2c70"},
{file = "numexpr-2.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f12cc851240f7911a47c91aaf223dba753e98e46dff3017282e633602e76a7"},
{file = "numexpr-2.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c368aa35ae9b18840e78b05f929d3a7b3abccdba9630a878c7db74ca2368339"},
{file = "numexpr-2.8.4-cp38-cp38-win32.whl", hash = "sha256:b96334fc1748e9ec4f93d5fadb1044089d73fb08208fdb8382ed77c893f0be01"},
{file = "numexpr-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:a6d2d7740ae83ba5f3531e83afc4b626daa71df1ef903970947903345c37bd03"},
{file = "numexpr-2.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:77898fdf3da6bb96aa8a4759a8231d763a75d848b2f2e5c5279dad0b243c8dfe"},
{file = "numexpr-2.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df35324666b693f13a016bc7957de7cc4d8801b746b81060b671bf78a52b9037"},
{file = "numexpr-2.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ac9cfe6d0078c5fc06ba1c1bbd20b8783f28c6f475bbabd3cad53683075cab"},
{file = "numexpr-2.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df3a1f6b24214a1ab826e9c1c99edf1686c8e307547a9aef33910d586f626d01"},
{file = "numexpr-2.8.4-cp39-cp39-win32.whl", hash = "sha256:7d71add384adc9119568d7e9ffa8a35b195decae81e0abf54a2b7779852f0637"},
{file = "numexpr-2.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:9f096d707290a6a00b6ffdaf581ee37331109fb7b6c8744e9ded7c779a48e517"},
{file = "numexpr-2.8.4.tar.gz", hash = "sha256:d5432537418d18691b9115d615d6daa17ee8275baef3edf1afbbf8bc69806147"},
]
[package.dependencies]
numpy = ">=1.13.3"
[[package]]
name = "numpy"
version = "1.24.3"
@ -2470,14 +2586,14 @@ files = [
[[package]]
name = "openai"
version = "0.27.4"
version = "0.27.5"
description = "Python client library for the OpenAI API"
category = "main"
optional = false
python-versions = ">=3.7.1"
files = [
{file = "openai-0.27.4-py3-none-any.whl", hash = "sha256:3b82c867d531e1fd2003d9de2131e1c4bfd4c70b1a3149e0543a555b30807b70"},
{file = "openai-0.27.4.tar.gz", hash = "sha256:9f9d27d26e62c6068f516c0729449954b5ef6994be1a6cbfe7dbefbc84423a04"},
{file = "openai-0.27.5-py3-none-any.whl", hash = "sha256:5b2121d8c0a4350626096fa482306d12e246a83b992530d54fd474f309f2882c"},
{file = "openai-0.27.5.tar.gz", hash = "sha256:75778ca05759c77dde704985ee16976ba58804212f10e61f44356e68ffb8390e"},
]
[package.dependencies]
@ -2491,6 +2607,21 @@ dev = ["black (>=21.6b0,<22.0)", "pytest (>=6.0.0,<7.0.0)", "pytest-asyncio", "p
embeddings = ["matplotlib", "numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "plotly", "scikit-learn (>=1.0.2)", "scipy", "tenacity (>=8.0.1)"]
wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "wandb"]
[[package]]
name = "openapi-schema-pydantic"
version = "1.2.4"
description = "OpenAPI (v3) specification schema as pydantic class"
category = "main"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "openapi-schema-pydantic-1.2.4.tar.gz", hash = "sha256:3e22cf58b74a69f752cc7e5f1537f6e44164282db2700cbbcd3bb99ddd065196"},
{file = "openapi_schema_pydantic-1.2.4-py3-none-any.whl", hash = "sha256:a932ecc5dcbb308950282088956e94dea069c9823c84e507d64f6b622222098c"},
]
[package.dependencies]
pydantic = ">=1.8.2"
[[package]]
name = "openpyxl"
version = "3.1.2"
@ -2567,6 +2698,21 @@ pytz = ">=2020.1"
[package.extras]
test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
[[package]]
name = "pandas-stubs"
version = "2.0.0.230412"
description = "Type annotations for pandas"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "pandas_stubs-2.0.0.230412-py3-none-any.whl", hash = "sha256:311ab8b42ee574d9fea5061d1f63aeca297e472de6073ba84bf2a017c6cb1b6b"},
{file = "pandas_stubs-2.0.0.230412.tar.gz", hash = "sha256:016f567cb9947edd0067ea2665ab00b77fa47e73a65ce1a097de4f499b3485c0"},
]
[package.dependencies]
types-pytz = ">=2022.1.1"
[[package]]
name = "parso"
version = "0.8.3"
@ -2704,14 +2850,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "platformdirs"
version = "3.3.0"
version = "3.5.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.3.0-py3-none-any.whl", hash = "sha256:ea61fd7b85554beecbbd3e9b37fb26689b227ffae38f73353cbcc1cf8bd01878"},
{file = "platformdirs-3.3.0.tar.gz", hash = "sha256:64370d47dc3fca65b4879f89bdead8197e93e05d696d6d1816243ebae8595da5"},
{file = "platformdirs-3.5.0-py3-none-any.whl", hash = "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4"},
{file = "platformdirs-3.5.0.tar.gz", hash = "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"},
]
[package.extras]
@ -3153,6 +3299,25 @@ 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", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "4.0.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
]
[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
@ -3473,14 +3638,14 @@ files = [
[[package]]
name = "requests"
version = "2.28.2"
version = "2.29.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"
python-versions = ">=3.7"
files = [
{file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
{file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
{file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"},
{file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"},
]
[package.dependencies]
@ -3513,14 +3678,14 @@ idna2008 = ["idna"]
[[package]]
name = "rich"
version = "13.3.4"
version = "13.3.5"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.3.4-py3-none-any.whl", hash = "sha256:22b74cae0278fd5086ff44144d3813be1cedc9115bdfabbfefd86400cb88b20a"},
{file = "rich-13.3.4.tar.gz", hash = "sha256:b5d573e13605423ec80bdd0cd5f8541f7844a0e71a13f74cf454ccb2f490708b"},
{file = "rich-13.3.5-py3-none-any.whl", hash = "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704"},
{file = "rich-13.3.5.tar.gz", hash = "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c"},
]
[package.dependencies]
@ -3787,77 +3952,80 @@ files = [
[[package]]
name = "sqlalchemy"
version = "1.4.47"
version = "2.0.11"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-1.4.47-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dcfb480bfc9e1fab726003ae00a6bfc67a29bad275b63a4e36d17fe7f13a624e"},
{file = "SQLAlchemy-1.4.47-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28fda5a69d6182589892422c5a9b02a8fd1125787aab1d83f1392aa955bf8d0a"},
{file = "SQLAlchemy-1.4.47-cp27-cp27m-win32.whl", hash = "sha256:45e799c1a41822eba6bee4e59b0e38764e1a1ee69873ab2889079865e9ea0e23"},
{file = "SQLAlchemy-1.4.47-cp27-cp27m-win_amd64.whl", hash = "sha256:10edbb92a9ef611f01b086e271a9f6c1c3e5157c3b0c5ff62310fb2187acbd4a"},
{file = "SQLAlchemy-1.4.47-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7a4df53472c9030a8ddb1cce517757ba38a7a25699bbcabd57dcc8a5d53f324e"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:511d4abc823152dec49461209607bbfb2df60033c8c88a3f7c93293b8ecbb13d"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbe57f39f531c5d68d5594ea4613daa60aba33bb51a8cc42f96f17bbd6305e8d"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca8ab6748e3ec66afccd8b23ec2f92787a58d5353ce9624dccd770427ee67c82"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299b5c5c060b9fbe51808d0d40d8475f7b3873317640b9b7617c7f988cf59fda"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-win32.whl", hash = "sha256:684e5c773222781775c7f77231f412633d8af22493bf35b7fa1029fdf8066d10"},
{file = "SQLAlchemy-1.4.47-cp310-cp310-win_amd64.whl", hash = "sha256:2bba39b12b879c7b35cde18b6e14119c5f1a16bd064a48dd2ac62d21366a5e17"},
{file = "SQLAlchemy-1.4.47-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:795b5b9db573d3ed61fae74285d57d396829e3157642794d3a8f72ec2a5c719b"},
{file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:989c62b96596b7938cbc032e39431e6c2d81b635034571d6a43a13920852fb65"},
{file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b67bda733da1dcdccaf354e71ef01b46db483a4f6236450d3f9a61efdba35a"},
{file = "SQLAlchemy-1.4.47-cp311-cp311-win32.whl", hash = "sha256:9a198f690ac12a3a807e03a5a45df6a30cd215935f237a46f4248faed62e69c8"},
{file = "SQLAlchemy-1.4.47-cp311-cp311-win_amd64.whl", hash = "sha256:03be6f3cb66e69fb3a09b5ea89d77e4bc942f3bf84b207dba84666a26799c166"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:16ee6fea316790980779268da47a9260d5dd665c96f225d28e7750b0bb2e2a04"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:557675e0befafa08d36d7a9284e8761c97490a248474d778373fb96b0d7fd8de"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb2797fee8a7914fb2c3dc7de404d3f96eb77f20fc60e9ee38dc6b0ca720f2c2"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28297aa29e035f29cba6b16aacd3680fbc6a9db682258d5f2e7b49ec215dbe40"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-win32.whl", hash = "sha256:998e782c8d9fd57fa8704d149ccd52acf03db30d7dd76f467fd21c1c21b414fa"},
{file = "SQLAlchemy-1.4.47-cp36-cp36m-win_amd64.whl", hash = "sha256:dde4d02213f1deb49eaaf8be8a6425948963a7af84983b3f22772c63826944de"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e98ef1babe34f37f443b7211cd3ee004d9577a19766e2dbacf62fce73c76245a"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a3879853208a242b5913f3a17c6ac0eae9dc210ff99c8f10b19d4a1ed8ed9b"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7120a2f72599d4fed7c001fa1cbbc5b4d14929436135768050e284f53e9fbe5e"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:048509d7f3ac27b83ad82fd96a1ab90a34c8e906e4e09c8d677fc531d12c23c5"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-win32.whl", hash = "sha256:6572d7c96c2e3e126d0bb27bfb1d7e2a195b68d951fcc64c146b94f088e5421a"},
{file = "SQLAlchemy-1.4.47-cp37-cp37m-win_amd64.whl", hash = "sha256:a6c3929df5eeaf3867724003d5c19fed3f0c290f3edc7911616616684f200ecf"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71d4bf7768169c4502f6c2b0709a02a33703544f611810fb0c75406a9c576ee1"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd45c60cc4f6d68c30d5179e2c2c8098f7112983532897566bb69c47d87127d3"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fdbb8e9d4e9003f332a93d6a37bca48ba8095086c97a89826a136d8eddfc455"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f216a51451a0a0466e082e163591f6dcb2f9ec182adb3f1f4b1fd3688c7582c"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-win32.whl", hash = "sha256:bd988b3362d7e586ef581eb14771bbb48793a4edb6fcf62da75d3f0f3447060b"},
{file = "SQLAlchemy-1.4.47-cp38-cp38-win_amd64.whl", hash = "sha256:32ab09f2863e3de51529aa84ff0e4fe89a2cb1bfbc11e225b6dbc60814e44c94"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:07764b240645627bc3e82596435bd1a1884646bfc0721642d24c26b12f1df194"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2a42017984099ef6f56438a6b898ce0538f6fadddaa902870c5aa3e1d82583"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6b6d807c76c20b4bc143a49ad47782228a2ac98bdcdcb069da54280e138847fc"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a94632ba26a666e7be0a7d7cc3f7acab622a04259a3aa0ee50ff6d44ba9df0d"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-win32.whl", hash = "sha256:f80915681ea9001f19b65aee715115f2ad310730c8043127cf3e19b3009892dd"},
{file = "SQLAlchemy-1.4.47-cp39-cp39-win_amd64.whl", hash = "sha256:fc700b862e0a859a37faf85367e205e7acaecae5a098794aff52fdd8aea77b12"},
{file = "SQLAlchemy-1.4.47.tar.gz", hash = "sha256:95fc02f7fc1f3199aaa47a8a757437134cf618e9d994c84effd53f530c38586f"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e9069faea64d3390d90d16e5b2bc0652d8eb979ccdfd555822d96bc8d93afda1"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8aea55b1754430449d43823c8c4da2d5c7621ccd1fcd4c36231417762542d4ef"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ccd20b5a4e3511c2f0c889b7b79a7462b6c6aa2c06d0f4943c27a552e35e091"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfea87230e34d7d55f67959ed09d3e60e09b77c76996de151c32f1b780135"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a836f391d7dc1039f10d2ef58cdc6e271462d6898dacdae1bfabfc16ca295f2c"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25bbf89e6f171d37cf3a993dbeee18cb85abe37a421c40e78131bf339e48da9d"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-win32.whl", hash = "sha256:0624852aec618438a4cd7a53ce00835435588506e6f8fbd60deaf9ac109f7cd0"},
{file = "SQLAlchemy-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:d7eab7d668f95a1a2ef443da17154834adf9c5ac742a5992d5ebecbdca7d943e"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa81761ff674d2e2d591fc88d31835d3ecf65bddb021a522f4eaaae831c584cf"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21f447403a1bfeb832a7384c4ac742b7baab04460632c0335e020e8e2c741d4b"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4d8d96c0a7265de8496250a2c2d02593da5e5e85ea24b5c54c2db028d74cf8c"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c4c5834789f718315cb25d1b95d18fde91b72a1a158cdc515d7f6380c1f02a3"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f57965a9d5882efdea0a2c87ae2f6c7dbc14591dcd0639209b50eec2b3ec947e"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0dd98b0be54503afc4c74e947720c3196f96fb2546bfa54d911d5de313c5463c"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-win32.whl", hash = "sha256:eec40c522781a58839df6a2a7a2d9fbaa473419a3ab94633d61e00a8c0c768b7"},
{file = "SQLAlchemy-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:62835d8cd6713458c032466c38a43e56503e19ea6e54b0e73295c6ab281fc0b1"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:216b9c4dbeaa143a36c9249f9e5a0fd7fa6549a1a3f9de9a2d30104f7e35d8b9"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7710fd24bcf33abed7ab7673dbb38ad48f20555835ff8c77258f07de46a87"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:718c0a9f8509542d0674c15b01f362b2f10e8bc425db74444bda4e073e06e660"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2a5fb41db86f6d4892edcf30bd67418dd757eb0246242648e610fa2bca7533d4"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:352dcd93e5a0421eee59dbac0000f8f811203cf228334d85d77b3ef075707322"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-win32.whl", hash = "sha256:fb21777cc9205b94f51688cdcba0924bdecbeb23dcf81473ff8c5352211e6e38"},
{file = "SQLAlchemy-2.0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9268d7417467e9fde5f4364c71ce490b18a4b83a6543b0d55d1f83fce42bda"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:125c41b3557179e9a514a1cfe2764433177ba6195b2264725ceaa7a2e8afcbde"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19a03413cf36e86674857e519936b9c9e52059ba9f6e2ab0ec75d9a458277cb"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e48d908695abe05435250e0a083416cc49bd5afd46bc16a7ec8725771aad8eac"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3728f7518aa70e5ce88fae4c68b5d7f25493f37d8d867e4a7d60905bd162cd0d"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1ab6ac214354957db83c72c65941af7e022d4c9324bdadc54d0266aa162a3828"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:abadc6bf6b2c0a0be4370513221563afdbac3901d29fcdb7faf23b4e1ed26068"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-win32.whl", hash = "sha256:78cbc8eba442c9b8dc2d90c43ac477f0ee27467617704cd82d741b2eb061afb2"},
{file = "SQLAlchemy-2.0.11-cp38-cp38-win_amd64.whl", hash = "sha256:384fdde6bd628d1a882f04aa9a40aa6928840b02d595ff5bd08abeae4c25f867"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:737a70c994f5b34e437a6ca754957a7a0f6f76c59fa460fc59d1bd15b8f8cb32"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e53e4920cd5872280256ddf6ca843b5d1435e0302847992bcb90f84b744999f"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409cc6cd15d4db5c5af2c4e2d3a2137815c31d065cea9a77dec92cbe7cfcf448"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71dd742e3146be6fdded0b95a4b779f7d81595760eab32b0f718089573d3b86"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d85ca17b070f7076ec2582324331cf3683c09146fd8bd2621e8d80d6c3a93bbf"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a785c30929a5d82f2fa1c60ec46d623d418b19981dc0c594da806d3901658e39"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-win32.whl", hash = "sha256:66f24708cebe5a4e900e221574b50e102908f60f539fea30f1922705c0e97744"},
{file = "SQLAlchemy-2.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:5a2f95901e6bbed27b4ad5d59ab3f970eda0ce0b9ede3a67b6f9a914149ed71b"},
{file = "SQLAlchemy-2.0.11-py3-none-any.whl", hash = "sha256:1d28e8278d943d9111d44720f92cc338282e956ed68849bfcee053c06bde4f39"},
{file = "SQLAlchemy-2.0.11.tar.gz", hash = "sha256:c3cbff7cced3c42dbe71448ce6bf4202b4a2d305e78dd77e3f280ba6cd245138"},
]
[package.dependencies]
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""}
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
typing-extensions = ">=4.2.0"
[package.extras]
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
mypy = ["mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0)"]
mysql-connector = ["mysql-connector-python"]
oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"]
oracle = ["cx-oracle (>=7)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql", "pymysql (<1)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3-binary"]
[[package]]
@ -4275,6 +4443,30 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
[[package]]
name = "types-pillow"
version = "9.5.0.2"
description = "Typing stubs for Pillow"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-Pillow-9.5.0.2.tar.gz", hash = "sha256:b3f9f621f259566c19c1deca21901017c8b1e3e200ed2e49e0a2d83c0a5175db"},
{file = "types_Pillow-9.5.0.2-py3-none-any.whl", hash = "sha256:58fdebd0ffa2353ecccdd622adde23bce89da5c0c8b96c34f2d1eca7b7e42d0e"},
]
[[package]]
name = "types-pytz"
version = "2023.3.0.0"
description = "Typing stubs for pytz"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"},
{file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"},
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.9"
@ -4289,14 +4481,14 @@ files = [
[[package]]
name = "types-requests"
version = "2.28.11.17"
version = "2.29.0.0"
description = "Typing stubs for requests"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-requests-2.28.11.17.tar.gz", hash = "sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0"},
{file = "types_requests-2.28.11.17-py3-none-any.whl", hash = "sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"},
{file = "types-requests-2.29.0.0.tar.gz", hash = "sha256:c86f4a955d943d2457120dbe719df24ef0924e11177164d10a0373cf311d7b4d"},
{file = "types_requests-2.29.0.0-py3-none-any.whl", hash = "sha256:4cf6e323e856c779fbe8815bb977a5bf5d6c5034713e4c17ff2a9a20610f5b27"},
]
[package.dependencies]
@ -4304,14 +4496,14 @@ types-urllib3 = "<1.27"
[[package]]
name = "types-urllib3"
version = "1.26.25.10"
version = "1.26.25.11"
description = "Typing stubs for urllib3"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-urllib3-1.26.25.10.tar.gz", hash = "sha256:c44881cde9fc8256d05ad6b21f50c4681eb20092552351570ab0a8a0653286d6"},
{file = "types_urllib3-1.26.25.10-py3-none-any.whl", hash = "sha256:12c744609d588340a07e45d333bf870069fc8793bcf96bae7a96d4712a42591d"},
{file = "types-urllib3-1.26.25.11.tar.gz", hash = "sha256:697102ddf4f781eed6f692353f40cee1098643526f5a8b99f49d2ede90fd3754"},
{file = "types_urllib3-1.26.25.11-py3-none-any.whl", hash = "sha256:04235e792139cf3624b25d38faab593456738fbdb7439634046172e3b1339400"},
]
[[package]]
@ -4802,4 +4994,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "f0a23552d8cbd3d38722a69698cf823cdd19a90b707538837da64a933f8d13b4"
content-hash = "c8ba5d9e0208fc55dc2f00efffed26d7a2c7d325568dfd697cc6499d210304d9"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "langflow"
version = "0.0.58"
version = "0.0.61"
description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"]
maintainers = [
@ -29,7 +29,7 @@ google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0"
typer = "^0.7.0"
gunicorn = "^20.1.0"
langchain = "^0.0.131"
langchain = "~0.0.150"
openai = "^0.27.2"
types-pyyaml = "^6.0.12.8"
dill = "^0.3.6"
@ -47,6 +47,7 @@ fake-useragent = "^1.1.3"
docstring-parser = "^0.15"
psycopg2-binary = "^2.9.6"
pyarrow = "^11.0.0"
websockets = "^11.0.2"
[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
@ -57,6 +58,10 @@ httpx = "^0.23.3"
pytest = "^7.2.2"
types-requests = "^2.28.11"
requests = "^2.28.0"
pytest-cov = "^4.0.0"
pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2"
[tool.ruff]
line-length = 120

View file

@ -1,52 +0,0 @@
# `python-base` sets up all our shared environment variables
FROM python:3.10-slim
# python
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
\
# pip
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
\
# poetry
# https://python-poetry.org/docs/configuration/#using-environment-variables
POETRY_VERSION=1.4.0 \
# make poetry install to this location
POETRY_HOME="/opt/poetry" \
# make poetry create the virtual environment in the project's root
# it gets named `.venv`
POETRY_VIRTUALENVS_IN_PROJECT=true \
# do not ask any interactive question
POETRY_NO_INTERACTION=1 \
\
# paths
# this is where our requirements + virtual environment will live
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
curl \
# deps for building python deps
build-essential libpq-dev
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
RUN curl -sSL https://install.python-poetry.org | python3 -
# copy project requirement files here to ensure they will be cached.
WORKDIR /app
COPY poetry.lock pyproject.toml ./
COPY langflow/ ./langflow
# poetry install
RUN poetry install --without dev
# build wheel
RUN poetry build -f wheel

View file

@ -1,6 +0,0 @@
#! /bin/bash
docker build -t logspace/backend_build -f build.Dockerfile .
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker push ibiscp/langflow:$VERSION

View file

@ -1 +1,4 @@
from langflow.interface.loading import load_flow_from_json # noqa
from langflow.interface.loading import load_flow_from_json
from langflow.cache import cache_manager
__all__ = ["load_flow_from_json", "cache_manager"]

View file

@ -3,6 +3,10 @@ from pydantic import BaseModel, validator
from langflow.graph.utils import extract_input_variables_from_prompt
class CacheResponse(BaseModel):
data: dict
class Code(BaseModel):
code: str

View file

@ -0,0 +1,16 @@
from typing import Any
from langchain.callbacks.base import AsyncCallbackHandler
from langflow.api.schemas import ChatResponse
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
class StreamingLLMCallbackHandler(AsyncCallbackHandler):
"""Callback handler for streaming LLM responses."""
def __init__(self, websocket):
self.websocket = websocket
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
await self.websocket.send_json(resp.dict())

View file

@ -0,0 +1,18 @@
from fastapi import APIRouter, WebSocket
from langflow.api.chat_manager import ChatManager
from langflow.utils.logger import logger
router = APIRouter()
chat_manager = ChatManager()
@router.websocket("/chat/{client_id}")
async def websocket_endpoint(client_id: str, websocket: WebSocket):
"""Websocket endpoint for chat."""
try:
await chat_manager.handle_websocket(client_id, websocket)
except Exception as e:
# Log stack trace
logger.exception(e)
raise e

View file

@ -0,0 +1,212 @@
import asyncio
from typing import Dict, List
from collections import defaultdict
from fastapi import WebSocket
import json
from langflow.api.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.cache.manager import Subject
from langflow.interface.run import (
get_result_and_steps,
load_or_build_langchain_object,
)
from langflow.interface.utils import pil_to_base64, try_setting_streaming_options
from langflow.utils.logger import logger
from langflow.cache import cache_manager
class ChatHistory(Subject):
def __init__(self):
super().__init__()
self.history: Dict[str, List[ChatMessage]] = defaultdict(list)
def add_message(self, client_id: str, message: ChatMessage):
"""Add a message to the chat history."""
self.history[client_id].append(message)
if not isinstance(message, FileResponse):
self.notify()
def get_history(self, client_id: str, filter=True) -> List[ChatMessage]:
"""Get the chat history for a client."""
if history := self.history.get(client_id, []):
if filter:
return [msg for msg in history if msg.type not in ["start", "stream"]]
return history
else:
return []
def empty_history(self, client_id: str):
"""Empty the chat history for a client."""
self.history[client_id] = []
class ChatManager:
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
self.chat_history = ChatHistory()
self.chat_history.attach(self.on_chat_history_update)
self.cache_manager = cache_manager
self.cache_manager.attach(self.update)
def on_chat_history_update(self):
"""Send the last chat message to the client."""
client_id = self.cache_manager.current_client_id
if client_id in self.active_connections:
chat_response = self.chat_history.get_history(client_id, filter=False)[-1]
if chat_response.is_bot:
# Process FileResponse
if isinstance(chat_response, FileResponse):
# If data_type is pandas, convert to csv
if chat_response.data_type == "pandas":
chat_response.data = chat_response.data.to_csv()
elif chat_response.data_type == "image":
# Base64 encode the image
chat_response.data = pil_to_base64(chat_response.data)
# get event loop
loop = asyncio.get_event_loop()
coroutine = self.send_json(client_id, chat_response)
asyncio.run_coroutine_threadsafe(coroutine, loop)
def update(self):
if self.cache_manager.current_client_id in self.active_connections:
self.last_cached_object_dict = self.cache_manager.get_last()
# Add a new ChatResponse with the data
chat_response = FileResponse(
message=None,
type="file",
data=self.last_cached_object_dict["obj"],
data_type=self.last_cached_object_dict["type"],
)
self.chat_history.add_message(
self.cache_manager.current_client_id, chat_response
)
async def connect(self, client_id: str, websocket: WebSocket):
await websocket.accept()
self.active_connections[client_id] = websocket
def disconnect(self, client_id: str):
del self.active_connections[client_id]
async def send_message(self, client_id: str, message: str):
websocket = self.active_connections[client_id]
await websocket.send_text(message)
async def send_json(self, client_id: str, message: ChatMessage):
websocket = self.active_connections[client_id]
await websocket.send_json(message.dict())
async def process_message(self, client_id: str, payload: Dict):
# Process the graph data and chat message
chat_message = payload.pop("message", "")
chat_message = ChatMessage(message=chat_message)
self.chat_history.add_message(client_id, chat_message)
graph_data = payload
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
self.chat_history.add_message(client_id, start_resp)
is_first_message = len(self.chat_history.get_history(client_id=client_id)) == 0
# Generate result and thought
try:
logger.debug("Generating result and thought")
result, intermediate_steps = await process_graph(
graph_data=graph_data,
is_first_message=is_first_message,
chat_message=chat_message,
websocket=self.active_connections[client_id],
)
except Exception as e:
# Log stack trace
logger.exception(e)
self.chat_history.empty_history(client_id)
raise e
# Send a response back to the frontend, if needed
intermediate_steps = intermediate_steps or ""
history = self.chat_history.get_history(client_id, filter=False)
file_responses = []
if history:
# Iterate backwards through the history
for msg in reversed(history):
if isinstance(msg, FileResponse):
if msg.data_type == "image":
# Base64 encode the image
msg.data = pil_to_base64(msg.data)
file_responses.append(msg)
if msg.type == "start":
break
response = ChatResponse(
message=result or "",
intermediate_steps=intermediate_steps.strip(),
type="end",
files=file_responses,
)
self.chat_history.add_message(client_id, response)
async def handle_websocket(self, client_id: str, websocket: WebSocket):
await self.connect(client_id, websocket)
try:
chat_history = self.chat_history.get_history(client_id)
# iterate and make BaseModel into dict
chat_history = [chat.dict() for chat in chat_history]
await websocket.send_json(chat_history)
while True:
json_payload = await websocket.receive_json()
try:
payload = json.loads(json_payload)
except TypeError:
payload = json_payload
if "clear_history" in payload:
self.chat_history.history[client_id] = []
continue
with self.cache_manager.set_client_id(client_id):
await self.process_message(client_id, payload)
except Exception as e:
# Handle any exceptions that might occur
logger.exception(e)
# send a message to the client
await self.send_message(client_id, str(e))
raise e
finally:
await self.active_connections[client_id].close(
code=1000, reason="Client disconnected"
)
self.disconnect(client_id)
async def process_graph(
graph_data: Dict,
is_first_message: bool,
chat_message: ChatMessage,
websocket: WebSocket,
):
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
langchain_object = try_setting_streaming_options(langchain_object, websocket)
logger.debug("Loaded langchain object")
if langchain_object is None:
# Raise user facing error
raise ValueError(
"There was an error loading the langchain_object. Please, check all the nodes and try again."
)
# Generate result and thought
try:
logger.debug("Generating result and thought")
result, intermediate_steps = get_result_and_steps(
langchain_object, chat_message.message or ""
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps
except Exception as e:
# Log stack trace
logger.exception(e)
raise e

View file

@ -0,0 +1,40 @@
from typing import Any, Union
from pydantic import BaseModel, validator
class ChatMessage(BaseModel):
"""Chat message schema."""
is_bot: bool = False
message: Union[str, None] = None
type: str = "human"
class ChatResponse(ChatMessage):
"""Chat response schema."""
intermediate_steps: str
type: str
is_bot: bool = True
files: list = []
@validator("type")
def validate_message_type(cls, v):
if v not in ["start", "stream", "end", "error", "info", "file"]:
raise ValueError("type must be start, stream, end, error, info, or file")
return v
class FileResponse(ChatMessage):
"""File response schema."""
data: Any
data_type: str
type: str = "file"
is_bot: bool = True
@validator("data_type")
def validate_data_type(cls, v):
if v not in ["image", "csv"]:
raise ValueError("data_type must be image or csv")
return v

View file

@ -7,6 +7,7 @@ from langflow.api.base import (
PromptValidationResponse,
validate_prompt,
)
from langflow.interface.run import build_graph
from langflow.utils.logger import logger
from langflow.utils.validate import validate_code
@ -33,3 +34,20 @@ def post_validate_prompt(prompt: Prompt):
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e
# validate node
@router.post("/node/{node_id}", status_code=200)
def post_validate_node(node_id: str, data: dict):
try:
# build graph
graph = build_graph(data)
# validate node
node = graph.get_node(node_id)
if node is not None:
_ = node.build()
return str(node.params)
raise Exception(f"Node {node_id} not found")
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e

View file

@ -0,0 +1 @@
from langflow.cache.manager import cache_manager # noqa

View file

@ -2,14 +2,17 @@ import base64
import contextlib
import functools
import hashlib
import json
import os
import tempfile
from collections import OrderedDict
from pathlib import Path
from typing import Any, Dict
import dill # type: ignore
CACHE: Dict[str, Any] = {}
def create_cache_folder(func):
def wrapper(*args, **kwargs):

149
src/backend/langflow/cache/manager.py vendored Normal file
View file

@ -0,0 +1,149 @@
from contextlib import contextmanager
from typing import Any, Awaitable, Callable, List, Optional
from PIL import Image
import pandas as pd
class Subject:
"""Base class for implementing the observer pattern."""
def __init__(self):
self.observers: List[Callable[[], None]] = []
def attach(self, observer: Callable[[], None]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], None]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
observer()
class AsyncSubject:
"""Base class for implementing the async observer pattern."""
def __init__(self):
self.observers: List[Callable[[], Awaitable]] = []
def attach(self, observer: Callable[[], Awaitable]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], Awaitable]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
async def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
await observer()
class CacheManager(Subject):
"""Manages cache for different clients and notifies observers on changes."""
def __init__(self):
super().__init__()
self.CACHE = {}
self.current_client_id = None
self.current_cache = {}
@contextmanager
def set_client_id(self, client_id: str):
"""
Context manager to set the current client_id and associated cache.
Args:
client_id (str): The client identifier.
"""
previous_client_id = self.current_client_id
self.current_client_id = client_id
self.current_cache = self.CACHE.setdefault(client_id, {})
try:
yield
finally:
self.current_client_id = previous_client_id
self.current_cache = self.CACHE.get(self.current_client_id, {})
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
"""
Add an object to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The object to cache.
obj_type (str): The type of the object.
"""
object_extensions = {
"image": "png",
"pandas": "csv",
}
if obj_type in object_extensions:
_extension = object_extensions[obj_type]
else:
_extension = type(obj).__name__.lower()
self.current_cache[name] = {
"obj": obj,
"type": obj_type,
"extension": extension or _extension,
}
self.notify()
def add_pandas(self, name: str, obj: Any):
"""
Add a pandas DataFrame or Series to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The pandas DataFrame or Series object.
"""
if isinstance(obj, (pd.DataFrame, pd.Series)):
self.add(name, obj.to_csv(), "pandas", extension="csv")
else:
raise ValueError("Object is not a pandas DataFrame or Series")
def add_image(self, name: str, obj: Any, extension: str = "png"):
"""
Add a PIL Image to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The PIL Image object.
"""
if isinstance(obj, Image.Image):
self.add(name, obj, "image", extension=extension)
else:
raise ValueError("Object is not a PIL Image")
def get(self, name: str):
"""
Get an object from the current client's cache.
Args:
name (str): The cache key.
Returns:
The cached object associated with the given cache key.
"""
return self.current_cache[name]
def get_last(self):
"""
Get the last added item in the current client's cache.
Returns:
The last added item in the cache.
"""
return list(self.current_cache.values())[-1]
cache_manager = CacheManager()

View file

@ -15,6 +15,11 @@ CUSTOM_NODES = {
"utilities": {
"SQLDatabase": nodes.SQLDatabaseNode(),
},
"chains": {
"SeriesCharacterChain": nodes.SeriesCharacterChainNode(),
"TimeTravelGuideChain": nodes.TimeTravelGuideChainNode(),
"MidJourneyPromptChain": nodes.MidJourneyPromptChainNode(),
},
}

View file

@ -4,16 +4,18 @@
# - Build each inner agent first, then build the outer agent
import contextlib
import inspect
import types
import warnings
from copy import deepcopy
from typing import Any, Dict, List, Optional
from langflow.cache import utils as cache_utils
from langflow.cache import base as cache_utils
from langflow.graph.constants import DIRECT_TYPES
from langflow.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT
from langflow.utils.logger import logger
from langflow.utils.util import sync_to_async
class Node:
@ -158,13 +160,21 @@ class Node:
continue
result = value.build()
# If the key is "func", then we need to use the run method
if key == "func" and not isinstance(result, types.FunctionType):
# func can be PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n')
# so we need to check if there is an attribute called run
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
if key == "func":
if not isinstance(result, types.FunctionType):
# func can be
# PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n')
# so we need to check if there is an attribute called run
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
elif inspect.iscoroutinefunction(result):
self.params["coroutine"] = result
else:
# turn result which is a function into a coroutine
# so that it can be awaited
self.params["coroutine"] = sync_to_async(result)
self.params[key] = result
elif isinstance(value, list) and all(

View file

@ -174,7 +174,7 @@ class SQLAgent(AgentExecutor):
def from_toolkit_and_llm(cls, llm: BaseLLM, database_uri: str, **kwargs: Any):
"""Construct a sql agent from an LLM and tools."""
db = SQLDatabase.from_uri(database_uri)
toolkit = SQLDatabaseToolkit(db=db)
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
# The right code should be this, but there is a problem with tools = toolkit.get_tools()
# related to `OPENAI_API_KEY`

View file

@ -37,7 +37,9 @@ class ChainCreator(LangChainTypeCreator):
try:
if name in get_custom_nodes(self.type_name).keys():
return get_custom_nodes(self.type_name)[name]
return build_template_from_class(name, self.type_to_loader_dict)
return build_template_from_class(
name, self.type_to_loader_dict, add_function=True
)
except ValueError as exc:
raise ValueError("Chain not found") from exc
except AttributeError as exc:

View file

@ -20,6 +20,7 @@ from langchain.llms.loading import load_llm_from_config
from langflow.interface.agents.custom import CUSTOM_AGENTS
from langflow.interface.importing.utils import import_by_type
from langflow.interface.run import fix_memory_inputs
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.types import get_type_list
from langflow.interface.utils import load_file_into_dict
@ -33,6 +34,12 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
return custom_agent.initialize(**params) # type: ignore
class_object = import_by_type(_type=base_type, name=node_type)
# check if it is a class before using issubclass
# if isinstance(class_object, type) and issubclass(class_object, BaseModel):
# # validate params
# fields = class_object.__fields__
# params = {key: value for key, value in params.items() if key in fields}
if base_type == "agents":
# We need to initialize it differently
@ -65,6 +72,7 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
return load_toolkits_executor(node_type, loaded_toolkit, params)
return loaded_toolkit
elif base_type == "embeddings":
# ? Why remove model from params?
params.pop("model")
return class_object(**params)
elif base_type == "vectorstores":
@ -106,7 +114,19 @@ def load_flow_from_json(path: str, build=True):
# Nodes, edges and root node
edges = data_graph["edges"]
graph = Graph(nodes, edges)
return graph.build() if build else graph
if build:
langchain_object = graph.build()
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = False
fix_memory_inputs(langchain_object)
return langchain_object
return graph
def replace_zero_shot_prompt_with_prompt_template(nodes):
@ -166,7 +186,9 @@ def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs)
allowed_tools = params["allowed_tools"]
llm_chain = params["llm_chain"]
tool_names = [tool.name for tool in allowed_tools]
agent = agent_class(allowed_tools=tool_names, llm_chain=llm_chain)
# Agent class requires an output_parser but Agent classes
# have a default output_parser.
agent = agent_class(allowed_tools=tool_names, llm_chain=llm_chain) # type: ignore
return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=allowed_tools,

View file

@ -3,7 +3,7 @@ import io
from typing import Any, Dict
from chromadb.errors import NotEnoughElementsException # type: ignore
from langflow.cache.utils import compute_dict_hash, load_cache, memoize_dict
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
from langflow.graph.graph import Graph
from langflow.interface import loading
from langflow.utils.logger import logger
@ -32,23 +32,23 @@ def load_or_build_langchain_object(data_graph, is_first_message=False):
return build_langchain_object_with_caching(data_graph)
@memoize_dict(maxsize=1)
@memoize_dict(maxsize=10)
def build_langchain_object_with_caching(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)
graph = build_graph(data_graph)
return graph.build()
def build_graph(data_graph):
nodes = data_graph["nodes"]
edges = data_graph["edges"]
return Graph(nodes, edges)
def build_langchain_object(data_graph):
"""
Build langchain object from data_graph.
@ -87,7 +87,7 @@ def process_graph(data_graph: Dict[str, Any]):
# Generate result and thought
logger.debug("Generating result and thought")
result, thought = get_result_and_thought_using_graph(langchain_object, message)
result, thought = get_result_and_steps(langchain_object, message)
logger.debug("Generated result and thought")
# Save langchain_object to cache
@ -118,7 +118,7 @@ def process_graph_cached(data_graph: Dict[str, Any]):
# Generate result and thought
logger.debug("Generating result and thought")
result, thought = get_result_and_thought_using_graph(langchain_object, message)
result, thought = get_result_and_steps(langchain_object, message)
logger.debug("Generated result and thought")
return {"result": str(result), "thought": thought.strip()}
@ -184,7 +184,7 @@ def fix_memory_inputs(langchain_object):
update_memory_keys(langchain_object, possible_new_mem_key)
def get_result_and_thought_using_graph(langchain_object, message: str):
def get_result_and_steps(langchain_object, message: str):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
@ -240,6 +240,61 @@ def get_result_and_thought_using_graph(langchain_object, message: str):
return result, thought
def async_get_result_and_steps(langchain_object, message: str):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
chat_input = None
memory_key = ""
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
memory_key = langchain_object.memory.memory_key
if hasattr(langchain_object, "input_keys"):
for key in langchain_object.input_keys:
if key not in [memory_key, "chat_history"]:
chat_input = {key: message}
else:
chat_input = message # type: ignore
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = False
fix_memory_inputs(langchain_object)
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
try:
# if hasattr(langchain_object, "acall"):
# output = await langchain_object.acall(chat_input)
# else:
output = langchain_object(chat_input)
except ValueError as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
output = langchain_object.run(chat_input)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []
)
result = (
output.get(langchain_object.output_keys[0])
if isinstance(output, dict)
else output
)
if intermediate_steps:
thought = format_intermediate_steps(intermediate_steps)
else:
thought = output_buffer.getvalue()
except Exception as exc:
raise ValueError(f"Error: {str(exc)}") from exc
return result, thought
def get_result_and_thought(extracted_json: Dict[str, Any], message: str):
"""Get result and thought from extracted json"""
try:

View file

@ -1,5 +1,12 @@
import base64
from io import BytesIO
import json
import os
from PIL.Image import Image
from langchain.callbacks.base import AsyncCallbackManager
from langchain.chat_models import AzureChatOpenAI, ChatOpenAI
from langchain.llms import AzureOpenAI, OpenAI
from langflow.api.callback import StreamingLLMCallbackHandler
import yaml
@ -20,3 +27,30 @@ def load_file_into_dict(file_path: str) -> dict:
raise ValueError("Unsupported file type. Please provide a JSON or YAML file.")
return data
def pil_to_base64(image: Image) -> str:
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue())
return img_str.decode("utf-8")
def try_setting_streaming_options(langchain_object, websocket):
# If the LLM type is OpenAI or ChatOpenAI,
# set streaming to True
# First we need to find the LLM
llm = None
if hasattr(langchain_object, "llm"):
llm = langchain_object.llm
elif hasattr(langchain_object, "llm_chain") and hasattr(
langchain_object.llm_chain, "llm"
):
llm = langchain_object.llm_chain.llm
if isinstance(llm, (OpenAI, ChatOpenAI, AzureOpenAI, AzureChatOpenAI)):
llm.streaming = bool(hasattr(llm, "streaming"))
stream_handler = StreamingLLMCallbackHandler(websocket)
stream_manager = AsyncCallbackManager([stream_handler])
llm.callback_manager = stream_manager
return langchain_object

View file

@ -3,6 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
from langflow.api.endpoints import router as endpoints_router
from langflow.api.validate import router as validate_router
from langflow.api.chat import router as chat_router
def create_app():
@ -23,6 +24,7 @@ def create_app():
app.include_router(endpoints_router)
app.include_router(validate_router)
app.include_router(chat_router)
return app

View file

@ -23,6 +23,7 @@ class TemplateFieldCreator(BaseModel, ABC):
options: list[str] = []
name: str = ""
display_name: Optional[str] = None
advanced: bool = True
def to_dict(self):
result = self.dict()
@ -228,6 +229,11 @@ class FrontendNode(BaseModel):
field.required = False
if field.value is None:
field.value = ""
if "kwargs" in field.name.lower():
field.advanced = True
field.required = False
field.show = False
# If the field.name contains api or api and key, then it might be an api key
# other conditions are to make sure that it is not an input or output variable
if "api" in key.lower() and "key" in key.lower():

View file

@ -101,6 +101,108 @@ class PythonFunctionNode(FrontendNode):
return super().to_dict()
class MidJourneyPromptChainNode(FrontendNode):
name: str = "MidJourneyPromptChain"
template: Template = Template(
type_name="MidJourneyPromptChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
],
)
description: str = "MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"MidJourneyPromptChain",
]
class TimeTravelGuideChainNode(FrontendNode):
name: str = "TimeTravelGuideChain"
template: Template = Template(
type_name="TimeTravelGuideChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
],
)
description: str = "Time travel guide chain to be used in the flow."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"TimeTravelGuideChain",
"Chain",
"ConversationChain",
]
class SeriesCharacterChainNode(FrontendNode):
name: str = "SeriesCharacterChain"
template: Template = Template(
type_name="SeriesCharacterChain",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="character",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="series",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
],
)
description: str = "SeriesCharacterChain is a chain you can use to have a conversation with a character from a series." # noqa
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"SeriesCharacterChain",
"function",
]
class ToolNode(FrontendNode):
name: str = "Tool"
template: Template = Template(
@ -216,7 +318,7 @@ class InitializeAgentNode(FrontendNode):
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"]
base_classes: list[str] = ["AgentExecutor", "function"]
def to_dict(self):
return super().to_dict()
@ -418,17 +520,29 @@ class ChainFrontendNode(FrontendNode):
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.advanced = False
if "key" in field.name:
field.password = False
field.show = False
if field.name in ["input_key", "output_key"]:
field.required = True
field.show = True
field.advanced = True
# Separated for possible future changes
if field.name == "prompt":
if field.name == "prompt" and field.value is None:
# if no prompt is provided, use the default prompt
field.required = False
field.show = True
field.advanced = False
if field.name == "memory":
field.required = False
field.show = True
field.advanced = False
if field.name == "verbose":
field.required = False
field.show = True
field.advanced = True
class LLMFrontendNode(FrontendNode):
@ -438,7 +552,7 @@ class LLMFrontendNode(FrontendNode):
"huggingfacehub_api_token": "HuggingFace Hub API Token",
}
FrontendNode.format_field(field, name)
SHOW_FIELDS = ["repo_id", "task", "model_kwargs"]
SHOW_FIELDS = ["repo_id"]
if field.name in SHOW_FIELDS:
field.show = True
@ -448,14 +562,21 @@ class LLMFrontendNode(FrontendNode):
# Required should be False to support
# loading the API key from environment variables
field.required = False
field.advanced = False
if field.name == "task":
field.required = True
field.show = True
field.is_list = True
field.options = ["text-generation", "text2text-generation"]
field.advanced = True
if display_name := display_names_dict.get(field.name):
field.display_name = display_name
if field.name == "model_kwargs":
field.field_type = "code"
field.advanced = True
field.show = True
elif field.name in ["model_name", "temperature"]:
field.advanced = False
field.show = True

View file

@ -1,3 +1,4 @@
from functools import wraps
import importlib
import inspect
import re
@ -301,3 +302,15 @@ def update_verbose(d: dict, new_value: bool) -> dict:
elif k == "verbose":
d[k] = new_value
return d
def sync_to_async(func):
"""
Decorator to convert a sync function to an async function.
"""
@wraps(func)
async def async_wrapper(*args, **kwargs):
return func(*args, **kwargs)
return async_wrapper

View file

@ -1,8 +0,0 @@
#! /bin/bash
poetry remove langchain
docker build -t logspace/backend_build -f build.Dockerfile .
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker run -p 5003:80 -d ibiscp/langflow:$VERSION
poetry add --editable ../../../langchain

View file

@ -1,5 +0,0 @@
FROM node:14-alpine
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build

View file

@ -1,11 +0,0 @@
#! /bin/bash
# Read the contents of the JSON file
json=$(cat package.json)
# Extract the value of the "version" field using jq
VERSION=$(echo "$json" | jq -r '.version')
docker build -t logspace/frontend_build -f build.Dockerfile .
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow_frontend:$VERSION .
docker push ibiscp/langflow_frontend:$VERSION

View file

@ -25,13 +25,14 @@
"ace-builds": "^1.16.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.3.2",
"base64-js": "^1.5.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.2",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"react-laag": "^2.0.5",
"react-router-dom": "^6.8.1",
"react-scripts": "5.0.1",
@ -5862,6 +5863,25 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@ -15040,9 +15060,9 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
"node_modules/react-icons": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
"peerDependencies": {
"react": "*"
}

View file

@ -20,13 +20,14 @@
"ace-builds": "^1.16.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.3.2",
"base64-js": "^1.5.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.2",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"react-laag": "^2.0.5",
"react-router-dom": "^6.8.1",
"react-scripts": "5.0.1",
@ -37,7 +38,7 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"start": "NODE_ENV=development react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
@ -60,5 +61,5 @@
"last 1 safari version"
]
},
"proxy": "http://backend:7860"
}
"proxy": "http://127.0.0.1:7860"
}

View file

@ -1,4 +1,4 @@
import { TrashIcon } from "@heroicons/react/24/outline";
import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/24/outline";
import {
classNames,
nodeColors,
@ -7,10 +7,13 @@ import {
} from "../../utils";
import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext";
import { useContext, useRef } from "react";
import { useContext, useState, useEffect, useRef } from "react";
import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext";
import NodeModal from "../../modals/NodeModal";
import { useCallback } from "react";
import { TabsContext } from "../../contexts/tabsContext";
export default function GenericNode({
data,
selected,
@ -21,7 +24,54 @@ export default function GenericNode({
const { setErrorData } = useContext(alertContext);
const showError = useRef(true);
const { types, deleteNode } = useContext(typesContext);
const { openPopUp } = useContext(PopUpContext);
const Icon = nodeIcons[types[data.type]];
const [validationStatus, setValidationStatus] = useState("idle");
// State for outline color
const [isValid, setIsValid] = useState(false);
const {save} = useContext(TabsContext)
const { reactFlowInstance } = useContext(typesContext);
const [params, setParams] = useState([]);
console.log();
useEffect(() => {
if (reactFlowInstance) {
setParams(Object.values(reactFlowInstance.toObject()));
}
}, [save]);
useEffect(() => {
try {
fetch(`/validate/node/${data.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reactFlowInstance.toObject()),
}).then((response) => {
console.log(response.status, response.body);
if (response.status === 200) {
setValidationStatus("success");
} else if (response.status === 500) {
setValidationStatus("error");
}
});
} catch (error) {
console.error("Error validating node:", error);
setValidationStatus("error");
}
}, [params]);
useEffect(() => {
if (validationStatus === "success") {
setIsValid(true);
} else {
setIsValid(false);
}
}, [validationStatus]);
if (!Icon) {
if (showError.current) {
setErrorData({
@ -34,9 +84,11 @@ export default function GenericNode({
deleteNode(data.id);
return;
}
return (
<div
className={classNames(
isValid ? "animate-pulse-green" : "border-red-outline",
selected ? "border border-blue-500" : "border dark:border-gray-700",
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
)}
@ -51,17 +103,46 @@ export default function GenericNode({
/>
<div className="truncate">{data.type}</div>
</div>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button>
<div className="flex gap-3">
<button
className="relative"
onClick={(event) => {
event.preventDefault();
openPopUp(<NodeModal data={data} />);
}}
>
<div className=" absolute text-red-600 -top-2 -right-1">
{Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced &&
data.node.template[t].required
)
? " *"
: ""}
</div>
<Cog6ToothIcon
className={classNames(
Object.keys(data.node.template).some(
(t) => data.node.template[t].advanced && data.node.template[t].show
)
? ""
: "hidden",
"w-6 h-6 dark:text-gray-500 hover:animate-spin"
)}
></Cog6ToothIcon>
</button>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button>
</div>
</div>
<div className="w-full h-full py-5">
<div className="w-full text-gray-500 px-5 text-sm">
<div className="w-full text-gray-500 px-5 pb-3 text-sm">
{data.node.description}
</div>
@ -70,13 +151,15 @@ export default function GenericNode({
.filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => (
<div key={idx}>
{idx === 0 ? (
{/* {idx === 0 ? (
<div
className={classNames(
"px-5 py-2 mt-2 dark:text-white text-center",
Object.keys(data.node.template).filter(
(key) =>
!key.startsWith("_") && data.node.template[key].show
!key.startsWith("_") &&
data.node.template[key].show &&
!data.node.template[key].advanced
).length === 0
? "hidden"
: ""
@ -86,8 +169,8 @@ export default function GenericNode({
</div>
) : (
<></>
)}
{data.node.template[t].show ? (
)} */}
{data.node.template[t].show && !data.node.template[t].advanced ? (
<ParameterComponent
data={data}
color={
@ -117,9 +200,17 @@ export default function GenericNode({
)}
</div>
))}
<div className="px-5 py-2 mt-2 dark:text-white text-center">
Output
<div
className={classNames(
Object.keys(data.node.template).length < 1 ? "hidden" : "",
"w-full flex justify-center"
)}
>
{" "}
</div>
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
Output
</div> */}
<ParameterComponent
data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}

View file

@ -1,15 +1,23 @@
import { XCircleIcon, XMarkIcon, InformationCircleIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
import {
XCircleIcon,
XMarkIcon,
InformationCircleIcon,
CheckCircleIcon,
} from "@heroicons/react/24/outline";
import { Link } from "react-router-dom";
import { Transition } from "@headlessui/react";
import { useState } from "react";
import { SingleAlertComponentType } from "../../../../types/alerts";
export default function SingleAlert({ dropItem, removeAlert}:SingleAlertComponentType) {
const [show, setShow] = useState(true);
const type = dropItem.type;
export default function SingleAlert({
dropItem,
removeAlert,
}: SingleAlertComponentType) {
const [show, setShow] = useState(true);
const type = dropItem.type;
return (
<Transition
return (
<Transition
className="relative"
show={show}
appear={true}
@ -20,115 +28,127 @@ export default function SingleAlert({ dropItem, removeAlert}:SingleAlertComponen
leaveFrom={"transform translate-x-0"}
leaveTo={"transform translate-x-[-100%]"}
>
{type === "error"?
<div className="flex bg-red-50 rounded-md p-3 mb-2 mx-2" key={dropItem.id}>
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
{type === "error" ? (
<div
className="flex bg-red-50 rounded-md p-3 mb-2 mx-2"
key={dropItem.id}
>
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{dropItem.title}
</h3>
{dropItem.list ? (
<div className="mt-2 text-sm text-red-700">
<ul className="list-disc space-y-1 pl-5">
{dropItem.list.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
) : (
<></>
)}
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-red-50 p-1.5 text-red-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{dropItem.title}
</h3>
{dropItem.list ? (
<div className="mt-2 text-sm text-red-700">
<ul className="list-disc space-y-1 pl-5">
{dropItem.list.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
</div>
</div>
) : type === "notice" ? (
<div
className="flex rounded-md bg-blue-50 p-3 mb-2 mx-2"
key={dropItem.id}
>
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-blue-400"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700">{dropItem.title}</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
{dropItem.link ? (
<Link
to={dropItem.link}
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
>
Details
</Link>
) : (
<></>
)}
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-red-50 p-1.5 text-red-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
:(type === "notice" ?
<div className="flex rounded-md bg-blue-50 p-3 mb-2 mx-2" key={dropItem.id}>
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-blue-400"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700">{dropItem.title}</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
{dropItem.link ? (
<Link
to={dropItem.link}
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
>
Details
</Link>
) : (
<></>
)}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
) : (
<div
className="flex bg-green-50 p-3 mb-2 mx-2 rounded-md"
key={dropItem.id}
>
<div className="flex-shrink-0">
<CheckCircleIcon
className="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">
{dropItem.title}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-green-50 p-1.5 text-green-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
:
<div className="flex bg-green-50 p-3 mb-2 mx-2 rounded-md" key={dropItem.id}>
<div className="flex-shrink-0">
<CheckCircleIcon
className="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">
{dropItem.title}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-green-50 p-1.5 text-green-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
)
}
</div>
)}
</Transition>
)
}
);
}

View file

@ -5,73 +5,62 @@ import { TrashIcon } from "@heroicons/react/24/outline";
import SingleAlert from "./components/singleAlertComponent";
import { AlertDropdownType } from "../../types/alerts";
import { PopUpContext } from "../../contexts/popUpContext";
import { useOnClickOutside } from "../hooks/useOnClickOutside";
export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null);
const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
componentRef.current &&
!componentRef.current.contains(event.target as Node)
) {
closePopUp();
}
}
// Use the custom hook
useOnClickOutside(componentRef, () => {
closePopUp();
});
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
const {
notificationList,
clearNotificationList,
removeFromNotificationList,
} = useContext(alertContext);
// Cleanup the event listener when the component is unmounted
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [componentRef]);
const {
notificationList,
clearNotificationList,
removeFromNotificationList,
} = useContext(alertContext);
return (
<div
ref={componentRef}
className="z-10 py-3 pb-4 rounded-md bg-white ring-1 ring-black ring-opacity-5 shadow-lg focus:outline-none overflow-hidden w-[16rem] h-[28rem] flex flex-col"
>
<div className="flex pl-3 flex-row justify-between text-md font-medium text-gray-800">
Notifications
<div className="flex gap-2 pr-3 ">
<button
className="hover:text-red-500"
onClick={() => {
closePopUp();
setTimeout(clearNotificationList, 100);
}}
>
<TrashIcon className="w-[1.1rem] h-[1.1rem]" />
</button>
<button className="hover:text-red-500" onClick={closePopUp}>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="mt-2 flex flex-col overflow-y-scroll w-full h-full scrollbar-hide">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="h-full w-full pb-16 text-gray-500 flex justify-center items-center">
No new notifications
</div>
)}
</div>
</div>
);
return (
<div
ref={componentRef}
className="z-10 py-3 pb-4 rounded-md bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 shadow-lg focus:outline-none overflow-hidden w-[16rem] h-[28rem] flex flex-col"
>
<div className="flex pl-3 flex-row justify-between text-md font-medium text-gray-800 dark:text-gray-200">
Notifications
<div className="flex gap-2 pr-3 ">
<button
className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500"
onClick={() => {
closePopUp();
setTimeout(clearNotificationList, 100);
}}
>
<TrashIcon className="w-[1.1rem] h-[1.1rem]" />
</button>
<button
className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500"
onClick={closePopUp}
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="mt-2 flex flex-col overflow-y-scroll w-full h-full scrollbar-hide text-gray-900 dark:text-gray-300">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="h-full w-full pb-16 text-gray-500 dark:text-gray-500 flex justify-center items-center">
No new notifications
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,33 @@
import { useEffect } from "react";
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or its children
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
// Attach the listener to the document
document.addEventListener("mousedown", listener, { passive: true });
// Attach the listener to the react-flow instance
const reactFlowContainer = document.querySelector(".react-flow");
if (reactFlowContainer) {
reactFlowContainer.addEventListener("mousedown", listener, {
passive: true,
});
}
// Clean up the listener when the component is unmounted
return () => {
document.removeEventListener("mousedown", listener);
if (reactFlowContainer) {
reactFlowContainer.removeEventListener("mousedown", listener);
}
};
}, [ref, handler]); // Rerun only if ref or handler changes
}

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="271px" height="271px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<defs>
<filter id="ldio-978hsxudfzl-filter" x="-100%" y="-100%" width="300%" height="300%" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="3.6"></feGaussianBlur>
<feComponentTransfer result="cutoff">
<feFuncA type="table" tableValues="0 0 0 0 0 0 1 1 1 1 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
<g filter="url(#ldio-978hsxudfzl-filter)"><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#2edbb5">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="5s" repeatCount="indefinite" begin="-0.2s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="5s" repeatCount="indefinite" begin="0s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#1d99ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="2.5s" repeatCount="indefinite" begin="-0.15000000000000002s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="2.5s" repeatCount="indefinite" begin="-0.05s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#4f41ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#8400ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.25s" repeatCount="indefinite" begin="-0.05s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.25s" repeatCount="indefinite" begin="-0.15000000000000002s"></animateTransform>
</g>
</g></g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,37 @@
import { Transition } from "@headlessui/react";
import { Bars3CenterLeftIcon, ChatBubbleBottomCenterTextIcon } from "@heroicons/react/24/outline";
import { nodeColors } from "../../../utils";
import { PopUpContext } from "../../../contexts/popUpContext";
import { useContext } from "react";
import ChatModal from "../../../modals/chatModal";
export default function ChatTrigger({open, setOpen,flow}){
const {openPopUp} = useContext(PopUpContext)
return(<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="absolute bottom-2 right-3">
<div style={{backgroundColor:nodeColors['chat']}} className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full dark:bg-gray-800 dark:border-gray-600 dark:text-white">
<button
onClick={() => {
setOpen(true);
}}
>
<div className="flex gap-3 items-center">
<ChatBubbleBottomCenterTextIcon
className="h-6 w-6 mt-1"
style={{ color: "white" }}
/>
</div>
</button>
</div>
</div>
</Transition>)
}

View file

@ -1,291 +1,32 @@
import { Transition } from "@headlessui/react";
import {
Bars3CenterLeftIcon,
LockClosedIcon,
PaperAirplaneIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import {
MouseEventHandler,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { sendAll } from "../../controllers/API";
import { alertContext } from "../../contexts/alertContext";
import { classNames, nodeColors, snakeToNormalCase } from "../../utils";
import { TabsContext } from "../../contexts/tabsContext";
import { ChatType } from "../../types/chat";
import ChatMessage from "./chatMessage";
import { NodeType } from "../../types/flow";
import { useEffect, useRef, useState } from "react";
import { ChatMessageType, ChatType } from "../../types/chat";
import ChatTrigger from "./chatTrigger";
import ChatModal from "../../modals/chatModal";
const _ = require("lodash");
export default function Chat({ flow, reactFlowInstance }: ChatType) {
const { updateFlow, lockChat, setLockChat, flows, tabIndex } =
useContext(TabsContext);
const [saveChat, setSaveChat] = useState(false);
const [open, setOpen] = useState(true);
const [chatValue, setChatValue] = useState("");
const [chatHistory, setChatHistory] = useState(flow.chat);
const { setErrorData, setNoticeData } = useContext(alertContext);
const addChatHistory = (
message: string,
isSend: boolean,
thought?: string
) => {
let tabsChange = false;
setChatHistory((old) => {
let newChat = _.cloneDeep(old);
if (JSON.stringify(flow.chat) !== JSON.stringify(old)) {
tabsChange = true;
return old;
}
if (thought) {
newChat.push({ message, isSend, thought });
} else {
newChat.push({ message, isSend });
}
return newChat;
});
if (tabsChange) {
if (thought) {
updateFlow({
..._.cloneDeep(flow),
chat: [...flow.chat, { isSend, message, thought }],
});
} else {
updateFlow({
..._.cloneDeep(flow),
chat: [...flow.chat, { isSend, message }],
});
}
}
setSaveChat((chat) => !chat);
};
useEffect(() => {
updateFlow({ ..._.cloneDeep(flow), chat: chatHistory });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveChat]);
useEffect(() => {
setChatHistory(flow.chat);
}, [flow]);
useEffect(() => {
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
}, [chatHistory]);
function validateNode(n: NodeType): Array<string> {
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
setNoticeData({
title:
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
});
return [];
}
const {
type,
node: { template },
} = n.data;
return Object.keys(template).reduce(
(errors: Array<string>, t) =>
errors.concat(
(template[t].required && template[t].show) &&
(!template[t].value || template[t].value === "") &&
!reactFlowInstance
.getEdges()
.some(
(e) =>
e.targetHandle.split("|")[1] === t &&
e.targetHandle.split("|")[2] === n.id
)
? [
`${type} is missing ${
template.display_name
? template.display_name
: snakeToNormalCase(template[t].name)
}.`,
]
: []
),
[] as string[]
);
}
function validateNodes() {
return reactFlowInstance
.getNodes()
.flatMap((n: NodeType) => validateNode(n));
}
const ref = useRef(null);
function sendMessage() {
if (chatValue !== "") {
let nodeValidationErrors = validateNodes();
if (nodeValidationErrors.length === 0) {
setLockChat(true);
let message = chatValue;
setChatValue("");
addChatHistory(message, true);
sendAll({
...reactFlowInstance.toObject(),
message,
chatHistory,
name: flow.name,
description: flow.description,
})
.then((r) => {
addChatHistory(r.data.result, false, r.data.thought);
setLockChat(false);
})
.catch((error) => {
setErrorData({
title: error.message ?? "Unknown Error",
list: [error.response.data.detail],
});
setLockChat(false);
let lastMessage;
setChatHistory((chatHistory) => {
let newChat = chatHistory;
lastMessage = newChat.pop().message;
return newChat;
});
setChatValue(lastMessage);
});
} else {
setErrorData({
title: "Oops! Looks like you missed some required information:",
list: nodeValidationErrors,
});
}
} else {
setErrorData({
title: "Error sending message",
list: ["The message cannot be empty."],
});
}
}
function clearChat() {
setChatHistory([]);
updateFlow({ ..._.cloneDeep(flow), chat: [] });
}
return (
<>
<Transition
show={open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="w-[340px] absolute bottom-0 right-1">
<div className="border dark:border-gray-700 h-full rounded-xl rounded-b-none bg-white dark:bg-gray-800 shadow">
<div
onClick={() => {
setOpen(false);
}}
className="flex justify-between cursor-pointer items-center px-5 py-2 border-b dark:border-b-gray-700"
>
<div className="flex gap-3 text-lg dark:text-white font-medium items-center">
<Bars3CenterLeftIcon
className="h-5 w-5 mt-1"
style={{ color: nodeColors["chat"] }}
/>
Chat
</div>
<button
className="hover:text-blue-500 dark:text-white"
onClick={(e) => {
e.stopPropagation();
clearChat();
}}
>
Clear
</button>
</div>
<div className="w-full h-[400px] flex gap-3 mb-auto overflow-y-auto scrollbar-hide flex-col bg-gray-50 dark:bg-gray-900 p-3 py-5">
{chatHistory.map((c, i) => (
<ChatMessage chat={c} key={i} />
))}
<div ref={ref}></div>
</div>
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex items-center justify-between p-3">
<div className="relative w-full mt-1 rounded-md shadow-sm">
<input
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat) {
sendMessage();
}
}}
type="text"
disabled={lockChat}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
className={classNames(
lockChat ? "bg-gray-500 text-white" : "dark:bg-gray-700",
"form-input block w-full rounded-md border-gray-300 dark:border-gray-600 dark:text-white pr-10 sm:text-sm"
)}
placeholder={"Send a message..."}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<LockClosedIcon
className="h-5 w-5 text-gray-400 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<PaperAirplaneIcon
className="h-5 w-5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</div>
</div>
</div>
</div>
</Transition>
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="absolute bottom-0 right-1">
<div className="border flex justify-center align-center py-1 px-3 rounded-xl rounded-b-none bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-white shadow">
<button
onClick={() => {
setOpen(true);
}}
>
<div className="flex gap-3 items-center">
<Bars3CenterLeftIcon
className="h-6 w-6 mt-1"
style={{ color: nodeColors["chat"] }}
/>
Chat
</div>
</button>
</div>
</div>
</Transition>
</>
);
export default function Chat({ flow }: ChatType) {
const [open, setOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
(event.key === "K" || event.key === "k") &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
setOpen((oldState) => !oldState);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<>
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
<ChatTrigger open={open} setOpen={setOpen} flow={flow} />
</>
);
}

View file

@ -19,9 +19,24 @@ export default function CodeAreaComponent({
}
}, [disabled, onChange]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"}>
<div
className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
}
>
<div className="w-full flex items-center gap-3">
<span
onClick={() => {
openPopUp(
<CodeAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")

View file

@ -4,18 +4,27 @@ import { Fragment, useState } from "react";
import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils";
export default function Dropdown({value, options, onSelect}:DropDownComponentType) {
let [internalValue,setInternalValue] = useState(value===""||!value?"Choose an option":value)
export default function Dropdown({
value,
options,
onSelect,
}: DropDownComponentType) {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
);
return (
<>
<Listbox value={internalValue} onChange={(value)=>{
setInternalValue(value)
onSelect(value)
}}>
<Listbox
value={internalValue}
onChange={(value) => {
setInternalValue(value);
onSelect(value);
}}
>
{({ open }) => (
<>
<div className="relative mt-1">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white dark:bg-gray-900 py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<span className="block w-max truncate">{internalValue}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
@ -32,14 +41,16 @@ export default function Dropdown({value, options, onSelect}:DropDownComponentTyp
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full dark:bg-gray-800 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.map((option, id) => (
<Listbox.Option
key={id}
className={({ active }) =>
classNames(
active ? "text-white bg-indigo-600" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
active
? "text-white bg-indigo-600 dark:bg-indigo-500"
: "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9 dark:text-gray-300 dark:bg-gray-800"
)
}
value={option}

View file

@ -69,6 +69,7 @@ export default function InputFileComponent({
>
<div className="w-full flex items-center gap-3">
<span
onClick={handleButtonClick}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")

View file

@ -6,30 +6,61 @@ import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components";
import PromptAreaModal from "../../modals/promptModal";
export default function PromptAreaComponent({ value, onChange, disabled }:TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext);
useEffect(() => {
if (disabled) {
setMyValue("");
onChange("");
}
}, [disabled, onChange]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed w-full" : " w-full"}>
<div className="w-full flex items-center gap-3">
<span
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>
{myValue !== "" ? myValue : 'Text empty'}
</span>
<button onClick={()=>{openPopUp(<PromptAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}>
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600" />
</button>
</div>
</div>
);
export default function PromptAreaComponent({
value,
onChange,
disabled,
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext);
useEffect(() => {
if (disabled) {
setMyValue("");
onChange("");
}
}, [disabled, onChange]);
return (
<div
className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : " w-full"
}
>
<div className="w-full flex items-center gap-3">
<span
onClick={() => {
openPopUp(
<PromptAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>
{myValue !== "" ? myValue : "Text empty"}
</span>
<button
onClick={() => {
openPopUp(
<PromptAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
>
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600" />
</button>
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components";
@ -17,7 +16,7 @@ export default function TextAreaComponent({ value, onChange, disabled }:TextArea
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div className="w-full flex items-center gap-3">
<span
<span onClick={()=>{openPopUp(<TextAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")

View file

@ -11,17 +11,15 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
return (
<>
<DarkProvider>
<LocationProvider>
<AlertProvider>
<TabsProvider>
<PopUpProvider>
<TypesProvider>
{children}
</TypesProvider>
</PopUpProvider>
</TabsProvider>
</AlertProvider>
</LocationProvider>
<TypesProvider>
<LocationProvider>
<AlertProvider>
<TabsProvider>
<PopUpProvider>{children}</PopUpProvider>
</TabsProvider>
</AlertProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</>
);

View file

@ -1,33 +1,33 @@
import { createContext } from "react";
import React, { useState } from "react";
//context to set JSX element on the DOM
// context to set JSX element on the DOM
export const PopUpContext = createContext({
openPopUp: (popUpElement: JSX.Element) => {},
closePopUp: () => {},
closePopUp: () => {},
});
interface PopUpProviderProps {
children: React.ReactNode;
children: React.ReactNode;
}
const PopUpProvider = ({ children }: PopUpProviderProps) => {
const [popUpElement, setPopUpElement] = useState<JSX.Element | null>(null);
const [popUpElements, setPopUpElements] = useState<JSX.Element[]>([]);
const openPopUp = (element: JSX.Element) => {
setPopUpElement(element);
};
const openPopUp = (element: JSX.Element) => {
setPopUpElements(prevPopUps => [element, ...prevPopUps]);
};
const closePopUp = () => {
setPopUpElement(null);
};
const closePopUp = () => {
setPopUpElements(prevPopUps => prevPopUps.slice(1));
};
return (
<PopUpContext.Provider value={{ openPopUp, closePopUp }}>
{children}
{popUpElement}
</PopUpContext.Provider>
);
return (
<PopUpContext.Provider value={{ openPopUp, closePopUp }}>
{children}
{popUpElements[0]}
</PopUpContext.Provider>
);
};
export default PopUpProvider;

View file

@ -1,11 +1,21 @@
import { createContext, useEffect, useState, useRef, ReactNode, useContext } from "react";
import {
createContext,
useEffect,
useState,
useRef,
ReactNode,
useContext,
} from "react";
import { FlowType } from "../types/flow";
import { TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase } from "../utils";
import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject } from "../utils";
import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext";
import { TemplateVariableType } from "../types/api";
const { v4: uuidv4 } = require('uuid');
const TabsContextInitialValue: TabsContextType = {
save:()=>{},
save: () => {},
tabIndex: 0,
setTabIndex: (index: number) => {},
flows: [],
@ -13,11 +23,9 @@ const TabsContextInitialValue: TabsContextType = {
addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0,
downloadFlow: (flow:FlowType) => {},
downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {},
lockChat: false,
setLockChat:(prevState:boolean)=>{},
hardReset:()=>{},
hardReset: () => {},
};
export const TabsContext = createContext<TabsContextType>(
@ -25,51 +33,63 @@ export const TabsContext = createContext<TabsContextType>(
);
export function TabsProvider({ children }: { children: ReactNode }) {
const {setNoticeData} = useContext(alertContext)
const { setNoticeData } = useContext(alertContext);
const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(0);
const [lockChat, setLockChat] = useState(false);
const [id, setId] = useState("");
const { templates } = useContext(typesContext);
const newNodeId = useRef(0);
function incrementNodeId() {
newNodeId.current = newNodeId.current + 1;
return newNodeId.current;
}
function save(){
function save() {
if (flows.length !== 0)
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
}
useEffect(() => {
//save tabs locally
save()
save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => {
//get tabs locally saved
let cookie = window.localStorage.getItem("tabsData");
if (cookie) {
let cookieObject = JSON.parse(cookie);
if (cookie && Object.keys(templates).length > 0) {
let cookieObject: LangFlowState = JSON.parse(cookie);
cookieObject.flows.forEach((flow) => {
flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length>0) {
node.data.node.template = updateObject(
node.data.node.template as TemplateVariableType,
templates[node.data.type][
"template"
] as unknown as TemplateVariableType
);
}
});
});
setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
}
}, []);
function hardReset(){
newNodeId.current=0;
setTabIndex(0);setFlows([]);setId(0);
}, [templates]);
function hardReset() {
newNodeId.current = 0;
setTabIndex(0);
setFlows([]);
setId("");
}
/**
* Downloads the current flow as a JSON file
*/
function downloadFlow(flow:FlowType) {
function downloadFlow(flow: FlowType) {
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flow)
@ -82,7 +102,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({title:"Warning: Critical data,JSON file may including API keys."})
setNoticeData({
title: "Warning: Critical data,JSON file may including API keys.",
});
}
/**
@ -103,7 +125,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// read the file as text
file.text().then((text) => {
// parse the text into a JSON object
addFlow(JSON.parse(text));
let flow: FlowType = JSON.parse(text);
flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length>0) {
node.data.node.template = updateObject(
node.data.node.template as TemplateVariableType,
templates[node.data.type][
"template"
] as unknown as TemplateVariableType
);
}
});
addFlow();
});
}
};
@ -139,19 +173,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
const description = flow?.description?flow.description:""
const description = flow?.description ? flow.description : "";
// Create a new flow with a default name if no flow is provided.
let newFlow: FlowType = {
description,
name: flow?.name??"New Flow",
name: flow?.name ?? "New Flow",
id: id.toString(),
data,
chat: flow ? flow.chat : [],
};
// Increment the ID counter.
setId((old) => old + 1);
setId(uuidv4());
// Add the new flow to the list of flows.
setFlows((prevState) => {
@ -171,10 +204,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description??""
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
newFlows[index].chat = newFlow.chat;
}
return newFlows;
});
@ -185,8 +217,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
value={{
save,
hardReset,
lockChat,
setLockChat,
tabIndex,
setTabIndex,
flows,

View file

@ -1,6 +1,8 @@
import { createContext, ReactNode, useState } from "react";
import { createContext, ReactNode, useEffect, useState } from "react";
import { Node} from "reactflow";
import { typesContextType } from "../types/typesContext";
import { getAll } from "../controllers/API";
import { APIKindType } from "../types/api";
//context to share types adn functions from nodes to flow
@ -10,6 +12,10 @@ const initialValue:typesContextType = {
deleteNode: () => {},
types: {},
setTypes: () => {},
templates: {},
setTemplates: () => {},
data:{},
setData:()=>{}
};
export const typesContext = createContext<typesContextType>(initialValue);
@ -17,6 +23,42 @@ export const typesContext = createContext<typesContextType>(initialValue);
export function TypesProvider({ children }:{children:ReactNode}) {
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
// Update the state of the component with the retrieved data.
setData(result.data);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType)=>{
acc[c] = result.data[curr][c]
})
return acc;
},{})
);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
function deleteNode(idx:string) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((n:Node) => n.id !== idx)
@ -31,6 +73,10 @@ export function TypesProvider({ children }:{children:ReactNode}) {
reactFlowInstance,
setReactFlowInstance,
deleteNode,
setTemplates,
templates,
data,
setData
}}
>
{children}

View file

@ -1,40 +1,43 @@
import { PromptTypeAPI, errorsTypeAPI } from './../../types/api/index';
import { APIObjectType, sendAllProps } from '../../types/api/index';
import { PromptTypeAPI, errorsTypeAPI } from "./../../types/api/index";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import axios, { AxiosResponse } from "axios";
import { FlowType } from '../../types/flow';
import { FlowType } from "../../types/flow";
export async function getAll():Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/all`);
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/all`);
}
export async function sendAll(data:sendAllProps) {
return await axios.post(`/predict`, data);
export async function sendAll(data: sendAllProps) {
return await axios.post(`/predict`, data);
}
export async function checkCode(code:string):Promise<AxiosResponse<errorsTypeAPI>>{
return await axios.post('/validate/code',{code})
export async function checkCode(
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await axios.post("/validate/code", { code });
}
export async function checkPrompt(template:string):Promise<AxiosResponse<PromptTypeAPI>>{
return await axios.post('/validate/prompt',{template})
export async function checkPrompt(
template: string
): Promise<AxiosResponse<PromptTypeAPI>> {
return await axios.post("/validate/prompt", { template });
}
export async function getExamples(): Promise<FlowType[]> {
const url = 'https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples';
const response = await axios.get(url);
const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith('.json');
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url);
return contentResponse.data;
});
const contents = await Promise.all(contentsPromises);
return contents;
}
const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
const response = await axios.get(url);
const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith(".json");
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url);
return contentResponse.data;
});
const contents = await Promise.all(contentsPromises);
return contents;
}

View file

@ -0,0 +1,166 @@
import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext";
import InputListComponent from "../../../../components/inputListComponent";
import Dropdown from "../../../../components/dropdownComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import InputComponent from "../../../../components/inputComponent";
import ToggleComponent from "../../../../components/toggleComponent";
import FloatComponent from "../../../../components/floatComponent";
import IntComponent from "../../../../components/intComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import { classNames } from "../../../../utils";
export default function ModalField({ data, title, required, id, name, type }) {
const { save } = useContext(TabsContext);
const [enabled, setEnabled] = useState(
data.node.template[name]?.value ?? false
);
const display =
type === "str" ||
type === "int" ||
type === "prompt" ||
type === "bool" ||
type === "float" ||
type === "file" ||
type === "code";
return (
<div
className={classNames(
"flex flex-row w-full items-center justify-between",
display ? "" : "hidden"
)}
>
{display && (
<div>
<span className="mx-2">{title}</span>
<span className="text-red-600">{required ? " *" : ""}</span>
</div>
)}
{type === "str" && !data.node.template[name].options ? (
<div className="w-1/2">
{data.node.template[name].list ? (
<InputListComponent
disabled={false}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={(t: string[]) => {
data.node.template[name].value = t;
save();
}}
/>
) : data.node.template[name].multiline ? (
<TextAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
) : (
<InputComponent
disabled={false}
password={data.node.template[name].password ?? false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
)}
</div>
) : type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleComponent
disabled={false}
enabled={enabled}
setEnabled={(t) => {
data.node.template[name].value = t;
setEnabled(t);
save();
}}
/>
</div>
) : type === "float" ? (
<div className="w-1/2">
<FloatComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "str" && data.node.template[name].options ? (
<div className="w-1/2">
<Dropdown
options={data.node.template[name].options}
onSelect={(newValue) => (data.node.template[name].value = newValue)}
value={data.node.template[name].value ?? "Choose an option"}
></Dropdown>
</div>
) : type === "int" ? (
<div className="w-1/2">
<IntComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "file" ? (
<div className="w-1/2">
<InputFileComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
}}
fileTypes={data.node.template[name].fileTypes}
suffixes={data.node.template[name].suffixes}
onFileChange={(t: string) => {
data.node.template[name].content = t;
save();
}}
></InputFileComponent>
</div>
) : type === "prompt" ? (
<div className="w-1/2">
<PromptAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "code" ? (
<div className="w-1/2">
<CodeAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : (
<div className="hidden"></div>
)}
</div>
);
}

View file

@ -0,0 +1,144 @@
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { NodeDataType } from "../../types/flow";
import { nodeColors, nodeIcons, snakeToNormalCase } from "../../utils";
import { typesContext } from "../../contexts/typesContext";
import ModalField from "./components/ModalField";
export default function NodeModal({ data }: { data: NodeDataType }) {
const [open, setOpen] = useState(true);
const { closePopUp } = useContext(PopUpContext);
const { types } = useContext(typesContext);
const ref = useRef();
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
const Icon = nodeIcons[types[data.type]];
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<Icon
className="w-10 mt-4 h-10 p-1 rounded"
style={{
color:
nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
{data.type}
</Dialog.Title>
</div>
</div>
<div className="h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
<div className="flex h-full w-full">
<div className="overflow-hidden px-4 sm:p-4 w-full rounded-lg bg-white dark:bg-gray-800 shadow">
<div className="flex flex-col h-full gap-5">
{
Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_"&& data.node.template[t].advanced && data.node.template[t].show)
.map((t: string, idx) => {
return (
<ModalField
key={idx}
data={data}
title={
data.node.template[t].display_name
? data.node.template[t].display_name
: data.node.template[t].name
? snakeToNormalCase(
data.node.template[t].name
)
: snakeToNormalCase(t)
}
required={data.node.template[t].required}
id={
data.node.template[t].type +
"|" +
t +
"|" +
data.id
}
name={t}
type={data.node.template[t].type}
/>
);
})
}
</div>
</div>
</div>
</div>
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
setModalOpen(false);
}}
>
Done
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -0,0 +1,55 @@
import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { classNames } from "../../../utils";
import { useRef } from "react";
export default function ChatInput({
lockChat,
chatValue,
sendMessage,
setChatValue,
}: {
lockChat: boolean;
chatValue: string;
sendMessage: Function;
setChatValue: Function;
}) {
const inputRef = useRef(null);
return (
<>
<textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
}
}}
ref={inputRef}
disabled={lockChat}
style={{resize: "none" }}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
className={classNames(
lockChat ? "bg-gray-500 text-white" : "dark:bg-gray-700",
"form-input block w-full custom-scroll h-10 rounded-md border-gray-300 dark:border-gray-600 dark:text-white pr-10 sm:text-sm"
)}
placeholder={"Send a message..."}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<LockClosedIcon
className="h-5 w-5 text-gray-400 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<PaperAirplaneIcon
className="h-5 w-5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</>
);
}

View file

@ -0,0 +1,82 @@
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import { ChatMessageType } from "../../../types/chat";
import { classNames } from "../../../utils";
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
import { UserIcon } from "@heroicons/react/24/solid";
import FileCard from "../fileComponent";
var Convert = require("ansi-to-html");
var convert = new Convert({ newline: true });
export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
const [hidden, setHidden] = useState(true);
return (
<div
className={classNames(
"w-full py-2 pl-2 flex",
chat.isSend ? "bg-white dark:bg-gray-800 " : "bg-gray-200 dark:bg-gray-700"
)}
>
<div
className={classNames(
"rounded-full w-8 h-8 flex items-center my-3 justify-center",
chat.isSend ? "bg-gray-900" : "bg-gray-200"
)}
>
{!chat.isSend && <img className="scale-150" src={AiIcon} />}
{chat.isSend && <UserIcon className="w-6 h-6 -mb-1 text-gray-200" />}
</div>
{!chat.isSend ? (
<div className="w-full text-start flex items-center">
<div className="w-full relative text-start inline-block text-gray-600 text-sm font-normal">
{hidden && chat.thought && chat.thought !== "" && (
<div
onClick={() => setHidden((prev) => !prev)}
className="absolute -top-1 -left-2 cursor-pointer"
>
<ChatBubbleOvalLeftEllipsisIcon className="w-5 h-5 animate-bounce dark:text-white" />
</div>
)}
{chat.thought && chat.thought !== "" && !hidden && (
<div
onClick={() => setHidden((prev) => !prev)}
className=" text-start inline-block rounded-md h-full border border-gray-300
bg-gray-100 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
dangerouslySetInnerHTML={{
__html: convert.toHtml(chat.thought),
}}
></div>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full px-4 pb-3 pt-3 pr-8">
<span className="dark:text-white">
{chat.message}
{chat.files && (
<div className="my-2 w-full">
{chat.files.map((file, index) => {
return (
<div key={index} className="my-2 w-full">
<FileCard
fileName={"Generated File"}
fileType={file.data_type}
content={file.data}
/>
</div>
);
})}
</div>
)}
</span>
</div>
</div>
</div>
) : (
<div className="w-full flex items-center">
<div className="text-start inline-block px-3 text-sm text-gray-600 dark:text-white">
{chat.message}
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,36 @@
import { CloudArrowDownIcon, DocumentIcon } from "@heroicons/react/24/outline";
import * as base64js from 'base64-js';
export default function FileCard({ fileName, content, fileType }) {
const handleDownload = () => {
const byteArray = new Uint8Array(base64js.toByteArray(content));
const blob = new Blob([byteArray], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName+".png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
<button
onClick={handleDownload}
className="bg-gray-100 shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
>
<div className="flex gap-2 text-current items-center w-full mr-2">
{" "}
<DocumentIcon className="w-8 h-8" />
<div className="flex flex-col items-start">
{" "}
<div className="truncate text-sm text-current">{fileName}</div>
<div className="truncate text-xs text-gray-500">{fileType}</div>
</div>
<CloudArrowDownIcon className="w-6 h-6 text-current ml-auto" />
</div>
</button>
);
}

View file

@ -0,0 +1,373 @@
import { Dialog, Transition } from "@headlessui/react";
import {
ChatBubbleOvalLeftEllipsisIcon,
LockClosedIcon,
PaperAirplaneIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { FlowType, NodeType } from "../../types/flow";
import { TabsContext } from "../../contexts/tabsContext";
import { alertContext } from "../../contexts/alertContext";
import { classNames, snakeToNormalCase } from "../../utils";
import { typesContext } from "../../contexts/typesContext";
import ChatMessage from "./chatMessage";
import { FaEraser } from "react-icons/fa";
import { sendAllProps } from "../../types/api";
import { ChatMessageType, ChatType } from "../../types/chat";
import ChatInput from "./chatInput";
const _ = require("lodash");
export default function ChatModal({
flow,
open,
setOpen,
}: {
open: boolean;
setOpen: Function;
flow: FlowType;
}) {
const [chatValue, setChatValue] = useState("");
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const { reactFlowInstance } = useContext(typesContext);
const { setErrorData, setNoticeData } = useContext(alertContext);
const [ws, setWs] = useState<WebSocket | null>(null);
const [lockChat, setLockChat] = useState(false);
const addChatHistory = (
message: string,
isSend: boolean,
thought?: string,
files?: Array<any>
) => {
setChatHistory((old) => {
let newChat = _.cloneDeep(old);
if (files) {
newChat.push({ message, isSend, files, thought });
} else if (thought) {
newChat.push({ message, isSend, thought });
} else {
newChat.push({ message, isSend });
}
return newChat;
});
};
function connectWS() {
console.log("conectou");
try {
const urlWs =
process.env.NODE_ENV === "development"
? `ws://localhost:7860/chat/${flow.id}`
: `wss://${window.location.host}/chat/${flow.id}`;
const newWs = new WebSocket(urlWs);
newWs.onopen = () => {
console.log("WebSocket connection established!");
};
newWs.onmessage = (event) => {
try {
setLockChat(false);
const data = JSON.parse(event.data);
console.log("Received data:", data);
//get chat history
if (Array.isArray(data)) {
console.log(data);
setChatHistory((_) => {
let newChatHistory: ChatMessageType[] = [];
data.forEach(
(chatItem: {
intermediate_steps?: "string";
is_bot: boolean;
message: string;
type: string;
files?: Array<any>;
}) => {
if (chatItem.message) {
newChatHistory.push(
chatItem.files
? {
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
files: chatItem.files,
}
: {
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
}
);
}
}
);
return newChatHistory;
});
}
if (data.type === "end") {
if (data.files) {
addChatHistory(
data.message,
false,
data.intermediate_steps,
data.files
);
} else {
addChatHistory(data.message, false, data.intermediate_steps);
}
}
if (data.type == "file") {
console.log(data);
}
} catch (error) {
if (event.data !== "Error: 1005") {
setErrorData({ title: event.data });
newWs.close();
connectWS();
}
}
};
newWs.onclose = (_) => {
if (open) {
setLockChat(false);
setTimeout(() => {
connectWS();
}, 1000);
}
};
newWs.onerror = (ev) => {
console.log(ev, "error");
};
setWs(newWs);
return newWs;
} catch {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"refresh the page",
"use a new flow tab",
"check if the backend is up",
],
});
}
}
useEffect(() => {
if (ws && (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING)) {
let newWs = connectWS();
return () => {
console.log("trigger");
newWs.close();
};
}
}, [lockChat]);
useEffect(() => {
let newWs = connectWS();
return () => {
console.log("trigger");
newWs.close();
};
}, []);
async function sendAll(data: sendAllProps) {
try {
if (ws) {
ws.send(JSON.stringify(data));
}
} catch (error) {
setErrorData({
title: "There was an erro sending the message",
list: [error.message],
});
setChatValue(data.message);
connectWS();
}
}
useEffect(() => {
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
}, [chatHistory]);
useEffect(() => {
if (ws && ws.readyState === ws.CLOSED) {
setLockChat(false);
}
}, [lockChat]);
function validateNode(n: NodeType): Array<string> {
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
setNoticeData({
title:
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
});
return [];
}
const {
type,
node: { template },
} = n.data;
return Object.keys(template).reduce(
(errors: Array<string>, t) =>
errors.concat(
template[t].required &&
template[t].show &&
(!template[t].value || template[t].value === "") &&
!reactFlowInstance
.getEdges()
.some(
(e) =>
e.targetHandle.split("|")[1] === t &&
e.targetHandle.split("|")[2] === n.id
)
? [
`${type} is missing ${
template.display_name
? template.display_name
: snakeToNormalCase(template[t].name)
}.`,
]
: []
),
[] as string[]
);
}
function validateNodes() {
return reactFlowInstance
.getNodes()
.flatMap((n: NodeType) => validateNode(n));
}
const ref = useRef(null);
function sendMessage() {
if (chatValue !== "") {
let nodeValidationErrors = validateNodes();
if (nodeValidationErrors.length === 0) {
setLockChat(true);
let message = chatValue;
setChatValue("");
addChatHistory(message, true);
sendAll({
...reactFlowInstance.toObject(),
message,
chatHistory,
name: flow.name,
description: flow.description,
});
} else {
setErrorData({
title: "Oops! Looks like you missed some required information:",
list: nodeValidationErrors,
});
}
} else {
setErrorData({
title: "Error sending message",
list: ["The message cannot be empty."],
});
}
}
function clearChat() {
setChatHistory([]);
ws.send(JSON.stringify({ clear_history: true }));
}
const { closePopUp } = useContext(PopUpContext);
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black backdrop-blur-sm dark:bg-gray-600 dark:bg-opacity-80 bg-opacity-80 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className=" drop-shadow-2xl relative flex flex-col justify-between transform h-[95%] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[690px]">
<div className="relative w-full">
<button
onClick={() => clearChat()}
className="absolute top-2 right-2 hover:text-red-500"
>
<FaEraser className="w-4 h-4" />
</button>
</div>
<div className="w-full h-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center overflow-scroll scrollbar-hide">
{chatHistory.length > 0 ? (
chatHistory.map((c, i) => <ChatMessage chat={c} key={i} />)
) : (
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle ">
<span>
👋{" "}
<span className="text-gray-600 text-lg">
LangFlow Chat
</span>
</span>
<br />
<div className="bg-gray-100 rounded-md w-2/4 px-6 py-8 border border-gray-200">
<span className="text-base text-gray-500">
Start a conversation and click the agents thoughts{" "}
<span>
<ChatBubbleOvalLeftEllipsisIcon className="w-6 h-6 inline animate-bounce " />
</span>{" "}
to inspect the chaining process.
</span>
</div>
</div>
)}
<div ref={ref}></div>
</div>
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center justify-between p-3">
<div className="relative w-full mt-1 rounded-md shadow-sm">
<ChatInput
chatValue={chatValue}
lockChat={lockChat}
sendMessage={sendMessage}
setChatValue={setChatValue}
/>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -1,9 +1,9 @@
import { Dialog, Transition } from "@headlessui/react";
import {
XMarkIcon,
ArrowDownTrayIcon,
DocumentDuplicateIcon,
ComputerDesktopIcon,
XMarkIcon,
ArrowDownTrayIcon,
DocumentDuplicateIcon,
ComputerDesktopIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
@ -12,165 +12,169 @@ import { TabsContext } from "../../contexts/tabsContext";
import { removeApiKeys } from "../../utils";
export default function ExportModal() {
const [open, setOpen] = useState(true);
const { closePopUp } = useContext(PopUpContext);
const ref = useRef();
const {setErrorData}= useContext(alertContext)
const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext);
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
const [checked,setChecked] = useState(true)
const [name,setName] = useState(flows[tabIndex].name)
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
const [open, setOpen] = useState(true);
const { closePopUp } = useContext(PopUpContext);
const ref = useRef();
const { setErrorData } = useContext(alertContext);
const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext);
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
const [checked, setChecked] = useState(true);
const [name, setName] = useState(flows[tabIndex].name);
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
<ArrowDownTrayIcon
className="h-6 w-6 text-blue-600"
aria-hidden="true"
/>
</div>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
Export as
</Dialog.Title>
</div>
</div>
<div className="pt-16 flex flex-col items-start justify-start h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-16">
<div className="w-full">
<label
htmlFor="name"
className="block mb-2 font-medium text-gray-700 dark:text-white"
>
Name
</label>
<input
onChange={(event) => {
if(event.target.value!=""){
let newFlow = flows[tabIndex];
newFlow.name = event.target.value;
setName(event.target.value)
updateFlow(newFlow);
}
else{
setName(event.target.value)
}
}}
type="text"
name="name"
value={name ?? null}
placeholder="File name"
id="name"
className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
/>
</div>
<div className="w-full">
<label
htmlFor="description"
className="block mb-2 font-medium text-gray-700 dark:text-white"
>
Description <span className="text-gray-400 text-sm"> (optional)</span>
</label>
<textarea
name="description"
id="description"
onChange={(event) => {
let newFlow = flows[tabIndex];
newFlow.description = event.target.value;
updateFlow(newFlow);
}}
value={flows[tabIndex].description ?? null}
placeholder="Flow description"
rows={3}
className=" focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
></textarea>
</div>
<div>
<label htmlFor="checkbox" className="flex items-center">
<input
onChange={(event) => {
setChecked(event.target.checked)
}}
checked={checked}
id="checkbox"
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
/>
<span className="ml-2 font-medium text-gray-700 dark:text-white">
Save with my API keys
</span>
</label>
</div>
<div className="w-full flex justify-end">
<button
onClick={() => {
if(checked) downloadFlow(flows[tabIndex]);
else downloadFlow(removeApiKeys(flows[tabIndex]))
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Download Flow
</button>
</div>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
<ArrowDownTrayIcon
className="h-6 w-6 text-blue-600"
aria-hidden="true"
/>
</div>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
Export as
</Dialog.Title>
</div>
</div>
<div className="pt-16 flex flex-col items-start justify-start h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-16">
<div className="w-full">
<label
htmlFor="name"
className="block mb-2 font-medium text-gray-700 dark:text-white"
>
Name
</label>
<input
onChange={(event) => {
if (event.target.value != "") {
let newFlow = flows[tabIndex];
newFlow.name = event.target.value;
setName(event.target.value);
updateFlow(newFlow);
} else {
setName(event.target.value);
}
}}
type="text"
name="name"
value={name ?? null}
placeholder="File name"
id="name"
className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="w-full">
<label
htmlFor="description"
className="block mb-2 font-medium text-gray-700 dark:text-white"
>
Description{" "}
<span className="text-gray-400 text-sm">
{" "}
(optional)
</span>
</label>
<textarea
name="description"
id="description"
onChange={(event) => {
let newFlow = flows[tabIndex];
newFlow.description = event.target.value;
updateFlow(newFlow);
}}
value={flows[tabIndex].description ?? null}
placeholder="Flow description"
rows={3}
className=" focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 text-gray-900 dark:text-gray-100 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
></textarea>
</div>
<div>
<label htmlFor="checkbox" className="flex items-center">
<input
onChange={(event) => {
setChecked(event.target.checked);
}}
checked={checked}
id="checkbox"
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
/>
<span className="ml-2 font-medium text-gray-700 dark:text-white">
Save with my API keys
</span>
</label>
</div>
<div className="w-full flex justify-end">
<button
onClick={() => {
if (checked) downloadFlow(flows[tabIndex]);
else downloadFlow(removeApiKeys(flows[tabIndex]));
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Download Flow
</button>
</div>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -2,43 +2,14 @@ import { Bars2Icon } from "@heroicons/react/24/outline";
import DisclosureComponent from "../DisclosureComponent";
import { nodeColors, nodeIcons, nodeNames } from "../../../../utils";
import { useContext, useEffect, useState } from "react";
import { getAll } from "../../../../controllers/API";
import { typesContext } from "../../../../contexts/typesContext";
import {
APIClassType,
APIKindType,
APIObjectType,
} from "../../../../types/api";
export default function ExtraSidebar() {
const [data, setData] = useState({});
const { setTypes } = useContext(typesContext);
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
// Update the state of the component with the retrieved data.
setData(result.data);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
const {data} = useContext(typesContext)
function onDragStart(
event: React.DragEvent<any>,
@ -51,7 +22,7 @@ export default function ExtraSidebar() {
return (
<div className="mt-1 w-full">
{Object.keys(data).map((d: keyof APIObjectType, i) => (
{Object.keys(data).sort().map((d: keyof APIObjectType, i) => (
<DisclosureComponent
key={i}
button={{
@ -60,7 +31,7 @@ export default function ExtraSidebar() {
}}
>
<div className="p-2 flex flex-col gap-2">
{Object.keys(data[d]).map((t: string, k) => (
{Object.keys(data[d]).sort().map((t: string, k) => (
<div key={k}>
<div
draggable
@ -84,7 +55,9 @@ export default function ExtraSidebar() {
</div>
</div>
))}
{Object.keys(data[d]).length===0 && <div className="text-gray-400 text-center">Coming soon</div>}
{Object.keys(data[d]).length === 0 && (
<div className="text-gray-400 text-center">Coming soon</div>
)}
</div>
</DisclosureComponent>
))}

View file

@ -5,116 +5,113 @@ import { TabsContext } from "../../../../contexts/tabsContext";
import FlowPage from "../..";
import { darkContext } from "../../../../contexts/darkContext";
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
BellIcon,
MoonIcon,
SunIcon,
ArrowDownTrayIcon,
ArrowUpTrayIcon,
BellIcon,
MoonIcon,
SunIcon,
} from "@heroicons/react/24/outline";
import { PopUpContext } from "../../../../contexts/popUpContext";
import AlertDropdown from "../../../../alerts/alertDropDown";
import { alertContext } from "../../../../contexts/alertContext";
import ImportModal from "../../../../modals/importModal";
import ExportModal from "../../../../modals/exportModal";
import { typesContext } from "../../../../contexts/typesContext";
export default function TabsManagerComponent() {
const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } =
useContext(TabsContext);
const { openPopUp } = useContext(PopUpContext);
const {templates} = useContext(typesContext)
const AlertWidth = 256;
const { dark, setDark } = useContext(darkContext);
const { notificationCenter, setNotificationCenter } =
useContext(alertContext);
useEffect(() => {
//create the first flow
if (flows.length === 0) {
if (flows.length === 0&& Object.keys(templates).length>0) {
addFlow();
}
}, [addFlow, flows.length]);
}, [addFlow, flows.length,templates]);
return (
<div className="h-full w-full flex flex-col">
<div className="w-full flex pr-2 flex-row text-center items-center bg-gray-100 dark:bg-gray-800 px-2">
{flows.map((flow, index) => {
return (
<TabComponent
onClick={() => setTabIndex(index)}
selected={index === tabIndex}
key={index}
flow={flow}
/>
);
})}
<TabComponent
onClick={() => {
addFlow();
}}
selected={false}
flow={null}
/>
<div className="ml-auto mr-2 flex gap-3">
<button
onClick={() =>
openPopUp(
<ImportModal
/>
)
}
className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500"
>
Import <ArrowUpTrayIcon className="w-5 h-5" />
</button>
<button
onClick={() =>openPopUp(<ExportModal/>)}
className="flex items-center gap-1 mr-2 text-sm text-gray-600 hover:text-gray-500"
>
Export <ArrowDownTrayIcon className="h-5 w-5" />
</button>
<button
className="text-gray-600 hover:text-gray-500 "
onClick={() => {
setDark(!dark);
}}
>
{dark ? (
<SunIcon className="h-5 w-5" />
) : (
<MoonIcon className="h-5 w-5" />
)}
</button>
<button
className="text-gray-600 hover:text-gray-500 relative"
onClick={(event: React.MouseEvent<HTMLElement>) => {
setNotificationCenter(false);
const top = (event.target as Element).getBoundingClientRect().top;
const left = (event.target as Element).getBoundingClientRect()
.left;
openPopUp(
<div
className="z-10 absolute"
style={{ top: top + 20, left: left - AlertWidth }}
>
<AlertDropdown />
</div>
);
}}
>
{notificationCenter && (
<div className="absolute w-1.5 h-1.5 rounded-full bg-red-600 right-[3px]"></div>
)}
<BellIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
<div className="w-full h-full">
<ReactFlowProvider>
{flows[tabIndex] ? (
<FlowPage flow={flows[tabIndex]}></FlowPage>
) : (
<></>
)}
</ReactFlowProvider>
</div>
</div>
);
return (
<div className="h-full w-full flex flex-col">
<div className="w-full flex pr-2 flex-row text-center items-center bg-gray-100 dark:bg-gray-800 px-2">
{flows.map((flow, index) => {
return (
<TabComponent
onClick={() => setTabIndex(index)}
selected={index === tabIndex}
key={index}
flow={flow}
/>
);
})}
<TabComponent
onClick={() => {
addFlow();
}}
selected={false}
flow={null}
/>
<div className="ml-auto mr-2 flex gap-3">
<button
onClick={() => openPopUp(<ImportModal />)}
className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500"
>
Import <ArrowUpTrayIcon className="w-5 h-5" />
</button>
<button
onClick={() => openPopUp(<ExportModal />)}
className="flex items-center gap-1 mr-2 text-sm text-gray-600 hover:text-gray-500"
>
Export <ArrowDownTrayIcon className="h-5 w-5" />
</button>
<button
className="text-gray-600 hover:text-gray-500 "
onClick={() => {
setDark(!dark);
}}
>
{dark ? (
<SunIcon className="h-5 w-5" />
) : (
<MoonIcon className="h-5 w-5" />
)}
</button>
<button
className="text-gray-600 hover:text-gray-500 relative"
onClick={(event: React.MouseEvent<HTMLElement>) => {
setNotificationCenter(false);
const top = (event.target as Element).getBoundingClientRect().top;
const left = (event.target as Element).getBoundingClientRect()
.left;
openPopUp(
<div
className="z-10 absolute"
style={{ top: top + 20, left: left - AlertWidth }}
>
<AlertDropdown />
</div>
);
}}
>
{notificationCenter && (
<div className="absolute w-1.5 h-1.5 rounded-full bg-red-600 right-[3px]"></div>
)}
<BellIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
<div className="w-full h-full">
<ReactFlowProvider>
{flows[tabIndex] ? (
<FlowPage flow={flows[tabIndex]}></FlowPage>
) : (
<></>
)}
</ReactFlowProvider>
</div>
</div>
);
}

View file

@ -1,15 +1,15 @@
import { useCallback, useContext, useEffect, useRef } from "react";
import ReactFlow, {
Background,
Controls,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
updateEdge,
EdgeChange,
Connection,
Edge,
Background,
Controls,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
updateEdge,
EdgeChange,
Connection,
Edge,
} from "reactflow";
import { locationContext } from "../../contexts/locationContext";
import ExtraSidebar from "./components/extraSidebarComponent";
@ -24,7 +24,7 @@ import { APIClassType } from "../../types/api";
import { isValidConnection } from "../../utils";
const nodeTypes = {
genericNode: GenericNode,
genericNode: GenericNode,
};
var _ = require("lodash");
@ -32,143 +32,150 @@ var _ = require("lodash");
export default function FlowPage({ flow }:{flow:FlowType}) {
let { updateFlow, incrementNodeId} =
useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance } =
const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext);
const reactFlowWrapper = useRef(null);
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
const { setErrorData } = useContext(alertContext);
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true)
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
const { setErrorData } = useContext(alertContext);
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, edges]);
//update flow when tabs change
useEffect(() => {
setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, edges]);
//update flow when tabs change
useEffect(() => {
setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
const onEdgesChangeMod = useCallback(
(s:EdgeChange[]) => {
onEdgesChange(s);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[onEdgesChange, setNodes]
);
const onEdgesChangeMod = useCallback(
(s: EdgeChange[]) => {
onEdgesChange(s);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[onEdgesChange, setNodes]
);
const onConnect = useCallback(
(params:Connection) => {
setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds)
);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[setEdges, setNodes]
);
const onConnect = useCallback(
(params: Connection) => {
setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds)
);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[setEdges, setNodes]
);
const onDragOver = useCallback((event:React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
const onDrop = useCallback(
(event:React.DragEvent) => {
event.preventDefault();
// Helper function to generate a unique node ID
function getId() {
return `dndnode_` + incrementNodeId();
}
// Get the current bounds of the ReactFlow wrapper element
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
// Extract the data from the drag event and parse it as a JSON object
let data:{type:string,node?:APIClassType} = JSON.parse(event.dataTransfer.getData("json"));
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
if (
data.type !== "chatInput" ||
(data.type === "chatInput" &&
!reactFlowInstance.getNodes().some((n) => n.type === "chatInputNode"))
) {
// Calculate the position where the node should be created
const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
// Generate a unique node ID
let newId = getId();
// Create a new node object
const newNode:NodeType = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
// Add the new node to the list of nodes in state
setNodes((nds) => nds.concat(newNode));
} else {
// If a chat input node already exists, set an error message
setErrorData({
title: "Error creating node",
list: ["There can't be more than one chat input."],
});
}
},
// Specify dependencies for useCallback
[incrementNodeId, reactFlowInstance, setErrorData, setNodes]
);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const onDelete = (mynodes) => {
setEdges(edges.filter((ns) => !nodes.some((n) => ns.source === n.id || ns.target === n.id)));
}
// Helper function to generate a unique node ID
function getId() {
return `dndnode_` + incrementNodeId();
}
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
// Get the current bounds of the ReactFlow wrapper element
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
const onEdgeUpdate = useCallback((oldEdge:Edge, newConnection:Connection) => {
if(isValidConnection(newConnection,reactFlowInstance)){
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
}, []);
// Extract the data from the drag event and parse it as a JSON object
let data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("json")
);
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
if (
data.type !== "chatInput" ||
(data.type === "chatInput" &&
!reactFlowInstance.getNodes().some((n) => n.type === "chatInputNode"))
) {
// Calculate the position where the node should be created
const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
// Generate a unique node ID
let newId = getId();
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
// Add the new node to the list of nodes in state
setNodes((nds) => nds.concat(newNode));
} else {
// If a chat input node already exists, set an error message
setErrorData({
title: "Error creating node",
list: ["There can't be more than one chat input."],
});
}
},
// Specify dependencies for useCallback
[incrementNodeId, reactFlowInstance, setErrorData, setNodes]
);
const onDelete = (mynodes) => {
setEdges(
edges.filter(
(ns) => !nodes.some((n) => ns.source === n.id || ns.target === n.id)
)
);
};
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
const onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => {
if (isValidConnection(newConnection, reactFlowInstance)) {
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[]
);
const onEdgeUpdateEnd = useCallback((_, edge) => {
if (!edgeUpdateSuccessful.current) {
@ -180,7 +187,7 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
return (
<div className="w-full h-full" ref={reactFlowWrapper}>
{Object.keys(types).length > 0 ? (
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
<>
<ReactFlow
nodes={nodes}

View file

@ -1,8 +1,8 @@
import { ReportHandler } from 'web-vitals';
import { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);

4
src/frontend/src/svg.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.svg" {
const content: any;
export default content;
}

View file

@ -1,12 +1,29 @@
export type ErrorAlertType = {title:string,list:Array<string>,id:string,removeAlert:(id:string)=>void}
export type NoticeAlertType = {title:string,link:string,id:string,removeAlert:(id:string)=>void}
export type SuccessAlertType = {title:string,id:string, removeAlert:(id:string)=>void}
export type SingleAlertComponentType = {dropItem:AlertItemType,removeAlert:(index:string)=>void}
export type ErrorAlertType = {
title: string;
list: Array<string>;
id: string;
removeAlert: (id: string) => void;
};
export type NoticeAlertType = {
title: string;
link: string;
id: string;
removeAlert: (id: string) => void;
};
export type SuccessAlertType = {
title: string;
id: string;
removeAlert: (id: string) => void;
};
export type SingleAlertComponentType = {
dropItem: AlertItemType;
removeAlert: (index: string) => void;
};
export type AlertDropdownType = {};
export type AlertItemType = {
type: "notice" | "error" | "success";
title: string;
link?: string;
list?: Array<string>;
id: string;
};
type: "notice" | "error" | "success";
title: string;
link?: string;
list?: Array<string>;
id: string;
};

View file

@ -1,20 +1,40 @@
import { Node,Edge,Viewport } from "reactflow"
import { Node, Edge, Viewport } from "reactflow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIObjectType = {kind:APIKindType,[key:string]:APIKindType}
export type APIKindType= {class:APIClassType,[key:string]:APIClassType}
export type APITemplateType = {variable:TemplateVariableType,[key:string]:TemplateVariableType}
export type APIClassType ={base_classes:Array<string>,description:string,template:APITemplateType,[key:string]:Array<string>|string|APITemplateType}
export type TemplateVariableType = {type:string,required:boolean,placeholder?:string,list:boolean,show:boolean,multiline?:boolean,value?:any,[key:string]:any}
export type sendAllProps={
nodes: Node[];
edges: Edge[];
name:string,
description:string;
viewport: Viewport;
message:string;
chatHistory:{message:string,isSend:boolean}[],
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
export type APITemplateType = {
variable: TemplateVariableType;
[key: string]: TemplateVariableType;
};
export type errorsTypeAPI={function:{errors:Array<string>},imports:{errors:Array<string>}}
export type PromptTypeAPI = {input_variables:Array<string>}
export type APIClassType = {
base_classes: Array<string>;
description: string;
template: APITemplateType;
[key: string]: Array<string> | string | APITemplateType;
};
export type TemplateVariableType = {
type: string;
required: boolean;
placeholder?: string;
list: boolean;
show: boolean;
multiline?: boolean;
value?: any;
[key: string]: any;
};
export type sendAllProps = {
nodes: Node[];
edges: Edge[];
name: string;
description: string;
viewport: Viewport;
message: string;
chatHistory: { message: string; isSend: boolean }[];
};
export type errorsTypeAPI = {
function: { errors: Array<string> };
imports: { errors: Array<string> };
};
export type PromptTypeAPI = { input_variables: Array<string> };

View file

@ -1,5 +1,10 @@
import { ReactFlowInstance } from 'reactflow';
import { ReactFlowInstance } from "reactflow";
import { FlowType } from "../flow";
export type ChatType = {flow:FlowType,reactFlowInstance:ReactFlowInstance}
export type ChatMessageType = { message: string; isSend: boolean, thought?:string }
export type ChatType = { flow: FlowType; reactFlowInstance: ReactFlowInstance };
export type ChatMessageType = {
message: string;
isSend: boolean;
thought?: string;
files?: Array<{ data: string; type: string; data_type: string }>;
};

View file

@ -1,81 +1,85 @@
import { ForwardRefExoticComponent, ReactElement, ReactNode } from "react";
import { NodeDataType } from "../flow/index";
export type InputComponentType = {
value: string;
disabled?: boolean;
onChange: (value: string) => void;
password: boolean;
value: string;
disabled?: boolean;
onChange: (value: string) => void;
password: boolean;
};
export type ToggleComponentType = {
enabled: boolean;
setEnabled: (state: boolean) => void;
disabled: boolean;
enabled: boolean;
setEnabled: (state: boolean) => void;
disabled: boolean;
};
export type DropDownComponentType = {
value: string;
options: string[];
onSelect: (value: string) => void;
value: string;
options: string[];
onSelect: (value: string) => void;
};
export type ParameterComponentType = {
data: NodeDataType;
title: string;
id: string;
color: string;
left: boolean;
type: string;
required?: boolean;
name?: string;
tooltipTitle: string;
data: NodeDataType;
title: string;
id: string;
color: string;
left: boolean;
type: string;
required?: boolean;
name?: string;
tooltipTitle: string;
};
export type InputListComponentType = {
value: string[];
onChange: (value: string[]) => void;
disabled: boolean;
value: string[];
onChange: (value: string[]) => void;
disabled: boolean;
};
export type TextAreaComponentType = {
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
};
export type FileComponentType = {
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
suffixes:Array<string>;
fileTypes:Array<string>;
onFileChange:(value: string) => void;
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
suffixes: Array<string>;
fileTypes: Array<string>;
onFileChange: (value: string) => void;
};
export type DisclosureComponentType = {
children: ReactNode;
button: {
title: string;
Icon: ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
buttons?: {
Icon: ReactElement;
title: string;
onClick: (event?: React.MouseEvent) => void;
}[];
};
children: ReactNode;
button: {
title: string;
Icon: ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
buttons?: {
Icon: ReactElement;
title: string;
onClick: (event?: React.MouseEvent) => void;
}[];
};
};
export type FloatComponentType = {
value: string;
disabled?: boolean;
onChange: (value: string) => void;
value: string;
disabled?: boolean;
onChange: (value: string) => void;
};
export type TooltipComponentType={children:ReactElement,title:string,placement?:
| 'bottom-end'
| 'bottom-start'
| 'bottom'
| 'left-end'
| 'left-start'
| 'left'
| 'right-end'
| 'right-start'
| 'right'
| 'top-end'
| 'top-start'
| 'top';}
export type TooltipComponentType = {
children: ReactElement;
title: string;
placement?:
| "bottom-end"
| "bottom-start"
| "bottom"
| "left-end"
| "left-start"
| "left"
| "right-end"
| "right-start"
| "right"
| "top-end"
| "top-start"
| "top";
};

View file

@ -1,3 +1,8 @@
import { HomeIcon } from "@heroicons/react/24/outline";
export type sidebarNavigationItemType = { name: string, href: string, icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>, current: boolean }
export type sidebarNavigationItemType = {
name: string;
href: string;
icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
current: boolean;
};

View file

@ -1,13 +1,22 @@
import { ChatMessageType } from './../chat/index';
import { APIClassType } from '../api/index';
import { ChatMessageType } from "./../chat/index";
import { APIClassType } from "../api/index";
import { ReactFlowJsonObject, XYPosition } from "reactflow";
export type FlowType = {
name: string;
id: string;
data: ReactFlowJsonObject;
chat: Array<ChatMessageType>;
description:string;
name: string;
id: string;
data: ReactFlowJsonObject;
description: string;
};
export type NodeType = {
id: string;
type?: string;
position: XYPosition;
data: NodeDataType;
};
export type NodeDataType = {
type: string;
node?: APIClassType;
id: string;
value: any;
};
export type NodeType = {id:string,type:string,position:XYPosition,data:NodeDataType}
export type NodeDataType = {type:string,node?:APIClassType,id:string,value:any}

View file

@ -1,17 +1,17 @@
import { FlowType } from "../flow";
export type TabsContextType = {
save:()=>void;
tabIndex: number;
setTabIndex: (index: number) => void;
flows: Array<FlowType>;
removeFlow: (id: string) => void;
addFlow: (flowData?: FlowType) => void;
updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => number;
downloadFlow: (flow:FlowType) => void;
uploadFlow: () => void;
lockChat:boolean;
setLockChat:(prevState:boolean)=>void;
hardReset:()=>void;
};
save: () => void;
tabIndex: number;
setTabIndex: (index: number) => void;
flows: Array<FlowType>;
removeFlow: (id: string) => void;
addFlow: (flowData?: FlowType) => void;
updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => number;
downloadFlow: (flow: FlowType) => void;
uploadFlow: () => void;
hardReset: () => void;
};
export type LangFlowState={ tabIndex:number, flows:FlowType[], id:string, nodeId:number }

View file

@ -0,0 +1,7 @@
const template:{[char: string]: string}={}
export type TemplateContextType = {
templates: typeof template;
setTemplates: (newState: {}) => void;
};

View file

@ -1,6 +1,9 @@
import { ReactFlowInstance } from "reactflow";
const types:{[char: string]: string}={}
const types:{[char: string]: string}={};
const template:{[char: string]: string}={}
const data:{[char: string]: string}={}
export type typesContextType = {
reactFlowInstance: ReactFlowInstance|null;
@ -8,4 +11,8 @@ export type typesContextType = {
deleteNode: (idx: string) => void;
types: typeof types;
setTypes: (newState: {}) => void;
};
templates: typeof template;
setTemplates: (newState: {}) => void;
data: typeof data;
setData: (newState: {}) => void;
};

View file

@ -14,13 +14,13 @@ import {
FingerPrintIcon,
ScissorsIcon,
CircleStackIcon,
Squares2X2Icon
Squares2X2Icon,
} from "@heroicons/react/24/outline";
import { Connection, Edge, Node, ReactFlowInstance } from "reactflow";
import { FlowType } from "./types/flow";
var _ = require('lodash')
var _ = require("lodash");
export function classNames(...classes:Array<string>) {
export function classNames(...classes: Array<string>) {
return classes.filter(Boolean).join(" ");
}
@ -70,7 +70,7 @@ export const borderLColors = {
gray: "border-l-gray-500",
};
export const nodeColors: {[char: string]: string} = {
export const nodeColors: { [char: string]: string } = {
prompts: "#4367BF",
llms: "#6344BE",
chains: "#FE7500",
@ -78,19 +78,19 @@ export const nodeColors: {[char: string]: string} = {
tools: "#FF3434",
memories: "#F5B85A",
advanced: "#000000",
chat: "#454173",
thought:"#272541",
embeddings:"#42BAA7",
documentloaders:"#7AAE42",
chat: "#198BF6",
thought: "#272541",
embeddings: "#42BAA7",
documentloaders: "#7AAE42",
vectorstores: "#AA8742",
textsplitters: "#B47CB5",
toolkits:"#DB2C2C",
wrappers:"#E6277A",
utilities:"#31A3CC",
unknown:"#9CA3AF"
toolkits: "#DB2C2C",
wrappers: "#E6277A",
utilities: "#31A3CC",
unknown: "#9CA3AF",
};
export const nodeNames:{[char: string]: string} = {
export const nodeNames: { [char: string]: string } = {
prompts: "Prompts",
llms: "LLMs",
chains: "Chains",
@ -102,14 +102,18 @@ export const nodeNames:{[char: string]: string} = {
embeddings: "Embeddings",
documentloaders: "Document Loaders",
vectorstores: "Vector Stores",
toolkits:"Toolkits",
wrappers:"Wrappers",
toolkits: "Toolkits",
wrappers: "Wrappers",
textsplitters: "Text Splitters",
utilities:"Utilities",
unknown:"Unknown"
utilities: "Utilities",
unknown: "Unknown",
};
export const nodeIcons:{[char: string]: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>} = {
export const nodeIcons: {
[char: string]: React.ForwardRefExoticComponent<
React.SVGProps<SVGSVGElement>
>;
} = {
agents: RocketLaunchIcon,
chains: LinkIcon,
memories: CpuChipIcon,
@ -118,14 +122,14 @@ export const nodeIcons:{[char: string]: React.ForwardRefExoticComponent<React.SV
tools: WrenchIcon,
advanced: ComputerDesktopIcon,
chat: Bars3CenterLeftIcon,
embeddings:FingerPrintIcon,
documentloaders:PaperClipIcon,
embeddings: FingerPrintIcon,
documentloaders: PaperClipIcon,
vectorstores: CircleStackIcon,
toolkits:WrenchScrewdriverIcon,
textsplitters:ScissorsIcon,
wrappers:GiftIcon,
utilities:Squares2X2Icon,
unknown:QuestionMarkCircleIcon
toolkits: WrenchScrewdriverIcon,
textsplitters: ScissorsIcon,
wrappers: GiftIcon,
utilities: Squares2X2Icon,
unknown: QuestionMarkCircleIcon,
};
export const bgColors = {
@ -218,7 +222,7 @@ export const taskTypeMap: { [key: string]: string } = {
MULTICLASS_CLASSIFICATION: "Multiclass Classification",
};
const charWidths:{[char: string]: number} = {
const charWidths: { [char: string]: number } = {
" ": 0.2,
"!": 0.2,
'"': 0.3,
@ -260,7 +264,7 @@ for (let i = 97; i <= 122; i++) {
charWidths[String.fromCharCode(i)] = 0.5;
}
export function measureTextWidth(text: string, fontSize:number) {
export function measureTextWidth(text: string, fontSize: number) {
let wordWidth = 0;
for (let j = 0; j < text.length; j++) {
let char = text[j];
@ -270,7 +274,11 @@ export function measureTextWidth(text: string, fontSize:number) {
return wordWidth;
}
export function measureTextHeight(text: string, width:number, fontSize:number) {
export function measureTextHeight(
text: string,
width: number,
fontSize: number
) {
const charHeight = fontSize;
const lineHeight = charHeight * 1.5;
const words = text.split(" ");
@ -319,19 +327,19 @@ export function snakeToNormalCase(str: string) {
.join(" ");
}
export function normalCaseToSnakeCase(str:string){
export function normalCaseToSnakeCase(str: string) {
return str
.split(" ")
.map((word, index) => {
if (index === 0) {
return word[0].toUpperCase() + word.slice(1).toLowerCase();
}
return word.toLowerCase();
})
.join("_");
.split(" ")
.map((word, index) => {
if (index === 0) {
return word[0].toUpperCase() + word.slice(1).toLowerCase();
}
return word.toLowerCase();
})
.join("_");
}
export function roundNumber(x:number, decimals:number) {
export function roundNumber(x: number, decimals: number) {
return Math.round(x * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
@ -345,12 +353,15 @@ export function getConnectedNodes(edge: Edge, nodes: Array<Node>): Array<Node> {
}
export function isValidConnection(
{ source, target, sourceHandle, targetHandle }:Connection,
reactFlowInstance:ReactFlowInstance
{ source, target, sourceHandle, targetHandle }: Connection,
reactFlowInstance: ReactFlowInstance
) {
if (
sourceHandle.split('|')[0] === targetHandle.split("|")[0] ||
sourceHandle.split('|').slice(2).some((t) => t === targetHandle.split("|")[0]) ||
sourceHandle.split("|")[0] === targetHandle.split("|")[0] ||
sourceHandle
.split("|")
.slice(2)
.some((t) => t === targetHandle.split("|")[0]) ||
targetHandle.split("|")[0] === "str"
) {
let targetNode = reactFlowInstance.getNode(target).data.node;
@ -375,16 +386,34 @@ export function isValidConnection(
return false;
}
export function removeApiKeys(flow:FlowType):FlowType{
let cleanFLow = _.cloneDeep(flow)
cleanFLow.data.nodes.forEach(node=>{
for(const key in node.data.node.template)
{
if(key.includes('api')){
console.log(node.data.node.template[key])
node.data.node.template[key].value = ''
export function removeApiKeys(flow: FlowType): FlowType {
let cleanFLow = _.cloneDeep(flow);
cleanFLow.data.nodes.forEach((node) => {
for (const key in node.data.node.template) {
if (key.includes("api")) {
console.log(node.data.node.template[key]);
node.data.node.template[key].value = "";
}
}
})
return cleanFLow
});
return cleanFLow;
}
export function updateObject<T extends Record<string, any>>(reference: T, objectToUpdate: T): T {
let clonedObject = _.cloneDeep(objectToUpdate)
// Loop through each key in the object to update
for (const key in clonedObject) {
// If the key is not in the reference object, delete it
if (!(key in reference)) {
delete clonedObject[key];
}
}
// Loop through each key in the reference object
for (const key in reference) {
// If the key is not in the object to update, add it
if (!(key in clonedObject)) {
clonedObject[key] = reference[key];
}
}
return clonedObject;
}

View file

@ -1,45 +1,78 @@
/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin')
const plugin = require("tailwindcss/plugin");
module.exports = {
content: ["./src/**/*.{js,ts,tsx,jsx}"],
darkMode: 'class',
important:true,
darkMode: "class",
important: true,
theme: {
extend: {},
extend: {
borderColor: {
"red-outline": "rgba(255, 0, 0, 0.8)",
"green-outline": "rgba(72, 187, 120, 0.7)",
},
boxShadow: {
"red-outline": "0 0 5px rgba(255, 0, 0, 0.5)",
"green-outline": "0 0 5px rgba(72, 187, 120, 0.7)",
},
animation: {
"pulse-green": "pulseGreen 1s linear",
},
keyframes: {
pulseGreen: {
"0%": { boxShadow: "0 0 0 0 rgba(72, 187, 120, 0.7)" },
"100%": { boxShadow: "0 0 0 10px rgba(72, 187, 120, 0)" },
},
},
},
},
plugins: [
require("@tailwindcss/forms")({
strategy: 'class', // only generate classes
strategy: "class", // only generate classes
}),
plugin(function ({ addUtilities }) {
addUtilities({
'.scrollbar-hide': {
".scrollbar-hide": {
/* IE and Edge */
'-ms-overflow-style': 'none',
"-ms-overflow-style": "none",
/* Firefox */
'scrollbar-width': 'none',
"scrollbar-width": "none",
/* Safari and Chrome */
'&::-webkit-scrollbar': {
display: 'none'
}
},
'.arrow-hide':{
'&::-webkit-inner-spin-button':{
'-webkit-appearance': 'none',
'margin': 0
"&::-webkit-scrollbar": {
display: "none",
},
'&::-webkit-outer-spin-button':{
'-webkit-appearance': 'none',
'margin': 0
},
".arrow-hide": {
"&::-webkit-inner-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
},
'.password':{
"-webkit-text-security":"disc",
"font-family": "text-security-disc"
},
'.custom-scroll':{
'&::-webkit-scrollbar': {
'width': '8px',
},
'&::-webkit-scrollbar-track': {
'backgroundColor': '#f1f1f1',
},
'&::-webkit-scrollbar-thumb': {
'backgroundColor': '#ccc',
'borderRadius': '999px',
},
'&::-webkit-scrollbar-thumb:hover': {
'backgroundColor': '#bbb'
}
}
)
})
}),require('@tailwindcss/line-clamp')
],
}
};

View file

@ -1,4 +1,8 @@
import json
from pathlib import Path
from typing import AsyncGenerator
from httpx import AsyncClient
import pytest
from fastapi.testclient import TestClient
@ -21,6 +25,15 @@ def get_text():
"""
@pytest.fixture()
async def async_client() -> AsyncGenerator:
from langflow.main import create_app
app = create_app()
async with AsyncClient(app=app, base_url="http://testserver") as client:
yield client
# Create client fixture for FastAPI
@pytest.fixture(scope="module")
def client():
@ -30,3 +43,37 @@ def client():
with TestClient(app) as client:
yield client
def get_graph(_type="basic"):
"""Get a graph from a json file"""
from langflow.graph.graph import Graph
if _type == "basic":
path = pytest.BASIC_EXAMPLE_PATH
elif _type == "complex":
path = pytest.COMPLEX_EXAMPLE_PATH
elif _type == "openapi":
path = pytest.OPENAPI_EXAMPLE_PATH
with open(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
def basic_graph():
return get_graph()
@pytest.fixture
def complex_graph():
return get_graph("complex")
@pytest.fixture
def openapi_graph():
return get_graph("openapi")

View file

@ -36,6 +36,7 @@ def test_zero_shot_agent(client: TestClient):
"name": "llm_chain",
"type": "LLMChain",
"list": False,
"advanced": True,
}
assert template["allowed_tools"] == {
"required": False,
@ -46,6 +47,7 @@ def test_zero_shot_agent(client: TestClient):
"name": "allowed_tools",
"type": "Tool",
"list": True,
"advanced": True,
}
@ -68,6 +70,7 @@ def test_json_agent(client: TestClient):
"name": "toolkit",
"type": "BaseToolkit",
"list": False,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -78,6 +81,7 @@ def test_json_agent(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": True,
}
@ -104,6 +108,7 @@ def test_csv_agent(client: TestClient):
"type": "file",
"list": False,
"content": None,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -114,6 +119,7 @@ def test_csv_agent(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": True,
}
@ -124,7 +130,7 @@ def test_initialize_agent(client: TestClient):
agents = json_response["agents"]
initialize_agent = agents["initialize_agent"]
assert initialize_agent["base_classes"] == ["AgentExecutor"]
assert initialize_agent["base_classes"] == ["AgentExecutor", "function"]
template = initialize_agent["template"]
assert template["agent"] == {
@ -143,6 +149,7 @@ def test_initialize_agent(client: TestClient):
"name": "agent",
"type": "str",
"list": True,
"advanced": True,
}
assert template["memory"] == {
"required": False,
@ -153,6 +160,7 @@ def test_initialize_agent(client: TestClient):
"name": "memory",
"type": "BaseChatMemory",
"list": False,
"advanced": True,
}
assert template["tools"] == {
"required": False,
@ -163,6 +171,7 @@ def test_initialize_agent(client: TestClient):
"name": "tools",
"type": "Tool",
"list": True,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -173,4 +182,5 @@ def test_initialize_agent(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": True,
}

View file

@ -3,7 +3,7 @@ import tempfile
from pathlib import Path
import pytest
from langflow.cache.utils import PREFIX, save_cache
from langflow.cache.base import PREFIX, save_cache
from langflow.interface.run import load_langchain_object

View file

@ -0,0 +1,81 @@
import pytest
from PIL import Image
import pandas as pd
from io import StringIO
from langflow.cache.manager import CacheManager
@pytest.fixture
def cache_manager():
return CacheManager()
def test_cache_manager_attach_detach_notify(cache_manager):
observer_called = False
def observer():
nonlocal observer_called
observer_called = True
cache_manager.attach(observer)
cache_manager.notify()
assert observer_called
observer_called = False
cache_manager.detach(observer)
cache_manager.notify()
assert not observer_called
def test_cache_manager_client_context(cache_manager):
with cache_manager.set_client_id("client1"):
cache_manager.add("foo", "bar", "string")
assert cache_manager.get("foo") == {
"obj": "bar",
"type": "string",
"extension": "str",
}
with cache_manager.set_client_id("client2"):
cache_manager.add("baz", "qux", "string")
assert cache_manager.get("baz") == {
"obj": "qux",
"type": "string",
"extension": "str",
}
with pytest.raises(KeyError):
cache_manager.get("foo")
def test_cache_manager_add_pandas(cache_manager):
df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
with cache_manager.set_client_id("client1"):
cache_manager.add_pandas("test_df", df)
cached_df = cache_manager.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_manager_add_image(cache_manager):
img = Image.new("RGB", (50, 50), color="red")
with cache_manager.set_client_id("client1"):
cache_manager.add_image("test_image", img)
cached_img = cache_manager.get("test_image")
assert cached_img["type"] == "image"
assert cached_img["extension"] == "png"
assert isinstance(cached_img["obj"], Image.Image)
def test_cache_manager_get_last(cache_manager):
with cache_manager.set_client_id("client1"):
cache_manager.add("foo", "bar", "string")
cache_manager.add("baz", "qux", "string")
last_item = cache_manager.get_last()
assert last_item == {"obj": "qux", "type": "string", "extension": "str"}

View file

@ -20,7 +20,12 @@ def test_conversation_chain(client: TestClient):
chain = chains["ConversationChain"]
# Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects
assert set(chain["base_classes"]) == {"LLMChain", "ConversationChain", "Chain"}
assert set(chain["base_classes"]) == {
"function",
"LLMChain",
"ConversationChain",
"Chain",
}
template = chain["template"]
assert template["memory"] == {
"required": False,
@ -31,16 +36,18 @@ def test_conversation_chain(client: TestClient):
"name": "memory",
"type": "BaseMemory",
"list": False,
"advanced": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"show": True,
"multiline": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -51,6 +58,7 @@ def test_conversation_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": False,
}
assert template["input_key"] == {
"required": True,
@ -62,6 +70,7 @@ def test_conversation_chain(client: TestClient):
"name": "input_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["output_key"] == {
"required": True,
@ -73,6 +82,7 @@ def test_conversation_chain(client: TestClient):
"name": "output_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["_type"] == "ConversationChain"
@ -91,7 +101,7 @@ def test_llm_chain(client: TestClient):
chain = chains["LLMChain"]
# Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects
assert set(chain["base_classes"]) == {"LLMChain", "Chain"}
assert set(chain["base_classes"]) == {"function", "LLMChain", "Chain"}
template = chain["template"]
assert template["memory"] == {
"required": False,
@ -102,17 +112,19 @@ def test_llm_chain(client: TestClient):
"name": "memory",
"type": "BaseMemory",
"list": False,
"advanced": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"show": True,
"multiline": False,
"value": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -123,6 +135,7 @@ def test_llm_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": False,
}
assert template["output_key"] == {
"required": True,
@ -134,6 +147,7 @@ def test_llm_chain(client: TestClient):
"name": "output_key",
"type": "str",
"list": False,
"advanced": True,
}
@ -145,7 +159,7 @@ def test_llm_checker_chain(client: TestClient):
chain = chains["LLMCheckerChain"]
# Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects
assert set(chain["base_classes"]) == {"LLMCheckerChain", "Chain"}
assert set(chain["base_classes"]) == {"function", "LLMCheckerChain", "Chain"}
template = chain["template"]
assert template["memory"] == {
"required": False,
@ -156,17 +170,19 @@ def test_llm_checker_chain(client: TestClient):
"name": "memory",
"type": "BaseMemory",
"list": False,
"advanced": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"show": True,
"multiline": False,
"value": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -177,6 +193,7 @@ def test_llm_checker_chain(client: TestClient):
"name": "llm",
"type": "BaseLLM",
"list": False,
"advanced": False,
}
assert template["input_key"] == {
"required": True,
@ -188,6 +205,7 @@ def test_llm_checker_chain(client: TestClient):
"name": "input_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["output_key"] == {
"required": True,
@ -199,6 +217,7 @@ def test_llm_checker_chain(client: TestClient):
"name": "output_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["_type"] == "LLMCheckerChain"
@ -217,7 +236,7 @@ def test_llm_math_chain(client: TestClient):
chain = chains["LLMMathChain"]
# Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects
assert set(chain["base_classes"]) == {"LLMMathChain", "Chain"}
assert set(chain["base_classes"]) == {"function", "LLMMathChain", "Chain"}
template = chain["template"]
assert template["memory"] == {
"required": False,
@ -228,17 +247,19 @@ def test_llm_math_chain(client: TestClient):
"name": "memory",
"type": "BaseMemory",
"list": False,
"advanced": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"show": True,
"multiline": False,
"value": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["llm"] == {
"required": True,
@ -249,6 +270,7 @@ def test_llm_math_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
"advanced": False,
}
assert template["input_key"] == {
"required": True,
@ -260,6 +282,7 @@ def test_llm_math_chain(client: TestClient):
"name": "input_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["output_key"] == {
"required": True,
@ -271,6 +294,7 @@ def test_llm_math_chain(client: TestClient):
"name": "output_key",
"type": "str",
"list": False,
"advanced": True,
}
assert template["_type"] == "LLMMathChain"
@ -291,6 +315,7 @@ def test_series_character_chain(client: TestClient):
# Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects
assert set(chain["base_classes"]) == {
"function",
"LLMChain",
"BaseCustomChain",
"Chain",
@ -298,35 +323,7 @@ def test_series_character_chain(client: TestClient):
"SeriesCharacterChain",
}
template = chain["template"]
assert template["memory"] == {
"required": False,
"placeholder": "",
"show": True,
"multiline": False,
"value": {
"chat_memory": {"messages": []},
"output_key": None,
"input_key": None,
"return_messages": False,
"human_prefix": "Human",
"ai_prefix": "AI",
"memory_key": "history",
},
"password": False,
"name": "memory",
"type": "BaseMemory",
"list": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
}
assert template["llm"] == {
"required": True,
"placeholder": "",
@ -336,50 +333,7 @@ def test_series_character_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
}
assert template["input_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "input",
"password": False,
"name": "input_key",
"type": "str",
"list": False,
}
assert template["output_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "response",
"password": False,
"name": "output_key",
"type": "str",
"list": False,
}
assert template["template"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": True,
"value": "I want you to act like {character} from {series}.\nI want you to respond and answer like {character}. do not write any explanations. only answer like {character}.\nYou must know all of the knowledge of {character}.\nCurrent conversation:\n{history}\nHuman: {input}\n{character}:", # noqa: E501
"password": False,
"name": "template",
"type": "str",
"list": False,
}
assert template["ai_prefix_value"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"value": "character",
"password": False,
"name": "ai_prefix_value",
"type": "str",
"list": False,
"advanced": False,
}
assert template["character"] == {
"required": True,
@ -390,6 +344,7 @@ def test_series_character_chain(client: TestClient):
"name": "character",
"type": "str",
"list": False,
"advanced": False,
}
assert template["series"] == {
"required": True,
@ -400,6 +355,7 @@ def test_series_character_chain(client: TestClient):
"name": "series",
"type": "str",
"list": False,
"advanced": False,
}
assert template["_type"] == "SeriesCharacterChain"
@ -429,55 +385,7 @@ def test_mid_journey_prompt_chain(client: TestClient):
# Test the template object
template = chain["template"]
assert template["memory"] == {
"required": False,
"placeholder": "",
"show": True,
"multiline": False,
"value": {
"chat_memory": {"messages": []},
"output_key": None,
"input_key": None,
"return_messages": False,
"human_prefix": "Human",
"ai_prefix": "AI",
"memory_key": "history",
},
"password": False,
"name": "memory",
"type": "BaseMemory",
"list": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
}
# Continue with other template object assertions
assert template["prompt"] == {
"required": False,
"placeholder": "",
"show": True,
"multiline": False,
"value": {
"input_variables": ["history", "input"],
"output_parser": None,
"partial_variables": {},
"template": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n\nCurrent conversation:\n{history}\nHuman: {input}\nAI:", # noqa: E501
"template_format": "f-string",
"validate_template": True,
"_type": "prompt",
},
"password": False,
"name": "prompt",
"type": "BasePromptTemplate",
"list": False,
}
assert template["llm"] == {
"required": True,
"placeholder": "",
@ -487,49 +395,7 @@ def test_mid_journey_prompt_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
}
assert template["output_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "response",
"password": False,
"name": "output_key",
"type": "str",
"list": False,
}
assert template["input_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "input",
"password": False,
"name": "input_key",
"type": "str",
"list": False,
}
assert template["template"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": True,
"value": 'I want you to act as a prompt generator for Midjourney\'s artificial intelligence program.\n Your job is to provide detailed and creative descriptions that will inspire unique and interesting images from the AI.\n Keep in mind that the AI is capable of understanding a wide range of language and can interpret abstract concepts, so feel free to be as imaginative and descriptive as possible.\n For example, you could describe a scene from a futuristic city, or a surreal landscape filled with strange creatures.\n The more detailed and imaginative your description, the more interesting the resulting image will be. Here is your first prompt:\n "A field of wildflowers stretches out as far as the eye can see, each one a different color and shape. In the distance, a massive tree towers over the landscape, its branches reaching up to the sky like tentacles."\n\n Current conversation:\n {history}\n Human: {input}\n AI:', # noqa: E501
"password": False,
"name": "template",
"type": "str",
"list": False,
}
assert template["ai_prefix_value"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "ai_prefix_value",
"type": "str",
"list": False,
"advanced": False,
}
# Test the description object
assert (
@ -557,55 +423,7 @@ def test_time_travel_guide_chain(client: TestClient):
# Test the template object
template = chain["template"]
assert template["memory"] == {
"required": False,
"placeholder": "",
"show": True,
"multiline": False,
"value": {
"chat_memory": {"messages": []},
"output_key": None,
"input_key": None,
"return_messages": False,
"human_prefix": "Human",
"ai_prefix": "AI",
"memory_key": "history",
},
"password": False,
"name": "memory",
"type": "BaseMemory",
"list": False,
}
assert template["verbose"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "verbose",
"type": "bool",
"list": False,
}
assert template["prompt"] == {
"required": False,
"placeholder": "",
"show": True,
"multiline": False,
"value": {
"input_variables": ["history", "input"],
"output_parser": None,
"partial_variables": {},
"template": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n\nCurrent conversation:\n{history}\nHuman: {input}\nAI:", # noqa: E501
"template_format": "f-string",
"validate_template": True,
"_type": "prompt",
},
"password": False,
"name": "prompt",
"type": "BasePromptTemplate",
"list": False,
}
assert template["llm"] == {
"required": True,
"placeholder": "",
@ -615,50 +433,7 @@ def test_time_travel_guide_chain(client: TestClient):
"name": "llm",
"type": "BaseLanguageModel",
"list": False,
}
assert template["output_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "response",
"password": False,
"name": "output_key",
"type": "str",
"list": False,
"advanced": False,
}
assert template["input_key"] == {
"required": True,
"placeholder": "",
"show": True,
"multiline": False,
"value": "input",
"password": False,
"name": "input_key",
"type": "str",
"list": False,
}
assert template["template"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": True,
"value": "I want you to act as my time travel guide. You are helpful and creative. I will provide you with the historical period or future time I want to visit and you will suggest the best events, sights, or people to experience. Provide the suggestions and any necessary information.\n Current conversation:\n {history}\n Human: {input}\n AI:", # noqa: E501
"password": False,
"name": "template",
"type": "str",
"list": False,
}
assert template["ai_prefix_value"] == {
"required": False,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "ai_prefix_value",
"type": "str",
"list": False,
}
assert chain["description"] == ""
assert chain["description"] == "Time travel guide chain to be used in the flow."

View file

@ -1,4 +1,3 @@
import json
from typing import Type, Union
import pytest
@ -15,7 +14,7 @@ from langflow.graph.nodes import (
ToolNode,
WrapperNode,
)
from langflow.interface.run import get_result_and_thought_using_graph
from langflow.interface.run import get_result_and_steps
from langflow.utils.payload import build_json, get_root_node
# Test cases for the graph module
@ -24,38 +23,6 @@ from langflow.utils.payload import build_json, get_root_node
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
def get_graph(_type="basic"):
"""Get a graph from a json file"""
if _type == "basic":
path = pytest.BASIC_EXAMPLE_PATH
elif _type == "complex":
path = pytest.COMPLEX_EXAMPLE_PATH
elif _type == "openapi":
path = pytest.OPENAPI_EXAMPLE_PATH
with open(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
def basic_graph():
return get_graph()
@pytest.fixture
def complex_graph():
return get_graph("complex")
@pytest.fixture
def openapi_graph():
return get_graph("openapi")
def get_node_by_type(graph, node_type: Type[Node]) -> Union[Node, None]:
"""Get a node by type"""
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
@ -441,7 +408,7 @@ def test_get_result_and_thought(basic_graph):
# now build again and check if FakeListLLM was used
# Get the result and thought
result, thought = get_result_and_thought_using_graph(langchain_object, message)
result, thought = get_result_and_steps(langchain_object, message)
# The result should be a str
assert isinstance(result, str)
# The thought should be a Thought

View file

@ -28,6 +28,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "cache",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["verbose"] == {
"required": False,
@ -39,6 +40,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["client"] == {
"required": False,
@ -49,6 +51,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "client",
"type": "Any",
"list": False,
"advanced": True,
}
assert template["repo_id"] == {
"required": False,
@ -60,6 +63,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "repo_id",
"type": "str",
"list": False,
"advanced": True,
}
assert template["task"] == {
"required": True,
@ -71,6 +75,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "task",
"type": "str",
"list": True,
"advanced": True,
}
assert template["model_kwargs"] == {
"required": False,
@ -81,6 +86,7 @@ def test_hugging_face_hub(client: TestClient):
"name": "model_kwargs",
"type": "code",
"list": False,
"advanced": True,
}
assert template["huggingfacehub_api_token"] == {
"required": False,
@ -92,6 +98,7 @@ def test_hugging_face_hub(client: TestClient):
"display_name": "HuggingFace Hub API Token",
"type": "str",
"list": False,
"advanced": False,
}
@ -113,6 +120,7 @@ def test_openai(client: TestClient):
"name": "cache",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["verbose"] == {
"required": False,
@ -123,6 +131,7 @@ def test_openai(client: TestClient):
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["client"] == {
"required": False,
@ -133,6 +142,7 @@ def test_openai(client: TestClient):
"name": "client",
"type": "Any",
"list": False,
"advanced": True,
}
assert template["model_name"] == {
"required": False,
@ -151,6 +161,7 @@ def test_openai(client: TestClient):
"name": "model_name",
"type": "str",
"list": True,
"advanced": False,
}
# Add more assertions for other properties here
assert template["temperature"] == {
@ -163,6 +174,7 @@ def test_openai(client: TestClient):
"name": "temperature",
"type": "float",
"list": False,
"advanced": False,
}
assert template["max_tokens"] == {
"required": False,
@ -174,6 +186,7 @@ def test_openai(client: TestClient):
"name": "max_tokens",
"type": "int",
"list": False,
"advanced": True,
}
assert template["top_p"] == {
"required": False,
@ -185,6 +198,7 @@ def test_openai(client: TestClient):
"name": "top_p",
"type": "float",
"list": False,
"advanced": True,
}
assert template["frequency_penalty"] == {
"required": False,
@ -196,6 +210,7 @@ def test_openai(client: TestClient):
"name": "frequency_penalty",
"type": "float",
"list": False,
"advanced": True,
}
assert template["presence_penalty"] == {
"required": False,
@ -207,6 +222,7 @@ def test_openai(client: TestClient):
"name": "presence_penalty",
"type": "float",
"list": False,
"advanced": True,
}
assert template["n"] == {
"required": False,
@ -218,6 +234,7 @@ def test_openai(client: TestClient):
"name": "n",
"type": "int",
"list": False,
"advanced": True,
}
assert template["best_of"] == {
"required": False,
@ -229,6 +246,7 @@ def test_openai(client: TestClient):
"name": "best_of",
"type": "int",
"list": False,
"advanced": True,
}
assert template["model_kwargs"] == {
"required": False,
@ -239,6 +257,7 @@ def test_openai(client: TestClient):
"name": "model_kwargs",
"type": "code",
"list": False,
"advanced": True,
}
assert template["openai_api_key"] == {
"required": False,
@ -251,6 +270,7 @@ def test_openai(client: TestClient):
"display_name": "OpenAI API Key",
"type": "str",
"list": False,
"advanced": False,
}
assert template["batch_size"] == {
"required": False,
@ -262,6 +282,7 @@ def test_openai(client: TestClient):
"name": "batch_size",
"type": "int",
"list": False,
"advanced": True,
}
assert template["request_timeout"] == {
"required": False,
@ -272,6 +293,7 @@ def test_openai(client: TestClient):
"name": "request_timeout",
"type": "Union[float, Tuple[float, float], NoneType]",
"list": False,
"advanced": True,
}
assert template["logit_bias"] == {
"required": False,
@ -282,6 +304,7 @@ def test_openai(client: TestClient):
"name": "logit_bias",
"type": "code",
"list": False,
"advanced": True,
}
assert template["max_retries"] == {
"required": False,
@ -293,6 +316,7 @@ def test_openai(client: TestClient):
"name": "max_retries",
"type": "int",
"list": False,
"advanced": True,
}
assert template["streaming"] == {
"required": False,
@ -304,6 +328,7 @@ def test_openai(client: TestClient):
"name": "streaming",
"type": "bool",
"list": False,
"advanced": True,
}
@ -326,6 +351,7 @@ def test_chat_open_ai(client: TestClient):
"name": "verbose",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["client"] == {
"required": False,
@ -336,6 +362,7 @@ def test_chat_open_ai(client: TestClient):
"name": "client",
"type": "Any",
"list": False,
"advanced": True,
}
assert template["model_name"] == {
"required": False,
@ -348,6 +375,7 @@ def test_chat_open_ai(client: TestClient):
"name": "model_name",
"type": "str",
"list": True,
"advanced": False,
}
assert template["temperature"] == {
"required": False,
@ -359,6 +387,7 @@ def test_chat_open_ai(client: TestClient):
"name": "temperature",
"type": "float",
"list": False,
"advanced": False,
}
assert template["model_kwargs"] == {
"required": False,
@ -369,6 +398,7 @@ def test_chat_open_ai(client: TestClient):
"name": "model_kwargs",
"type": "code",
"list": False,
"advanced": True,
}
assert template["openai_api_key"] == {
"required": False,
@ -381,6 +411,7 @@ def test_chat_open_ai(client: TestClient):
"display_name": "OpenAI API Key",
"type": "str",
"list": False,
"advanced": False,
}
assert template["request_timeout"] == {
"required": False,
@ -392,6 +423,7 @@ def test_chat_open_ai(client: TestClient):
"name": "request_timeout",
"type": "int",
"list": False,
"advanced": True,
}
assert template["max_retries"] == {
"required": False,
@ -403,6 +435,7 @@ def test_chat_open_ai(client: TestClient):
"name": "max_retries",
"type": "int",
"list": False,
"advanced": True,
}
assert template["streaming"] == {
"required": False,
@ -414,6 +447,7 @@ def test_chat_open_ai(client: TestClient):
"name": "streaming",
"type": "bool",
"list": False,
"advanced": True,
}
assert template["n"] == {
"required": False,
@ -425,6 +459,7 @@ def test_chat_open_ai(client: TestClient):
"name": "n",
"type": "int",
"list": False,
"advanced": True,
}
assert template["max_tokens"] == {
@ -436,6 +471,7 @@ def test_chat_open_ai(client: TestClient):
"name": "max_tokens",
"type": "int",
"list": False,
"advanced": True,
}
assert template["_type"] == "ChatOpenAI"
assert (

View file

@ -27,6 +27,7 @@ def test_prompt_template(client: TestClient):
"name": "input_variables",
"type": "str",
"list": True,
"advanced": True,
}
assert template["output_parser"] == {
"required": False,
@ -37,6 +38,7 @@ def test_prompt_template(client: TestClient):
"name": "output_parser",
"type": "BaseOutputParser",
"list": False,
"advanced": True,
}
assert template["partial_variables"] == {
"required": False,
@ -47,6 +49,7 @@ def test_prompt_template(client: TestClient):
"name": "partial_variables",
"type": "code",
"list": False,
"advanced": True,
}
assert template["template"] == {
"required": True,
@ -57,6 +60,7 @@ def test_prompt_template(client: TestClient):
"name": "template",
"type": "prompt",
"list": False,
"advanced": True,
}
assert template["template_format"] == {
"required": False,
@ -68,6 +72,7 @@ def test_prompt_template(client: TestClient):
"name": "template_format",
"type": "str",
"list": False,
"advanced": True,
}
assert template["validate_template"] == {
"required": False,
@ -79,6 +84,7 @@ def test_prompt_template(client: TestClient):
"name": "validate_template",
"type": "bool",
"list": False,
"advanced": True,
}
@ -100,6 +106,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "examples",
"type": "prompt",
"list": True,
"advanced": True,
}
assert template["example_selector"] == {
"required": False,
@ -110,6 +117,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "example_selector",
"type": "BaseExampleSelector",
"list": False,
"advanced": True,
}
assert template["example_prompt"] == {
"required": True,
@ -120,6 +128,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "example_prompt",
"type": "PromptTemplate",
"list": False,
"advanced": True,
}
assert template["suffix"] == {
"required": True,
@ -130,6 +139,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "suffix",
"type": "prompt",
"list": False,
"advanced": True,
}
assert template["example_separator"] == {
"required": False,
@ -141,6 +151,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "example_separator",
"type": "str",
"list": False,
"advanced": True,
}
assert template["prefix"] == {
"required": False,
@ -152,6 +163,7 @@ def test_few_shot_prompt_template(client: TestClient):
"name": "prefix",
"type": "prompt",
"list": False,
"advanced": True,
}
@ -172,6 +184,7 @@ def test_zero_shot_prompt(client: TestClient):
"name": "prefix",
"type": "str",
"list": False,
"advanced": True,
}
assert template["suffix"] == {
"required": True,
@ -183,6 +196,7 @@ def test_zero_shot_prompt(client: TestClient):
"name": "suffix",
"type": "str",
"list": False,
"advanced": True,
}
assert template["format_instructions"] == {
"required": False,
@ -194,4 +208,5 @@ def test_zero_shot_prompt(client: TestClient):
"name": "format_instructions",
"type": "str",
"list": False,
"advanced": True,
}

46
tests/test_websocket.py Normal file
View file

@ -0,0 +1,46 @@
import json
from unittest.mock import patch
from fastapi.testclient import TestClient
def test_websocket_connection(client: TestClient):
with client.websocket_connect("/chat/test_client") as websocket:
assert websocket.scope["client"] == ["testclient", 50000]
assert websocket.scope["path"] == "/chat/test_client"
def test_chat_history(client: TestClient):
# Mock the process_graph function to return a specific value
with patch("langflow.api.chat_manager.process_graph") as mock_process_graph:
mock_process_graph.return_value = ("Hello, I'm a mock response!", "")
with client.websocket_connect("/chat/test_client") as websocket:
# First message should be the history
history = websocket.receive_json()
assert history == [] # Empty history
# Send a message
payload = {"message": "Hello"}
websocket.send_json(json.dumps(payload))
# Receive the response from the server
response = websocket.receive_json()
assert response == {
"is_bot": True,
"message": None,
"type": "start",
"intermediate_steps": "",
"files": [],
}
# Send another message
payload = {"message": "How are you?"}
websocket.send_json(json.dumps(payload))
# Receive the response from the server
response = websocket.receive_json()
assert response == {
"is_bot": True,
"message": "Hello, I'm a mock response!",
"type": "end",
"intermediate_steps": "",
"files": [],
}