diff --git a/.github/actions/poetry_caching/action.yml b/.github/actions/poetry_caching/action.yml index fb76b1723..e185e7094 100644 --- a/.github/actions/poetry_caching/action.yml +++ b/.github/actions/poetry_caching/action.yml @@ -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 diff --git a/README.md b/README.md index 626a472dd..80b42c26d 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,49 @@ -# [![Langflow](https://github.com/langflow-ai/langflow/blob/dev/docs/static/img/hero.png)](https://www.langflow.org) +# [![Langflow](./docs/static/img/hero.png)](https://www.langflow.org) -### [Langflow](https://www.langflow.org) is a new, visual way to build, iterate and deploy AI apps. +

+ A visual framework for building Gen-AI and RAG apps with LangChain +

+

+ Open-source, Python-powered, fully customizable, LLM and vector store agnostic +

-# ⚑️ Documentation and Community +

+ Docs - + Join our Discord - + Follow us on X - + Live demo +

-- [Documentation](https://docs.langflow.org) -- [Discord](https://discord.com/invite/EqksyE2EX9) +

+ + + + + + +

-# πŸ“¦ Installation +

+ Your GIF +

+ +# πŸ“ 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. + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial_spot.md) + +## Deploy on Railway + +Use this template to deploy Langflow 1.0 Preview on Railway: + +[![Deploy 1.0 Preview on Railway](https://railway.app/button.svg)](https://railway.app/template/UsJ1uB?referralCode=MnPSdg) + +Or this one to deploy Langflow 0.6.x: + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg) + +## Deploy on Render + + +Deploy to Render + + # πŸ–₯️ 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. - -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial_spot.md) - -## Deploy on Railway - -Use this template to deploy Langflow 1.0 Preview on Railway: - -[![Deploy 1.0 Preview on Railway](https://railway.app/button.svg)](https://railway.app/template/UsJ1uB?referralCode=MnPSdg) - -Or this one to deploy Langflow 0.6.x: - -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg) - -## Deploy on Render - - -Deploy to Render - - -# πŸ‘‹ 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. diff --git a/docs/docs/administration/custom-component.mdx b/docs/docs/administration/custom-component.mdx index e82c56851..02a137d07 100644 --- a/docs/docs/administration/custom-component.mdx +++ b/docs/docs/administration/custom-component.mdx @@ -74,11 +74,6 @@ class DocumentProcessor(CustomComponent): - - Check out [FlowRunner Component](../examples/flow-runner) for a more complex - example. - - --- ## Rules diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index 828677310..1880fcb0d 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -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. - - Check out the [FlowRunner](../examples/flow-runner) example to understand how to call a flow from a custom component. - - diff --git a/docs/docs/examples/buffer-memory.mdx b/docs/docs/examples/buffer-memory.mdx deleted file mode 100644 index b196f9031..000000000 --- a/docs/docs/examples/buffer-memory.mdx +++ /dev/null @@ -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"; - - - -#### Download Flow - - - -- [`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) - - diff --git a/docs/docs/examples/chat-memory.mdx b/docs/docs/examples/chat-memory.mdx new file mode 100644 index 000000000..d9b7d2e20 --- /dev/null +++ b/docs/docs/examples/chat-memory.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/combine-text.mdx b/docs/docs/examples/combine-text.mdx new file mode 100644 index 000000000..0d7524a5b --- /dev/null +++ b/docs/docs/examples/combine-text.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/conversation-chain.mdx b/docs/docs/examples/conversation-chain.mdx deleted file mode 100644 index 294d1b440..000000000 --- a/docs/docs/examples/conversation-chain.mdx +++ /dev/null @@ -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. - - - -Make sure to always get the API key from the provider. - - - -## ⛓️ Langflow Example - -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; - - - -#### Download Flow - - - -- [`ConversationChain`](https://python.langchain.com/docs/modules/chains/) -- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai) - - diff --git a/docs/docs/examples/create-record.mdx b/docs/docs/examples/create-record.mdx new file mode 100644 index 000000000..f94ba84bd --- /dev/null +++ b/docs/docs/examples/create-record.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/csv-loader.mdx b/docs/docs/examples/csv-loader.mdx deleted file mode 100644 index 25f3bb444..000000000 --- a/docs/docs/examples/csv-loader.mdx +++ /dev/null @@ -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. - - - 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). - - - - Once you build this flow, ask questions about the data in the chat interface - (e.g., number of rows or columns). - - -## ⛓️ Langflow Example - -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; - - - -#### Download Flow - - - -- [`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) - - diff --git a/docs/docs/examples/flow-runner.mdx b/docs/docs/examples/flow-runner.mdx deleted file mode 100644 index fda7a8d39..000000000 --- a/docs/docs/examples/flow-runner.mdx +++ /dev/null @@ -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". - - - -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. - -
- -Example 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): - 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)) - -``` - -
- - - -```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). - - - 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. - - ---- - -```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. - - - Make sure that the field type is _`str`_ and _`options`_ values are strings. - - - - -Done! This is what our script and custom component looks like: - -
- - - - - -
- -import ZoomableImage from "/src/theme/ZoomableImage.js"; -import Admonition from "@theme/Admonition"; diff --git a/docs/docs/examples/pass.mdx b/docs/docs/examples/pass.mdx new file mode 100644 index 000000000..cdf1858d5 --- /dev/null +++ b/docs/docs/examples/pass.mdx @@ -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! + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/python-function.mdx b/docs/docs/examples/python-function.mdx deleted file mode 100644 index 2bb4b93e1..000000000 --- a/docs/docs/examples/python-function.mdx +++ /dev/null @@ -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 -``` - - - 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. - - -The `AgentInitializer` component is a quick way to construct an agent from the model and tools. - - - 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). - - -## ⛓️ Langflow Example - -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; - - - -#### Download Flow - - - -- [`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/) - - diff --git a/docs/docs/examples/searchapi-tool.mdx b/docs/docs/examples/searchapi-tool.mdx deleted file mode 100644 index d3cb4734a..000000000 --- a/docs/docs/examples/searchapi-tool.mdx +++ /dev/null @@ -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. - - - To use the SearchApi, you must first obtain an API key by registering at [SearchApi's website](https://www.searchapi.io/). - - -In the given example, we specify `engine` as `youtube_transcripts` and provide a `video_id`. - - - All engines and parameters can be found in [SearchApi documentation](https://www.searchapi.io/docs/google). - - -The `RetrievalQA` chain processes a `Document` along with a user's question to return an answer. - - - In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the - LLM, but feel free to experiment with other Language Models! - - -The `RetrievalQA` takes `CombineDocsChain` and `SearchApi` tool as inputs, using the tool as a `Document` to answer questions. - - - Learn more about the SearchApi - [here](https://python.langchain.com/docs/integrations/tools/searchapi). - - -## ⛓️ Langflow Example - -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; - - - -#### Download Flow - - - -- [`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) - - \ No newline at end of file diff --git a/docs/docs/examples/serp-api-tool.mdx b/docs/docs/examples/serp-api-tool.mdx deleted file mode 100644 index 175b6f1be..000000000 --- a/docs/docs/examples/serp-api-tool.mdx +++ /dev/null @@ -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. - - - 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. - - -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. - - - In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the - LLM, but feel free to experiment with other Language Models! - - -The `ZeroShotAgent` takes the `LLMChain` and the `Search` tool as inputs, using the tool to find information when necessary. - - - Learn more about the Serp API - [here](https://python.langchain.com/docs/integrations/providers/serpapi ). - - -## ⛓️ Langflow Example - -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; - - - -#### Download Flow - - - -- [`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) - - diff --git a/docs/docs/examples/store-message.mdx b/docs/docs/examples/store-message.mdx new file mode 100644 index 000000000..610bf645c --- /dev/null +++ b/docs/docs/examples/store-message.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/sub-flow.mdx b/docs/docs/examples/sub-flow.mdx new file mode 100644 index 000000000..ae7e5c9da --- /dev/null +++ b/docs/docs/examples/sub-flow.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/examples/text-operator.mdx b/docs/docs/examples/text-operator.mdx new file mode 100644 index 000000000..5637dbc79 --- /dev/null +++ b/docs/docs/examples/text-operator.mdx @@ -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. + +
+ +
\ No newline at end of file diff --git a/docs/docs/getting-started/install-langflow.mdx b/docs/docs/getting-started/install-langflow.mdx index d78514909..836645a1e 100644 --- a/docs/docs/getting-started/install-langflow.mdx +++ b/docs/docs/getting-started/install-langflow.mdx @@ -9,7 +9,7 @@ import Admonition from "@theme/Admonition"; Langflow v1.0 alpha is also available in HuggingFace Spaces. [Clone the space using this link](https://huggingface.co/spaces/Langflow/Langflow-Preview?duplicate=true), to create your own Langflow workspace in minutes. -Langflow 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 diff --git a/docs/docs/integrations/notion/intro.md b/docs/docs/integrations/notion/intro.md index ec8738dc7..11b8b7823 100644 --- a/docs/docs/integrations/notion/intro.md +++ b/docs/docs/integrations/notion/intro.md @@ -10,8 +10,8 @@ The Notion integration in Langflow enables seamless connectivity with Notion dat diff --git a/docs/sidebars.js b/docs/sidebars.js index 2b891b589..d3f4f2671 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -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", ], }, { diff --git a/docs/static/img/langflow_basic_howto.gif b/docs/static/img/langflow_basic_howto.gif new file mode 100644 index 000000000..023a294e0 Binary files /dev/null and b/docs/static/img/langflow_basic_howto.gif differ diff --git a/docs/static/img/notion/notion_bundle.jpg b/docs/static/img/notion/notion_bundle.jpg new file mode 100644 index 000000000..b6dc62da7 Binary files /dev/null and b/docs/static/img/notion/notion_bundle.jpg differ diff --git a/docs/static/videos/chat_memory.mp4 b/docs/static/videos/chat_memory.mp4 new file mode 100644 index 000000000..ffed26a74 Binary files /dev/null and b/docs/static/videos/chat_memory.mp4 differ diff --git a/docs/static/videos/combine_text.mp4 b/docs/static/videos/combine_text.mp4 new file mode 100644 index 000000000..7e48303c2 Binary files /dev/null and b/docs/static/videos/combine_text.mp4 differ diff --git a/docs/static/videos/create_record.mp4 b/docs/static/videos/create_record.mp4 new file mode 100644 index 000000000..558f702e3 Binary files /dev/null and b/docs/static/videos/create_record.mp4 differ diff --git a/docs/static/videos/pass.mp4 b/docs/static/videos/pass.mp4 new file mode 100644 index 000000000..bb062364e Binary files /dev/null and b/docs/static/videos/pass.mp4 differ diff --git a/docs/static/videos/store_message.mp4 b/docs/static/videos/store_message.mp4 new file mode 100644 index 000000000..c8352da0e Binary files /dev/null and b/docs/static/videos/store_message.mp4 differ diff --git a/docs/static/videos/sub_flow.mp4 b/docs/static/videos/sub_flow.mp4 new file mode 100644 index 000000000..24222e815 Binary files /dev/null and b/docs/static/videos/sub_flow.mp4 differ diff --git a/docs/static/videos/text_operator.mp4 b/docs/static/videos/text_operator.mp4 new file mode 100644 index 000000000..3124e6bc4 Binary files /dev/null and b/docs/static/videos/text_operator.mp4 differ diff --git a/poetry.lock b/poetry.lock index 4fe8ad926..ebc123a67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index b360e08f8..bd0f46420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] maintainers = [ diff --git a/src/backend/base/langflow/alembic/script.py.mako b/src/backend/base/langflow/alembic/script.py.mako index bc9bca83a..6086a860c 100644 --- a/src/backend/base/langflow/alembic/script.py.mako +++ b/src/backend/base/langflow/alembic/script.py.mako @@ -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"} diff --git a/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py b/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py new file mode 100644 index 000000000..0feec1b8b --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py @@ -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 ### diff --git a/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py b/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py new file mode 100644 index 000000000..699df1437 --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py @@ -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 ### diff --git a/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py b/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py new file mode 100644 index 000000000..3d6dd604c --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py @@ -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 ### diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 006099b15..26633082e 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -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: diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index ce7d34cf6..44b1fa275 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -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) diff --git a/src/backend/base/langflow/api/v1/login.py b/src/backend/base/langflow/api/v1/login.py index 3851bbd2d..cde6bd28b 100644 --- a/src/backend/base/langflow/api/v1/login.py +++ b/src/backend/base/langflow/api/v1/login.py @@ -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( diff --git a/src/backend/base/langflow/base/curl/__init__.py b/src/backend/base/langflow/base/curl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/base/langflow/base/curl/parse.py b/src/backend/base/langflow/base/curl/parse.py new file mode 100644 index 000000000..c86638306 --- /dev/null +++ b/src/backend/base/langflow/base/curl/parse.py @@ -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, + ) diff --git a/src/backend/base/langflow/components/data/APIRequest.py b/src/backend/base/langflow/components/data/APIRequest.py index f4cf476f0..2065f90c7 100644 --- a/src/backend/base/langflow/components/data/APIRequest.py +++ b/src/backend/base/langflow/components/data/APIRequest.py @@ -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 diff --git a/src/backend/base/langflow/components/models/OllamaModel.py b/src/backend/base/langflow/components/models/OllamaModel.py index 5806ad770..f591e4a5c 100644 --- a/src/backend/base/langflow/components/models/OllamaModel.py +++ b/src/backend/base/langflow/components/models/OllamaModel.py @@ -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": diff --git a/src/backend/base/langflow/components/vectorstores/Qdrant.py b/src/backend/base/langflow/components/vectorstores/Qdrant.py index dabaa17fc..794e282db 100644 --- a/src/backend/base/langflow/components/vectorstores/Qdrant.py +++ b/src/backend/base/langflow/components/vectorstores/Qdrant.py @@ -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: diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index 3066e2909..27574950c 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -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") diff --git a/src/backend/base/langflow/main.py b/src/backend/base/langflow/main.py index 07ecae396..93d0e7f04 100644 --- a/src/backend/base/langflow/main.py +++ b/src/backend/base/langflow/main.py @@ -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): diff --git a/src/backend/base/langflow/services/auth/utils.py b/src/backend/base/langflow/services/auth/utils.py index f8396077c..0e0aead88 100644 --- a/src/backend/base/langflow/services/auth/utils.py +++ b/src/backend/base/langflow/services/auth/utils.py @@ -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)}, diff --git a/src/backend/base/langflow/services/database/models/flow/model.py b/src/backend/base/langflow/services/database/models/flow/model.py index 17b5e8931..ddfa98e9c 100644 --- a/src/backend/base/langflow/services/database/models/flow/model.py +++ b/src/backend/base/langflow/services/database/models/flow/model.py @@ -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 diff --git a/src/backend/base/langflow/services/database/models/folder/model.py b/src/backend/base/langflow/services/database/models/folder/model.py index 6ce038c63..dc2dfaa80 100644 --- a/src/backend/base/langflow/services/database/models/folder/model.py +++ b/src/backend/base/langflow/services/database/models/folder/model.py @@ -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 diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index f7c6440f2..0f9d0d029 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -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" diff --git a/src/backend/base/langflow/services/settings/service.py b/src/backend/base/langflow/services/settings/service.py index f7ef2980d..95088e829 100644 --- a/src/backend/base/langflow/services/settings/service.py +++ b/src/backend/base/langflow/services/settings/service.py @@ -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(): diff --git a/src/backend/base/langflow/utils/migration.py b/src/backend/base/langflow/utils/migration.py new file mode 100644 index 000000000..b85522c5b --- /dev/null +++ b/src/backend/base/langflow/utils/migration.py @@ -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] diff --git a/src/backend/base/poetry.lock b/src/backend/base/poetry.lock index c1cde8032..cb43dd88f 100644 --- a/src/backend/base/poetry.lock +++ b/src/backend/base/poetry.lock @@ -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" diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index 8a1acbcd7..96d7434b7 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -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 "] 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] diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 510500e2b..3d144f194 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -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(); diff --git a/src/frontend/src/components/codeTabsComponent/index.tsx b/src/frontend/src/components/codeTabsComponent/index.tsx index 1e745f950..0a0b691c8 100644 --- a/src/frontend/src/components/codeTabsComponent/index.tsx +++ b/src/frontend/src/components/codeTabsComponent/index.tsx @@ -841,9 +841,7 @@ export default function CodeTabsComponent({ node.data.node!.template[ templateField ].value?.toString() === "{}" - ? { - // yourkey: "value", - } + ? {} : node.data.node! .template[ templateField diff --git a/src/frontend/src/components/dictComponent/index.tsx b/src/frontend/src/components/dictComponent/index.tsx index 2cf622e93..5161135ed 100644 --- a/src/frontend/src/components/dictComponent/index.tsx +++ b/src/frontend/src/components/dictComponent/index.tsx @@ -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 (
= ({ 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) => { const { value } = event.target; @@ -29,6 +32,18 @@ export const EditFlowSettings: React.FC = ({ setDescription!(event.target.value); }; + const handleEndpointNameChange = (event: ChangeEvent) => { + // 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 = ({ )} + {setEndpointName && ( + + )} ); }; diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index fb83b18fb..15a79ec1d 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -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); diff --git a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx index 8a1dc4a29..ae40c4613 100644 --- a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx +++ b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx @@ -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 ( <>
@@ -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 (
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) => ({ diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index b6dc2ef86..2d0f46da3 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -61,7 +61,7 @@ export async function sendAll(data: sendAllProps) { } export async function postValidateCode( - code: string, + code: string ): Promise> { 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> { 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 { 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> { 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> { 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> { 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> { // 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> { 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> { export async function getUsersPage( skip: number, - limit: number, + limit: number ): Promise> { 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 { 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 { 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 { 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> { // 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> { // 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> { const config = {}; config["params"] = { flow_id: flowId }; @@ -999,7 +1000,7 @@ export async function deleteFlowPool( } export async function multipleDeleteFlowsComponents( - flowIds: string[], + flowIds: string[] ): Promise> { 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; columns: Array }> { 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; columns: Array }> { const config = {}; config["params"] = { flow_id: id }; diff --git a/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx b/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx index fe84645a6..80d85afdc 100644 --- a/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx @@ -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 => { 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!) ); }} > @@ -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} diff --git a/src/frontend/src/customNodes/genericNode/index.tsx b/src/frontend/src/customNodes/genericNode/index.tsx index 9e22d9ac1..ef73e72cc 100644 --- a/src/frontend/src/customNodes/genericNode/index.tsx +++ b/src/frontend/src/customNodes/genericNode/index.tsx @@ -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; diff --git a/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx b/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx index 935db2644..fceb2f087 100644 --- a/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx +++ b/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx @@ -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); diff --git a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx index e8eed8f8a..48be0ceeb 100644 --- a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx @@ -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: '\\` : "" } diff --git a/src/frontend/src/modals/apiModal/views/index.tsx b/src/frontend/src/modals/apiModal/views/index.tsx index 6e614e03f..a0a614b06 100644 --- a/src/frontend/src/modals/apiModal/views/index.tsx +++ b/src/frontend/src/modals/apiModal/views/index.tsx @@ -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( ); - }, + } ); export default ApiModal; diff --git a/src/frontend/src/modals/dictAreaModal/index.tsx b/src/frontend/src/modals/dictAreaModal/index.tsx index e12575335..7aee82d14 100644 --- a/src/frontend/src/modals/dictAreaModal/index.tsx +++ b/src/frontend/src/modals/dictAreaModal/index.tsx @@ -29,7 +29,7 @@ export default function DictAreaModal({ useEffect(() => { if (value) ref.current = value; - }, [ref]); + }, [value]); return ( diff --git a/src/frontend/src/modals/editNodeModal/index.tsx b/src/frontend/src/modals/editNodeModal/index.tsx index 0db4bdcf5..775f00f03 100644 --- a/src/frontend/src/modals/editNodeModal/index.tsx +++ b/src/frontend/src/modals/editNodeModal/index.tsx @@ -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 ( { 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" - : "", + : "" )} > { handleOnNewValue( isEnabled, - templateParam, + templateParam ); }} size="small" @@ -630,7 +632,7 @@ const EditNodeModal = forwardRef( ); - }, + } ); export default EditNodeModal; diff --git a/src/frontend/src/modals/flowLogsModal/index.tsx b/src/frontend/src/modals/flowLogsModal/index.tsx index 70a8802c2..6f2a09633 100644 --- a/src/frontend/src/modals/flowLogsModal/index.tsx +++ b/src/frontend/src/modals/flowLogsModal/index.tsx @@ -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({ diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx index 1daeb0fe1..ea4ad09cb 100644 --- a/src/frontend/src/modals/flowSettingsModal/index.tsx +++ b/src/frontend/src/modals/flowSettingsModal/index.tsx @@ -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([]); @@ -41,7 +53,7 @@ export default function FlowSettingsModal({ }, [flows]); return ( - + Settings @@ -51,8 +63,10 @@ export default function FlowSettingsModal({ invalidNameList={nameLists} name={name} description={description} + endpointName={endpoint_name} setName={setName} setDescription={setDescription} + setEndpointName={setEndpointName} /> diff --git a/src/frontend/src/modals/genericModal/index.tsx b/src/frontend/src/modals/genericModal/index.tsx index 0e111c548..369ccbb96 100644 --- a/src/frontend/src/modals/genericModal/index.tsx +++ b/src/frontend/src/modals/genericModal/index.tsx @@ -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({
{type === TypeModal.PROMPT && isEdit && !readonly ? ( diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 7d1d33d5f..587dc6d73 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -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( - 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({