refactor: migrate from Record to Message (#2113)

* chore: Update launch.json to use debugpy instead of python for debugging

* refactor: Update import statements for Record in langflow components

* feat: Add image handling functionality to langflow schema

* update projects

* 📝 (constants.py): Add 'output_types' to NODE_FORMAT_ATTRIBUTES for consistency and completeness

♻️ (setup.py): Refactor imports to improve readability and maintainability
♻️ (setup.py): Update code to remove fields that are not in the latest template for consistency

* refactor: Update schema from Record to Message

* refactor: Remove print statement in MonitorService

* refactor: Remove fields not in the latest template for consistency

* refactor: Update code to handle Record objects in utils.py

* update projects

* 📝 (monitor.py): Add type hint for message_id parameter in update_message function
📝 (parse.py): Rename ParsedContext to ParsedArgs for clarity
📝 (chat.py): Remove unused imports and methods in ChatComponent class
📝 (StoreMessage.py): Change return type of store_message method from list[Record] to list[Message]
📝 (base.py): Change type hint from Dict[str, str | list[str]] to Mapping[str, str | list[str]] in update_raw_params method
📝 (loading.py): Add condition to check if raw is not None before accessing its attributes in instantiate_custom_component function
📝 (memory.py): Change return type of get_messages function from list[Record] to list[Message]
📝 (memory.py): Change parameter type of add_messages function from Message to Message | list[Message]
📝 (image.py): Add type hint for image_prompt_value variable in Message class

🐛 (record.py): fix type hint for image_prompt_value variable to ImagePromptValue to improve code clarity and maintainability

* chore: Add orjson options for serialization

* chore: Update orjson options for serialization in setup.py

* chore: Update input_value options for models

This commit updates the input_value options for the models in the `OpenAIModel.py`, `MistralModel.py`, `CohereModel.py`, `VertexAiModel.py`, `ChatLiteLLMModel.py`, `OllamaModel.py`, `HuggingFaceModel.py`, `AnthropicModel.py`, and `AmazonBedrockModel.py` files. The `input_value` now supports the additional input type "Prompt". This change allows for more flexibility in the input data that can be provided to the models.

Fixes #<issue_number>

* chore: Update edges with latest component versions

This commit updates the edges in the project data with the latest component versions. It ensures that the source and target nodes are correctly updated based on their corresponding nodes in the project. The commit also includes escaping of JSON dumps for the source and target handles in the edges.

* 📝 (utils.py): Remove unnecessary async keyword from dict_values_to_string function to improve code readability and consistency
🔧 (utils.py): Simplify handling of Message objects by directly accessing the text property instead of calling to_lc_message() method

* chore: Refactor PromptComponent to use updated Prompt class and remove unused imports

* feat: Add support for image files in Message model

This commit modifies the Message model to support image files as attachments. It introduces the `is_image_file` function to check if a file is an image, and the `to_content_dict` method in the Image class to convert the image object to a content dictionary. Additionally, the `get_file_content_dicts` method is added to generate content dictionaries for all files in the message, including images. This enhancement improves the handling of image attachments in the messaging system.

Fixes #<issue_number>

* update projects and lock

* chore: Update LCModelComponent to use Prompt instead of Record

* refactor: Update artifact type to include message in utils.py

* fix: Add check for input_value to only pass if string

*  (switchOutputView/index.tsx): introduce constant RECORD_TYPES to store valid record types for better readability and maintainability
🔧 (switchOutputView/index.tsx): refactor switch cases to use RECORD_TYPES constant for checking valid record types and simplify the logic for handling different types of result messages

* feat: Enable loading from database for openai_api_key field in Langflow starter projects

This commit updates the Langflow starter projects by enabling the loading of the `openai_api_key` field from the database. Previously, the field was not being loaded from the database, but now it will be loaded and used in the projects. This change improves the functionality and flexibility of the projects.

Fixes #<issue_number>

* ♻️ (constants.py): remove unnecessary import statement and clean up code formatting in ORJSON_OPTIONS constant definition

* refactor: Update MemoryComponent to use messages instead of records

This commit updates the MemoryComponent class in the langflow/components/helpers/MemoryComponent.py file to use the term "messages" instead of "records" for better clarity and consistency. It also updates the get_messages method to return a list of Message objects instead of Record objects. This change improves the naming and readability of the code.

* refactor: Update Message model to include timestamp conversion function

This commit updates the Message model in the langflow/schema/message.py file to include a new function `_timestamp_to_str` that converts the timestamp to a string format. This function is used as a BeforeValidator for the `timestamp` field, ensuring that it is always formatted correctly. This change improves the consistency and reliability of the timestamp handling in the messaging system.

* refactor: Update test_data_components.py to improve directory component loading

This commit updates the test_data_components.py file to improve the loading of the directory component. It ensures that the directory component can load mdx files from the ../docs/docs/components directory. This change enhances the functionality and reliability of the directory component.

Fixes #<issue_number>

* refactor: Update .gitattributes to specify working-tree-encoding for .mdx and .json files

This commit updates the .gitattributes file to specify the working-tree-encoding for .mdx and .json files. It sets the encoding to UTF-8 for both file types, ensuring consistent handling of character encoding. This change improves the reliability and compatibility of the repository.

Fixes #<issue_number>

* fix: 🐛 corrects encoding error

* refactor: Update toolkits.mdx to improve documentation and fix formatting

* refactor: Add dictdiffer library as a dependency

This commit adds the dictdiffer library as a dependency in the poetry.lock file. The dictdiffer library is a useful tool for diffing and patching dictionaries. It will enhance the functionality and flexibility of the project.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-06-09 18:00:48 -07:00 committed by GitHub
commit df57570852
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 8846 additions and 8116 deletions

5
.gitattributes vendored
View file

@ -11,12 +11,12 @@
*.ts text
*.tsx text
*.md text
*.mdx text
*.mdx text working-tree-encoding = UTF-8
*.yml text
*.yaml text
*.xml text
*.csv text
*.json text
*.json text working-tree-encoding = UTF-8
*.sh text
*.Dockerfile text
Dockerfile text
@ -32,3 +32,4 @@ Dockerfile text
*.mp4 binary
*.svg binary
*.csv binary

8
.vscode/launch.json vendored
View file

@ -3,7 +3,7 @@
"configurations": [
{
"name": "Debug Backend",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
@ -26,7 +26,7 @@
},
{
"name": "Debug CLI",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "langflow",
"args": [
@ -43,7 +43,7 @@
},
{
"name": "Python: Remote Attach",
"type": "python",
"type": "debugpy",
"request": "attach",
"justMyCode": true,
"connect": {
@ -65,7 +65,7 @@
},
{
"name": "Python: Debug Tests",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"purpose": ["debug-test"],

View file

@ -1,9 +1,11 @@
import Admonition from '@theme/Admonition';
import Admonition from "@theme/Admonition";
# Toolkits
<Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION">
<p>
We appreciate your understanding as we polish our documentation it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝
</p>
</Admonition>
<p>
We appreciate your understanding as we polish our documentation - it may
contain some rough edges. Share your feedback or report issues to help us
improve! 🛠️📝
</p>
</Admonition>

94
poetry.lock generated
View file

@ -471,17 +471,17 @@ files = [
[[package]]
name = "boto3"
version = "1.34.121"
version = "1.34.122"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "boto3-1.34.121-py3-none-any.whl", hash = "sha256:4e79e400d6d44b4eee5deda6ac0ecd08a3f5a30c45a0d30712795cdc4459fd79"},
{file = "boto3-1.34.121.tar.gz", hash = "sha256:ec89f3e0b0dc959c418df29e14d3748c0b05ab7acf7c0b90c839e9f340a659fa"},
{file = "boto3-1.34.122-py3-none-any.whl", hash = "sha256:b2d7400ff84fa547e53b3d9acfa3c95d65d45b5886ba1ede1f7df4768d1cc0b1"},
{file = "boto3-1.34.122.tar.gz", hash = "sha256:56840d8ce91654d182f1c113f0791fa2113c3aa43230c50b4481f235348a6037"},
]
[package.dependencies]
botocore = ">=1.34.121,<1.35.0"
botocore = ">=1.34.122,<1.35.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@ -490,13 +490,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.34.121"
version = "1.34.122"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
{file = "botocore-1.34.121-py3-none-any.whl", hash = "sha256:25b05c7646a9f240cde1c8f839552a43f27e71e15c42600275dea93e219f7dd9"},
{file = "botocore-1.34.121.tar.gz", hash = "sha256:1a8f94b917c47dfd84a0b531ab607dc53570efb0d073d8686600f2d2be985323"},
{file = "botocore-1.34.122-py3-none-any.whl", hash = "sha256:6d75df3af831b62f0c7baa109728d987e0a8d34bfadf0476eb32e2f29a079a36"},
{file = "botocore-1.34.122.tar.gz", hash = "sha256:9374e16a36f1062c3e27816e8599b53eba99315dfac71cc84fc3aee3f5d3cbe3"},
]
[package.dependencies]
@ -1450,13 +1450,13 @@ tests = ["pytest"]
[[package]]
name = "dataclasses-json"
version = "0.6.6"
version = "0.6.7"
description = "Easily serialize dataclasses to and from JSON."
optional = false
python-versions = "<4.0,>=3.7"
files = [
{file = "dataclasses_json-0.6.6-py3-none-any.whl", hash = "sha256:e54c5c87497741ad454070ba0ed411523d46beb5da102e221efb873801b0ba85"},
{file = "dataclasses_json-0.6.6.tar.gz", hash = "sha256:0c09827d26fffda27f1be2fed7a7a01a29c5ddcd2eb6393ad5ebf9d77e9deae8"},
{file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"},
{file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"},
]
[package.dependencies]
@ -1590,6 +1590,23 @@ files = [
[package.dependencies]
packaging = "*"
[[package]]
name = "dictdiffer"
version = "0.9.0"
description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
optional = false
python-versions = "*"
files = [
{file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"},
{file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"},
]
[package.extras]
all = ["Sphinx (>=3)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "tox (>=3.7.0)"]
docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"]
[[package]]
name = "dill"
version = "0.3.7"
@ -2392,8 +2409,8 @@ files = [
[package.dependencies]
cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
greenlet = [
{version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""},
{version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""},
{version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""},
]
"zope.event" = "*"
"zope.interface" = "*"
@ -2520,12 +2537,12 @@ files = [
google-auth = ">=2.14.1,<3.0.dev0"
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
grpcio = [
{version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
]
grpcio-status = [
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
]
proto-plus = ">=1.22.3,<2.0.0dev"
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0"
@ -2594,13 +2611,13 @@ httplib2 = ">=0.19.0"
[[package]]
name = "google-cloud-aiplatform"
version = "1.54.0"
version = "1.54.1"
description = "Vertex AI API client library"
optional = false
python-versions = ">=3.8"
files = [
{file = "google-cloud-aiplatform-1.54.0.tar.gz", hash = "sha256:6f5187d35a32951028465804fbb42b478362bf41e2b634ddd22b150299f6e1d8"},
{file = "google_cloud_aiplatform-1.54.0-py2.py3-none-any.whl", hash = "sha256:7b3ed849b9fb59a01bd6f44444ccbb7d18495b867a26f913542f6b2d4c3de252"},
{file = "google-cloud-aiplatform-1.54.1.tar.gz", hash = "sha256:01c231961cc1a1a3b049ea3ef71fb11e77b2d56d632d020ce09e419b27ff77f2"},
{file = "google_cloud_aiplatform-1.54.1-py2.py3-none-any.whl", hash = "sha256:43f70fcd572f15317d769e5a0e04cfb7c0e259ead3fe581d2fba4f203ace5617"},
]
[package.dependencies]
@ -4291,13 +4308,13 @@ extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"]
[[package]]
name = "langchainhub"
version = "0.1.17"
version = "0.1.18"
description = "The LangChain Hub API client"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langchainhub-0.1.17-py3-none-any.whl", hash = "sha256:4c609b3948252c71670f0d98f73413b515cfd2f6701a7b40ce959203e6133e04"},
{file = "langchainhub-0.1.17.tar.gz", hash = "sha256:af7df0cb1cebc7a6e0864e8632ae48ecad39ed96568f699c78657b9d04e50b46"},
{file = "langchainhub-0.1.18-py3-none-any.whl", hash = "sha256:11501f15e7f34715ecc8892587daa35c6f2a3005e1f2926c9bcabd31fc2c100c"},
{file = "langchainhub-0.1.18.tar.gz", hash = "sha256:f2d0d8bf3abe4ca5e70511d8220bdc9ccea28d5267bcfd0e5ef9c53bd5bd3bad"},
]
[package.dependencies]
@ -4363,13 +4380,13 @@ url = "src/backend/base"
[[package]]
name = "langfuse"
version = "2.35.0"
version = "2.35.2"
description = "A client library for accessing langfuse"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langfuse-2.35.0-py3-none-any.whl", hash = "sha256:e9df2474a01f8e167b7b13674c554915415b27064e48ad207054475f7fa8f82d"},
{file = "langfuse-2.35.0.tar.gz", hash = "sha256:b1d4b478233eefbc8a6fc63ca00ca82f6afecf2b0fdc1835ca65e751cf901577"},
{file = "langfuse-2.35.2-py3-none-any.whl", hash = "sha256:d01a23842cab484594f03878aacb9732ef8fd361158eb819c7bf43f758a0954b"},
{file = "langfuse-2.35.2.tar.gz", hash = "sha256:32b2e6c5bc71b4efdc430c6b964ab1c1e1ba1e105a4a73912c38b3959dc4502d"},
]
[package.dependencies]
@ -4403,13 +4420,13 @@ requests = ">=2,<3"
[[package]]
name = "litellm"
version = "1.40.4"
version = "1.40.7"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
files = [
{file = "litellm-1.40.4-py3-none-any.whl", hash = "sha256:b3b8e4401f717c3a18595446bfdb80fc6bb74974aac4eae537fb7b3be37fbf9e"},
{file = "litellm-1.40.4.tar.gz", hash = "sha256:3edaa1189742afd7c7df2b122f77373d47154a8fb6df6187ff5875e188baa3e1"},
{file = "litellm-1.40.7-py3-none-any.whl", hash = "sha256:c98dd8733e632aba16f14bf82e56f7159222097a6d085b242a3140b5d3e7baa4"},
{file = "litellm-1.40.7.tar.gz", hash = "sha256:557bb19e8e484d0dfe8e4eaa9ccefc888617852988a46d6e7adc41585a2c0600"},
]
[package.dependencies]
@ -4451,13 +4468,13 @@ test = ["httpx (>=0.24.1)", "pytest (>=7.4.0)", "scipy (>=1.10)"]
[[package]]
name = "locust"
version = "2.28.0"
version = "2.29.0"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.28.0-py3-none-any.whl", hash = "sha256:766be879db030c0118e7d9fca712f3538c4e628bdebf59468fa1c6c2fab217d3"},
{file = "locust-2.28.0.tar.gz", hash = "sha256:260557eec866f7e34a767b6c916b5b278167562a280480aadb88f43d962fbdeb"},
{file = "locust-2.29.0-py3-none-any.whl", hash = "sha256:aa9d94d3604ed9f2aab3248460d91e55d3de980a821dffdf8658b439b049d03f"},
{file = "locust-2.29.0.tar.gz", hash = "sha256:649c99ce49d00720a3084c0109547035ad9021222835386599a8b545d31ebe51"},
]
[package.dependencies]
@ -4471,7 +4488,10 @@ msgpack = ">=1.0.0"
psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
pyzmq = ">=25.0.0"
requests = ">=2.26.0"
requests = [
{version = ">=2.32.2", markers = "python_version > \"3.11\""},
{version = ">=2.26.0", markers = "python_version <= \"3.11\""},
]
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
Werkzeug = ">=2.0.0"
@ -5567,13 +5587,13 @@ sympy = "*"
[[package]]
name = "openai"
version = "1.32.0"
version = "1.33.0"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.7.1"
files = [
{file = "openai-1.32.0-py3-none-any.whl", hash = "sha256:953d57669f309002044fd2f678aba9f07a43256d74b3b00cd04afb5b185568ea"},
{file = "openai-1.32.0.tar.gz", hash = "sha256:a6df15a7ab9344b1bc2bc8d83639f68b7a7e2453c0f5e50c1666547eee86f0bd"},
{file = "openai-1.33.0-py3-none-any.whl", hash = "sha256:621163b56570897ab8389d187f686a53d4771fd6ce95d481c0a9611fe8bc4229"},
{file = "openai-1.33.0.tar.gz", hash = "sha256:1169211a7b326ecbc821cafb427c29bfd0871f9a3e0947dd9e51acb3b0f1df78"},
]
[package.dependencies]
@ -5899,9 +5919,9 @@ files = [
[package.dependencies]
numpy = [
{version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""},
{version = ">=1.22.4,<2", markers = "python_version < \"3.11\""},
{version = ">=1.23.2,<2", markers = "python_version == \"3.11\""},
{version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""},
]
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
@ -9025,13 +9045,13 @@ files = [
[[package]]
name = "typing-extensions"
version = "4.12.1"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"},
{file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"},
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
@ -10038,4 +10058,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.13"
content-hash = "2ba268be17a69253c9631ec721ece465a85a22949c2df7c712b7aa12d1a002fa"
content-hash = "0ee3f3bef82d57be2ab4ae7b70215ebca67b5bd5223e6a9322ee1837516a3cc6"

View file

@ -115,6 +115,7 @@ pytest-asyncio = "^0.23.0"
pytest-profiling = "^1.7.0"
pre-commit = "^3.7.0"
vulture = "^2.11"
dictdiffer = "^0.9.0"
[tool.poetry.extras]
deploy = ["celery", "redis", "flower"]

View file

@ -161,7 +161,7 @@ async def build_vertex(
else:
graph = cache.get("result")
vertex = graph.get_vertex(vertex_id)
log_object = None
try:
lock = chat_service._cache_locks[flow_id_str]
(

View file

@ -1,4 +1,5 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from langflow.services.deps import get_monitor_service
@ -79,7 +80,7 @@ async def delete_messages(
@router.post("/messages/{message_id}", response_model=MessageModelResponse)
async def update_message(
message_id: str,
message_id: int,
message: MessageModelRequest,
monitor_service: MonitorService = Depends(get_monitor_service),
):
@ -135,3 +136,4 @@ async def get_transactions(
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))

View file

@ -2,7 +2,6 @@ from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing_extensions import TypedDict
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer
@ -15,7 +14,6 @@ from langflow.services.database.models.api_key.model import ApiKeyRead
from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import FlowCreate, FlowRead
from langflow.services.database.models.user import UserRead
from langflow.utils.schemas import ChatOutputResponse
class BuildStatus(Enum):
@ -244,6 +242,7 @@ class VerticesOrderResponse(BaseModel):
run_id: UUID
vertices_to_run: List[str]
class ResultDataResponse(BaseModel):
results: Optional[Any] = Field(default_factory=dict)
logs: List[Log | None] = Field(default_factory=list)

View file

@ -7,7 +7,7 @@ from langchain_core.runnables import Runnable
from langflow.base.agents.utils import get_agents_list, records_to_messages
from langflow.custom import CustomComponent
from langflow.field_typing import Text, Tool
from langflow.schema.schema import Record
from langflow.schema import Record
class LCAgentComponent(CustomComponent):

View file

@ -13,7 +13,7 @@ from langchain_core.prompts import BasePromptTemplate, ChatPromptTemplate
from langchain_core.tools import BaseTool
from pydantic import BaseModel
from langflow.schema.schema import Record
from langflow.schema import Record
from .default_prompts import XML_AGENT_PROMPT

View file

@ -7,9 +7,11 @@ Constants:
- FIELD_FORMAT_ATTRIBUTES: A list of attributes used for formatting fields.
"""
import orjson
STREAM_INFO_TEXT = "Stream the response from the model. Streaming works only in Chat."
NODE_FORMAT_ATTRIBUTES = ["beta", "icon", "display_name", "description"]
NODE_FORMAT_ATTRIBUTES = ["beta", "icon", "display_name", "description", "output_types"]
FIELD_FORMAT_ATTRIBUTES = [
@ -27,3 +29,5 @@ FIELD_FORMAT_ATTRIBUTES = [
"refresh_button_text",
"options",
]
ORJSON_OPTIONS = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_OMIT_MICROSECONDS

View file

@ -16,7 +16,7 @@ from collections import OrderedDict, namedtuple
from http.cookies import SimpleCookie
ParsedArgs = namedtuple(
"ParsedContext",
"ParsedArgs",
[
"command",
"url",

View file

@ -1,13 +1,14 @@
import json
import unicodedata
import xml.etree.ElementTree as ET
from concurrent import futures
from pathlib import Path
from typing import Callable, List, Optional, Text
import unicodedata
import chardet
import orjson
import yaml
from langflow.schema.schema import Record
from langflow.schema import Record
# Types of files that can be read simply by file.read()
# and have 100% to be completely readable
@ -106,7 +107,7 @@ def read_text_file(file_path: str) -> str:
result = chardet.detect(raw_data)
encoding = result["encoding"]
if encoding in ["Windows-1254", "MacRoman"]:
if encoding in ["Windows-1252", "Windows-1254"]:
encoding = "utf-8"
with open(file_path, "r", encoding=encoding) as f:
@ -139,7 +140,7 @@ def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[R
# if file is json, yaml, or xml, we can parse it
if file_path.endswith(".json"):
text = json.loads(text)
text = orjson.loads(text)
if isinstance(text, dict):
text = {k: normalize_text(v) if isinstance(v, str) else v for k, v in text.items()}
elif isinstance(text, list):

View file

@ -1,7 +1,7 @@
from typing import List
from langflow.graph.schema import ResultData, RunOutputs
from langflow.schema.schema import Record
from langflow.schema import Record
def build_records_from_run_outputs(run_outputs: RunOutputs) -> List[Record]:

View file

@ -2,10 +2,9 @@ from typing import Optional, Union
from langflow.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.memory import store_message
from langflow.schema import Record
from langflow.schema.message import Message
class ChatComponent(CustomComponent):
@ -52,102 +51,34 @@ class ChatComponent(CustomComponent):
def store_message(
self,
message: Union[str, Text, Record],
session_id: Optional[str] = None,
sender: Optional[str] = None,
sender_name: Optional[str] = None,
) -> list[Record]:
records = store_message(
message: Message,
) -> list[Message]:
messages = store_message(
message,
session_id=session_id,
sender=sender,
sender_name=sender_name,
flow_id=self.graph.flow_id,
)
self.status = records
return records
self.status = messages
return messages
def build_with_record(
self,
sender: Optional[str] = "User",
sender_name: Optional[str] = "User",
input_value: Optional[Union[str, Record]] = None,
input_value: Optional[Union[str, Record, Message]] = None,
files: Optional[list[str]] = None,
session_id: Optional[str] = None,
return_record: Optional[bool] = False,
record_template: str = "Text: {text}\nData: {data}",
) -> Union[Text, Record]:
input_value_record: Optional[Record] = None
if return_record:
if isinstance(input_value, Record):
# Update the data of the record
input_value.data["sender"] = sender
input_value.data["sender_name"] = sender_name
input_value.data["session_id"] = session_id
input_value.data["files"] = files
else:
input_value_record = Record(
text=input_value,
data={
"sender": sender,
"sender_name": sender_name,
"session_id": session_id,
"files": files,
},
)
elif isinstance(input_value, Record):
input_value = records_to_text(template=record_template, records=input_value)
if not input_value:
input_value = ""
if return_record and input_value_record:
result: Union[Text, Record] = input_value_record
else:
result = input_value
self.status = result
if session_id and isinstance(result, (Record, str)):
self.store_message(result, session_id, sender, sender_name)
return result
) -> Message:
message: Message | None = None
def build_no_record(
self,
sender: Optional[str] = "User",
sender_name: Optional[str] = "User",
input_value: Optional[str] = None,
files: Optional[list[str]] = None,
session_id: Optional[str] = None,
return_record: Optional[bool] = False,
record_template: str = "Text: {text}\nData: {data}",
) -> Union[Text, Record]:
input_value_record: Optional[Record] = None
if files and not return_record:
raise ValueError("Files can only be provided when Return Record is enabled.")
if return_record:
if isinstance(input_value, Record):
# Update the data of the record
input_value.data["sender"] = sender
input_value.data["sender_name"] = sender_name
input_value.data["session_id"] = session_id
input_value.data["files"] = files
else:
input_value_record = Record(
text=input_value,
data={
"sender": sender,
"sender_name": sender_name,
"session_id": session_id,
"files": files,
},
)
elif isinstance(input_value, Record):
input_value = records_to_text(template=record_template, records=input_value)
if not input_value:
input_value = ""
if return_record and input_value_record:
result: Union[Text, Record] = input_value_record
if isinstance(input_value, Record):
# Update the data of the record
message = Message.from_record(input_value)
else:
result = input_value
self.status = result
if session_id and isinstance(result, (Record, str)):
self.store_message(result, session_id, sender, sender_name)
return result
message = Message(
text=input_value, sender=sender, sender_name=sender_name, files=files, session_id=session_id
)
self.status = message
if session_id and isinstance(message, Message):
self.store_message(message)
return message

View file

@ -3,7 +3,7 @@ from typing import Optional
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.schema.schema import Record
from langflow.schema import Record
class TextComponent(CustomComponent):

View file

@ -1,7 +1,7 @@
from typing import Optional
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class BaseMemoryComponent(CustomComponent):

View file

@ -3,11 +3,10 @@ from typing import Optional, Union
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.language_models.llms import LLM
from langchain_core.load import load
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.field_typing.prompt import Prompt
class LCModelComponent(CustomComponent):
@ -85,7 +84,7 @@ class LCModelComponent(CustomComponent):
return status_message
def get_chat_result(
self, runnable: BaseChatModel, stream: bool, input_value: str | Record, system_message: Optional[str] = None
self, runnable: BaseChatModel, stream: bool, input_value: str | Prompt, system_message: Optional[str] = None
):
messages: list[Union[HumanMessage, SystemMessage]] = []
if not input_value and not system_message:
@ -93,20 +92,21 @@ class LCModelComponent(CustomComponent):
if system_message:
messages.append(SystemMessage(content=system_message))
if input_value:
if isinstance(input_value, Record):
if isinstance(input_value, Prompt):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if "prompt" in input_value:
prompt = load(input_value.prompt)
prompt = input_value.load_lc_prompt()
runnable = prompt | runnable
else:
messages.append(input_value.to_lc_message())
else:
messages.append(HumanMessage(content=input_value))
inputs = messages or {}
if stream:
return runnable.stream(messages)
return runnable.stream(inputs)
else:
message = runnable.invoke(messages)
message = runnable.invoke(inputs)
result = message.content
if isinstance(message, AIMessage):
status_message = self.build_status_message(message)

View file

@ -1,10 +1,9 @@
import base64
from copy import deepcopy
from langchain_core.documents import Document
from langflow.schema import Record
from langflow.services.deps import get_storage_service
from langflow.schema.message import Message
def record_to_string(record: Record) -> str:
@ -20,7 +19,7 @@ def record_to_string(record: Record) -> str:
return record.get_text()
async def dict_values_to_string(d: dict) -> dict:
def dict_values_to_string(d: dict) -> dict:
"""
Converts the values of a dictionary to strings.
@ -36,44 +35,21 @@ async def dict_values_to_string(d: dict) -> dict:
# it could be a list of records or documents or strings
if isinstance(value, list):
for i, item in enumerate(value):
if isinstance(item, Record):
d_copy[key][i] = item.to_lc_message()
if isinstance(item, Message):
d_copy[key][i] = item.text
elif isinstance(item, Record):
d_copy[key][i] = record_to_string(item)
elif isinstance(item, Document):
d_copy[key][i] = document_to_string(item)
elif isinstance(value, Message):
d_copy[key] = value.text
elif isinstance(value, Record):
if "files" in value and value.files:
files = await get_file_paths(value.files)
value.files = files
d_copy[key] = value.to_lc_message()
d_copy[key] = record_to_string(value)
elif isinstance(value, Document):
d_copy[key] = document_to_string(value)
return d_copy
async def get_file_paths(files: list[str]):
storage_service = get_storage_service()
file_paths = []
for file in files:
flow_id, file_name = file.split("/")
file_paths.append(storage_service.build_full_path(flow_id=flow_id, file_name=file_name))
return file_paths
async def get_files(
file_paths: str,
convert_to_base64: bool = False,
):
storage_service = get_storage_service()
file_objects = []
for file_path in file_paths:
flow_id, file_name = file_path.split("/")
file_object = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
if convert_to_base64:
file_object = base64.b64encode(file_object).decode("utf-8")
file_objects.append(file_object)
return file_objects
def document_to_string(document: Document) -> str:
"""
Convert a document to a string.

View file

@ -5,7 +5,7 @@ from langchain_core.prompts import ChatPromptTemplate
from langflow.base.agents.agent import LCAgentComponent
from langflow.field_typing import BaseLanguageModel, Text, Tool
from langflow.schema.schema import Record
from langflow.schema import Record
class ToolCallingAgentComponent(LCAgentComponent):

View file

@ -3,10 +3,9 @@ from typing import List, Optional
from langchain.agents import create_xml_agent
from langchain_core.prompts import ChatPromptTemplate
from langflow.base.agents.agent import LCAgentComponent
from langflow.field_typing import BaseLanguageModel, Text, Tool
from langflow.schema.schema import Record
from langflow.schema import Record
class XMLAgentComponent(LCAgentComponent):

View file

@ -5,7 +5,7 @@ from langchain_core.documents import Document
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
from langflow.schema.schema import Record
from langflow.schema import Record
class RetrievalQAComponent(CustomComponent):

View file

@ -3,8 +3,8 @@ import uuid
from typing import Any, Optional
from langflow.custom import CustomComponent
from langflow.schema import Record
from langflow.schema.dotdict import dotdict
from langflow.schema.schema import Record
class WebhookComponent(CustomComponent):

View file

@ -6,8 +6,8 @@ from langchain_core.prompts.chat import HumanMessagePromptTemplate, SystemMessag
from langflow.base.agents.agent import LCAgentComponent
from langflow.base.agents.utils import AGENTS, AgentSpec, get_agents_list
from langflow.field_typing import BaseLanguageModel, Text, Tool
from langflow.schema import Record
from langflow.schema.dotdict import dotdict
from langflow.schema.schema import Record
class AgentComponent(LCAgentComponent):

View file

@ -7,8 +7,8 @@ from langflow.custom import CustomComponent
from langflow.field_typing import Tool
from langflow.graph.graph.base import Graph
from langflow.helpers.flow import get_flow_inputs
from langflow.schema import Record
from langflow.schema.dotdict import dotdict
from langflow.schema.schema import Record
class FlowToolComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import List, Optional
from langflow.custom import CustomComponent
from langflow.memory import get_messages, store_message
from langflow.schema import Record
from langflow.schema.message import Message
class StoreMessageComponent(CustomComponent):
@ -31,12 +31,11 @@ class StoreMessageComponent(CustomComponent):
sender_name: Optional[str] = None,
session_id: Optional[str] = None,
message: str = "",
) -> List[Record]:
) -> List[Message]:
store_message(
sender=sender,
sender_name=sender_name,
session_id=session_id,
message=message,
message=Message(
text=message, sender=sender, sender_name=sender_name, flow_id=self.graph.flow_id, session_id=session_id
)
)
self.status = get_messages(session_id=session_id)

View file

@ -2,9 +2,9 @@ from typing import Optional
from langflow.base.memory.memory import BaseMemoryComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.helpers.record import messages_to_text
from langflow.memory import get_messages
from langflow.schema.schema import Record
from langflow.schema.message import Message
class MemoryComponent(BaseMemoryComponent):
@ -43,7 +43,7 @@ class MemoryComponent(BaseMemoryComponent):
},
}
def get_messages(self, **kwargs) -> list[Record]:
def get_messages(self, **kwargs) -> list[Message]:
# Validate kwargs by checking if it contains the correct keys
if "sender" not in kwargs:
kwargs["sender"] = None
@ -77,6 +77,6 @@ class MemoryComponent(BaseMemoryComponent):
limit=n_messages,
order=order,
)
messages_str = records_to_text(template=record_template or "", records=messages)
messages_str = messages_to_text(template=record_template or "", messages=messages)
self.status = messages_str
return messages_str

View file

@ -1,8 +1,7 @@
from typing import Optional, Union
from typing import Optional
from langflow.base.io.chat import ChatComponent
from langflow.field_typing import Text
from langflow.schema import Record
from langflow.schema.message import Message
class ChatInput(ChatComponent):
@ -27,13 +26,11 @@ class ChatInput(ChatComponent):
input_value: Optional[str] = None,
files: Optional[list[str]] = None,
session_id: Optional[str] = None,
return_record: Optional[bool] = False,
) -> Union[Text, Record]:
return super().build_no_record(
) -> Message:
return super().build_with_record(
sender=sender,
sender_name=sender_name,
input_value=input_value,
files=files,
session_id=session_id,
return_record=return_record,
)

View file

@ -1,9 +1,6 @@
from langchain_core.prompts import ChatPromptTemplate
from langflow.base.prompts.utils import dict_values_to_string
from langflow.custom import CustomComponent
from langflow.field_typing import Prompt, TemplateField, Text
from langflow.schema.schema import Record
from langflow.field_typing import TemplateField
from langflow.field_typing.prompt import Prompt
class PromptComponent(CustomComponent):
@ -21,10 +18,7 @@ class PromptComponent(CustomComponent):
self,
template: Prompt,
**kwargs,
) -> Record:
prompt_template = ChatPromptTemplate.from_template(Text(template))
kwargs = await dict_values_to_string(kwargs)
messages = list(kwargs.values())
prompt = prompt_template + messages
self.status = f'Prompt:\n"{template}"'
return Record(data={"prompt": prompt.to_json()})
) -> Prompt:
prompt = await Prompt.from_template_and_variables(template, kwargs)
self.status = prompt.format_text()
return prompt

View file

@ -3,7 +3,7 @@ from typing import Optional
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
from langflow.services.database.models.base import orjson_dumps

View file

@ -4,7 +4,7 @@ from langchain_astradb.chat_message_histories import AstraDBChatMessageHistory
from langflow.base.memory.memory import BaseMemoryComponent
from langflow.field_typing import Text
from langflow.schema.schema import Record
from langflow.schema import Record
class AstraDBMessageReaderComponent(BaseMemoryComponent):

View file

@ -1,11 +1,11 @@
from typing import Optional
from langchain_astradb import AstraDBChatMessageHistory
from langchain_core.messages import BaseMessage
from langflow.base.memory.memory import BaseMemoryComponent
from langflow.field_typing import Text
from langflow.schema.schema import Record
from langchain_core.messages import BaseMessage
from langchain_astradb import AstraDBChatMessageHistory
from langflow.schema import Record
class AstraDBMessageWriterComponent(BaseMemoryComponent):

View file

@ -4,7 +4,7 @@ from langchain_community.chat_message_histories.zep import SearchScope, SearchTy
from langflow.base.memory.memory import BaseMemoryComponent
from langflow.field_typing import Text
from langflow.schema.schema import Record
from langflow.schema import Record
class ZepMessageReaderComponent(BaseMemoryComponent):

View file

@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Optional
from langflow.base.memory.memory import BaseMemoryComponent
from langflow.field_typing import Text
from langflow.schema.schema import Record
from langflow.schema import Record
if TYPE_CHECKING:
from zep_python.langchain import ZepChatMessageHistory

View file

@ -58,7 +58,7 @@ class AmazonBedrockComponent(LCModelComponent):
"advanced": True,
},
"cache": {"display_name": "Cache"},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"system_message": {
"display_name": "System Message",
"info": "System message to pass to the model.",

View file

@ -63,7 +63,7 @@ class AnthropicLLM(LCModelComponent):
"info": "Endpoint of the Anthropic API. Defaults to 'https://api.anthropic.com' if not specified.",
},
"code": {"show": False},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"advanced": True,

View file

@ -78,7 +78,7 @@ class AzureChatOpenAIComponent(LCModelComponent):
"info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.",
},
"code": {"show": False},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -81,7 +81,7 @@ class QianfanChatEndpointComponent(LCModelComponent):
"info": "Endpoint of the Qianfan LLM, required if custom model used.",
},
"code": {"show": False},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -111,7 +111,7 @@ class ChatLiteLLMModelComponent(LCModelComponent):
"required": False,
"default": False,
},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -43,7 +43,7 @@ class CohereComponent(LCModelComponent):
"type": "float",
"show": True,
},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -37,7 +37,7 @@ class HuggingFaceEndpointsComponent(LCModelComponent):
"advanced": True,
},
"code": {"show": False},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -27,7 +27,7 @@ class MistralAIModelComponent(LCModelComponent):
def build_config(self):
return {
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"max_tokens": {
"display_name": "Max Tokens",
"advanced": True,

View file

@ -194,7 +194,7 @@ class ChatOllamaComponent(LCModelComponent):
"info": "Template to use for generating text.",
"advanced": True,
},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -28,7 +28,7 @@ class OpenAIModelComponent(LCModelComponent):
def build_config(self):
return {
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"max_tokens": {
"display_name": "Max Tokens",
"advanced": True,

View file

@ -73,7 +73,7 @@ class ChatVertexAIComponent(LCModelComponent):
"value": False,
"advanced": True,
},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record"]},
"input_value": {"display_name": "Input", "input_types": ["Text", "Record", "Prompt"]},
"stream": {
"display_name": "Stream",
"info": STREAM_INFO_TEXT,

View file

@ -1,8 +1,7 @@
from typing import Optional, Union
from typing import Optional
from langflow.base.io.chat import ChatComponent
from langflow.field_typing import Text
from langflow.schema import Record
from langflow.schema.message import Message
class ChatOutput(ChatComponent):
@ -16,16 +15,12 @@ class ChatOutput(ChatComponent):
sender_name: Optional[str] = "AI",
input_value: Optional[str] = None,
session_id: Optional[str] = None,
return_record: Optional[bool] = False,
record_template: Optional[str] = "{text}",
files: Optional[list[str]] = None,
) -> Union[Text, Record]:
) -> Message:
return super().build_with_record(
sender=sender,
sender_name=sender_name,
input_value=input_value,
session_id=session_id,
return_record=return_record,
record_template=record_template or "",
files=files,
)

View file

@ -3,7 +3,7 @@ from typing import List
from langchain_text_splitters import CharacterTextSplitter
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
from langflow.utils.util import unescape_string

View file

@ -3,7 +3,7 @@ from typing import List, Optional
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class LanguageRecursiveTextSplitterComponent(CustomComponent):

View file

@ -3,7 +3,7 @@ from typing import Optional
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
from langflow.services.database.models.base import orjson_dumps

View file

@ -1,10 +1,11 @@
from typing import List, Optional
from langchain_core.embeddings import Embeddings
from langflow.components.vectorstores.base.model import LCVectorStoreComponent
from langflow.components.vectorstores.Redis import RedisComponent
from langflow.field_typing import Text
from langflow.schema import Record
from langchain_core.embeddings import Embeddings
class RedisSearchComponent(RedisComponent, LCVectorStoreComponent):

View file

@ -1,10 +1,11 @@
from typing import List, Optional
from langchain_core.embeddings import Embeddings
from langflow.components.vectorstores.base.model import LCVectorStoreComponent
from langflow.components.vectorstores.Weaviate import WeaviateVectorStoreComponent
from langflow.field_typing import Text
from langflow.schema import Record
from langchain_core.embeddings import Embeddings
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):

View file

@ -1,10 +1,11 @@
from typing import List
from langchain_core.embeddings import Embeddings
from langflow.components.vectorstores.base.model import LCVectorStoreComponent
from langflow.components.vectorstores.pgvector import PGVectorComponent
from langflow.field_typing import Text
from langflow.schema import Record
from langchain_core.embeddings import Embeddings
class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent):

View file

@ -1,11 +1,12 @@
from typing import List, Optional, Union
from langchain_astradb import AstraDBVectorStore
from langchain_astradb.utils.astradb import SetupMode
from langchain_core.retrievers import BaseRetriever
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings, VectorStore
from langflow.schema import Record
from langchain_core.retrievers import BaseRetriever
class AstraDBVectorStoreComponent(CustomComponent):
@ -156,3 +157,4 @@ class AstraDBVectorStoreComponent(CustomComponent):
)
return vector_store
return vector_store

View file

@ -8,7 +8,7 @@ from langchain_core.retrievers import BaseRetriever
from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class ChromaComponent(CustomComponent):

View file

@ -1,18 +1,16 @@
from typing import List, Optional, Union
from langchain_community.vectorstores import CouchbaseVectorStore
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings, VectorStore
from langflow.schema import Record
from datetime import timedelta
from typing import List, Optional, Union
from couchbase.auth import PasswordAuthenticator # type: ignore
from couchbase.cluster import Cluster # type: ignore
from couchbase.options import ClusterOptions # type: ignore
from langchain_community.vectorstores import CouchbaseVectorStore
from langchain_core.retrievers import BaseRetriever
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings, VectorStore
from langflow.schema import Record
class CouchbaseComponent(CustomComponent):
display_name = "Couchbase"

View file

@ -6,7 +6,7 @@ from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings
from langflow.schema.schema import Record
from langflow.schema import Record
class FAISSComponent(CustomComponent):

View file

@ -4,7 +4,7 @@ from langchain_community.vectorstores.mongodb_atlas import MongoDBAtlasVectorSea
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings
from langflow.schema.schema import Record
from langflow.schema import Record
class MongoDBAtlasComponent(CustomComponent):

View file

@ -8,7 +8,7 @@ from langchain_pinecone.vectorstores import PineconeVectorStore
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings
from langflow.schema.schema import Record
from langflow.schema import Record
class PineconeComponent(CustomComponent):

View file

@ -6,7 +6,7 @@ from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings
from langflow.schema.schema import Record
from langflow.schema import Record
class QdrantComponent(CustomComponent):

View file

@ -6,7 +6,7 @@ from langchain_core.retrievers import BaseRetriever
from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class RedisComponent(CustomComponent):

View file

@ -7,7 +7,7 @@ from supabase.client import Client, create_client
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings
from langflow.schema.schema import Record
from langflow.schema import Record
class SupabaseComponent(CustomComponent):

View file

@ -6,7 +6,7 @@ from langchain_core.retrievers import BaseRetriever
from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class UpstashVectorStoreComponent(CustomComponent):

View file

@ -9,7 +9,7 @@ from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.field_typing import BaseRetriever
from langflow.schema.schema import Record
from langflow.schema import Record
class VectaraComponent(CustomComponent):

View file

@ -8,7 +8,7 @@ from langchain_core.retrievers import BaseRetriever
from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class WeaviateVectorStoreComponent(CustomComponent):

View file

@ -6,7 +6,7 @@ from langchain_core.retrievers import BaseRetriever
from langchain_core.vectorstores import VectorStore
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
from langflow.schema import Record
class PGVectorComponent(CustomComponent):

View file

@ -7,6 +7,7 @@ import yaml
from cachetools import TTLCache, cachedmethod
from langchain_core.documents import Document
from pydantic import BaseModel
from langflow.custom.code_parser.utils import (
extract_inner_type_from_generic_alias,
extract_union_types_from_generic_alias,

View file

@ -19,13 +19,13 @@ from .constants import (
Embeddings,
NestedDict,
Object,
Prompt,
PromptTemplate,
Text,
TextSplitter,
Tool,
VectorStore,
)
from .prompt import Prompt
from .range_spec import RangeSpec

View file

@ -6,7 +6,7 @@ from langchain.memory.chat_memory import BaseChatMemory
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.language_models import BaseLLM, BaseLanguageModel
from langchain_core.language_models import BaseLanguageModel, BaseLLM
from langchain_core.memory import BaseMemory
from langchain_core.output_parsers import BaseOutputParser
from langchain_core.prompts import BasePromptTemplate, ChatPromptTemplate, PromptTemplate
@ -15,6 +15,8 @@ from langchain_core.tools import Tool
from langchain_core.vectorstores import VectorStore
from langchain_text_splitters import TextSplitter
from langflow.field_typing.prompt import Prompt
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]
@ -27,10 +29,6 @@ class Data:
pass
class Prompt:
pass
class Code:
pass

View file

@ -0,0 +1,41 @@
from langchain_core.load import load
from langchain_core.messages import HumanMessage
from langchain_core.prompts import BaseChatPromptTemplate, ChatPromptTemplate, PromptTemplate
from langflow.base.prompts.utils import dict_values_to_string
from langflow.schema.message import Message
from langflow.schema.record import Record
class Prompt(Record):
def load_lc_prompt(self):
if "prompt" not in self:
raise ValueError("Prompt is required.")
return load(self.prompt)
@classmethod
def from_lc_prompt(
cls,
prompt: BaseChatPromptTemplate,
):
prompt_json = prompt.to_json()
return cls(prompt=prompt_json)
def format_text(self):
prompt_template = PromptTemplate.from_template(self.template)
variables_with_str_values = dict_values_to_string(self.variables)
formatted_prompt = prompt_template.format(**variables_with_str_values)
return formatted_prompt
@classmethod
async def from_template_and_variables(cls, template: str, variables: dict):
instance = cls(template=template, variables=variables)
contents = [{"type": "text", "text": instance.format_text()}]
# Get all Message instances from the kwargs
for value in variables.values():
if isinstance(value, Message):
content_dicts = await value.get_file_content_dicts()
contents.extend(content_dicts)
prompt_template = ChatPromptTemplate.from_messages([HumanMessage(content=contents)])
instance.prompt = prompt_template.to_json()
return instance

View file

@ -2,11 +2,11 @@ from enum import Enum
from typing import Any, Generator, Union
from langchain_core.documents import Document
from langflow.schema.schema import Record
from pydantic import BaseModel
from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.schema.schema import Record
from langflow.schema import Record
from langflow.schema.message import Message
class UnbuiltObject:
@ -24,6 +24,7 @@ class ArtifactType(str, Enum):
ARRAY = "array"
STREAM = "stream"
UNKNOWN = "unknown"
MESSAGE = "message"
def validate_prompt(prompt: str):
@ -80,9 +81,14 @@ def get_artifact_type(custom_component, build_result) -> str:
case list():
result = ArtifactType.ARRAY
case Message():
result = ArtifactType.MESSAGE
if result == ArtifactType.UNKNOWN:
if isinstance(build_result, Generator):
result = ArtifactType.STREAM
elif isinstance(value, Message) and isinstance(value.text, Generator):
result = ArtifactType.STREAM
return result.value

View file

@ -4,7 +4,7 @@ import inspect
import os
import types
from enum import Enum
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Mapping, Optional
from loguru import logger
@ -373,7 +373,7 @@ class Vertex:
self.load_from_db_fields = load_from_db_fields
self._raw_params = params.copy()
def update_raw_params(self, new_params: Dict[str, str | list[str]], overwrite: bool = False):
def update_raw_params(self, new_params: Mapping[str, str | list[str]], overwrite: bool = False):
"""
Update the raw parameters of the vertex with the given new parameters.
@ -424,7 +424,7 @@ class Vertex:
try:
messages = [
ChatOutputResponse(
message=artifacts["message"],
message=artifacts["text"],
sender=artifacts.get("sender"),
sender_name=artifacts.get("sender_name"),
session_id=artifacts.get("session_id"),
@ -444,7 +444,7 @@ class Vertex:
# We need to set the artifacts to pass information
# to the frontend
self.set_artifacts()
artifacts = self.artifacts
artifacts = self.artifacts_raw
if isinstance(artifacts, dict):
messages = self.extract_messages_from_artifacts(artifacts)
else:

View file

@ -9,6 +9,7 @@ from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, Interface
from langflow.graph.utils import ArtifactType, UnbuiltObject, serialize_field
from langflow.graph.vertex.base import Vertex
from langflow.schema import Record
from langflow.schema.message import Message
from langflow.schema.schema import INPUT_FIELD_NAME
from langflow.services.monitor.utils import log_vertex_build
from langflow.utils.schemas import ChatOutputResponse, RecordOutputResponse
@ -98,11 +99,13 @@ class InterfaceVertex(Vertex):
# Turn the dict into a pleasing to
# read JSON inside a code block
message = dict_to_codeblock(self._built_object)
elif isinstance(self._built_object, Record):
message = self._built_object.text
elif isinstance(message, (AsyncIterator, Iterator)):
stream_url = self.build_stream_url()
message = ""
elif isinstance(self._built_object, Message):
if isinstance(message, (AsyncIterator, Iterator)):
stream_url = self.build_stream_url()
message = ""
self._built_object.text = message
else:
message = self._built_object.text
elif not isinstance(self._built_object, str):
message = str(self._built_object)
# if the message is a generator or iterator

View file

@ -1,3 +1,3 @@
from .record import docs_to_records, records_to_text
from .record import docs_to_records, records_to_text, messages_to_text
__all__ = ["docs_to_records", "records_to_text"]
__all__ = ["docs_to_records", "records_to_text", "messages_to_text"]

View file

@ -6,7 +6,8 @@ from pydantic.v1 import BaseModel, Field, create_model
from sqlmodel import Session, select
from langflow.graph.schema import RunOutputs
from langflow.schema.schema import INPUT_FIELD_NAME, Record
from langflow.schema import Record
from langflow.schema.schema import INPUT_FIELD_NAME
from langflow.services.database.models.flow import Flow
from langflow.services.deps import get_session, get_settings_service, session_scope
@ -279,4 +280,4 @@ def generate_unique_flow_name(flow_name, user_id, session):
# If a flow with the name already exists, append (n) to the name and increment n
flow_name = f"{original_name} ({n})"
n += 1
n += 1

View file

@ -1,7 +1,9 @@
from typing import Union
from langchain_core.documents import Document
from langflow.schema import Record
from langflow.schema.message import Message
def docs_to_records(documents: list[Document]) -> list[Record]:
@ -27,7 +29,7 @@ def records_to_text(template: str, records: Union[Record, list[Record]]) -> str:
Returns:
list[str]: The converted list of texts.
"""
if isinstance(records, Record):
if isinstance(records, (Record)):
records = [records]
# Check if there are any format strings in the template
_records = []
@ -39,3 +41,27 @@ def records_to_text(template: str, records: Union[Record, list[Record]]) -> str:
formated_records = [template.format(data=record.data, **record.data) for record in _records]
return "\n".join(formated_records)
def messages_to_text(template: str, messages: Union[Message, list[Message]]) -> str:
"""
Converts a list of Messages to a list of texts.
Args:
messages (list[Message]): The list of Messages to convert.
Returns:
list[str]: The converted list of texts.
"""
if isinstance(messages, (Message)):
messages = [messages]
# Check if there are any format strings in the template
_messages = []
for message in messages:
# If it is not a message, create one with the key "text"
if not isinstance(message, Message):
raise ValueError("All elements in the list must be of type Message.")
_messages.append(message)
formated_messages = [template.format(data=message.model_dump(), **message.model_dump()) for message in _messages]
return "\n".join(formated_messages)

View file

@ -1,3 +1,4 @@
import json
import logging
import os
import shutil
@ -12,17 +13,14 @@ from emoji import demojize, purely_emoji # type: ignore
from loguru import logger
from sqlmodel import select
from langflow.base.constants import FIELD_FORMAT_ATTRIBUTES, NODE_FORMAT_ATTRIBUTES
from langflow.base.constants import FIELD_FORMAT_ATTRIBUTES, NODE_FORMAT_ATTRIBUTES, ORJSON_OPTIONS
from langflow.interface.types import get_all_components
from langflow.services.auth.utils import create_super_user
from langflow.services.database.models.flow.model import Flow, FlowCreate
from langflow.services.database.models.folder.model import Folder, FolderCreate
from langflow.services.database.models.user.crud import get_user_by_username
from langflow.services.deps import get_settings_service, session_scope
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
from langflow.services.deps import get_variable_service, get_storage_service
from langflow.services.database.models.user.crud import get_user_by_username
from langflow.services.deps import get_settings_service, get_storage_service, get_variable_service, session_scope
STARTER_FOLDER_NAME = "Starter Projects"
STARTER_FOLDER_DESCRIPTION = "Starter projects to help you get started in Langflow."
@ -75,10 +73,84 @@ def update_projects_components_with_latest_component_versions(project_data, all_
}
)
node_data["template"][field_name][attr] = field_dict[attr]
# Remove fields that are not in the latest template
if node_data.get("display_name") != "Prompt":
for field_name in list(node_data["template"].keys()):
if field_name not in latest_template:
node_data["template"].pop(field_name)
log_node_changes(node_changes_log)
return project_data_copy
def update_edges_with_latest_component_versions(project_data):
edge_changes_log = defaultdict(list)
project_data_copy = deepcopy(project_data)
for edge in project_data_copy.get("edges", []):
source_handle = edge.get("data").get("sourceHandle")
target_handle = edge.get("data").get("targetHandle")
# Now find the source and target nodes in the nodes list
source_node = next(
(node for node in project_data.get("nodes", []) if node.get("id") == edge.get("source")), None
)
target_node = next(
(node for node in project_data.get("nodes", []) if node.get("id") == edge.get("target")), None
)
if source_node and target_node:
source_node_data = source_node.get("data").get("node")
target_node_data = target_node.get("data").get("node")
new_base_classes = source_node_data.get("base_classes")
if source_handle["baseClasses"] != new_base_classes:
edge_changes_log[source_node_data["display_name"]].append(
{
"attr": "baseClasses",
"old_value": source_handle["baseClasses"],
"new_value": new_base_classes,
}
)
source_handle["baseClasses"] = new_base_classes
field_name = target_handle.get("fieldName")
if field_name in target_node_data.get("template"):
if target_handle["inputTypes"] != target_node_data.get("template").get(field_name).get("input_types"):
edge_changes_log[target_node_data["display_name"]].append(
{
"attr": "inputTypes",
"old_value": target_handle["inputTypes"],
"new_value": target_node_data.get("template").get(field_name).get("input_types"),
}
)
target_handle["inputTypes"] = target_node_data.get("template").get(field_name).get("input_types")
escaped_source_handle = escape_json_dump(source_handle)
escaped_target_handle = escape_json_dump(target_handle)
if edge["sourceHandle"] != escaped_source_handle:
edge_changes_log[source_node_data["display_name"]].append(
{
"attr": "sourceHandle",
"old_value": edge["sourceHandle"],
"new_value": escaped_source_handle,
}
)
edge["sourceHandle"] = escaped_source_handle
if edge["targetHandle"] != escaped_target_handle:
edge_changes_log[target_node_data["display_name"]].append(
{
"attr": "targetHandle",
"old_value": edge["targetHandle"],
"new_value": escaped_target_handle,
}
)
edge["targetHandle"] = escaped_target_handle
else:
logger.error(f"Source or target node not found for edge: {edge}")
log_node_changes(edge_changes_log)
return project_data_copy
def escape_json_dump(edge_dict):
return json.dumps(edge_dict).replace('"', "œ")
def log_node_changes(node_changes_log):
# The idea here is to log the changes that were made to the nodes in debug
# Something like:
@ -155,7 +227,7 @@ def get_project_data(project):
def update_project_file(project_path, project, updated_project_data):
project["data"] = updated_project_data
with open(project_path, "w", encoding="utf-8") as f:
f.write(orjson.dumps(project, option=orjson.OPT_INDENT_2).decode())
f.write(orjson.dumps(project, option=ORJSON_OPTIONS).decode())
logger.info(f"Updated starter project {project['name']} file")
@ -320,6 +392,7 @@ def create_or_update_starter_projects():
updated_project_data = update_projects_components_with_latest_component_versions(
project_data, all_types_dict
)
updated_project_data = update_edges_with_latest_component_versions(updated_project_data)
if updated_project_data != project_data:
project_data = updated_project_data
# We also need to update the project data in the file

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@ from loguru import logger
from langflow.custom.eval import eval_custom_component_code
from langflow.graph.utils import get_artifact_type, post_process_raw
from langflow.schema.schema import Record
from langflow.schema import Record
if TYPE_CHECKING:
from langflow.custom import CustomComponent
@ -130,10 +130,10 @@ async def instantiate_custom_component(params, user_id, vertex, fallback_to_env_
if not isinstance(custom_repr, str):
custom_repr = str(custom_repr)
raw = custom_component.repr_value
if hasattr(raw, "data"):
if hasattr(raw, "data") and raw is not None:
raw = raw.data
elif hasattr(raw, "model_dump"):
elif hasattr(raw, "model_dump") and raw is not None:
raw = raw.model_dump()
artifact_type = get_artifact_type(custom_component, build_result)

View file

@ -1,9 +1,9 @@
import warnings
from typing import List, Optional, Union
from typing import List, Optional
from loguru import logger
from langflow.schema import Record
from langflow.schema.message import Message
from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import MessageModel
@ -39,54 +39,46 @@ def get_messages(
order=order,
)
records: list[Record] = []
messages: list[Message] = []
# messages_df has a timestamp
# it gets the last 5 messages, for example
# but now they are ordered from most recent to least recent
# so we need to reverse the order
messages_df = messages_df[::-1] if order == "DESC" else messages_df
for row in messages_df.itertuples():
record = Record(
data={
"text": row.message,
"sender": row.sender,
"sender_name": row.sender_name,
"session_id": row.session_id,
"timestamp": row.timestamp,
},
)
records.append(record)
msg = Message(text=row.text, sender=row.sender, sender_name=row.sender_name, timestamp=row.timestamp)
return records
messages.append(msg)
return messages
def add_messages(records: Union[list[Record], Record], flow_id: Optional[str] = None):
def add_messages(messages: Message | list[Message], flow_id: Optional[str] = None):
"""
Add a message to the monitor service.
"""
try:
monitor_service = get_monitor_service()
if not isinstance(messages, list):
messages = [messages]
if isinstance(records, Record):
records = [records]
if not all(isinstance(message, Message) for message in messages):
types = ", ".join([str(type(message)) for message in messages])
raise ValueError(f"The messages must be instances of Message. Found: {types}")
if not all(isinstance(record, (Record, str)) for record in records):
types = ", ".join([str(type(record)) for record in records])
raise ValueError(f"The records must be instances of Record. Found: {types}")
messages_models: list[MessageModel] = []
for msg in messages:
msg.timestamp = monitor_service.get_timestamp()
messages_models.append(MessageModel.from_message(msg, flow_id=flow_id))
messages: list[MessageModel] = []
for record in records:
record.timestamp = monitor_service.get_timestamp()
messages.append(MessageModel.from_record(record, flow_id=flow_id))
for message in messages:
for message_model in messages_models:
try:
monitor_service.add_message(message)
monitor_service.add_message(message_model)
except Exception as e:
logger.error(f"Error adding message to monitor service: {e}")
logger.exception(e)
raise e
return records
return messages_models
except Exception as e:
logger.exception(e)
raise e
@ -100,28 +92,22 @@ def delete_messages(session_id: str):
session_id (str): The session ID associated with the messages to delete.
"""
monitor_service = get_monitor_service()
monitor_service.delete_messages(session_id)
monitor_service.delete_messages_session(session_id)
def store_message(
message: Union[str, Record],
session_id: Optional[str] = None,
sender: Optional[str] = None,
sender_name: Optional[str] = None,
message: Message,
flow_id: Optional[str] = None,
) -> List[Record]:
) -> List[Message]:
"""
Stores a message in the memory.
Args:
message (Union[str, Record]): The message to be stored. It can be either a string or a Record object.
session_id (Optional[str]): The session ID associated with the message.
sender (Optional[str]): The sender ID associated with the message.
sender_name (Optional[str]): The name of the sender associated with the message.
message (Message): The message to store.
flow_id (Optional[str]): The flow ID associated with the message. When running from the CustomComponent you can access this using `self.graph.flow_id`.
Returns:
List[Record]: A list of records containing the stored message.
List[Message]: A list of records containing the stored message.
Raises:
ValueError: If any of the required parameters (session_id, sender, sender_name) is not provided.
@ -130,26 +116,7 @@ def store_message(
warnings.warn("No message provided.")
return []
if not session_id or not sender or not sender_name:
if not message.session_id or not message.sender or not message.sender_name:
raise ValueError("All of session_id, sender, and sender_name must be provided.")
if isinstance(message, Record):
record = message
record.data.update(
{
"session_id": session_id,
"sender": sender,
"sender_name": sender_name,
}
)
elif isinstance(message, str):
record = Record(
data={
"text": message,
"session_id": session_id,
"sender": sender,
"sender_name": sender_name,
},
)
return add_messages([record], flow_id=flow_id)
return add_messages([message], flow_id=flow_id)

View file

@ -1,4 +1,4 @@
from .dotdict import dotdict
from .schema import Record
from .record import Record
__all__ = ["Record", "dotdict"]

View file

@ -0,0 +1,63 @@
import base64
from PIL import Image as PILImage
from pydantic import BaseModel
from langflow.services.deps import get_storage_service
IMAGE_ENDPOINT = "/files/images/"
def is_image_file(file_path):
try:
with PILImage.open(file_path) as img:
img.verify() # Verify that it is, in fact, an image
return True
except (IOError, SyntaxError):
return False
async def get_file_paths(files: list[str]):
storage_service = get_storage_service()
file_paths = []
for file in files:
flow_id, file_name = file.split("/")
file_paths.append(storage_service.build_full_path(flow_id=flow_id, file_name=file_name))
return file_paths
async def get_files(
file_paths: list[str],
convert_to_base64: bool = False,
):
storage_service = get_storage_service()
file_objects: list[str | bytes] = []
for file_path in file_paths:
flow_id, file_name = file_path.split("/")
file_object = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
if convert_to_base64:
file_base64 = base64.b64encode(file_object).decode("utf-8")
file_objects.append(file_base64)
else:
file_objects.append(file_object)
return file_objects
class Image(BaseModel):
path: str | None = None
url: str | None = None
def to_base64(self):
if self.path:
files = get_files([self.path], convert_to_base64=True)
return files[0]
raise ValueError("Image path is not set.")
def to_content_dict(self):
return {
"type": "image_url",
"image_url": self.to_base64(),
}
def get_url(self):
return f"{IMAGE_ENDPOINT}{self.path}"

View file

@ -0,0 +1,111 @@
from datetime import datetime, timezone
from typing import Annotated, Any, AsyncIterator, Iterator, Optional
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.prompt_values import ImagePromptValue
from langchain_core.prompts.image import ImagePromptTemplate
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, field_serializer
from langflow.schema.image import Image, get_file_paths, is_image_file
from langflow.schema.record import Record
def _timestamp_to_str(timestamp: datetime) -> str:
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
class Message(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
# Helper class to deal with image data
text: Optional[str | AsyncIterator | Iterator] = Field(default="")
sender: str
sender_name: str
files: Optional[list[str | Image]] = Field(default=[])
session_id: Optional[str] = Field(default="")
timestamp: Annotated[str, BeforeValidator(_timestamp_to_str)] = Field(
default=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
)
flow_id: Optional[str] = None
def model_post_init(self, __context: Any) -> None:
new_files = []
for file in self.files or []:
if is_image_file(file):
new_files.append(Image(path=file))
else:
new_files.append(file)
self.files = new_files
def to_lc_message(
self,
) -> BaseMessage:
"""
Converts the Record to a BaseMessage.
Returns:
BaseMessage: The converted BaseMessage.
"""
# The idea of this function is to be a helper to convert a Record to a BaseMessage
# It will use the "sender" key to determine if the message is Human or AI
# If the key is not present, it will default to AI
# But first we check if all required keys are present in the data dictionary
# they are: "text", "sender"
if self.text is None or not self.sender:
raise ValueError("Missing required keys ('text', 'sender') in Message.")
if self.sender == "User":
if self.files:
contents = [{"type": "text", "text": self.text}]
contents.extend(self.get_file_content_dicts())
human_message = HumanMessage(content=contents)
else:
human_message = HumanMessage(
content=[{"type": "text", "text": self.text}],
)
return human_message
return AIMessage(content=self.text)
@classmethod
def from_record(cls, record: Record) -> "Message":
"""
Converts a BaseMessage to a Record.
Args:
record (BaseMessage): The BaseMessage to convert.
Returns:
Record: The converted Record.
"""
return cls(
text=record.text,
sender=record.sender,
sender_name=record.sender_name,
files=record.files,
session_id=record.session_id,
timestamp=record.timestamp,
flow_id=record.flow_id,
)
@field_serializer("text", mode="plain")
def serialize_text(self, value):
if isinstance(value, AsyncIterator):
return ""
elif isinstance(value, Iterator):
return ""
return value
async def get_file_content_dicts(self):
content_dicts = []
files = await get_file_paths(self.files)
for file in files:
if isinstance(file, Image):
content_dicts.append(file.to_content_dict())
else:
image_template = ImagePromptTemplate()
image_prompt_value: ImagePromptValue = image_template.invoke(input={"path": file})
content_dicts.append({"type": "image_url", "image_url": image_prompt_value.image_url})
return content_dicts

View file

@ -0,0 +1,202 @@
import copy
import json
from typing import cast, Optional
from langchain_core.documents import Document
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
from langchain_core.prompts.image import ImagePromptTemplate
from pydantic import BaseModel, model_serializer, model_validator
from langchain_core.prompt_values import ImagePromptValue
class Record(BaseModel):
"""
Represents a record with text and optional data.
Attributes:
data (dict, optional): Additional data associated with the record.
"""
text_key: str = "text"
data: dict = {}
default_value: Optional[str] = ""
@model_validator(mode="before")
def validate_data(cls, values):
if not values.get("data"):
values["data"] = {}
# Any other keyword should be added to the data dictionary
for key in values:
if key not in values["data"] and key not in {"text_key", "data", "default_value"}:
values["data"][key] = values[key]
return values
@model_serializer(mode="plain", when_used="json")
def serialize_model(self):
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return data
def get_text(self):
"""
Retrieves the text value from the data dictionary.
If the text key is present in the data dictionary, the corresponding value is returned.
Otherwise, the default value is returned.
Returns:
The text value from the data dictionary or the default value.
"""
return self.data.get(self.text_key, self.default_value)
@classmethod
def from_document(cls, document: Document) -> "Record":
"""
Converts a Document to a Record.
Args:
document (Document): The Document to convert.
Returns:
Record: The converted Record.
"""
data = document.metadata
data["text"] = document.page_content
return cls(data=data, text_key="text")
@classmethod
def from_lc_message(cls, message: BaseMessage) -> "Record":
"""
Converts a BaseMessage to a Record.
Args:
message (BaseMessage): The BaseMessage to convert.
Returns:
Record: The converted Record.
"""
data: dict = {"text": message.content}
data["metadata"] = cast(dict, message.to_json())
return cls(data=data, text_key="text")
def __add__(self, other: "Record") -> "Record":
"""
Combines the data of two records by attempting to add values for overlapping keys
for all types that support the addition operation. Falls back to the value from 'other'
record when addition is not supported.
"""
combined_data = self.data.copy()
for key, value in other.data.items():
# If the key exists in both records and both values support the addition operation
if key in combined_data:
try:
combined_data[key] += value
except TypeError:
# Fallback: Use the value from 'other' record if addition is not supported
combined_data[key] = value
else:
# If the key is not in the first record, simply add it
combined_data[key] = value
return Record(data=combined_data)
def to_lc_document(self) -> Document:
"""
Converts the Record to a Document.
Returns:
Document: The converted Document.
"""
text = self.data.pop(self.text_key, self.default_value)
return Document(page_content=text, metadata=self.data)
def to_lc_message(
self,
) -> HumanMessage | SystemMessage:
"""
Converts the Record to a BaseMessage.
Returns:
BaseMessage: The converted BaseMessage.
"""
# The idea of this function is to be a helper to convert a Record to a BaseMessage
# It will use the "sender" key to determine if the message is Human or AI
# If the key is not present, it will default to AI
# But first we check if all required keys are present in the data dictionary
# they are: "text", "sender"
if not all(key in self.data for key in ["text", "sender"]):
raise ValueError(f"Missing required keys ('text', 'sender') in Record: {self.data}")
sender = self.data.get("sender", "Machine")
text = self.data.get("text", "")
files = self.data.get("files", [])
if sender == "User":
if files:
contents = [{"type": "text", "text": text}]
for file_path in files:
image_template = ImagePromptTemplate()
image_prompt_value: ImagePromptValue = image_template.invoke(input={"path": file_path})
contents.append({"type": "image_url", "image_url": image_prompt_value.image_url})
human_message = HumanMessage(content=contents)
else:
human_message = HumanMessage(
content=[{"type": "text", "text": text}],
)
return human_message
return AIMessage(content=text)
def __getattr__(self, key):
"""
Allows attribute-like access to the data dictionary.
"""
try:
if key.startswith("__"):
return self.__getattribute__(key)
if key in {"data", "text_key"} or key.startswith("_"):
return super().__getattr__(key)
return self.data.get(key, self.default_value)
except KeyError:
# Fallback to default behavior to raise AttributeError for undefined attributes
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'")
def __setattr__(self, key, value):
"""
Allows attribute-like setting of values in the data dictionary,
while still allowing direct assignment to class attributes.
"""
if key in {"data", "text_key"} or key.startswith("_"):
super().__setattr__(key, value)
else:
self.data[key] = value
def __delattr__(self, key):
"""
Allows attribute-like deletion from the data dictionary.
"""
if key in {"data", "text_key"} or key.startswith("_"):
super().__delattr__(key)
else:
del self.data[key]
def __deepcopy__(self, memo):
"""
Custom deepcopy implementation to handle copying of the Record object.
"""
# Create a new Record object with a deep copy of the data dictionary
return Record(data=copy.deepcopy(self.data, memo), text_key=self.text_key, default_value=self.default_value)
# check which attributes the Record has by checking the keys in the data dictionary
def __dir__(self):
return super().__dir__() + list(self.data.keys())
def __str__(self) -> str:
# return a JSON string representation of the Record atributes
try:
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return json.dumps(data, indent=4)
except Exception:
return str(self.data)
def __contains__(self, key):
return key in self.data

View file

@ -1,207 +1,7 @@
import copy
import json
from typing import Literal, Optional, cast
from typing import Literal
from typing_extensions import TypedDict
from langchain_core.documents import Document
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.prompts.image import ImagePromptTemplate
from pydantic import BaseModel, model_serializer, model_validator
class Record(BaseModel):
"""
Represents a record with text and optional data.
Attributes:
data (dict, optional): Additional data associated with the record.
"""
text_key: str = "text"
data: dict = {}
default_value: Optional[str] = ""
@model_validator(mode="before")
def validate_data(cls, values):
if not values.get("data"):
values["data"] = {}
# Any other keyword should be added to the data dictionary
for key in values:
if key not in values["data"] and key not in {"text_key", "data", "default_value"}:
values["data"][key] = values[key]
return values
@model_serializer(mode="plain", when_used="json")
def serialize_model(self):
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return data
def get_text(self):
"""
Retrieves the text value from the data dictionary.
If the text key is present in the data dictionary, the corresponding value is returned.
Otherwise, the default value is returned.
Returns:
The text value from the data dictionary or the default value.
"""
return self.data.get(self.text_key, self.default_value)
@classmethod
def from_document(cls, document: Document) -> "Record":
"""
Converts a Document to a Record.
Args:
document (Document): The Document to convert.
Returns:
Record: The converted Record.
"""
data = document.metadata
data["text"] = document.page_content
return cls(data=data, text_key="text")
@classmethod
def from_lc_message(cls, message: BaseMessage) -> "Record":
"""
Converts a BaseMessage to a Record.
Args:
message (BaseMessage): The BaseMessage to convert.
Returns:
Record: The converted Record.
"""
data: dict = {"text": message.content}
data["metadata"] = cast(dict, message.to_json())
return cls(data=data, text_key="text")
def __add__(self, other: "Record") -> "Record":
"""
Combines the data of two records by attempting to add values for overlapping keys
for all types that support the addition operation. Falls back to the value from 'other'
record when addition is not supported.
"""
combined_data = self.data.copy()
for key, value in other.data.items():
# If the key exists in both records and both values support the addition operation
if key in combined_data:
try:
combined_data[key] += value
except TypeError:
# Fallback: Use the value from 'other' record if addition is not supported
combined_data[key] = value
else:
# If the key is not in the first record, simply add it
combined_data[key] = value
return Record(data=combined_data)
def to_lc_document(self) -> Document:
"""
Converts the Record to a Document.
Returns:
Document: The converted Document.
"""
text = self.data.pop(self.text_key, self.default_value)
return Document(page_content=text, metadata=self.data)
def to_lc_message(
self,
) -> BaseMessage:
"""
Converts the Record to a BaseMessage.
Returns:
BaseMessage: The converted BaseMessage.
"""
# The idea of this function is to be a helper to convert a Record to a BaseMessage
# It will use the "sender" key to determine if the message is Human or AI
# If the key is not present, it will default to AI
# But first we check if all required keys are present in the data dictionary
# they are: "text", "sender"
if not all(key in self.data for key in ["text", "sender"]):
raise ValueError(f"Missing required keys ('text', 'sender') in Record: {self.data}")
sender = self.data.get("sender", "Machine")
text = self.data.get("text", "")
files = self.data.get("files", [])
if sender == "User":
if files:
contents = [{"type": "text", "text": text}]
for file_path in files:
image_template = ImagePromptTemplate()
image_prompt_value = image_template.invoke(input={"path": file_path})
contents.append({"type": "image_url", "image_url": image_prompt_value.image_url})
human_message = HumanMessage(content=contents)
else:
human_message = HumanMessage(
content=[{"type": "text", "text": text}],
)
return human_message
return AIMessage(content=text)
def __getattr__(self, key):
"""
Allows attribute-like access to the data dictionary.
"""
try:
if key.startswith("__"):
return self.__getattribute__(key)
if key in {"data", "text_key"} or key.startswith("_"):
return super().__getattr__(key)
return self.data.get(key, self.default_value)
except KeyError:
# Fallback to default behavior to raise AttributeError for undefined attributes
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'")
def __setattr__(self, key, value):
"""
Allows attribute-like setting of values in the data dictionary,
while still allowing direct assignment to class attributes.
"""
if key in {"data", "text_key"} or key.startswith("_"):
super().__setattr__(key, value)
else:
self.data[key] = value
def __delattr__(self, key):
"""
Allows attribute-like deletion from the data dictionary.
"""
if key in {"data", "text_key"} or key.startswith("_"):
super().__delattr__(key)
else:
del self.data[key]
def __deepcopy__(self, memo):
"""
Custom deepcopy implementation to handle copying of the Record object.
"""
# Create a new Record object with a deep copy of the data dictionary
return Record(data=copy.deepcopy(self.data, memo), text_key=self.text_key, default_value=self.default_value)
# check which attributes the Record has by checking the keys in the data dictionary
def __dir__(self):
return super().__dir__() + list(self.data.keys())
def __str__(self) -> str:
# return a JSON string representation of the Record atributes
try:
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return json.dumps(data, indent=4)
except Exception:
return str(self.data)
def __contains__(self, key):
return key in self.data
INPUT_FIELD_NAME = "input_value"
InputType = Literal["chat", "text", "any"]

View file

@ -13,7 +13,7 @@ from pydantic import field_serializer, field_validator
from sqlalchemy import UniqueConstraint
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.schema.schema import Record
from langflow.schema import Record
if TYPE_CHECKING:
from langflow.services.database.models.folder import Folder

View file

@ -1,11 +1,10 @@
import json
from datetime import datetime
from typing import TYPE_CHECKING, Any, Optional
from typing import Any, Optional
from pydantic import BaseModel, Field, field_serializer, field_validator
if TYPE_CHECKING:
from langflow.schema import Record
from langflow.schema.message import Message
class TransactionModel(BaseModel):
@ -77,7 +76,7 @@ class MessageModel(BaseModel):
sender: str
sender_name: str
session_id: str
message: str
text: str
files: list[str] = []
class Config:
@ -91,18 +90,17 @@ class MessageModel(BaseModel):
return v
@classmethod
def from_record(cls, record: "Record", flow_id: Optional[str] = None):
def from_message(cls, message: Message, flow_id: Optional[str] = None):
# first check if the record has all the required fields
if not record.data or ("sender" not in record.data and "sender_name" not in record.data):
raise ValueError("The record does not have the required fields 'sender' and 'sender_name' in the data.")
if not message.text or not message.sender or not message.sender_name:
raise ValueError("The message does not have the required fields 'sender' and 'sender_name' in the data.")
return cls(
sender=record.sender,
sender_name=record.sender_name,
message=record.text,
session_id=record.session_id,
files=record.files or [],
artifacts=record.artifacts or {},
timestamp=record.timestamp,
sender=message.sender,
sender_name=message.sender_name,
text=message.text,
session_id=message.session_id,
files=message.files or [],
timestamp=message.timestamp,
flow_id=flow_id,
)
@ -121,7 +119,7 @@ class MessageModelResponse(MessageModel):
class MessageModelRequest(MessageModel):
message: str = Field(default="")
text: str = Field(default="")
sender: str = Field(default="")
sender_name: str = Field(default="")
session_id: str = Field(default="")

View file

@ -92,8 +92,6 @@ class MonitorService(Service):
with duckdb.connect(str(self.db_path)) as conn:
df = conn.execute(query).df()
print(query)
return df.to_dict(orient="records")
def delete_vertex_builds(self, flow_id: Optional[str] = None):
@ -134,7 +132,7 @@ class MonitorService(Service):
order: Optional[str] = "DESC",
limit: Optional[int] = None,
):
query = "SELECT index, flow_id, sender_name, sender, session_id, message, timestamp FROM messages"
query = "SELECT index, flow_id, sender_name, sender, session_id, text, timestamp FROM messages"
conditions = []
if sender:
conditions.append(f"sender = '{sender}'")

View file

@ -10,5 +10,5 @@ class DefaultPromptField(TemplateField):
advanced: bool = False
multiline: bool = True
input_types: list[str] = ["Document", "Record", "Text"]
input_types: list[str] = ["Document", "Message", "Record", "Text"]
value: str = "" # Set the value to empty string

View file

@ -7,8 +7,7 @@ from typing import Any, Dict, List, Optional, Union
from docstring_parser import parse
from langflow.schema.schema import Record
from langflow.schema import Record
from langflow.services.deps import get_settings_service
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants

View file

@ -24,6 +24,7 @@ export default function SwitchOutputView(nodeId): JSX.Element {
const results = flowPoolNode?.data?.logs[0] ?? "";
const resultType = results?.type;
let resultMessage = results?.message;
const RECORD_TYPES = ["record", "object", "array", "message"];
if (resultMessage.raw) {
resultMessage = resultMessage.raw;
}
@ -41,34 +42,20 @@ export default function SwitchOutputView(nodeId): JSX.Element {
<TextOutputView left={false} value={resultMessage} />
</Case>
<Case condition={resultType === "record"}>
<Case condition={RECORD_TYPES.includes(resultType)}>
<RecordsOutputComponent
rows={[resultMessage] ?? []}
rows={
Array.isArray(resultMessage)
? (resultMessage as Array<any>).every((item) => item.data)
? (resultMessage as Array<any>).map((item) => item.data)
: resultMessage
: [resultMessage]
}
pagination={true}
columnMode="union"
/>
</Case>
<Case condition={resultType === "object"}>
<RecordsOutputComponent
rows={[resultMessage]}
pagination={true}
columnMode="union"
/>
</Case>
{Array.isArray(resultMessage) && (
<Case condition={resultType === "array"}>
<RecordsOutputComponent
rows={
(resultMessage as Array<any>).every((item) => item.data)
? (resultMessage as Array<any>).map((item) => item.data)
: resultMessage
}
pagination={true}
columnMode="union"
/>
</Case>
)}
<Case condition={resultType === "stream"}>
<div className="flex h-full w-full items-center justify-center align-middle">
<Alert variant={"default"} className="w-fit">

View file

@ -63,7 +63,7 @@ export async function sendAll(data: sendAllProps) {
}
export async function postValidateCode(
code: string,
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code });
}
@ -78,7 +78,7 @@ export async function postValidateCode(
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType,
frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
@ -151,7 +151,7 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType,
updatedFlow: FlowType
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
@ -329,7 +329,7 @@ export async function getHealth() {
*
*/
export async function getBuildStatus(
flowId: string,
flowId: string
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
@ -342,7 +342,7 @@ export async function getBuildStatus(
*
*/
export async function postBuildInit(
flow: FlowType,
flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
@ -358,7 +358,7 @@ export async function postBuildInit(
*/
export async function uploadFile(
file: File,
id: string,
id: string
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
@ -380,7 +380,7 @@ export async function getProfilePictures(): Promise<ProfilePicturesTypeAPI | nul
export async function postCustomComponent(
code: string,
apiClass: APIClassType,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
@ -393,7 +393,7 @@ export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any,
field_value: any
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
@ -415,7 +415,7 @@ export async function onLogin(user: LoginType) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
}
);
if (response.status === 200) {
@ -477,11 +477,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage(
skip: number,
limit: number,
limit: number
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
);
if (res.status === 200) {
return res.data;
@ -518,7 +518,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user,
user
);
if (res.status === 200) {
return res.data;
@ -592,7 +592,7 @@ export async function saveFlowStore(
last_tested_version?: string;
},
tags: string[],
publicFlow = false,
publicFlow = false
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -721,7 +721,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`,
`${BASE_URL_API}store/components/${component_id}`
);
if (res.status === 200) {
return res.data;
@ -736,7 +736,7 @@ export async function searchComponent(
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[],
tags?: string[]
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
@ -848,7 +848,7 @@ export async function updateFlowStore(
},
tags: string[],
publicFlow = false,
id: string,
id: string
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -932,7 +932,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable(
name: string,
value: string,
id: string,
id: string
) {
try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -951,7 +951,7 @@ export async function getVerticesOrder(
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?: Node[],
Edges?: Edge[],
Edges?: Edge[]
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
@ -971,7 +971,7 @@ export async function getVerticesOrder(
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`,
data,
config,
config
);
}
@ -979,16 +979,19 @@ export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string,
files?: string[],
files?: string[]
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
const data = { inputs: { input_value: input_value ?? "" } };
let data = {};
if (typeof input_value !== "undefined") {
data["inputs"] = { input_value: input_value };
}
if (data && files) {
data["files"] = files;
}
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
data,
data
);
}
@ -1012,7 +1015,7 @@ export async function getFlowPool({
}
export async function deleteFlowPool(
flowId: string,
flowId: string
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
@ -1026,7 +1029,7 @@ export async function deleteFlowPool(
* @returns A promise that resolves to an array of AxiosResponse objects representing the delete responses.
*/
export async function multipleDeleteFlowsComponents(
flowIds: string[],
flowIds: string[]
): Promise<AxiosResponse<any>[]> {
const batches: string[][] = [];
@ -1049,7 +1052,7 @@ export async function multipleDeleteFlowsComponents(
// Execute all delete requests
const responses: Promise<AxiosResponse<any>>[] = batches.map((batch) =>
deleteBatch(batch),
deleteBatch(batch)
);
// Return the responses after all requests are completed
@ -1059,7 +1062,7 @@ export async function multipleDeleteFlowsComponents(
export async function getTransactionTable(
id: string,
mode: "intersection" | "union",
params = {},
params = {}
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
@ -1075,7 +1078,7 @@ export async function getMessagesTable(
mode: "intersection" | "union",
id?: string,
excludedFields?: string[],
params = {},
params = {}
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
if (id) {

View file

@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
import httpx
import pytest
import respx
from dictdiffer import diff
from httpx import Response
from langflow.components import data
@ -164,8 +165,8 @@ def test_directory_without_mocks():
assert len(results) == len(projects)
# each result is a Record that contains the content attribute
# each are dict that are exactly the same as one of the projects
for result in results:
assert result.text in projects
for i, result in enumerate(results):
assert result.text in projects, list(diff(result.text, projects[i]))
# in ../docs/docs/components there are many mdx files
# check if the directory component can load them

View file

@ -448,7 +448,7 @@ def test_successful_run_no_payload(client, starter_project, created_api_key):
assert all(["ChatOutput" in _id for _id in ids])
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
assert all([name in display_names for name in ["Chat Output"]])
inner_results = [output.get("results").get("result") for output in outputs_dict.get("outputs")]
inner_results = [output.get("results").get("text") for output in outputs_dict.get("outputs")]
assert all([result is not None for result in inner_results]), inner_results
@ -478,7 +478,7 @@ def test_successful_run_with_output_type_text(client, starter_project, created_a
assert all(["ChatOutput" in _id for _id in ids]), ids
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
assert all([name in display_names for name in ["Chat Output"]]), display_names
inner_results = [output.get("results").get("result") for output in outputs_dict.get("outputs")]
inner_results = [output.get("results").get("text") for output in outputs_dict.get("outputs")]
expected_result = ""
assert all([expected_result in result for result in inner_results]), inner_results
@ -509,7 +509,7 @@ def test_successful_run_with_output_type_any(client, starter_project, created_ap
assert all(["ChatOutput" in _id or "TextOutput" in _id for _id in ids]), ids
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
assert all([name in display_names for name in ["Chat Output"]]), display_names
inner_results = [output.get("results").get("result") for output in outputs_dict.get("outputs")]
inner_results = [output.get("results").get("text") for output in outputs_dict.get("outputs")]
expected_result = ""
assert all([expected_result in result for result in inner_results]), inner_results
@ -567,7 +567,7 @@ def test_successful_run_with_input_type_text(client, starter_project, created_ap
text_input_outputs = [output for output in outputs_dict.get("outputs") if "TextInput" in output.get("component_id")]
assert len(text_input_outputs) == 0
# Now we check if the input_value is correct
assert all([output.get("results").get("result") == "value1" for output in text_input_outputs]), text_input_outputs
assert all([output.get("results").get("text") == "value1" for output in text_input_outputs]), text_input_outputs
# Now do the same for "chat" input type
@ -598,7 +598,7 @@ def test_successful_run_with_input_type_chat(client, starter_project, created_ap
chat_input_outputs = [output for output in outputs_dict.get("outputs") if "ChatInput" in output.get("component_id")]
assert len(chat_input_outputs) == 1
# Now we check if the input_value is correct
assert all([output.get("results").get("result") == "value1" for output in chat_input_outputs]), chat_input_outputs
assert all([output.get("results").get("text") == "value1" for output in chat_input_outputs]), chat_input_outputs
def test_successful_run_with_input_type_any(client, starter_project, created_api_key):
@ -632,7 +632,7 @@ def test_successful_run_with_input_type_any(client, starter_project, created_api
]
assert len(any_input_outputs) == 1
# Now we check if the input_value is correct
assert all([output.get("results").get("result") == "value1" for output in any_input_outputs]), any_input_outputs
assert all([output.get("results").get("text") == "value1" for output in any_input_outputs]), any_input_outputs
@pytest.mark.api_key_required