Merge branch 'python_custom_node_component' of github.com:logspace-ai/langflow into python_custom_node_component

This commit is contained in:
Lucas Oliveira 2023-07-28 14:49:11 -03:00
commit 1ea05b3584
92 changed files with 4582 additions and 1520 deletions

View file

@ -1,6 +1,7 @@
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
import Admonition from "@theme/Admonition";
# Chains
@ -12,22 +13,23 @@ Chains, in the context of language models, refer to a series of calls made to a
The `CombineDocsChain` incorporates methods to combine or aggregate loaded documents for question-answering functionality.
:::info
<Admonition type="info">
Works as a proxy of LangChains [documents](https://python.langchain.com/docs/modules/chains/document/) chains generated by the `load_qa_chain` function.
:::
</Admonition>
**Params**
- **LLM:** Language Model to use in the chain.
- **chain_type:** The chain type to be used. Each one of them applies a different “combination strategy”.
- **stuff**: The stuff [documents](https://python.langchain.com/docs/modules/chains/document/stuff) chain (“stuff" as in "to stuff" or "to fill") is the most straightforward of *the* document chains. It takes a list of documents, inserts them all into a prompt, and passes that prompt to an LLM. This chain is well-suited for applications where documents are small and only a few are passed in for most calls.
- **map_reduce**: The map-reduce [documents](https://python.langchain.com/docs/modules/chains/document/map_reduce) chain first applies an LLM chain to each document individually (the Map step), treating the chain output as a new document. It then passes all the new documents to a separate combined documents chain to get a single output (the Reduce step). It can optionally first compress or collapse the mapped documents to make sure that they fit in the combined documents chain (which will often pass them to an LLM). This compression step is performed recursively if necessary.
- **map_rerank**: The map re-rank [documents](https://python.langchain.com/docs/modules/chains/document/map_rerank) chain runs an initial prompt on each document that not only tries to complete a task but also gives a score for how certain it is in its answer. The highest-scoring response is returned.
- **refine**: The refine [documents](https://python.langchain.com/docs/modules/chains/document/refine) chain constructs a response by looping over the input documents and iteratively updating its answer. For each document, it passes all non-document inputs, the current document, and the latest intermediate answer to an LLM chain to get a new answer.
Since the Refine chain only passes a single document to the LLM at a time, it is well-suited for tasks that require analyzing more documents than can fit in the model's context. The obvious tradeoff is that this chain will make far more LLM calls than, for example, the Stuff documents chain. There are also certain tasks that are difficult to accomplish iteratively. For example, the Refine chain can perform poorly when documents frequently cross-reference one another or when a task requires detailed information from many documents.
- **stuff**: The stuff [documents](https://python.langchain.com/docs/modules/chains/document/stuff) chain (“stuff" as in "to stuff" or "to fill") is the most straightforward of _the_ document chains. It takes a list of documents, inserts them all into a prompt, and passes that prompt to an LLM. This chain is well-suited for applications where documents are small and only a few are passed in for most calls.
- **map_reduce**: The map-reduce [documents](https://python.langchain.com/docs/modules/chains/document/map_reduce) chain first applies an LLM chain to each document individually (the Map step), treating the chain output as a new document. It then passes all the new documents to a separate combined documents chain to get a single output (the Reduce step). It can optionally first compress or collapse the mapped documents to make sure that they fit in the combined documents chain (which will often pass them to an LLM). This compression step is performed recursively if necessary.
- **map_rerank**: The map re-rank [documents](https://python.langchain.com/docs/modules/chains/document/map_rerank) chain runs an initial prompt on each document that not only tries to complete a task but also gives a score for how certain it is in its answer. The highest-scoring response is returned.
- **refine**: The refine [documents](https://python.langchain.com/docs/modules/chains/document/refine) chain constructs a response by looping over the input documents and iteratively updating its answer. For each document, it passes all non-document inputs, the current document, and the latest intermediate answer to an LLM chain to get a new answer.
Since the Refine chain only passes a single document to the LLM at a time, it is well-suited for tasks that require analyzing more documents than can fit in the model's context. The obvious tradeoff is that this chain will make far more LLM calls than, for example, the Stuff documents chain. There are also certain tasks that are difficult to accomplish iteratively. For example, the Refine chain can perform poorly when documents frequently cross-reference one another or when a task requires detailed information from many documents.
---
@ -41,7 +43,7 @@ The `ConversationChain` is a straightforward chain for interactive conversations
- **Memory:** Default memory store.
- **input_key:** Used to specify the key under which the user input will be stored in the conversation memory. It allows you to provide the user's input to the chain for processing and generating a response.
- **output_key:** Used to specify the key under which the generated response will be stored in the conversation memory. It allows you to retrieve the response using the specified key.
- **verbose:** This parameter is used to control the level of detail in the output of the chain. When set to True, it will print out some internal states of the chain while it is being run, which can be helpful for debugging and understanding the chain's behavior. If set to False, it will suppress the verbose output — defaults to `False`.
- **verbose:** This parameter is used to control the level of detail in the output of the chain. When set to True, it will print out some internal states of the chain while it is being run, which can be helpful for debugging and understanding the chain's behavior. If set to False, it will suppress the verbose output — defaults to `False`.
---
@ -49,11 +51,11 @@ The `ConversationChain` is a straightforward chain for interactive conversations
The `ConversationalRetrievalChain` extracts information and provides answers by combining document search and question-answering abilities.
:::info
<Admonition type="info">
A retriever is a component that finds documents based on a query. It doesn't store the documents themselves, but it returns the ones that match the query.
:::
</Admonition >
**Params**
@ -61,12 +63,13 @@ A retriever is a component that finds documents based on a query. It doesn't sto
- **Memory:** Default memory store.
- **Retriever:** The retriever used to fetch relevant documents.
- **chain_type:** The chain type to be used. Each one of them applies a different “combination strategy”.
- **stuff**: The stuff [documents](https://python.langchain.com/docs/modules/chains/document/stuff) chain (“stuff" as in "to stuff" or "to fill") is the most straightforward of *the* document chains. It takes a list of documents, inserts them all into a prompt, and passes that prompt to an LLM. This chain is well-suited for applications where documents are small and only a few are passed in for most calls.
- **map_reduce**: The map-reduce [documents](https://python.langchain.com/docs/modules/chains/document/map_reduce) chain first applies an LLM chain to each document individually (the Map step), treating the chain output as a new document. It then passes all the new documents to a separate combined documents chain to get a single output (the Reduce step). It can optionally first compress or collapse the mapped documents to make sure that they fit in the combined documents chain (which will often pass them to an LLM). This compression step is performed recursively if necessary.
- **map_rerank**: The map re-rank [documents](https://python.langchain.com/docs/modules/chains/document/map_rerank) chain runs an initial prompt on each document that not only tries to complete a task but also gives a score for how certain it is in its answer. The highest-scoring response is returned.
- **refine**: The refine [documents](https://python.langchain.com/docs/modules/chains/document/refine) chain constructs a response by looping over the input documents and iteratively updating its answer. For each document, it passes all non-document inputs, the current document, and the latest intermediate answer to an LLM chain to get a new answer.
Since the Refine chain only passes a single document to the LLM at a time, it is well-suited for tasks that require analyzing more documents than can fit in the model's context. The obvious tradeoff is that this chain will make far more LLM calls than, for example, the Stuff documents chain. There are also certain tasks that are difficult to accomplish iteratively. For example, the Refine chain can perform poorly when documents frequently cross-reference one another or when a task requires detailed information from many documents.
- **stuff**: The stuff [documents](https://python.langchain.com/docs/modules/chains/document/stuff) chain (“stuff" as in "to stuff" or "to fill") is the most straightforward of _the_ document chains. It takes a list of documents, inserts them all into a prompt, and passes that prompt to an LLM. This chain is well-suited for applications where documents are small and only a few are passed in for most calls.
- **map_reduce**: The map-reduce [documents](https://python.langchain.com/docs/modules/chains/document/map_reduce) chain first applies an LLM chain to each document individually (the Map step), treating the chain output as a new document. It then passes all the new documents to a separate combined documents chain to get a single output (the Reduce step). It can optionally first compress or collapse the mapped documents to make sure that they fit in the combined documents chain (which will often pass them to an LLM). This compression step is performed recursively if necessary.
- **map_rerank**: The map re-rank [documents](https://python.langchain.com/docs/modules/chains/document/map_rerank) chain runs an initial prompt on each document that not only tries to complete a task but also gives a score for how certain it is in its answer. The highest-scoring response is returned.
- **refine**: The refine [documents](https://python.langchain.com/docs/modules/chains/document/refine) chain constructs a response by looping over the input documents and iteratively updating its answer. For each document, it passes all non-document inputs, the current document, and the latest intermediate answer to an LLM chain to get a new answer.
Since the Refine chain only passes a single document to the LLM at a time, it is well-suited for tasks that require analyzing more documents than can fit in the model's context. The obvious tradeoff is that this chain will make far more LLM calls than, for example, the Stuff documents chain. There are also certain tasks that are difficult to accomplish iteratively. For example, the Refine chain can perform poorly when documents frequently cross-reference one another or when a task requires detailed information from many documents.
- **return_source_documents:** Used to specify whether or not to include the source documents that were used to answer the question in the output. When set to `True`, source documents will be included in the output along with the generated answer. This can be useful for providing additional context or references to the user — defaults to `True`.
- **verbose:** Whether or not to run in verbose mode. In verbose mode, intermediate logs will be printed to the console — defaults to `False`.
@ -108,17 +111,17 @@ The `LLMMathChain` works by using the language model with an `LLMChain` to under
`RetrievalQA` is a chain used to find relevant documents or information to answer a given query. The retriever is responsible for returning the relevant documents based on the query, and the QA component then extracts the answer from those documents. The retrieval QA system combines the capabilities of both the retriever and the QA component to provide accurate and relevant answers to user queries.
:::info
<Admonition type="info">
A retriever is a component that finds documents based on a query. It doesn't store the documents themselves, but it returns the ones that match the query.
:::
</Admonition >
**Params**
- **Combine Documents Chain:** Chain to use to combine the documents.
- **Memory:** Default memory store.
- **Retriever:** The retriever used to fetch relevant documents.
- **Retriever:** The retriever used to fetch relevant documents.
- **input_key:** This parameter is used to specify the key in the input data that contains the question. It is used to retrieve the question from the input data and pass it to the question-answering model for generating the answer — defaults to `query`.
- **output_key:** This parameter is used to specify the key in the output data where the generated answer will be stored. It is used to retrieve the answer from the output data after the question-answering model has generated it — defaults to `result`.
- **return_source_documents:** Used to specify whether or not to include the source documents that were used to answer the question in the output. When set to `True`, source documents will be included in the output along with the generated answer. This can be useful for providing additional context or references to the user — defaults to `True`.
@ -134,4 +137,4 @@ The `SQLDatabaseChain` finds answers to questions using a SQL database. It works
- **Db:** SQL Database to connect to.
- **LLM:** Language Model to use in the chain.
- **Prompt:** Prompt template to translate natural language to SQL.
- **Prompt:** Prompt template to translate natural language to SQL.

View file

@ -0,0 +1,79 @@
import Admonition from "@theme/Admonition";
# Custom Component
Used to create a custom component. The code is the class that will be converted to a Custom Component with the fields and formatting you define.
**Params**
- **Code:** The Python code to define the component.
The code must be a class that inherits from the _`langflow.CustomComponent`_ class.
The type annotations of the _`build`_ instance method will be used to create the fields of the component.
| Supported types |
| --------------------------------------------------------- |
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
| _`langchain.chains.base.Chain`_ |
| _`langchain.PromptTemplate`_ |
| _`langchain.llms.base.BaseLLM`_ |
| _`langchain.Tool`_ |
| _`langchain.document_loaders.base.BaseLoader`_ |
| _`langchain.schema.Document`_ |
| _`langchain.text_splitters.TextSplitter`_ |
| _`langchain.vectorstores.base.VectorStore`_ |
| _`langchain.embeddings.base.Embeddings`_ |
| _`langchain.schema.BaseRetriever`_ |
The class can have a [_`build_config`_](focus://8) instance method, which is used to define configuration fields for the component.
The [_`build_config`_](focus://8) method should always return a dictionary with specific keys representing the field names and their corresponding configurations.
It must follow the format described below:
The top-level keys are the field names.
Their values are of type _`dict`_ with any of the following keys (all of them are **optional**):
| Key | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _`field_type: str`_ | The type of the field (can be any of the types supported by the _`build`_ method, passed as a string). |
| _`is_list: bool`_ | If the field is a list. |
| _`options: List[str]`_ | This defines the options to be displayed. The _`field_type`_ should invariably be _`str`_. If you set the _`value`_ attribute to one of the options, it will be selected by default. Having _`options`_ will display a dropdown menu. |
| _`multiline: bool`_ | When the field is a string, this would allow the user to open a text editor. |
| _`input_types: List[str]`_ | To be used when you want a _`str`_ field to have connectable handles. |
| _`display_name: str`_ | To define the name of the field. |
| _`advanced: bool`_ | To hide the field in default view. This is useful when a field is for advanced users or you simply want to remove it from view. |
| _`password: bool`_ | To mask the input text. This is used to allow the user to hide the values of the text (e.g. API keys). |
| _`required: bool`_ | To make the field required. |
| _`info: str`_ | To add a tooltip to the field. |
| _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is file. Defines which file types will be accepted. For example, json, yaml or yml. |
The CustomComponent class provides the following methods:
| Method name | Description |
| -------------- | ------------------------------------------------------------- |
| _`list_flows`_ | Returns a list of Flow objects with an _`id`_ and a _`name`_. |
| _`load_flow`_ | Loads a flow from the given _`id`_. |
### Usage
```python
# In the CustomComponent class
def build(self, flow_name: str)
# This is a list of Flow objects with an id and a name
flows = self.list_flows()
found_flow = next((f for f in flows if f.name == flow_name), None)
if not found_flow:
raise ValueError(f"Flow {flow_name} not found")
flow = self.load_flow(found_flow.id)
result = flow() # Run the flow
return result
```
<Admonition type="info" label="Tip">
[Learn more about Custom Components](../guidelines/custom-component)
</Admonition>

View file

@ -1,3 +1,5 @@
import Admonition from "@theme/Admonition";
# Prompts
A prompt refers to the input given to a language model. It is constructed from multiple components and can be parametrized using prompt templates. A prompt template is a reproducible way to generate prompts and allow for easy customization through input variables.
@ -8,8 +10,10 @@ A prompt refers to the input given to a language model. It is constructed from m
The `PromptTemplate` component allows users to create prompts and define variables that provide control over instructing the model. The template can take in a set of variables from the end user and generates the prompt once the conversation is initiated.
:::info
Once a variable is defined in the prompt template, it becomes a component input of its own. Check out [Prompt Customization](../guidelines/prompt-customization.mdx) to learn more.
:::
<Admonition type="info">
Once a variable is defined in the prompt template, it becomes a component
input of its own. Check out [Prompt
Customization](../guidelines/prompt-customization.mdx) to learn more.
</Admonition>
- **template:** Template used to format an individual request.
- **template:** Template used to format an individual request.

View file

@ -1,3 +1,5 @@
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.
@ -17,9 +19,10 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/Buffer_Memory.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`ConversationBufferMemory`](https://python.langchain.com/docs/modules/memory/how_to/buffer)
- [`ConversationChain`](https://python.langchain.com/docs/modules/chains/)
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
:::
</Admonition>

View file

@ -1,10 +1,14 @@
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.
:::info
<Admonition type="info">
Make sure to always get the API key from the provider.
:::
</Admonition>
## ⛓️ Langflow Example
@ -21,8 +25,9 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/Basic_Chat.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`ConversationChain`](https://python.langchain.com/docs/modules/chains/)
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
:::
</Admonition>

View file

@ -1,3 +1,5 @@
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:
@ -7,13 +9,18 @@ The `VectoStoreAgent` component retrieves information from one or more vector st
- 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.
:::info
The vector store is used for efficient semantic search, while `VectorStoreInfo` carries information about it, such as its name and description. Embeddings are a way to represent words, phrases, or any entities in a vector space. Learn more about them [here](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings).
:::
<Admonition type="info">
The vector store is used for efficient semantic search, while
`VectorStoreInfo` carries information about it, such as its name and
description. Embeddings are a way to represent words, phrases, or any entities
in a vector space. Learn more about them
[here](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings).
</Admonition>
:::tip
Once you build this flow, ask questions about the data in the chat interface (e.g., number of rows or columns).
:::
<Admonition type="tip">
Once you build this flow, ask questions about the data in the chat interface
(e.g., number of rows or columns).
</Admonition>
## ⛓️ Langflow Example
@ -30,7 +37,7 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/CSV_Loader.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`CSVLoader`](https://python.langchain.com/docs/modules/data_connection/document_loaders/integrations/csv)
- [`CharacterTextSplitter`](https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter)
@ -39,4 +46,5 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
- [`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://python.langchain.com/docs/modules/agents/toolkits/vectorstore)
:::
</Admonition>

View file

@ -0,0 +1,354 @@
---
description: Custom Components
hide_table_of_contents: true
---
import ZoomableImage from "/src/theme/ZoomableImage.js";
import Admonition from "@theme/Admonition";
# FlowRunner Component
The CustomComponent allows us to create new components that can interact with Langflow itself.
In this example, we are going to create a component that allows us to run other flows.
What we will see:
- How to list the flows in the collection using the _`list_flows`_ method.
- How to load a flow using the _`load_flow`_ method.
- Configure a field to be a dropdown menu using the _`options`_ parameter.
<CH.Scrollycoding rows={20} className={""}>
```python
from langflow import CustomComponent
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self):
...
def build(self):
...
```
This is the basic structure of a custom component.
---
```python
from langflow import CustomComponent
# focus
class FlowRunner(CustomComponent):
# focus
display_name = "Flow Runner"
# focus
description = "Run other flows"
def build_config(self):
...
def build(self):
...
```
So, let's start by adding the _`display_name`_ and a _`description`_.
---
```python
from langflow 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):
...
```
Now let's import _`Document`_ from the _`langchain.schema`_ module, which will be our return type for the _`build`_ method.
---
```python
from langflow 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:
...
```
Let's add the [parameters](focus://11[20:55]) and the [return type](focus://11[60:69]) to the _`build`_ method.
The parameters we added are:
- _`flow_name`_ is the name of the flow to be run.
- _`document`_ is what is going to be passed to the flow.
- We are using the _`Document`_ type, which will add a [handle](../guidelines/components) to the component.
---
```python
from langflow 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:
# focus
# List the flows
# focus
flows = self.list_flows()
```
Now we can start writing the code for the _`build`_ method.
We will start by listing the flows in the collection using the _`list_flows`_ method.
---
```python
from langflow 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()
# focus
# Get the flow that matches the selected name
# focus
flow = next(filter(lambda f: f.name == flow_name, flows))
```
We can then get the flow that matches the selected name.
<Admonition type="caution">
From version 0.4.0 names are unique, but in previous versions, they were not.
This might lead to unexpected results if you have flows with the same name.
</Admonition>
---
```python
from langflow 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
flow = next(filter(lambda f: f.name == flow_name, flows))
# focus
# Load the flow
# focus
tweaks = {}
# focus
flow = self.load_flow(flow.id, tweaks)
```
Now we can load the flow using the _`load_flow`_ method.
The _`tweaks`_ parameter is a dictionary that allows you to customize the flow.
You can find more information about it in the [features guidelines](../guidelines/features#code).
---
```python
from langflow 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
flow = next(filter(lambda f: f.name == flow_name, flows))
# Load the flow
tweaks = {}
flow = self.load_flow(flow.id, tweaks)
# focus
# Get the page_content from the document
# focus
page_content = document.page_content
```
Now we can get the _`page_content`_ from the document.
This is the content that will be passed to the flow and it depends on many factors.
In this example, we are using a Document because we can use a [Loader](../components/loaders) to load a Document but
then we'll have to process the page_content depending on what is the input of the flow we are running.
<Admonition type="tip">
One other approach would be to create another CustomComponent that builds a
dictionary and then we use that as the input for the FlowRunner.
</Admonition>
---
```python
from langflow 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
flow = next(filter(lambda f: f.name == flow_name, flows))
# Load the flow
tweaks = {}
flow = self.load_flow(flow.id, tweaks)
# Get the page_content from the document
page_content = document.page_content
# Use it in the flow
result = flow(page_content)
return Document(page_content=str(result))
```
Finally, we can use the _`page_content`_ in the flow and return the result.
---
```python focus=9:16
from langflow 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: Optional[Document] = None) -> Document:
# List the flows
flows = self.list_flows()
# Get the flow that matches the selected name
flow = next(filter(lambda f: f.name == flow_name, flows))
# Load the flow
tweaks = {}
flow = self.load_flow(flow.id, tweaks)
# Get the page_content from the document
page_content = document.page_content
# Use it in the flow
result = flow(page_content)
return Document(page_content=str(result))
```
Now we can add the field customization to the _`build_config`_ method.
We are using the _`options`_ parameter to make the _`flow_name`_ field a dropdown menu.
<Admonition type="caution">
Make sure the type of the field is _`str`_ and the values of the _`options`_
are strings.
</Admonition>
---
</CH.Scrollycoding>
In Langflow, this is how our script looks like:
{" "}
<ZoomableImage
alt="Document Processor code"
sources={{
light: "img/flow_runner_code.png",
}}
style={{
maxWidth: "70%",
margin: "0 auto",
display: "flex",
justifyContent: "center",
}}
/>
And here is our brand new custom component:
{" "}
<ZoomableImage
alt="Document Processor component"
sources={{
light: "img/flow_runner.png",
}}
style={{
width: "50%",
margin: "0 auto",
display: "flex",
justifyContent: "center",
}}
/>

View file

@ -7,16 +7,14 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
We welcome all examples that can help our community learn and explore Langflow's capabilities.
Langflow Examples is a repository on [GitHub](https://github.com/logspace-ai/langflow_examples) that contains examples of flows that people can use for inspiration and learning.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/community-examples.png",
}}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/community-examples.png",
}}
style={{ width: "100%" }}
/>
To upload examples, please follow these steps:

View file

@ -1,3 +1,5 @@
import Admonition from "@theme/Admonition";
# MidJourney Prompt Chain
The `MidJourneyPromptChain` can be used to generate imaginative and detailed MidJourney prompts.
@ -14,9 +16,11 @@ And get a response such as:
Imagine a mysterious forest, the trees are tall and ancient, their branches reaching up to the sky. Through the darkness, a dragon emerges from the shadows, its scales shimmering in the moonlight. Its wingspan is immense, and its eyes glow with a fierce intensity. It is a majestic and powerful creature, one that commands both respect and fear.
```
:::tip
Notice that the `ConversationSummaryMemory` stores a summary of the conversation over time. Try using it to create better prompts as the conversation goes on.
:::
<Admonition type="tip">
Notice that the `ConversationSummaryMemory` stores a summary of the
conversation over time. Try using it to create better prompts as the
conversation goes on.
</Admonition>
## ⛓️ Langflow Example
@ -33,8 +37,9 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/MidJourney_Prompt_Chain.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
- [`ConversationSummaryMemory`](https://python.langchain.com/docs/modules/memory/how_to/summary)
:::
</Admonition>

View file

@ -1,12 +1,15 @@
import Admonition from "@theme/Admonition";
# Multiple Vector Stores
The example below shows an agent operating with two vector stores built upon different data sources.
The `TextLoader` loads a TXT file, while the `WebBaseLoader` pulls text from webpages into a document format to accessed downstream. The `Chroma` vector stores are created analogous to what we have demonstrated in our [CSV Loader](/examples/csv-loader.mdx) example. Finally, the `VectorStoreRouterAgent` constructs an agent that routes between the vector stores.
:::info
Get the TXT file used [here](https://github.com/hwchase17/chat-your-data/blob/master/state_of_the_union.txt).
:::
<Admonition type="info">
Get the TXT file used
[here](https://github.com/hwchase17/chat-your-data/blob/master/state_of_the_union.txt).
</Admonition>
URL used by the `WebBaseLoader`:
@ -14,13 +17,15 @@ URL used by the `WebBaseLoader`:
https://pt.wikipedia.org/wiki/Harry_Potter
```
:::tip
When you build the flow, request information about one of the sources. The agent should be able to use the correct source to generate a response.
:::
<Admonition type="tip">
When you build the flow, request information about one of the sources. The
agent should be able to use the correct source to generate a response.
</Admonition>
:::info
Learn more about Multiple Vector Stores [here](https://python.langchain.com/docs/modules/agents/toolkits/vectorstore?highlight=Multiple%20Vector%20Stores#multiple-vectorstores).
:::
<Admonition type="info">
Learn more about Multiple Vector Stores
[here](https://python.langchain.com/docs/modules/agents/toolkits/vectorstore?highlight=Multiple%20Vector%20Stores#multiple-vectorstores).
</Admonition>
## ⛓️ Langflow Example
@ -37,7 +42,7 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/Multiple_Vector_Stores.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`WebBaseLoader`](https://python.langchain.com/docs/modules/data_connection/document_loaders/integrations/web_base)
- [`TextLoader`](https://python.langchain.com/docs/modules/data_connection/document_loaders/integrations/unstructured_file)
@ -49,4 +54,4 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
- [`VectorStoreRouterToolkit`](https://python.langchain.com/docs/modules/agents/toolkits/vectorstore)
- [`VectorStoreRouterAgent`](https://python.langchain.com/docs/modules/agents/toolkits/vectorstore)
:::
</Admonition>

View file

@ -1,3 +1,5 @@
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.
@ -15,15 +17,19 @@ def is_brazilian_zipcode(zipcode: str) -> bool:
return False
```
:::tip
When a tool is called, it is often desirable to have its output returned directly to the user. You can do this by setting the **return_direct** flag for a tool to be True.
:::
<Admonition type="tip">
When a tool is called, it is often desirable to have its output returned
directly to the user. You can do this by setting the **return_direct** flag
for a tool to be True.
</Admonition>
The `AgentInitializer` component is a quick way to construct an agent from the model and tools.
:::info
The `PythonFunction` is a custom component that uses the LangChain 🦜🔗 tool decorator. Learn more about it [here](https://python.langchain.com/docs/modules/agents/tools/how_to/custom_tools).
:::
<Admonition type="info">
The `PythonFunction` is a custom component that uses the LangChain 🦜🔗 tool
decorator. Learn more about it
[here](https://python.langchain.com/docs/modules/agents/tools/how_to/custom_tools).
</Admonition>
## ⛓️ Langflow Example
@ -40,9 +46,10 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/Python_Function.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`PythonFunctionTool`](https://python.langchain.com/docs/modules/agents/tools/how_to/custom_tools)
- [`ChatOpenAI`](https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai)
- [`AgentInitializer`](https://python.langchain.com/docs/modules/agents/)
:::
</Admonition>

View file

@ -1,24 +1,29 @@
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.
:::info
To use the Serp API, you first need to sign up [Serp API](https://serpapi.com/) for an API key on the provider's website.
:::
<Admonition type="info">
To use the Serp API, you first need to sign up [Serp
API](https://serpapi.com/) for an API key on the provider's website.
</Admonition>
Here, the `ZeroShotPrompt` component specifies a prompt template for the `ZeroShotAgent`. Set a _Prefix_ and _Suffix_ with rules for the agent to obey. In the example, we used default templates.
The `LLMChain` is a simple chain that takes in a prompt template, formats it with the user input, and returns the response from an LLM.
:::tip
In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the LLM, but feel free to experiment with other Language Models!
:::
<Admonition type="tip">
In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the
LLM, but feel free to experiment with other Language Models!
</Admonition>
The `ZeroShotAgent` takes the `LLMChain` and the `Search` tool as inputs, using the tool to find information when necessary.
:::info
Learn more about the Serp API [here](https://python.langchain.com/docs/modules/agents/tools/integrations/serpapi).
:::
<Admonition type="info">
Learn more about the Serp API
[here](https://python.langchain.com/docs/modules/agents/tools/integrations/serpapi).
</Admonition>
## ⛓️ Langflow Example
@ -35,11 +40,12 @@ import ZoomableImage from "/src/theme/ZoomableImage.js";
#### <a target="\_blank" href="json_files/SerpAPI_Tool.json" download>Download Flow</a>
:::note LangChain Components 🦜🔗
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`ZeroShotPrompt`](https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/)
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
- [`LLMChain`](https://python.langchain.com/docs/modules/chains/foundational/llm_chain)
- [`Search`](https://python.langchain.com/docs/modules/agents/tools/integrations/serpapi)
- [`ZeroShotAgent`](https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent)
:::
</Admonition>

View file

@ -6,15 +6,14 @@ import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/hugging-face.png",
}}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/hugging-face.png",
}}
style={{ width: "100%" }}
/>
Check out Langflow on [HuggingFace Spaces](https://huggingface.co/spaces/Logspace/Langflow).

View file

@ -7,58 +7,46 @@ import ReactPlayer from "react-player";
Langflows chat interface provides a user-friendly experience and functionality to interact with the model and customize the prompt. The sidebar brings options that allow users to view and edit pre-defined prompt variables. This feature facilitates quick experimentation by enabling the modification of variable values right in the chat.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
Notice that editing variables in the chat interface take place temporarily and wont change their original value in the components once the chat is closed.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface2.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface2.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
To view the complete prompt in its original, structured format, click the "Display Prompt" option. This feature lets you see the prompt exactly as it entered the model.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface3.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface3.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
In the chat interface, you can redefine which variable should be interpreted as the chat input. This gives you control over these inputs and allows dynamic and creative interactions.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface4.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/chat_interface4.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>

View file

@ -0,0 +1,209 @@
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 Widget
<div style={{ marginBottom: "20px" }}>
The <b>Langflow Chat Widget</b> is a powerful web component that enables
communication with a Langflow project. This widget allows for a chat interface
embedding, allowing the integration of Langflow into web applications
effortlessly.
</div>
## Features
🌟 **Seamless Integration:** Easily integrate the Langflow Chat Widget into your website or web application with just a few lines of JavaScript.
🚀 **Interactive Chat Interface:** Engage your users with a user-friendly conversation, powered by Langflow's advanced language understanding capabilities.
🎛️ **Customizable Styling:** Customize the appearance of the chat widget to match your application's design and branding.
🌐 **Multilingual Support:** Communicate with users in multiple languages, opening up your application to a global audience.
---
## Usage
<div style={{ marginBottom: "20px" }}>
You can get the HTML code embedded with the chat by clicking the Code button
at the Sidebar after building a flow.
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/widget-sidebar.png"),
}}
style={{ width: "50%", maxWidth: "600px", margin: "0 auto" }}
/>
<div style={{ marginBottom: "20px" }}>
Clicking the Chat Widget HTML tab, you'll get the code to be inserted. Read
below to learn how to use it with HTML, React and Angular.
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/widget-code.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
---
### HTML
The Chat Widget can be embedded into any HTML page, inside a _`<body>`_ tag, as demonstrated in the video below.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ReactPlayer playing controls url="/videos/langflow_widget.mp4" />
</div>
---
### React
To embed the Chat Widget using React, you'll need to insert this _`<script>`_ tag into the React _index.html_ file, inside the _`<body>`_ tag:
```html
<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script>
```
Then, declare your Web Component and encapsulate it in a React component.
```jsx
declare global {
namespace JSX {
interface IntrinsicElements {
"langflow-chat": any;
}
}
}
export default function ChatWidget({ className }) {
return (
<div className={className}>
<langflow-chat
chat_inputs='{"your_key":"value"}'
chat_input_field="your_chat_key"
flow_id="your_flow_id"
host_url="langflow_url"
></langflow-chat>
</div>
);
}
```
Finally, you can place the component anywhere in your code to display the Chat Widget.
---
### Angular
To use it in Angular, first add this _`<script>`_ tag into the Angular _index.html_ file, inside the _`<body>`_ tag.
```html
<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script>
```
When you use a custom web component in an Angular template, the Angular compiler might show a warning when it doesn't recognize the custom elements by default. To suppress this warning, add _`CUSTOM_ELEMENTS_SCHEMA`_ to the module's _`@NgModule.schemas`_.
- Open the module file (it typically ends with _.module.ts_) where you'd add the _`langflow-chat`_ web component.
- Import _`CUSTOM_ELEMENTS_SCHEMA`_ at the top of the file:
```ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
```
- Add _`CUSTOM_ELEMENTS_SCHEMA`_ to the 'schemas' array inside the '@NgModule' decorator:
```ts
@NgModule({
declarations: [
// ... Other components and directives ...
],
imports: [
// ... Other imported modules ...
],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Add the CUSTOM_ELEMENTS_SCHEMA here
})
export class YourModule {}
```
In your Angular project, find the component belonging to the module where _`CUSTOM_ELEMENTS_SCHEMA`_ was added.
- Inside the template, add the _`langflow-chat`_ tag to include the Chat Widget in your component's view:
```jsx
<langflow-chat
chat_inputs='{"your_key":"value"}'
chat_input_field="your_chat_key"
flow_id="your_flow_id"
host_url="langflow_url"
></langflow-chat>
```
<Admonition type="info">
<ul>
<li>
_`CUSTOM_ELEMENTS_SCHEMA`_ is a built-in schema that allows Angular to
recognize custom elements.
</li>
<li>
Adding _`CUSTOM_ELEMENTS_SCHEMA`_ tells Angular to allow custom elements
in your templates, and it will suppress the warning related to unknown
elements like _`langflow-chat`_.
</li>
<li>
Notice that you can only use the Chat Widget in components that are part
of the module where you added _`CUSTOM_ELEMENTS_SCHEMA`_.
</li>
</ul>
</Admonition>
---
## Configuration
Use the widget API to customize your Chat Widget:
<Admonition type="caution">
Props with the type JSON need to be passed as Stringified JSONs, with the
format &#123;<span>"key":"value"</span>&#125;.
</Admonition>
| Prop | Type | Required | Description |
| --------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bot_message_style | JSON | No | Applies custom formatting to bot messages. |
| chat_input_field | String | Yes | Defines the type of the input field for chat messages. |
| chat_inputs | JSON | Yes | Determines the chat input elements and their respective values. |
| chat_output_key | String | No | Specifies which output to display if multiple outputs are available. |
| chat_position | String | No | Positions the chat window on the screen (options include: top-left, top-center, top-right, center-left, center-right, bottom-right, bottom-center, bottom-left). |
| chat_trigger_style | JSON | No | Styles the chat trigger button. |
| chat_window_style | JSON | No | Customizes the overall appearance of the chat window. |
| error_message_style | JSON | No | Sets the format for error messages within the chat window. |
| flow_id | String | Yes | Identifies the flow that the component is associated with. |
| height | Number | No | Sets the height of the chat window in pixels. |
| host_url | String | Yes | Specifies the URL of the host for chat component communication. |
| input_container_style | JSON | No | Applies styling to the container where chat messages are entered. |
| input_style | JSON | No | Sets the style for the chat input field. |
| online | Boolean | No | Toggles the online status of the chat component. |
| online_message | String | No | Sets a custom message to display when the chat component is online. |
| placeholder | String | No | Sets the placeholder text for the chat input field. |
| placeholder_sending | String | No | Sets the placeholder text to display while a message is being sent. |
| send_button_style | JSON | No | Sets the style for the send button in the chat window. |
| send_icon_style | JSON | No | Sets the style for the send icon in the chat window. |
| tweaks | JSON | No | Applies additional custom adjustments for the associated flow. |
| user_message_style | JSON | No | Determines the formatting for user messages in the chat window. |
| width | Number | No | Sets the width of the chat window in pixels. |
| window_title | String | No | Sets the title displayed in the chat window's header or title bar. |

View file

@ -25,17 +25,14 @@ Components are the building blocks of the flows. They are made of inputs, output
of that type is required.
</div>
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/single-compenent.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/single-compenent.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
<div style={{ marginBottom: "20px" }}>
On the top right corner, you will find the component status icon 🔴. Make the

View file

@ -0,0 +1,344 @@
---
description: Custom Components
hide_table_of_contents: true
---
import ZoomableImage from "/src/theme/ZoomableImage.js";
import Admonition from "@theme/Admonition";
# Custom Components
In Langflow, a Custom Component is a special component type that allows users to extend the functionality of the platform by creating their own reusable and configurable components.
A Custom Component is created from a user-defined Python script that uses the _`CustomComponent`_ class provided by the Langflow library. These components can be as simple as a basic function that takes and returns a string or as complex as a combination of multiple sub-components and API calls.
Let's take a look at the basic rules and features, then we'll go over an example.
## TL;DR
- Create a class that inherits from _`CustomComponent`_ and contains a _`build`_ method.
- Use arguments with [Type Annotations (or Type Hints)](https://docs.python.org/3/library/typing.html) of the _`build`_ method to create component fields.
- Use the _`build_config`_ method to customize these fields look and behave.
Here is an example:
<CH.Code linuNumbers={false}>
```python
from langflow import CustomComponent
from langchain.schema import Document
class DocumentProcessor(CustomComponent):
display_name = "Document Processor"
description = "This component processes a document"
def build_config(self) -> dict:
options = ["Uppercase", "Lowercase", "Titlecase"]
return {
"function": {"options": options,
"value": options[0]}}
def build(self, document: Document, function: str) -> Document:
page_content = document.page_content
if function == "Uppercase":
page_content = page_content.upper()
elif function == "Lowercase":
page_content = page_content.lower()
elif function == "Titlecase":
page_content = page_content.title()
return Document(page_content=page_content)
```
</CH.Code>
<Admonition type="tip">
Check out [FlowRunner Component](../examples/flow-runner) for a more powerful
example.
</Admonition>
---
## Rules
The Python script for every Custom Component should follow a set of rules. Let's go over them, one by one:
<CH.Scrollycoding rows={20} className={""}>
### Rule 1
The script must contain a **single class** that inherits from _`CustomComponent`_.
```python
from langflow import CustomComponent
from langchain.schema import Document
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self) -> dict:
...
def build(self, document: Document, function: str) -> Document:
...
```
---
### Rule 2
This class requires a _`build`_ method, which is used to run the component and defines its fields.
```python
from langflow import CustomComponent
from langchain.schema import Document
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self) -> dict:
...
# focus
# mark
def build(self) -> Document:
...
```
---
The [Return Type Annotation](https://docs.python.org/3/library/typing.html) of the _`build`_ method defines the component type (e.g., Chain, BaseLLM or basic Python types). Check out all supported types in the [component reference](../components/custom).
```python
from langflow import CustomComponent
from langchain.schema import Document
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self) -> dict:
...
# focus[20:31]
# mark
def build(self) -> Document:
...
```
---
```python
from langflow import CustomComponent
from langchain.schema import Document
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self) -> dict:
...
def build(self) -> Document:
...
```
### Rule 3
The class can have a [_`build_config`_](focus://8) method, which is used to define configuration fields for the component. The [_`build_config`_](focus://8) method should always return a dictionary with specific keys representing the field names and their corresponding configurations. It must follow the format described below:
- Top-level keys are field names.
- Their values are also of type _`dict`_. They specify the behavior of the generated fields.
Check out the [component reference](../components/custom) for more details on the available field configurations.
---
```python
from langflow import CustomComponent
from langchain.schema import Document
class MyComponent(CustomComponent):
display_name = "Custom Component"
description = "This is a custom component"
def build_config(self) -> dict:
...
def build(self) -> Document:
...
```
## Example
Let's create a simple component that takes a document and a function name as input and returns a document with the page content processed by the selected function.
<Admonition type="info" label="Tip">
If you were to do this using Langflow's native components, you would create a Tool and ask the agent to use it.
</Admonition>
---
### Pick a display name
First, let's choose a name for our component by adding a _`display_name`_ attribute. This is the component name to be displayed in the canvas.
The name of the class is not important, but let's call it _`DocumentProcessor`_.
```python
from langflow import CustomComponent
from langchain.schema import Document
# focus
class DocumentProcessor(CustomComponent):
# focus
display_name = "Document Processor"
description = "This is a custom component"
def build_config(self) -> dict:
...
def build(self) -> Document:
...
```
---
### Write a description
We can also write a description for it using the _`description`_ attribute.
```python
from langflow import CustomComponent
from langchain.schema import Document
class DocumentProcessor(CustomComponent):
display_name = "Document Processor"
description = "This component processes a document"
def build_config(self) -> dict:
...
def build(self) -> Document:
...
```
---
```python
from langflow import CustomComponent
from langchain.schema import Document
class DocumentProcessor(CustomComponent):
display_name = "Document Processor"
description = "This component processes a document"
def build_config(self) -> dict:
...
def build(self, document: Document, function: str) -> Document:
page_content = document.page_content
if function == "Uppercase":
page_content = page_content.upper()
elif function == "Lowercase":
page_content = page_content.lower()
elif function == "Titlecase":
page_content = page_content.title()
return Document(page_content=page_content)
```
### Add the build method
The parameters used are:
- _`document`_ is the document to be processed.
- _`function`_ is the name of the function to be applied to the document.
The return type is _`Document`_.
This method is called when the component is built (i.e. when you click the _Build_ button in the canvas).
<Admonition type="info">
One important aspect of the Type Hints is that generally base Python types add
different kinds of fields while other types such as Document add a
[handle](../guidelines/components) to the component.
</Admonition>
---
### Customize the fields
The _`build_config`_ method will be used to configure the fields of the component.
- _`options`_ defines that the field will be a dropdown menu. The values must be _`str`_ and the type of the field should also be _`str`_.
- _`value`_ is the default value of the field.
- _`display_name`_ is the name of the field to be displayed in the canvas.
This method is called when the code is processed (i.e. when you click _Check and Save_ in the code editor).
```python
from langflow import CustomComponent
from langchain.schema import Document
class DocumentProcessor(CustomComponent):
display_name = "Document Processor"
description = "This component processes a document"
def build_config(self) -> dict:
options = ["Uppercase", "Lowercase", "Titlecase"]
return {
"function": {"options": options,
"value": options[0],
"display_name": "Function"
},
"document": {"display_name": "Document"}
}
def build(self, document: Document, function: str) -> Document:
page_content = document.page_content
if function == "Uppercase":
page_content = page_content.upper()
elif function == "Lowercase":
page_content = page_content.lower()
elif function == "Titlecase":
page_content = page_content.title()
return Document(page_content=page_content)
```
</CH.Scrollycoding>
In Langflow, this is how our script looks like:
{" "}
<ZoomableImage
alt="Document Processor code"
sources={{
light: "img/document_processor_code.png",
}}
style={{
maxWidth: "70%",
margin: "0 auto",
display: "flex",
justifyContent: "center",
}}
/>
And here is our brand new custom component:
{" "}
<ZoomableImage
alt="Document Processor component"
sources={{
light: "img/document_processor.png",
}}
style={{
width: "50%",
margin: "0 auto",
display: "flex",
justifyContent: "center",
}}
/>

View file

@ -2,6 +2,7 @@ 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";
# Features
@ -12,17 +13,14 @@ import ReactPlayer from "react-player";
below:
</div>
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/features.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/features.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
<div style={{ marginBottom: "20px" }}>
Further down, we will explain each of these options.
@ -34,9 +32,10 @@ import ReactPlayer from "react-player";
Flows can be exported and imported as JSON files.
:::caution
<Admonition type="caution">
Watch out for API keys being stored in local files.
:::
</Admonition>
---

View file

@ -7,80 +7,62 @@ import ReactPlayer from "react-player";
The prompt template allows users to create prompts and define variables that provide control over instructing the model.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
Variables can be used to define instructions, questions, context, inputs, or examples for the model and can be created with any chosen name in curly brackets, e.g., `{variable_name}`. They act as placeholders for parts of the text that can be easily modified.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization2.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization2.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
Once inserted, these variables are immediately recognized as new fields in the prompt component. Here, you can define their values within the component itself or leave a field empty to be adjusted over the chat interface.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization3.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization3.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
You can also use documents or output parsers as prompt variables. By plugging them into prompt handles, theyll disable and feed that input field.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization4.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization4.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
With this, users can interact with documents, webpages, or any other type of content directly from the prompt, which allows for seamless integration of external resources with the language model.
If working with an interactive (chat-like) flow, remember to keep one of the input variables empty to behave as the chat input.
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization5.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/prompt_customization5.png"),
}}
style={{ width: "100%", maxWidth: "800px", margin: "0 auto" }}
/>

View file

@ -6,13 +6,11 @@ import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
<div
style={{ marginBottom: "20px", display: "flex", justifyContent: "center" }}
>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/new_langflow2.gif",
}}
/>
</div>
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/new_langflow2.gif",
}}
style={{ width: "100%" }}
/>

View file

@ -1,127 +1,141 @@
const lightCodeTheme = require("prism-react-renderer/themes/github");
const { remarkCodeHike } = require("@code-hike/mdx");
// With JSDoc @type annotations, IDEs can provide config autocompletion
/** @type {import('@docusaurus/types').DocusaurusConfig} */
(
module.exports = {
title: "Langflow Documentation",
tagline: "Langflow is a GUI for LangChain, designed with react-flow",
favicon: "img/favicon.ico",
url: "https://logspace-ai.github.io",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
organizationName: "logspace-ai",
projectName: "langflow",
trailingSlash: false,
customFields: {
mendableAnonKey: process.env.MENDABLE_ANON_KEY,
},
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"@docusaurus/preset-classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
routeBasePath: "/",
sidebarPath: require.resolve("./sidebars.js"),
path: "docs",
// sidebarPath: 'sidebars.js',
},
theme: {
customCss: require.resolve("./src/css/custom.css"),
},
}),
],
],
plugins: [
["docusaurus-node-polyfills", { excludeAliases: ["console"] }],
"docusaurus-plugin-image-zoom",
// ....
async function myPlugin(context, options) {
return {
name: "docusaurus-tailwindcss",
configurePostCss(postcssOptions) {
// Appends TailwindCSS and AutoPrefixer.
postcssOptions.plugins.push(require("tailwindcss"));
postcssOptions.plugins.push(require("autoprefixer"));
return postcssOptions;
},
};
},
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
module.exports = {
title: "Langflow Documentation",
tagline: "Langflow is a GUI for LangChain, designed with react-flow",
favicon: "img/favicon.ico",
url: "https://logspace-ai.github.io",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
organizationName: "logspace-ai",
projectName: "langflow",
trailingSlash: false,
customFields: {
mendableAnonKey: process.env.MENDABLE_ANON_KEY,
},
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"@docusaurus/preset-classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
navbar: {
hideOnScroll: true,
title: "Langflow",
logo: {
alt: "Langflow",
src: "img/chain.png",
},
items: [
// right
{
position: "right",
href: "https://github.com/logspace-ai/langflow",
position: "right",
className: "header-github-link",
target: "_blank",
rel: null,
},
{
position: "right",
href: "https://twitter.com/logspace_ai",
position: "right",
className: "header-twitter-link",
target: "_blank",
rel: null,
},
{
position: "right",
href: "https://discord.gg/EqksyE2EX9",
position: "right",
className: "header-discord-link",
target: "_blank",
rel: null,
},
docs: {
beforeDefaultRemarkPlugins: [
[
remarkCodeHike,
{ theme: "monokai", showCopyButton: true, lineNumbers: true },
],
],
routeBasePath: "/",
sidebarPath: require.resolve("./sidebars.js"),
path: "docs",
// sidebarPath: 'sidebars.js',
},
theme: {
customCss: [
require.resolve("@code-hike/mdx/styles.css"),
require.resolve("./src/css/custom.css"),
],
},
tableOfContents: {
minHeadingLevel: 2,
maxHeadingLevel: 5,
},
colorMode: {
defaultMode: "light",
disableSwitch: true,
respectPrefersColorScheme: false,
},
announcementBar: {
content:
'⭐️ If you like ⛓Langflow, star it on <a target="_blank" rel="noopener noreferrer" href="https://github.com/logspace-ai/langflow">GitHub</a>! ⭐️',
backgroundColor: "#B53D38", //Mustard Yellow #D19900 #D4B20B - Salmon #E9967A
textColor: "#fff",
isCloseable: false,
},
footer: {
links: [],
copyright: `Copyright © ${new Date().getFullYear()} Logspace.`,
},
zoom: {
selector: ".markdown :not(a) > img:not(.no-zoom)",
background: {
light: "rgba(240, 240, 240, 0.9)",
},
config: {},
},
prism: {
theme: lightCodeTheme,
},
}),
}
);
],
],
plugins: [
["docusaurus-node-polyfills", { excludeAliases: ["console"] }],
"docusaurus-plugin-image-zoom",
// ....
async function myPlugin(context, options) {
return {
name: "docusaurus-tailwindcss",
configurePostCss(postcssOptions) {
// Appends TailwindCSS and AutoPrefixer.
postcssOptions.plugins.push(require("tailwindcss"));
postcssOptions.plugins.push(require("autoprefixer"));
return postcssOptions;
},
};
},
],
themes: ["mdx-v2"],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
hideOnScroll: true,
title: "Langflow",
logo: {
alt: "Langflow",
src: "img/chain.png",
},
items: [
// right
{
position: "right",
href: "https://github.com/logspace-ai/langflow",
position: "right",
className: "header-github-link",
target: "_blank",
rel: null,
},
{
position: "right",
href: "https://twitter.com/logspace_ai",
position: "right",
className: "header-twitter-link",
target: "_blank",
rel: null,
},
{
position: "right",
href: "https://discord.gg/EqksyE2EX9",
position: "right",
className: "header-discord-link",
target: "_blank",
rel: null,
},
],
},
tableOfContents: {
minHeadingLevel: 2,
maxHeadingLevel: 5,
},
colorMode: {
defaultMode: "light",
disableSwitch: true,
respectPrefersColorScheme: false,
},
announcementBar: {
content:
'⭐️ If you like ⛓Langflow, star it on <a target="_blank" rel="noopener noreferrer" href="https://github.com/logspace-ai/langflow">GitHub</a>! ⭐️',
backgroundColor: "#B53D38", //Mustard Yellow #D19900 #D4B20B - Salmon #E9967A
textColor: "#fff",
isCloseable: false,
},
footer: {
links: [],
copyright: `Copyright © ${new Date().getFullYear()} Logspace.`,
},
zoom: {
selector: ".markdown :not(a) > img:not(.no-zoom)",
background: {
light: "rgba(240, 240, 240, 0.9)",
},
config: {},
},
// prism: {
// theme: require("prism-react-renderer/themes/dracula"),
// },
docs: {
sidebar: {
hideable: true,
},
},
}),
};

2033
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,12 +15,13 @@
},
"dependencies": {
"@babel/preset-react": "^7.22.3",
"@code-hike/mdx": "^0.9.0",
"@docusaurus/core": "2.4.1",
"@docusaurus/plugin-ideal-image": "^2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@docusaurus/theme-classic": "^2.4.1",
"@docusaurus/theme-search-algolia": "^2.4.1",
"@mdx-js/react": "^1.6.22",
"@mdx-js/react": "^2.3.0",
"@mendable/search": "^0.0.114",
"@pbe/react-yandex-maps": "^1.2.4",
"@prismicio/client": "^7.0.1",
@ -28,6 +29,7 @@
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"docusaurus-plugin-image-zoom": "^0.1.4",
"docusaurus-theme-mdx-v2": "^0.1.2",
"jquery": "^3.7.0",
"medium-zoom": "^1.0.8",
"node-fetch": "^3.3.1",
@ -67,4 +69,4 @@
"engines": {
"node": ">=16.14"
}
}
}

View file

@ -21,6 +21,8 @@ module.exports = {
"guidelines/collection",
"guidelines/prompt-customization",
"guidelines/chat-interface",
"guidelines/chat-widget",
"guidelines/custom-component",
],
},
{
@ -30,6 +32,7 @@ module.exports = {
items: [
"components/agents",
"components/chains",
"components/custom",
"components/embeddings",
"components/llms",
"components/loaders",
@ -63,6 +66,7 @@ module.exports = {
label: "Examples",
collapsed: false,
items: [
"examples/flow-runner",
"examples/conversation-chain",
"examples/buffer-memory",
"examples/midjourney-prompt-chain",

View file

@ -3,17 +3,19 @@
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
:root {
:root {
--ifm-background-color: var(--token-primary-bg-c);
--ifm-navbar-link-hover-color: initial;
--ifm-navbar-padding-vertical: 0;
--ifm-navbar-item-padding-vertical: 0;
--ifm-font-family-base: -apple-system, BlinkMacSystemFont, Inter, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI emoji';
--ifm-font-family-monospace: 'SFMono-Regular', 'Roboto Mono', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--ifm-font-family-base: -apple-system, BlinkMacSystemFont, Inter, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI emoji";
--ifm-font-family-monospace: "SFMono-Regular", "Roboto Mono", Consolas,
"Liberation Mono", Menlo, Courier, monospace;
}
.theme-doc-sidebar-item-category.menu__list-item:not(:first-child) {
margin-top: 1.5rem!important;
margin-top: 1.5rem !important;
}
.docusaurus-highlight-code-line {
@ -31,7 +33,7 @@
transform: skewY(6deg);
}
[class^='announcementBar'] {
[class^="announcementBar"] {
z-index: 10;
}
@ -112,7 +114,7 @@ body {
}
.header-github-link:before {
content: '';
content: "";
width: 24px;
height: 24px;
display: flex;
@ -126,7 +128,7 @@ body {
}
.header-twitter-link::before {
content: '';
content: "";
width: 24px;
height: 24px;
display: flex;
@ -140,7 +142,7 @@ body {
}
.header-discord-link::before {
content: '';
content: "";
width: 24px;
height: 24px;
display: flex;
@ -148,7 +150,6 @@ body {
background-size: contain;
}
/* Images */
.image-rendering-crisp {
image-rendering: crisp-edges;
@ -164,7 +165,7 @@ body {
.img-center {
display: flex;
justify-content: center;
width: 100%,
width: 100%;
}
.resized-image {
@ -188,4 +189,22 @@ body {
.mendable-search {
width: 140px;
}
}
}
/*
.ch-scrollycoding {
gap: 10rem !important;
} */
.ch-scrollycoding-content {
max-width: 55% !important;
min-width: 40% !important;
}
.ch-scrollycoding-sticker {
max-width: 60% !important;
min-width: 45% !important;
}
.ch-scrollycoding-step-content {
min-height: 70px;
}

View file

@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import ThemedImage from '@theme/ThemedImage';
import useBaseUrl from '@docusaurus/useBaseUrl';
import React, { useState, useEffect } from "react";
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
const ZoomableImage = ({ alt, sources }) => {
const ZoomableImage = ({ alt, sources, style }) => {
// add style here
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = () => {
@ -10,27 +11,36 @@ const ZoomableImage = ({ alt, sources }) => {
};
const handleKeyPress = (event) => {
if (event.key === 'Escape') {
if (event.key === "Escape") {
setIsFullscreen(false);
}
};
useEffect(() => {
if (isFullscreen) {
document.addEventListener('keydown', handleKeyPress);
document.addEventListener("keydown", handleKeyPress);
} else {
document.removeEventListener('keydown', handleKeyPress);
document.removeEventListener("keydown", handleKeyPress);
}
return () => {
document.removeEventListener('keydown', handleKeyPress);
document.removeEventListener("keydown", handleKeyPress);
};
}, [isFullscreen]);
// Default style
const defaultStyle = {
width: "50%",
margin: "0 auto",
display: "flex",
justifyContent: "center",
};
return (
<div
className={`zoomable-image ${isFullscreen ? 'fullscreen' : ''}`}
className={`zoomable-image ${isFullscreen ? "fullscreen" : ""}`}
onClick={toggleFullscreen}
style={{ ...defaultStyle, ...style }}
>
<ThemedImage
className="zoomable-image-inner"

BIN
docs/static/img/document_processor.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
docs/static/img/flow_runner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
docs/static/img/flow_runner_code.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
docs/static/img/widget-code.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

BIN
docs/static/img/widget-sidebar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

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

Binary file not shown.

View file

@ -1,14 +1,17 @@
[tool.poetry]
name = "langflow"
version = "0.3.2"
version = "0.3.3"
description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"]
maintainers = [
"Carlos Coelho <carlos@logspace.ai>",
"Cristhian Zanforlin <cristhian.lousa@gmail.com>",
"Gabriel Almeida <gabriel@logspace.ai>",
"Gustavo Schaedler <gustavopoa@gmail.com>",
"Igor Carvalho <igorr.ackerman@gmail.com>",
"Lucas Eduoli <lucaseduoli@gmail.com>",
"Otávio Anovazzi <otavio2204@gmail.com>",
"Rodrigo Nader <rodrigo@logspace.ai>",
]
repository = "https://github.com/logspace-ai/langflow"
license = "MIT"

View file

@ -1,7 +1,7 @@
from importlib import metadata
from langflow.cache import cache_manager
from langflow.processing.process import load_flow_from_json
from langflow.utils.types import Prompt
from langflow.interface.custom.custom_component import CustomComponent
try:
__version__ = metadata.version(__package__)
@ -10,4 +10,4 @@ except metadata.PackageNotFoundError:
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = ["load_flow_from_json", "cache_manager", "Prompt"]
__all__ = ["load_flow_from_json", "cache_manager", "CustomComponent"]

View file

@ -2,8 +2,9 @@ import ast
import inspect
import traceback
from typing import Dict, Any, Type, Union
from typing import Dict, Any, List, Type, Union
from fastapi import HTTPException
from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails
class CodeSyntaxError(HTTPException):
@ -54,13 +55,13 @@ class CodeParser:
return tree
def parse_node(self, node: ast.AST) -> None:
def parse_node(self, node: Union[ast.stmt, ast.AST]) -> None:
"""
Parses an AST node and updates the data
dictionary with the relevant information.
"""
if handler := self.handlers.get(type(node)):
handler(node)
if handler := self.handlers.get(type(node)): # type: ignore
handler(node) # type: ignore
def parse_imports(self, node: Union[ast.Import, ast.ImportFrom]) -> None:
"""
@ -92,27 +93,73 @@ class CodeParser:
"""
Extracts details from a single function or method node.
"""
func = {
"name": node.name,
"doc": ast.get_docstring(node),
"args": [],
"body": [],
"return_type": ast.unparse(node.returns) if node.returns else None,
}
func = CallableCodeDetails(
name=node.name,
doc=ast.get_docstring(node),
args=[],
body=[],
return_type=ast.unparse(node.returns) if node.returns else None,
)
# Handle positional arguments with default values
defaults = [None] * (len(node.args.args) - len(node.args.defaults)) + [
ast.unparse(default) if default else None for default in node.args.defaults
func.args = self.parse_function_args(node)
func.body = self.parse_function_body(node)
return func.dict()
def parse_function_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the arguments of a function or method node.
"""
args = []
args += self.parse_positional_args(node)
args += self.parse_varargs(node)
args += self.parse_keyword_args(node)
args += self.parse_kwargs(node)
return args
def parse_positional_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the positional arguments of a function or method node.
"""
num_args = len(node.args.args)
num_defaults = len(node.args.defaults)
num_missing_defaults = num_args - num_defaults
missing_defaults = [None] * num_missing_defaults
default_values = [
ast.unparse(default).strip("'") if default else None
for default in node.args.defaults
]
# Now check all default values to see if there
# are any "None" values in the middle
default_values = [
None if value == "None" else value for value in default_values
]
for arg, default in zip(node.args.args, defaults):
func["args"].append(self.parse_arg(arg, default))
defaults = missing_defaults + default_values
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.args, defaults)
]
return args
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the *args argument of a function or method node.
"""
args = []
# Handle *args
if node.args.vararg:
func["args"].append(self.parse_arg(node.args.vararg, None))
args.append(self.parse_arg(node.args.vararg, None))
# Handle keyword-only arguments with default values
return args
def parse_keyword_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the keyword-only arguments of a function or method node.
"""
kw_defaults = [None] * (
len(node.args.kwonlyargs) - len(node.args.kw_defaults)
) + [
@ -120,16 +167,28 @@ class CodeParser:
for default in node.args.kw_defaults
]
for arg, default in zip(node.args.kwonlyargs, kw_defaults):
func["args"].append(self.parse_arg(arg, default))
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.kwonlyargs, kw_defaults)
]
return args
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the **kwargs argument of a function or method node.
"""
args = []
# Handle **kwargs
if node.args.kwarg:
func["args"].append(self.parse_arg(node.args.kwarg, None))
args.append(self.parse_arg(node.args.kwarg, None))
for line in node.body:
func["body"].append(ast.unparse(line))
return func
return args
def parse_function_body(self, node: ast.FunctionDef) -> List[str]:
"""
Parses the body of a function or method node.
"""
return [ast.unparse(line) for line in node.body]
def parse_assign(self, stmt):
"""
@ -164,29 +223,31 @@ class CodeParser:
"""
Extracts "classes" from the code, including inheritance and init methods.
"""
class_dict = {
"name": node.name,
"doc": ast.get_docstring(node),
"bases": [ast.unparse(base) for base in node.bases],
"attributes": [],
"methods": [],
}
class_details = ClassCodeDetails(
name=node.name,
doc=ast.get_docstring(node),
bases=[ast.unparse(base) for base in node.bases],
attributes=[],
methods=[],
init=None,
)
for stmt in node.body:
if isinstance(stmt, ast.Assign):
if attr := self.parse_assign(stmt):
class_dict["attributes"].append(attr)
class_details.attributes.append(attr)
elif isinstance(stmt, ast.AnnAssign):
if attr := self.parse_ann_assign(stmt):
class_dict["attributes"].append(attr)
class_details.attributes.append(attr)
elif isinstance(stmt, ast.FunctionDef):
method, is_init = self.parse_function_def(stmt)
if is_init:
class_dict["init"] = method
class_details.init = method
else:
class_dict["methods"].append(method)
class_details.methods.append(method)
self.data["classes"].append(class_dict)
self.data["classes"].append(class_details.dict())
def parse_global_vars(self, node: ast.Assign) -> None:
"""

View file

@ -1,4 +1,5 @@
import ast
from typing import Optional
from pydantic import BaseModel
from fastapi import HTTPException
@ -20,7 +21,7 @@ class Component(BaseModel):
"The name of the entrypoint function must be provided."
)
code: str
code: Optional[str]
function_entrypoint_name = "build"
field_config: dict = {}

View file

@ -20,13 +20,22 @@ LANGCHAIN_BASE_TYPES = {
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
}
DEFAULT_CUSTOM_COMPONENT_CODE = """
from langflow import Prompt
from langflow.interface.custom.custom_component import CustomComponent
from langflow import CustomComponent
from langchain.llms.base import BaseLLM
from langchain.chains import LLMChain
@ -38,11 +47,12 @@ import requests
class YourComponent(CustomComponent):
display_name: str = "Your Component"
description: str = "Your description"
field_config = { "url": { "multiline": True, "required": True } }
def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document:
def build_config(self):
return { "url": { "multiline": True, "required": True } }
def build(self, url: str, llm: BaseLLM, prompt: PromptTemplate) -> Document:
response = requests.get(url)
prompt = PromptTemplate.from_template(template)
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(response.text[:300])
return Document(page_content=str(result))

View file

@ -1,11 +1,10 @@
from typing import Callable, Optional
from typing import Any, Callable, List, Optional
from fastapi import HTTPException
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component
from langflow.utils import validate
from uuid import UUID
from langflow.database.base import session_getter
from langflow.database.models.flow import Flow
from pydantic import Extra
@ -17,14 +16,14 @@ class CustomComponent(Component, extra=Extra.allow):
code_class_base_inheritance = "CustomComponent"
function_entrypoint_name = "build"
function: Optional[Callable] = None
return_type_valid_list = list(LANGCHAIN_BASE_TYPES.keys())
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
repr_value: Optional[str] = ""
def __init__(self, **data):
super().__init__(**data)
def custom_repr(self):
return self.repr_value
return str(self.repr_value)
def build_config(self):
return self.field_config
@ -44,13 +43,15 @@ class CustomComponent(Component, extra=Extra.allow):
return True
def is_check_valid(self) -> bool:
return self._class_template_validation(self.code)
return self._class_template_validation(self.code) if self.code else False
def get_code_tree(self, code: str):
return super().get_code_tree(code)
@property
def get_function_entrypoint_args(self) -> str:
if not self.code:
return ""
tree = self.get_code_tree(self.code)
component_classes = [
@ -78,6 +79,8 @@ class CustomComponent(Component, extra=Extra.allow):
@property
def get_function_entrypoint_return_type(self) -> str:
if not self.code:
return ""
tree = self.get_code_tree(self.code)
component_classes = [
@ -138,16 +141,19 @@ class CustomComponent(Component, extra=Extra.allow):
def get_function(self):
return validate.create_function(self.code, self.function_entrypoint_name)
def load_flow(self, flow_id: UUID = None):
def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
from langflow.processing.process import build_sorted_vertices_with_caching
from langflow.processing.process import process_tweaks
with session_getter() as session:
data_graph = flow.data if (flow := session.get(Flow, flow_id)) else None
if not data_graph:
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
if not graph_data:
raise ValueError(f"Flow {flow_id} not found")
return build_sorted_vertices_with_caching(data_graph)
if tweaks:
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
return build_sorted_vertices_with_caching(graph_data)
def list_flows(self):
def list_flows(self) -> List[Flow]:
with session_getter() as session:
flows = session.query(Flow).all()
return flows

View file

@ -0,0 +1,29 @@
from pydantic import BaseModel, Field
from typing import Optional
class ClassCodeDetails(BaseModel):
"""
A dataclass for storing details about a class.
"""
name: str
doc: Optional[str]
bases: list
attributes: list
methods: list
init: Optional[dict] = Field(default_factory=dict)
class CallableCodeDetails(BaseModel):
"""
A dataclass for storing details about a callable.
"""
name: str
doc: Optional[str]
args: list
body: list
return_type: Optional[str]

View file

@ -1,17 +1,15 @@
import contextlib
import json
from typing import Any, Callable, Dict, List, Sequence, Type
from typing import Any, Callable, Dict, Sequence, Type
from langchain.agents import ZeroShotAgent
from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.base import BaseToolkit
from langchain.agents.tools import BaseTool
from langflow.interface.initialize.llm import initialize_vertexai
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type
from langflow.interface.initialize.vector_store import vecstore_initializer
from langchain.schema import Document, BaseOutputParser
from pydantic import ValidationError
from langflow.interface.importing.utils import (
@ -212,68 +210,8 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params:
def instantiate_prompt(node_type, class_object, params: Dict):
if node_type == "ZeroShotPrompt":
if "tools" not in params:
params["tools"] = []
return ZeroShotAgent.create_prompt(**params)
elif "MessagePromptTemplate" in node_type:
# Then we only need the template
from_template_params = {
"template": params.pop("prompt", params.pop("template", ""))
}
if not from_template_params.get("template"):
raise ValueError("Prompt template is required")
prompt = class_object.from_template(**from_template_params)
elif node_type == "ChatPromptTemplate":
prompt = class_object.from_messages(**params)
else:
prompt = class_object(**params)
format_kwargs: Dict[str, Any] = {}
for input_variable in prompt.input_variables:
if input_variable in params:
variable = params[input_variable]
if isinstance(variable, str):
format_kwargs[input_variable] = variable
elif isinstance(variable, BaseOutputParser) and hasattr(
variable, "get_format_instructions"
):
format_kwargs[input_variable] = variable.get_format_instructions()
elif isinstance(variable, List) and all(
isinstance(item, Document) for item in variable
):
# Format document to contain page_content and metadata
# as one string separated by a newline
if len(variable) > 1:
content = "\n".join(
[item.page_content for item in variable if item.page_content]
)
else:
content = variable[0].page_content
# content could be a json list of strings
with contextlib.suppress(json.JSONDecodeError):
content = json.loads(content)
if isinstance(content, list):
content = ",".join([str(item) for item in content])
format_kwargs[input_variable] = content
# handle_keys will be a list but it does not exist yet
# so we need to create it
if (
isinstance(variable, List)
and all(isinstance(item, Document) for item in variable)
) or (
isinstance(variable, BaseOutputParser)
and hasattr(variable, "get_format_instructions")
):
if "handle_keys" not in format_kwargs:
format_kwargs["handle_keys"] = []
# Add the handle_keys to the list
format_kwargs["handle_keys"].append(input_variable)
params, prompt = handle_node_type(node_type, class_object, params)
format_kwargs = handle_format_kwargs(prompt, params)
return prompt, format_kwargs

View file

@ -0,0 +1,103 @@
import contextlib
import json
from typing import Any, Dict, List
from langchain.agents import ZeroShotAgent
from langchain.schema import Document, BaseOutputParser
def handle_node_type(node_type, class_object, params: Dict):
if node_type == "ZeroShotPrompt":
params = check_tools_in_params(params)
prompt = ZeroShotAgent.create_prompt(**params)
elif "MessagePromptTemplate" in node_type:
prompt = instantiate_from_template(class_object, params)
elif node_type == "ChatPromptTemplate":
prompt = class_object.from_messages(**params)
else:
prompt = class_object(**params)
return params, prompt
def check_tools_in_params(params: Dict):
if "tools" not in params:
params["tools"] = []
return params
def instantiate_from_template(class_object, params: Dict):
from_template_params = {
"template": params.pop("prompt", params.pop("template", ""))
}
if not from_template_params.get("template"):
raise ValueError("Prompt template is required")
return class_object.from_template(**from_template_params)
def handle_format_kwargs(prompt, params: Dict):
format_kwargs: Dict[str, Any] = {}
for input_variable in prompt.input_variables:
if input_variable in params:
format_kwargs = handle_variable(params, input_variable, format_kwargs)
return format_kwargs
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):
variable = params[input_variable]
if isinstance(variable, str):
format_kwargs[input_variable] = variable
elif isinstance(variable, BaseOutputParser) and hasattr(
variable, "get_format_instructions"
):
format_kwargs[input_variable] = variable.get_format_instructions()
elif is_instance_of_list_or_document(variable):
format_kwargs = format_document(variable, input_variable, format_kwargs)
if needs_handle_keys(variable):
format_kwargs = add_handle_keys(input_variable, format_kwargs)
return format_kwargs
def is_instance_of_list_or_document(variable):
return (
isinstance(variable, List)
and all(isinstance(item, Document) for item in variable)
or isinstance(variable, Document)
)
def format_document(variable, input_variable: str, format_kwargs: Dict):
variable = variable if isinstance(variable, List) else [variable]
content = format_content(variable)
format_kwargs[input_variable] = content
return format_kwargs
def format_content(variable):
if len(variable) > 1:
return "\n".join([item.page_content for item in variable if item.page_content])
content = variable[0].page_content
return try_to_load_json(content)
def try_to_load_json(content):
with contextlib.suppress(json.JSONDecodeError):
content = json.loads(content)
if isinstance(content, list):
content = ",".join([str(item) for item in content])
return content
def needs_handle_keys(variable):
return is_instance_of_list_or_document(variable) or (
isinstance(variable, BaseOutputParser)
and hasattr(variable, "get_format_instructions")
)
def add_handle_keys(input_variable: str, format_kwargs: Dict):
if "handle_keys" not in format_kwargs:
format_kwargs["handle_keys"] = []
format_kwargs["handle_keys"].append(input_variable)
return format_kwargs

View file

@ -55,7 +55,7 @@ TOOL_INPUTS = {
show=True,
value="",
suffixes=[".json", ".yaml", ".yml"],
fileTypes=["json", "yaml", "yml"],
file_types=["json", "yaml", "yml"],
),
}

View file

@ -48,29 +48,3 @@ class PythonFunctionTool(Function, Tool):
class PythonFunction(Function):
code: str
class CustomComponent_old(BaseModel):
code: str
function: Optional[Callable] = None
imports: Optional[str] = None
# Eval code and store the class
def __init__(self, **data):
super().__init__(**data)
# Validate the Class code
@validator("code")
def validate_func(cls, v):
try:
validate.eval_function(v)
except Exception as e:
raise e
return v
def get_function(self):
"""Get the function"""
function_name = validate.extract_function_name(self.code)
return validate.create_function(self.code, function_name)

View file

@ -1,6 +1,6 @@
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.importing.utils import get_function_custom
@ -98,6 +98,13 @@ def add_new_custom_field(
display_name = field_config.pop("display_name", field_name)
field_type = field_config.pop("field_type", field_type)
field_type = process_type(field_type)
field_value = field_config.pop("value", field_value)
field_advanced = field_config.pop("advanced", False)
# If options is a list, then it's a dropdown
# If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list)
field_config["is_list"] = is_list or field_config.get("is_list", False)
if "name" in field_config:
warnings.warn(
@ -114,7 +121,7 @@ def add_new_custom_field(
value=field_value,
show=True,
required=required,
advanced=False,
advanced=field_advanced,
placeholder=placeholder,
display_name=display_name,
**field_config,
@ -126,8 +133,9 @@ def add_new_custom_field(
# TODO: Move to correct place
def add_code_field(template, raw_code):
def add_code_field(template, raw_code, field_config):
# Field with the Python code to allow update
code_field = {
"code": {
"dynamic": True,
@ -138,7 +146,7 @@ def add_code_field(template, raw_code):
"value": raw_code,
"password": False,
"name": "code",
"advanced": False,
"advanced": field_config.pop("advanced", False),
"type": "code",
"list": False,
}
@ -183,22 +191,30 @@ def update_display_name_and_description(frontend_node, template_config):
frontend_node["description"] = template_config["description"]
def build_field_config(custom_component):
def build_field_config(custom_component: CustomComponent):
"""Build the field configuration for a custom component"""
try:
custom_class = get_function_custom(custom_component.code)
return custom_class().build_config()
except Exception as exc:
logger.error(f"Error while building field config: {exc}")
logger.error(f"Error while getting custom function: {str(exc)}")
return {}
try:
return custom_class().build_config()
except Exception as exc:
logger.error(f"Error while building field config: {str(exc)}")
return {}
def add_extra_fields(frontend_node, field_config, function_args):
"""Add extra fields to the frontend node"""
if function_args is None:
if function_args is None or function_args == "":
return
# sort function_args which is a list of dicts
function_args.sort(key=lambda x: x["name"])
for extra_field in function_args:
if "name" not in extra_field or extra_field["name"] == "self":
continue
@ -232,19 +248,19 @@ def get_field_properties(extra_field):
def add_base_classes(frontend_node, return_type):
"""Add base classes to the frontend node"""
if return_type not in LANGCHAIN_BASE_TYPES or return_type is None:
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
raise HTTPException(
status_code=400,
detail={
"error": (
"Invalid return type should be one of: "
f"{list(LANGCHAIN_BASE_TYPES.keys())}"
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
),
"traceback": traceback.format_exc(),
},
)
return_type_instance = LANGCHAIN_BASE_TYPES.get(return_type)
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
base_classes = get_base_classes(return_type_instance)
for base_class in base_classes:
@ -268,7 +284,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
frontend_node, field_config, custom_component.get_function_entrypoint_args
)
frontend_node = add_code_field(frontend_node, custom_component.code)
frontend_node = add_code_field(
frontend_node, custom_component.code, field_config.get("code", {})
)
add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type
@ -287,8 +305,8 @@ def load_files_from_path(path: str):
def build_and_validate_all_files(reader, file_list):
"""Build and validate all files"""
data = reader.build_component_menu_list(file_list)
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
invalid_components = reader.filter_loaded_components(data=data, with_errors=True)
return valid_components, invalid_components
@ -341,12 +359,15 @@ def build_invalid_menu(invalid_components):
.get(type(CustomComponent()).__name__)
)
component_template["error"] = component.get("error", None)
component_template.get("template").get("code")["value"] = component_code
invalid_menu[menu_name][component_name] = component_template
except Exception as exc:
logger.error(f"Error while creating custom component: {exc}")
logger.error(
f"Error while creating custom component [{component_name}]: {str(exc)}"
)
return invalid_menu

View file

@ -145,7 +145,7 @@ class CSVAgentNode(FrontendNode):
name="path",
value="",
suffixes=[".csv"],
fileTypes=["csv"],
file_types=["csv"],
),
TemplateField(
field_type="BaseLanguageModel",

View file

@ -53,6 +53,7 @@ class FrontendNode(BaseModel):
output_types: List[str] = []
field_formatters: FieldFormatters = Field(default_factory=FieldFormatters)
beta: bool = False
error: Optional[str] = None
# field formatters is an instance attribute but it is not used in the class
# so we need to create a method to get it
@ -85,6 +86,7 @@ class FrontendNode(BaseModel):
"output_types": self.output_types,
"documentation": self.documentation,
"beta": self.beta,
"error": self.error,
},
}

View file

@ -8,6 +8,7 @@ FORCE_SHOW_FIELDS = [
"headers",
"max_value_length",
"max_tokens",
"google_cse_id",
]
DEFAULT_PROMPT = """

View file

@ -14,7 +14,7 @@ def build_file_field(
name=name,
value="",
suffixes=suffixes,
fileTypes=fileTypes,
file_types=fileTypes,
)

View file

@ -19,7 +19,7 @@ class LLMFrontendNode(FrontendNode):
name="credentials",
value="",
suffixes=[".json"],
fileTypes=["json"],
file_types=["json"],
)
)

View file

@ -9,7 +9,7 @@ from docstring_parser import parse # type: ignore
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants
from langflow.utils.logger import logger
from multiprocess import cpu_count
from multiprocess import cpu_count # type: ignore
def build_template_from_function(
@ -301,13 +301,15 @@ def get_type(value: Any) -> Union[str, type]:
return _type if isinstance(_type, str) else _type.__name__
def remove_optional_wrapper(_type: str) -> str:
def remove_optional_wrapper(_type: Union[str, type]) -> str:
"""
Removes the 'Optional' wrapper from the type string.
Returns:
The type string with the 'Optional' wrapper removed.
"""
if isinstance(_type, type):
_type = str(_type)
if "Optional" in _type:
_type = _type.replace("Optional[", "")[:-1]

View file

@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -29,6 +30,7 @@
"@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2",
"ace-builds": "^1.16.0",
"add": "^2.0.6",
@ -143,6 +145,8 @@
},
"node_modules/@babel/compat-data": {
"version": "7.22.9",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
"integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -1460,6 +1464,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz",
"integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.1.2",
"license": "MIT",
@ -2610,6 +2651,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
"dependencies": {
"axios": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"dev": true,
@ -9533,8 +9583,9 @@
"license": "MIT"
},
"node_modules/word-wrap": {
"version": "1.2.3",
"license": "MIT",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"engines": {
"node": ">=0.10.0"
}

View file

@ -15,6 +15,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -24,6 +25,7 @@
"@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2",
"ace-builds": "^1.16.0",
"add": "^2.0.6",

View file

@ -132,7 +132,7 @@ export default function GenericNode({
</span>
) : (
<div className="max-h-96 overflow-auto">
{validationStatus.params
{typeof validationStatus.params === "string"
? validationStatus.params
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)

View file

@ -1,66 +1,72 @@
import { useContext, useRef } from "react";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../components/ui/popover";
import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { AlertDropdownType } from "../../types/alerts";
import { useOnClickOutside } from "../hooks/useOnClickOutside";
import SingleAlert from "./components/singleAlertComponent";
export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null);
// Use the custom hook
useOnClickOutside(componentRef, () => {
closePopUp();
});
export default function AlertDropdown({ children }: AlertDropdownType) {
const {
notificationList,
clearNotificationList,
removeFromNotificationList,
setNotificationCenter,
} = useContext(alertContext);
const [open, setOpen] = useState(false);
return (
<div
ref={componentRef}
className="z-10 flex h-[500px] w-[400px] flex-col overflow-hidden rounded-md bg-muted px-2 py-3 pb-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
<Popover
open={open}
onOpenChange={(k) => {
setOpen(k);
if (k) setNotificationCenter(false);
}}
>
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">
<button
className="text-foreground hover:text-status-red"
onClick={() => {
closePopUp();
setTimeout(clearNotificationList, 100);
}}
>
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
</button>
<button
className="text-foreground hover:text-status-red"
onClick={closePopUp}
>
<IconComponent name="X" className="h-5 w-5" />
</button>
</div>
</div>
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
No new notifications
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">
<button
className="text-foreground hover:text-status-red"
onClick={() => {
setOpen(false);
setTimeout(clearNotificationList, 100);
}}
>
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
</button>
<button
className="text-foreground hover:text-status-red"
onClick={() => {
setOpen(false);
}}
>
<IconComponent name="X" className="h-5 w-5" />
</button>
</div>
)}
</div>
</div>
</div>
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
No new notifications
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}

View file

@ -12,9 +12,8 @@ import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType) {
const [open, setOpen] = useState(false);
const [isBuilt, setIsBuilt] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { tabsState } = useContext(TabsContext);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -45,7 +45,7 @@ export default function CodeAreaComponent({
editNode
? "input-edit-node input-dialog"
: (disabled ? " input-disable input-ring " : "") +
" input-primary text-muted-foreground "
" primary-input text-muted-foreground "
}
>
{myValue !== "" ? myValue : "Type something..."}

View file

@ -145,7 +145,7 @@ export default function CodeTabsComponent({
</TabsTrigger>
))}
</TabsList>
{Number(activeTab) < 3 && (
{Number(activeTab) < 4 && (
<div className="float-right mx-1 flex gap-2">
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
@ -174,15 +174,23 @@ export default function CodeTabsComponent({
className="api-modal-tabs-content"
key={index} // Remember to add a unique key prop
>
{index < 3 ? (
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
style={oneDark}
>
{tab.code}
</SyntaxHighlighter>
) : index === 3 ? (
{index < 4 ? (
<>
{tab.description && (
<div
className="mb-2 w-full text-left text-sm"
dangerouslySetInnerHTML={{ __html: tab.description }}
></div>
)}
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
style={oneDark}
>
{tab.code}
</SyntaxHighlighter>
</>
) : index === 4 ? (
<>
<div className="api-modal-according-display">
<div

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { Input } from "../ui/input";
export default function FloatComponent({
value,
@ -19,8 +20,8 @@ export default function FloatComponent({
}, [disabled, onChange]);
return (
<div className={"w-full " + (disabled ? "float-component-pointer" : "")}>
<input
<div className="w-full">
<Input
type="number"
step={step}
min={min}
@ -34,12 +35,8 @@ export default function FloatComponent({
}}
max={max}
value={value ?? ""}
className={
"nopan nodrag noundo nocopy " +
(editNode
? "input-edit-node"
: "input-primary" + (disabled ? " input-disable " : ""))
}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={
editNode ? "Number 0 to 1" : "Type a number from zero to one"
}

View file

@ -1,13 +1,11 @@
import { useContext, useEffect, useState } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { Link, useLocation, useParams } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import { getRepoStars } from "../../controllers/API";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -15,14 +13,9 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
export default function Header() {
const { flows, addFlow, tabId } = useContext(TabsContext);
const { openPopUp } = useContext(PopUpContext);
const { templates } = useContext(typesContext);
const { id } = useParams();
const AlertWidth = 384;
const { flows, tabId } = useContext(TabsContext);
const { dark, setDark } = useContext(darkContext);
const { notificationCenter, setNotificationCenter, setErrorData } =
useContext(alertContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const [stars, setStars] = useState(null);
@ -111,33 +104,18 @@ export default function Header() {
<IconComponent name="MoonIcon" className="side-bar-button-size" />
)}
</button>
<button
className="extra-side-bar-save-disable relative"
onClick={(event: React.MouseEvent<HTMLElement>) => {
setNotificationCenter(false);
const { top, left } = (
event.target as Element
).getBoundingClientRect();
openPopUp(
<>
<div
className="absolute z-10"
style={{ top: top + 40, left: left - AlertWidth }}
>
<AlertDropdown />
</div>
<div className="header-notifications-box"></div>
</>
);
}}
>
{notificationCenter && <div className="header-notifications"></div>}
<IconComponent
name="Bell"
className="side-bar-button-size"
aria-hidden="true"
/>
</button>
<AlertDropdown>
<div className="extra-side-bar-save-disable relative">
{notificationCenter && (
<div className="header-notifications"></div>
)}
<IconComponent
name="Bell"
className="side-bar-button-size"
aria-hidden="true"
/>
</div>
</AlertDropdown>
</div>
</div>
</div>

View file

@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function InputComponent({
value,
@ -19,16 +20,15 @@ export default function InputComponent({
}, [disabled, onChange]);
return (
<div className={disabled ? "input-component-div" : "relative"}>
<input
<div className="relative w-full">
<Input
value={value}
disabled={disabled}
className={classNames(
disabled ? " input-disable " : "",
password && !pwdVisible && value !== "" ? " text-clip password " : "",
editNode ? " input-edit-node " : " input-primary ",
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
"nopan nodrag noundo nocopy"
password && !editNode ? "pr-10" : ""
)}
placeholder={password && editNode ? "Key" : "Type something..."}
onChange={(e) => {

View file

@ -102,8 +102,8 @@ export default function InputFileComponent({
editNode
? "input-edit-node input-dialog text-muted-foreground"
: disabled
? "input-disable input-dialog input-primary"
: "input-dialog input-primary text-muted-foreground"
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
}
>
{myValue !== "" ? myValue : "No file"}

View file

@ -2,7 +2,9 @@ import { useEffect } from "react";
import { InputListComponentType } from "../../types/components";
import _ from "lodash";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import { Input } from "../ui/input";
export default function InputListComponent({
value,
@ -18,23 +20,19 @@ export default function InputListComponent({
return (
<div
className={
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
}
)}
>
{value.map((i, idx) => {
return (
<div key={idx} className="flex w-full gap-3">
<input
<Input
disabled={disabled}
type="text"
value={i}
className={
"nopan nodrag noundo nocopy " +
(editNode
? "input-edit-node "
: "input-primary " + (disabled ? "input-disable" : ""))
}
className={editNode ? "input-edit-node" : ""}
placeholder="Type something..."
onChange={(e) => {
let newInputList = _.cloneDeep(value);

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { Input } from "../ui/input";
export default function IntComponent({
value,
@ -17,13 +18,8 @@ export default function IntComponent({
}, [disabled, onChange]);
return (
<div
className={
"w-full " +
(disabled ? "pointer-events-none w-full cursor-not-allowed" : "")
}
>
<input
<div className="w-full">
<Input
onKeyDown={(event) => {
if (
event.key !== "Backspace" &&
@ -51,12 +47,8 @@ export default function IntComponent({
}
}}
value={value ?? ""}
className={
"nopan nodrag noundo nocopy " +
(editNode
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable" : ""))
}
className={editNode ? "input-edit-node" : ""}
disabled={disabled}
placeholder={editNode ? "Integer number" : "Type an integer number"}
onChange={(e) => {
onChange(e.target.value);

View file

@ -51,7 +51,7 @@ export default function PromptAreaComponent({
editNode
? "input-edit-node input-dialog"
: (disabled ? " input-disable text-ring " : "") +
" input-primary text-muted-foreground "
" primary-input text-muted-foreground "
}
>
{value !== "" ? value : "Type your prompt here..."}

View file

@ -3,6 +3,7 @@ import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Input } from "../ui/input";
export default function TextAreaComponent({
value,
@ -18,42 +19,36 @@ export default function TextAreaComponent({
}, [disabled]);
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<div className="flex w-full items-center">
<input
<div className="flex w-full items-center">
<Input
value={value}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={"Type something..."}
onChange={(e) => {
onChange(e.target.value);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
className={
(editNode
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable" : "")) +
" nopan nodrag noundo nocopy w-full"
}
placeholder={"Type something..."}
onChange={(e) => {
onChange(e.target.value);
setValue={(t: string) => {
onChange(t);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
setValue={(t: string) => {
onChange(t);
}}
>
{!editNode && (
<IconComponent
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</GenericModal>
</div>
>
{!editNode && (
<IconComponent
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</GenericModal>
</div>
</div>
);

View file

@ -9,10 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<input
type={type}
className={cn(
"nopan nodrag noundo nocopy flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
className={cn("nopan nodrag noundo nocopy primary-input", className)}
ref={ref}
{...props}
/>

View file

@ -0,0 +1,30 @@
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "../../utils/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -8,10 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
className={cn("nopan nodrag noundo nocopy textarea-primary", className)}
ref={ref}
{...props}
/>

View file

@ -491,3 +491,14 @@ export const NOUNS: string[] = [
*
*/
export const USER_PROJECTS_HEADER = "My Collection";
/**
* URLs excluded from error retries.
* @constant
*
*/
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
];

View file

@ -5,7 +5,6 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { DarkProvider } from "./darkContext";
import { LocationProvider } from "./locationContext";
import PopUpProvider from "./popUpContext";
import { TabsProvider } from "./tabsContext";
import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext";
@ -22,9 +21,7 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<AlertProvider>
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>
<PopUpProvider>{children}</PopUpProvider>
</UndoRedoProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</AlertProvider>

View file

@ -1,38 +0,0 @@
import React, { createContext, useState } from "react";
// context to set JSX element on the DOM
export const PopUpContext = createContext({
openPopUp: (popUpElement: JSX.Element) => {},
closePopUp: () => {},
setCloseEdit: (value: string) => {},
closeEdit: "",
});
interface PopUpProviderProps {
children: React.ReactNode;
}
const PopUpProvider = ({ children }: PopUpProviderProps) => {
const [popUpElements, setPopUpElements] = useState<JSX.Element[]>([]);
const openPopUp = (element: JSX.Element) => {
setPopUpElements((prevPopUps) => [element, ...prevPopUps]);
};
const closePopUp = () => {
setPopUpElements((prevPopUps) => prevPopUps.slice(1));
};
const [closeEdit, setCloseEdit] = useState("");
return (
<PopUpContext.Provider
value={{ openPopUp, closePopUp, closeEdit, setCloseEdit }}
>
{children}
{popUpElements[0]}
</PopUpContext.Provider>
);
};
export default PopUpProvider;

View file

@ -40,6 +40,8 @@ const TabsContextInitialValue: TabsContextType = {
downloadFlows: () => {},
uploadFlows: () => {},
uploadFlow: () => {},
isBuilt: false,
setIsBuilt: (state: boolean) => {},
hardReset: () => {},
saveFlow: async (flow: FlowType) => {},
lastCopiedSelection: null,
@ -583,10 +585,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
}
const [isBuilt, setIsBuilt] = useState(false);
return (
<TabsContext.Provider
value={{
saveFlow,
isBuilt,
setIsBuilt,
lastCopiedSelection,
setLastCopiedSelection,
hardReset,

View file

@ -71,18 +71,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Clear the interval if successful.
clearInterval(intervalId);
} catch (error) {
retryCount++;
// On error, double the delay for the next attempt up to a maximum.
delay = Math.min(30000, delay * 2);
// Log errors but don't do anything else - the function will try again on the next interval.
console.error(error);
// Clear the old interval and start a new one with the new delay.
if (retryCount <= maxRetryCount) {
clearInterval(intervalId);
intervalId = setInterval(getTypes, delay);
} else {
console.error("Max retry attempts reached. Stopping retries.");
}
console.error("An error has occurred while fetching types.");
}
}

View file

@ -0,0 +1,62 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect, useRef } from "react";
import { URL_EXCLUDED_FROM_ERROR_RETRIES } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
baseURL: "",
});
function ApiInterceptor() {
const retryCounts = useRef([]);
const { setErrorData } = useContext(alertContext);
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
return Promise.reject(error);
}
let retryCount = 0;
while (retryCount < 4) {
await sleep(5000); // Sleep for 5 seconds
retryCount++;
try {
const response = await axios.request(error.config);
return response;
} catch (error) {
if (retryCount === 3) {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"Refresh the page",
"Use a new flow tab",
"Check if the backend is up",
"Endpoint: " + error.config?.url,
],
});
return Promise.reject(error);
}
}
}
}
);
return () => {
// Clean up the interceptor when the component unmounts
api.interceptors.response.eject(interceptor);
};
}, [retryCounts]);
return null;
}
// Function to sleep for a given duration in milliseconds
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export { ApiInterceptor, api };

View file

@ -1,5 +1,6 @@
import axios, { AxiosResponse } from "axios";
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
import { api } from "../../controllers/API/api";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import { FlowStyleType, FlowType } from "../../types/flow";
import {
@ -17,16 +18,14 @@ import {
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
*/
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/api/v1/all`);
return await api.get(`/api/v1/all`);
}
const GITHUB_API_URL = "https://api.github.com";
export async function getRepoStars(owner, repo) {
try {
const response = await axios.get(
`${GITHUB_API_URL}/repos/${owner}/${repo}`
);
const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
return response.data.stargazers_count;
} catch (error) {
console.error("Error fetching repository data:", error);
@ -41,13 +40,13 @@ export async function getRepoStars(owner, repo) {
* @returns {AxiosResponse<any>} The API response.
*/
export async function sendAll(data: sendAllProps) {
return await axios.post(`/api/v1/predict`, data);
return await api.post(`/api/v1/predict`, data);
}
export async function postValidateCode(
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await axios.post("/api/v1/validate/code", { code });
return await api.post("/api/v1/validate/code", { code });
}
/**
@ -62,7 +61,7 @@ export async function postValidatePrompt(
template: string,
frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> {
return await axios.post("/api/v1/validate/prompt", {
return await api.post("/api/v1/validate/prompt", {
name: name,
template: template,
frontend_node: frontend_node,
@ -77,14 +76,14 @@ export async function postValidatePrompt(
export async function getExamples(): Promise<FlowType[]> {
const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples?ref=main";
const response = await axios.get(url);
const response = await api.get(url);
const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith(".json");
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url);
const contentResponse = await api.get(file.download_url);
return contentResponse.data;
});
@ -106,11 +105,12 @@ export async function saveFlowToDatabase(newFlow: {
style?: FlowStyleType;
}): Promise<FlowType> {
try {
const response = await axios.post("/api/v1/flows/", {
const response = await api.post("/api/v1/flows/", {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -131,7 +131,7 @@ export async function updateFlowInDatabase(
updatedFlow: FlowType
): Promise<FlowType> {
try {
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
const response = await api.patch(`/api/v1/flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
@ -155,7 +155,7 @@ export async function updateFlowInDatabase(
*/
export async function readFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/");
const response = await api.get("/api/v1/flows/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -168,7 +168,7 @@ export async function readFlowsFromDatabase() {
export async function downloadFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/download/");
const response = await api.get("/api/v1/flows/download/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -181,7 +181,7 @@ export async function downloadFlowsFromDatabase() {
export async function uploadFlowsToDatabase(flows) {
try {
const response = await axios.post(`/api/v1/flows/upload/`, flows);
const response = await api.post(`/api/v1/flows/upload/`, flows);
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
@ -202,7 +202,7 @@ export async function uploadFlowsToDatabase(flows) {
*/
export async function deleteFlowFromDatabase(flowId: string) {
try {
const response = await axios.delete(`/api/v1/flows/${flowId}`);
const response = await api.delete(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -222,7 +222,7 @@ export async function deleteFlowFromDatabase(flowId: string) {
*/
export async function getFlowFromDatabase(flowId: number) {
try {
const response = await axios.get(`/api/v1/flows/${flowId}`);
const response = await api.get(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -241,7 +241,7 @@ export async function getFlowFromDatabase(flowId: number) {
*/
export async function getFlowStylesFromDatabase() {
try {
const response = await axios.get("/api/v1/flow_styles/");
const response = await api.get("/api/v1/flow_styles/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -261,7 +261,7 @@ export async function getFlowStylesFromDatabase() {
*/
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try {
const response = await axios.post("/api/v1/flow_styles/", flowStyle, {
const response = await api.post("/api/v1/flow_styles/", flowStyle, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
@ -284,7 +284,7 @@ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
*/
export async function getVersion() {
const respnose = await axios.get("/api/v1/version");
const respnose = await api.get("/api/v1/version");
return respnose.data;
}
@ -294,7 +294,7 @@ export async function getVersion() {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
*/
export async function getHealth() {
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1
return await api.get("/health"); // Health is the only endpoint that doesn't require /api/v1
}
/**
@ -306,7 +306,7 @@ export async function getHealth() {
export async function getBuildStatus(
flowId: string
): Promise<BuildStatusTypeAPI> {
return await axios.get(`/api/v1/build/${flowId}/status`);
return await api.get(`/api/v1/build/${flowId}/status`);
}
//docs for postbuildinit
@ -319,7 +319,7 @@ export async function getBuildStatus(
export async function postBuildInit(
flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> {
return await axios.post(`/api/v1/build/init/${flow.id}`, flow);
return await api.post(`/api/v1/build/init/${flow.id}`, flow);
}
// fetch(`/upload/${id}`, {
@ -337,12 +337,12 @@ export async function uploadFile(
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
return await axios.post(`/api/v1/upload/${id}`, formData);
return await api.post(`/api/v1/upload/${id}`, formData);
}
export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
return await axios.post(`/api/v1/custom_component`, { code });
return await api.post(`/api/v1/custom_component`, { code });
}

View file

@ -4,7 +4,13 @@ import App from "./App";
import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals";
import "./index.css";
import { ApiInterceptor } from "./controllers/API/api";
// @ts-ignore
import "./style/index.css";
// @ts-ignore
import "./style/applies.css";
// @ts-ignore
import "./style/classes.css";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@ -13,6 +19,7 @@ root.render(
<ContextWrapper>
<BrowserRouter>
<App />
<ApiInterceptor />
</BrowserRouter>
</ContextWrapper>
);

View file

@ -21,6 +21,7 @@ import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
} from "../../utils/utils";
import BaseModal from "../baseModal";
@ -29,9 +30,11 @@ const ApiModal = forwardRef(
{
flow,
children,
disable,
}: {
flow: FlowType;
children: ReactNode;
disable: boolean;
},
ref
) => {
@ -43,6 +46,7 @@ const ApiModal = forwardRef(
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState);
const curl_code = getCurlCode(flow, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState);
const tweaksCode = buildTweaks(flow);
const [tabs, setTabs] = useState([
{
@ -67,6 +71,15 @@ const ApiModal = forwardRef(
language: "py",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
]);
function startState() {
@ -111,6 +124,15 @@ const ApiModal = forwardRef(
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
{
name: "Tweaks",
mode: "python",
@ -143,6 +165,15 @@ const ApiModal = forwardRef(
language: "py",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
]);
}
}, [flow["data"]["nodes"], open]);
@ -210,13 +241,15 @@ const ApiModal = forwardRef(
tweak.current.push(newTweak);
}
const pythonApiCode = getPythonApiCode(flow, tweak.current);
const curl_code = getCurlCode(flow, tweak.current);
const pythonCode = getPythonCode(flow, tweak.current);
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState);
const curl_code = getCurlCode(flow, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState);
tabs[0].code = curl_code;
tabs[1].code = pythonApiCode;
tabs[2].code = pythonCode;
tabs[3].code = widgetCode;
setTweak(tweak.current);
}
@ -253,7 +286,7 @@ const ApiModal = forwardRef(
}
return (
<BaseModal open={open} setOpen={setOpen}>
<BaseModal open={open} setOpen={setOpen} disable={disable}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">Code</span>

View file

@ -46,11 +46,13 @@ interface BaseModalProps {
];
open?: boolean;
setOpen?: (open: boolean) => void;
disable?: boolean;
size?: "smaller" | "small" | "medium" | "large" | "large-h-full";
}
function BaseModal({
open,
setOpen,
disable = false,
children,
size = "large",
}: BaseModalProps) {
@ -99,7 +101,10 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full" hidden={triggerChild ? false : true}>
<DialogTrigger
className={"w-full " + (disable ? "button-disable" : "")}
hidden={triggerChild ? false : true}
>
{triggerChild}
</DialogTrigger>
<DialogContent className={minWidth}>

View file

@ -96,6 +96,7 @@ export default function CodeAreaModal({
if (data) {
setNodeClass(data);
setValue(code);
setError({ detail: { error: undefined, traceback: undefined } });
setOpen(false);
}
})

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import IconComponent from "../../../components/genericIconComponent";
import { Textarea } from "../../../components/ui/textarea";
import { classNames } from "../../../utils/utils";
export default function ChatInput({
@ -25,7 +26,7 @@ export default function ChatInput({
return (
<div className="relative">
<textarea
<Textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
@ -68,10 +69,10 @@ export default function ChatInput({
className={classNames(
"form-modal-send-button",
noInput
? "bg-indigo-600 text-background"
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-emerald-600 text-background"
: "bg-chat-send text-background"
)}
disabled={lockChat}
onClick={() => sendMessage()}

View file

@ -395,7 +395,7 @@ export default function FormModal({
<span className="pr-2">Chat</span>
<IconComponent
name="prompts"
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</DialogTitle>

View file

@ -1,6 +1,7 @@
import { useContext, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext";
import { TabsContext } from "../../../../contexts/tabsContext";
@ -18,7 +19,7 @@ import DisclosureComponent from "../DisclosureComponent";
export default function ExtraSidebar() {
const { data } = useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow } =
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
useContext(TabsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
@ -60,56 +61,72 @@ export default function ExtraSidebar() {
return (
<div className="side-bar-arrangement">
<div className="side-bar-buttons-arrangement">
<ShadTooltip content="Import" side="top">
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
<ShadTooltip content="Export" side="top">
<div className="side-bar-button">
<ShadTooltip content="Import" side="top">
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
</div>
<div className="side-bar-button">
<ExportModal>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</div>
</ExportModal>
</ShadTooltip>
<ShadTooltip content="Code" side="top">
{flow && flow.data && (
<ApiModal flow={flow}>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="Code2" className="side-bar-button-size" />
<IconComponent
name="FileDown"
className="side-bar-button-size"
/>
</div>
</ApiModal>
)}
</ShadTooltip>
</ExportModal>
</div>
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
<ApiModal flow={flow} disable={!isBuilt}>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
</ApiModal>
)}
</div>
</ShadTooltip>
<ShadTooltip content="Save" side="top">
<button
className="extra-side-bar-buttons"
onClick={(event) => {
saveFlow(flow);
setSuccessData({ title: "Changes saved successfully" });
}}
disabled={!isPending}
>
<IconComponent
name="Save"
<div className="side-bar-button">
<ShadTooltip content="Save" side="top">
<button
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
}
/>
</button>
</ShadTooltip>
onClick={(event) => {
saveFlow(flow);
setSuccessData({ title: "Changes saved successfully" });
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
}
/>
</button>
</ShadTooltip>
</div>
</div>
<Separator />
<div className="side-bar-search-div-placement">
<input
<Input
type="text"
name="search"
id="search"

View file

@ -2,127 +2,6 @@
@tailwind components;
@tailwind utilities;
/* TODO: Confirm that all colors here are found in tailwind config */
@layer base {
:root {
--background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--error-background: #fef2f2;
--error-foreground: #991b1b;
--success-background: #f0fdf4;
--success-foreground: #14532d;
--info-background: #f0f4fd;
--info-foreground: #141653;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--chat-bot-icon: #afe6ef;
--chat-user-icon: #aface9;
--beta-background: rgb(219 234 254);
--beta-foreground: rgb(37 99 235);
/* Colors that are shared in dark and light mode */
--blur-shared: #151923de;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #b4c3da;
--status-red: #ef4444;
--status-yellow: #eab308;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
}
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--card: 224 25% 15.5%; /* hsl(224 71% 4%) */
--card-foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--border: 216 24% 17%; /* hsl(216 34% 17%) */
--input: 216 24% 17%; /* hsl(216 34% 17%) */
--primary: 210 20% 80%; /* hsl(210 20% 80%) */
--primary-foreground: 222.2 27.4% 1.2%; /* hsl(222 47% 1%) */
--secondary: 222.2 37.4% 7.2%; /* hsl(222 47% 11%) */
--secondary-foreground: 210 40% 80%; /* hsl(210 40% 80%) */
--accent: 216 24% 20%; /* hsl(216 34% 17%) */
--accent-foreground: 210 30% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 24% 30%; /* hsl(216 24% 30%) */
--radius: 0.5rem;
--round-btn-shadow: #00000063;
--success-background: #022c22;
--success-foreground: #ecfdf5;
--error-foreground: #fef2f2;
--error-background: #450a0a;
--info-foreground: #eff6ff;
--info-background: #172554;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--beta-background: rgb(37 99 235);
--beta-foreground: rgb(219 234 254);
/* Colors that are shared in dark and light mode */
--blur-shared: #151923d2;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #2d3b54;
--status-red: #ef4444;
--status-yellow: #eab308;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
--chat-bot-icon: #235d70;
--chat-user-icon: #4f3d6e;
}
}
@layer base {
* {
@apply border-border;
@ -134,39 +13,6 @@
}
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
pre {
font-family: inherit;
}
/* The style below sets the cursor property of the element with the class .react-flow__pane to the default cursor.
The cursor: default; property value restores the browser's default cursor style for the targeted element. By applying this style, the element will no longer have a custom cursor appearance such as "grab" or any other custom cursor defined elsewhere in the application. Instead, it will revert to the default cursor style determined by the browser, typically an arrow-shaped cursor. */
.react-flow__pane {
cursor: default;
}
.AccordionContent {
overflow: hidden;
}
.AccordionContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.AccordionContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
@ -185,6 +31,36 @@ The cursor: default; property value restores the browser's default cursor style
}
}
@keyframes gradient-motion-start {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 130, 184);
}
80% {
stop-color: rgb(255, 165, 100);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@keyframes gradient-motion-end {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 165, 100);
}
80% {
stop-color: rgb(255, 130, 184);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@layer components {
.round-buttons-position {
@apply fixed right-4;
@ -231,6 +107,12 @@ The cursor: default; property value restores the browser's default cursor style
.side-bar-buttons-arrangement {
@apply mb-2 mt-2 flex w-full items-center justify-between gap-2 px-2;
}
.side-bar-button {
@apply flex w-full;
}
.button-disable {
@apply pointer-events-none;
}
.extra-side-bar-buttons {
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
}
@ -240,18 +122,20 @@ The cursor: default; property value restores the browser's default cursor style
.button-div-style {
@apply flex gap-2;
}
.input-primary:focus {
@apply focus:border-ring focus:placeholder-transparent focus:ring-ring;
}
.input-primary {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground sm:text-sm;
.primary-input {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
}
.input-edit-node{
@apply input-primary border-border pt-0.5 pb-0.5 text-left w-full
/* The same as primary-input but no-truncate */
.textarea-primary {
@apply form-input block w-full rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
}
.input-edit-node {
@apply primary-input w-full pb-0.5 pt-0.5 text-left;
}
.input-search {
@apply input-primary mx-2 pr-7;
@apply primary-input mx-2 pr-7;
}
.input-disable {
@apply border-transparent bg-border placeholder:text-ring;
@ -375,7 +259,7 @@ The cursor: default; property value restores the browser's default cursor style
}
.generic-node-status-position {
@apply relative top-[1.5px] h-5 w-5;
@apply relative top-[3px] h-5 w-5;
}
.generic-node-status-animation {
@ -536,7 +420,7 @@ The cursor: default; property value restores the browser's default cursor style
@apply input-edit-node relative pr-8;
}
.dropdown-component-false-outline {
@apply input-primary py-2 pl-3 pr-10 text-left;
@apply primary-input py-2 pl-3 pr-10 text-left;
}
.dropdown-component-display {
@apply block w-full truncate bg-background;
@ -551,7 +435,7 @@ The cursor: default; property value restores the browser's default cursor style
@apply z-10 mt-1 max-h-60 overflow-auto rounded-md bg-background py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
}
.dropdown-component-true-options {
@apply dropdown-component-options lg:w-[32%]
@apply dropdown-component-options lg:w-[32%];
}
.dropdown-component-false-options {
@apply dropdown-component-options w-full;
@ -597,7 +481,7 @@ The cursor: default; property value restores the browser's default cursor style
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-b bg-muted;
@apply flex-max-width h-12 items-center justify-between border-border bg-muted;
}
.header-start-display {
@apply flex w-96 items-center justify-start gap-2;
@ -902,6 +786,9 @@ The cursor: default; property value restores the browser's default cursor style
.node-modal-button-box {
@apply flex-max-width flex-row-reverse bg-input px-4 pb-3;
}
.link-color {
@apply font-semibold text-foreground;
}
.node-modal-button {
@apply inline-flex w-full justify-center rounded-md border border-transparent bg-status-red px-4 py-2 text-base font-medium text-background shadow-sm hover:bg-ring sm:ml-3 sm:w-auto sm:text-sm;
}
@ -1117,54 +1004,19 @@ The cursor: default; property value restores the browser's default cursor style
.ace-editor-save-btn {
@apply flex-max-width h-fit justify-end;
}
.export-modal-save-api {
@apply font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
}
.beta-badge-wrapper {
@apply absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg;
}
.beta-badge-content {
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground
}
.export-modal-save-api {
@apply font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground;
}
.chat-message-highlight {
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
}
}
@keyframes gradient-motion-start {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 130, 184);
}
80% {
stop-color: rgb(255, 165, 100);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@keyframes gradient-motion-end {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 165, 100);
}
80% {
stop-color: rgb(255, 130, 184);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
.gradient-end {
animation: gradient-motion-end 3s infinite forwards;
}
.gradient-start {
animation: gradient-motion-start 4s infinite forwards;
}

View file

@ -0,0 +1,38 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
pre {
font-family: inherit;
}
.react-flow__pane {
cursor: default;
}
.AccordionContent {
overflow: hidden;
}
.AccordionContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.AccordionContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
.gradient-end {
animation: gradient-motion-end 3s infinite forwards;
}
.gradient-start {
animation: gradient-motion-start 4s infinite forwards;
}

View file

@ -0,0 +1,128 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* TODO: Confirm that all colors here are found in tailwind config */
@layer base {
:root {
--background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--error-background: #fef2f2;
--error-foreground: #991b1b;
--success-background: #f0fdf4;
--success-foreground: #14532d;
--info-background: #f0f4fd;
--info-foreground: #141653;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--low-indigo: #e0e7ff;
--beta-background: rgb(219 234 254);
--beta-foreground: rgb(37 99 235);
--chat-bot-icon: #afe6ef;
--chat-user-icon: #aface9;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923de;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #b4c3da;
--status-red: #ef4444;
--status-yellow: #eab308;
--chat-send: #059669;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
}
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--card: 224 25% 15.5%; /* hsl(224 71% 4%) */
--card-foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--border: 216 24% 17%; /* hsl(216 34% 17%) */
--input: 216 24% 17%; /* hsl(216 34% 17%) */
--primary: 210 20% 80%; /* hsl(210 20% 80%) */
--primary-foreground: 222.2 27.4% 1.2%; /* hsl(222 47% 1%) */
--secondary: 222.2 37.4% 7.2%; /* hsl(222 47% 11%) */
--secondary-foreground: 210 40% 80%; /* hsl(210 40% 80%) */
--accent: 216 24% 20%; /* hsl(216 34% 17%) */
--accent-foreground: 210 30% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 24% 30%; /* hsl(216 24% 30%) */
--radius: 0.5rem;
--round-btn-shadow: #00000063;
--success-background: #022c22;
--success-foreground: #ecfdf5;
--error-foreground: #fef2f2;
--error-background: #450a0a;
--info-foreground: #eff6ff;
--info-background: #172554;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--low-indigo: #e0e7ff;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923d2;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #2d3b54;
--status-red: #ef4444;
--status-yellow: #eab308;
--chat-send: #059669;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
--beta-background: rgb(37 99 235);
--beta-foreground: rgb(219 234 254);
--chat-bot-icon: #235d70;
--chat-user-icon: #4f3d6e;
}
}

View file

@ -19,7 +19,9 @@ export type SingleAlertComponentType = {
dropItem: AlertItemType;
removeAlert: (index: string) => void;
};
export type AlertDropdownType = {};
export type AlertDropdownType = {
children: JSX.Element;
};
export type AlertItemType = {
type: "notice" | "error" | "success";
title: string;

View file

@ -18,6 +18,8 @@ export type TabsContextType = {
) => void;
downloadFlows: () => void;
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow?: boolean, file?: File) => void;
hardReset: () => void;
getNodeId: (nodeType: string) => string;

View file

@ -467,7 +467,7 @@ export function getCurlCode(
/**
* Function to get the python code for the API
* @param {string} flowName - The name of the flow
* @param {string} flow - The current flow
* @returns {string} - The python code
*/
export function getPythonCode(
@ -489,3 +489,33 @@ flow = load_flow_from_json("${flowName}.json", tweaks=TWEAKS)
inputs = ${inputs}
flow(inputs)`;
}
/**
* Function to get the widget code for the API
* @param {string} flow - The current flow.
* @returns {string} - The widget code
*/
export function getWidgetCode(flow: FlowType, tabsState?: TabsState): string {
const flowId = flow.id;
const flowName = flow.name;
const inputs = buildInputs(tabsState, flow.id);
return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script>
<!-- chat_inputs: Stringified JSON with all the input keys and its values. The value of the key that is defined
as chat_input_field will be overwritten by the chat message.
chat_input_field: Input key that you want the chat to send the user message with. -->
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
${
tabsState[flow.id] && tabsState[flow.id].formKeysData
? `chat_inputs='${inputs}'
chat_input_field="${
Object.keys(tabsState[flow.id].formKeysData.input_keys)[0]
}"
`
: ""
}host_url="http://localhost:7860"
></langflow-chat>`;
}

View file

@ -29,6 +29,8 @@ module.exports = {
},
extend: {
colors: {
"low-indigo": "var(--low-indigo)",
"chat-send": "var(--chat-send)",
connection: "var(--connection)",
"almost-dark-gray": "var(--almost-dark-gray)",
"almost-light-blue": "var(--almost-light-blue)",

View file

@ -170,6 +170,7 @@ def test_csv_agent(client: TestClient):
"multiline": False,
"value": "",
"suffixes": [".csv"],
"fileTypes": ["csv"],
"password": False,
"name": "path",
"type": "file",

View file

@ -1,8 +1,11 @@
import ast
import pytest
import types
from uuid import uuid4
from fastapi import HTTPException
from langflow.database.models.flow import Flow, FlowCreate
from langflow.interface.custom.base import CustomComponent
from langflow.interface.custom.component import (
Component,
@ -56,7 +59,8 @@ def test_code_parser_get_tree():
def test_code_parser_syntax_error():
"""
Test the __get_tree method raises the CodeSyntaxError when given incorrect syntax.
Test the __get_tree method raises the
CodeSyntaxError when given incorrect syntax.
"""
code_syntax_error = "zzz import os"
@ -85,7 +89,8 @@ def test_component_get_code_tree():
def test_component_code_null_error():
"""
Test the get_function method raises the ComponentCodeNullError when the code is empty.
Test the get_function method raises the
ComponentCodeNullError when the code is empty.
"""
component = Component(code="", function_entrypoint_name="")
with pytest.raises(ComponentCodeNullError):
@ -139,7 +144,8 @@ def test_custom_component_get_function():
def test_code_parser_parse_imports_import():
"""
Test the parse_imports method of the CodeParser class with an import statement.
Test the parse_imports method of the CodeParser
class with an import statement.
"""
parser = CodeParser(code_default)
tree = parser._CodeParser__get_tree()
@ -151,7 +157,8 @@ def test_code_parser_parse_imports_import():
def test_code_parser_parse_imports_importfrom():
"""
Test the parse_imports method of the CodeParser class with an import from statement.
Test the parse_imports method of the CodeParser
class with an import from statement.
"""
parser = CodeParser("from os import path")
tree = parser._CodeParser__get_tree()
@ -202,16 +209,18 @@ def test_code_parser_parse_global_vars():
def test_component_get_function_valid():
"""
Test the get_function method of the Component class with valid code and function_entrypoint_name.
Test the get_function method of the Component
class with valid code and function_entrypoint_name.
"""
component = Component(code="def build(): pass", function_entrypoint_name="build")
function = component.get_function()
assert callable(function)
my_function = component.get_function()
assert callable(my_function)
def test_custom_component_get_function_entrypoint_args():
"""
Test the get_function_entrypoint_args property of the CustomComponent class.
Test the get_function_entrypoint_args
property of the CustomComponent class.
"""
custom_component = CustomComponent(
code=code_default, function_entrypoint_name="build"
@ -225,7 +234,8 @@ def test_custom_component_get_function_entrypoint_args():
def test_custom_component_get_function_entrypoint_return_type():
"""
Test the get_function_entrypoint_return_type property of the CustomComponent class.
Test the get_function_entrypoint_return_type
property of the CustomComponent class.
"""
custom_component = CustomComponent(
code=code_default, function_entrypoint_name="build"
@ -247,7 +257,8 @@ def test_custom_component_get_main_class_name():
def test_custom_component_get_function_valid():
"""
Test the get_function property of the CustomComponent class with valid code and function_entrypoint_name.
Test the get_function property of the CustomComponent
class with valid code and function_entrypoint_name.
"""
custom_component = CustomComponent(
code="def build(): pass", function_entrypoint_name="build"
@ -280,7 +291,8 @@ def test_code_parser_parse_arg_with_annotation():
def test_code_parser_parse_callable_details_no_args():
"""
Test the parse_callable_details method of the CodeParser class with a function with no arguments.
Test the parse_callable_details method of the
CodeParser class with a function with no arguments.
"""
parser = CodeParser("")
node = ast.FunctionDef(
@ -327,7 +339,8 @@ def test_code_parser_parse_ann_assign():
def test_code_parser_parse_function_def_not_init():
"""
Test the parse_function_def method of the CodeParser class with a function that is not __init__.
Test the parse_function_def method of the
CodeParser class with a function that is not __init__.
"""
parser = CodeParser("")
stmt = ast.FunctionDef(
@ -346,7 +359,8 @@ def test_code_parser_parse_function_def_not_init():
def test_code_parser_parse_function_def_init():
"""
Test the parse_function_def method of the CodeParser class with an __init__ function.
Test the parse_function_def method of the
CodeParser class with an __init__ function.
"""
parser = CodeParser("")
stmt = ast.FunctionDef(
@ -385,7 +399,8 @@ def test_custom_component_class_template_validation_no_code():
def test_custom_component_get_code_tree_syntax_error():
"""
Test the get_code_tree method of the CustomComponent class raises the CodeSyntaxError when given incorrect syntax.
Test the get_code_tree method of the CustomComponent class
raises the CodeSyntaxError when given incorrect syntax.
"""
custom_component = CustomComponent(
code="import os as", function_entrypoint_name="build"
@ -396,7 +411,8 @@ def test_custom_component_get_code_tree_syntax_error():
def test_custom_component_get_function_entrypoint_args_no_args():
"""
Test the get_function_entrypoint_args property of the CustomComponent class with a build method with no arguments.
Test the get_function_entrypoint_args property of
the CustomComponent class with a build method with no arguments.
"""
my_code = """
class MyMainClass(CustomComponent):
@ -425,7 +441,8 @@ class MyClass(CustomComponent):
def test_custom_component_get_main_class_name_no_main_class():
"""
Test the get_main_class_name property of the CustomComponent class when there is no main class.
Test the get_main_class_name property of the
CustomComponent class when there is no main class.
"""
my_code = """
def build():
@ -438,7 +455,8 @@ def build():
def test_custom_component_build_not_implemented():
"""
Test the build method of the CustomComponent class raises the NotImplementedError.
Test the build method of the CustomComponent
class raises the NotImplementedError.
"""
custom_component = CustomComponent(
code="def build(): pass", function_entrypoint_name="build"
@ -447,462 +465,94 @@ def test_custom_component_build_not_implemented():
custom_component.build()
# -------------------------------------------------------
# @pytest.fixture
# def custom_chain():
# return '''
# from __future__ import annotations
# from typing import Any, Dict, List, Optional
def test_build_config_no_code():
component = CustomComponent(code=None)
# from pydantic import Extra
assert component.get_function_entrypoint_args == ""
assert component.get_function_entrypoint_return_type == ""
# from langchain.schema import BaseLanguageModel, Document
# from langchain.callbacks.manager import (
# AsyncCallbackManagerForChainRun,
# CallbackManagerForChainRun,
# )
# from langchain.chains.base import Chain
# from langchain.prompts import StringPromptTemplate
# from langflow.interface.custom.base import CustomComponent
# class MyCustomChain(Chain):
# """
# An example of a custom chain.
# """
@pytest.fixture
def component():
return CustomComponent(
field_config={
"fields": {
"llm": {"type": "str"},
"url": {"type": "str"},
"year": {"type": "int"},
}
}
)
# from typing import Any, Dict, List, Optional
# from pydantic import Extra
@pytest.fixture(scope="session")
def test_flow(db):
flow_data = {
"nodes": [{"id": "1"}, {"id": "2"}],
"edges": [{"source": "1", "target": "2"}],
}
# from langchain.schema import BaseLanguageModel, Document
# from langchain.callbacks.manager import (
# AsyncCallbackManagerForChainRun,
# CallbackManagerForChainRun,
# )
# from langchain.chains.base import Chain
# from langchain.prompts import StringPromptTemplate
# from langflow.interface.custom.base import CustomComponent
# Create flow
flow = FlowCreate(
id=uuid4(), name="Test Flow", description="Fixture flow", data=flow_data
)
# class MyCustomChain(Chain):
# """
# An example of a custom chain.
# """
# Add to database
db.add(flow)
db.commit()
# prompt: StringPromptTemplate
# """Prompt object to use."""
# llm: BaseLanguageModel
# output_key: str = "text" #: :meta private:
yield flow
# class Config:
# """Configuration for this pydantic object."""
# Clean up
db.delete(flow)
db.commit()
# extra = Extra.forbid
# arbitrary_types_allowed = True
# @property
# def input_keys(self) -> List[str]:
# """Will be whatever keys the prompt expects.
@pytest.fixture(scope="session")
def db(app):
# Setup database for tests
yield app.db
# :meta private:
# """
# return self.prompt.input_variables
# Teardown
app.db.drop_all()
# @property
# def output_keys(self) -> List[str]:
# """Will always return text key.
# :meta private:
# """
# return [self.output_key]
def test_list_flows_return_type(component):
flows = component.list_flows()
assert isinstance(flows, list)
# def _call(
# self,
# inputs: Dict[str, Any],
# run_manager: Optional[CallbackManagerForChainRun] = None,
# ) -> Dict[str, str]:
# # Your custom chain logic goes here
# # This is just an example that mimics LLMChain
# prompt_value = self.prompt.format_prompt(**inputs)
# # Whenever you call a language model, or another chain, you should pass
# # a callback manager to it. This allows the inner run to be tracked by
# # any callbacks that are registered on the outer run.
# # You can always obtain a callback manager for this by calling
# # `run_manager.get_child()` as shown below.
# response = self.llm.generate_prompt(
# [prompt_value],
# callbacks=run_manager.get_child() if run_manager else None,
# )
def test_list_flows_flow_objects(component):
flows = component.list_flows()
assert all(isinstance(flow, Flow) for flow in flows)
# # If you want to log something about this run, you can do so by calling
# # methods on the `run_manager`, as shown below. This will trigger any
# # callbacks that are registered for that event.
# if run_manager:
# run_manager.on_text("Log something about this run")
# return {self.output_key: response.generations[0][0].text}
def test_build_config_return_type(component):
config = component.build_config()
assert isinstance(config, dict)
# async def _acall(
# self,
# inputs: Dict[str, Any],
# run_manager: Optional[AsyncCallbackManagerForChainRun] = None,
# ) -> Dict[str, str]:
# # Your custom chain logic goes here
# # This is just an example that mimics LLMChain
# prompt_value = self.prompt.format_prompt(**inputs)
# # Whenever you call a language model, or another chain, you should pass
# # a callback manager to it. This allows the inner run to be tracked by
# # any callbacks that are registered on the outer run.
# # You can always obtain a callback manager for this by calling
# # `run_manager.get_child()` as shown below.
# response = await self.llm.agenerate_prompt(
# [prompt_value],
# callbacks=run_manager.get_child() if run_manager else None,
# )
def test_build_config_has_fields(component):
config = component.build_config()
assert "fields" in config
# # If you want to log something about this run, you can do so by calling
# # methods on the `run_manager`, as shown below. This will trigger any
# # callbacks that are registered for that event.
# if run_manager:
# await run_manager.on_text("Log something about this run")
# return {self.output_key: response.generations[0][0].text}
def test_build_config_fields_dict(component):
config = component.build_config()
assert isinstance(config["fields"], dict)
# @property
# def _chain_type(self) -> str:
# return "my_custom_chain"
# class CustomChain(CustomComponent):
# display_name: str = "Custom Chain"
# field_config = {
# "prompt": {"field_type": "prompt"},
# "llm": {"field_type": "BaseLanguageModel"},
# }
def test_build_config_field_keys(component):
config = component.build_config()
assert all(isinstance(key, str) for key in config["fields"])
# def build(self, prompt, llm, input: str) -> Document:
# chain = MyCustomChain(prompt=prompt, llm=llm)
# return chain(input)
# '''
def test_build_config_field_values_dict(component):
config = component.build_config()
assert all(isinstance(value, dict) for value in config["fields"].values())
# @pytest.fixture
# def data_processing():
# return """
# import pandas as pd
# from langchain.schema import Document
# from langflow.interface.custom.base import CustomComponent
# class CSVLoaderComponent(CustomComponent):
# display_name: str = "CSV Loader"
# field_config = {
# "filename": {"field_type": "str", "required": True},
# "column_name": {"field_type": "str", "required": True},
# }
# def build(self, filename: str, column_name: str) -> Document:
# # Load the CSV file
# df = pd.read_csv(filename)
# # Verify the column exists
# if column_name not in df.columns:
# raise ValueError(f"Column '{column_name}' not found in the CSV file")
# # Convert each row of the specified column to a document object
# documents = []
# for content in df[column_name]:
# metadata = {"filename": filename}
# documents.append(Document(page_content=str(content), metadata=metadata))
# return documents
# """
# @pytest.fixture
# def filter_docs():
# return """
# from langchain.schema import Document
# from langflow.interface.custom.base import CustomComponent
# from typing import List
# class DocumentFilterByLengthComponent(CustomComponent):
# display_name: str = "Document Filter By Length"
# field_config = {
# "documents": {"field_type": "Document", "required": True},
# "max_length": {"field_type": "int", "required": True},
# }
# def build(self, documents: List[Document], max_length: int) -> List[Document]:
# # Filter the documents by length
# filtered_documents = [doc for doc in documents if len(doc.page_content) <= max_length]
# return filtered_documents
# """
# @pytest.fixture
# def get_request():
# return """
# import requests
# from typing import Dict, Union
# from langchain.schema import Document
# from langflow.interface.custom.base import CustomComponent
# class GetRequestComponent(CustomComponent):
# display_name: str = "GET Request"
# field_config = {
# "url": {"field_type": "str", "required": True},
# }
# def build(self, url: str) -> Document:
# # Send a GET request to the URL
# response = requests.get(url)
# # Raise an exception if the request was not successful
# if response.status_code != 200:
# raise ValueError(f"GET request failed: {response.status_code} status code")
# # Create a document with the response text and the URL as metadata
# document = Document(page_content=response.text, metadata={"url": url})
# return document
# """
# @pytest.fixture
# def post_request():
# return """
# import requests
# from typing import Dict, Union
# from langchain.schema import Document
# from langflow.interface.custom.base import CustomComponent
# class PostRequestComponent(CustomComponent):
# display_name: str = "POST Request"
# field_config = {
# "url": {"field_type": "str", "required": True},
# "data": {"field_type": "dict", "required": True},
# }
# def build(self, url: str, data: Dict[str, Union[str, int]]) -> Document:
# # Send a POST request to the URL
# response = requests.post(url, data=data)
# # Raise an exception if the request was not successful
# if response.status_code != 200:
# raise ValueError(f"POST request failed: {response.status_code} status code")
# # Create a document with the response text and the URL and data as metadata
# document = Document(page_content=response.text, metadata={"url": url, "data": data})
# return document
# """
# @pytest.fixture
# def code_default():
# return """
# from langflow import Prompt
# from langflow.interface.custom.custom_component import CustomComponent
# from langchain.llms.base import BaseLLM
# from langchain.chains import LLMChain
# from langchain import PromptTemplate
# from langchain.schema import Document
# import requests
# class YourComponent(CustomComponent):
# #display_name: str = "Your Component"
# #description: str = "Your description"
# #field_config = { "url": { "multiline": True, "required": True } }
# def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document:
# response = requests.get(url)
# prompt = PromptTemplate.from_template(template)
# chain = LLMChain(llm=llm, prompt=prompt)
# result = chain.run(response.text[:300])
# return Document(page_content=str(result))
# """
# @pytest.fixture(params=[
# 'code_default', 'custom_chain', 'data_processing',
# 'filter_docs', 'get_request', 'post_request'])
# def component_code(
# request, code_default, custom_chain, data_processing,
# filter_docs, get_request, post_request):
# return locals()[request.param]
# def test_empty_code_tree(component_code):
# """
# Test the situation when the code tree is empty.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {}
# assert cc.get_function_entrypoint_args == ''
# assert cc.get_function_entrypoint_return_type == ''
# assert cc.get_main_class_name == ''
# assert cc.build_template_config == {}
# def test_class_template_validation(component_code):
# """
# Test the _class_template_validation method.
# """
# cc = CustomComponent(code=component_code)
# assert cc._class_template_validation(component_code) == True
# with pytest.raises(HTTPException):
# cc._class_template_validation(None)
# def test_get_code_tree(component_code):
# """
# Test the get_code_tree method.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {'classes': []}
# assert cc.get_code_tree(component_code) == {'classes': []}
# def test_get_function_entrypoint_args(component_code):
# """
# Test the get_function_entrypoint_args method.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {'classes': []}
# assert cc.get_function_entrypoint_args == ''
# def test_get_function_entrypoint_return_type(component_code):
# """
# Test the get_function_entrypoint_return_type method.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {'classes': []}
# assert cc.get_function_entrypoint_return_type == ''
# def test_get_main_class_name(component_code):
# """
# Test the get_main_class_name method.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {'classes': []}
# assert cc.get_main_class_name == ''
# def test_build_template_config(component_code):
# """
# Test the build_template_config method.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {
# 'classes': [{'name': '', 'attributes': []}]}
# assert cc.build_template_config == {}
# def test_get_function(component_code):
# """
# Test the get_function method.
# """
# cc = CustomComponent(code=component_code, function_entrypoint_name='build')
# assert callable(cc.get_function)
# def test_build(component_code):
# """
# Test the build method.
# """
# cc = CustomComponent(code=component_code)
# with pytest.raises(NotImplementedError):
# cc.build()
# @pytest.mark.parametrize("entrypoint_name", ["build", "non_exist_method"])
# def test_set_non_existing_function_entrypoint_name(component_code, entrypoint_name):
# """
# Test setting a non-existing function entrypoint name.
# """
# cc = CustomComponent(
# code=component_code,
# function_entrypoint_name=entrypoint_name
# )
# with pytest.raises(AttributeError):
# cc.get_function
# @pytest.mark.parametrize("base_class", ["CustomComponent", "NonExistingClass"])
# def test_set_non_existing_base_class(component_code, base_class):
# """
# Test setting a non-existing base class.
# """
# cc = CustomComponent(code=component_code)
# cc.code_class_base_inheritance = base_class
# with pytest.raises(AttributeError):
# cc.get_main_class_name
# def test_class_with_no_methods(component_code):
# """
# Test a component class with no methods.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {
# 'classes': [
# {
# 'name': 'CustomComponent',
# 'methods': [],
# 'bases': ['CustomComponent']
# }
# ]
# }
# assert cc.get_function_entrypoint_args == ''
# assert cc.get_function_entrypoint_return_type == ''
# def test_class_with_no_bases(component_code):
# """
# Test a component class with no bases.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {
# 'classes': [
# {
# 'name': 'CustomComponent',
# 'methods': [],
# 'bases': []
# }
# ]
# }
# assert cc.get_function_entrypoint_args == ''
# assert cc.get_function_entrypoint_return_type == ''
# def test_class_with_no_name(component_code):
# """
# Test a component class with no name.
# """
# cc = CustomComponent(code=component_code)
# with patch.object(cc, 'get_code_tree') as mocked_get_code_tree:
# mocked_get_code_tree.return_value = {'classes': [
# {'name': '', 'methods': [], 'bases': ['CustomComponent']}]}
# assert cc.get_main_class_name == ''
# @pytest.mark.parametrize("input_code", ["", "not a valid python code"])
# def test_invalid_input_code(input_code):
# """
# Test inputting an invalid Python code.
# """
# with pytest.raises(SyntaxError):
# cc = CustomComponent(code=input_code)
def test_build_config_field_value_keys(component):
config = component.build_config()
field_values = config["fields"].values()
assert all("type" in value for value in field_values)