Merge remote-tracking branch 'origin/dev' into two_edges

This commit is contained in:
ogabrielluiz 2024-05-30 14:48:38 -03:00
commit d0829f5dbe
81 changed files with 1127 additions and 1069 deletions

View file

@ -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
View file

@ -1,21 +1,49 @@
<!-- markdownlint-disable MD030 --> <!-- markdownlint-disable MD030 -->
# [![Langflow](https://github.com/langflow-ai/langflow/blob/dev/docs/static/img/hero.png)](https://www.langflow.org) # [![Langflow](./docs/static/img/hero.png)](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.
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](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:
[![Deploy 1.0 Preview on Railway](https://railway.app/button.svg)](https://railway.app/template/UsJ1uB?referralCode=MnPSdg)
Or this one to deploy Langflow 0.6.x:
[![Deploy on Railway](https://railway.app/button.svg)](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.
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](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:
[![Deploy 1.0 Preview on Railway](https://railway.app/button.svg)](https://railway.app/template/UsJ1uB?referralCode=MnPSdg)
Or this one to deploy Langflow 0.6.x:
[![Deploy on Railway](https://railway.app/button.svg)](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.

View file

@ -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

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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";

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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

View file

@ -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" }}
/> />

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 MiB

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

Binary file not shown.

BIN
docs/static/videos/combine_text.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/create_record.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/pass.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/store_message.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/sub_flow.mp4 vendored Normal file

Binary file not shown.

BIN
docs/static/videos/text_operator.mp4 vendored Normal file

Binary file not shown.

42
poetry.lock generated
View file

@ -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"

View file

@ -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 = [

View file

@ -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"}

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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:

View file

@ -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)

View file

@ -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(

View 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,
)

View file

@ -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

View file

@ -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":

View file

@ -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:

View file

@ -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")

View file

@ -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):

View file

@ -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)},

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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():

View 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]

View file

@ -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"

View file

@ -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]

View file

@ -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();

View file

@ -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

View file

@ -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(

View file

@ -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>
)}
</> </>
); );
}; };

View file

@ -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);

View file

@ -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) => ({

View file

@ -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 };

View file

@ -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}

View file

@ -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;

View file

@ -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);

View file

@ -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>'\\` : ""
} }

View file

@ -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;

View file

@ -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}>

View file

@ -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;

View file

@ -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({

View file

@ -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>

View file

@ -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 ? (

View file

@ -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

View file

@ -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 {

View file

@ -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(

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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):

View file

@ -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"

View file

@ -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)