diff --git a/.vscode/launch.json b/.vscode/launch.json index e09e76cc8..bb61b0b9e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,4 +1,5 @@ { + "version": "0.2.0", "configurations": [ { "name": "Debug Backend", @@ -38,6 +39,15 @@ "request": "launch", "url": "http://localhost:3000/", "webRoot": "${workspaceRoot}/src/frontend" + }, + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "justMyCode": false } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da7ec1977..c58bb92f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,11 @@ to contributions, whether it be in the form of a new feature, improved infra, or To contribute to this project, please follow a ["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow. Please do not try to push directly to this repo unless you are a maintainer. +The branch structure is as follows: + +- `main`: The stable version of Langflow +- `dev`: The development version of Langflow. This branch is used to test new features before they are merged into `main` and, as such, may be unstable. + ## 🗺️Contributing Guidelines ## 🚩GitHub Issues diff --git a/README.md b/README.md index 3d795015d..9137ea714 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,8 @@ flow("Hey, have you heard of Langflow?") We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible. +--- + Join our [Discord](https://discord.com/invite/EqksyE2EX9) server to ask questions, make suggestions and showcase your projects! 🦾
diff --git a/poetry.lock b/poetry.lock index ced8e8e4c..aecc8ce57 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,25 @@ files = [ {file = "aiostream-0.4.5.tar.gz", hash = "sha256:3ecbf87085230fbcd9605c32ca20c4fb41af02c71d076eab246ea22e35947d88"}, ] +[[package]] +name = "alembic" +version = "1.11.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.7" +files = [ + {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"}, + {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["python-dateutil"] + [[package]] name = "anthropic" version = "0.3.8" @@ -3148,6 +3167,25 @@ docs = ["sphinx (>=1.6.0)", "sphinx-bootstrap-theme"] flake8 = ["flake8"] tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"] +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown" version = "3.4.4" @@ -3196,7 +3234,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, @@ -4036,28 +4074,67 @@ files = [ [[package]] name = "orjson" -version = "3.9.4" +version = "3.9.3" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.7" files = [ - {file = "orjson-3.9.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2e83ec1ee66d83b558a6d273d8a01b86563daa60bea9bc040e2c1cb8008de61f"}, - {file = "orjson-3.9.4-cp310-none-win32.whl", hash = "sha256:04cd7f4a4f4cd2fe43d104eb70e7435c6fcbdde7aa0cde4230e444fbc66924d3"}, - {file = "orjson-3.9.4-cp310-none-win_amd64.whl", hash = "sha256:4fdb59cfa00e10c82e09d1c32a9ce08a38bd29496ba20a73cd7f498e3a0a5024"}, - {file = "orjson-3.9.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:daeed2502ddf1f2b29ec8da2fe2ea82807a5c4acf869608ce6c476db8171d070"}, - {file = "orjson-3.9.4-cp311-none-win32.whl", hash = "sha256:e12492ce65cb10f385e70a88badc6046bc720fa7d468db27b7429d85d41beaeb"}, - {file = "orjson-3.9.4-cp311-none-win_amd64.whl", hash = "sha256:3b9f8bf43a5367d5522f80e7d533c98d880868cd0b640b9088c9237306eca6e8"}, - {file = "orjson-3.9.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0b400cf89c15958cd829c8a4ade8f5dd73588e63d2fb71a00483e7a74e9f92da"}, - {file = "orjson-3.9.4-cp312-none-win_amd64.whl", hash = "sha256:a533e664a0e3904307d662c5d45775544dc2b38df6e39e213ff6a86ceaa3d53c"}, - {file = "orjson-3.9.4-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:149d1b7630771222f73ecb024ab5dd8e7f41502402b02015494d429bacc4d5c1"}, - {file = "orjson-3.9.4-cp37-none-win32.whl", hash = "sha256:bcda6179eb863c295eb5ea832676d33ef12c04d227b4c98267876c8322e5a96e"}, - {file = "orjson-3.9.4-cp37-none-win_amd64.whl", hash = "sha256:3d947366127abef192419257eb7db7fcee0841ced2b49ccceba43b65e9ce5e3f"}, - {file = "orjson-3.9.4-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a7d029fc34a516f7eae29b778b30371fcb621134b2acfe4c51c785102aefc6cf"}, - {file = "orjson-3.9.4-cp38-none-win32.whl", hash = "sha256:94d15ee45c2aaed334688e511aa73b4681f7c08a0810884c6b3ae5824dea1222"}, - {file = "orjson-3.9.4-cp38-none-win_amd64.whl", hash = "sha256:336ec8471102851f0699198031924617b7a77baadea889df3ffda6000bd59f4c"}, - {file = "orjson-3.9.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2f57ccb50e9e123709e9f2d7b1a9e09e694e49d1fa5c5585e34b8e3f01929dc3"}, - {file = "orjson-3.9.4-cp39-none-win32.whl", hash = "sha256:b5b5038187b74e2d33e5caee8a7e83ddeb6a21da86837fa2aac95c69aeb366e6"}, - {file = "orjson-3.9.4-cp39-none-win_amd64.whl", hash = "sha256:915da36bc93ef0c659fa50fe7939d4f208804ad252fc4fc8d55adbbb82293c48"}, + {file = "orjson-3.9.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:082714b5554fcced092c45272f22a93400389733083c43f5043c4316e86f57a2"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97ddec69ca4fa1b66d512cf4f4a3fe6a57c4bf21209295ab2f4ada415996e08a"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab7501722ec2172b1c6ea333bc47bba3bbb9b5fc0e3e891191e8447f43d3187d"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ae680163ab09f04683d35fbd63eee858019f0066640f7cbad4dba3e7422a4bc"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e5abca1e0a9d110bab7346fab0acd3b7848d2ee13318bc24a31bbfbdad974b8"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c55f42a8b07cdb7d514cfaeb56f6e9029eef1cbc8e670ac31fc377c46b993cd1"}, + {file = "orjson-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:303f1324f5ea516f8e874ea0f8d15c581caabdca59fc990705fc76f3bd9f3bdf"}, + {file = "orjson-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c444e3931ea4fe7dec26d195486a681fedc0233230c9b84848f8e60affd4a4"}, + {file = "orjson-3.9.3-cp310-none-win32.whl", hash = "sha256:63333de96d83091023c9c99cc579973a2977b15feb5cdc8d9660104c886e9ab8"}, + {file = "orjson-3.9.3-cp310-none-win_amd64.whl", hash = "sha256:7bce6ff507a83c6a4b6b00726f3a7d7aed0b1f0884aac0440e95b55cac0b113e"}, + {file = "orjson-3.9.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec4421f377cce51decd6ea3869a8b41e9f05c50bf6acef8284f8906e642992c4"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b3177bd67756e53bdbd72c79fae3507796a67b67c32a16f4b55cad48ef25c13"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b21908252c8a13b8f48d4cccdb7fabb592824cf39c9fa4e9076015dd65eabeba"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7b795c6ac344b0c49776b7e135a9bed0cd15b1ade2a4c7b3a19e3913247702e"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac43842f5ba26e6f21b4e63312bd1137111a9b9821d7f7dfe189a4015c6c6bc"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8def4f6560c7b6dbc4b356dfd8e6624a018d920ce5a2864291a2bf1052cd6b68"}, + {file = "orjson-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bbc0dafd1de42c8dbfd6e5d1fe4deab15d2de474e11475921286bebefd109ec8"}, + {file = "orjson-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:85b1870d5420292419b34002659082d77f31b13d4d8cbd67bed9d717c775a0fb"}, + {file = "orjson-3.9.3-cp311-none-win32.whl", hash = "sha256:d6ece3f48f14a06c325181f2b9bd9a9827aac2ecdcad11eb12f561fb697eaaaa"}, + {file = "orjson-3.9.3-cp311-none-win_amd64.whl", hash = "sha256:448feda092c681c0a5b8eec62dd4f625ad5d316dafd56c81fb3f05b5221827ff"}, + {file = "orjson-3.9.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:413d7cf731f1222373360128a3d5232d52630a7355f446bf2659fc3445ec0b76"}, + {file = "orjson-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009a0f79804c604998b068f5f942e40546913ed45ee2f0a3d0e75695bf7543fa"}, + {file = "orjson-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ce062844255cce4d6a8a150e8e78b9fcd6c5a3f1ff3f8792922de25827c25b9c"}, + {file = "orjson-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:776659e18debe5de73c30b0957cd6454fcc61d87377fcb276441fca1b9f1305d"}, + {file = "orjson-3.9.3-cp312-none-win_amd64.whl", hash = "sha256:47b237da3818c8e546df4d2162f0a5cfd50b7b58528907919a27244141e0e48e"}, + {file = "orjson-3.9.3-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f954115d8496d4ab5975438e3ce07780c1644ea0a66c78a943ef79f33769b61a"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c57100517b6dbfe34181ed2248bebfab03bd2a7aafb6fbf849c6fd3bb2fbda"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa6017140fe487ab8fae605a2890c94c6fbe7a8e763ff33bbdb00e27ce078cfd"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fe77af2ff33c370fb06c9fdf004a66d85ea19c77f0273bbf70c70f98f832725"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2fa8c385b27bab886caa098fa3ae114d56571ae6e7a5610cb624d7b0a66faed"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8323739e7905ae4ec4dbdebb31067d28be981f30c11b6ae88ddec2671c0b3194"}, + {file = "orjson-3.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ad43fd5b1ededb54fe01e67468710fcfec8a5830e4ce131f85e741ea151a18e9"}, + {file = "orjson-3.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:42cb645780f732c829bc351346a54157d57f2bc409e671ee36b9fc1037bb77fe"}, + {file = "orjson-3.9.3-cp37-none-win32.whl", hash = "sha256:b84542669d1b0175dc2870025b73cbd4f4a3beb17796de6ec82683663e0400f3"}, + {file = "orjson-3.9.3-cp37-none-win_amd64.whl", hash = "sha256:1440a404ce84f43e2f8e97d8b5fe6f271458e0ffd37290dc3a9f6aa067c69930"}, + {file = "orjson-3.9.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1da8edaefb75f25b449ed4e22d00b9b49211b97dcefd44b742bdd8721d572788"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47210746acda49febe3bb07253eb5d63d7c7511beec5fa702aad3ce64e15664f"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:893c62afd5b26f04e2814dffa4d9d4060583ac43dc3e79ed3eadf62a5ac37b2c"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32aef33ae33901c327fd5679f91fa37199834d122dffd234416a6fe4193d1982"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd2761384ddb9de63b20795845d5cedadf052255a34c3ff1750cfc77b29d9926"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2502b4af2055050dcc74718f2647b65102087c6f5b3f939e2e1a3e3099602"}, + {file = "orjson-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fa7c7a39eeb8dd171f59d96fd4610f908ac14b2f2eb268f4498e5f310bda8da7"}, + {file = "orjson-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc3fe0c0ae7acf00d827efe2506131f1b19af3c87e3d76b0e081748984e51c26"}, + {file = "orjson-3.9.3-cp38-none-win32.whl", hash = "sha256:5b1ff8e920518753b310034e5796f0116f7732b0b27531012d46f0b54f3c8c85"}, + {file = "orjson-3.9.3-cp38-none-win_amd64.whl", hash = "sha256:9f2b1007174c93dd838f52e623c972df33057e3cb7ad9341b7d9bbd66b8d8fb4"}, + {file = "orjson-3.9.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cddc5b8bd7b0d1dfd36637eedbd83726b8b8a5969d3ecee70a9b54a94b8a0258"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c3bbf4b6f94fad2fd73c81293da8b343fbd07ce48d7836c07d0d54b58c8e93"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5cc22ef6973992db18952f8b978781e19a0c62c098f475db936284df9311df7"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcea93630986209c690f27f32398956b04ccbba8f1fa7c3d1bb88a01d9ab87a"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:526cb34e63faaad908c34597294507b7a4b999a436b4f206bc4e60ff4e911c20"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5ac6e30ee10af57f52e72f9c8b9bc4846a9343449d10ca2ae9760615da3042"}, + {file = "orjson-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6c37ab097c062bdf535105c7156839c4e370065c476bb2393149ad31a2cdf6e"}, + {file = "orjson-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27d69628f449c52a7a34836b15ec948804254f7954457f88de53f2f4de99512f"}, + {file = "orjson-3.9.3-cp39-none-win32.whl", hash = "sha256:5297463d8831c2327ed22bf92eb6d50347071ff1c73fb4702d50b8bc514aeac9"}, + {file = "orjson-3.9.3-cp39-none-win_amd64.whl", hash = "sha256:69a33486b5b6e5a99939fdb13c1c0d8bcc7c89fe6083e7b9ce3c70931ca9fb71"}, + {file = "orjson-3.9.3.tar.gz", hash = "sha256:d3da4faf6398154c1e75d32778035fa7dc284814809f76e8f8d50c4f54859399"}, ] [[package]] @@ -7540,4 +7617,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "1329d94d3cb37062393d79da99fb3fa7d214ebdcdab6402c411561f960c6689f" +content-hash = "7c6d7dc33a9b0ae9da053fb78b9f2eabbe78df38c4763e5a8719df6249d6f657" diff --git a/pyproject.toml b/pyproject.toml index 80ec08228..5b7054b65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ readme = "README.md" keywords = ["nlp", "langchain", "openai", "gpt", "gui"] packages = [{ include = "langflow", from = "src/backend" }] include = ["src/backend/langflow/*", "src/backend/langflow/**/*"] - +documentation = "https://docs.langflow.org" [tool.poetry.scripts] langflow = "langflow.__main__:main" @@ -63,7 +63,7 @@ python-multipart = "^0.0.6" sqlmodel = "^0.0.8" faiss-cpu = "^1.7.4" anthropic = "^0.3.0" -orjson = "^3.9.1" +orjson = "3.9.3" multiprocess = "^0.70.14" cachetools = "^5.3.1" types-cachetools = "^5.3.0.5" @@ -77,6 +77,7 @@ psycopg = "^3.1.9" psycopg-binary = "^3.1.9" fastavro = "^1.8.0" langchain-experimental = "^0.0.8" +alembic = "^1.11.2" [tool.poetry.group.dev.dependencies] black = "^23.1.0" diff --git a/src/backend/langflow/__init__.py b/src/backend/langflow/__init__.py index 5920369e2..f6eb836cc 100644 --- a/src/backend/langflow/__init__.py +++ b/src/backend/langflow/__init__.py @@ -1,5 +1,7 @@ from importlib import metadata -from langflow.cache import cache_manager + +# Deactivate cache manager for now +# from langflow.services.cache import cache_manager from langflow.processing.process import load_flow_from_json from langflow.interface.custom.custom_component import CustomComponent diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index fa167f188..43247b10f 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -1,6 +1,7 @@ import sys import time import httpx +from langflow.services.utils import get_settings_manager from langflow.utils.util import get_number_of_workers from multiprocess import Process # type: ignore import platform @@ -12,7 +13,6 @@ from rich import box from rich import print as rprint import typer from langflow.main import setup_app -from langflow.settings import settings from langflow.utils.logger import configure, logger import webbrowser from dotenv import load_dotenv @@ -30,19 +30,19 @@ def update_settings( """Update the settings from a config file.""" # Check for database_url in the environment variables - + settings_manager = get_settings_manager() if config: logger.debug(f"Loading settings from {config}") - settings.update_from_yaml(config, dev=dev) + settings_manager.settings.update_from_yaml(config, dev=dev) if remove_api_keys: logger.debug(f"Setting remove_api_keys to {remove_api_keys}") - settings.update_settings(REMOVE_API_KEYS=remove_api_keys) + settings_manager.settings.update_settings(REMOVE_API_KEYS=remove_api_keys) if cache: logger.debug(f"Setting cache to {cache}") - settings.update_settings(CACHE=cache) + settings_manager.settings.update_settings(CACHE=cache) if components_path: logger.debug(f"Adding component path {components_path}") - settings.update_settings(COMPONENTS_PATH=components_path) + settings_manager.settings.update_settings(COMPONENTS_PATH=components_path) def serve_on_jcloud(): @@ -106,7 +106,9 @@ def serve( help="Path to the directory containing custom components.", envvar="LANGFLOW_COMPONENTS_PATH", ), - config: str = typer.Option("config.yaml", help="Path to the configuration file."), + config: str = typer.Option( + Path(__file__).parent / "config.yaml", help="Path to the configuration file." + ), # .env file param env_file: Path = typer.Option( None, help="Path to the .env file containing environment variables." @@ -146,6 +148,11 @@ def serve( help="Remove API keys from the projects saved in the database.", envvar="LANGFLOW_REMOVE_API_KEYS", ), + backend_only: bool = typer.Option( + False, + help="Run only the backend server without the frontend.", + envvar="LANGFLOW_BACKEND_ONLY", + ), ): """ Run the Langflow server. @@ -167,7 +174,7 @@ def serve( ) # create path object if path is provided static_files_dir: Optional[Path] = Path(path) if path else None - app = setup_app(static_files_dir=static_files_dir) + app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only) # check if port is being used if is_port_in_use(port, host): port = get_free_port(port) @@ -179,6 +186,10 @@ def serve( "timeout": timeout, } + # Define an env variable to know if we are just testing the server + if "pytest" in sys.modules: + return + if platform.system() in ["Windows"]: # Run using uvicorn on MacOS and Windows # Windows doesn't support gunicorn diff --git a/src/backend/langflow/alembic.ini b/src/backend/langflow/alembic.ini new file mode 100644 index 000000000..379661422 --- /dev/null +++ b/src/backend/langflow/alembic.ini @@ -0,0 +1,113 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# This is the path to the db in the root of the project. +# When the user runs the Langflow the database url will +# be set dinamically. +sqlalchemy.url = sqlite:///../../../langflow.db + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/src/backend/langflow/alembic/README b/src/backend/langflow/alembic/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/src/backend/langflow/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/src/backend/langflow/alembic/env.py b/src/backend/langflow/alembic/env.py new file mode 100644 index 000000000..310894431 --- /dev/null +++ b/src/backend/langflow/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from langflow.services.database.manager import SQLModel + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = SQLModel.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/backend/langflow/alembic/script.py.mako b/src/backend/langflow/alembic/script.py.mako new file mode 100644 index 000000000..6ce335109 --- /dev/null +++ b/src/backend/langflow/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py b/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py new file mode 100644 index 000000000..0100df44d --- /dev/null +++ b/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py @@ -0,0 +1,42 @@ +"""Remove FlowStyles table + +Revision ID: 0a534bdfd84b +Revises: 4814b6f4abfd +Create Date: 2023-08-07 14:09:06.844104 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "0a534bdfd84b" +down_revision: Union[str, None] = "4814b6f4abfd" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("flowstyle") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "flowstyle", + sa.Column("color", sa.VARCHAR(), nullable=False), + sa.Column("emoji", sa.VARCHAR(), nullable=False), + sa.Column("flow_id", sa.CHAR(length=32), nullable=True), + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + # ### end Alembic commands ### diff --git a/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py b/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py new file mode 100644 index 000000000..0b2f32657 --- /dev/null +++ b/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py @@ -0,0 +1,65 @@ +"""Add Flow table + +Revision ID: 4814b6f4abfd +Revises: +Create Date: 2023-08-05 17:47:42.879824 + +""" + +import contextlib +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = "4814b6f4abfd" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # This suppress is used to not break the migration if the table already exists. + with contextlib.suppress(sa.exc.OperationalError): + op.create_table( + "flow", + sa.Column("data", sa.JSON(), nullable=True), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.create_index( + op.f("ix_flow_description"), "flow", ["description"], unique=False + ) + op.create_index(op.f("ix_flow_name"), "flow", ["name"], unique=False) + with contextlib.suppress(sa.exc.OperationalError): + op.create_table( + "flowstyle", + sa.Column("color", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("emoji", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True), + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("flowstyle") + op.drop_index(op.f("ix_flow_name"), table_name="flow") + op.drop_index(op.f("ix_flow_description"), table_name="flow") + op.drop_table("flow") + # ### end Alembic commands ### diff --git a/src/backend/langflow/api/router.py b/src/backend/langflow/api/router.py index b9c51c11e..ea1938a75 100644 --- a/src/backend/langflow/api/router.py +++ b/src/backend/langflow/api/router.py @@ -5,7 +5,6 @@ from langflow.api.v1 import ( endpoints_router, validate_router, flows_router, - flow_styles_router, component_router, ) @@ -17,4 +16,3 @@ router.include_router(endpoints_router) router.include_router(validate_router) router.include_router(component_router) router.include_router(flows_router) -router.include_router(flow_styles_router) diff --git a/src/backend/langflow/api/v1/__init__.py b/src/backend/langflow/api/v1/__init__.py index f001152a9..b6e7b36d8 100644 --- a/src/backend/langflow/api/v1/__init__.py +++ b/src/backend/langflow/api/v1/__init__.py @@ -2,7 +2,6 @@ from langflow.api.v1.endpoints import router as endpoints_router from langflow.api.v1.validate import router as validate_router from langflow.api.v1.chat import router as chat_router from langflow.api.v1.flows import router as flows_router -from langflow.api.v1.flow_styles import router as flow_styles_router from langflow.api.v1.components import router as component_router __all__ = [ @@ -11,5 +10,4 @@ __all__ = [ "component_router", "validate_router", "flows_router", - "flow_styles_router", ] diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 06a2fdda0..611407e8d 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -3,13 +3,13 @@ from fastapi.responses import StreamingResponse from langflow.api.utils import build_input_keys_response from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData -from langflow.chat.manager import ChatManager +from langflow.services import service_manager, ServiceType from langflow.graph.graph.base import Graph from langflow.utils.logger import logger from cachetools import LRUCache router = APIRouter(tags=["Chat"]) -chat_manager = ChatManager() + flow_data_store: LRUCache = LRUCache(maxsize=10) @@ -17,6 +17,7 @@ flow_data_store: LRUCache = LRUCache(maxsize=10) async def chat(client_id: str, websocket: WebSocket): """Websocket endpoint for chat.""" try: + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) if client_id in chat_manager.in_memory_cache: await chat_manager.handle_websocket(client_id, websocket) else: @@ -45,6 +46,7 @@ async def init_build(graph_data: dict, flow_id: str): return InitResponse(flowId=flow_id) # Delete from cache if already exists + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) if flow_id in chat_manager.in_memory_cache: with chat_manager.in_memory_cache._lock: chat_manager.in_memory_cache.delete(flow_id) @@ -160,7 +162,7 @@ async def stream_build(flow_id: str): "handle_keys": [], } yield str(StreamData(event="message", data=input_keys_response)) - + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) chat_manager.set_cache(flow_id, langchain_object) # We need to reset the chat history chat_manager.chat_history.empty_history(flow_id) diff --git a/src/backend/langflow/api/v1/components.py b/src/backend/langflow/api/v1/components.py index 1e34da2aa..4071461fb 100644 --- a/src/backend/langflow/api/v1/components.py +++ b/src/backend/langflow/api/v1/components.py @@ -1,8 +1,8 @@ from datetime import timezone from typing import List from uuid import UUID -from langflow.database.models.component import Component, ComponentModel -from langflow.database.base import get_session +from langflow.services.database.models.component import Component, ComponentModel +from langflow.services.utils import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.exc import IntegrityError diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 24af55588..5d0c9a900 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -1,12 +1,11 @@ from http import HTTPStatus from typing import Annotated, Optional -from langflow.cache.utils import save_uploaded_file -from langflow.database.models.flow import Flow +from langflow.services.cache.utils import save_uploaded_file +from langflow.services.database.models.flow import Flow from langflow.processing.process import process_graph_cached, process_tweaks +from langflow.services.utils import get_settings_manager from langflow.utils.logger import logger -from langflow.settings import settings - from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body from langflow.interface.custom.custom_component import CustomComponent @@ -26,7 +25,7 @@ from langflow.interface.types import ( build_langchain_custom_component_list_from_path, ) -from langflow.database.base import get_session +from langflow.services.utils import get_session from sqlmodel import Session # build router @@ -40,15 +39,20 @@ def get_all(): # custom_components is a list of dicts # need to merge all the keys into one dict custom_components_from_file = {} - if settings.COMPONENTS_PATH: - logger.info(f"Building custom components from {settings.COMPONENTS_PATH}") + settings_manager = get_settings_manager() + if settings_manager.settings.COMPONENTS_PATH: + logger.info( + f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}" + ) custom_component_dicts = [ build_langchain_custom_component_list_from_path(str(path)) - for path in settings.COMPONENTS_PATH + for path in settings_manager.settings.COMPONENTS_PATH ] logger.info(f"Loading {len(custom_component_dicts)} category(ies)") for custom_component_dict in custom_component_dicts: # custom_component_dict is a dict of dicts + if not custom_component_dict: + continue category = list(custom_component_dict.keys())[0] logger.info( f"Loading {len(custom_component_dict[category])} component(s) from category {category}" diff --git a/src/backend/langflow/api/v1/flow_styles.py b/src/backend/langflow/api/v1/flow_styles.py deleted file mode 100644 index 40e292eb3..000000000 --- a/src/backend/langflow/api/v1/flow_styles.py +++ /dev/null @@ -1,83 +0,0 @@ -from uuid import UUID -from langflow.database.models.flow_style import ( - FlowStyle, - FlowStyleCreate, - FlowStyleRead, - FlowStyleUpdate, -) -from langflow.database.base import get_session -from sqlmodel import Session, select -from fastapi import APIRouter, Depends, HTTPException - - -# build router -router = APIRouter(prefix="/flow_styles", tags=["FlowStyles"]) - -# FlowStyleCreate: -# class FlowStyleBase(SQLModel): -# color: str = Field(index=True) -# emoji: str = Field(index=False) -# flow_id: UUID = Field(default=None, foreign_key="flow.id") - - -@router.post("/", response_model=FlowStyleRead) -def create_flow_style( - *, session: Session = Depends(get_session), flow_style: FlowStyleCreate -): - """Create a new flow_style.""" - db_flow_style = FlowStyle.from_orm(flow_style) - session.add(db_flow_style) - session.commit() - session.refresh(db_flow_style) - return db_flow_style - - -@router.get("/", response_model=list[FlowStyleRead]) -def read_flow_styles(*, session: Session = Depends(get_session)): - """Read all flows.""" - try: - flows = session.exec(select(FlowStyle)).all() - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) from e - return flows - - -@router.get("/{flow_styles_id}", response_model=FlowStyleRead) -def read_flow_style(*, session: Session = Depends(get_session), flow_styles_id: UUID): - """Read a flow_style.""" - if flow_style := session.get(FlowStyle, flow_styles_id): - return flow_style - else: - raise HTTPException(status_code=404, detail="FlowStyle not found") - - -@router.patch("/{flow_style_id}", response_model=FlowStyleRead) -def update_flow_style( - *, - session: Session = Depends(get_session), - flow_style_id: UUID, - flow_style: FlowStyleUpdate, -): - """Update a flow_style.""" - db_flow_style = session.get(FlowStyle, flow_style_id) - if not db_flow_style: - raise HTTPException(status_code=404, detail="FlowStyle not found") - flow_data = flow_style.dict(exclude_unset=True) - for key, value in flow_data.items(): - if hasattr(db_flow_style, key) and value is not None: - setattr(db_flow_style, key, value) - session.add(db_flow_style) - session.commit() - session.refresh(db_flow_style) - return db_flow_style - - -@router.delete("/{flow_id}") -def delete_flow_style(*, session: Session = Depends(get_session), flow_id: UUID): - """Delete a flow_style.""" - flow_style = session.get(FlowStyle, flow_id) - if not flow_style: - raise HTTPException(status_code=404, detail="FlowStyle not found") - session.delete(flow_style) - session.commit() - return {"message": "FlowStyle deleted successfully"} diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 9f5042fcb..3145ced3c 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -1,16 +1,15 @@ from typing import List from uuid import UUID -from langflow.settings import settings from langflow.api.utils import remove_api_keys from langflow.api.v1.schemas import FlowListCreate, FlowListRead -from langflow.database.models.flow import ( +from langflow.services.database.models.flow import ( Flow, FlowCreate, FlowRead, - FlowReadWithStyle, FlowUpdate, ) -from langflow.database.base import get_session +from langflow.services.utils import get_session +from langflow.services.utils import get_settings_manager from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from fastapi.encoders import jsonable_encoder @@ -32,7 +31,7 @@ def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate): return db_flow -@router.get("/", response_model=list[FlowReadWithStyle], status_code=200) +@router.get("/", response_model=list[FlowRead], status_code=200) def read_flows(*, session: Session = Depends(get_session)): """Read all flows.""" try: @@ -42,7 +41,7 @@ def read_flows(*, session: Session = Depends(get_session)): return [jsonable_encoder(flow) for flow in flows] -@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200) +@router.get("/{flow_id}", response_model=FlowRead, status_code=200) def read_flow(*, session: Session = Depends(get_session), flow_id: UUID): """Read a flow.""" if flow := session.get(Flow, flow_id): @@ -61,7 +60,8 @@ def update_flow( if not db_flow: raise HTTPException(status_code=404, detail="Flow not found") flow_data = flow.dict(exclude_unset=True) - if settings.REMOVE_API_KEYS: + settings_manager = get_settings_manager() + if settings_manager.settings.REMOVE_API_KEYS: flow_data = remove_api_keys(flow_data) for key, value in flow_data.items(): setattr(db_flow, key, value) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 0148dac6d..776e90034 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -1,7 +1,7 @@ from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Union -from langflow.database.models.flow import FlowCreate, FlowRead +from langflow.services.database.models.flow import FlowCreate, FlowRead from pydantic import BaseModel, Field, validator import json diff --git a/src/backend/langflow/cache/__init__.py b/src/backend/langflow/cache/__init__.py deleted file mode 100644 index 723aa9e18..000000000 --- a/src/backend/langflow/cache/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from langflow.cache.manager import cache_manager -from langflow.cache.flow import InMemoryCache - -__all__ = [ - "cache_manager", - "InMemoryCache", -] diff --git a/src/backend/langflow/database/base.py b/src/backend/langflow/database/base.py deleted file mode 100644 index 546c341c1..000000000 --- a/src/backend/langflow/database/base.py +++ /dev/null @@ -1,78 +0,0 @@ -from contextlib import contextmanager -import os - -from sqlmodel import SQLModel, Session, create_engine -from langflow.utils.logger import logger - - -class Engine: - _instance = None - - @classmethod - def get(cls): - logger.debug("Getting database engine") - if cls._instance is None: - cls.create() - return cls._instance - - @classmethod - def create(cls): - logger.debug("Creating database engine") - from langflow.settings import settings - - if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): - settings.DATABASE_URL = langflow_database_url - logger.debug("Using LANGFLOW_DATABASE_URL") - - if settings.DATABASE_URL and settings.DATABASE_URL.startswith("sqlite"): - connect_args = {"check_same_thread": False} - else: - connect_args = {} - if not settings.DATABASE_URL: - raise RuntimeError("No database_url provided") - cls._instance = create_engine(settings.DATABASE_URL, connect_args=connect_args) - - @classmethod - def update(cls): - logger.debug("Updating database engine") - cls._instance = None - cls.create() - - -def create_db_and_tables(): - logger.debug("Creating database and tables") - try: - SQLModel.metadata.create_all(Engine.get()) - except Exception as exc: - logger.error(f"Error creating database and tables: {exc}") - raise RuntimeError("Error creating database and tables") from exc - # Now check if the table Flow exists, if not, something went wrong - # and we need to create the tables again. - from sqlalchemy import inspect - - inspector = inspect(Engine.get()) - if "flow" not in inspector.get_table_names(): - logger.error("Something went wrong creating the database and tables.") - logger.error("Please check your database settings.") - - raise RuntimeError("Something went wrong creating the database and tables.") - else: - logger.debug("Database and tables created successfully") - - -@contextmanager -def session_getter(): - try: - session = Session(Engine.get()) - yield session - except Exception as e: - print("Session rollback because of exception:", e) - session.rollback() - raise - finally: - session.close() - - -def get_session(): - with session_getter() as session: - yield session diff --git a/src/backend/langflow/database/models/__init__.py b/src/backend/langflow/database/models/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/backend/langflow/database/models/flow_style.py b/src/backend/langflow/database/models/flow_style.py deleted file mode 100644 index fe53799fe..000000000 --- a/src/backend/langflow/database/models/flow_style.py +++ /dev/null @@ -1,33 +0,0 @@ -# Path: src/backend/langflow/database/models/flowstyle.py - -from langflow.database.models.base import SQLModelSerializable -from sqlmodel import Field, Relationship -from uuid import UUID, uuid4 -from typing import TYPE_CHECKING, Optional - -if TYPE_CHECKING: - from langflow.database.models.flow import Flow - - -class FlowStyleBase(SQLModelSerializable): - color: str - emoji: str - flow_id: UUID = Field(default=None, foreign_key="flow.id") - - -class FlowStyle(FlowStyleBase, table=True): - id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) - flow: "Flow" = Relationship(back_populates="style") - - -class FlowStyleUpdate(SQLModelSerializable): - color: Optional[str] = None - emoji: Optional[str] = None - - -class FlowStyleCreate(FlowStyleBase): - pass - - -class FlowStyleRead(FlowStyleBase): - id: UUID diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 99b4e2b3d..f0d3986cf 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -1,7 +1,7 @@ from typing import Dict, Generator, List, Type, Union from langflow.graph.edge.base import Edge -from langflow.graph.graph.constants import VERTEX_TYPE_MAP +from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.types import ( FileToolVertex, @@ -187,10 +187,12 @@ class Graph: """Returns the node class based on the node type.""" if node_type in FILE_TOOLS: return FileToolVertex - if node_type in VERTEX_TYPE_MAP: - return VERTEX_TYPE_MAP[node_type] + if node_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP: + return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_type] return ( - VERTEX_TYPE_MAP[node_lc_type] if node_lc_type in VERTEX_TYPE_MAP else Vertex + lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_lc_type] + if node_lc_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP + else Vertex ) def _build_vertices(self) -> List[Vertex]: diff --git a/src/backend/langflow/graph/graph/constants.py b/src/backend/langflow/graph/graph/constants.py index 5e5c3b709..c9fea48b5 100644 --- a/src/backend/langflow/graph/graph/constants.py +++ b/src/backend/langflow/graph/graph/constants.py @@ -1,4 +1,3 @@ -from langflow.graph.vertex.base import Vertex from langflow.graph.vertex import types from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator @@ -15,23 +14,45 @@ from langflow.interface.wrappers.base import wrapper_creator from langflow.interface.output_parsers.base import output_parser_creator from langflow.interface.retrievers.base import retriever_creator from langflow.interface.custom.base import custom_component_creator -from typing import Dict, Type +from langflow.utils.lazy_load import LazyLoadDictBase -VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = { - **{t: types.PromptVertex for t in prompt_creator.to_list()}, - **{t: types.AgentVertex for t in agent_creator.to_list()}, - **{t: types.ChainVertex for t in chain_creator.to_list()}, - **{t: types.ToolVertex for t in tool_creator.to_list()}, - **{t: types.ToolkitVertex for t in toolkits_creator.to_list()}, - **{t: types.WrapperVertex for t in wrapper_creator.to_list()}, - **{t: types.LLMVertex for t in llm_creator.to_list()}, - **{t: types.MemoryVertex for t in memory_creator.to_list()}, - **{t: types.EmbeddingVertex for t in embedding_creator.to_list()}, - **{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()}, - **{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, - **{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, - **{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, - **{t: types.CustomComponentVertex for t in custom_component_creator.to_list()}, - **{t: types.RetrieverVertex for t in retriever_creator.to_list()}, -} +class VertexTypesDict(LazyLoadDictBase): + def __init__(self): + self._all_types_dict = None + + @property + def VERTEX_TYPE_MAP(self): + return self.all_types_dict + + def _build_dict(self): + langchain_types_dict = self.get_type_dict() + return { + **langchain_types_dict, + "Custom": ["Custom Tool", "Python Function"], + } + + def get_type_dict(self): + return { + **{t: types.PromptVertex for t in prompt_creator.to_list()}, + **{t: types.AgentVertex for t in agent_creator.to_list()}, + **{t: types.ChainVertex for t in chain_creator.to_list()}, + **{t: types.ToolVertex for t in tool_creator.to_list()}, + **{t: types.ToolkitVertex for t in toolkits_creator.to_list()}, + **{t: types.WrapperVertex for t in wrapper_creator.to_list()}, + **{t: types.LLMVertex for t in llm_creator.to_list()}, + **{t: types.MemoryVertex for t in memory_creator.to_list()}, + **{t: types.EmbeddingVertex for t in embedding_creator.to_list()}, + **{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()}, + **{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, + **{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, + **{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, + **{ + t: types.CustomComponentVertex + for t in custom_component_creator.to_list() + }, + **{t: types.RetrieverVertex for t in retriever_creator.to_list()}, + } + + +lazy_load_vertex_dict = VertexTypesDict() diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index cb7dc4905..ac7f72b4d 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -1,6 +1,6 @@ import ast from langflow.interface.initialize import loading -from langflow.interface.listing import ALL_TYPES_DICT +from langflow.interface.listing import lazy_load_dict from langflow.utils.constants import DIRECT_TYPES from langflow.utils.logger import logger from langflow.utils.util import sync_to_async @@ -62,7 +62,7 @@ class Vertex: ) if self.base_type is None: - for base_type, value in ALL_TYPES_DICT.items(): + for base_type, value in lazy_load_dict.ALL_TYPES_DICT.items(): if self.vertex_type in value: self.base_type = base_type break diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index cc5214c0c..ec8c42aba 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -5,7 +5,8 @@ from langchain.agents import types from langflow.custom.customs import get_custom_nodes from langflow.interface.agents.custom import CUSTOM_AGENTS from langflow.interface.base import LangChainTypeCreator -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.agents import AgentFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class, build_template_from_method @@ -53,13 +54,17 @@ class AgentCreator(LangChainTypeCreator): # Now this is a generator def to_list(self) -> List[str]: names = [] + settings_manager = get_settings_manager() for _, agent in self.type_to_loader_dict.items(): agent_name = ( agent.function_name() if hasattr(agent, "function_name") else agent.__name__ ) - if agent_name in settings.AGENTS or settings.DEV: + if ( + agent_name in settings_manager.settings.AGENTS + or settings_manager.settings.DEV + ): names.append(agent_name) return names diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 76d859b1f..d1ed83b5a 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -2,13 +2,14 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Type, Union from langchain.chains.base import Chain from langchain.agents import AgentExecutor +from langflow.services.utils import get_settings_manager from pydantic import BaseModel from langflow.template.field.base import TemplateField from langflow.template.frontend_node.base import FrontendNode from langflow.template.template.base import Template from langflow.utils.logger import logger -from langflow.settings import settings + # Assuming necessary imports for Field, Template, and FrontendNode classes @@ -26,9 +27,12 @@ class LangChainTypeCreator(BaseModel, ABC): @property def docs_map(self) -> Dict[str, str]: """A dict with the name of the component as key and the documentation link as value.""" + settings_manager = get_settings_manager() if self.name_docs_dict is None: try: - type_settings = getattr(settings, self.type_name.upper()) + type_settings = getattr( + settings_manager.settings, self.type_name.upper() + ) self.name_docs_dict = { name: value_dict["documentation"] for name, value_dict in type_settings.items() diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py index fe58397b2..b906dbd25 100644 --- a/src/backend/langflow/interface/chains/base.py +++ b/src/backend/langflow/interface/chains/base.py @@ -3,7 +3,8 @@ from typing import Any, Dict, List, Optional, Type from langflow.custom.customs import get_custom_nodes from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.chains import ChainFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class, build_template_from_method @@ -30,6 +31,7 @@ class ChainCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: if self.type_dict is None: + settings_manager = get_settings_manager() self.type_dict: dict[str, Any] = { chain_name: import_class(f"langchain.chains.{chain_name}") for chain_name in chains.__all__ @@ -43,7 +45,8 @@ class ChainCreator(LangChainTypeCreator): self.type_dict = { name: chain for name, chain in self.type_dict.items() - if name in settings.CHAINS or settings.DEV + if name in settings_manager.settings.CHAINS + or settings_manager.settings.DEV } return self.type_dict diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index ce8956660..5388f40d8 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -3,11 +3,12 @@ from fastapi import HTTPException from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.component import Component from langflow.interface.custom.directory_reader import DirectoryReader +from langflow.services.utils import get_db_manager from langflow.utils import validate -from langflow.database.base import session_getter -from langflow.database.models.flow import Flow +from langflow.services.database.utils import session_getter +from langflow.services.database.models.flow import Flow from pydantic import Extra import yaml @@ -159,7 +160,8 @@ class CustomComponent(Component, extra=Extra.allow): from langflow.processing.process import build_sorted_vertices_with_caching from langflow.processing.process import process_tweaks - with session_getter() as session: + db_manager = get_db_manager() + with session_getter(db_manager) as session: graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None if not graph_data: raise ValueError(f"Flow {flow_id} not found") @@ -169,7 +171,8 @@ class CustomComponent(Component, extra=Extra.allow): def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]: get_session = get_session or session_getter - with get_session() as session: + db_manager = get_db_manager() + with get_session(db_manager) as session: flows = session.query(Flow).all() return flows @@ -182,8 +185,8 @@ class CustomComponent(Component, extra=Extra.allow): get_session: Optional[Callable] = None, ) -> Flow: get_session = get_session or session_getter - - with get_session() as session: + db_manager = get_db_manager() + with get_session(db_manager) as session: if flow_id: flow = session.query(Flow).get(flow_id) elif flow_name: diff --git a/src/backend/langflow/interface/document_loaders/base.py b/src/backend/langflow/interface/document_loaders/base.py index ebae1e5a4..db0832ff3 100644 --- a/src/backend/langflow/interface/document_loaders/base.py +++ b/src/backend/langflow/interface/document_loaders/base.py @@ -1,9 +1,10 @@ from typing import Dict, List, Optional, Type from langflow.interface.base import LangChainTypeCreator +from langflow.services.utils import get_settings_manager from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode from langflow.interface.custom_lists import documentloaders_type_to_cls_dict -from langflow.settings import settings + from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -30,10 +31,12 @@ class DocumentLoaderCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ documentloader.__name__ for documentloader in self.type_to_loader_dict.values() - if documentloader.__name__ in settings.DOCUMENTLOADERS or settings.DEV + if documentloader.__name__ in settings_manager.settings.DOCUMENTLOADERS + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/embeddings/base.py b/src/backend/langflow/interface/embeddings/base.py index 7572a06cc..169985d37 100644 --- a/src/backend/langflow/interface/embeddings/base.py +++ b/src/backend/langflow/interface/embeddings/base.py @@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type from langflow.interface.base import LangChainTypeCreator from langflow.interface.custom_lists import embedding_type_to_cls_dict -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode from langflow.utils.logger import logger @@ -32,10 +33,12 @@ class EmbeddingCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ embedding.__name__ for embedding in self.type_to_loader_dict.values() - if embedding.__name__ in settings.EMBEDDINGS or settings.DEV + if embedding.__name__ in settings_manager.settings.EMBEDDINGS + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index fe3090f65..1cab1efbc 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -14,34 +14,43 @@ from langflow.interface.wrappers.base import wrapper_creator from langflow.interface.output_parsers.base import output_parser_creator from langflow.interface.retrievers.base import retriever_creator from langflow.interface.custom.base import custom_component_creator +from langflow.utils.lazy_load import LazyLoadDictBase -def get_type_dict(): - return { - "agents": agent_creator.to_list(), - "prompts": prompt_creator.to_list(), - "llms": llm_creator.to_list(), - "tools": tool_creator.to_list(), - "chains": chain_creator.to_list(), - "memory": memory_creator.to_list(), - "toolkits": toolkits_creator.to_list(), - "wrappers": wrapper_creator.to_list(), - "documentLoaders": documentloader_creator.to_list(), - "vectorStore": vectorstore_creator.to_list(), - "embeddings": embedding_creator.to_list(), - "textSplitters": textsplitter_creator.to_list(), - "utilities": utility_creator.to_list(), - "outputParsers": output_parser_creator.to_list(), - "retrievers": retriever_creator.to_list(), - "custom_components": custom_component_creator.to_list(), - } +class AllTypesDict(LazyLoadDictBase): + def __init__(self): + self._all_types_dict = None + + @property + def ALL_TYPES_DICT(self): + return self.all_types_dict + + def _build_dict(self): + langchain_types_dict = self.get_type_dict() + return { + **langchain_types_dict, + "Custom": ["Custom Tool", "Python Function"], + } + + def get_type_dict(self): + return { + "agents": agent_creator.to_list(), + "prompts": prompt_creator.to_list(), + "llms": llm_creator.to_list(), + "tools": tool_creator.to_list(), + "chains": chain_creator.to_list(), + "memory": memory_creator.to_list(), + "toolkits": toolkits_creator.to_list(), + "wrappers": wrapper_creator.to_list(), + "documentLoaders": documentloader_creator.to_list(), + "vectorStore": vectorstore_creator.to_list(), + "embeddings": embedding_creator.to_list(), + "textSplitters": textsplitter_creator.to_list(), + "utilities": utility_creator.to_list(), + "outputParsers": output_parser_creator.to_list(), + "retrievers": retriever_creator.to_list(), + "custom_components": custom_component_creator.to_list(), + } -LANGCHAIN_TYPES_DICT = get_type_dict() - -# Now we'll build a dict with Langchain types and ours - -ALL_TYPES_DICT = { - **LANGCHAIN_TYPES_DICT, - "Custom": ["Custom Tool", "Python Function"], -} +lazy_load_dict = AllTypesDict() diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py index 06aedd3cb..f562b99ed 100644 --- a/src/backend/langflow/interface/llms/base.py +++ b/src/backend/langflow/interface/llms/base.py @@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type from langflow.interface.base import LangChainTypeCreator from langflow.interface.custom_lists import llm_type_to_cls_dict -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.llms import LLMFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -33,10 +34,12 @@ class LLMCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ llm.__name__ for llm in self.type_to_loader_dict.values() - if llm.__name__ in settings.LLMS or settings.DEV + if llm.__name__ in settings_manager.settings.LLMS + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py index 9cd25381c..70665602c 100644 --- a/src/backend/langflow/interface/memories/base.py +++ b/src/backend/langflow/interface/memories/base.py @@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type from langflow.interface.base import LangChainTypeCreator from langflow.interface.custom_lists import memory_type_to_cls_dict -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.memories import MemoryFrontendNode from langflow.utils.logger import logger @@ -48,10 +49,12 @@ class MemoryCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ memory.__name__ for memory in self.type_to_loader_dict.values() - if memory.__name__ in settings.MEMORIES or settings.DEV + if memory.__name__ in settings_manager.settings.MEMORIES + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/output_parsers/base.py b/src/backend/langflow/interface/output_parsers/base.py index b5235ad58..256b521e1 100644 --- a/src/backend/langflow/interface/output_parsers/base.py +++ b/src/backend/langflow/interface/output_parsers/base.py @@ -4,7 +4,8 @@ from langchain import output_parsers from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class, build_template_from_method @@ -23,6 +24,7 @@ class OutputParserCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: if self.type_dict is None: + settings_manager = get_settings_manager() self.type_dict = { output_parser_name: import_class( f"langchain.output_parsers.{output_parser_name}" @@ -33,7 +35,8 @@ class OutputParserCreator(LangChainTypeCreator): self.type_dict = { name: output_parser for name, output_parser in self.type_dict.items() - if name in settings.OUTPUT_PARSERS or settings.DEV + if name in settings_manager.settings.OUTPUT_PARSERS + or settings_manager.settings.DEV } return self.type_dict diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index c062a4a35..5aa41dfb2 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -5,7 +5,8 @@ from langchain import prompts from langflow.custom.customs import get_custom_nodes from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.prompts import PromptFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -20,6 +21,7 @@ class PromptCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: + settings_manager = get_settings_manager() if self.type_dict is None: self.type_dict = { prompt_name: import_class(f"langchain.prompts.{prompt_name}") @@ -34,7 +36,8 @@ class PromptCreator(LangChainTypeCreator): self.type_dict = { name: prompt for name, prompt in self.type_dict.items() - if name in settings.PROMPTS or settings.DEV + if name in settings_manager.settings.PROMPTS + or settings_manager.settings.DEV } return self.type_dict diff --git a/src/backend/langflow/interface/retrievers/base.py b/src/backend/langflow/interface/retrievers/base.py index 759cd5916..db1cfd165 100644 --- a/src/backend/langflow/interface/retrievers/base.py +++ b/src/backend/langflow/interface/retrievers/base.py @@ -4,7 +4,8 @@ from langchain import retrievers from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.retrievers import RetrieverFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_method, build_template_from_class @@ -48,10 +49,12 @@ class RetrieverCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ retriever for retriever in self.type_to_loader_dict.keys() - if retriever in settings.RETRIEVERS or settings.DEV + if retriever in settings_manager.settings.RETRIEVERS + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index 97f47334e..cb0573bf7 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -1,4 +1,4 @@ -from langflow.cache.utils import memoize_dict +from langflow.services.cache.utils import memoize_dict from langflow.graph import Graph from langflow.utils.logger import logger diff --git a/src/backend/langflow/interface/text_splitters/base.py b/src/backend/langflow/interface/text_splitters/base.py index 787f20d82..87b778c4c 100644 --- a/src/backend/langflow/interface/text_splitters/base.py +++ b/src/backend/langflow/interface/text_splitters/base.py @@ -1,9 +1,10 @@ from typing import Dict, List, Optional, Type from langflow.interface.base import LangChainTypeCreator +from langflow.services.utils import get_settings_manager from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode from langflow.interface.custom_lists import textsplitter_type_to_cls_dict -from langflow.settings import settings + from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -30,10 +31,12 @@ class TextSplitterCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ textsplitter.__name__ for textsplitter in self.type_to_loader_dict.values() - if textsplitter.__name__ in settings.TEXTSPLITTERS or settings.DEV + if textsplitter.__name__ in settings_manager.settings.TEXTSPLITTERS + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py index b7c165a4d..c13ffdbd9 100644 --- a/src/backend/langflow/interface/toolkits/base.py +++ b/src/backend/langflow/interface/toolkits/base.py @@ -4,7 +4,8 @@ from langchain.agents import agent_toolkits from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class, import_module -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -29,13 +30,15 @@ class ToolkitCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: if self.type_dict is None: + settings_manager = get_settings_manager() self.type_dict = { toolkit_name: import_class( f"langchain.agents.agent_toolkits.{toolkit_name}" ) # if toolkit_name is not lower case it is a class for toolkit_name in agent_toolkits.__all__ - if not toolkit_name.islower() and toolkit_name in settings.TOOLKITS + if not toolkit_name.islower() + and toolkit_name in settings_manager.settings.TOOLKITS } return self.type_dict diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 8c9158c05..1dbc9a6ed 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -15,7 +15,8 @@ from langflow.interface.tools.constants import ( OTHER_TOOLS, ) from langflow.interface.tools.util import get_tool_params -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.field.base import TemplateField from langflow.template.template.base import Template from langflow.utils import util @@ -66,6 +67,7 @@ class ToolCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: + settings_manager = get_settings_manager() if self.tools_dict is None: all_tools = {} @@ -74,7 +76,10 @@ class ToolCreator(LangChainTypeCreator): tool_name = tool_params.get("name") or tool - if tool_name in settings.TOOLS or settings.DEV: + if ( + tool_name in settings_manager.settings.TOOLS + or settings_manager.settings.DEV + ): if tool_name == "JsonSpec": tool_params["path"] = tool_params.pop("dict_") # type: ignore all_tools[tool_name] = { diff --git a/src/backend/langflow/interface/utilities/base.py b/src/backend/langflow/interface/utilities/base.py index b0ee4d4be..eb8cd60af 100644 --- a/src/backend/langflow/interface/utilities/base.py +++ b/src/backend/langflow/interface/utilities/base.py @@ -5,7 +5,8 @@ from langchain import SQLDatabase, utilities from langflow.custom.customs import get_custom_nodes from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.utilities import UtilitiesFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_class @@ -26,6 +27,7 @@ class UtilityCreator(LangChainTypeCreator): from the langchain.chains module and filtering them according to the settings.utilities list. """ if self.type_dict is None: + settings_manager = get_settings_manager() self.type_dict = { utility_name: import_class(f"langchain.utilities.{utility_name}") for utility_name in utilities.__all__ @@ -35,7 +37,8 @@ class UtilityCreator(LangChainTypeCreator): self.type_dict = { name: utility for name, utility in self.type_dict.items() - if name in settings.UTILITIES or settings.DEV + if name in settings_manager.settings.UTILITIES + or settings_manager.settings.DEV } return self.type_dict diff --git a/src/backend/langflow/interface/utils.py b/src/backend/langflow/interface/utils.py index d6c7b9023..1fddbf80f 100644 --- a/src/backend/langflow/interface/utils.py +++ b/src/backend/langflow/interface/utils.py @@ -9,7 +9,8 @@ import yaml from langchain.base_language import BaseLanguageModel from PIL.Image import Image from langflow.utils.logger import logger -from langflow.chat.config import ChatConfig +from langflow.services.chat.config import ChatConfig +from langflow.services.utils import get_settings_manager def load_file_into_dict(file_path: str) -> dict: @@ -63,13 +64,11 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]: def setup_llm_caching(): """Setup LLM caching.""" - - from langflow.settings import settings - + settings_manager = get_settings_manager() try: - set_langchain_cache(settings) + set_langchain_cache(settings_manager.settings) except ImportError: - logger.warning(f"Could not import {settings.CACHE}. ") + logger.warning(f"Could not import {settings_manager.settings.CACHE}. ") except Exception as exc: logger.warning(f"Could not setup LLM caching. Error: {exc}") diff --git a/src/backend/langflow/interface/vector_store/base.py b/src/backend/langflow/interface/vector_store/base.py index 4a937ba89..4b8ca2b64 100644 --- a/src/backend/langflow/interface/vector_store/base.py +++ b/src/backend/langflow/interface/vector_store/base.py @@ -4,7 +4,8 @@ from langchain import vectorstores from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class -from langflow.settings import settings +from langflow.services.utils import get_settings_manager + from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode from langflow.utils.logger import logger from langflow.utils.util import build_template_from_method @@ -43,10 +44,12 @@ class VectorstoreCreator(LangChainTypeCreator): return None def to_list(self) -> List[str]: + settings_manager = get_settings_manager() return [ vectorstore for vectorstore in self.type_to_loader_dict.keys() - if vectorstore in settings.VECTORSTORES or settings.DEV + if vectorstore in settings_manager.settings.VECTORSTORES + or settings_manager.settings.DEV ] diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 5b3341693..1702fb8f9 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,13 +6,15 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from langflow.api import router -from langflow.database.base import create_db_and_tables, Engine from langflow.interface.utils import setup_llm_caching +from langflow.services.database.utils import initialize_database +from langflow.services.manager import initialize_services from langflow.utils.logger import configure def create_app(): """Create the FastAPI app and include the router.""" + configure() app = FastAPI() @@ -32,10 +34,10 @@ def create_app(): allow_methods=["*"], allow_headers=["*"], ) - app.include_router(router) - app.on_event("startup")(Engine.update) - app.on_event("startup")(create_db_and_tables) + + app.on_event("startup")(initialize_services) + app.on_event("startup")(initialize_database) app.on_event("startup")(setup_llm_caching) return app @@ -68,16 +70,19 @@ def get_static_files_dir(): return frontend_path / "frontend" -def setup_app(static_files_dir: Optional[Path] = None) -> FastAPI: +def setup_app( + static_files_dir: Optional[Path] = None, backend_only: bool = False +) -> FastAPI: """Setup the FastAPI app.""" # get the directory of the current file if not static_files_dir: static_files_dir = get_static_files_dir() - if not static_files_dir or not static_files_dir.exists(): + if not backend_only and (not static_files_dir or not static_files_dir.exists()): raise RuntimeError(f"Static files directory {static_files_dir} does not exist.") app = create_app() - setup_static_files(app, static_files_dir) + if not backend_only and static_files_dir is not None: + setup_static_files(app, static_files_dir) return app diff --git a/src/backend/langflow/services/__init__.py b/src/backend/langflow/services/__init__.py new file mode 100644 index 000000000..8ac74b5b9 --- /dev/null +++ b/src/backend/langflow/services/__init__.py @@ -0,0 +1,4 @@ +from .manager import service_manager +from .schema import ServiceType + +__all__ = ["service_manager", "ServiceType"] diff --git a/src/backend/langflow/services/base.py b/src/backend/langflow/services/base.py new file mode 100644 index 000000000..6bca6c4e2 --- /dev/null +++ b/src/backend/langflow/services/base.py @@ -0,0 +1,2 @@ +class Service: + name: str diff --git a/src/backend/langflow/services/cache/__init__.py b/src/backend/langflow/services/cache/__init__.py new file mode 100644 index 000000000..79e143807 --- /dev/null +++ b/src/backend/langflow/services/cache/__init__.py @@ -0,0 +1,11 @@ +from . import factory, manager +from langflow.services.cache.manager import cache_manager +from langflow.services.cache.flow import InMemoryCache + + +__all__ = [ + "cache_manager", + "factory", + "manager", + "InMemoryCache", +] diff --git a/src/backend/langflow/cache/base.py b/src/backend/langflow/services/cache/base.py similarity index 100% rename from src/backend/langflow/cache/base.py rename to src/backend/langflow/services/cache/base.py diff --git a/src/backend/langflow/services/cache/factory.py b/src/backend/langflow/services/cache/factory.py new file mode 100644 index 000000000..77f8d58d1 --- /dev/null +++ b/src/backend/langflow/services/cache/factory.py @@ -0,0 +1,11 @@ +from langflow.services.cache.manager import CacheManager +from langflow.services.factory import ServiceFactory + + +class CacheManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(CacheManager) + + def create(self, settings_service): + # Here you would have logic to create and configure a CacheManager + return CacheManager() diff --git a/src/backend/langflow/cache/flow.py b/src/backend/langflow/services/cache/flow.py similarity index 98% rename from src/backend/langflow/cache/flow.py rename to src/backend/langflow/services/cache/flow.py index 6d8fee977..0c10c51e1 100644 --- a/src/backend/langflow/cache/flow.py +++ b/src/backend/langflow/services/cache/flow.py @@ -2,7 +2,7 @@ import threading import time from collections import OrderedDict -from langflow.cache.base import BaseCache +from langflow.services.cache.base import BaseCache class InMemoryCache(BaseCache): diff --git a/src/backend/langflow/cache/manager.py b/src/backend/langflow/services/cache/manager.py similarity index 97% rename from src/backend/langflow/cache/manager.py rename to src/backend/langflow/services/cache/manager.py index 13b281008..ce9a338ef 100644 --- a/src/backend/langflow/cache/manager.py +++ b/src/backend/langflow/services/cache/manager.py @@ -1,5 +1,6 @@ from contextlib import contextmanager from typing import Any, Awaitable, Callable, List, Optional +from langflow.services.base import Service import pandas as pd from PIL import Image @@ -49,9 +50,11 @@ class AsyncSubject: await observer() -class CacheManager(Subject): +class CacheManager(Subject, Service): """Manages cache for different clients and notifies observers on changes.""" + name = "cache_manager" + def __init__(self): super().__init__() self._cache = {} diff --git a/src/backend/langflow/cache/utils.py b/src/backend/langflow/services/cache/utils.py similarity index 100% rename from src/backend/langflow/cache/utils.py rename to src/backend/langflow/services/cache/utils.py diff --git a/src/backend/langflow/chat/__init__.py b/src/backend/langflow/services/chat/__init__.py similarity index 100% rename from src/backend/langflow/chat/__init__.py rename to src/backend/langflow/services/chat/__init__.py diff --git a/src/backend/langflow/chat/config.py b/src/backend/langflow/services/chat/config.py similarity index 100% rename from src/backend/langflow/chat/config.py rename to src/backend/langflow/services/chat/config.py diff --git a/src/backend/langflow/services/chat/factory.py b/src/backend/langflow/services/chat/factory.py new file mode 100644 index 000000000..03597ed11 --- /dev/null +++ b/src/backend/langflow/services/chat/factory.py @@ -0,0 +1,11 @@ +from langflow.services.chat.manager import ChatManager +from langflow.services.factory import ServiceFactory + + +class ChatManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(ChatManager) + + def create(self, settings_service): + # Here you would have logic to create and configure a ChatManager + return ChatManager() diff --git a/src/backend/langflow/chat/manager.py b/src/backend/langflow/services/chat/manager.py similarity index 95% rename from src/backend/langflow/chat/manager.py rename to src/backend/langflow/services/chat/manager.py index 2c3427a12..a49f48273 100644 --- a/src/backend/langflow/chat/manager.py +++ b/src/backend/langflow/services/chat/manager.py @@ -1,10 +1,12 @@ from collections import defaultdict from fastapi import WebSocket, status from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse -from langflow.cache import cache_manager -from langflow.cache.manager import Subject -from langflow.chat.utils import process_graph +from langflow.services.base import Service +from langflow.services import service_manager +from langflow.services.cache.manager import Subject +from langflow.services.chat.utils import process_graph from langflow.interface.utils import pil_to_base64 +from langflow.services.schema import ServiceType from langflow.utils.logger import logger @@ -12,7 +14,7 @@ import asyncio import json from typing import Any, Dict, List -from langflow.cache.flow import InMemoryCache +from langflow.services.cache.flow import InMemoryCache class ChatHistory(Subject): @@ -42,11 +44,13 @@ class ChatHistory(Subject): self.history[client_id] = [] -class ChatManager: +class ChatManager(Service): + name = "chat_manager" + def __init__(self): self.active_connections: Dict[str, WebSocket] = {} self.chat_history = ChatHistory() - self.cache_manager = cache_manager + self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER) self.cache_manager.attach(self.update) self.in_memory_cache = InMemoryCache() diff --git a/src/backend/langflow/chat/utils.py b/src/backend/langflow/services/chat/utils.py similarity index 100% rename from src/backend/langflow/chat/utils.py rename to src/backend/langflow/services/chat/utils.py diff --git a/src/backend/langflow/database/__init__.py b/src/backend/langflow/services/database/__init__.py similarity index 100% rename from src/backend/langflow/database/__init__.py rename to src/backend/langflow/services/database/__init__.py diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py new file mode 100644 index 000000000..fecf24543 --- /dev/null +++ b/src/backend/langflow/services/database/factory.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING +from langflow.services.database.manager import DatabaseManager +from langflow.services.factory import ServiceFactory + +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsManager + + +class DatabaseManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(DatabaseManager) + + def create(self, settings_service: "SettingsManager"): + # Here you would have logic to create and configure a DatabaseManager + if not settings_service.settings.DATABASE_URL: + raise ValueError("No database URL provided") + return DatabaseManager(settings_service.settings.DATABASE_URL) diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py new file mode 100644 index 000000000..92385a457 --- /dev/null +++ b/src/backend/langflow/services/database/manager.py @@ -0,0 +1,67 @@ +from pathlib import Path +from langflow.services.base import Service +from sqlmodel import SQLModel, Session, create_engine +from langflow.utils.logger import logger +from alembic.config import Config +from alembic import command +from langflow.services.database import models # noqa + + +class DatabaseManager(Service): + name = "database_manager" + + def __init__(self, database_url: str): + self.database_url = database_url + # This file is in langflow.services.database.manager.py + # the ini is in langflow + langflow_dir = Path(__file__).parent.parent.parent + self.script_location = langflow_dir / "alembic" + self.alembic_cfg_path = langflow_dir / "alembic.ini" + self.engine = create_engine(database_url) + + def __enter__(self): + self._session = Session(self.engine) + return self._session + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: # If an exception has been raised + logger.error( + f"Session rollback because of exception: {exc_type.__name__} {exc_value}" + ) + self._session.rollback() + else: + self._session.commit() + self._session.close() + + def get_session(self): + with Session(self.engine) as session: + yield session + + def run_migrations(self): + logger.info( + f"Running DB migrations in {self.script_location} on {self.database_url}" + ) + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", str(self.script_location)) + alembic_cfg.set_main_option("sqlalchemy.url", self.database_url) + command.upgrade(alembic_cfg, "head") + + def create_db_and_tables(self): + logger.debug("Creating database and tables") + try: + SQLModel.metadata.create_all(self.engine) + except Exception as exc: + logger.error(f"Error creating database and tables: {exc}") + raise RuntimeError("Error creating database and tables") from exc + + # Now check if the table "flow" exists, if not, something went wrong + # and we need to create the tables again. + from sqlalchemy import inspect + + inspector = inspect(self.engine) + if "flow" not in inspector.get_table_names(): + logger.error("Something went wrong creating the database and tables.") + logger.error("Please check your database settings.") + raise RuntimeError("Something went wrong creating the database and tables.") + else: + logger.debug("Database and tables created successfully") diff --git a/src/backend/langflow/services/database/models/__init__.py b/src/backend/langflow/services/database/models/__init__.py new file mode 100644 index 000000000..da47bc5fe --- /dev/null +++ b/src/backend/langflow/services/database/models/__init__.py @@ -0,0 +1,4 @@ +from .flow import Flow + + +__all__ = ["Flow"] diff --git a/src/backend/langflow/database/models/base.py b/src/backend/langflow/services/database/models/base.py similarity index 100% rename from src/backend/langflow/database/models/base.py rename to src/backend/langflow/services/database/models/base.py diff --git a/src/backend/langflow/database/models/component.py b/src/backend/langflow/services/database/models/component.py similarity index 92% rename from src/backend/langflow/database/models/component.py rename to src/backend/langflow/services/database/models/component.py index bb2408cdb..5c4e6c13a 100644 --- a/src/backend/langflow/database/models/component.py +++ b/src/backend/langflow/services/database/models/component.py @@ -1,4 +1,4 @@ -from langflow.database.models.base import SQLModelSerializable, SQLModel +from langflow.services.database.models.base import SQLModelSerializable, SQLModel from sqlmodel import Field from typing import Optional from datetime import datetime diff --git a/src/backend/langflow/database/models/flow.py b/src/backend/langflow/services/database/models/flow.py similarity index 72% rename from src/backend/langflow/database/models/flow.py rename to src/backend/langflow/services/database/models/flow.py index f9e3aa249..2bc83f9dc 100644 --- a/src/backend/langflow/database/models/flow.py +++ b/src/backend/langflow/services/database/models/flow.py @@ -1,13 +1,12 @@ # Path: src/backend/langflow/database/models/flow.py -from langflow.database.models.base import SQLModelSerializable +from langflow.services.database.models.base import SQLModelSerializable from pydantic import validator -from sqlmodel import Field, Relationship, JSON, Column +from sqlmodel import Field, JSON, Column from uuid import UUID, uuid4 from typing import Dict, Optional # if TYPE_CHECKING: -from langflow.database.models.flow_style import FlowStyle, FlowStyleRead class FlowBase(SQLModelSerializable): @@ -35,11 +34,6 @@ class FlowBase(SQLModelSerializable): class Flow(FlowBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) - style: Optional["FlowStyle"] = Relationship( - back_populates="flow", - # use "uselist=False" to make it a one-to-one relationship - sa_relationship_kwargs={"uselist": False}, - ) class FlowCreate(FlowBase): @@ -50,10 +44,6 @@ class FlowRead(FlowBase): id: UUID -class FlowReadWithStyle(FlowRead): - style: Optional["FlowStyleRead"] = None - - class FlowUpdate(SQLModelSerializable): name: Optional[str] = None description: Optional[str] = None diff --git a/src/backend/langflow/services/database/utils.py b/src/backend/langflow/services/database/utils.py new file mode 100644 index 000000000..20b2bbbb4 --- /dev/null +++ b/src/backend/langflow/services/database/utils.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING +from langflow.utils.logger import logger +from contextlib import contextmanager + +from sqlmodel import Session + +if TYPE_CHECKING: + from langflow.services.database.manager import DatabaseManager + + +def initialize_database(): + logger.debug("Initializing database") + from langflow.services import service_manager, ServiceType + + database_manager = service_manager.get(ServiceType.DATABASE_MANAGER) + database_manager.run_migrations() + database_manager.create_db_and_tables() + logger.debug("Database initialized") + + +@contextmanager +def session_getter(db_manager: "DatabaseManager"): + try: + session = Session(db_manager.engine) + yield session + except Exception as e: + print("Session rollback because of exception:", e) + session.rollback() + raise + finally: + session.close() diff --git a/src/backend/langflow/services/factory.py b/src/backend/langflow/services/factory.py new file mode 100644 index 000000000..c37f4e9c2 --- /dev/null +++ b/src/backend/langflow/services/factory.py @@ -0,0 +1,6 @@ +class ServiceFactory: + def __init__(self, service_class): + self.service_class = service_class + + def create(self, *args, **kwargs): + raise NotImplementedError diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py new file mode 100644 index 000000000..1606b3a82 --- /dev/null +++ b/src/backend/langflow/services/manager.py @@ -0,0 +1,87 @@ +from langflow.services.schema import ServiceType +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langflow.services.factory import ServiceFactory + + +class ServiceManager: + """ + Manages the creation of different services. + """ + + def __init__(self): + self.services = {} + self.factories = {} + + def register_factory(self, service_factory: "ServiceFactory"): + """ + Registers a new factory. + """ + self.factories[service_factory.service_class.name] = service_factory + + def get(self, service_name: ServiceType): + """ + Get (or create) a service by its name. + """ + if service_name not in self.services: + self._create_service(service_name) + + return self.services[service_name] + + def _create_service(self, service_name: ServiceType): + """ + Create a new service given its name. + """ + self._validate_service_creation(service_name) + + if service_name == ServiceType.SETTINGS_MANAGER: + self.services[service_name] = self.factories[service_name].create() + else: + settings_service = self.get(ServiceType.SETTINGS_MANAGER) + self.services[service_name] = self.factories[service_name].create( + settings_service + ) + + def _validate_service_creation(self, service_name: ServiceType): + """ + Validate whether the service can be created. + """ + if service_name not in self.factories: + raise ValueError( + f"No factory registered for the service class '{service_name.name}'" + ) + + if ( + ServiceType.SETTINGS_MANAGER not in self.factories + and service_name != ServiceType.SETTINGS_MANAGER + ): + raise ValueError( + f"Cannot create service '{service_name.name}' before the settings service" + ) + + def update(self, service_name: ServiceType): + """ + Update a service by its name. + """ + if service_name in self.services: + self.services.pop(service_name, None) + self.get(service_name) + + +service_manager = ServiceManager() + + +def initialize_services(): + """ + Initialize all the services needed. + """ + from langflow.services.database import factory as database_factory + from langflow.services.cache import factory as cache_factory + from langflow.services.chat import factory as chat_factory + from langflow.services.settings import factory as settings_factory + + service_manager.register_factory(settings_factory.SettingsManagerFactory()) + service_manager.register_factory(database_factory.DatabaseManagerFactory()) + service_manager.register_factory(cache_factory.CacheManagerFactory()) + service_manager.register_factory(chat_factory.ChatManagerFactory()) diff --git a/src/backend/langflow/services/schema.py b/src/backend/langflow/services/schema.py new file mode 100644 index 000000000..695763afc --- /dev/null +++ b/src/backend/langflow/services/schema.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class ServiceType(str, Enum): + """ + Enum for the different types of services that can be + registered with the service manager. + """ + + CACHE_MANAGER = "cache_manager" + SETTINGS_MANAGER = "settings_manager" + DATABASE_MANAGER = "database_manager" + CHAT_MANAGER = "chat_manager" diff --git a/src/backend/langflow/services/settings/__init__.py b/src/backend/langflow/services/settings/__init__.py new file mode 100644 index 000000000..2191bf2cc --- /dev/null +++ b/src/backend/langflow/services/settings/__init__.py @@ -0,0 +1,3 @@ +from . import factory, manager + +__all__ = ["factory", "manager"] diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py new file mode 100644 index 000000000..1eb2793b3 --- /dev/null +++ b/src/backend/langflow/services/settings/base.py @@ -0,0 +1,168 @@ +import contextlib +import json +import os +from typing import Optional, List +from pathlib import Path + +import yaml +from pydantic import BaseSettings, root_validator, validator +from langflow.utils.logger import logger + +BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components") + + +class Settings(BaseSettings): + CHAINS: dict = {} + AGENTS: dict = {} + PROMPTS: dict = {} + LLMS: dict = {} + TOOLS: dict = {} + MEMORIES: dict = {} + EMBEDDINGS: dict = {} + VECTORSTORES: dict = {} + DOCUMENTLOADERS: dict = {} + WRAPPERS: dict = {} + RETRIEVERS: dict = {} + TOOLKITS: dict = {} + TEXTSPLITTERS: dict = {} + UTILITIES: dict = {} + OUTPUT_PARSERS: dict = {} + CUSTOM_COMPONENTS: dict = {} + + DEV: bool = False + DATABASE_URL: Optional[str] = None + CACHE: str = "InMemoryCache" + REMOVE_API_KEYS: bool = False + COMPONENTS_PATH: List[str] = [] + + @validator("DATABASE_URL", pre=True) + def set_database_url(cls, value): + if not value: + logger.debug( + "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" + ) + if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): + value = langflow_database_url + logger.debug("Using LANGFLOW_DATABASE_URL env variable.") + else: + logger.debug("No DATABASE_URL env variable, using sqlite database") + value = "sqlite:///./langflow.db" + + return value + + @validator("COMPONENTS_PATH", pre=True) + def set_components_path(cls, value): + if os.getenv("LANGFLOW_COMPONENTS_PATH"): + logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") + langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") + if ( + Path(langflow_component_path).exists() + and langflow_component_path not in value + ): + if isinstance(langflow_component_path, list): + for path in langflow_component_path: + if path not in value: + value.append(path) + logger.debug( + f"Extending {langflow_component_path} to components_path" + ) + elif langflow_component_path not in value: + value.append(langflow_component_path) + logger.debug( + f"Appending {langflow_component_path} to components_path" + ) + + if not value: + value = [BASE_COMPONENTS_PATH] + logger.debug("Setting default components path to components_path") + elif BASE_COMPONENTS_PATH not in value: + value.append(BASE_COMPONENTS_PATH) + logger.debug("Adding default components path to components_path") + + logger.debug(f"Components path: {value}") + return value + + class Config: + validate_assignment = True + extra = "ignore" + env_prefix = "LANGFLOW_" + + @root_validator(allow_reuse=True) + def validate_lists(cls, values): + for key, value in values.items(): + if key != "dev" and not value: + values[key] = [] + return values + + def update_from_yaml(self, file_path: str, dev: bool = False): + new_settings = load_settings_from_yaml(file_path) + self.CHAINS = new_settings.CHAINS or {} + self.AGENTS = new_settings.AGENTS or {} + self.PROMPTS = new_settings.PROMPTS or {} + self.LLMS = new_settings.LLMS or {} + self.TOOLS = new_settings.TOOLS or {} + self.MEMORIES = new_settings.MEMORIES or {} + self.WRAPPERS = new_settings.WRAPPERS or {} + self.TOOLKITS = new_settings.TOOLKITS or {} + self.TEXTSPLITTERS = new_settings.TEXTSPLITTERS or {} + self.UTILITIES = new_settings.UTILITIES or {} + self.EMBEDDINGS = new_settings.EMBEDDINGS or {} + self.VECTORSTORES = new_settings.VECTORSTORES or {} + self.DOCUMENTLOADERS = new_settings.DOCUMENTLOADERS or {} + self.RETRIEVERS = new_settings.RETRIEVERS or {} + self.OUTPUT_PARSERS = new_settings.OUTPUT_PARSERS or {} + self.CUSTOM_COMPONENTS = new_settings.CUSTOM_COMPONENTS or {} + self.COMPONENTS_PATH = new_settings.COMPONENTS_PATH or [] + self.DEV = dev + + def update_settings(self, **kwargs): + logger.debug("Updating settings") + for key, value in kwargs.items(): + # value may contain sensitive information, so we don't want to log it + if not hasattr(self, key): + logger.debug(f"Key {key} not found in settings") + continue + logger.debug(f"Updating {key}") + if isinstance(getattr(self, key), list): + # value might be a '[something]' string + with contextlib.suppress(json.decoder.JSONDecodeError): + value = json.loads(str(value)) + if isinstance(value, list): + for item in value: + if item not in getattr(self, key): + getattr(self, key).append(item) + logger.debug(f"Extended {key}") + else: + getattr(self, key).append(value) + logger.debug(f"Appended {key}") + + else: + setattr(self, key, value) + logger.debug(f"Updated {key}") + logger.debug(f"{key}: {getattr(self, key)}") + + +def save_settings_to_yaml(settings: Settings, file_path: str): + with open(file_path, "w") as f: + settings_dict = settings.dict() + yaml.dump(settings_dict, f) + + +def load_settings_from_yaml(file_path: str) -> Settings: + # Check if a string is a valid path or a file name + if "/" not in file_path: + # Get current path + current_path = os.path.dirname(os.path.abspath(__file__)) + + file_path = os.path.join(current_path, file_path) + + with open(file_path, "r") as f: + settings_dict = yaml.safe_load(f) + settings_dict = {k.upper(): v for k, v in settings_dict.items()} + + for key in settings_dict: + if key not in Settings.__fields__.keys(): + raise KeyError(f"Key {key} not found in settings") + logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") + + return Settings(**settings_dict) diff --git a/src/backend/langflow/services/settings/factory.py b/src/backend/langflow/services/settings/factory.py new file mode 100644 index 000000000..ab22e22b8 --- /dev/null +++ b/src/backend/langflow/services/settings/factory.py @@ -0,0 +1,15 @@ +from pathlib import Path +from langflow.services.settings.manager import SettingsManager +from langflow.services.factory import ServiceFactory + + +class SettingsManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(SettingsManager) + + def create(self): + # Here you would have logic to create and configure a SettingsManager + langflow_dir = Path(__file__).parent.parent.parent + return SettingsManager.load_settings_from_yaml( + str(langflow_dir / "config.yaml") + ) diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py new file mode 100644 index 000000000..a357c4804 --- /dev/null +++ b/src/backend/langflow/services/settings/manager.py @@ -0,0 +1,36 @@ +from langflow.services.base import Service +from langflow.services.settings.base import Settings +from langflow.utils.logger import logger +import os +import yaml + + +class SettingsManager(Service): + name = "settings_manager" + + def __init__(self, settings: Settings): + super().__init__() + self.settings = settings + + @classmethod + def load_settings_from_yaml(cls, file_path: str) -> "SettingsManager": + # Check if a string is a valid path or a file name + if "/" not in file_path: + # Get current path + current_path = os.path.dirname(os.path.abspath(__file__)) + + file_path = os.path.join(current_path, file_path) + + with open(file_path, "r") as f: + settings_dict = yaml.safe_load(f) + settings_dict = {k.upper(): v for k, v in settings_dict.items()} + + for key in settings_dict: + if key not in Settings.__fields__.keys(): + raise KeyError(f"Key {key} not found in settings") + logger.debug( + f"Loading {len(settings_dict[key])} {key} from {file_path}" + ) + + settings = Settings(**settings_dict) + return cls(settings) diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/services/settings/settings.py similarity index 100% rename from src/backend/langflow/settings.py rename to src/backend/langflow/services/settings/settings.py diff --git a/src/backend/langflow/services/utils.py b/src/backend/langflow/services/utils.py new file mode 100644 index 000000000..049e82c0f --- /dev/null +++ b/src/backend/langflow/services/utils.py @@ -0,0 +1,18 @@ +from langflow.services import ServiceType, service_manager +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsManager + + +def get_settings_manager() -> "SettingsManager": + return service_manager.get(ServiceType.SETTINGS_MANAGER) + + +def get_db_manager(): + return service_manager.get(ServiceType.DATABASE_MANAGER) + + +def get_session(): + db_manager = service_manager.get(ServiceType.DATABASE_MANAGER) + yield from db_manager.get_session() diff --git a/src/backend/langflow/utils/lazy_load.py b/src/backend/langflow/utils/lazy_load.py new file mode 100644 index 000000000..df0130acc --- /dev/null +++ b/src/backend/langflow/utils/lazy_load.py @@ -0,0 +1,15 @@ +class LazyLoadDictBase: + def __init__(self): + self._all_types_dict = None + + @property + def all_types_dict(self): + if self._all_types_dict is None: + self._all_types_dict = self._build_dict() + return self._all_types_dict + + def _build_dict(self): + raise NotImplementedError + + def get_type_dict(self): + raise NotImplementedError diff --git a/src/frontend/.dockerignore b/src/frontend/.dockerignore index 600e365ec..ca5762007 100644 --- a/src/frontend/.dockerignore +++ b/src/frontend/.dockerignore @@ -1 +1,2 @@ -**/node_modules \ No newline at end of file +**/node_modules +**/build \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 45a8f8f1f..e90d03d0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from contextlib import contextmanager import json from pathlib import Path -from typing import AsyncGenerator +from typing import AsyncGenerator, TYPE_CHECKING from langflow.api.v1.flows import get_session from langflow.graph.graph.base import Graph @@ -10,6 +10,10 @@ from fastapi.testclient import TestClient from httpx import AsyncClient from sqlmodel import SQLModel, Session, create_engine from sqlmodel.pool import StaticPool +from typer.testing import CliRunner + +if TYPE_CHECKING: + from langflow.services.database.manager import DatabaseManager def pytest_configure(): @@ -114,8 +118,8 @@ def client_fixture(session: Session): app = create_app() app.dependency_overrides[get_session] = get_session_override - - yield TestClient(app) + with TestClient(app) as client: + yield client app.dependency_overrides.clear() @@ -134,15 +138,20 @@ def client_fixture(session: Session): # create a fixture for session_getter above @pytest.fixture(name="session_getter") -def session_getter_fixture(): +def session_getter_fixture(client): engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool ) SQLModel.metadata.create_all(engine) @contextmanager - def blank_session_getter(): - with Session(engine) as session: + def blank_session_getter(db_manager: "DatabaseManager"): + with Session(db_manager.engine) as session: yield session yield blank_session_getter + + +@pytest.fixture +def runner(): + return CliRunner() diff --git a/tests/test_cache_manager.py b/tests/test_cache_manager.py index f3e65481e..660512634 100644 --- a/tests/test_cache_manager.py +++ b/tests/test_cache_manager.py @@ -2,7 +2,7 @@ from io import StringIO import pandas as pd import pytest -from langflow.cache.manager import CacheManager +from langflow.services.cache.manager import CacheManager from PIL import Image diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 000000000..408500d7a --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,30 @@ +from pathlib import Path +from tempfile import tempdir +from langflow.__main__ import app +import pytest + +from langflow.services import utils + + +@pytest.fixture(scope="module") +def default_settings(): + return [ + "--backend-only", + "--no-open-browser", + ] + + +def test_components_path(runner, client, default_settings): + # Create a foldr in the tmp directory + temp_dir = Path(tempdir) + # create a "components" folder + temp_dir = temp_dir / "components" + temp_dir.mkdir(exist_ok=True) + + result = runner.invoke( + app, + ["--components-path", str(temp_dir), *default_settings], + ) + assert result.exit_code == 0, result.stdout + settings_manager = utils.get_settings_manager() + assert temp_dir in settings_manager.settings.COMPONENTS_PATH diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index 199906dda..f20311cec 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -5,7 +5,7 @@ from uuid import uuid4 from fastapi import HTTPException -from langflow.database.models.flow import Flow, FlowCreate +from langflow.services.database.models.flow import Flow, FlowCreate from langflow.interface.custom.base import CustomComponent from langflow.interface.custom.component import ( Component, diff --git a/tests/test_database.py b/tests/test_database.py index bc512b6b0..52a5daa4c 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -5,16 +5,9 @@ from uuid import UUID, uuid4 from sqlalchemy.orm import Session from fastapi.testclient import TestClient -from fastapi.encoders import jsonable_encoder from langflow.api.v1.schemas import FlowListCreate -from langflow.database.models.flow import Flow, FlowCreate, FlowUpdate - -from langflow.database.models.flow_style import ( - FlowStyleCreate, - FlowStyleRead, - FlowStyleUpdate, -) +from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate @pytest.fixture(scope="module") @@ -56,33 +49,12 @@ def test_read_flows(client: TestClient, json_flow: str): assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - flow_style = FlowStyleCreate(color="red", emoji="👍", flow_id=response.json()["id"]) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - assert response.json()["color"] == flow_style.color - assert response.json()["emoji"] == flow_style.emoji - assert response.json()["flow_id"] == str(flow_style.flow_id) - flow = FlowCreate(name="Test Flow", description="description", data=data) response = client.post("api/v1/flows/", json=flow.dict()) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - # Now we need to create FlowStyle objects for each Flow - flow_style = FlowStyleCreate( - color="green", emoji="👍", flow_id=response.json()["id"] - ) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - assert response.json()["color"] == flow_style.color - assert response.json()["emoji"] == flow_style.emoji - assert response.json()["flow_id"] == str(flow_style.flow_id) - response = client.get("api/v1/flows/") assert response.status_code == 200 assert len(response.json()) > 0 @@ -97,21 +69,10 @@ def test_read_flow(client: TestClient, json_flow: str): # turn it into a UUID flow_id = UUID(flow_id) - flow_style = FlowStyleCreate(color="green", emoji="👍", flow_id=flow_id) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - response_json = response.json() - assert response_json["color"] == flow_style.color - assert response_json["emoji"] == flow_style.emoji - assert response_json["flow_id"] == str(flow_style.flow_id) - response = client.get(f"api/v1/flows/{flow_id}") assert response.status_code == 200 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - assert response.json()["style"]["color"] == flow_style.color def test_update_flow(client: TestClient, json_flow: str): @@ -275,66 +236,3 @@ def test_read_empty_flows(client: TestClient): response = client.get("api/v1/flows/") assert response.status_code == 200 assert len(response.json()) == 0 - - -def test_create_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="🔴") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - assert response.status_code == 200 - created_flow_style = FlowStyleRead(**response.json()) - assert created_flow_style.color == flow_style.color - assert created_flow_style.emoji == flow_style.emoji - - -def test_read_flow_styles(client: TestClient): - response = client.get("api/v1/flow_styles/") - assert response.status_code == 200 - flow_styles = [FlowStyleRead(**flow_style) for flow_style in response.json()] - assert not flow_styles - # Create test data - flow_style = FlowStyleCreate(color="red", emoji="🔴") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - assert response.status_code == 200 - # Check response data - response = client.get("api/v1/flow_styles/") - assert response.status_code == 200 - flow_styles = [FlowStyleRead(**flow_style) for flow_style in response.json()] - assert len(flow_styles) == 1 - assert flow_styles[0].color == flow_style.color - assert flow_styles[0].emoji == flow_style.emoji - - -def test_read_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="🔴") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - response = client.get(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 200 - read_flow_style = FlowStyleRead(**response.json()) - assert read_flow_style == created_flow_style - - -def test_update_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="🔴") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - to_update_flow_style = FlowStyleUpdate(color="blue") - response = client.patch( - f"api/v1/flow_styles/{created_flow_style.id}", json=to_update_flow_style.dict() - ) - assert response.status_code == 200 - updated_flow_style = FlowStyleRead(**response.json()) - assert updated_flow_style.color == "blue" - assert updated_flow_style.emoji == flow_style.emoji - - -def test_delete_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="🔴") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - response = client.delete(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 200 - assert response.json() == {"message": "FlowStyle deleted successfully"} - - response = client.get(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 404 diff --git a/tests/test_llms_template.py b/tests/test_llms_template.py index d8f9e96f3..f1b76e18e 100644 --- a/tests/test_llms_template.py +++ b/tests/test_llms_template.py @@ -1,13 +1,14 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager def test_llms_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() llms = json_response["llms"] - assert set(llms.keys()) == set(settings.LLMS) + assert set(llms.keys()) == set(settings_manager.settings.LLMS) # def test_hugging_face_hub(client: TestClient): diff --git a/tests/test_prompts_template.py b/tests/test_prompts_template.py index fa7a683bd..dde313c20 100644 --- a/tests/test_prompts_template.py +++ b/tests/test_prompts_template.py @@ -1,13 +1,14 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager def test_prompts_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() prompts = json_response["prompts"] - assert set(prompts.keys()) == set(settings.PROMPTS) + assert set(prompts.keys()) == set(settings_manager.settings.PROMPTS) def test_prompt_template(client: TestClient): diff --git a/tests/test_vectorstore_template.py b/tests/test_vectorstore_template.py index bac950ee1..6ae4843ac 100644 --- a/tests/test_vectorstore_template.py +++ b/tests/test_vectorstore_template.py @@ -1,12 +1,13 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager # check that all agents are in settings.agents # are in json_response["agents"] def test_vectorstores_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() vectorstores = json_response["vectorstores"] - assert set(vectorstores.keys()) == set(settings.VECTORSTORES) + assert set(vectorstores.keys()) == set(settings_manager.settings.VECTORSTORES) diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 57a0e95f6..dd668c287 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -1,6 +1,6 @@ from fastapi import WebSocketDisconnect -# from langflow.chat.manager import ChatManager +# from langflow.services.chat.manager import ChatManager import pytest