Merge remote-tracking branch 'origin/dev' into two_edges
This commit is contained in:
commit
d0829f5dbe
81 changed files with 1127 additions and 1069 deletions
7
.github/actions/poetry_caching/action.yml
vendored
7
.github/actions/poetry_caching/action.yml
vendored
|
|
@ -77,7 +77,12 @@ runs:
|
||||||
POETRY_VERSION: ${{ inputs.poetry-version }}
|
POETRY_VERSION: ${{ inputs.poetry-version }}
|
||||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||||
# Install poetry using the python version installed by setup-python step.
|
# Install poetry using the python version installed by setup-python step.
|
||||||
run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose
|
run: |
|
||||||
|
pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose
|
||||||
|
pipx ensurepath
|
||||||
|
# Ensure the poetry binary is available in the PATH.
|
||||||
|
# Test that the poetry binary is available.
|
||||||
|
poetry --version
|
||||||
|
|
||||||
- name: Restore pip and poetry cached dependencies
|
- name: Restore pip and poetry cached dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|
|
||||||
102
README.md
102
README.md
|
|
@ -1,21 +1,49 @@
|
||||||
<!-- markdownlint-disable MD030 -->
|
<!-- markdownlint-disable MD030 -->
|
||||||
|
|
||||||
# [](https://www.langflow.org)
|
# [](https://www.langflow.org)
|
||||||
|
|
||||||
### [Langflow](https://www.langflow.org) is a new, visual way to build, iterate and deploy AI apps.
|
<p align="center"><strong>
|
||||||
|
A visual framework for building Gen-AI and RAG apps with LangChain
|
||||||
|
</strong></p>
|
||||||
|
<p align="center" style="font-size: 12px;">
|
||||||
|
Open-source, Python-powered, fully customizable, LLM and vector store agnostic
|
||||||
|
</p>
|
||||||
|
|
||||||
# ⚡️ Documentation and Community
|
<p align="center" style="font-size: 12px;">
|
||||||
|
<a href="https://docs.langflow.org" style="text-decoration: underline;">Docs</a> -
|
||||||
|
<a href="https://discord.com/invite/EqksyE2EX9" style="text-decoration: underline;">Join our Discord</a> -
|
||||||
|
<a href="https://twitter.com/langflow_ai" style="text-decoration: underline;">Follow us on X</a> -
|
||||||
|
<a href="https://huggingface.co/spaces/Langflow/Langflow-Preview" style="text-decoration: underline;">Live demo</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
- [Documentation](https://docs.langflow.org)
|
<p align="center">
|
||||||
- [Discord](https://discord.com/invite/EqksyE2EX9)
|
<a href="https://github.com/langflow-ai/langflow">
|
||||||
|
<img src="https://img.shields.io/github/stars/langflow-ai/langflow">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.com/invite/EqksyE2EX9">
|
||||||
|
<img src="https://img.shields.io/discord/1116803230643527710?label=Discord">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
# 📦 Installation
|
<p align="center">
|
||||||
|
<img src="./docs/static/img/langflow_basic_howto.gif" alt="Your GIF" style="border: 3px solid #211C43;">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# 📝 Content
|
||||||
|
|
||||||
|
- [Get Started](#-get-started)
|
||||||
|
- [Create Flows](#-create-flows)
|
||||||
|
- [Deploy](#deploy)
|
||||||
|
- [Command Line Interface (CLI)](#️-command-line-interface-cli)
|
||||||
|
- [Contribute](#-contribute)
|
||||||
|
|
||||||
|
# 📦 Get Started
|
||||||
|
|
||||||
You can install Langflow with pip:
|
You can install Langflow with pip:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Make sure you have Python 3.10 installed on your system.
|
# Make sure you have Python 3.10 or greater installed on your system.
|
||||||
# Install the pre-release version
|
# Install the pre-release version (recommended for the latest updates)
|
||||||
python -m pip install langflow --pre --force-reinstall
|
python -m pip install langflow --pre --force-reinstall
|
||||||
|
|
||||||
# or stable version
|
# or stable version
|
||||||
|
|
@ -28,9 +56,9 @@ Then, run Langflow with:
|
||||||
python -m langflow run
|
python -m langflow run
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also preview Langflow in [HuggingFace Spaces](https://huggingface.co/spaces/Langflow/Langflow-Preview). [Clone the space using this link](https://huggingface.co/spaces/Langflow/Langflow-Preview?duplicate=true), to create your own Langflow workspace in minutes.
|
You can also preview Langflow in [HuggingFace Spaces](https://huggingface.co/spaces/Langflow/Langflow-Preview). [Clone the space using this link](https://huggingface.co/spaces/Langflow/Langflow-Preview?duplicate=true) to create your own Langflow workspace in minutes.
|
||||||
|
|
||||||
# 🎨 Creating Flows
|
# 🎨 Create Flows
|
||||||
|
|
||||||
Creating flows with Langflow is easy. Simply drag components from the sidebar onto the canvas and connect them to start building your application.
|
Creating flows with Langflow is easy. Simply drag components from the sidebar onto the canvas and connect them to start building your application.
|
||||||
|
|
||||||
|
|
@ -46,6 +74,32 @@ from langflow.load import run_flow_from_json
|
||||||
results = run_flow_from_json("path/to/flow.json", input_value="Hello, World!")
|
results = run_flow_from_json("path/to/flow.json", input_value="Hello, World!")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
|
||||||
|
## Deploy Langflow on Google Cloud Platform
|
||||||
|
|
||||||
|
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](https://github.com/langflow-ai/langflow/blob/dev/docs/docs/deployment/gcp-deployment.md) document.
|
||||||
|
|
||||||
|
Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Cloud Shell, clone the Langflow repository, and start an **interactive tutorial** that will guide you through the process of setting up the necessary resources and deploying Langflow on your GCP project.
|
||||||
|
|
||||||
|
[](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial_spot.md)
|
||||||
|
|
||||||
|
## Deploy on Railway
|
||||||
|
|
||||||
|
Use this template to deploy Langflow 1.0 Preview on Railway:
|
||||||
|
|
||||||
|
[](https://railway.app/template/UsJ1uB?referralCode=MnPSdg)
|
||||||
|
|
||||||
|
Or this one to deploy Langflow 0.6.x:
|
||||||
|
|
||||||
|
[](https://railway.app/template/JMXEWp?referralCode=MnPSdg)
|
||||||
|
|
||||||
|
## Deploy on Render
|
||||||
|
|
||||||
|
<a href="https://render.com/deploy?repo=https://github.com/langflow-ai/langflow/tree/main">
|
||||||
|
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" />
|
||||||
|
</a>
|
||||||
|
|
||||||
# 🖥️ Command Line Interface (CLI)
|
# 🖥️ Command Line Interface (CLI)
|
||||||
|
|
||||||
Langflow provides a command-line interface (CLI) for easy management and configuration.
|
Langflow provides a command-line interface (CLI) for easy management and configuration.
|
||||||
|
|
@ -87,33 +141,7 @@ You can configure many of the CLI options using environment variables. These can
|
||||||
|
|
||||||
A sample `.env` file named `.env.example` is included with the project. Copy this file to a new file named `.env` and replace the example values with your actual settings. If you're setting values in both your OS and the `.env` file, the `.env` settings will take precedence.
|
A sample `.env` file named `.env.example` is included with the project. Copy this file to a new file named `.env` and replace the example values with your actual settings. If you're setting values in both your OS and the `.env` file, the `.env` settings will take precedence.
|
||||||
|
|
||||||
# Deployment
|
# 👋 Contribute
|
||||||
|
|
||||||
## Deploy Langflow on Google Cloud Platform
|
|
||||||
|
|
||||||
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](GCP_DEPLOYMENT.md) document.
|
|
||||||
|
|
||||||
Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Cloud Shell, clone the Langflow repository, and start an **interactive tutorial** that will guide you through the process of setting up the necessary resources and deploying Langflow on your GCP project.
|
|
||||||
|
|
||||||
[](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial_spot.md)
|
|
||||||
|
|
||||||
## Deploy on Railway
|
|
||||||
|
|
||||||
Use this template to deploy Langflow 1.0 Preview on Railway:
|
|
||||||
|
|
||||||
[](https://railway.app/template/UsJ1uB?referralCode=MnPSdg)
|
|
||||||
|
|
||||||
Or this one to deploy Langflow 0.6.x:
|
|
||||||
|
|
||||||
[](https://railway.app/template/JMXEWp?referralCode=MnPSdg)
|
|
||||||
|
|
||||||
## Deploy on Render
|
|
||||||
|
|
||||||
<a href="https://render.com/deploy?repo=https://github.com/langflow-ai/langflow/tree/main">
|
|
||||||
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
# 👋 Contributing
|
|
||||||
|
|
||||||
We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible.
|
We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,6 @@ class DocumentProcessor(CustomComponent):
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Admonition type="tip">
|
|
||||||
Check out [FlowRunner Component](../examples/flow-runner) for a more complex
|
|
||||||
example.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,3 @@ The `CustomComponent` class also provides helpful methods for specific tasks (e.
|
||||||
- `field_order`: Controls the display order of fields.
|
- `field_order`: Controls the display order of fields.
|
||||||
- `icon`: Sets the canvas display icon.
|
- `icon`: Sets the canvas display icon.
|
||||||
|
|
||||||
<Admonition type="info" label="Tip">
|
|
||||||
Check out the [FlowRunner](../examples/flow-runner) example to understand how to call a flow from a custom component.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# Buffer Memory
|
|
||||||
|
|
||||||
For certain applications, retaining past interactions is crucial. For that, chains and agents may accept a memory component as one of their input parameters. The `ConversationBufferMemory` component is one of them. It stores messages and extracts them into variables.
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/buffer-memory.png",
|
|
||||||
dark: "img/buffer-memory.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "80%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/Buffer_Memory.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`ConversationBufferMemory`](https://python.langchain.com/docs/modules/memory/types/buffer)
|
|
||||||
- [`ConversationChain`](https://python.langchain.com/docs/modules/chains/)
|
|
||||||
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
17
docs/docs/examples/chat-memory.mdx
Normal file
17
docs/docs/examples/chat-memory.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Chat Memory
|
||||||
|
|
||||||
|
The **Chat Memory** component restores previous messages given a Session ID, which can be any string.
|
||||||
|
|
||||||
|
This component is available under the **Helpers** tab of the Langflow preview.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/chat_memory.mp4" />
|
||||||
|
</div>
|
||||||
21
docs/docs/examples/combine-text.mdx
Normal file
21
docs/docs/examples/combine-text.mdx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Combine Text
|
||||||
|
|
||||||
|
With LLM pipelines, combining text from different sources may be as important as splitting text.
|
||||||
|
|
||||||
|
The **Combine Text** component concatenates two text inputs into a single chunk using a specified delimiter, such as whitespace or a newline.
|
||||||
|
|
||||||
|
Also, check out **Combine Texts (Unsorted)** as a similar alternative.
|
||||||
|
|
||||||
|
This component is available under the **Helpers** tab of the Langflow preview.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/combine_text.mp4" />
|
||||||
|
</div>
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# Conversation Chain
|
|
||||||
|
|
||||||
This example shows how to instantiate a simple `ConversationChain` component using a Language Model (LLM). Once the Node Status turns green 🟢, the chat will be ready to take in user messages. Here, we used `ChatOpenAI` to act as the required LLM input, but you can use any LLM for this purpose.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
|
|
||||||
Make sure to always get the API key from the provider.
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/basic-chat.png",
|
|
||||||
dark: "img/basic-chat.png",
|
|
||||||
}}
|
|
||||||
|
|
||||||
style={{
|
|
||||||
width: "80%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/Basic_Chat.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`ConversationChain`](https://python.langchain.com/docs/modules/chains/)
|
|
||||||
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
17
docs/docs/examples/create-record.mdx
Normal file
17
docs/docs/examples/create-record.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Create Record
|
||||||
|
|
||||||
|
In Langflow, a `Record` has a structure very similar to a Python dictionary. It is a key-value pair data structure.
|
||||||
|
|
||||||
|
The **Create Record** component allows you to dynamically create a `Record` from a specified number of inputs. You can add as many key-value pairs as you want (as long as it is less than 15 😅). Once you've chosen the number of `Records`, add keys and fill up values, or pass on values from other components to the component using the input handles.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/create_record.mp4" />
|
||||||
|
</div>
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# CSV Loader
|
|
||||||
|
|
||||||
The `VectoStoreAgent` component retrieves information from one or more vector stores. This example shows a `VectoStoreAgent` connected to a CSV file through the `Chroma` vector store. Process description:
|
|
||||||
|
|
||||||
- The `CSVLoader` loads a CSV file into a list of documents.
|
|
||||||
- The extracted data is then processed by the `CharacterTextSplitter`, which splits the text into small, meaningful chunks (usually sentences).
|
|
||||||
- These chunks feed the `Chroma` vector store, which converts them into vectors and stores them for fast indexing.
|
|
||||||
- Finally, the agent accesses the information of the vector store through the `VectorStoreInfo` tool.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
The vector store is used for efficient semantic search, while
|
|
||||||
`VectorStoreInfo` carries information about it, such as its name and
|
|
||||||
description. Embeddings are a way to represent words, phrases, or any entities
|
|
||||||
in a vector space. Learn more about them
|
|
||||||
[here](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
<Admonition type="tip">
|
|
||||||
Once you build this flow, ask questions about the data in the chat interface
|
|
||||||
(e.g., number of rows or columns).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/csv-loader.png",
|
|
||||||
dark: "img/csv-loader.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "80%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/CSV_Loader.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`CSVLoader`](https://python.langchain.com/docs/integrations/document_loaders/csv)
|
|
||||||
- [`CharacterTextSplitter`](https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter)
|
|
||||||
- [`OpenAIEmbedding`](https://python.langchain.com/docs/integrations/text_embedding/openai)
|
|
||||||
- [`Chroma`](https://python.langchain.com/docs/integrations/vectorstores/chroma)
|
|
||||||
- [`VectorStoreInfo`](https://python.langchain.com/docs/modules/data_connection/vectorstores/)
|
|
||||||
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
|
|
||||||
- [`VectorStoreAgent`](https://js.langchain.com/docs/modules/agents/tools/how_to/agents_with_vectorstores)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
---
|
|
||||||
description: Custom Components
|
|
||||||
hide_table_of_contents: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# FlowRunner Component
|
|
||||||
|
|
||||||
The CustomComponent class allows us to create components that interact with Langflow itself. In this example, we will make a component that runs other flows available in "My Collection".
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Document Processor Component"
|
|
||||||
sources={{
|
|
||||||
light: "img/flow_runner.png",
|
|
||||||
dark: "img/flow_runner.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "30%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
We will cover how to:
|
|
||||||
|
|
||||||
- List Collection flows using the _`list_flows`_ method.
|
|
||||||
- Load a flow using the _`load_flow`_ method.
|
|
||||||
- Configure a dropdown input field using the _`options`_ parameter.
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
|
|
||||||
<summary>Example Code</summary>
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
flows = self.list_flows()
|
|
||||||
flow_names = [f.name for f in flows]
|
|
||||||
return {"flow_name": {"options": flow_names,
|
|
||||||
"display_name": "Flow Name",
|
|
||||||
},
|
|
||||||
"document": {"display_name": "Document"}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
# Get the flow that matches the selected name
|
|
||||||
# You can also get the flow by id
|
|
||||||
# using self.get_flow(flow_id=flow_id)
|
|
||||||
tweaks = {}
|
|
||||||
flow = self.get_flow(flow_name=flow_name, tweaks=tweaks)
|
|
||||||
# Get the page_content from the document
|
|
||||||
if document and isinstance(document, list):
|
|
||||||
document = document[0]
|
|
||||||
page_content = document.page_content
|
|
||||||
# Use it in the flow
|
|
||||||
result = flow(page_content)
|
|
||||||
return Document(page_content=str(result))
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<CH.Scrollycoding rows={20} className={""}>
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
|
|
||||||
|
|
||||||
class MyComponent(CustomComponent):
|
|
||||||
display_name = "Custom Component"
|
|
||||||
description = "This is a custom component"
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The typical structure of a Custom Component is composed of _`display_name`_ and _`description`_ attributes, _`build`_ and _`build_config`_ methods.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
|
|
||||||
|
|
||||||
# focus
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
# focus
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
# focus
|
|
||||||
description = "Run other flows"
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's start by defining our component's _`display_name`_ and _`description`_.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
# focus
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Second, we will import _`Document`_ from the [_langchain.schema_](https://docs.langchain.com/docs/components/schema/) module. This will be the return type of the _`build`_ method.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
# focus
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
# focus
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, let's add the [parameters](focus://11[20:55]) and the [return type](focus://11[60:69]) to the _`build`_ method. The parameters added are:
|
|
||||||
|
|
||||||
- _`flow_name`_ is the name of the flow we want to run.
|
|
||||||
- _`document`_ is the input document to be passed to that flow.
|
|
||||||
- Since _`Document`_ is a Langchain type, it will add an input [handle](../administration/components) to the component ([see more](../components/custom)).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python focus=13:14
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
We can now start writing the _`build`_ method. Let's list available flows in "My Collection" using the _`list_flows`_ method.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python focus=15:18
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
# Get the flow that matches the selected name
|
|
||||||
# You can also get the flow by id
|
|
||||||
# using self.get_flow(flow_id=flow_id)
|
|
||||||
tweaks = {}
|
|
||||||
flow = self.get_flow(flow_name=flow_name, tweaks=tweaks)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
And retrieve a flow that matches the selected name (we'll make a dropdown input field for the user to choose among flow names).
|
|
||||||
|
|
||||||
<Admonition type="caution">
|
|
||||||
From version 0.4.0, names are unique, which was not the case in previous
|
|
||||||
versions. This might lead to unexpected results if using flows with the same
|
|
||||||
name.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
# Get the flow that matches the selected name
|
|
||||||
# You can also get the flow by id
|
|
||||||
# using self.get_flow(flow_id=flow_id)
|
|
||||||
tweaks = {}
|
|
||||||
flow = self.get_flow(flow_name=flow_name, tweaks=tweaks)
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
You can load this flow using _`get_flow`_ and set a _`tweaks`_ dictionary to customize it. Find more about tweaks in our [features guidelines](../administration/features#code).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
# Get the flow that matches the selected name
|
|
||||||
# You can also get the flow by id
|
|
||||||
# using self.get_flow(flow_id=flow_id)
|
|
||||||
tweaks = {}
|
|
||||||
flow = self.get_flow(flow_name=flow_name, tweaks=tweaks)
|
|
||||||
# Get the page_content from the document
|
|
||||||
if document and isinstance(document, list):
|
|
||||||
document = document[0]
|
|
||||||
page_content = document.page_content
|
|
||||||
# Use it in the flow
|
|
||||||
result = flow(page_content)
|
|
||||||
return Document(page_content=str(result))
|
|
||||||
```
|
|
||||||
|
|
||||||
We are using a _`Document`_ as input because it is a straightforward way to pass text data in Langflow (specifically because you can connect it to many [loaders](../components/loaders)).
|
|
||||||
Generally, a flow will take a string or a dictionary as input because that's what LangChain components expect.
|
|
||||||
In case you are passing a dictionary, you need to build it according to the needs of the flow you are using.
|
|
||||||
|
|
||||||
The content of a document can be extracted using the _`page_content`_ attribute, which is a string, and passed as an argument to the selected flow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```python focus=9:16
|
|
||||||
from langflow.custom import CustomComponent
|
|
||||||
from langchain.schema import Document
|
|
||||||
|
|
||||||
|
|
||||||
class FlowRunner(CustomComponent):
|
|
||||||
display_name = "Flow Runner"
|
|
||||||
description = "Run other flows using a document as input."
|
|
||||||
|
|
||||||
def build_config(self):
|
|
||||||
flows = self.list_flows()
|
|
||||||
flow_names = [f.name for f in flows]
|
|
||||||
return {"flow_name": {"options": flow_names,
|
|
||||||
"display_name": "Flow Name",
|
|
||||||
},
|
|
||||||
"document": {"display_name": "Document"}
|
|
||||||
}
|
|
||||||
|
|
||||||
def build(self, flow_name: str, document: Document) -> Document:
|
|
||||||
# List the flows
|
|
||||||
flows = self.list_flows()
|
|
||||||
# Get the flow that matches the selected name
|
|
||||||
# You can also get the flow by id
|
|
||||||
# using self.get_flow(flow_id=flow_id)
|
|
||||||
tweaks = {}
|
|
||||||
flow = self.get_flow(flow_name=flow_name, tweaks=tweaks)
|
|
||||||
# Get the page_content from the document
|
|
||||||
if document and isinstance(document, list):
|
|
||||||
document = document[0]
|
|
||||||
page_content = document.page_content
|
|
||||||
# Use it in the flow
|
|
||||||
result = flow(page_content)
|
|
||||||
return Document(page_content=str(result))
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, we can add field customizations through the _`build_config`_ method. Here we added the _`options`_ key to make the _`flow_name`_ field a dropdown menu. Check out the [custom component reference](../components/custom) for a list of available keys.
|
|
||||||
|
|
||||||
<Admonition type="caution">
|
|
||||||
Make sure that the field type is _`str`_ and _`options`_ values are strings.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
</CH.Scrollycoding>
|
|
||||||
|
|
||||||
Done! This is what our script and custom component looks like:
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Document Processor Code"
|
|
||||||
sources={{
|
|
||||||
light: "img/flow_runner_code.png",
|
|
||||||
dark: "img/flow_runner_code.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
maxWidth: "100%",
|
|
||||||
margin: "0 auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Document Processor Component"
|
|
||||||
sources={{
|
|
||||||
light: "img/flow_runner.png",
|
|
||||||
dark: "img/flow_runner.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
margin: "0 auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
17
docs/docs/examples/pass.mdx
Normal file
17
docs/docs/examples/pass.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Pass
|
||||||
|
|
||||||
|
Sometimes all you need to do is… nothing!
|
||||||
|
|
||||||
|
The **Pass** component enables you to ignore one input and move forward with another one. This is super helpful to swap routes for A/B testing!
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/pass.mp4" />
|
||||||
|
</div>
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# Python Function
|
|
||||||
|
|
||||||
Langflow allows you to create a customized tool using the `PythonFunction` connected to a `Tool` component. In this example, Regex is used in Python to validate a pattern.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import re
|
|
||||||
|
|
||||||
def is_brazilian_zipcode(zipcode: str) -> bool:
|
|
||||||
pattern = r"\d{5}-?\d{3}"
|
|
||||||
|
|
||||||
# Check if the zip code matches the pattern
|
|
||||||
if re.match(pattern, zipcode):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
<Admonition type="tip">
|
|
||||||
When a tool is called, it is often desirable to have its output returned
|
|
||||||
directly to the user. You can do this by setting the **return_direct** flag
|
|
||||||
for a tool to be True.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
The `AgentInitializer` component is a quick way to construct an agent from the model and tools.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
The `PythonFunction` is a custom component that uses the LangChain 🦜🔗 tool
|
|
||||||
decorator. Learn more about it
|
|
||||||
[here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/python-function.png",
|
|
||||||
dark: "img/python-function.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "80%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/Python_Function.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`PythonFunctionTool`](https://python.langchain.com/docs/modules/agents/tools/custom_tools)
|
|
||||||
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
|
|
||||||
- [`AgentInitializer`](https://python.langchain.com/docs/modules/agents/)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# SearchApi Tool
|
|
||||||
|
|
||||||
The [SearchApi](https://www.searchapi.io/) allows developers to retrieve results from search engines such as Google, Google Scholar, YouTube, YouTube transcripts, and more, and can be used as in Langflow through the `SearchApi` tool.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
To use the SearchApi, you must first obtain an API key by registering at [SearchApi's website](https://www.searchapi.io/).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
In the given example, we specify `engine` as `youtube_transcripts` and provide a `video_id`.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
All engines and parameters can be found in [SearchApi documentation](https://www.searchapi.io/docs/google).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
The `RetrievalQA` chain processes a `Document` along with a user's question to return an answer.
|
|
||||||
|
|
||||||
<Admonition type="tip">
|
|
||||||
In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the
|
|
||||||
LLM, but feel free to experiment with other Language Models!
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
The `RetrievalQA` takes `CombineDocsChain` and `SearchApi` tool as inputs, using the tool as a `Document` to answer questions.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
Learn more about the SearchApi
|
|
||||||
[here](https://python.langchain.com/docs/integrations/tools/searchapi).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/searchapi-tool.png",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/SearchApi_Tool.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
|
|
||||||
- [`SearchApiAPIWrapper`](https://python.langchain.com/docs/integrations/providers/searchapi#wrappers)
|
|
||||||
- [`ZeroShotAgent`](https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import Admonition from "@theme/Admonition";
|
|
||||||
|
|
||||||
# Serp API Tool
|
|
||||||
|
|
||||||
The [Serp API](https://serpapi.com/) (Search Engine Results Page) allows developers to scrape results from search engines such as Google, Bing and Yahoo, and can be used as in Langflow through the `Search` component.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
To use the Serp API, you first need to sign up [Serp
|
|
||||||
API](https://serpapi.com/) for an API key on the provider's website.
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
Here, the `ZeroShotPrompt` component specifies a prompt template for the `ZeroShotAgent`. Set a _Prefix_ and _Suffix_ with rules for the agent to obey. In the example, we used default templates.
|
|
||||||
|
|
||||||
The `LLMChain` is a simple chain that takes in a prompt template, formats it with the user input, and returns the response from an LLM.
|
|
||||||
|
|
||||||
<Admonition type="tip">
|
|
||||||
In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the
|
|
||||||
LLM, but feel free to experiment with other Language Models!
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
The `ZeroShotAgent` takes the `LLMChain` and the `Search` tool as inputs, using the tool to find information when necessary.
|
|
||||||
|
|
||||||
<Admonition type="info">
|
|
||||||
Learn more about the Serp API
|
|
||||||
[here](https://python.langchain.com/docs/integrations/providers/serpapi ).
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
## ⛓️ Langflow Example
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
|
||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
|
||||||
|
|
||||||
<ZoomableImage
|
|
||||||
alt="Docusaurus themed image"
|
|
||||||
sources={{
|
|
||||||
light: "img/serp-api-tool.png",
|
|
||||||
dark: "img/serp-api-tool.png",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "80%",
|
|
||||||
margin: "20px auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
#### <a target="\_blank" href="json_files/SerpAPI_Tool.json" download>Download Flow</a>
|
|
||||||
|
|
||||||
<Admonition type="note" title="LangChain Components 🦜🔗">
|
|
||||||
|
|
||||||
- [`ZeroShotPrompt`](https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/)
|
|
||||||
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
|
|
||||||
- [`LLMChain`](https://python.langchain.com/docs/modules/chains/foundational/llm_chain)
|
|
||||||
- [`Search`](https://python.langchain.com/docs/integrations/providers/serpapi)
|
|
||||||
- [`ZeroShotAgent`](https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent)
|
|
||||||
|
|
||||||
</Admonition>
|
|
||||||
17
docs/docs/examples/store-message.mdx
Normal file
17
docs/docs/examples/store-message.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Store Message
|
||||||
|
|
||||||
|
The **Store Message** component allows you to save information under a specified Session ID and sender type.
|
||||||
|
|
||||||
|
The **Message History** component can then be used to retrieve stored messages.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/store_message.mp4" />
|
||||||
|
</div>
|
||||||
15
docs/docs/examples/sub-flow.mdx
Normal file
15
docs/docs/examples/sub-flow.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Sub Flow
|
||||||
|
|
||||||
|
The **Sub Flow** component enables a user to select a previously built flow and dynamically generate a component out of it.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/sub_flow.mp4" />
|
||||||
|
</div>
|
||||||
15
docs/docs/examples/text-operator.mdx
Normal file
15
docs/docs/examples/text-operator.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
# Text Operator
|
||||||
|
|
||||||
|
The **Text Operator** component simplifies logic. It evaluates the results from another component (for example, if the input text exactly equals `Tuna`) and runs another component based on the results. Basically, the text operator is an if/else component for your flow.
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ReactPlayer playing controls url="/videos/text_operator.mp4" />
|
||||||
|
</div>
|
||||||
|
|
@ -9,7 +9,7 @@ import Admonition from "@theme/Admonition";
|
||||||
Langflow v1.0 alpha is also available in HuggingFace Spaces. [Clone the space using this link](https://huggingface.co/spaces/Langflow/Langflow-Preview?duplicate=true), to create your own Langflow workspace in minutes.
|
Langflow v1.0 alpha is also available in HuggingFace Spaces. [Clone the space using this link](https://huggingface.co/spaces/Langflow/Langflow-Preview?duplicate=true), to create your own Langflow workspace in minutes.
|
||||||
</Admonition>
|
</Admonition>
|
||||||
|
|
||||||
Langflow requires [Python 3.10](https://www.python.org/downloads/release/python-3100/) and [pip](https://pypi.org/project/pip/) or [pipx](https://pipx.pypa.io/stable/installation/) to be installed on your system.
|
Langflow requires [Python >=3.10](https://www.python.org/downloads/release/python-3100/) and [pip](https://pypi.org/project/pip/) or [pipx](https://pipx.pypa.io/stable/installation/) to be installed on your system.
|
||||||
|
|
||||||
Install Langflow with pip:
|
Install Langflow with pip:
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ The Notion integration in Langflow enables seamless connectivity with Notion dat
|
||||||
<ZoomableImage
|
<ZoomableImage
|
||||||
alt="Notion Components in Langflow"
|
alt="Notion Components in Langflow"
|
||||||
sources={{
|
sources={{
|
||||||
light: "img/notion/notion_components_bundle.png",
|
light: "img/notion/notion_bundle.jpg",
|
||||||
dark: "img/notion/notion_components_bundle_dark.png",
|
dark: "img/notion/notion_bundle.jpg",
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%", margin: "20px 0" }}
|
style={{ width: "100%", margin: "20px 0" }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,13 @@ module.exports = {
|
||||||
label: "Example Components",
|
label: "Example Components",
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
"examples/flow-runner",
|
"examples/chat-memory",
|
||||||
"examples/conversation-chain",
|
"examples/combine-text",
|
||||||
"examples/buffer-memory",
|
"examples/create-record",
|
||||||
"examples/csv-loader",
|
"examples/pass",
|
||||||
"examples/searchapi-tool",
|
"examples/store-message",
|
||||||
"examples/serp-api-tool",
|
"examples/sub-flow",
|
||||||
"examples/python-function",
|
"examples/text-operator",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
BIN
docs/static/img/langflow_basic_howto.gif
vendored
Normal file
BIN
docs/static/img/langflow_basic_howto.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 MiB |
BIN
docs/static/img/notion/notion_bundle.jpg
vendored
Normal file
BIN
docs/static/img/notion/notion_bundle.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/static/videos/chat_memory.mp4
vendored
Normal file
BIN
docs/static/videos/chat_memory.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/combine_text.mp4
vendored
Normal file
BIN
docs/static/videos/combine_text.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/create_record.mp4
vendored
Normal file
BIN
docs/static/videos/create_record.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/pass.mp4
vendored
Normal file
BIN
docs/static/videos/pass.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/store_message.mp4
vendored
Normal file
BIN
docs/static/videos/store_message.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/sub_flow.mp4
vendored
Normal file
BIN
docs/static/videos/sub_flow.mp4
vendored
Normal file
Binary file not shown.
BIN
docs/static/videos/text_operator.mp4
vendored
Normal file
BIN
docs/static/videos/text_operator.mp4
vendored
Normal file
Binary file not shown.
42
poetry.lock
generated
42
poetry.lock
generated
|
|
@ -4308,13 +4308,13 @@ extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchainhub"
|
name = "langchainhub"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
description = "The LangChain Hub API client"
|
description = "The LangChain Hub API client"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.8.1"
|
python-versions = "<4.0,>=3.8.1"
|
||||||
files = [
|
files = [
|
||||||
{file = "langchainhub-0.1.16-py3-none-any.whl", hash = "sha256:a4379a1879cc6b441b8d02cc65e28a54f160fba61c9d1d4b0eddc3a276dff99a"},
|
{file = "langchainhub-0.1.17-py3-none-any.whl", hash = "sha256:4c609b3948252c71670f0d98f73413b515cfd2f6701a7b40ce959203e6133e04"},
|
||||||
{file = "langchainhub-0.1.16.tar.gz", hash = "sha256:9f11e68fddb575e70ef4b28800eedbd9eeb180ba508def04f7153ea5b246b6fc"},
|
{file = "langchainhub-0.1.17.tar.gz", hash = "sha256:af7df0cb1cebc7a6e0864e8632ae48ecad39ed96568f699c78657b9d04e50b46"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -4323,7 +4323,7 @@ types-requests = ">=2.31.0.2,<3.0.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langflow-base"
|
name = "langflow-base"
|
||||||
version = "0.0.52"
|
version = "0.0.53"
|
||||||
description = "A Python package with a built-in web application"
|
description = "A Python package with a built-in web application"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
|
|
@ -4365,6 +4365,7 @@ python-socketio = "^5.11.0"
|
||||||
rich = "^13.7.0"
|
rich = "^13.7.0"
|
||||||
sqlmodel = "^0.0.18"
|
sqlmodel = "^0.0.18"
|
||||||
typer = "^0.12.0"
|
typer = "^0.12.0"
|
||||||
|
uncurl = "^0.0.11"
|
||||||
uvicorn = "^0.29.0"
|
uvicorn = "^0.29.0"
|
||||||
websockets = "*"
|
websockets = "*"
|
||||||
|
|
||||||
|
|
@ -4419,13 +4420,13 @@ requests = ">=2,<3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litellm"
|
name = "litellm"
|
||||||
version = "1.38.12"
|
version = "1.39.2"
|
||||||
description = "Library to easily interface with LLM API providers"
|
description = "Library to easily interface with LLM API providers"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "litellm-1.38.12-py3-none-any.whl", hash = "sha256:bd473eb9fe17bc312e1e8c1335fde87c67022f9af9e9571947f359e1e908a925"},
|
{file = "litellm-1.39.2-py3-none-any.whl", hash = "sha256:843cb9a4d45c89ba6da95529815ec83ee7e4b7fe07aa0ed633102f600fddd9ad"},
|
||||||
{file = "litellm-1.38.12.tar.gz", hash = "sha256:815fadee177413a98061210bf9cfece4eae1e9bfb2aadc3f2f8f9d620b1ffb6b"},
|
{file = "litellm-1.39.2.tar.gz", hash = "sha256:96c4f3d522ccf32817357b1e9f5f63fa36a4a884f336314e1f6d66c0576d689e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -7554,13 +7555,13 @@ langchain = ["langchain (>=0.0.321)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "realtime"
|
name = "realtime"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
description = ""
|
description = ""
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.8"
|
python-versions = "<4.0,>=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "realtime-1.0.4-py3-none-any.whl", hash = "sha256:b06bea001985f089167320bda1e91c6b2d866f56ca810bb8d768ee3cf695ee21"},
|
{file = "realtime-1.0.5-py3-none-any.whl", hash = "sha256:93342fbcb8812ed8d81733f2782c1199376f0471e78014675420c7d31f2f327d"},
|
||||||
{file = "realtime-1.0.4.tar.gz", hash = "sha256:a9095f60121a365e84656c582e6ccd8dc8b3a732ddddb2ccd26cc3d32b77bdf6"},
|
{file = "realtime-1.0.5.tar.gz", hash = "sha256:4abbb3218b6ce8bd8d9d3b1112661d325e36ceab67a0e918673d0fd8fca04fb1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -7676,13 +7677,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.2"
|
version = "2.32.3"
|
||||||
description = "Python HTTP for Humans."
|
description = "Python HTTP for Humans."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
|
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||||
{file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
|
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -9158,6 +9159,21 @@ files = [
|
||||||
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
|
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uncurl"
|
||||||
|
version = "0.0.11"
|
||||||
|
description = "A library to convert curl requests to python-requests."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "uncurl-0.0.11-py3-none-any.whl", hash = "sha256:5961e93f07a5c9f2ef8ae4245bd92b0a6ce503c851de980f5b70080ae74cdc59"},
|
||||||
|
{file = "uncurl-0.0.11.tar.gz", hash = "sha256:530c9bbd4d118f4cde6194165ff484cc25b0661cd256f19e9d5fcb53fc077790"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyperclip = "*"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uritemplate"
|
name = "uritemplate"
|
||||||
version = "4.1.1"
|
version = "4.1.1"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "langflow"
|
name = "langflow"
|
||||||
version = "1.0.0a41"
|
version = "1.0.0a42"
|
||||||
description = "A Python package with a built-in web application"
|
description = "A Python package with a built-in web application"
|
||||||
authors = ["Langflow <contact@langflow.org>"]
|
authors = ["Langflow <contact@langflow.org>"]
|
||||||
maintainers = [
|
maintainers = [
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
from sqlalchemy.engine.reflection import Inspector
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
from langflow.utils import migration
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|
@ -22,13 +23,9 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
inspector = Inspector.from_engine(conn) # type: ignore
|
|
||||||
table_names = inspector.get_table_names()
|
|
||||||
${upgrades if upgrades else "pass"}
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
inspector = Inspector.from_engine(conn) # type: ignore
|
|
||||||
table_names = inspector.get_table_names()
|
|
||||||
${downgrades if downgrades else "pass"}
|
${downgrades if downgrades else "pass"}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""Add unique constraints per user in folder table
|
||||||
|
|
||||||
|
Revision ID: 1c79524817ed
|
||||||
|
Revises: 3bb0ddf32dfb
|
||||||
|
Create Date: 2024-05-29 23:12:09.146880
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "1c79524817ed"
|
||||||
|
down_revision: Union[str, None] = "3bb0ddf32dfb"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")]
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("folder", schema=None) as batch_op:
|
||||||
|
if "unique_folder_name" not in constraints_names:
|
||||||
|
batch_op.create_unique_constraint("unique_folder_name", ["user_id", "name"])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")]
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("folder", schema=None) as batch_op:
|
||||||
|
if "unique_folder_name" in constraints_names:
|
||||||
|
batch_op.drop_constraint("unique_folder_name", type_="unique")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Add unique constraints per user in flow table
|
||||||
|
|
||||||
|
Revision ID: 3bb0ddf32dfb
|
||||||
|
Revises: a72f5cf9c2f9
|
||||||
|
Create Date: 2024-05-29 23:08:43.935040
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "3bb0ddf32dfb"
|
||||||
|
down_revision: Union[str, None] = "a72f5cf9c2f9"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
indexes_names = [index["name"] for index in inspector.get_indexes("flow")]
|
||||||
|
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")]
|
||||||
|
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||||
|
if "ix_flow_endpoint_name" in indexes_names:
|
||||||
|
batch_op.drop_index("ix_flow_endpoint_name")
|
||||||
|
batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=False)
|
||||||
|
if "unique_flow_endpoint_name" not in constraints_names:
|
||||||
|
batch_op.create_unique_constraint("unique_flow_endpoint_name", ["user_id", "endpoint_name"])
|
||||||
|
if "unique_flow_name" not in constraints_names:
|
||||||
|
batch_op.create_unique_constraint("unique_flow_name", ["user_id", "name"])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
indexes_names = [index["name"] for index in inspector.get_indexes("flow")]
|
||||||
|
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")]
|
||||||
|
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||||
|
if "unique_flow_name" in constraints_names:
|
||||||
|
batch_op.drop_constraint("unique_flow_name", type_="unique")
|
||||||
|
if "unique_flow_endpoint_name" in constraints_names:
|
||||||
|
batch_op.drop_constraint("unique_flow_endpoint_name", type_="unique")
|
||||||
|
if "ix_flow_endpoint_name" in indexes_names:
|
||||||
|
batch_op.drop_index(batch_op.f("ix_flow_endpoint_name"))
|
||||||
|
batch_op.create_index("ix_flow_endpoint_name", ["endpoint_name"], unique=1)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Add endpoint name col
|
||||||
|
|
||||||
|
Revision ID: a72f5cf9c2f9
|
||||||
|
Revises: 29fe8f1f806b
|
||||||
|
Create Date: 2024-05-29 21:44:04.240816
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "a72f5cf9c2f9"
|
||||||
|
down_revision: Union[str, None] = "29fe8f1f806b"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||||
|
indexes = inspector.get_indexes("flow")
|
||||||
|
index_names = [index["name"] for index in indexes]
|
||||||
|
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||||
|
if "endpoint_name" not in column_names:
|
||||||
|
batch_op.add_column(sa.Column("endpoint_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||||
|
if "ix_flow_endpoint_name" not in index_names:
|
||||||
|
batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=True)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn) # type: ignore
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||||
|
indexes = inspector.get_indexes("flow")
|
||||||
|
index_names = [index["name"] for index in indexes]
|
||||||
|
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||||
|
if "ix_flow_endpoint_name" in index_names:
|
||||||
|
batch_op.drop_index(batch_op.f("ix_flow_endpoint_name"))
|
||||||
|
if "endpoint_name" in column_names:
|
||||||
|
batch_op.drop_column("endpoint_name")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -53,10 +53,10 @@ def get_all(
|
||||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
@router.post("/run/{flow_id_or_name}", response_model=RunResponse, response_model_exclude_none=True)
|
||||||
async def simplified_run_flow(
|
async def simplified_run_flow(
|
||||||
db: Annotated[Session, Depends(get_session)],
|
db: Annotated[Session, Depends(get_session)],
|
||||||
flow_id: UUID,
|
flow_id_or_name: str,
|
||||||
input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(),
|
input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(),
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
api_key_user: User = Depends(api_key_security),
|
api_key_user: User = Depends(api_key_security),
|
||||||
|
|
@ -111,8 +111,21 @@ async def simplified_run_flow(
|
||||||
This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching.
|
This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching.
|
||||||
"""
|
"""
|
||||||
session_id = input_request.session_id
|
session_id = input_request.session_id
|
||||||
|
endpoint_name = None
|
||||||
|
flow_id_str = None
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
flow_id = UUID(flow_id_or_name)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
endpoint_name = flow_id_or_name
|
||||||
|
flow = db.exec(
|
||||||
|
select(Flow).where(Flow.endpoint_name == endpoint_name).where(Flow.user_id == api_key_user.id)
|
||||||
|
).first()
|
||||||
|
if flow is None:
|
||||||
|
raise ValueError(f"Flow with endpoint name {endpoint_name} not found")
|
||||||
|
flow_id = flow.id
|
||||||
|
|
||||||
flow_id_str = str(flow_id)
|
flow_id_str = str(flow_id)
|
||||||
artifacts = {}
|
artifacts = {}
|
||||||
if input_request.session_id:
|
if input_request.session_id:
|
||||||
|
|
@ -172,10 +185,13 @@ async def simplified_run_flow(
|
||||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
if f"Flow {flow_id_str} not found" in str(exc):
|
if flow_id_str and f"Flow {flow_id_str} not found" in str(exc):
|
||||||
logger.error(f"Flow {flow_id_str} not found")
|
logger.error(f"Flow {flow_id_str} not found")
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
elif f"Session {session_id} not found" in str(exc):
|
elif endpoint_name and f"Flow with endpoint name {endpoint_name} not found" in str(exc):
|
||||||
|
logger.error(f"Flow with endpoint name {endpoint_name} not found")
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
|
elif session_id and f"Session {session_id} not found" in str(exc):
|
||||||
logger.error(f"Session {session_id} not found")
|
logger.error(f"Session {session_id} not found")
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,22 @@ def read_flows(
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||||
|
remove_example_flows: bool = False,
|
||||||
):
|
):
|
||||||
"""Read all flows."""
|
"""
|
||||||
|
Retrieve a list of flows.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_user (User): The current authenticated user.
|
||||||
|
session (Session): The database session.
|
||||||
|
settings_service (SettingsService): The settings service.
|
||||||
|
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
|
||||||
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: A list of flows in JSON format.
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
auth_settings = settings_service.auth_settings
|
auth_settings = settings_service.auth_settings
|
||||||
if auth_settings.AUTO_LOGIN:
|
if auth_settings.AUTO_LOGIN:
|
||||||
|
|
@ -73,15 +87,16 @@ def read_flows(
|
||||||
flows = validate_is_component(flows) # type: ignore
|
flows = validate_is_component(flows) # type: ignore
|
||||||
flow_ids = [flow.id for flow in flows]
|
flow_ids = [flow.id for flow in flows]
|
||||||
# with the session get the flows that DO NOT have a user_id
|
# with the session get the flows that DO NOT have a user_id
|
||||||
try:
|
if not remove_example_flows:
|
||||||
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
try:
|
||||||
|
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||||
|
|
||||||
example_flows = folder.flows if folder else []
|
example_flows = folder.flows if folder else []
|
||||||
for example_flow in example_flows:
|
for example_flow in example_flows:
|
||||||
if example_flow.id not in flow_ids:
|
if example_flow.id not in flow_ids:
|
||||||
flows.append(example_flow) # type: ignore
|
flows.append(example_flow) # type: ignore
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
return [jsonable_encoder(flow) for flow in flows]
|
return [jsonable_encoder(flow) for flow in flows]
|
||||||
|
|
@ -120,30 +135,49 @@ def update_flow(
|
||||||
settings_service=Depends(get_settings_service),
|
settings_service=Depends(get_settings_service),
|
||||||
):
|
):
|
||||||
"""Update a flow."""
|
"""Update a flow."""
|
||||||
|
try:
|
||||||
|
db_flow = read_flow(
|
||||||
|
session=session,
|
||||||
|
flow_id=flow_id,
|
||||||
|
current_user=current_user,
|
||||||
|
settings_service=settings_service,
|
||||||
|
)
|
||||||
|
if not db_flow:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
flow_data = flow.model_dump(exclude_unset=True)
|
||||||
|
if settings_service.settings.remove_api_keys:
|
||||||
|
flow_data = remove_api_keys(flow_data)
|
||||||
|
for key, value in flow_data.items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(db_flow, key, value)
|
||||||
|
db_flow.updated_at = datetime.now(timezone.utc)
|
||||||
|
if db_flow.folder_id is None:
|
||||||
|
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
|
||||||
|
if default_folder:
|
||||||
|
db_flow.folder_id = default_folder.id
|
||||||
|
session.add(db_flow)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
except Exception as e:
|
||||||
|
# If it is a validation error, return the error message
|
||||||
|
if hasattr(e, "errors"):
|
||||||
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
|
elif "UNIQUE constraint failed" in str(e):
|
||||||
|
# Get the name of the column that failed
|
||||||
|
columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0]
|
||||||
|
# UNIQUE constraint failed: flow.user_id, flow.name
|
||||||
|
# or UNIQUE constraint failed: flow.name
|
||||||
|
# if the column has id in it, we want the other column
|
||||||
|
column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0]
|
||||||
|
|
||||||
db_flow = read_flow(
|
raise HTTPException(
|
||||||
session=session,
|
status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique"
|
||||||
flow_id=flow_id,
|
) from e
|
||||||
current_user=current_user,
|
elif isinstance(e, HTTPException):
|
||||||
settings_service=settings_service,
|
raise e
|
||||||
)
|
else:
|
||||||
if not db_flow:
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
raise HTTPException(status_code=404, detail="Flow not found")
|
|
||||||
flow_data = flow.model_dump(exclude_unset=True)
|
|
||||||
if settings_service.settings.remove_api_keys:
|
|
||||||
flow_data = remove_api_keys(flow_data)
|
|
||||||
for key, value in flow_data.items():
|
|
||||||
if value is not None:
|
|
||||||
setattr(db_flow, key, value)
|
|
||||||
db_flow.updated_at = datetime.now(timezone.utc)
|
|
||||||
if db_flow.folder_id is None:
|
|
||||||
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
|
|
||||||
if default_folder:
|
|
||||||
db_flow.folder_id = default_folder.id
|
|
||||||
session.add(db_flow)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(db_flow)
|
|
||||||
return db_flow
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{flow_id}", status_code=200)
|
@router.delete("/{flow_id}", status_code=200)
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,7 @@ async def login_to_get_access_token(
|
||||||
async def auto_login(
|
async def auto_login(
|
||||||
response: Response,
|
response: Response,
|
||||||
db: Session = Depends(get_session),
|
db: Session = Depends(get_session),
|
||||||
settings_service=Depends(get_settings_service),
|
settings_service=Depends(get_settings_service)
|
||||||
variable_service: VariableService = Depends(get_variable_service),
|
|
||||||
):
|
):
|
||||||
auth_settings = settings_service.auth_settings
|
auth_settings = settings_service.auth_settings
|
||||||
if settings_service.auth_settings.AUTO_LOGIN:
|
if settings_service.auth_settings.AUTO_LOGIN:
|
||||||
|
|
@ -88,8 +87,7 @@ async def auto_login(
|
||||||
expires=None, # Set to None to make it a session cookie
|
expires=None, # Set to None to make it a session cookie
|
||||||
domain=auth_settings.COOKIE_DOMAIN,
|
domain=auth_settings.COOKIE_DOMAIN,
|
||||||
)
|
)
|
||||||
variable_service.initialize_user_variables(user_id, db)
|
|
||||||
create_default_folder_if_it_doesnt_exist(db, user_id)
|
|
||||||
return tokens
|
return tokens
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
0
src/backend/base/langflow/base/curl/__init__.py
Normal file
0
src/backend/base/langflow/base/curl/__init__.py
Normal file
89
src/backend/base/langflow/base/curl/parse.py
Normal file
89
src/backend/base/langflow/base/curl/parse.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
This file contains a fix for the implementation of the `uncurl` library, which is available at https://github.com/spulec/uncurl.git.
|
||||||
|
|
||||||
|
The `uncurl` library provides a way to parse and convert cURL commands into Python requests. However, there are some issues with the original implementation that this file aims to fix.
|
||||||
|
|
||||||
|
The `parse_context` function in this file takes a cURL command as input and returns a `ParsedContext` object, which contains the parsed information from the cURL command, such as the HTTP method, URL, headers, cookies, etc.
|
||||||
|
|
||||||
|
The `normalize_newlines` function is a helper function that replaces the line continuation character ("\") followed by a newline with a space.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
from collections import OrderedDict, namedtuple
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
|
||||||
|
from uncurl.api import parser # type: ignore
|
||||||
|
|
||||||
|
parser.add_argument("-x", "--proxy", default={})
|
||||||
|
parser.add_argument("-U", "--proxy-user", default="")
|
||||||
|
|
||||||
|
ParsedContext = namedtuple("ParsedContext", ["method", "url", "data", "headers", "cookies", "verify", "auth", "proxy"])
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_newlines(multiline_text):
|
||||||
|
return multiline_text.replace(" \\\n", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_context(curl_command):
|
||||||
|
method = "get"
|
||||||
|
|
||||||
|
tokens = shlex.split(normalize_newlines(curl_command))
|
||||||
|
tokens = [token for token in tokens if token and token != " "]
|
||||||
|
parsed_args = parser.parse_args(tokens)
|
||||||
|
|
||||||
|
post_data = parsed_args.data or parsed_args.data_binary
|
||||||
|
if post_data:
|
||||||
|
method = "post"
|
||||||
|
|
||||||
|
if parsed_args.X:
|
||||||
|
method = parsed_args.X.lower()
|
||||||
|
|
||||||
|
cookie_dict = OrderedDict()
|
||||||
|
quoted_headers = OrderedDict()
|
||||||
|
|
||||||
|
for curl_header in parsed_args.header:
|
||||||
|
if curl_header.startswith(":"):
|
||||||
|
occurrence = [m.start() for m in re.finditer(":", curl_header)]
|
||||||
|
header_key, header_value = curl_header[: occurrence[1]], curl_header[occurrence[1] + 1 :]
|
||||||
|
else:
|
||||||
|
header_key, header_value = curl_header.split(":", 1)
|
||||||
|
|
||||||
|
if header_key.lower().strip("$") == "cookie":
|
||||||
|
cookie = SimpleCookie(bytes(header_value, "ascii").decode("unicode-escape"))
|
||||||
|
for key in cookie:
|
||||||
|
cookie_dict[key] = cookie[key].value
|
||||||
|
else:
|
||||||
|
quoted_headers[header_key] = header_value.strip()
|
||||||
|
|
||||||
|
# add auth
|
||||||
|
user = parsed_args.user
|
||||||
|
if parsed_args.user:
|
||||||
|
user = tuple(user.split(":"))
|
||||||
|
|
||||||
|
# add proxy and its authentication if it's available.
|
||||||
|
proxies = parsed_args.proxy
|
||||||
|
# proxy_auth = parsed_args.proxy_user
|
||||||
|
if parsed_args.proxy and parsed_args.proxy_user:
|
||||||
|
proxies = {
|
||||||
|
"http": "http://{}@{}/".format(parsed_args.proxy_user, parsed_args.proxy),
|
||||||
|
"https": "http://{}@{}/".format(parsed_args.proxy_user, parsed_args.proxy),
|
||||||
|
}
|
||||||
|
elif parsed_args.proxy:
|
||||||
|
proxies = {
|
||||||
|
"http": "http://{}/".format(parsed_args.proxy),
|
||||||
|
"https": "http://{}/".format(parsed_args.proxy),
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedContext(
|
||||||
|
method=method,
|
||||||
|
url=parsed_args.url,
|
||||||
|
data=post_data,
|
||||||
|
headers=quoted_headers,
|
||||||
|
cookies=cookie_dict,
|
||||||
|
verify=parsed_args.insecure,
|
||||||
|
auth=user,
|
||||||
|
proxy=proxies,
|
||||||
|
)
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from langflow.base.curl.parse import parse_context
|
||||||
from langflow.custom import CustomComponent
|
from langflow.custom import CustomComponent
|
||||||
|
from langflow.field_typing import NestedDict
|
||||||
from langflow.schema import Record
|
from langflow.schema import Record
|
||||||
|
from langflow.schema.dotdict import dotdict
|
||||||
|
|
||||||
|
|
||||||
class APIRequest(CustomComponent):
|
class APIRequest(CustomComponent):
|
||||||
|
|
@ -17,10 +21,15 @@ class APIRequest(CustomComponent):
|
||||||
|
|
||||||
field_config = {
|
field_config = {
|
||||||
"urls": {"display_name": "URLs", "info": "URLs to make requests to."},
|
"urls": {"display_name": "URLs", "info": "URLs to make requests to."},
|
||||||
|
"curl": {
|
||||||
|
"display_name": "Curl",
|
||||||
|
"info": "Paste a curl command to populate the fields.",
|
||||||
|
"refresh_button": True,
|
||||||
|
"refresh_button_text": "",
|
||||||
|
},
|
||||||
"method": {
|
"method": {
|
||||||
"display_name": "Method",
|
"display_name": "Method",
|
||||||
"info": "The HTTP method to use.",
|
"info": "The HTTP method to use.",
|
||||||
"field_type": "str",
|
|
||||||
"options": ["GET", "POST", "PATCH", "PUT"],
|
"options": ["GET", "POST", "PATCH", "PUT"],
|
||||||
"value": "GET",
|
"value": "GET",
|
||||||
},
|
},
|
||||||
|
|
@ -36,12 +45,33 @@ class APIRequest(CustomComponent):
|
||||||
},
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"display_name": "Timeout",
|
"display_name": "Timeout",
|
||||||
"field_type": "int",
|
|
||||||
"info": "The timeout to use for the request.",
|
"info": "The timeout to use for the request.",
|
||||||
"value": 5,
|
"value": 5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:
|
||||||
|
try:
|
||||||
|
parsed = parse_context(curl)
|
||||||
|
build_config["urls"]["value"] = [parsed.url]
|
||||||
|
build_config["method"]["value"] = parsed.method.upper()
|
||||||
|
build_config["headers"]["value"] = dict(parsed.headers)
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_data = json.loads(parsed.data)
|
||||||
|
build_config["body"]["value"] = json_data
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(e)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Error parsing curl: {exc}")
|
||||||
|
raise ValueError(f"Error parsing curl: {exc}")
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||||
|
if field_name == "curl" and field_value is not None:
|
||||||
|
build_config = self.parse_curl(field_value, build_config)
|
||||||
|
return build_config
|
||||||
|
|
||||||
async def make_request(
|
async def make_request(
|
||||||
self,
|
self,
|
||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
|
|
@ -94,21 +124,25 @@ class APIRequest(CustomComponent):
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
urls: List[str],
|
urls: List[str],
|
||||||
headers: Optional[Record] = None,
|
curl: Optional[str] = None,
|
||||||
body: Optional[Record] = None,
|
headers: Optional[NestedDict] = {},
|
||||||
|
body: Optional[NestedDict] = {},
|
||||||
timeout: int = 5,
|
timeout: int = 5,
|
||||||
) -> List[Record]:
|
) -> List[Record]:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers_dict = {}
|
headers_dict = {}
|
||||||
else:
|
elif isinstance(headers, Record):
|
||||||
headers_dict = headers.data
|
headers_dict = headers.data
|
||||||
|
else:
|
||||||
|
headers_dict = headers
|
||||||
|
|
||||||
bodies = []
|
bodies = []
|
||||||
if body:
|
if body:
|
||||||
if isinstance(body, list):
|
if not isinstance(body, list):
|
||||||
bodies = [b.data for b in body]
|
bodies = [body]
|
||||||
else:
|
else:
|
||||||
bodies = [body.data]
|
bodies = body
|
||||||
|
bodies = [b.data if isinstance(b, Record) else b for b in bodies] # type: ignore
|
||||||
|
|
||||||
if len(urls) != len(bodies):
|
if len(urls) != len(bodies):
|
||||||
# add bodies with None
|
# add bodies with None
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
from typing import Any, Dict, List, Optional, Union
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from langchain_community.chat_models.ollama import ChatOllama
|
|
||||||
from langflow.base.constants import STREAM_INFO_TEXT
|
|
||||||
from langflow.base.models.model import LCModelComponent
|
|
||||||
from langchain_core.caches import BaseCache
|
|
||||||
|
|
||||||
from langflow.field_typing import Text
|
|
||||||
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from langchain_community.chat_models.ollama import ChatOllama
|
||||||
|
|
||||||
|
from langflow.base.constants import STREAM_INFO_TEXT
|
||||||
|
from langflow.base.models.model import LCModelComponent
|
||||||
|
from langflow.field_typing import Text
|
||||||
|
|
||||||
|
|
||||||
class ChatOllamaComponent(LCModelComponent):
|
class ChatOllamaComponent(LCModelComponent):
|
||||||
|
|
@ -26,18 +16,12 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
field_order = [
|
field_order = [
|
||||||
"base_url",
|
"base_url",
|
||||||
"headers",
|
"headers",
|
||||||
|
|
||||||
"keep_alive_flag",
|
"keep_alive_flag",
|
||||||
"keep_alive",
|
"keep_alive",
|
||||||
|
|
||||||
"metadata",
|
"metadata",
|
||||||
"model",
|
"model",
|
||||||
|
|
||||||
|
|
||||||
"temperature",
|
"temperature",
|
||||||
"cache",
|
"cache",
|
||||||
|
|
||||||
|
|
||||||
"format",
|
"format",
|
||||||
"metadata",
|
"metadata",
|
||||||
"mirostat",
|
"mirostat",
|
||||||
|
|
@ -67,10 +51,7 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
"base_url": {
|
"base_url": {
|
||||||
"display_name": "Base URL",
|
"display_name": "Base URL",
|
||||||
"info": "Endpoint of the Ollama API. Defaults to 'http://localhost:11434' if not specified.",
|
"info": "Endpoint of the Ollama API. Defaults to 'http://localhost:11434' if not specified.",
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"format": {
|
"format": {
|
||||||
"display_name": "Format",
|
"display_name": "Format",
|
||||||
"info": "Specify the format of the output (e.g., json)",
|
"info": "Specify the format of the output (e.g., json)",
|
||||||
|
|
@ -79,13 +60,10 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
"headers": {
|
"headers": {
|
||||||
"display_name": "Headers",
|
"display_name": "Headers",
|
||||||
"advanced": True,
|
"advanced": True,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"keep_alive_flag": {
|
"keep_alive_flag": {
|
||||||
"display_name": "Unload interval",
|
"display_name": "Unload interval",
|
||||||
"options": ["Keep", "Immediately","Minute", "Hour", "sec" ],
|
"options": ["Keep", "Immediately", "Minute", "Hour", "sec"],
|
||||||
"real_time_refresh": True,
|
"real_time_refresh": True,
|
||||||
"refresh_button": True,
|
"refresh_button": True,
|
||||||
},
|
},
|
||||||
|
|
@ -93,9 +71,6 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
"display_name": "interval",
|
"display_name": "interval",
|
||||||
"info": "How long the model will stay loaded into memory.",
|
"info": "How long the model will stay loaded into memory.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"model": {
|
"model": {
|
||||||
"display_name": "Model Name",
|
"display_name": "Model Name",
|
||||||
"options": [],
|
"options": [],
|
||||||
|
|
@ -109,14 +84,6 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
"value": 0.8,
|
"value": 0.8,
|
||||||
"info": "Controls the creativity of model responses.",
|
"info": "Controls the creativity of model responses.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
"format": {
|
|
||||||
"display_name": "Format",
|
|
||||||
"field_type": "str",
|
|
||||||
"info": "Specify the format of the output (e.g., json).",
|
|
||||||
"advanced": True,
|
|
||||||
},
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"display_name": "Metadata",
|
"display_name": "Metadata",
|
||||||
"info": "Metadata to add to the run trace.",
|
"info": "Metadata to add to the run trace.",
|
||||||
|
|
@ -129,7 +96,6 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"real_time_refresh": True,
|
"real_time_refresh": True,
|
||||||
"refresh_button": True,
|
"refresh_button": True,
|
||||||
|
|
||||||
},
|
},
|
||||||
"mirostat_eta": {
|
"mirostat_eta": {
|
||||||
"display_name": "Mirostat Eta",
|
"display_name": "Mirostat Eta",
|
||||||
|
|
@ -260,10 +226,14 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
build_config["mirostat_tau"]["value"] = 5
|
build_config["mirostat_tau"]["value"] = 5
|
||||||
|
|
||||||
if field_name == "model":
|
if field_name == "model":
|
||||||
base_url = build_config.get("base_url", {}).get(
|
base_url_dict = build_config.get("base_url", {})
|
||||||
"value", "http://localhost:11434")
|
base_url_load_from_db = base_url_dict.get("load_from_db", False)
|
||||||
build_config["model"]["options"] = self.get_model(
|
base_url_value = base_url_dict.get("value")
|
||||||
base_url + "/api/tags")
|
if base_url_load_from_db:
|
||||||
|
base_url_value = self.variables(base_url_value)
|
||||||
|
elif not base_url_value:
|
||||||
|
base_url_value = "http://localhost:11434"
|
||||||
|
build_config["model"]["options"] = self.get_model(base_url_value + "/api/tags")
|
||||||
|
|
||||||
if field_name == "keep_alive_flag":
|
if field_name == "keep_alive_flag":
|
||||||
if field_value == "Keep":
|
if field_value == "Keep":
|
||||||
|
|
@ -276,9 +246,6 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
build_config["keep_alive"]["advanced"] = False
|
build_config["keep_alive"]["advanced"] = False
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_model(self, url: str) -> List[str]:
|
def get_model(self, url: str) -> List[str]:
|
||||||
try:
|
try:
|
||||||
|
|
@ -287,8 +254,7 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
model_names = [model['name']
|
model_names = [model["name"] for model in data.get("models", [])]
|
||||||
for model in data.get("models", [])]
|
|
||||||
return model_names
|
return model_names
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Could not retrieve models") from e
|
raise ValueError("Could not retrieve models") from e
|
||||||
|
|
@ -299,15 +265,13 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
base_url: Optional[str],
|
base_url: Optional[str],
|
||||||
model: str,
|
model: str,
|
||||||
input_value: Text,
|
input_value: Text,
|
||||||
|
mirostat: Optional[str] = "Disabled",
|
||||||
mirostat: Optional[str],
|
|
||||||
mirostat_eta: Optional[float] = None,
|
mirostat_eta: Optional[float] = None,
|
||||||
mirostat_tau: Optional[float] = None,
|
mirostat_tau: Optional[float] = None,
|
||||||
|
|
||||||
repeat_last_n: Optional[int] = None,
|
repeat_last_n: Optional[int] = None,
|
||||||
verbose: Optional[bool] = None,
|
verbose: Optional[bool] = None,
|
||||||
keep_alive: Optional[int] = None,
|
keep_alive: Optional[int] = None,
|
||||||
keep_alive_flag: Optional[str] = None,
|
keep_alive_flag: Optional[str] = "Keep",
|
||||||
num_ctx: Optional[int] = None,
|
num_ctx: Optional[int] = None,
|
||||||
num_gpu: Optional[int] = None,
|
num_gpu: Optional[int] = None,
|
||||||
format: Optional[str] = None,
|
format: Optional[str] = None,
|
||||||
|
|
@ -326,12 +290,9 @@ class ChatOllamaComponent(LCModelComponent):
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
system_message: Optional[str] = None,
|
system_message: Optional[str] = None,
|
||||||
) -> Text:
|
) -> Text:
|
||||||
|
|
||||||
if not base_url:
|
if not base_url:
|
||||||
base_url = "http://localhost:11434"
|
base_url = "http://localhost:11434"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if keep_alive_flag == "Minute":
|
if keep_alive_flag == "Minute":
|
||||||
keep_alive_instance = f"{keep_alive}m"
|
keep_alive_instance = f"{keep_alive}m"
|
||||||
elif keep_alive_flag == "Hour":
|
elif keep_alive_flag == "Hour":
|
||||||
|
|
|
||||||
|
|
@ -67,22 +67,19 @@ class QdrantComponent(CustomComponent):
|
||||||
documents.append(_input.to_lc_document())
|
documents.append(_input.to_lc_document())
|
||||||
else:
|
else:
|
||||||
documents.append(_input)
|
documents.append(_input)
|
||||||
if documents is None:
|
if not documents:
|
||||||
from qdrant_client import QdrantClient
|
from qdrant_client import QdrantClient
|
||||||
|
|
||||||
client = QdrantClient(
|
client = QdrantClient(
|
||||||
location=location,
|
location=location,
|
||||||
url=host,
|
url=url,
|
||||||
port=port,
|
port=port,
|
||||||
grpc_port=grpc_port,
|
grpc_port=grpc_port,
|
||||||
https=https,
|
https=https,
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
prefer_grpc=prefer_grpc,
|
prefer_grpc=prefer_grpc,
|
||||||
metadata_payload_key=metadata_payload_key,
|
|
||||||
content_payload_key=content_payload_key,
|
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
collection_name=collection_name,
|
|
||||||
host=host,
|
host=host,
|
||||||
path=path,
|
path=path,
|
||||||
)
|
)
|
||||||
|
|
@ -90,6 +87,8 @@ class QdrantComponent(CustomComponent):
|
||||||
client=client,
|
client=client,
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
embeddings=embedding,
|
embeddings=embedding,
|
||||||
|
content_payload_key=content_payload_key,
|
||||||
|
metadata_payload_key=metadata_payload_key,
|
||||||
)
|
)
|
||||||
return vs
|
return vs
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from emoji import demojize, purely_emoji # type: ignore
|
from emoji import demojize, purely_emoji # type: ignore
|
||||||
|
|
@ -10,10 +13,16 @@ 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
|
||||||
from langflow.interface.types import get_all_components
|
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.flow.model import Flow, FlowCreate
|
||||||
from langflow.services.database.models.folder.model import Folder, FolderCreate
|
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.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_settings_service, session_scope, get_variable_service
|
||||||
|
|
||||||
|
|
||||||
STARTER_FOLDER_NAME = "Starter Projects"
|
STARTER_FOLDER_NAME = "Starter Projects"
|
||||||
STARTER_FOLDER_DESCRIPTION = "Starter projects to help you get started in Langflow."
|
STARTER_FOLDER_DESCRIPTION = "Starter projects to help you get started in Langflow."
|
||||||
|
|
||||||
|
|
@ -205,6 +214,63 @@ def create_starter_folder(session):
|
||||||
return session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
return session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_uuid(val):
|
||||||
|
try:
|
||||||
|
uuid_obj = UUID(val)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return str(uuid_obj) == val
|
||||||
|
|
||||||
|
def load_flows_from_directory():
|
||||||
|
settings_service = get_settings_service()
|
||||||
|
flows_path = settings_service.settings.load_flows_path
|
||||||
|
if not flows_path:
|
||||||
|
return
|
||||||
|
if not settings_service.auth_settings.AUTO_LOGIN:
|
||||||
|
logging.warning("AUTO_LOGIN is disabled, not loading flows from directory")
|
||||||
|
return
|
||||||
|
|
||||||
|
with session_scope() as session:
|
||||||
|
user_id = get_user_by_username(session, settings_service.auth_settings.SUPERUSER).id
|
||||||
|
files = [f for f in os.listdir(flows_path) if os.path.isfile(os.path.join(flows_path, f))]
|
||||||
|
for filename in files:
|
||||||
|
if not filename.endswith(".json"):
|
||||||
|
continue
|
||||||
|
logger.info(f"Loading flow from file: {filename}")
|
||||||
|
with open(os.path.join(flows_path, filename), "r", encoding="utf-8") as file:
|
||||||
|
flow = orjson.loads(file.read())
|
||||||
|
no_json_name = filename.replace(".json", "")
|
||||||
|
flow_endpoint_name = flow.get("endpoint_name")
|
||||||
|
if _is_valid_uuid(no_json_name):
|
||||||
|
flow["id"] = no_json_name
|
||||||
|
flow_id = flow.get("id")
|
||||||
|
|
||||||
|
existing = find_existing_flow(session, flow_id, flow_endpoint_name)
|
||||||
|
if existing:
|
||||||
|
logger.info(f"Updating existing flow: {flow_id} with endpoint name {flow_endpoint_name}")
|
||||||
|
for key, value in flow.items():
|
||||||
|
setattr(existing, key, value)
|
||||||
|
existing.updated_at = datetime.utcnow()
|
||||||
|
existing.user_id = user_id
|
||||||
|
session.add(existing)
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
logger.info(f"Creating new flow: {flow_id} with endpoint name {flow_endpoint_name}")
|
||||||
|
flow["user_id"] = user_id
|
||||||
|
flow = Flow.model_validate(flow, from_attributes=True)
|
||||||
|
flow.updated_at = datetime.utcnow()
|
||||||
|
session.add(flow)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def find_existing_flow(session, flow_id, flow_endpoint_name):
|
||||||
|
if flow_endpoint_name:
|
||||||
|
stmt = select(Flow).where(Flow.endpoint_name == flow_endpoint_name)
|
||||||
|
if existing := session.exec(stmt).first():
|
||||||
|
return existing
|
||||||
|
stmt = select(Flow).where(Flow.id == flow_id)
|
||||||
|
if existing := session.exec(stmt).first():
|
||||||
|
return existing
|
||||||
|
return None
|
||||||
def create_or_update_starter_projects():
|
def create_or_update_starter_projects():
|
||||||
components_paths = get_settings_service().settings.components_path
|
components_paths = get_settings_service().settings.components_path
|
||||||
try:
|
try:
|
||||||
|
|
@ -249,3 +315,20 @@ def create_or_update_starter_projects():
|
||||||
project_icon_bg_color,
|
project_icon_bg_color,
|
||||||
new_folder.id,
|
new_folder.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_super_user_if_needed():
|
||||||
|
settings_service = get_settings_service()
|
||||||
|
if not settings_service.auth_settings.AUTO_LOGIN:
|
||||||
|
return
|
||||||
|
username = settings_service.auth_settings.SUPERUSER
|
||||||
|
password = settings_service.auth_settings.SUPERUSER_PASSWORD
|
||||||
|
if not username or not password:
|
||||||
|
raise ValueError("SUPERUSER and SUPERUSER_PASSWORD must be set in the settings if AUTO_LOGIN is true.")
|
||||||
|
|
||||||
|
with session_scope() as session:
|
||||||
|
super_user = create_super_user(db=session, username=username, password=password)
|
||||||
|
get_variable_service().initialize_user_variables(super_user.id, session)
|
||||||
|
create_default_folder_if_it_doesnt_exist(session, super_user.id)
|
||||||
|
session.commit()
|
||||||
|
logger.info("Super user initialized")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from rich import print as rprint
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
from langflow.api import router
|
from langflow.api import router
|
||||||
from langflow.initial_setup.setup import create_or_update_starter_projects
|
from langflow.initial_setup.setup import create_or_update_starter_projects, initialize_super_user_if_needed, load_flows_from_directory
|
||||||
from langflow.interface.utils import setup_llm_caching
|
from langflow.interface.utils import setup_llm_caching
|
||||||
from langflow.services.plugins.langfuse_plugin import LangfuseInstance
|
from langflow.services.plugins.langfuse_plugin import LangfuseInstance
|
||||||
from langflow.services.utils import initialize_services, teardown_services
|
from langflow.services.utils import initialize_services, teardown_services
|
||||||
|
|
@ -53,7 +53,9 @@ def get_lifespan(fix_migration=False, socketio_server=None):
|
||||||
initialize_services(fix_migration=fix_migration, socketio_server=socketio_server)
|
initialize_services(fix_migration=fix_migration, socketio_server=socketio_server)
|
||||||
setup_llm_caching()
|
setup_llm_caching()
|
||||||
LangfuseInstance.update()
|
LangfuseInstance.update()
|
||||||
|
initialize_super_user_if_needed()
|
||||||
create_or_update_starter_projects()
|
create_or_update_starter_projects()
|
||||||
|
load_flows_from_directory()
|
||||||
yield
|
yield
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "langflow migration --fix" not in str(exc):
|
if "langflow migration --fix" not in str(exc):
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,6 @@ async def get_current_user(
|
||||||
if token:
|
if token:
|
||||||
return await get_current_user_by_jwt(token, db)
|
return await get_current_user_by_jwt(token, db)
|
||||||
else:
|
else:
|
||||||
if not query_param and not header_param:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="An API key as query or header, or a JWT token must be passed",
|
|
||||||
)
|
|
||||||
user = await api_key_security(query_param, header_param, db)
|
user = await api_key_security(query_param, header_param, db)
|
||||||
if user:
|
if user:
|
||||||
return user
|
return user
|
||||||
|
|
@ -216,15 +211,14 @@ def create_super_user(
|
||||||
|
|
||||||
def create_user_longterm_token(db: Session = Depends(get_session)) -> tuple[UUID, dict]:
|
def create_user_longterm_token(db: Session = Depends(get_session)) -> tuple[UUID, dict]:
|
||||||
settings_service = get_settings_service()
|
settings_service = get_settings_service()
|
||||||
|
|
||||||
username = settings_service.auth_settings.SUPERUSER
|
username = settings_service.auth_settings.SUPERUSER
|
||||||
password = settings_service.auth_settings.SUPERUSER_PASSWORD
|
super_user = get_user_by_username(db, username)
|
||||||
if not username or not password:
|
if not super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Missing first superuser credentials",
|
detail="Super user hasn't been created"
|
||||||
)
|
)
|
||||||
super_user = create_super_user(db=db, username=username, password=password)
|
|
||||||
|
|
||||||
access_token_expires_longterm = timedelta(days=365)
|
access_token_expires_longterm = timedelta(days=365)
|
||||||
access_token = create_token(
|
access_token = create_token(
|
||||||
data={"sub": str(super_user.id)},
|
data={"sub": str(super_user.id)},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# Path: src/backend/langflow/services/database/models/flow/model.py
|
# Path: src/backend/langflow/services/database/models/flow/model.py
|
||||||
|
|
||||||
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Dict, Optional
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
@ -7,7 +8,9 @@ from uuid import UUID, uuid4
|
||||||
|
|
||||||
import emoji
|
import emoji
|
||||||
from emoji import purely_emoji # type: ignore
|
from emoji import purely_emoji # type: ignore
|
||||||
|
from fastapi import HTTPException, status
|
||||||
from pydantic import field_serializer, field_validator
|
from pydantic import field_serializer, field_validator
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
|
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
|
||||||
|
|
||||||
from langflow.schema.schema import Record
|
from langflow.schema.schema import Record
|
||||||
|
|
@ -26,6 +29,24 @@ class FlowBase(SQLModel):
|
||||||
is_component: Optional[bool] = Field(default=False, nullable=True)
|
is_component: Optional[bool] = Field(default=False, nullable=True)
|
||||||
updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True)
|
updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True)
|
||||||
folder_id: Optional[UUID] = Field(default=None, nullable=True)
|
folder_id: Optional[UUID] = Field(default=None, nullable=True)
|
||||||
|
endpoint_name: Optional[str] = Field(default=None, nullable=True, index=True)
|
||||||
|
|
||||||
|
@field_validator("endpoint_name")
|
||||||
|
@classmethod
|
||||||
|
def validate_endpoint_name(cls, v):
|
||||||
|
# Endpoint name must be a string containing only letters, numbers, hyphens, and underscores
|
||||||
|
if v is not None:
|
||||||
|
if not isinstance(v, str):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="Endpoint name must be a string",
|
||||||
|
)
|
||||||
|
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="Endpoint name must contain only letters, numbers, hyphens, and underscores",
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
@field_validator("icon_bg_color")
|
@field_validator("icon_bg_color")
|
||||||
def validate_icon_bg_color(cls, v):
|
def validate_icon_bg_color(cls, v):
|
||||||
|
|
@ -128,6 +149,11 @@ class Flow(FlowBase, table=True):
|
||||||
record = Record(data=data)
|
record = Record(data=data)
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("user_id", "name", name="unique_flow_name"),
|
||||||
|
UniqueConstraint("user_id", "endpoint_name", name="unique_flow_endpoint_name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FlowCreate(FlowBase):
|
class FlowCreate(FlowBase):
|
||||||
user_id: Optional[UUID] = None
|
user_id: Optional[UUID] = None
|
||||||
|
|
@ -145,3 +171,21 @@ class FlowUpdate(SQLModel):
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
data: Optional[Dict] = None
|
data: Optional[Dict] = None
|
||||||
folder_id: Optional[UUID] = None
|
folder_id: Optional[UUID] = None
|
||||||
|
endpoint_name: Optional[str] = None
|
||||||
|
|
||||||
|
@field_validator("endpoint_name")
|
||||||
|
@classmethod
|
||||||
|
def validate_endpoint_name(cls, v):
|
||||||
|
# Endpoint name must be a string containing only letters, numbers, hyphens, and underscores
|
||||||
|
if v is not None:
|
||||||
|
if not isinstance(v, str):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="Endpoint name must be a string",
|
||||||
|
)
|
||||||
|
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="Endpoint name must contain only letters, numbers, hyphens, and underscores",
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
from langflow.services.database.models.flow.model import FlowRead
|
from langflow.services.database.models.flow.model import FlowRead
|
||||||
|
|
@ -30,6 +31,8 @@ class Folder(FolderBase, table=True):
|
||||||
back_populates="folder", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"}
|
back_populates="folder", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__table_args__ = (UniqueConstraint("user_id", "name", name="unique_folder_name"),)
|
||||||
|
|
||||||
|
|
||||||
class FolderCreate(FolderBase):
|
class FolderCreate(FolderBase):
|
||||||
components_list: Optional[List[UUID]] = None
|
components_list: Optional[List[UUID]] = None
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class Settings(BaseSettings):
|
||||||
remove_api_keys: bool = False
|
remove_api_keys: bool = False
|
||||||
components_path: List[str] = []
|
components_path: List[str] = []
|
||||||
langchain_cache: str = "InMemoryCache"
|
langchain_cache: str = "InMemoryCache"
|
||||||
|
load_flows_path: Optional[str] = None
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
redis_host: str = "localhost"
|
redis_host: str = "localhost"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
@ -7,7 +8,6 @@ from langflow.services.base import Service
|
||||||
from langflow.services.settings.auth import AuthSettings
|
from langflow.services.settings.auth import AuthSettings
|
||||||
from langflow.services.settings.base import Settings
|
from langflow.services.settings.base import Settings
|
||||||
|
|
||||||
|
|
||||||
class SettingsService(Service):
|
class SettingsService(Service):
|
||||||
name = "settings_service"
|
name = "settings_service"
|
||||||
|
|
||||||
|
|
@ -27,7 +27,6 @@ class SettingsService(Service):
|
||||||
|
|
||||||
with open(file_path, "r") as f:
|
with open(file_path, "r") as f:
|
||||||
settings_dict = yaml.safe_load(f)
|
settings_dict = yaml.safe_load(f)
|
||||||
settings_dict = {k.upper(): v for k, v in settings_dict.items()}
|
|
||||||
|
|
||||||
for key in settings_dict:
|
for key in settings_dict:
|
||||||
if key not in Settings.model_fields.keys():
|
if key not in Settings.model_fields.keys():
|
||||||
|
|
|
||||||
65
src/backend/base/langflow/utils/migration.py
Normal file
65
src/backend/base/langflow/utils/migration.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
|
||||||
|
|
||||||
|
def table_exists(name, conn):
|
||||||
|
"""
|
||||||
|
Check if a table exists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name (str): The name of the table to check.
|
||||||
|
conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the table exists, False otherwise.
|
||||||
|
"""
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
return name in inspector.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def column_exists(table_name, column_name, conn):
|
||||||
|
"""
|
||||||
|
Check if a column exists in a table.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
table_name (str): The name of the table to check.
|
||||||
|
column_name (str): The name of the column to check.
|
||||||
|
conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the column exists, False otherwise.
|
||||||
|
"""
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
return column_name in [column["name"] for column in inspector.get_columns(table_name)]
|
||||||
|
|
||||||
|
|
||||||
|
def foreign_key_exists(table_name, fk_name, conn):
|
||||||
|
"""
|
||||||
|
Check if a foreign key exists in a table.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
table_name (str): The name of the table to check.
|
||||||
|
fk_name (str): The name of the foreign key to check.
|
||||||
|
conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the foreign key exists, False otherwise.
|
||||||
|
"""
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
return fk_name in [fk["name"] for fk in inspector.get_foreign_keys(table_name)]
|
||||||
|
|
||||||
|
|
||||||
|
def constraint_exists(table_name, constraint_name, conn):
|
||||||
|
"""
|
||||||
|
Check if a constraint exists in a table.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
table_name (str): The name of the table to check.
|
||||||
|
constraint_name (str): The name of the constraint to check.
|
||||||
|
conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the constraint exists, False otherwise.
|
||||||
|
"""
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
constraints = inspector.get_unique_constraints(table_name)
|
||||||
|
return constraint_name in [constraint["name"] for constraint in constraints]
|
||||||
17
src/backend/base/poetry.lock
generated
17
src/backend/base/poetry.lock
generated
|
|
@ -2856,6 +2856,21 @@ files = [
|
||||||
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
|
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uncurl"
|
||||||
|
version = "0.0.11"
|
||||||
|
description = "A library to convert curl requests to python-requests."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "uncurl-0.0.11-py3-none-any.whl", hash = "sha256:5961e93f07a5c9f2ef8ae4245bd92b0a6ce503c851de980f5b70080ae74cdc59"},
|
||||||
|
{file = "uncurl-0.0.11.tar.gz", hash = "sha256:530c9bbd4d118f4cde6194165ff484cc25b0661cd256f19e9d5fcb53fc077790"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyperclip = "*"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -3250,4 +3265,4 @@ local = []
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "31d8e5ce045ef7d94e63058559b5f8181e6b51fc923c4904f45481443d59235d"
|
content-hash = "1dc0dd442df5d174ea85c859208f5cfea9d4785c7b7a93f2c6bf8c92e93d8cad"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "langflow-base"
|
name = "langflow-base"
|
||||||
version = "0.0.52"
|
version = "0.0.53"
|
||||||
description = "A Python package with a built-in web application"
|
description = "A Python package with a built-in web application"
|
||||||
authors = ["Langflow <contact@langflow.org>"]
|
authors = ["Langflow <contact@langflow.org>"]
|
||||||
maintainers = [
|
maintainers = [
|
||||||
|
|
@ -62,6 +62,7 @@ emoji = "^2.12.0"
|
||||||
cryptography = "^42.0.5"
|
cryptography = "^42.0.5"
|
||||||
asyncer = "^0.0.5"
|
asyncer = "^0.0.5"
|
||||||
pyperclip = "^1.8.2"
|
pyperclip = "^1.8.2"
|
||||||
|
uncurl = "^0.0.11"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ export default function App() {
|
||||||
await getFoldersApi();
|
await getFoldersApi();
|
||||||
await getTypes();
|
await getTypes();
|
||||||
await refreshFlows();
|
await refreshFlows();
|
||||||
console.log(axios.defaults);
|
|
||||||
const res = await getGlobalVariables();
|
const res = await getGlobalVariables();
|
||||||
setGlobalVariables(res);
|
setGlobalVariables(res);
|
||||||
checkHasStore();
|
checkHasStore();
|
||||||
|
|
|
||||||
|
|
@ -841,9 +841,7 @@ export default function CodeTabsComponent({
|
||||||
node.data.node!.template[
|
node.data.node!.template[
|
||||||
templateField
|
templateField
|
||||||
].value?.toString() === "{}"
|
].value?.toString() === "{}"
|
||||||
? {
|
? {}
|
||||||
// yourkey: "value",
|
|
||||||
}
|
|
||||||
: node.data.node!
|
: node.data.node!
|
||||||
.template[
|
.template[
|
||||||
templateField
|
templateField
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ export default function DictComponent({
|
||||||
editNode = false,
|
editNode = false,
|
||||||
id = "",
|
id = "",
|
||||||
}: DictComponentType): JSX.Element {
|
}: DictComponentType): JSX.Element {
|
||||||
|
// Create a reference to the value
|
||||||
|
const ref = useRef(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
onChange({});
|
onChange({});
|
||||||
|
|
@ -19,10 +22,9 @@ export default function DictComponent({
|
||||||
}, [disabled]);
|
}, [disabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) onChange(value);
|
// Update the reference value
|
||||||
|
ref.current = value;
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const ref = useRef(value);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,14 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
||||||
name,
|
name,
|
||||||
invalidNameList,
|
invalidNameList,
|
||||||
description,
|
description,
|
||||||
|
endpointName,
|
||||||
maxLength = 50,
|
maxLength = 50,
|
||||||
setName,
|
setName,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
setEndpointName,
|
||||||
}: InputProps): JSX.Element => {
|
}: InputProps): JSX.Element => {
|
||||||
const [isMaxLength, setIsMaxLength] = useState(false);
|
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||||
|
const [isEndpointNameValid, setIsEndpointNameValid] = useState(true);
|
||||||
|
|
||||||
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
|
|
@ -29,6 +32,18 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
||||||
setDescription!(event.target.value);
|
setDescription!(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
// Validate the endpoint name
|
||||||
|
// use this regex r'^[a-zA-Z0-9_-]+$'
|
||||||
|
const isValid =
|
||||||
|
(/^[a-zA-Z0-9_-]+$/.test(event.target.value) &&
|
||||||
|
event.target.value.length <= maxLength) ||
|
||||||
|
// empty is also valid
|
||||||
|
event.target.value.length === 0;
|
||||||
|
setIsEndpointNameValid(isValid);
|
||||||
|
setEndpointName!(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
|
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
|
||||||
const handleFocus = (event) => event.target.select();
|
const handleFocus = (event) => event.target.select();
|
||||||
|
|
||||||
|
|
@ -91,6 +106,32 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
|
{setEndpointName && (
|
||||||
|
<Label>
|
||||||
|
<div className="edit-flow-arrangement mt-3">
|
||||||
|
<span className="font-medium">Endpoint name:</span>
|
||||||
|
{!isEndpointNameValid && (
|
||||||
|
<span className="edit-flow-span">
|
||||||
|
Invalid endpoint name. Use only letters, numbers, hyphens, and
|
||||||
|
underscores ({maxLength} characters max).
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
|
||||||
|
onChange={handleEndpointNameChange}
|
||||||
|
type="text"
|
||||||
|
name="endpoint_name"
|
||||||
|
value={endpointName ?? ""}
|
||||||
|
placeholder="An alternative name for the run endpoint"
|
||||||
|
maxLength={maxLength}
|
||||||
|
id="endpoint_name"
|
||||||
|
onDoubleClickCapture={(event) => {
|
||||||
|
handleFocus(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,8 @@ export default function InputFileComponent({
|
||||||
uploadFile(file, currentFlowId)
|
uploadFile(file, currentFlowId)
|
||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log(CONSOLE_SUCCESS_MSG);
|
|
||||||
// Get the file name from the response
|
// Get the file name from the response
|
||||||
const { file_path } = data;
|
const { file_path } = data;
|
||||||
console.log("File name:", file_path);
|
|
||||||
|
|
||||||
// sets the value that goes to the backend
|
// sets the value that goes to the backend
|
||||||
onFileChange(file_path);
|
onFileChange(file_path);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
const [foldersNames, setFoldersNames] = useState({});
|
const [foldersNames, setFoldersNames] = useState({});
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
const [editFolders, setEditFolderName] = useState(
|
const [editFolders, setEditFolderName] = useState(
|
||||||
folders.map((obj) => ({ name: obj.name, edit: false })),
|
folders.map((obj) => ({ name: obj.name, edit: false }))
|
||||||
);
|
);
|
||||||
const uploadFolder = useFolderStore((state) => state.uploadFolder);
|
const uploadFolder = useFolderStore((state) => state.uploadFolder);
|
||||||
const currentFolder = pathname.split("/");
|
const currentFolder = pathname.split("/");
|
||||||
|
|
@ -58,7 +58,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
|
|
||||||
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
|
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
|
||||||
folderId,
|
folderId,
|
||||||
handleFolderChange,
|
handleFolderChange
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUploadFlowsToFolder = () => {
|
const handleUploadFlowsToFolder = () => {
|
||||||
|
|
@ -73,7 +73,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
|
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
getFoldersApi(true);
|
getFoldersApi(true);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,8 +91,6 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
folders.map((obj) => ({ name: obj.name, edit: false }));
|
folders.map((obj) => ({ name: obj.name, edit: false }));
|
||||||
}, [folders]);
|
}, [folders]);
|
||||||
|
|
||||||
console.log(folderId, folderIdDragging);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex shrink-0 items-center justify-between">
|
<div className="flex shrink-0 items-center justify-between">
|
||||||
|
|
@ -120,7 +118,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
<>
|
<>
|
||||||
{folders.map((item, index) => {
|
{folders.map((item, index) => {
|
||||||
const editFolderName = editFolders?.filter(
|
const editFolderName = editFolders?.filter(
|
||||||
(folder) => folder.name === item.name,
|
(folder) => folder.name === item.name
|
||||||
)[0];
|
)[0];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -136,7 +134,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
? "border border-border bg-muted hover:bg-muted"
|
? "border border-border bg-muted hover:bg-muted"
|
||||||
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
|
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
|
||||||
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
|
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
|
||||||
folderIdDragging === item.id! ? "bg-border" : "",
|
folderIdDragging === item.id! ? "bg-border" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => handleChangeFolder!(item.id!)}
|
onClick={() => handleChangeFolder!(item.id!)}
|
||||||
>
|
>
|
||||||
|
|
@ -206,7 +204,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
folders.map((obj) => ({
|
folders.map((obj) => ({
|
||||||
name: obj.name,
|
name: obj.name,
|
||||||
edit: false,
|
edit: false,
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
|
|
@ -239,10 +237,10 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
};
|
};
|
||||||
const updatedFolder = await updateFolder(
|
const updatedFolder = await updateFolder(
|
||||||
body,
|
body,
|
||||||
item.id!,
|
item.id!
|
||||||
);
|
);
|
||||||
const updateFolders = folders.filter(
|
const updateFolders = folders.filter(
|
||||||
(f) => f.name !== item.name,
|
(f) => f.name !== item.name
|
||||||
);
|
);
|
||||||
setFolders([...updateFolders, updatedFolder]);
|
setFolders([...updateFolders, updatedFolder]);
|
||||||
setFoldersNames({});
|
setFoldersNames({});
|
||||||
|
|
@ -250,7 +248,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
folders.map((obj) => ({
|
folders.map((obj) => ({
|
||||||
name: obj.name,
|
name: obj.name,
|
||||||
edit: false,
|
edit: false,
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setFoldersNames((old) => ({
|
setFoldersNames((old) => ({
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export async function sendAll(data: sendAllProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postValidateCode(
|
export async function postValidateCode(
|
||||||
code: string,
|
code: string
|
||||||
): Promise<AxiosResponse<errorsTypeAPI>> {
|
): Promise<AxiosResponse<errorsTypeAPI>> {
|
||||||
return await api.post(`${BASE_URL_API}validate/code`, { code });
|
return await api.post(`${BASE_URL_API}validate/code`, { code });
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ export async function postValidateCode(
|
||||||
export async function postValidatePrompt(
|
export async function postValidatePrompt(
|
||||||
name: string,
|
name: string,
|
||||||
template: string,
|
template: string,
|
||||||
frontend_node: APIClassType,
|
frontend_node: APIClassType
|
||||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||||
return api.post(`${BASE_URL_API}validate/prompt`, {
|
return api.post(`${BASE_URL_API}validate/prompt`, {
|
||||||
name,
|
name,
|
||||||
|
|
@ -149,7 +149,7 @@ export async function saveFlowToDatabase(newFlow: {
|
||||||
* @throws Will throw an error if the update fails.
|
* @throws Will throw an error if the update fails.
|
||||||
*/
|
*/
|
||||||
export async function updateFlowInDatabase(
|
export async function updateFlowInDatabase(
|
||||||
updatedFlow: FlowType,
|
updatedFlow: FlowType
|
||||||
): Promise<FlowType> {
|
): Promise<FlowType> {
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
|
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
|
||||||
|
|
@ -157,6 +157,7 @@ export async function updateFlowInDatabase(
|
||||||
data: updatedFlow.data,
|
data: updatedFlow.data,
|
||||||
description: updatedFlow.description,
|
description: updatedFlow.description,
|
||||||
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
|
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
|
||||||
|
endpoint_name: updatedFlow.endpoint_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response?.status !== 200) {
|
if (response?.status !== 200) {
|
||||||
|
|
@ -326,7 +327,7 @@ export async function getHealth() {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export async function getBuildStatus(
|
export async function getBuildStatus(
|
||||||
flowId: string,
|
flowId: string
|
||||||
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
|
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
|
||||||
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
|
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +340,7 @@ export async function getBuildStatus(
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export async function postBuildInit(
|
export async function postBuildInit(
|
||||||
flow: FlowType,
|
flow: FlowType
|
||||||
): Promise<AxiosResponse<InitTypeAPI>> {
|
): Promise<AxiosResponse<InitTypeAPI>> {
|
||||||
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
|
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +356,7 @@ export async function postBuildInit(
|
||||||
*/
|
*/
|
||||||
export async function uploadFile(
|
export async function uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
id: string,
|
id: string
|
||||||
): Promise<AxiosResponse<UploadFileTypeAPI>> {
|
): Promise<AxiosResponse<UploadFileTypeAPI>> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
@ -364,7 +365,7 @@ export async function uploadFile(
|
||||||
|
|
||||||
export async function postCustomComponent(
|
export async function postCustomComponent(
|
||||||
code: string,
|
code: string,
|
||||||
apiClass: APIClassType,
|
apiClass: APIClassType
|
||||||
): Promise<AxiosResponse<APIClassType>> {
|
): Promise<AxiosResponse<APIClassType>> {
|
||||||
// let template = apiClass.template;
|
// let template = apiClass.template;
|
||||||
return await api.post(`${BASE_URL_API}custom_component`, {
|
return await api.post(`${BASE_URL_API}custom_component`, {
|
||||||
|
|
@ -377,7 +378,7 @@ export async function postCustomComponentUpdate(
|
||||||
code: string,
|
code: string,
|
||||||
template: APITemplateType,
|
template: APITemplateType,
|
||||||
field: string,
|
field: string,
|
||||||
field_value: any,
|
field_value: any
|
||||||
): Promise<AxiosResponse<APIClassType>> {
|
): Promise<AxiosResponse<APIClassType>> {
|
||||||
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
||||||
code,
|
code,
|
||||||
|
|
@ -399,7 +400,7 @@ export async function onLogin(user: LoginType) {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
|
@ -461,11 +462,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
|
||||||
|
|
||||||
export async function getUsersPage(
|
export async function getUsersPage(
|
||||||
skip: number,
|
skip: number,
|
||||||
limit: number,
|
limit: number
|
||||||
): Promise<Array<Users>> {
|
): Promise<Array<Users>> {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(
|
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) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -502,7 +503,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
|
||||||
try {
|
try {
|
||||||
const res = await api.patch(
|
const res = await api.patch(
|
||||||
`${BASE_URL_API}users/${user_id}/reset-password`,
|
`${BASE_URL_API}users/${user_id}/reset-password`,
|
||||||
user,
|
user
|
||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -576,7 +577,7 @@ export async function saveFlowStore(
|
||||||
last_tested_version?: string;
|
last_tested_version?: string;
|
||||||
},
|
},
|
||||||
tags: string[],
|
tags: string[],
|
||||||
publicFlow = false,
|
publicFlow = false
|
||||||
): Promise<FlowType> {
|
): Promise<FlowType> {
|
||||||
try {
|
try {
|
||||||
const response = await api.post(`${BASE_URL_API}store/components/`, {
|
const response = await api.post(`${BASE_URL_API}store/components/`, {
|
||||||
|
|
@ -705,7 +706,7 @@ export async function postStoreComponents(component: Component) {
|
||||||
export async function getComponent(component_id: string) {
|
export async function getComponent(component_id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(
|
const res = await api.get(
|
||||||
`${BASE_URL_API}store/components/${component_id}`,
|
`${BASE_URL_API}store/components/${component_id}`
|
||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -720,7 +721,7 @@ export async function searchComponent(
|
||||||
page?: number | null,
|
page?: number | null,
|
||||||
limit?: number | null,
|
limit?: number | null,
|
||||||
status?: string | null,
|
status?: string | null,
|
||||||
tags?: string[],
|
tags?: string[]
|
||||||
): Promise<StoreComponentResponse | undefined> {
|
): Promise<StoreComponentResponse | undefined> {
|
||||||
try {
|
try {
|
||||||
let url = `${BASE_URL_API}store/components/`;
|
let url = `${BASE_URL_API}store/components/`;
|
||||||
|
|
@ -832,7 +833,7 @@ export async function updateFlowStore(
|
||||||
},
|
},
|
||||||
tags: string[],
|
tags: string[],
|
||||||
publicFlow = false,
|
publicFlow = false,
|
||||||
id: string,
|
id: string
|
||||||
): Promise<FlowType> {
|
): Promise<FlowType> {
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
|
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
|
||||||
|
|
@ -916,7 +917,7 @@ export async function deleteGlobalVariable(id: string) {
|
||||||
export async function updateGlobalVariable(
|
export async function updateGlobalVariable(
|
||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
id: string,
|
id: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
|
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
|
||||||
|
|
@ -935,7 +936,7 @@ export async function getVerticesOrder(
|
||||||
startNodeId?: string | null,
|
startNodeId?: string | null,
|
||||||
stopNodeId?: string | null,
|
stopNodeId?: string | null,
|
||||||
nodes?: Node[],
|
nodes?: Node[],
|
||||||
Edges?: Edge[],
|
Edges?: Edge[]
|
||||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||||
// nodeId is optional and is a query parameter
|
// nodeId is optional and is a query parameter
|
||||||
// if nodeId is not provided, the API will return all vertices
|
// if nodeId is not provided, the API will return all vertices
|
||||||
|
|
@ -955,19 +956,19 @@ export async function getVerticesOrder(
|
||||||
return await api.post(
|
return await api.post(
|
||||||
`${BASE_URL_API}build/${flowId}/vertices`,
|
`${BASE_URL_API}build/${flowId}/vertices`,
|
||||||
data,
|
data,
|
||||||
config,
|
config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postBuildVertex(
|
export async function postBuildVertex(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
vertexId: string,
|
vertexId: string,
|
||||||
input_value: string,
|
input_value: string
|
||||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||||
// input_value is optional and is a query parameter
|
// input_value is optional and is a query parameter
|
||||||
return await api.post(
|
return await api.post(
|
||||||
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
||||||
input_value ? { inputs: { input_value: input_value } } : undefined,
|
input_value ? { inputs: { input_value: input_value } } : undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -991,7 +992,7 @@ export async function getFlowPool({
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFlowPool(
|
export async function deleteFlowPool(
|
||||||
flowId: string,
|
flowId: string
|
||||||
): Promise<AxiosResponse<any>> {
|
): Promise<AxiosResponse<any>> {
|
||||||
const config = {};
|
const config = {};
|
||||||
config["params"] = { flow_id: flowId };
|
config["params"] = { flow_id: flowId };
|
||||||
|
|
@ -999,7 +1000,7 @@ export async function deleteFlowPool(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function multipleDeleteFlowsComponents(
|
export async function multipleDeleteFlowsComponents(
|
||||||
flowIds: string[],
|
flowIds: string[]
|
||||||
): Promise<AxiosResponse<any>> {
|
): Promise<AxiosResponse<any>> {
|
||||||
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
|
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
|
||||||
flow_ids: flowIds,
|
flow_ids: flowIds,
|
||||||
|
|
@ -1009,7 +1010,7 @@ export async function multipleDeleteFlowsComponents(
|
||||||
export async function getTransactionTable(
|
export async function getTransactionTable(
|
||||||
id: string,
|
id: string,
|
||||||
mode: "intersection" | "union",
|
mode: "intersection" | "union",
|
||||||
params = {},
|
params = {}
|
||||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||||
const config = {};
|
const config = {};
|
||||||
config["params"] = { flow_id: id };
|
config["params"] = { flow_id: id };
|
||||||
|
|
@ -1024,7 +1025,7 @@ export async function getTransactionTable(
|
||||||
export async function getMessagesTable(
|
export async function getMessagesTable(
|
||||||
id: string,
|
id: string,
|
||||||
mode: "intersection" | "union",
|
mode: "intersection" | "union",
|
||||||
params = {},
|
params = {}
|
||||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||||
const config = {};
|
const config = {};
|
||||||
config["params"] = { flow_id: id };
|
config["params"] = { flow_id: id };
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||||
import DictComponent from "../../../../components/dictComponent";
|
import DictComponent from "../../../../components/dictComponent";
|
||||||
import Dropdown from "../../../../components/dropdownComponent";
|
import Dropdown from "../../../../components/dropdownComponent";
|
||||||
import FloatComponent from "../../../../components/floatComponent";
|
import FloatComponent from "../../../../components/floatComponent";
|
||||||
import ForwardedIconComponent, {
|
import { default as IconComponent } from "../../../../components/genericIconComponent";
|
||||||
default as IconComponent,
|
|
||||||
} from "../../../../components/genericIconComponent";
|
|
||||||
import InputFileComponent from "../../../../components/inputFileComponent";
|
import InputFileComponent from "../../../../components/inputFileComponent";
|
||||||
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
|
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
|
||||||
import InputListComponent from "../../../../components/inputListComponent";
|
import InputListComponent from "../../../../components/inputListComponent";
|
||||||
|
|
@ -18,19 +16,9 @@ import ShadTooltip from "../../../../components/shadTooltipComponent";
|
||||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
import { Button } from "../../../../components/ui/button";
|
import { Button } from "../../../../components/ui/button";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "../../../../components/ui/dropdown-menu";
|
|
||||||
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
||||||
import {
|
import { LANGFLOW_SUPPORTED_TYPES } from "../../../../constants/constants";
|
||||||
LANGFLOW_SUPPORTED_TYPES,
|
|
||||||
TOOLTIP_EMPTY,
|
|
||||||
} from "../../../../constants/constants";
|
|
||||||
import { Case } from "../../../../shared/components/caseComponent";
|
import { Case } from "../../../../shared/components/caseComponent";
|
||||||
import useAlertStore from "../../../../stores/alertStore";
|
|
||||||
import useFlowStore from "../../../../stores/flowStore";
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||||
import { useTypesStore } from "../../../../stores/typesStore";
|
import { useTypesStore } from "../../../../stores/typesStore";
|
||||||
|
|
@ -48,12 +36,11 @@ import {
|
||||||
scapedJSONStringfy,
|
scapedJSONStringfy,
|
||||||
} from "../../../../utils/reactflowUtils";
|
} from "../../../../utils/reactflowUtils";
|
||||||
import { nodeColors } from "../../../../utils/styleUtils";
|
import { nodeColors } from "../../../../utils/styleUtils";
|
||||||
import { classNames, cn, groupByFamily } from "../../../../utils/utils";
|
import { classNames, groupByFamily } from "../../../../utils/utils";
|
||||||
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
||||||
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
||||||
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
||||||
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
||||||
import TooltipRenderComponent from "../tooltipRenderComponent";
|
|
||||||
import HandleTooltips from "../HandleTooltipComponent";
|
import HandleTooltips from "../HandleTooltipComponent";
|
||||||
import OutputComponent from "../OutputComponent";
|
import OutputComponent from "../OutputComponent";
|
||||||
|
|
||||||
|
|
@ -92,7 +79,7 @@ export default function ParameterComponent({
|
||||||
debouncedHandleUpdateValues,
|
debouncedHandleUpdateValues,
|
||||||
setNode,
|
setNode,
|
||||||
isLoading,
|
isLoading,
|
||||||
setIsLoading,
|
setIsLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
|
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
|
||||||
|
|
@ -100,7 +87,7 @@ export default function ParameterComponent({
|
||||||
name,
|
name,
|
||||||
takeSnapshot,
|
takeSnapshot,
|
||||||
setNode,
|
setNode,
|
||||||
updateNodeInternals,
|
updateNodeInternals
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
|
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
|
||||||
|
|
@ -109,7 +96,7 @@ export default function ParameterComponent({
|
||||||
let disabled =
|
let disabled =
|
||||||
edges.some(
|
edges.some(
|
||||||
(edge) =>
|
(edge) =>
|
||||||
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
|
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
|
||||||
const handleRefreshButtonPress = async (name, data) => {
|
const handleRefreshButtonPress = async (name, data) => {
|
||||||
|
|
@ -120,7 +107,7 @@ export default function ParameterComponent({
|
||||||
|
|
||||||
const handleOnNewValue = async (
|
const handleOnNewValue = async (
|
||||||
newValue: string | string[] | boolean | Object[],
|
newValue: string | string[] | boolean | Object[],
|
||||||
skipSnapshot: boolean | undefined = false,
|
skipSnapshot: boolean | undefined = false
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
handleOnNewValueHook(newValue, skipSnapshot);
|
handleOnNewValueHook(newValue, skipSnapshot);
|
||||||
};
|
};
|
||||||
|
|
@ -201,14 +188,14 @@ export default function ParameterComponent({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||||
"h-3 w-3 rounded-full border-2 bg-background",
|
"h-3 w-3 rounded-full border-2 bg-background",
|
||||||
!showNode ? "mt-0" : "",
|
!showNode ? "mt-0" : ""
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
borderColor: color ?? nodeColors.unknown,
|
borderColor: color ?? nodeColors.unknown,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilterEdge(
|
setFilterEdge(
|
||||||
groupByFamily(myData, tooltipTitle!, left, nodes!),
|
groupByFamily(myData, tooltipTitle!, left, nodes!)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
></Handle>
|
></Handle>
|
||||||
|
|
@ -293,12 +280,12 @@ export default function ParameterComponent({
|
||||||
}
|
}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
left ? "-ml-0.5" : "-mr-0.5",
|
left ? "-ml-0.5" : "-mr-0.5",
|
||||||
"h-3 w-3 rounded-full border-2 bg-background",
|
"h-3 w-3 rounded-full border-2 bg-background"
|
||||||
)}
|
)}
|
||||||
style={{ borderColor: color ?? nodeColors.unknown }}
|
style={{ borderColor: color ?? nodeColors.unknown }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilterEdge(
|
setFilterEdge(
|
||||||
groupByFamily(myData, tooltipTitle!, left, nodes!),
|
groupByFamily(myData, tooltipTitle!, left, nodes!)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -548,9 +535,7 @@ export default function ParameterComponent({
|
||||||
value={
|
value={
|
||||||
!data.node!.template[name]?.value ||
|
!data.node!.template[name]?.value ||
|
||||||
data.node!.template[name]?.value?.toString() === "{}"
|
data.node!.template[name]?.value?.toString() === "{}"
|
||||||
? {
|
? {}
|
||||||
// yourkey: "value",
|
|
||||||
}
|
|
||||||
: data.node!.template[name]?.value
|
: data.node!.template[name]?.value
|
||||||
}
|
}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import Loading from "../../components/ui/loading";
|
||||||
import { Textarea } from "../../components/ui/textarea";
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
import Xmark from "../../components/ui/xmark";
|
import Xmark from "../../components/ui/xmark";
|
||||||
import {
|
import {
|
||||||
NATIVE_CATEGORIES,
|
|
||||||
RUN_TIMESTAMP_PREFIX,
|
RUN_TIMESTAMP_PREFIX,
|
||||||
STATUS_BUILD,
|
STATUS_BUILD,
|
||||||
STATUS_BUILDING,
|
STATUS_BUILDING,
|
||||||
|
|
@ -77,18 +76,14 @@ export default function GenericNode({
|
||||||
// This one should run only once
|
// This one should run only once
|
||||||
// first check if data.type in NATIVE_CATEGORIES
|
// first check if data.type in NATIVE_CATEGORIES
|
||||||
// if not return
|
// if not return
|
||||||
if (
|
if (!data.node?.template?.code?.value) return;
|
||||||
!NATIVE_CATEGORIES.includes(types[data.type]) ||
|
|
||||||
!data.node?.template?.code?.value
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const thisNodeTemplate = templates[data.type].template;
|
const thisNodeTemplate = templates[data.type].template;
|
||||||
// if the template does not have a code key
|
// if the template does not have a code key
|
||||||
// return
|
// return
|
||||||
if (!thisNodeTemplate.code) return;
|
if (!thisNodeTemplate.code) return;
|
||||||
const currentCode = thisNodeTemplate.code?.value;
|
const currentCode = thisNodeTemplate.code?.value;
|
||||||
const thisNodesCode = data.node!.template?.code?.value;
|
const thisNodesCode = data.node!.template?.code?.value;
|
||||||
const componentsToIgnore = ["Custom Component", "Prompt"];
|
const componentsToIgnore = ["Custom Component"];
|
||||||
if (
|
if (
|
||||||
currentCode !== thisNodesCode &&
|
currentCode !== thisNodesCode &&
|
||||||
!componentsToIgnore.includes(data.node!.display_name)
|
!componentsToIgnore.includes(data.node!.display_name)
|
||||||
|
|
@ -390,12 +385,10 @@ export default function GenericNode({
|
||||||
"generic-node-div",
|
"generic-node-div",
|
||||||
specificClassFromBuildStatus
|
specificClassFromBuildStatus
|
||||||
);
|
);
|
||||||
console.log("names", names);
|
|
||||||
return names;
|
return names;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBaseBorderClass = (selected) => {
|
const getBaseBorderClass = (selected) => {
|
||||||
console.log("data.node?.frozen", data.node?.frozen);
|
|
||||||
let className = selected ? "border border-ring" : "border";
|
let className = selected ? "border border-ring" : "border";
|
||||||
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
||||||
return data.node?.frozen ? frozenClass : className;
|
return data.node?.frozen ? frozenClass : className;
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ export default function IOFileInput({ field, updateValue }: IOFileInputProps) {
|
||||||
uploadFile(file, currentFlowId)
|
uploadFile(file, currentFlowId)
|
||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log("File uploaded successfully");
|
|
||||||
// Get the file name from the response
|
// Get the file name from the response
|
||||||
const { file_path, flowId } = data;
|
const { file_path, flowId } = data;
|
||||||
setFilePath(file_path);
|
setFilePath(file_path);
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ export default function getCurlCode(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
isAuth: boolean,
|
isAuth: boolean,
|
||||||
tweaksBuildedObject,
|
tweaksBuildedObject,
|
||||||
|
endpointName?: string
|
||||||
): string {
|
): string {
|
||||||
const tweaksObject = tweaksBuildedObject[0];
|
const tweaksObject = tweaksBuildedObject[0];
|
||||||
|
// show the endpoint name in the curl command if it exists
|
||||||
return `curl -X POST \\
|
return `curl -X POST \\
|
||||||
${window.location.protocol}//${
|
${window.location.protocol}//${window.location.host}/api/v1/run/${
|
||||||
window.location.host
|
endpointName || flowId
|
||||||
}/api/v1/run/${flowId}?stream=false \\
|
}?stream=false \\
|
||||||
-H 'Content-Type: application/json'\\${
|
-H 'Content-Type: application/json'\\${
|
||||||
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ import { buildContent } from "../utils/build-content";
|
||||||
import { buildTweaks } from "../utils/build-tweaks";
|
import { buildTweaks } from "../utils/build-tweaks";
|
||||||
import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object";
|
import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object";
|
||||||
import { getChangesType } from "../utils/get-changes-types";
|
import { getChangesType } from "../utils/get-changes-types";
|
||||||
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
|
|
||||||
import { getValue } from "../utils/get-value";
|
|
||||||
import getPythonApiCode from "../utils/get-python-api-code";
|
|
||||||
import getCurlCode from "../utils/get-curl-code";
|
import getCurlCode from "../utils/get-curl-code";
|
||||||
|
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
|
||||||
|
import getPythonApiCode from "../utils/get-python-api-code";
|
||||||
import getPythonCode from "../utils/get-python-code";
|
import getPythonCode from "../utils/get-python-code";
|
||||||
|
import { getValue } from "../utils/get-value";
|
||||||
import getWidgetCode from "../utils/get-widget-code";
|
import getWidgetCode from "../utils/get-widget-code";
|
||||||
import tabsArray from "../utils/tabs-array";
|
import tabsArray from "../utils/tabs-array";
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ const ApiModal = forwardRef(
|
||||||
flow: FlowType;
|
flow: FlowType;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
},
|
},
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
const tweak = useTweaksStore((state) => state.tweak);
|
const tweak = useTweaksStore((state) => state.tweak);
|
||||||
const addTweaks = useTweaksStore((state) => state.setTweak);
|
const addTweaks = useTweaksStore((state) => state.setTweak);
|
||||||
|
|
@ -47,7 +47,12 @@ const ApiModal = forwardRef(
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState("0");
|
const [activeTab, setActiveTab] = useState("0");
|
||||||
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak);
|
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak);
|
||||||
const curl_code = getCurlCode(flow?.id, autoLogin, tweak);
|
const curl_code = getCurlCode(
|
||||||
|
flow?.id,
|
||||||
|
autoLogin,
|
||||||
|
tweak,
|
||||||
|
flow?.endpoint_name
|
||||||
|
);
|
||||||
const pythonCode = getPythonCode(flow?.name, tweak);
|
const pythonCode = getPythonCode(flow?.name, tweak);
|
||||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||||
const tweaksCode = buildTweaks(flow);
|
const tweaksCode = buildTweaks(flow);
|
||||||
|
|
@ -106,7 +111,7 @@ const ApiModal = forwardRef(
|
||||||
buildTweakObject(
|
buildTweakObject(
|
||||||
nodeId,
|
nodeId,
|
||||||
element.data.node.template[templateField].value,
|
element.data.node.template[templateField].value,
|
||||||
element.data.node.template[templateField],
|
element.data.node.template[templateField]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -123,7 +128,7 @@ const ApiModal = forwardRef(
|
||||||
async function buildTweakObject(
|
async function buildTweakObject(
|
||||||
tw: string,
|
tw: string,
|
||||||
changes: string | string[] | boolean | number | Object[] | Object,
|
changes: string | string[] | boolean | number | Object[] | Object,
|
||||||
template: TemplateVariableType,
|
template: TemplateVariableType
|
||||||
) {
|
) {
|
||||||
changes = getChangesType(changes, template);
|
changes = getChangesType(changes, template);
|
||||||
|
|
||||||
|
|
@ -161,7 +166,12 @@ const ApiModal = forwardRef(
|
||||||
|
|
||||||
const addCodes = (cloneTweak) => {
|
const addCodes = (cloneTweak) => {
|
||||||
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak);
|
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak);
|
||||||
const curl_code = getCurlCode(flow?.id, autoLogin, cloneTweak);
|
const curl_code = getCurlCode(
|
||||||
|
flow?.id,
|
||||||
|
autoLogin,
|
||||||
|
cloneTweak,
|
||||||
|
flow?.endpoint_name
|
||||||
|
);
|
||||||
const pythonCode = getPythonCode(flow?.name, cloneTweak);
|
const pythonCode = getPythonCode(flow?.name, cloneTweak);
|
||||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||||
|
|
||||||
|
|
@ -204,7 +214,7 @@ const ApiModal = forwardRef(
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ApiModal;
|
export default ApiModal;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default function DictAreaModal({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) ref.current = value;
|
if (value) ref.current = value;
|
||||||
}, [ref]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
||||||
|
|
|
||||||
|
|
@ -43,19 +43,23 @@ import BaseModal from "../baseModal";
|
||||||
const EditNodeModal = forwardRef(
|
const EditNodeModal = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
data,
|
|
||||||
nodeLength,
|
nodeLength,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
data,
|
||||||
}: {
|
}: {
|
||||||
data: NodeDataType;
|
|
||||||
nodeLength: number;
|
nodeLength: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
|
data: NodeDataType;
|
||||||
},
|
},
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [myData, setMyData] = useState(data);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
|
|
||||||
|
const dataFromStore = nodes.find((node) => node.id === node.id)?.data;
|
||||||
|
|
||||||
|
const [myData, setMyData] = useState(dataFromStore ?? data);
|
||||||
|
|
||||||
const edges = useFlowStore((state) => state.edges);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
|
@ -121,7 +125,7 @@ const EditNodeModal = forwardRef(
|
||||||
"edit-node-modal-box",
|
"edit-node-modal-box",
|
||||||
nodeLength > limitScrollFieldsModal
|
nodeLength > limitScrollFieldsModal
|
||||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||||
: "",
|
: ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{nodeLength > 0 && (
|
{nodeLength > 0 && (
|
||||||
|
|
@ -143,8 +147,8 @@ const EditNodeModal = forwardRef(
|
||||||
templateParam.charAt(0) !== "_" &&
|
templateParam.charAt(0) !== "_" &&
|
||||||
myData.node?.template[templateParam].show &&
|
myData.node?.template[templateParam].show &&
|
||||||
LANGFLOW_SUPPORTED_TYPES.has(
|
LANGFLOW_SUPPORTED_TYPES.has(
|
||||||
myData.node!.template[templateParam].type,
|
myData.node!.template[templateParam].type
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.map((templateParam, index) => {
|
.map((templateParam, index) => {
|
||||||
let id = {
|
let id = {
|
||||||
|
|
@ -166,8 +170,8 @@ const EditNodeModal = forwardRef(
|
||||||
myData.node?.template[templateParam]
|
myData.node?.template[templateParam]
|
||||||
.proxy,
|
.proxy,
|
||||||
}
|
}
|
||||||
: id,
|
: id
|
||||||
),
|
)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
|
|
@ -229,7 +233,7 @@ const EditNodeModal = forwardRef(
|
||||||
onChange={(value: string[]) => {
|
onChange={(value: string[]) => {
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
value,
|
value,
|
||||||
templateParam,
|
templateParam
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -253,11 +257,11 @@ const EditNodeModal = forwardRef(
|
||||||
.value ?? ""
|
.value ?? ""
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={(
|
||||||
value: string | string[],
|
value: string | string[]
|
||||||
) => {
|
) => {
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
value,
|
value,
|
||||||
templateParam,
|
templateParam
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -297,9 +301,7 @@ const EditNodeModal = forwardRef(
|
||||||
myData.node!.template[
|
myData.node!.template[
|
||||||
templateParam
|
templateParam
|
||||||
]?.value?.toString() === "{}"
|
]?.value?.toString() === "{}"
|
||||||
? {
|
? {}
|
||||||
// yourkey: "value",
|
|
||||||
}
|
|
||||||
: myData.node!.template[templateParam]
|
: myData.node!.template[templateParam]
|
||||||
.value
|
.value
|
||||||
}
|
}
|
||||||
|
|
@ -309,7 +311,7 @@ const EditNodeModal = forwardRef(
|
||||||
].value = newValue;
|
].value = newValue;
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
newValue,
|
newValue,
|
||||||
templateParam,
|
templateParam
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
id="editnode-div-dict-input"
|
id="editnode-div-dict-input"
|
||||||
|
|
@ -326,7 +328,7 @@ const EditNodeModal = forwardRef(
|
||||||
myData.node!.template[templateParam].value
|
myData.node!.template[templateParam].value
|
||||||
?.length > 1
|
?.length > 1
|
||||||
? "my-3"
|
? "my-3"
|
||||||
: "",
|
: ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<KeypairListComponent
|
<KeypairListComponent
|
||||||
|
|
@ -342,7 +344,7 @@ const EditNodeModal = forwardRef(
|
||||||
myData.node!.template[
|
myData.node!.template[
|
||||||
templateParam
|
templateParam
|
||||||
].value,
|
].value,
|
||||||
type(templateParam)!,
|
type(templateParam)!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
duplicateKey={errorDuplicateKey}
|
duplicateKey={errorDuplicateKey}
|
||||||
|
|
@ -353,11 +355,11 @@ const EditNodeModal = forwardRef(
|
||||||
templateParam
|
templateParam
|
||||||
].value = valueToNumbers;
|
].value = valueToNumbers;
|
||||||
setErrorDuplicateKey(
|
setErrorDuplicateKey(
|
||||||
hasDuplicateKeys(valueToNumbers),
|
hasDuplicateKeys(valueToNumbers)
|
||||||
);
|
);
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
valueToNumbers,
|
valueToNumbers,
|
||||||
templateParam,
|
templateParam
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
isList={
|
isList={
|
||||||
|
|
@ -387,7 +389,7 @@ const EditNodeModal = forwardRef(
|
||||||
setEnabled={(isEnabled) => {
|
setEnabled={(isEnabled) => {
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
isEnabled,
|
isEnabled,
|
||||||
templateParam,
|
templateParam
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
|
|
@ -630,7 +632,7 @@ const EditNodeModal = forwardRef(
|
||||||
</BaseModal.Footer>
|
</BaseModal.Footer>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default EditNodeModal;
|
export default EditNodeModal;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
|
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
import TableComponent from "../../components/tableComponent";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
|
||||||
|
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import useFlowStore from "../../stores/flowStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
import { FlowSettingsPropsType } from "../../types/components";
|
import { FlowSettingsPropsType } from "../../types/components";
|
||||||
import { FlowType, NodeDataType } from "../../types/flow";
|
import { FlowType, NodeDataType } from "../../types/flow";
|
||||||
import BaseModal from "../baseModal";
|
import BaseModal from "../baseModal";
|
||||||
import TableComponent from "../../components/tableComponent";
|
|
||||||
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
|
|
||||||
import {
|
|
||||||
ColDef,
|
|
||||||
ColGroupDef,
|
|
||||||
SizeColumnsToFitGridStrategy,
|
|
||||||
} from "ag-grid-community";
|
|
||||||
import useAlertStore from "../../stores/alertStore";
|
|
||||||
import useFlowStore from "../../stores/flowStore";
|
|
||||||
|
|
||||||
export default function FlowLogsModal({
|
export default function FlowLogsModal({
|
||||||
open,
|
open,
|
||||||
|
|
@ -41,8 +38,17 @@ export default function FlowLogsModal({
|
||||||
function handleClick(): void {
|
function handleClick(): void {
|
||||||
currentFlow!.name = name;
|
currentFlow!.name = name;
|
||||||
currentFlow!.description = description;
|
currentFlow!.description = description;
|
||||||
saveFlow(currentFlow!);
|
saveFlow(currentFlow!)
|
||||||
setOpen(false);
|
?.then(() => {
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
useAlertStore.getState().setErrorData({
|
||||||
|
title: "Error while saving changes",
|
||||||
|
list: [(err as AxiosError).response?.data.detail ?? ""],
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,7 +72,7 @@ export default function FlowLogsModal({
|
||||||
.some((template) => template["stream"] && template["stream"].value);
|
.some((template) => template["stream"] && template["stream"].value);
|
||||||
console.log(
|
console.log(
|
||||||
haStream,
|
haStream,
|
||||||
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template),
|
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template)
|
||||||
);
|
);
|
||||||
if (haStream) {
|
if (haStream) {
|
||||||
setNoticeData({
|
setNoticeData({
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import EditFlowSettings from "../../components/editFlowSettingsComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
import { FlowSettingsPropsType } from "../../types/components";
|
import { FlowSettingsPropsType } from "../../types/components";
|
||||||
import { FlowType } from "../../types/flow";
|
import { FlowType } from "../../types/flow";
|
||||||
|
|
@ -22,12 +23,23 @@ export default function FlowSettingsModal({
|
||||||
|
|
||||||
const [name, setName] = useState(currentFlow!.name);
|
const [name, setName] = useState(currentFlow!.name);
|
||||||
const [description, setDescription] = useState(currentFlow!.description);
|
const [description, setDescription] = useState(currentFlow!.description);
|
||||||
|
const [endpoint_name, setEndpointName] = useState(currentFlow!.endpoint_name);
|
||||||
|
|
||||||
function handleClick(): void {
|
function handleClick(): void {
|
||||||
currentFlow!.name = name;
|
currentFlow!.name = name;
|
||||||
currentFlow!.description = description;
|
currentFlow!.description = description;
|
||||||
saveFlow(currentFlow!);
|
currentFlow!.endpoint_name = endpoint_name;
|
||||||
setOpen(false);
|
saveFlow(currentFlow!)
|
||||||
|
?.then(() => {
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
useAlertStore.getState().setErrorData({
|
||||||
|
title: "Error while saving changes",
|
||||||
|
list: [(err as AxiosError).response?.data.detail ?? ""],
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nameLists, setNameList] = useState<string[]>([]);
|
const [nameLists, setNameList] = useState<string[]>([]);
|
||||||
|
|
@ -41,7 +53,7 @@ export default function FlowSettingsModal({
|
||||||
}, [flows]);
|
}, [flows]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal open={open} setOpen={setOpen} size="smaller">
|
<BaseModal open={open} setOpen={setOpen} size="smaller-h-full">
|
||||||
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
|
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
|
||||||
<span className="pr-2">Settings</span>
|
<span className="pr-2">Settings</span>
|
||||||
<IconComponent name="Settings2" className="mr-2 h-4 w-4 " />
|
<IconComponent name="Settings2" className="mr-2 h-4 w-4 " />
|
||||||
|
|
@ -51,8 +63,10 @@ export default function FlowSettingsModal({
|
||||||
invalidNameList={nameLists}
|
invalidNameList={nameLists}
|
||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
|
endpointName={endpoint_name}
|
||||||
setName={setName}
|
setName={setName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
|
setEndpointName={setEndpointName}
|
||||||
/>
|
/>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export default function GenericModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredWordsHighlight = matches.filter(
|
const filteredWordsHighlight = matches.filter(
|
||||||
(word) => !invalid_chars.includes(word),
|
(word) => !invalid_chars.includes(word)
|
||||||
);
|
);
|
||||||
|
|
||||||
setWordsHighlight(filteredWordsHighlight);
|
setWordsHighlight(filteredWordsHighlight);
|
||||||
|
|
@ -134,7 +134,7 @@ export default function GenericModal({
|
||||||
// to the first key of the custom_fields object
|
// to the first key of the custom_fields object
|
||||||
if (field_name === "") {
|
if (field_name === "") {
|
||||||
field_name = Array.isArray(
|
field_name = Array.isArray(
|
||||||
apiReturn.data?.frontend_node?.custom_fields?.[""],
|
apiReturn.data?.frontend_node?.custom_fields?.[""]
|
||||||
)
|
)
|
||||||
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
|
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
|
||||||
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
|
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
|
||||||
|
|
@ -166,7 +166,6 @@ export default function GenericModal({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
return setErrorData({
|
return setErrorData({
|
||||||
title: PROMPT_ERROR_ALERT,
|
title: PROMPT_ERROR_ALERT,
|
||||||
|
|
@ -210,7 +209,7 @@ export default function GenericModal({
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
!isEdit ? "rounded-lg border" : "",
|
!isEdit ? "rounded-lg border" : "",
|
||||||
"flex h-full w-full",
|
"flex h-full w-full"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export default function NodeToolbarComponent({
|
||||||
data.node.template[templateField].type === "Any" ||
|
data.node.template[templateField].type === "Any" ||
|
||||||
data.node.template[templateField].type === "int" ||
|
data.node.template[templateField].type === "int" ||
|
||||||
data.node.template[templateField].type === "dict" ||
|
data.node.template[templateField].type === "dict" ||
|
||||||
data.node.template[templateField].type === "NestedDict")
|
data.node.template[templateField].type === "NestedDict"),
|
||||||
).length;
|
).length;
|
||||||
const templates = useTypesStore((state) => state.templates);
|
const templates = useTypesStore((state) => state.templates);
|
||||||
const hasStore = useStoreStore((state) => state.hasStore);
|
const hasStore = useStoreStore((state) => state.hasStore);
|
||||||
|
|
@ -85,7 +85,7 @@ export default function NodeToolbarComponent({
|
||||||
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
||||||
const [showOverrideModal, setShowOverrideModal] = useState(false);
|
const [showOverrideModal, setShowOverrideModal] = useState(false);
|
||||||
const [flowComponent, setFlowComponent] = useState<FlowType>(
|
const [flowComponent, setFlowComponent] = useState<FlowType>(
|
||||||
createFlowComponent(cloneDeep(data), version)
|
createFlowComponent(cloneDeep(data), version),
|
||||||
);
|
);
|
||||||
|
|
||||||
const openInNewTab = (url) => {
|
const openInNewTab = (url) => {
|
||||||
|
|
@ -100,7 +100,7 @@ export default function NodeToolbarComponent({
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
const setLastCopiedSelection = useFlowStore(
|
const setLastCopiedSelection = useFlowStore(
|
||||||
(state) => state.setLastCopiedSelection
|
(state) => state.setLastCopiedSelection,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
|
|
@ -150,7 +150,7 @@ export default function NodeToolbarComponent({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
setNodes,
|
setNodes,
|
||||||
setEdges
|
setEdges,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "override":
|
case "override":
|
||||||
|
|
@ -174,7 +174,7 @@ export default function NodeToolbarComponent({
|
||||||
y: 10,
|
y: 10,
|
||||||
paneX: nodes.find((node) => node.id === data.id)?.position.x,
|
paneX: nodes.find((node) => node.id === data.id)?.position.x,
|
||||||
paneY: nodes.find((node) => node.id === data.id)?.position.y,
|
paneY: nodes.find((node) => node.id === data.id)?.position.y,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "update":
|
case "update":
|
||||||
|
|
@ -212,13 +212,13 @@ export default function NodeToolbarComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSaved = flows.some((flow) =>
|
const isSaved = flows.some((flow) =>
|
||||||
Object.values(flow).includes(data.node?.display_name!)
|
Object.values(flow).includes(data.node?.display_name!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
|
||||||
const handleOnNewValue = (
|
const handleOnNewValue = (
|
||||||
newValue: string | string[] | boolean | Object[]
|
newValue: string | string[] | boolean | Object[],
|
||||||
): void => {
|
): void => {
|
||||||
if (data.node!.template[name].value !== newValue) {
|
if (data.node!.template[name].value !== newValue) {
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
|
|
@ -401,7 +401,7 @@ export default function NodeToolbarComponent({
|
||||||
data-testid="save-button-modal"
|
data-testid="save-button-modal"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||||
hasCode ? " " : " rounded-l-md "
|
hasCode ? " " : " rounded-l-md ",
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -419,7 +419,7 @@ export default function NodeToolbarComponent({
|
||||||
<button
|
<button
|
||||||
data-testid="duplicate-button-modal"
|
data-testid="duplicate-button-modal"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -467,7 +467,7 @@ export default function NodeToolbarComponent({
|
||||||
<div
|
<div
|
||||||
data-testid="more-options-modal"
|
data-testid="more-options-modal"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default function PlaygroundPage() {
|
||||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||||
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
||||||
const setCurrentFlowId = useFlowsManagerStore(
|
const setCurrentFlowId = useFlowsManagerStore(
|
||||||
(state) => state.setCurrentFlowId,
|
(state) => state.setCurrentFlowId
|
||||||
);
|
);
|
||||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||||
|
|
@ -29,7 +29,6 @@ export default function PlaygroundPage() {
|
||||||
|
|
||||||
// Set flow tab id
|
// Set flow tab id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("id", id);
|
|
||||||
if (getFlowById(id!)) {
|
if (getFlowById(id!)) {
|
||||||
setCurrentFlowId(id!);
|
setCurrentFlowId(id!);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,6 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
status: BuildStatus,
|
status: BuildStatus,
|
||||||
runId: string,
|
runId: string,
|
||||||
) {
|
) {
|
||||||
console.log("handleBuildUpdate", vertexBuildData, status, runId);
|
|
||||||
if (vertexBuildData && vertexBuildData.inactivated_vertices) {
|
if (vertexBuildData && vertexBuildData.inactivated_vertices) {
|
||||||
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
|
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
|
||||||
get().updateBuildStatus(
|
get().updateBuildStatus(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { cloneDeep, debounce } from "lodash";
|
import { cloneDeep, debounce } from "lodash";
|
||||||
import { Edge, Node, Viewport, XYPosition } from "reactflow";
|
import { Edge, Node, Viewport, XYPosition } from "reactflow";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
@ -87,12 +86,12 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
if (dbData) {
|
if (dbData) {
|
||||||
const { data, flows } = processFlows(dbData, false);
|
const { data, flows } = processFlows(dbData, false);
|
||||||
const examples = flows.filter(
|
const examples = flows.filter(
|
||||||
(flow) => flow.folder_id === starterFolderId,
|
(flow) => flow.folder_id === starterFolderId
|
||||||
);
|
);
|
||||||
get().setExamples(examples);
|
get().setExamples(examples);
|
||||||
|
|
||||||
const flowsWithoutStarterFolder = flows.filter(
|
const flowsWithoutStarterFolder = flows.filter(
|
||||||
(flow) => flow.folder_id !== starterFolderId,
|
(flow) => flow.folder_id !== starterFolderId
|
||||||
);
|
);
|
||||||
|
|
||||||
get().setFlows(flowsWithoutStarterFolder);
|
get().setFlows(flowsWithoutStarterFolder);
|
||||||
|
|
@ -120,7 +119,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
if (get().currentFlow) {
|
if (get().currentFlow) {
|
||||||
get().saveFlow(
|
get().saveFlow(
|
||||||
{ ...get().currentFlow!, data: { nodes, edges, viewport } },
|
{ ...get().currentFlow!, data: { nodes, edges, viewport } },
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -146,7 +145,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
return updatedFlow;
|
return updatedFlow;
|
||||||
}
|
}
|
||||||
return flow;
|
return flow;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
//update tabs state
|
//update tabs state
|
||||||
|
|
||||||
|
|
@ -155,11 +154,9 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
useAlertStore.getState().setErrorData({
|
|
||||||
title: "Error while saving changes",
|
|
||||||
list: [(err as AxiosError).message],
|
|
||||||
});
|
|
||||||
reject(err);
|
reject(err);
|
||||||
|
set({ saveLoading: false });
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, SAVE_DEBOUNCE_TIME),
|
}, SAVE_DEBOUNCE_TIME),
|
||||||
|
|
@ -197,7 +194,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
flow?: FlowType,
|
flow?: FlowType,
|
||||||
override?: boolean,
|
override?: boolean,
|
||||||
position?: XYPosition,
|
position?: XYPosition,
|
||||||
fromDragAndDrop?: boolean,
|
fromDragAndDrop?: boolean
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
let flowData = flow
|
let flowData = flow
|
||||||
? processDataFromFlow(flow)
|
? processDataFromFlow(flow)
|
||||||
|
|
@ -212,7 +209,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
const newFlow = createNewFlow(
|
const newFlow = createNewFlow(
|
||||||
flowData!,
|
flowData!,
|
||||||
flow!,
|
flow!,
|
||||||
folder_id || my_collection_id!,
|
folder_id || my_collection_id!
|
||||||
);
|
);
|
||||||
const { id } = await saveFlowToDatabase(newFlow);
|
const { id } = await saveFlowToDatabase(newFlow);
|
||||||
newFlow.id = id;
|
newFlow.id = id;
|
||||||
|
|
@ -232,11 +229,10 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
// addFlowToLocalState(newFlow);
|
// addFlowToLocalState(newFlow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("folder id", folder_id);
|
|
||||||
const newFlow = createNewFlow(
|
const newFlow = createNewFlow(
|
||||||
flowData!,
|
flowData!,
|
||||||
flow!,
|
flow!,
|
||||||
folder_id || my_collection_id!,
|
folder_id || my_collection_id!
|
||||||
);
|
);
|
||||||
|
|
||||||
const newName = addVersionToDuplicates(newFlow, get().flows);
|
const newName = addVersionToDuplicates(newFlow, get().flows);
|
||||||
|
|
@ -271,8 +267,8 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
useFlowStore
|
useFlowStore
|
||||||
.getState()
|
.getState()
|
||||||
.paste(
|
.paste(
|
||||||
{ nodes: flowData?.nodes, edges: flowData?.edges },
|
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
|
||||||
position ?? { x: 10, y: 10 },
|
position ?? { x: 10, y: 10 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -282,7 +278,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
multipleDeleteFlowsComponents(id)
|
multipleDeleteFlowsComponents(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const { data, flows } = processFlows(
|
const { data, flows } = processFlows(
|
||||||
get().flows.filter((flow) => !id.includes(flow.id)),
|
get().flows.filter((flow) => !id.includes(flow.id))
|
||||||
);
|
);
|
||||||
get().setFlows(flows);
|
get().setFlows(flows);
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
|
|
@ -302,7 +298,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
deleteFlowFromDatabase(id)
|
deleteFlowFromDatabase(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const { data, flows } = processFlows(
|
const { data, flows } = processFlows(
|
||||||
get().flows.filter((flow) => flow.id !== id),
|
get().flows.filter((flow) => flow.id !== id)
|
||||||
);
|
);
|
||||||
get().setFlows(flows);
|
get().setFlows(flows);
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
|
|
@ -324,7 +320,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
let componentFlow = get().flows.find(
|
let componentFlow = get().flows.find(
|
||||||
(componentFlow) =>
|
(componentFlow) =>
|
||||||
componentFlow.is_component && componentFlow.name === key,
|
componentFlow.is_component && componentFlow.name === key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (componentFlow) {
|
if (componentFlow) {
|
||||||
|
|
@ -372,7 +368,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
fileData,
|
fileData,
|
||||||
undefined,
|
undefined,
|
||||||
position,
|
position,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
resolve(id);
|
resolve(id);
|
||||||
}
|
}
|
||||||
|
|
@ -413,7 +409,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
return get().addFlow(
|
return get().addFlow(
|
||||||
true,
|
true,
|
||||||
createFlowComponent(component, useDarkStore.getState().version),
|
createFlowComponent(component, useDarkStore.getState().version),
|
||||||
override,
|
override
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
takeSnapshot: () => {
|
takeSnapshot: () => {
|
||||||
|
|
@ -434,7 +430,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
if (pastLength > 0) {
|
if (pastLength > 0) {
|
||||||
past[currentFlowId] = past[currentFlowId].slice(
|
past[currentFlowId] = past[currentFlowId].slice(
|
||||||
pastLength - defaultOptions.maxHistorySize + 1,
|
pastLength - defaultOptions.maxHistorySize + 1,
|
||||||
pastLength,
|
pastLength
|
||||||
);
|
);
|
||||||
|
|
||||||
past[currentFlowId].push(newState);
|
past[currentFlowId].push(newState);
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,11 @@ export type IconComponentProps = {
|
||||||
export type InputProps = {
|
export type InputProps = {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
endpointName?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
setName?: (name: string) => void;
|
setName?: (name: string) => void;
|
||||||
setDescription?: (description: string) => void;
|
setDescription?: (description: string) => void;
|
||||||
|
setEndpointName?: (endpointName: string) => void;
|
||||||
invalidNameList?: string[];
|
invalidNameList?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -526,7 +528,7 @@ export type nodeToolbarPropsType = {
|
||||||
updateNodeCode?: (
|
updateNodeCode?: (
|
||||||
newNodeClass: APIClassType,
|
newNodeClass: APIClassType,
|
||||||
code: string,
|
code: string,
|
||||||
name: string,
|
name: string
|
||||||
) => void;
|
) => void;
|
||||||
setShowState: (show: boolean | SetStateAction<boolean>) => void;
|
setShowState: (show: boolean | SetStateAction<boolean>) => void;
|
||||||
isOutdated?: boolean;
|
isOutdated?: boolean;
|
||||||
|
|
@ -576,7 +578,7 @@ export type chatMessagePropsType = {
|
||||||
updateChat: (
|
updateChat: (
|
||||||
chat: ChatMessageType,
|
chat: ChatMessageType,
|
||||||
message: string,
|
message: string,
|
||||||
stream_url?: string,
|
stream_url?: string
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -668,12 +670,12 @@ export type codeTabsPropsType = {
|
||||||
value: string,
|
value: string,
|
||||||
node: NodeType,
|
node: NodeType,
|
||||||
template: TemplateVariableType,
|
template: TemplateVariableType,
|
||||||
tweak: tweakType,
|
tweak: tweakType
|
||||||
) => string;
|
) => string;
|
||||||
buildTweakObject?: (
|
buildTweakObject?: (
|
||||||
tw: string,
|
tw: string,
|
||||||
changes: string | string[] | boolean | number | Object[] | Object,
|
changes: string | string[] | boolean | number | Object[] | Object,
|
||||||
template: TemplateVariableType,
|
template: TemplateVariableType
|
||||||
) => Promise<string | void>;
|
) => Promise<string | void>;
|
||||||
};
|
};
|
||||||
activeTweaks?: boolean;
|
activeTweaks?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export type FlowType = {
|
||||||
id: string;
|
id: string;
|
||||||
data: ReactFlowJsonObject | null;
|
data: ReactFlowJsonObject | null;
|
||||||
description: string;
|
description: string;
|
||||||
|
endpoint_name?: string;
|
||||||
style?: FlowStyleType;
|
style?: FlowStyleType;
|
||||||
is_component?: boolean;
|
is_component?: boolean;
|
||||||
last_tested_version?: string;
|
last_tested_version?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
|
||||||
# we need to import tmpdir
|
# we need to import tmpdir
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -92,6 +94,12 @@ class Config:
|
||||||
result_backend = "redis://localhost:6379/0"
|
result_backend = "redis://localhost:6379/0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="load_flows_dir")
|
||||||
|
def load_flows_dir():
|
||||||
|
tempdir = tempfile.TemporaryDirectory()
|
||||||
|
yield tempdir.name
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="distributed_env")
|
@pytest.fixture(name="distributed_env")
|
||||||
def setup_env(monkeypatch):
|
def setup_env(monkeypatch):
|
||||||
monkeypatch.setenv("LANGFLOW_CACHE_TYPE", "redis")
|
monkeypatch.setenv("LANGFLOW_CACHE_TYPE", "redis")
|
||||||
|
|
@ -209,7 +217,7 @@ def json_vector_store():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="client", autouse=True)
|
@pytest.fixture(name="client", autouse=True)
|
||||||
def client_fixture(session: Session, monkeypatch, request):
|
def client_fixture(session: Session, monkeypatch, request, load_flows_dir):
|
||||||
# Set the database url to a test database
|
# Set the database url to a test database
|
||||||
if "noclient" in request.keywords:
|
if "noclient" in request.keywords:
|
||||||
yield
|
yield
|
||||||
|
|
@ -218,6 +226,11 @@ def client_fixture(session: Session, monkeypatch, request):
|
||||||
db_path = Path(db_dir) / "test.db"
|
db_path = Path(db_dir) / "test.db"
|
||||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||||
|
if "load_flows" in request.keywords:
|
||||||
|
shutil.copyfile(pytest.BASIC_EXAMPLE_PATH,
|
||||||
|
os.path.join(load_flows_dir, "c54f9130-f2fa-4a3e-b22a-3856d946351b.json"))
|
||||||
|
monkeypatch.setenv("LANGFLOW_LOAD_FLOWS_PATH", load_flows_dir)
|
||||||
|
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true")
|
||||||
|
|
||||||
from langflow.main import create_app
|
from langflow.main import create_app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import httpx
|
||||||
import pytest
|
import pytest
|
||||||
import respx
|
import respx
|
||||||
from httpx import Response
|
from httpx import Response
|
||||||
from langflow.components import (
|
|
||||||
data,
|
from langflow.components import data
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -34,6 +33,27 @@ async def test_successful_get_request(api_request):
|
||||||
assert result.data["result"] == mock_response
|
assert result.data["result"] == mock_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_curl(api_request):
|
||||||
|
# Arrange
|
||||||
|
field_value = (
|
||||||
|
"curl -X GET https://example.com/api/test -H 'Content-Type: application/json' -d '{\"key\": \"value\"}'"
|
||||||
|
)
|
||||||
|
build_config = {
|
||||||
|
"method": {"value": ""},
|
||||||
|
"urls": {"value": []},
|
||||||
|
"headers": {},
|
||||||
|
"body": {},
|
||||||
|
}
|
||||||
|
# Act
|
||||||
|
new_build_config = api_request.parse_curl(field_value, build_config.copy())
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert new_build_config["method"]["value"] == "GET"
|
||||||
|
assert new_build_config["urls"]["value"] == ["https://example.com/api/test"]
|
||||||
|
assert new_build_config["headers"]["value"] == {"Content-Type": "application/json"}
|
||||||
|
assert new_build_config["body"]["value"] == {"key": "value"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_failed_request(api_request):
|
async def test_failed_request(api_request):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
from typing import Optional, List
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
|
@ -11,6 +13,7 @@ from langflow.services.database.models.base import orjson_dumps
|
||||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
|
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
|
||||||
from langflow.services.database.utils import session_getter
|
from langflow.services.database.utils import session_getter
|
||||||
from langflow.services.deps import get_db_service
|
from langflow.services.deps import get_db_service
|
||||||
|
from langflow.services.settings.base import Settings
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|
@ -30,14 +33,14 @@ def json_style():
|
||||||
def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||||
flow = orjson.loads(json_flow)
|
flow = orjson.loads(json_flow)
|
||||||
data = flow["data"]
|
data = flow["data"]
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert response.json()["name"] == flow.name
|
assert response.json()["name"] == flow.name
|
||||||
assert response.json()["data"] == flow.data
|
assert response.json()["data"] == flow.data
|
||||||
# flow is optional so we can create a flow without a flow
|
# flow is optional so we can create a flow without a flow
|
||||||
flow = FlowCreate(name="Test Flow")
|
flow = FlowCreate(name="Test Flow")
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(exclude_unset=True), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert response.json()["name"] == flow.name
|
assert response.json()["name"] == flow.name
|
||||||
assert response.json()["data"] == flow.data
|
assert response.json()["data"] == flow.data
|
||||||
|
|
@ -46,14 +49,14 @@ def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_
|
||||||
def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||||
flow_data = orjson.loads(json_flow)
|
flow_data = orjson.loads(json_flow)
|
||||||
data = flow_data["data"]
|
data = flow_data["data"]
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert response.json()["name"] == flow.name
|
assert response.json()["name"] == flow.name
|
||||||
assert response.json()["data"] == flow.data
|
assert response.json()["data"] == flow.data
|
||||||
|
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert response.json()["name"] == flow.name
|
assert response.json()["name"] == flow.name
|
||||||
assert response.json()["data"] == flow.data
|
assert response.json()["data"] == flow.data
|
||||||
|
|
@ -67,7 +70,7 @@ def test_read_flow(client: TestClient, json_flow: str, active_user, logged_in_he
|
||||||
flow = orjson.loads(json_flow)
|
flow = orjson.loads(json_flow)
|
||||||
data = flow["data"]
|
data = flow["data"]
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
flow_id = response.json()["id"] # flow_id should be a UUID but is a string
|
flow_id = response.json()["id"] # flow_id should be a UUID but is a string
|
||||||
# turn it into a UUID
|
# turn it into a UUID
|
||||||
flow_id = UUID(flow_id)
|
flow_id = UUID(flow_id)
|
||||||
|
|
@ -83,7 +86,7 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_
|
||||||
data = flow["data"]
|
data = flow["data"]
|
||||||
|
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
|
|
||||||
flow_id = response.json()["id"]
|
flow_id = response.json()["id"]
|
||||||
updated_flow = FlowUpdate(
|
updated_flow = FlowUpdate(
|
||||||
|
|
@ -91,7 +94,7 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_
|
||||||
description="updated description",
|
description="updated description",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers)
|
response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["name"] == updated_flow.name
|
assert response.json()["name"] == updated_flow.name
|
||||||
|
|
@ -103,7 +106,7 @@ def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_
|
||||||
flow = orjson.loads(json_flow)
|
flow = orjson.loads(json_flow)
|
||||||
data = flow["data"]
|
data = flow["data"]
|
||||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||||
response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||||
flow_id = response.json()["id"]
|
flow_id = response.json()["id"]
|
||||||
response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
|
response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
@ -223,8 +226,8 @@ def test_update_flow_idempotency(client: TestClient, json_flow: str, active_user
|
||||||
response = client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers)
|
response = client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers)
|
||||||
flow_id = response.json()["id"]
|
flow_id = response.json()["id"]
|
||||||
updated_flow = FlowCreate(name="Updated Flow", description="description", data=data)
|
updated_flow = FlowCreate(name="Updated Flow", description="description", data=data)
|
||||||
response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers)
|
response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||||
response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers)
|
response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||||
assert response1.json() == response2.json()
|
assert response1.json() == response2.json()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -237,8 +240,8 @@ def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user
|
||||||
description="description",
|
description="description",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.dict(), headers=logged_in_headers)
|
response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404, response.text
|
||||||
|
|
||||||
|
|
||||||
def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
|
def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
|
||||||
|
|
@ -252,3 +255,13 @@ def test_read_only_starter_projects(client: TestClient, active_user, logged_in_h
|
||||||
starter_projects = load_starter_projects()
|
starter_projects = load_starter_projects()
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert len(response.json()) == len(starter_projects)
|
assert len(response.json()) == len(starter_projects)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.load_flows
|
||||||
|
def test_load_flows(client: TestClient, load_flows_dir):
|
||||||
|
client.get("/api/v1/auto_login")
|
||||||
|
response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["name"] == "BasicExample"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -652,7 +652,7 @@ def test_invalid_flow_id(client, created_api_key):
|
||||||
headers = {"x-api-key": created_api_key.api_key}
|
headers = {"x-api-key": created_api_key.api_key}
|
||||||
flow_id = "invalid-flow-id"
|
flow_id = "invalid-flow-id"
|
||||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text
|
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text
|
||||||
headers = {"x-api-key": created_api_key.api_key}
|
headers = {"x-api-key": created_api_key.api_key}
|
||||||
flow_id = UUID(int=0)
|
flow_id = UUID(int=0)
|
||||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue