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