merge dev into feat-more

This commit is contained in:
igorrCarvalho 2023-09-21 15:17:08 -03:00
commit e4ba7364bb
345 changed files with 18856 additions and 8899 deletions

View file

@ -1,32 +1,33 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal // README at: https://github.com/devcontainers/templates/tree/main/src/universal
{ {
"name": "LangChain Demo Container", "name": "Langflow Demo Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.10", "image": "mcr.microsoft.com/devcontainers/python:3.10",
"features": { "features": {
"ghcr.io/devcontainers/features/aws-cli:1": {}, "ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}, "ghcr.io/devcontainers/features/docker-in-docker": {},
"ghcr.io/devcontainers/features/node": {} "ghcr.io/devcontainers/features/node": {}
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"actboy168.tasks", "actboy168.tasks",
"GitHub.copilot", "GitHub.copilot",
"ms-python.python", "ms-python.python",
"eamodio.gitlens" "eamodio.gitlens",
] "GitHub.vscode-pull-request-github"
} ]
}, }
// Features to add to the dev container. More info: https://containers.dev/features. },
// "features": {}, // Features to add to the dev container. More info: https://containers.dev/features.
// Use 'forwardPorts' to make a list of ports inside the container available locally. // "features": {},
// "forwardPorts": [], // Use 'forwardPorts' to make a list of ports inside the container available locally.
// Use 'postCreateCommand' to run commands after the container is created. // "forwardPorts": [],
"postCreateCommand": "pipx install 'langflow>=0.0.33' && langflow --host 0.0.0.0" // Use 'postCreateCommand' to run commands after the container is created.
// Configure tool-specific properties. "postCreateCommand": "pipx install 'langflow>=0.0.33' && langflow --host 0.0.0.0"
// "customizations": {}, // Configure tool-specific properties.
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "customizations": {},
// "remoteUser": "root" // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
} }

View file

@ -1,35 +1,41 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal // README at: https://github.com/devcontainers/templates/tree/main/src/universal
{ {
"name": "LangChain Dev Container", "name": "Langflow Dev Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/universal:2-linux", "image": "mcr.microsoft.com/devcontainers/python:1-3.10-bullseye",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}
},
"customizations": {
"vscode": {"extensions": [
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"sourcery.sourcery",
"eamodio.gitlens"
]}
},
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
// "features": {}, "features": {
"ghcr.io/devcontainers/features/node": {},
"ghcr.io/devcontainers-contrib/features/poetry": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "poetry install" "postCreateCommand": "make install_frontend && make install_backend",
// Configure tool-specific properties. "containerEnv": {
// "customizations": {}, "POETRY_VIRTUALENVS_IN_PROJECT": "true"
},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Configure tool-specific properties.
// "remoteUser": "root" "customizations": {
"vscode": {
"extensions": [
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"sourcery.sourcery",
"eamodio.gitlens",
"ms-vscode.makefile-tools",
"GitHub.vscode-pull-request-github"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
} }

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
.venv/

34
.gitattributes vendored Normal file
View file

@ -0,0 +1,34 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
*.py text
*.js text
*.jsx text
*.ts text
*.tsx text
*.md text
*.mdx text
*.yml text
*.yaml text
*.xml text
*.csv text
*.json text
*.sh text
*.Dockerfile text
Dockerfile text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.ico binary
*.gif binary
*.mp4 binary
*.svg binary
*.csv binary

View file

@ -14,9 +14,7 @@ env:
jobs: jobs:
if_release: if_release:
if: | if: ${{ (github.event.pull_request.merged == true) && contains(github.event.pull_request.labels.*.name, 'pre-release') }}
${{ github.event.pull_request.merged == true }}
&& ${{ contains(github.event.pull_request.labels.*.name, 'pre-release') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -40,6 +38,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
draft: false draft: false
generateReleaseNotes: true generateReleaseNotes: true
prerelease: true
tag: v${{ steps.check-version.outputs.version }} tag: v${{ steps.check-version.outputs.version }}
commit: main commit: main
- name: Publish to PyPI - name: Publish to PyPI

1
.gitignore vendored
View file

@ -253,3 +253,4 @@ langflow.db
.docusaurus/ .docusaurus/
/tmp/* /tmp/*
src/backend/langflow/frontend/

10
.vscode/launch.json vendored
View file

@ -1,4 +1,5 @@
{ {
"version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug Backend", "name": "Debug Backend",
@ -38,6 +39,15 @@
"request": "launch", "request": "launch",
"url": "http://localhost:3000/", "url": "http://localhost:3000/",
"webRoot": "${workspaceRoot}/src/frontend" "webRoot": "${workspaceRoot}/src/frontend"
},
{
"name": "Python: Debug Tests",
"type": "python",
"request": "launch",
"program": "${file}",
"purpose": ["debug-test"],
"console": "integratedTerminal",
"justMyCode": false
} }
] ]
} }

48
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,48 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Init",
"type": "shell",
"command": "make init"
},
// make backend
{
"label": "Backend",
"type": "shell",
"command": "make backend"
},
// make frontend
{
"label": "Frontend",
"type": "shell",
"command": "make frontend"
},
// make test
{
"label": "Test",
"type": "shell",
"command": "make tests"
},
// make lint
{
"label": "Lint",
"type": "shell",
"command": "make lint"
},
// make format
{
"label": "Format",
"type": "shell",
"command": "make format"
},
// make install
{
"label": "Install",
"type": "shell",
"command": "make install_backend && make install_frontend"
}
]
}

View file

@ -7,6 +7,11 @@ to contributions, whether it be in the form of a new feature, improved infra, or
To contribute to this project, please follow a ["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow. To contribute to this project, please follow a ["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow.
Please do not try to push directly to this repo unless you are a maintainer. Please do not try to push directly to this repo unless you are a maintainer.
The branch structure is as follows:
- `main`: The stable version of Langflow
- `dev`: The development version of Langflow. This branch is used to test new features before they are merged into `main` and, as such, may be unstable.
## 🗺️Contributing Guidelines ## 🗺️Contributing Guidelines
## 🚩GitHub Issues ## 🚩GitHub Issues

View file

@ -19,7 +19,7 @@ coverage:
--cov-report term-missing:skip-covered --cov-report term-missing:skip-covered
tests: tests:
poetry run pytest tests poetry run pytest tests -n auto
format: format:
poetry run black . poetry run black .
@ -27,20 +27,38 @@ format:
cd src/frontend && npm run format cd src/frontend && npm run format
lint: lint:
poetry run mypy . poetry run mypy src/backend/langflow
poetry run black . --check poetry run black . --check
poetry run ruff . --fix poetry run ruff . --fix
install_frontend: install_frontend:
cd src/frontend && npm install cd src/frontend && npm install
install_frontendc:
cd src/frontend && rm -rf node_modules package-lock.json && npm install
run_frontend: run_frontend:
cd src/frontend && npm start cd src/frontend && npm start
run_cli:
poetry run langflow run --path src/frontend/build
run_cli_debug:
poetry run langflow run --path src/frontend/build --log-level debug
setup_devcontainer:
make init
make build_frontend
poetry run langflow --path src/frontend/build
frontend: frontend:
make install_frontend make install_frontend
make run_frontend make run_frontend
frontendc:
make install_frontendc
make run_frontend
install_backend: install_backend:
poetry install poetry install
@ -51,7 +69,7 @@ backend:
build_and_run: build_and_run:
echo 'Removing dist folder' echo 'Removing dist folder'
rm -rf dist rm -rf dist
make build && poetry run pip install dist/*.tar.gz && poetry run langflow make build && poetry run pip install dist/*.tar.gz && poetry run langflow run
build_and_install: build_and_install:
echo 'Removing dist folder' echo 'Removing dist folder'

View file

@ -245,7 +245,7 @@ print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))
## Deploy on Railway ## Deploy on Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/Emy2sU?referralCode=MnPSdg) [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg)
## Deploy on Render ## Deploy on Render
@ -275,6 +275,8 @@ flow("Hey, have you heard of Langflow?")
We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible. We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible.
---
Join our [Discord](https://discord.com/invite/EqksyE2EX9) server to ask questions, make suggestions and showcase your projects! 🦾 Join our [Discord](https://discord.com/invite/EqksyE2EX9) server to ask questions, make suggestions and showcase your projects! 🦾
<p> <p>

View file

@ -15,4 +15,4 @@ COPY ./ ./
# Install dependencies # Install dependencies
RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi
CMD ["uvicorn","--factory", "langflow.main:create_app", "--host", "0.0.0.0", "--port", "5003", "--reload", "log-level", "debug"] CMD ["uvicorn", "--factory", "src.backend.langflow.main:create_app", "--host", "0.0.0.0", "--port", "7860", "--reload", "--log-level", "debug"]

View file

@ -11,7 +11,7 @@ services:
[ [
"sh", "sh",
"-c", "-c",
"pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload",
] ]
ports: ports:
- 7860:7860 - 7860:7860

View file

@ -9,7 +9,7 @@ services:
- "7860:7860" - "7860:7860"
volumes: volumes:
- ./:/app - ./:/app
command: bash -c "uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload" command: bash -c "uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload"
frontend: frontend:
build: build:

9
docker_example/README.md Normal file
View file

@ -0,0 +1,9 @@
# LangFlow Docker Running
```sh
git clone git@github.com:logspace-ai/langflow.git
cd langflow/docker_example
docker compose up
```
The web UI will be accessible on port [7860](http://localhost:7860/)

View file

@ -33,6 +33,7 @@ The CustomComponent class serves as the foundation for creating custom component
| Supported Types | | Supported Types |
| --------------------------------------------------------- | | --------------------------------------------------------- |
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ | | _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
| _`langflow.field_typing.NestedDict`_ |
| _`langchain.chains.base.Chain`_ | | _`langchain.chains.base.Chain`_ |
| _`langchain.PromptTemplate`_ | | _`langchain.PromptTemplate`_ |
| _`langchain.llms.base.BaseLLM`_ | | _`langchain.llms.base.BaseLLM`_ |
@ -44,6 +45,8 @@ The CustomComponent class serves as the foundation for creating custom component
| _`langchain.embeddings.base.Embeddings`_ | | _`langchain.embeddings.base.Embeddings`_ |
| _`langchain.schema.BaseRetriever`_ | | _`langchain.schema.BaseRetriever`_ |
The difference between _`dict`_ and _`langflow.field_typing.NestedDict`_ is that one adds a simple key-value pair field, while the other opens a more robust dictionary editor.
<Admonition type="info"> <Admonition type="info">
Unlike Langchain types, base Python types do not add a Unlike Langchain types, base Python types do not add a
[handle](../guidelines/components) to the field by default. To add handles, [handle](../guidelines/components) to the field by default. To add handles,

View file

@ -73,3 +73,25 @@ Used to load [OpenAI’s](https://openai.com/) embedding models.
- **request_timeout:** Used to specify the maximum amount of time, in milliseconds, to wait for a response from the OpenAI API when generating embeddings for a given text. - **request_timeout:** Used to specify the maximum amount of time, in milliseconds, to wait for a response from the OpenAI API when generating embeddings for a given text.
- **tiktoken_model_name:** Used to count the number of tokens in documents to constrain them to be under a certain limit. By default, when set to None, this will be the same as the embedding model name. - **tiktoken_model_name:** Used to count the number of tokens in documents to constrain them to be under a certain limit. By default, when set to None, this will be the same as the embedding model name.
---
### VertexAIEmbeddings
Wrapper around [Google Vertex AI](https://cloud.google.com/vertex-ai) [Embeddings API](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings).
:::info
Vertex AI is a cloud computing platform offered by Google Cloud Platform (GCP). It provides access, management, and development of applications and services through global data centers. To use Vertex AI PaLM, you need to have the [google-cloud-aiplatform](https://pypi.org/project/google-cloud-aiplatform/) Python package installed and credentials configured for your environment.
:::
- **credentials:** The default custom credentials (google.auth.credentials.Credentials) to use.
- **location:** The default location to use when making API calls – defaults to `us-central1`.
- **max_output_tokens:** Token limit determines the maximum amount of text output from one prompt – defaults to `128`.
- **model_name:** The name of the Vertex AI large language model – defaults to `text-bison`.
- **project:** The default GCP project to use when making Vertex API calls.
- **request_parallelism:** The amount of parallelism allowed for requests issued to VertexAI models – defaults to `5`.
- **temperature:** Tunes the degree of randomness in text generations. Should be a non-negative value – defaults to `0`.
- **top_k:** How the model selects tokens for output, the next token is selected from – defaults to `40`.
- **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`.
- **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored.
- **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 help debug and understand the chain's behavior. If set to False, it will suppress the verbose output – defaults to `False`.

View file

@ -196,3 +196,25 @@ Vertex AI is a cloud computing platform offered by Google Cloud Platform (GCP).
- **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`. - **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`.
- **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored. - **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored.
- **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 help debug and understand 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 help debug and understand the chain's behavior. If set to False, it will suppress the verbose output – defaults to `False`.
---
### ChatVertexAI
Wrapper around [Google Vertex AI](https://cloud.google.com/vertex-ai) large language models.
:::info
Vertex AI is a cloud computing platform offered by Google Cloud Platform (GCP). It provides access, management, and development of applications and services through global data centers. To use Vertex AI PaLM, you need to have the [google-cloud-aiplatform](https://pypi.org/project/google-cloud-aiplatform/) Python package installed and credentials configured for your environment.
:::
- **credentials:** The default custom credentials (google.auth.credentials.Credentials) to use.
- **location:** The default location to use when making API calls – defaults to `us-central1`.
- **max_output_tokens:** Token limit determines the maximum amount of text output from one prompt – defaults to `128`.
- **model_name:** The name of the Vertex AI large language model – defaults to `text-bison`.
- **project:** The default GCP project to use when making Vertex API calls.
- **request_parallelism:** The amount of parallelism allowed for requests issued to VertexAI models – defaults to `5`.
- **temperature:** Tunes the degree of randomness in text generations. Should be a non-negative value – defaults to `0`.
- **top_k:** How the model selects tokens for output, the next token is selected from – defaults to `40`.
- **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`.
- **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored.
- **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 help debug and understand the chain's behavior. If set to False, it will suppress the verbose output – defaults to `False`.

View file

@ -1,11 +1,13 @@
import Admonition from '@theme/Admonition'; import Admonition from "@theme/Admonition";
# Text Splitters # Text Splitters
<Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION"> <Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION">
<p> <p>
We appreciate your understanding as we polish our documentation – it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝 We appreciate your understanding as we polish our documentation – it may
</p> contain some rough edges. Share your feedback or report issues to help us
improve! 🛠️📝
</p>
</Admonition> </Admonition>
A text splitter is a tool that divides a document or text into smaller chunks or segments. It is used to break down large texts into more manageable pieces for analysis or processing. A text splitter is a tool that divides a document or text into smaller chunks or segments. It is used to break down large texts into more manageable pieces for analysis or processing.
@ -22,11 +24,11 @@ The `CharacterTextSplitter` is used to split a long text into smaller chunks bas
- **chunk_overlap:** Determines the number of characters that overlap between consecutive chunks when splitting text. It specifies how much of the previous chunk should be included in the next chunk. - **chunk_overlap:** Determines the number of characters that overlap between consecutive chunks when splitting text. It specifies how much of the previous chunk should be included in the next chunk.
For example, if the `chunk_overlap` is set to 20 and the `chunk_size` is set to 100, the splitter will create chunks of 100 characters each, but the last 20 characters of each chunk will overlap with the first 20 characters of the next chunk. This allows for a smoother transition between chunks and ensures that no information is lost – defaults to `200`. For example, if the `chunk_overlap` is set to 20 and the `chunk_size` is set to 100, the splitter will create chunks of 100 characters each, but the last 20 characters of each chunk will overlap with the first 20 characters of the next chunk. This allows for a smoother transition between chunks and ensures that no information is lost – defaults to `200`.
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk. - **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
For example, if the chunk_size is set to 100, the splitter will create chunks of 100 characters each. If the text is longer than 100 characters, it will be divided into multiple chunks of equal size, except for the last chunk, which may be smaller if there are remaining characters –defaults to `1000`. For example, if the chunk_size is set to 100, the splitter will create chunks of 100 characters each. If the text is longer than 100 characters, it will be divided into multiple chunks of equal size, except for the last chunk, which may be smaller if there are remaining characters –defaults to `1000`.
- **separator:** Specifies the character that will be used to split the text into chunks – defaults to `.` - **separator:** Specifies the character that will be used to split the text into chunks – defaults to `.`
@ -44,6 +46,18 @@ The `RecursiveCharacterTextSplitter` splits the text by trying to keep paragra
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk. - **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
- **separator_type:** The parameter allows the user to split the code with multiple language support. It supports various languages such as Text, Ruby, Python, Solidity, Java, and more. Defaults to `Text`. - **separators:** The `separators` in RecursiveCharacterTextSplitter are the characters used to split the text into chunks. The text splitter tries to create chunks based on splitting on the first character in the list of `separators`. If any chunks are too large, it moves on to the next character in the list and continues splitting. Defaults to ["\n\n", "\n", " ", ""].
- **separators:** The `separators` in RecursiveCharacterTextSplitter are the characters used to split the text into chunks. The text splitter tries to create chunks based on splitting on the first character in the list of `separators`. If any chunks are too large, it moves on to the next character in the list and continues splitting. Defaults to `.` ### LanguageRecursiveTextSplitter
The `LanguageRecursiveTextSplitter` is a text splitter that splits the text into smaller chunks based on the (programming) language of the text.
**Params**
- **Documents:** Input documents to split.
- **chunk_overlap:** Determines the number of characters that overlap between consecutive chunks when splitting text. It specifies how much of the previous chunk should be included in the next chunk.
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
- **separator_type:** The parameter allows the user to split the code with multiple language support. It supports various languages such as Ruby, Python, Solidity, Java, and more. Defaults to `Python`.

View file

@ -7,3 +7,57 @@ import Admonition from '@theme/Admonition';
We appreciate your understanding as we polish our documentation – it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝 We appreciate your understanding as we polish our documentation – it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝
</p> </p>
</Admonition> </Admonition>
### BingSearchRun
Bing Search is a web search engine owned and operated by Microsoft. It provides search results for various types of content, including web pages, images, videos, and news articles. It uses a combination of algorithms and human editors to deliver search results to users.
**Params**
- **Api Wrapper:** A BingSearchAPIWrapper component that takes the search URL and a subscription key.
### Calculator
The calculator tool provides mathematical calculation capabilities to an agent by leveraging an LLMMathChain. It allows the agent to perform math when needed to answer questions.
**Params**
- **LLM:** Language Model to use in the calculation.
### GoogleSearchResults
A wrapper around Google Search. Useful for when the user needs to answer questions about with more control over the JSON data returned from the API. It returns the full JSON response configured based on the parameters passed to the API wrapper.
**Params**
- **Api Wrapper:** A GoogleSearchAPIWrapper with Google API key and CSE ID
### GoogleSearchRun
A quick wrapper around Google Search. It executes the search query and returns just the first result snippet from the highest-priority result type.
**Params**
- **Api Wrapper:** A GoogleSearchAPIWrapper with Google API key and CSE ID
### GoogleSerperRun
A low-cost Google Search API.
**Params**
- **Api Wrapper:** A GoogleSerperAPIWrapper component with API key and result keys
### InfoSQLDatabaseTool
Tool for getting metadata about a SQL database. The input to this tool is a comma-separated list of tables, and the output is the schema and sample rows for those tables. Example Input: `“table1`, `table2`, `table3”`.
**Params**
- **Db:** SQLDatabase to query.

View file

@ -1,10 +1,76 @@
import Admonition from '@theme/Admonition'; import Admonition from "@theme/Admonition";
# Utilities # Utilities
<Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION"> <Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION">
<p> <p>
We appreciate your understanding as we polish our documentation – it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝 We appreciate your understanding as we polish our documentation – it may
</p> contain some rough edges. Share your feedback or report issues to help us
improve! 🛠️📝
</p>
</Admonition> </Admonition>
Utilities are a set of actions that can be used to perform common tasks in a flow. They are available in the **Utilities** section in the sidebar.
---
### GET Request
Make a GET request to the given URL.
**Params**
- **URL:** The URL to make the request to. There can be more than one URL, in which case the request will be made to each URL in order.
- **Headers:** A dictionary of headers to send with the request.
**Output**
- **List of Documents:** A list of Documents containing the JSON response from each request.
---
### POST Request
Make a POST request to the given URL.
**Params**
- **URL:** The URL to make the request to.
- **Headers:** A dictionary of headers to send with the request.
- **Document:** The Document containing a JSON object to send with the request.
**Output**
- **Document:** The JSON response from the request as a Document.
---
### Update Request
Make a PATCH or PUT request to the given URL.
**Params**
- **URL:** The URL to make the request to.
- **Headers:** A dictionary of headers to send with the request.
- **Document:** The Document containing a JSON object to send with the request.
- **Method:** The HTTP method to use for the request. Can be either `PATCH` or `PUT`.
**Output**
- **Document:** The JSON response from the request as a Document.
---
### JSON Document Builder
Build a Document containing a JSON object using a key and another Document page content.
**Params**
- **Key:** The key to use for the JSON object.
- **Document:** The Document page to use for the JSON object.
**Output**
- **List of Documents:** A list containing the Document with the JSON object.

View file

@ -65,7 +65,6 @@ class DocumentProcessor(CustomComponent):
light: "img/document_processor.png", light: "img/document_processor.png",
}} }}
style={{ style={{
width: "40%",
margin: "0 auto", margin: "0 auto",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
@ -388,7 +387,7 @@ Your structure should look something like this:
The recommended way to load custom components is to set the _`LANGFLOW_COMPONENTS_PATH`_ environment variable to the path of your custom components directory. Then, run the Langflow CLI as usual. The recommended way to load custom components is to set the _`LANGFLOW_COMPONENTS_PATH`_ environment variable to the path of your custom components directory. Then, run the Langflow CLI as usual.
```bash ```bash
export LANGFLOW_COMPONENTS_PATH=/path/to/components export LANGFLOW_COMPONENTS_PATH='["/path/to/components"]'
langflow langflow
``` ```

View file

@ -0,0 +1,49 @@
# Integrating Langfuse with Langflow
## Introduction
Langfuse is an open-source tracing and analytics tool designed for LLM applications. Integrating Langfuse with Langflow provides detailed production traces and granular insights into quality, cost, and latency. This integration allows you to monitor and debug your Langflow's chat or APIs easily.
## Step-by-Step Instructions
### Step 1: Create a Langfuse account
1. Go to [Langfuse](https://langfuse.com) and click on the "Sign In" button in the top right corner.
2. Click on the "Sign Up" button and create an account.
3. Once logged in, click on "Settings" and then on "Create new API keys."
4. Copy the Public key and the Secret Key and save them somewhere safe.
{/* Add these keys to your environment variables in the following step. */}
### Step 2: Set up Langfuse in Langflow
1. **Export the Environment Variables**: You'll need to export the environment variables `LANGFLOW_LANGFUSE_SECRET_KEY` and `LANGFLOW_LANGFUSE_PUBLIC_KEY` with the values obtained in Step 1.
You can do this by executing the following commands in your terminal:
```bash
export LANGFLOW_LANGFUSE_SECRET_KEY=<your secret key>
export LANGFLOW_LANGFUSE_PUBLIC_KEY=<your public key>
```
Alternatively, you can run the Langflow CLI command:
```bash
LANGFLOW_LANGFUSE_SECRET_KEY=<your secret key> LANGFLOW_LANGFUSE_PUBLIC_KEY=<your public key> langflow
```
If you are self-hosting Langfuse, you can also set the environment variable `LANGFLOW_LANGFUSE_HOST` to point to your Langfuse instance. By default, Langfuse points to the cloud instance at `https://cloud.langfuse.com`.
2. **Verify Integration**: Ensure that the environment variables are set correctly by checking their existence in your environment, for example by running:
```bash
echo $LANGFLOW_LANGFUSE_SECRET_KEY
echo $LANGFLOW_LANGFUSE_PUBLIC_KEY
```
3. **Monitor Langflow**: Now, whenever you use Langflow's chat or API, you will be able to see the tracing of your conversations in Langfuse.
That's it! You have successfully integrated Langfuse with Langflow, enhancing observability and debugging capabilities for your LLM application.
---
Note: For more details or customized configurations, please refer to the official [Langfuse documentation](https://langfuse.com/docs/integrations/langchain).

View file

@ -43,7 +43,7 @@ This guide takes you through the process of augmenting the "Basic Chat with Prom
8. Connect this loader to the `{context}` variable that we just added. 8. Connect this loader to the `{context}` variable that we just added.
9. In the "Web Page" field, enter "https://langflow.org/how-upload-examples". 9. In the "Web Page" field, enter "https://docs.langflow.org/how-upload-examples".
10. Now, click on "ConversationBufferMemory". 10. Now, click on "ConversationBufferMemory".

71
docs/package-lock.json generated
View file

@ -16,7 +16,7 @@
"@docusaurus/theme-classic": "^2.4.1", "@docusaurus/theme-classic": "^2.4.1",
"@docusaurus/theme-search-algolia": "^2.4.1", "@docusaurus/theme-search-algolia": "^2.4.1",
"@mdx-js/react": "^2.3.0", "@mdx-js/react": "^2.3.0",
"@mendable/search": "^0.0.114", "@mendable/search": "^0.0.154",
"@pbe/react-yandex-maps": "^1.2.4", "@pbe/react-yandex-maps": "^1.2.4",
"@prismicio/client": "^7.0.1", "@prismicio/client": "^7.0.1",
"@uiball/loaders": "^1.2.6", "@uiball/loaders": "^1.2.6",
@ -3250,10 +3250,11 @@
} }
}, },
"node_modules/@mendable/search": { "node_modules/@mendable/search": {
"version": "0.0.114", "version": "0.0.154",
"resolved": "https://registry.npmjs.org/@mendable/search/-/search-0.0.114.tgz", "resolved": "https://registry.npmjs.org/@mendable/search/-/search-0.0.154.tgz",
"integrity": "sha512-0uR+zxONuu/16bpLli49Jocr5fee1WIjs06KzU1AnHsR+fdFBmfrlpgTDWctgGuXPzS5Dorlw4VMlR5dPW5qVQ==", "integrity": "sha512-adNwXlIaMXVMCkPU2uUdghfn05Dmxb0BnE95SRLQJ6evHajsNFQdRl5Ltj3WijG+qo4ozTIJcPOBYrDPKMTPVw==",
"dependencies": { "dependencies": {
"html-react-parser": "^4.2.0",
"posthog-js": "^1.45.1" "posthog-js": "^1.45.1"
}, },
"peerDependencies": { "peerDependencies": {
@ -9351,6 +9352,33 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/html-dom-parser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-4.0.0.tgz",
"integrity": "sha512-TUa3wIwi80f5NF8CVWzkopBVqVAtlawUzJoLwVLHns0XSJGynss4jiY0mTWpiDOsuyw+afP+ujjMgRh9CoZcXw==",
"dependencies": {
"domhandler": "5.0.3",
"htmlparser2": "9.0.0"
}
},
"node_modules/html-dom-parser/node_modules/htmlparser2": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz",
"integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/html-entities": { "node_modules/html-entities": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz",
@ -9394,6 +9422,20 @@
"node": ">= 12" "node": ">= 12"
} }
}, },
"node_modules/html-react-parser": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.1.tgz",
"integrity": "sha512-Dxzdowj5Zu/+7mr8X8PzCFbPXGuwCwGB2u4cB6oxZGES9inw85qlvnlfPD75VGKUGjcgsXs+9Dpj+THWNQyOBw==",
"dependencies": {
"domhandler": "5.0.3",
"html-dom-parser": "4.0.0",
"react-property": "2.0.0",
"style-to-js": "1.1.3"
},
"peerDependencies": {
"react": "0.14 || 15 || 16 || 17 || 18"
}
},
"node_modules/html-tags": { "node_modules/html-tags": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@ -15324,6 +15366,11 @@
"react": ">=16.6.0" "react": ">=16.6.0"
} }
}, },
"node_modules/react-property": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
"integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw=="
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "5.3.4", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
@ -17510,6 +17557,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/style-to-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz",
"integrity": "sha512-zKI5gN/zb7LS/Vm0eUwjmjrXWw8IMtyA8aPBJZdYiQTXj4+wQ3IucOLIOnF7zCHxvW8UhIGh/uZh/t9zEHXNTQ==",
"dependencies": {
"style-to-object": "0.4.1"
}
},
"node_modules/style-to-js/node_modules/style-to-object": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz",
"integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==",
"dependencies": {
"inline-style-parser": "0.1.1"
}
},
"node_modules/style-to-object": { "node_modules/style-to-object": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",

View file

@ -22,7 +22,7 @@
"@docusaurus/theme-classic": "^2.4.1", "@docusaurus/theme-classic": "^2.4.1",
"@docusaurus/theme-search-algolia": "^2.4.1", "@docusaurus/theme-search-algolia": "^2.4.1",
"@mdx-js/react": "^2.3.0", "@mdx-js/react": "^2.3.0",
"@mendable/search": "^0.0.114", "@mendable/search": "^0.0.154",
"@pbe/react-yandex-maps": "^1.2.4", "@pbe/react-yandex-maps": "^1.2.4",
"@prismicio/client": "^7.0.1", "@prismicio/client": "^7.0.1",
"@uiball/loaders": "^1.2.6", "@uiball/loaders": "^1.2.6",

View file

@ -42,6 +42,7 @@ module.exports = {
"components/text-splitters", "components/text-splitters",
"components/toolkits", "components/toolkits",
"components/tools", "components/tools",
"components/utilities",
"components/vector-stores", "components/vector-stores",
"components/wrappers", "components/wrappers",
], ],
@ -50,7 +51,11 @@ module.exports = {
type: "category", type: "category",
label: "Step-by-Step Guides", label: "Step-by-Step Guides",
collapsed: false, collapsed: false,
items: ["guides/loading_document", "guides/chatprompttemplate_guide"], items: [
"guides/loading_document",
"guides/chatprompttemplate_guide",
"guides/langfuse_integration",
],
}, },
// { // {
// type: 'category', // type: 'category',

View file

@ -37,7 +37,7 @@ export default function FooterWrapper(props) {
const mendableFloatingButton = React.createElement(MendableFloatingButton, { const mendableFloatingButton = React.createElement(MendableFloatingButton, {
floatingButtonStyle: { color: "#000000", backgroundColor: "#f6f6f6" }, floatingButtonStyle: { color: "#000000", backgroundColor: "#f6f6f6" },
anon_key: customFields.mendableAnonKey, // Mendable Search Public ANON key, ok to be public anon_key: 'b7f52734-297c-41dc-8737-edbd13196394', // Mendable Search Public ANON key, ok to be public
showSimpleSearch: true, showSimpleSearch: true,
icon: icon, icon: icon,
}); });

2
docs/static/CNAME vendored
View file

@ -1 +1 @@
langflow.org docs.langflow.org

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

1551
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
{
"devDependencies": {
"@svgr/cli": "^8.0.1"
}
}

3342
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.4.1" version = "0.5.0a1"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [
@ -19,7 +19,7 @@ readme = "README.md"
keywords = ["nlp", "langchain", "openai", "gpt", "gui"] keywords = ["nlp", "langchain", "openai", "gpt", "gui"]
packages = [{ include = "langflow", from = "src/backend" }] packages = [{ include = "langflow", from = "src/backend" }]
include = ["src/backend/langflow/*", "src/backend/langflow/**/*"] include = ["src/backend/langflow/*", "src/backend/langflow/**/*"]
documentation = "https://docs.langflow.org"
[tool.poetry.scripts] [tool.poetry.scripts]
langflow = "langflow.__main__:main" langflow = "langflow.__main__:main"
@ -32,38 +32,38 @@ beautifulsoup4 = "^4.12.2"
google-search-results = "^2.4.1" google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.9.0" typer = "^0.9.0"
gunicorn = "^21.1.0" gunicorn = "^21.2.0"
langchain = "^0.0.250" langchain = "^0.0.274"
openai = "^0.27.8" openai = "^0.27.8"
pandas = "^2.0.0" pandas = "^2.0.0"
chromadb = "^0.3.21" chromadb = "^0.3.0"
huggingface-hub = { version = "^0.16.0", extras = ["inference"] } huggingface-hub = { version = "^0.16.0", extras = ["inference"] }
rich = "^13.4.2" rich = "^13.5.0"
llama-cpp-python = { version = "~0.1.0", optional = true } llama-cpp-python = { version = "~0.1.0", optional = true }
networkx = "^3.1" networkx = "^3.1"
unstructured = "^0.7.0" unstructured = "^0.10.0"
pypdf = "^3.11.0" pypdf = "^3.15.0"
lxml = "^4.9.2" lxml = "^4.9.2"
pysrt = "^1.1.2" pysrt = "^1.1.2"
fake-useragent = "^1.1.3" fake-useragent = "^1.2.1"
docstring-parser = "^0.15" docstring-parser = "^0.15"
psycopg2-binary = "^2.9.6" psycopg2-binary = "^2.9.6"
pyarrow = "^12.0.0" pyarrow = "^12.0.0"
tiktoken = "~0.4.0" tiktoken = "~0.4.0"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
langchain-serve = { version = ">0.0.51", optional = true } langchain-serve = { version = ">0.0.51", optional = true }
qdrant-client = "^1.3.0" qdrant-client = "^1.4.0"
websockets = "^10.3" websockets = "^10.3"
weaviate-client = "^3.21.0" weaviate-client = "^3.23.0"
jina = "3.15.2" jina = "3.15.2"
sentence-transformers = { version = "^2.2.2", optional = true } sentence-transformers = { version = "^2.2.2", optional = true }
ctransformers = { version = "^0.2.10", optional = true } ctransformers = { version = "^0.2.10", optional = true }
cohere = "^4.11.0" cohere = "^4.21.0"
python-multipart = "^0.0.6" python-multipart = "^0.0.6"
sqlmodel = "^0.0.8" sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4" faiss-cpu = "^1.7.4"
anthropic = "^0.3.0" anthropic = "^0.3.0"
orjson = "^3.9.1" orjson = "3.9.3"
multiprocess = "^0.70.14" multiprocess = "^0.70.14"
cachetools = "^5.3.1" cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5" types-cachetools = "^5.3.0.5"
@ -77,6 +77,17 @@ psycopg = "^3.1.9"
psycopg-binary = "^3.1.9" psycopg-binary = "^3.1.9"
fastavro = "^1.8.0" fastavro = "^1.8.0"
langchain-experimental = "^0.0.8" langchain-experimental = "^0.0.8"
alembic = "^1.11.2"
passlib = "^1.7.4"
bcrypt = "^4.0.1"
python-jose = "^3.3.0"
metaphor-python = "^0.1.11"
pywin32 = { version = "^306", markers = "sys_platform == 'win32'" }
loguru = "^0.7.1"
langfuse = "^1.0.13"
pillow = "^10.0.0"
metal-sdk = "^2.0.2"
markupsafe = "^2.1.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^23.1.0" black = "^23.1.0"
@ -92,6 +103,13 @@ pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2" types-pillow = "^9.5.0.2"
types-appdirs = "^1.4.3.5" types-appdirs = "^1.4.3.5"
types-pyyaml = "^6.0.12.8" types-pyyaml = "^6.0.12.8"
types-python-jose = "^3.3.4.8"
types-passlib = "^1.7.7.13"
pytest-mock = "^3.11.1"
pytest-xdist = "^3.3.1"
types-pywin32 = "^306.0.0.4"
types-google-cloud-ndb = "^2.2.0.0"
pytest-sugar = "^0.9.7"
[tool.poetry.extras] [tool.poetry.extras]

View file

@ -3,9 +3,14 @@ services:
- type: web - type: web
name: langflow name: langflow
runtime: docker runtime: docker
plan: free
dockerfilePath: ./Dockerfile dockerfilePath: ./Dockerfile
repo: https://github.com/logspace-ai/langflow repo: https://github.com/logspace-ai/langflow
branch: main branch: main
healthCheckPath: /health healthCheckPath: /health
autoDeploy: false autoDeploy: false
envVars:
- key: LANGFLOW_DATABASE_URL
value: sqlite:////home/user/.cache/langflow/langflow.db
disk:
name: langflow-data
mountPath: /home/user/.cache/langflow

View file

@ -11,4 +11,4 @@ RUN rm *.whl
EXPOSE 80 EXPOSE 80
CMD [ "uvicorn", "--host", "0.0.0.0", "--port", "80", "langflow.backend.app:app" ] CMD [ "uvicorn", "--host", "0.0.0.0", "--port", "7860", "--factory", "langflow.main:create_app" ]

View file

@ -1,5 +1,7 @@
from importlib import metadata from importlib import metadata
from langflow.cache import cache_manager
# Deactivate cache manager for now
# from langflow.services.cache import cache_manager
from langflow.processing.process import load_flow_from_json from langflow.processing.process import load_flow_from_json
from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.custom_component import CustomComponent

View file

@ -1,8 +1,11 @@
import sys import sys
import time import time
import httpx import httpx
from langflow.utils.util import get_number_of_workers from langflow.services.database.utils import session_getter
from multiprocess import Process # type: ignore from langflow.services.manager import initialize_services, initialize_settings_manager
from langflow.services.utils import get_db_manager, get_settings_manager
from multiprocess import Process, cpu_count # type: ignore
import platform import platform
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@ -10,19 +13,49 @@ import socket
from rich.panel import Panel from rich.panel import Panel
from rich import box from rich import box
from rich import print as rprint from rich import print as rprint
from rich.table import Table
import typer import typer
from langflow.main import setup_app from langflow.main import setup_app
from langflow.settings import settings
from langflow.utils.logger import configure, logger from langflow.utils.logger import configure, logger
import webbrowser import webbrowser
from dotenv import load_dotenv from dotenv import load_dotenv
from rich.console import Console
console = Console()
app = typer.Typer() app = typer.Typer()
def get_number_of_workers(workers=None):
if workers == -1 or workers is None:
workers = (cpu_count() * 2) + 1
logger.debug(f"Number of workers: {workers}")
return workers
def display_results(results):
"""
Display the results of the migration.
"""
for table_results in results:
table = Table(title=f"Migration {table_results.table_name}")
table.add_column("Name")
table.add_column("Type")
table.add_column("Status")
for result in table_results.results:
status = "Success" if result.success else "Failure"
color = "green" if result.success else "red"
table.add_row(result.name, result.type, f"[{color}]{status}[/{color}]")
console.print(table)
console.print() # Print a new line
def update_settings( def update_settings(
config: str, config: str,
cache: str, cache: Optional[str] = None,
dev: bool = False, dev: bool = False,
remove_api_keys: bool = False, remove_api_keys: bool = False,
components_path: Optional[Path] = None, components_path: Optional[Path] = None,
@ -30,19 +63,20 @@ def update_settings(
"""Update the settings from a config file.""" """Update the settings from a config file."""
# Check for database_url in the environment variables # Check for database_url in the environment variables
initialize_settings_manager()
settings_manager = get_settings_manager()
if config: if config:
logger.debug(f"Loading settings from {config}") logger.debug(f"Loading settings from {config}")
settings.update_from_yaml(config, dev=dev) settings_manager.settings.update_from_yaml(config, dev=dev)
if remove_api_keys: if remove_api_keys:
logger.debug(f"Setting remove_api_keys to {remove_api_keys}") logger.debug(f"Setting remove_api_keys to {remove_api_keys}")
settings.update_settings(REMOVE_API_KEYS=remove_api_keys) settings_manager.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
if cache: if cache:
logger.debug(f"Setting cache to {cache}") logger.debug(f"Setting cache to {cache}")
settings.update_settings(CACHE=cache) settings_manager.settings.update_settings(CACHE=cache)
if components_path: if components_path:
logger.debug(f"Adding component path {components_path}") logger.debug(f"Adding component path {components_path}")
settings.update_settings(COMPONENTS_PATH=components_path) settings_manager.settings.update_settings(COMPONENTS_PATH=components_path)
def serve_on_jcloud(): def serve_on_jcloud():
@ -92,7 +126,7 @@ def serve_on_jcloud():
@app.command() @app.command()
def serve( def run(
host: str = typer.Option( host: str = typer.Option(
"127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST" "127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"
), ),
@ -106,7 +140,9 @@ def serve(
help="Path to the directory containing custom components.", help="Path to the directory containing custom components.",
envvar="LANGFLOW_COMPONENTS_PATH", envvar="LANGFLOW_COMPONENTS_PATH",
), ),
config: str = typer.Option("config.yaml", help="Path to the configuration file."), config: str = typer.Option(
Path(__file__).parent / "config.yaml", help="Path to the configuration file."
),
# .env file param # .env file param
env_file: Path = typer.Option( env_file: Path = typer.Option(
None, help="Path to the .env file containing environment variables." None, help="Path to the .env file containing environment variables."
@ -117,10 +153,10 @@ def serve(
log_file: Path = typer.Option( log_file: Path = typer.Option(
"logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE" "logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE"
), ),
cache: str = typer.Option( cache: Optional[str] = typer.Option(
envvar="LANGFLOW_LANGCHAIN_CACHE", envvar="LANGFLOW_LANGCHAIN_CACHE",
help="Type of cache to use. (InMemoryCache, SQLiteCache)", help="Type of cache to use. (InMemoryCache, SQLiteCache)",
default="SQLiteCache", default=None,
), ),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"), jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"), dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
@ -146,6 +182,11 @@ def serve(
help="Remove API keys from the projects saved in the database.", help="Remove API keys from the projects saved in the database.",
envvar="LANGFLOW_REMOVE_API_KEYS", envvar="LANGFLOW_REMOVE_API_KEYS",
), ),
backend_only: bool = typer.Option(
False,
help="Run only the backend server without the frontend.",
envvar="LANGFLOW_BACKEND_ONLY",
),
): ):
""" """
Run the Langflow server. Run the Langflow server.
@ -167,7 +208,7 @@ def serve(
) )
# create path object if path is provided # create path object if path is provided
static_files_dir: Optional[Path] = Path(path) if path else None static_files_dir: Optional[Path] = Path(path) if path else None
app = setup_app(static_files_dir=static_files_dir) app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
# check if port is being used # check if port is being used
if is_port_in_use(port, host): if is_port_in_use(port, host):
port = get_free_port(port) port = get_free_port(port)
@ -179,6 +220,10 @@ def serve(
"timeout": timeout, "timeout": timeout,
} }
# Define an env variable to know if we are just testing the server
if "pytest" in sys.modules:
return
if platform.system() in ["Windows"]: if platform.system() in ["Windows"]:
# Run using uvicorn on MacOS and Windows # Run using uvicorn on MacOS and Windows
# Windows doesn't support gunicorn # Windows doesn't support gunicorn
@ -299,6 +344,43 @@ def run_langflow(host, port, log_level, options, app):
sys.exit(1) sys.exit(1)
@app.command()
def superuser(
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
password: str = typer.Option(
..., prompt=True, hide_input=True, help="Password for the superuser."
),
):
initialize_services()
db_manager = get_db_manager()
with session_getter(db_manager) as session:
from langflow.services.auth.utils import create_super_user
if create_super_user(db=session, username=username, password=password):
# Verify that the superuser was created
from langflow.services.database.models.user.user import User
user = session.query(User).filter(User.username == username).first()
if user is None:
typer.echo("Superuser creation failed.")
return
typer.echo("Superuser created successfully.")
else:
typer.echo("Superuser creation failed.")
@app.command()
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
initialize_services()
db_manager = get_db_manager()
if not test:
db_manager.run_migrations()
results = db_manager.run_migrations_test()
display_results(results)
def main(): def main():
app() app()

View file

@ -0,0 +1,113 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# This is the path to the db in the root of the project.
# When the user runs the Langflow the database url will
# be set dinamically.
sqlalchemy.url = sqlite:///../../../langflow.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View file

@ -0,0 +1 @@
Generic single-database configuration.

View file

@ -0,0 +1,81 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from langflow.services.database.manager import SQLModel
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata, render_as_batch=True
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View file

@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,177 @@
"""Adds tables
Revision ID: 260dbcc8b680
Revises:
Create Date: 2023-08-27 19:49:02.681355
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "260dbcc8b680"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
# List existing tables
existing_tables = inspector.get_table_names()
# Drop 'flowstyle' table if it exists
# and other related indices
if "flowstyle" in existing_tables:
op.drop_table("flowstyle")
if "ix_flowstyle_flow_id" in [
index["name"] for index in inspector.get_indexes("flowstyle")
]:
op.drop_index("ix_flowstyle_flow_id", table_name="flowstyle")
existing_indices_flow = []
existing_fks_flow = []
if "flow" in existing_tables:
existing_indices_flow = [
index["name"] for index in inspector.get_indexes("flow")
]
# Existing foreign keys for the 'flow' table, if it exists
existing_fks_flow = [
fk["referred_table"] + "." + fk["referred_columns"][0]
for fk in inspector.get_foreign_keys("flow")
]
# Now check if the columns user_id exists in the 'flow' table
# If it does not exist, we need to create the foreign key
if "user" not in existing_tables:
op.create_table(
"user",
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("username", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("password", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_superuser", sa.Boolean(), nullable=False),
sa.Column("create_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("last_login_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_user_username"), ["username"], unique=True
)
if "apikey" not in existing_tables:
op.create_table(
"apikey",
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("last_used_at", sa.DateTime(), nullable=True),
sa.Column("total_uses", sa.Integer(), nullable=False, default=0),
sa.Column("is_active", sa.Boolean(), nullable=False, default=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
with op.batch_alter_table("apikey", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True
)
batch_op.create_index(batch_op.f("ix_apikey_name"), ["name"], unique=False)
batch_op.create_index(
batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False
)
if "flow" not in existing_tables:
op.create_table(
"flow",
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
# Conditionally create indices for 'flow' table
# if _alembic_tmp_flow exists, then we need to drop it first
# This is to deal with SQLite not being able to ROLLBACK
# for some unknown reason
if "_alembic_tmp_flow" in existing_tables:
op.drop_table("_alembic_tmp_flow")
with op.batch_alter_table("flow", schema=None) as batch_op:
flow_columns = [col["name"] for col in inspector.get_columns("flow")]
if "user_id" not in flow_columns:
batch_op.add_column(
sa.Column(
"user_id",
sqlmodel.sql.sqltypes.GUID(),
nullable=True, # This should be False, but we need to allow NULL values for now
)
)
if "user.id" not in existing_fks_flow:
batch_op.create_foreign_key("fk_flow_user_id", "user", ["user_id"], ["id"])
if "ix_flow_description" not in existing_indices_flow:
batch_op.create_index(
batch_op.f("ix_flow_description"), ["description"], unique=False
)
if "ix_flow_name" not in existing_indices_flow:
batch_op.create_index(batch_op.f("ix_flow_name"), ["name"], unique=False)
with op.batch_alter_table("flow", schema=None) as batch_op:
if "ix_flow_user_id" not in existing_indices_flow:
batch_op.create_index(
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
# List existing tables
existing_tables = inspector.get_table_names()
if "flow" in existing_tables:
with op.batch_alter_table("flow", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_flow_user_id"))
batch_op.drop_index(batch_op.f("ix_flow_name"))
batch_op.drop_index(batch_op.f("ix_flow_description"))
op.drop_table("flow")
if "apikey" in existing_tables:
with op.batch_alter_table("apikey", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_apikey_user_id"))
batch_op.drop_index(batch_op.f("ix_apikey_name"))
batch_op.drop_index(batch_op.f("ix_apikey_api_key"))
op.drop_table("apikey")
if "user" in existing_tables:
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_user_username"))
op.drop_table("user")
if "flowstyle" in existing_tables:
op.drop_table("flowstyle")
if "component" in existing_tables:
op.drop_table("component")
# ### end Alembic commands ###

View file

@ -0,0 +1,49 @@
"""Add profile-image column
Revision ID: 67cc006d50bf
Revises: 260dbcc8b680
Create Date: 2023-09-08 07:36:13.387318
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "67cc006d50bf"
down_revision: Union[str, None] = "260dbcc8b680"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
if "user" in inspector.get_table_names() and "profile_image" not in [
column["name"] for column in inspector.get_columns("user")
]:
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
)
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
if "user" in inspector.get_table_names() and "profile_image" in [
column["name"] for column in inspector.get_columns("user")
]:
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("profile_image")
# ### end Alembic commands ###

View file

@ -5,8 +5,10 @@ from langflow.api.v1 import (
endpoints_router, endpoints_router,
validate_router, validate_router,
flows_router, flows_router,
flow_styles_router,
component_router, component_router,
users_router,
api_key_router,
login_router,
) )
router = APIRouter( router = APIRouter(
@ -17,4 +19,6 @@ router.include_router(endpoints_router)
router.include_router(validate_router) router.include_router(validate_router)
router.include_router(component_router) router.include_router(component_router)
router.include_router(flows_router) router.include_router(flows_router)
router.include_router(flow_styles_router) router.include_router(users_router)
router.include_router(api_key_router)
router.include_router(login_router)

View file

@ -66,3 +66,30 @@ def merge_nested_dicts(dict1, dict2):
else: else:
dict1[key] = value dict1[key] = value
return dict1 return dict1
def merge_nested_dicts_with_renaming(dict1, dict2):
for key, value in dict2.items():
if (
key in dict1
and isinstance(value, dict)
and isinstance(dict1.get(key), dict)
):
for sub_key, sub_value in value.items():
if sub_key in dict1[key]:
new_key = get_new_key(dict1[key], sub_key)
dict1[key][new_key] = sub_value
else:
dict1[key][sub_key] = sub_value
else:
dict1[key] = value
return dict1
def get_new_key(dictionary, original_key):
counter = 1
new_key = original_key + " (" + str(counter) + ")"
while new_key in dictionary:
counter += 1
new_key = original_key + " (" + str(counter) + ")"
return new_key

View file

@ -2,8 +2,10 @@ from langflow.api.v1.endpoints import router as endpoints_router
from langflow.api.v1.validate import router as validate_router from langflow.api.v1.validate import router as validate_router
from langflow.api.v1.chat import router as chat_router from langflow.api.v1.chat import router as chat_router
from langflow.api.v1.flows import router as flows_router from langflow.api.v1.flows import router as flows_router
from langflow.api.v1.flow_styles import router as flow_styles_router
from langflow.api.v1.components import router as component_router from langflow.api.v1.components import router as component_router
from langflow.api.v1.users import router as users_router
from langflow.api.v1.api_key import router as api_key_router
from langflow.api.v1.login import router as login_router
__all__ = [ __all__ = [
"chat_router", "chat_router",
@ -11,5 +13,7 @@ __all__ = [
"component_router", "component_router",
"validate_router", "validate_router",
"flows_router", "flows_router",
"flow_styles_router", "users_router",
"api_key_router",
"login_router",
] ]

View file

@ -0,0 +1,61 @@
from uuid import UUID
from fastapi import APIRouter, HTTPException, Depends
from langflow.api.v1.schemas import ApiKeysResponse
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.api_key.api_key import (
ApiKeyCreate,
UnmaskedApiKeyRead,
)
# Assuming you have these methods in your service layer
from langflow.services.database.models.api_key.crud import (
get_api_keys,
create_api_key,
delete_api_key,
)
from langflow.services.database.models.user.user import User
from langflow.services.utils import get_session
from sqlmodel import Session
router = APIRouter(tags=["APIKey"], prefix="/api_key")
@router.get("/", response_model=ApiKeysResponse)
def get_api_keys_route(
db: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user),
):
try:
user_id = current_user.id
keys = get_api_keys(db, user_id)
return ApiKeysResponse(total_count=len(keys), user_id=user_id, api_keys=keys)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/", response_model=UnmaskedApiKeyRead)
def create_api_key_route(
req: ApiKeyCreate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_session),
):
try:
user_id = current_user.id
return create_api_key(db, req, user_id=user_id)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) from e
@router.delete("/{api_key_id}")
def delete_api_key_route(
api_key_id: UUID,
current_user=Depends(get_current_active_user),
db: Session = Depends(get_session),
):
try:
delete_api_key(db, api_key_id)
return {"detail": "API Key deleted"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) from e

View file

@ -1,3 +1,4 @@
from typing import Optional
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
@ -20,7 +21,8 @@ class FrontendNodeRequest(FrontendNode):
class ValidatePromptRequest(BaseModel): class ValidatePromptRequest(BaseModel):
name: str name: str
template: str template: str
frontend_node: FrontendNodeRequest # optional for tweak call
frontend_node: Optional[FrontendNodeRequest] = None
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}} # Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
@ -39,7 +41,8 @@ class CodeValidationResponse(BaseModel):
class PromptValidationResponse(BaseModel): class PromptValidationResponse(BaseModel):
input_variables: list input_variables: list
frontend_node: FrontendNodeRequest # object return for tweak call
frontend_node: Optional[FrontendNodeRequest] = None
INVALID_CHARACTERS = { INVALID_CHARACTERS = {

View file

@ -10,7 +10,7 @@ from fastapi import WebSocket
from langchain.schema import AgentAction, LLMResult, AgentFinish from langchain.schema import AgentAction, LLMResult, AgentFinish
from langflow.utils.logger import logger from loguru import logger
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py # https://github.com/hwchase17/chat-langchain/blob/master/callback.py

View file

@ -1,37 +1,79 @@
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketException, status from fastapi import (
APIRouter,
Depends,
HTTPException,
Query,
WebSocket,
WebSocketException,
status,
)
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langflow.api.utils import build_input_keys_response from langflow.api.utils import build_input_keys_response
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
from langflow.chat.manager import ChatManager
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
from langflow.utils.logger import logger from langflow.services.auth.utils import get_current_active_user, get_current_user
from loguru import logger
from langflow.services.utils import get_chat_manager, get_session
from cachetools import LRUCache from cachetools import LRUCache
from sqlmodel import Session
from langflow.services.chat.manager import ChatManager
router = APIRouter(tags=["Chat"]) router = APIRouter(tags=["Chat"])
chat_manager = ChatManager()
flow_data_store: LRUCache = LRUCache(maxsize=10) flow_data_store: LRUCache = LRUCache(maxsize=10)
@router.websocket("/chat/{client_id}") @router.websocket("/chat/{client_id}")
async def chat(client_id: str, websocket: WebSocket): async def chat(
client_id: str,
websocket: WebSocket,
token: str = Query(...),
db: Session = Depends(get_session),
chat_manager: "ChatManager" = Depends(get_chat_manager),
):
"""Websocket endpoint for chat.""" """Websocket endpoint for chat."""
try: try:
await websocket.accept()
user = await get_current_user(token, db)
if not user:
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
)
if not user.is_active:
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
)
if client_id in chat_manager.in_memory_cache: if client_id in chat_manager.in_memory_cache:
await chat_manager.handle_websocket(client_id, websocket) await chat_manager.handle_websocket(client_id, websocket)
else: else:
# We accept the connection but close it immediately # We accept the connection but close it immediately
# if the flow is not built yet # if the flow is not built yet
await websocket.accept()
message = "Please, build the flow before sending messages" message = "Please, build the flow before sending messages"
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message) await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message)
except WebSocketException as exc: except WebSocketException as exc:
logger.error(f"Websocket error: {exc}") logger.error(f"Websocket error: {exc}")
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc)) await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
except Exception as exc:
logger.error(f"Error in chat websocket: {exc}")
messsage = exc.detail if isinstance(exc, HTTPException) else str(exc)
if "Could not validate credentials" in str(exc):
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
)
else:
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=messsage)
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201) @router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
async def init_build(graph_data: dict, flow_id: str): async def init_build(
graph_data: dict,
flow_id: str,
current_user=Depends(get_current_active_user),
chat_manager: "ChatManager" = Depends(get_chat_manager),
):
"""Initialize the build by storing graph data and returning a unique session ID.""" """Initialize the build by storing graph data and returning a unique session ID."""
try: try:
@ -52,6 +94,7 @@ async def init_build(graph_data: dict, flow_id: str):
flow_data_store[flow_id] = { flow_data_store[flow_id] = {
"graph_data": graph_data, "graph_data": graph_data,
"status": BuildStatus.STARTED, "status": BuildStatus.STARTED,
"user_id": current_user.id,
} }
return InitResponse(flowId=flow_id) return InitResponse(flowId=flow_id)
@ -79,7 +122,9 @@ async def build_status(flow_id: str):
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse) @router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
async def stream_build(flow_id: str): async def stream_build(
flow_id: str, chat_manager: "ChatManager" = Depends(get_chat_manager)
):
"""Stream the build process based on stored flow data.""" """Stream the build process based on stored flow data."""
async def event_stream(flow_id): async def event_stream(flow_id):
@ -97,6 +142,7 @@ async def stream_build(flow_id: str):
return return
graph_data = flow_data_store[flow_id].get("graph_data") graph_data = flow_data_store[flow_id].get("graph_data")
user_id = flow_data_store[flow_id]["user_id"]
if not graph_data: if not graph_data:
error_message = "No data provided" error_message = "No data provided"
@ -104,14 +150,9 @@ async def stream_build(flow_id: str):
return return
logger.debug("Building langchain object") logger.debug("Building langchain object")
try:
# Some error could happen when building the graph # Some error could happen when building the graph
graph = Graph.from_payload(graph_data) graph = Graph.from_payload(graph_data)
except Exception as exc:
logger.exception(exc)
error_message = str(exc)
yield str(StreamData(event="error", data={"error": error_message}))
return
number_of_nodes = len(graph.nodes) number_of_nodes = len(graph.nodes)
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
@ -122,11 +163,13 @@ async def stream_build(flow_id: str):
"log": f"Building node {vertex.vertex_type}", "log": f"Building node {vertex.vertex_type}",
} }
yield str(StreamData(event="log", data=log_dict)) yield str(StreamData(event="log", data=log_dict))
vertex.build() vertex.build(user_id)
params = vertex._built_object_repr() params = vertex._built_object_repr()
valid = True valid = True
logger.debug(f"Building node {str(vertex.vertex_type)}") logger.debug(f"Building node {str(vertex.vertex_type)}")
logger.debug(f"Output: {params}") logger.debug(
f"Output: {params[:100]}{'...' if len(params) > 100 else ''}"
)
if vertex.artifacts: if vertex.artifacts:
# The artifacts will be prompt variables # The artifacts will be prompt variables
# passed to build_input_keys_response # passed to build_input_keys_response
@ -155,12 +198,11 @@ async def stream_build(flow_id: str):
) )
else: else:
input_keys_response = { input_keys_response = {
"input_keys": {}, "input_keys": None,
"memory_keys": [], "memory_keys": [],
"handle_keys": [], "handle_keys": [],
} }
yield str(StreamData(event="message", data=input_keys_response)) yield str(StreamData(event="message", data=input_keys_response))
chat_manager.set_cache(flow_id, langchain_object) chat_manager.set_cache(flow_id, langchain_object)
# We need to reset the chat history # We need to reset the chat history
chat_manager.chat_history.empty_history(flow_id) chat_manager.chat_history.empty_history(flow_id)

View file

@ -1,8 +1,8 @@
from datetime import timezone from datetime import timezone
from typing import List from typing import List
from uuid import UUID from uuid import UUID
from langflow.database.models.component import Component, ComponentModel from langflow.services.database.models.component import Component, ComponentModel
from langflow.database.base import get_session from langflow.services.utils import get_session
from sqlmodel import Session, select from sqlmodel import Session, select
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError

View file

@ -1,14 +1,15 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Annotated, Optional from typing import Annotated, Any, Optional, Union
from langflow.services.auth.utils import api_key_security, get_current_active_user
from langflow.cache.utils import save_uploaded_file from langflow.services.cache.utils import save_uploaded_file
from langflow.database.models.flow import Flow from langflow.services.database.models.flow import Flow
from langflow.processing.process import process_graph_cached, process_tweaks from langflow.processing.process import process_graph_cached, process_tweaks
from langflow.utils.logger import logger from langflow.services.database.models.user.user import User
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from loguru import logger
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status
import sqlalchemy as sa
from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.custom_component import CustomComponent
@ -18,7 +19,7 @@ from langflow.api.v1.schemas import (
CustomComponentCode, CustomComponentCode,
) )
from langflow.api.utils import merge_nested_dicts from langflow.api.utils import merge_nested_dicts_with_renaming
from langflow.interface.types import ( from langflow.interface.types import (
build_langchain_types_dict, build_langchain_types_dict,
@ -26,52 +27,93 @@ from langflow.interface.types import (
build_langchain_custom_component_list_from_path, build_langchain_custom_component_list_from_path,
) )
from langflow.database.base import get_session from langflow.services.utils import get_session
from sqlmodel import Session from sqlmodel import Session
# build router # build router
router = APIRouter(tags=["Base"]) router = APIRouter(tags=["Base"])
@router.get("/all") @router.get("/all", dependencies=[Depends(get_current_active_user)])
def get_all(): def get_all(
settings_manager=Depends(get_settings_manager),
):
logger.debug("Building langchain types dict") logger.debug("Building langchain types dict")
native_components = build_langchain_types_dict() native_components = build_langchain_types_dict()
# custom_components is a list of dicts # custom_components is a list of dicts
# need to merge all the keys into one dict # need to merge all the keys into one dict
custom_components_from_file = {} custom_components_from_file: dict[str, Any] = {}
if settings.COMPONENTS_PATH: if settings_manager.settings.COMPONENTS_PATH:
logger.info(f"Building custom components from {settings.COMPONENTS_PATH}") logger.info(
custom_component_dicts = [ f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
build_langchain_custom_component_list_from_path(str(path)) )
for path in settings.COMPONENTS_PATH
]
logger.info(f"Loading {len(custom_component_dicts)} custom components")
custom_component_dicts = []
processed_paths = []
for path in settings_manager.settings.COMPONENTS_PATH:
if str(path) in processed_paths:
continue
custom_component_dict = build_langchain_custom_component_list_from_path(
str(path)
)
custom_component_dicts.append(custom_component_dict)
processed_paths.append(str(path))
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
for custom_component_dict in custom_component_dicts: for custom_component_dict in custom_component_dicts:
custom_components_from_file = merge_nested_dicts( # custom_component_dict is a dict of dicts
if not custom_component_dict:
continue
category = list(custom_component_dict.keys())[0]
logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict custom_components_from_file, custom_component_dict
) )
logger.info(f"Loaded {custom_component_dict}")
return merge_nested_dicts(native_components, custom_components_from_file) return merge_nested_dicts_with_renaming(
native_components, custom_components_from_file
)
# For backwards compatibility we will keep the old endpoint # For backwards compatibility we will keep the old endpoint
@router.post("/predict/{flow_id}", response_model=ProcessResponse) @router.post(
@router.post("/process/{flow_id}", response_model=ProcessResponse) "/predict/{flow_id}",
response_model=ProcessResponse,
dependencies=[Depends(api_key_security)],
)
@router.post(
"/process/{flow_id}",
response_model=ProcessResponse,
)
async def process_flow( async def process_flow(
session: Annotated[Session, Depends(get_session)],
flow_id: str, flow_id: str,
inputs: Optional[dict] = None, inputs: Optional[dict] = None,
tweaks: Optional[dict] = None, tweaks: Optional[dict] = None,
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821 clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
session: Session = Depends(get_session), session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
api_key_user: User = Depends(api_key_security),
): ):
""" """
Endpoint to process an input with a given flow_id. Endpoint to process an input with a given flow_id.
""" """
try: try:
flow = session.get(Flow, flow_id) if api_key_user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API Key",
)
# Get the flow that matches the flow_id and belongs to the user
flow = (
session.query(Flow)
.filter(Flow.id == flow_id)
.filter(Flow.user_id == api_key_user.id)
.first()
)
if flow is None: if flow is None:
raise ValueError(f"Flow {flow_id} not found") raise ValueError(f"Flow {flow_id} not found")
@ -83,10 +125,26 @@ async def process_flow(
graph_data = process_tweaks(graph_data, tweaks) graph_data = process_tweaks(graph_data, tweaks)
except Exception as exc: except Exception as exc:
logger.error(f"Error processing tweaks: {exc}") logger.error(f"Error processing tweaks: {exc}")
response = process_graph_cached(graph_data, inputs, clear_cache) response, session_id = process_graph_cached(
return ProcessResponse( graph_data, inputs, clear_cache, session_id
result=response,
) )
return ProcessResponse(result=response, session_id=session_id)
except sa.exc.StatementError as exc:
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc):
# This means the Flow ID is not a valid UUID which means it can't find the flow
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
) from exc
except ValueError as exc:
if f"Flow {flow_id} not found" in str(exc):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
) from exc
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
except Exception as e: except Exception as e:
# Log stack trace # Log stack trace
logger.exception(e) logger.exception(e)

View file

@ -1,83 +0,0 @@
from uuid import UUID
from langflow.database.models.flow_style import (
FlowStyle,
FlowStyleCreate,
FlowStyleRead,
FlowStyleUpdate,
)
from langflow.database.base import get_session
from sqlmodel import Session, select
from fastapi import APIRouter, Depends, HTTPException
# build router
router = APIRouter(prefix="/flow_styles", tags=["FlowStyles"])
# FlowStyleCreate:
# class FlowStyleBase(SQLModel):
# color: str = Field(index=True)
# emoji: str = Field(index=False)
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
@router.post("/", response_model=FlowStyleRead)
def create_flow_style(
*, session: Session = Depends(get_session), flow_style: FlowStyleCreate
):
"""Create a new flow_style."""
db_flow_style = FlowStyle.from_orm(flow_style)
session.add(db_flow_style)
session.commit()
session.refresh(db_flow_style)
return db_flow_style
@router.get("/", response_model=list[FlowStyleRead])
def read_flow_styles(*, session: Session = Depends(get_session)):
"""Read all flows."""
try:
flows = session.exec(select(FlowStyle)).all()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return flows
@router.get("/{flow_styles_id}", response_model=FlowStyleRead)
def read_flow_style(*, session: Session = Depends(get_session), flow_styles_id: UUID):
"""Read a flow_style."""
if flow_style := session.get(FlowStyle, flow_styles_id):
return flow_style
else:
raise HTTPException(status_code=404, detail="FlowStyle not found")
@router.patch("/{flow_style_id}", response_model=FlowStyleRead)
def update_flow_style(
*,
session: Session = Depends(get_session),
flow_style_id: UUID,
flow_style: FlowStyleUpdate,
):
"""Update a flow_style."""
db_flow_style = session.get(FlowStyle, flow_style_id)
if not db_flow_style:
raise HTTPException(status_code=404, detail="FlowStyle not found")
flow_data = flow_style.dict(exclude_unset=True)
for key, value in flow_data.items():
if hasattr(db_flow_style, key) and value is not None:
setattr(db_flow_style, key, value)
session.add(db_flow_style)
session.commit()
session.refresh(db_flow_style)
return db_flow_style
@router.delete("/{flow_id}")
def delete_flow_style(*, session: Session = Depends(get_session), flow_id: UUID):
"""Delete a flow_style."""
flow_style = session.get(FlowStyle, flow_id)
if not flow_style:
raise HTTPException(status_code=404, detail="FlowStyle not found")
session.delete(flow_style)
session.commit()
return {"message": "FlowStyle deleted successfully"}

View file

@ -1,70 +1,101 @@
from typing import List from typing import List
from uuid import UUID from uuid import UUID
from langflow.settings import settings from fastapi.encoders import jsonable_encoder
from langflow.api.utils import remove_api_keys from langflow.api.utils import remove_api_keys
from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.api.v1.schemas import FlowListCreate, FlowListRead
from langflow.database.models.flow import ( from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow import (
Flow, Flow,
FlowCreate, FlowCreate,
FlowRead, FlowRead,
FlowReadWithStyle,
FlowUpdate, FlowUpdate,
) )
from langflow.database.base import get_session from langflow.services.database.models.user.user import User
from sqlmodel import Session, select from langflow.services.utils import get_session
from langflow.services.utils import get_settings_manager
import orjson
from sqlmodel import Session
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi import File, UploadFile from fastapi import File, UploadFile
import json
# build router # build router
router = APIRouter(prefix="/flows", tags=["Flows"]) router = APIRouter(prefix="/flows", tags=["Flows"])
@router.post("/", response_model=FlowRead, status_code=201) @router.post("/", response_model=FlowRead, status_code=201)
def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate): def create_flow(
*,
session: Session = Depends(get_session),
flow: FlowCreate,
current_user: User = Depends(get_current_active_user),
):
"""Create a new flow.""" """Create a new flow."""
if flow.user_id is None:
flow.user_id = current_user.id
db_flow = Flow.from_orm(flow) db_flow = Flow.from_orm(flow)
session.add(db_flow) session.add(db_flow)
session.commit() session.commit()
session.refresh(db_flow) session.refresh(db_flow)
return db_flow return db_flow
@router.get("/", response_model=list[FlowReadWithStyle], status_code=200) @router.get("/", response_model=list[FlowRead], status_code=200)
def read_flows(*, session: Session = Depends(get_session)): def read_flows(
*,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user),
):
"""Read all flows.""" """Read all flows."""
try: try:
flows = session.exec(select(Flow)).all() flows = current_user.flows
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e raise HTTPException(status_code=500, detail=str(e)) from e
return [jsonable_encoder(flow) for flow in flows] return [jsonable_encoder(flow) for flow in flows]
@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200) @router.get("/{flow_id}", response_model=FlowRead, status_code=200)
def read_flow(*, session: Session = Depends(get_session), flow_id: UUID): def read_flow(
*,
session: Session = Depends(get_session),
flow_id: UUID,
current_user: User = Depends(get_current_active_user),
):
"""Read a flow.""" """Read a flow."""
if flow := session.get(Flow, flow_id): if user_flow := (
return flow session.query(Flow)
.filter(Flow.id == flow_id)
.filter(Flow.user_id == current_user.id)
.first()
):
return user_flow
else: else:
raise HTTPException(status_code=404, detail="Flow not found") raise HTTPException(status_code=404, detail="Flow not found")
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200) @router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
def update_flow( def update_flow(
*, session: Session = Depends(get_session), flow_id: UUID, flow: FlowUpdate *,
session: Session = Depends(get_session),
flow_id: UUID,
flow: FlowUpdate,
current_user: User = Depends(get_current_active_user),
settings_manager=Depends(get_settings_manager),
): ):
"""Update a flow.""" """Update a flow."""
db_flow = session.get(Flow, flow_id) db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
if not db_flow: if not db_flow:
raise HTTPException(status_code=404, detail="Flow not found") raise HTTPException(status_code=404, detail="Flow not found")
flow_data = flow.dict(exclude_unset=True) flow_data = flow.dict(exclude_unset=True)
if settings.REMOVE_API_KEYS: if settings_manager.settings.REMOVE_API_KEYS:
flow_data = remove_api_keys(flow_data) flow_data = remove_api_keys(flow_data)
for key, value in flow_data.items(): for key, value in flow_data.items():
setattr(db_flow, key, value) if value is not None:
setattr(db_flow, key, value)
session.add(db_flow) session.add(db_flow)
session.commit() session.commit()
session.refresh(db_flow) session.refresh(db_flow)
@ -72,9 +103,14 @@ def update_flow(
@router.delete("/{flow_id}", status_code=200) @router.delete("/{flow_id}", status_code=200)
def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID): def delete_flow(
*,
session: Session = Depends(get_session),
flow_id: UUID,
current_user: User = Depends(get_current_active_user),
):
"""Delete a flow.""" """Delete a flow."""
flow = session.get(Flow, flow_id) flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
if not flow: if not flow:
raise HTTPException(status_code=404, detail="Flow not found") raise HTTPException(status_code=404, detail="Flow not found")
session.delete(flow) session.delete(flow)
@ -86,10 +122,16 @@ def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
@router.post("/batch/", response_model=List[FlowRead], status_code=201) @router.post("/batch/", response_model=List[FlowRead], status_code=201)
def create_flows(*, session: Session = Depends(get_session), flow_list: FlowListCreate): def create_flows(
*,
session: Session = Depends(get_session),
flow_list: FlowListCreate,
current_user: User = Depends(get_current_active_user),
):
"""Create multiple new flows.""" """Create multiple new flows."""
db_flows = [] db_flows = []
for flow in flow_list.flows: for flow in flow_list.flows:
flow.user_id = current_user.id
db_flow = Flow.from_orm(flow) db_flow = Flow.from_orm(flow)
session.add(db_flow) session.add(db_flow)
db_flows.append(db_flow) db_flows.append(db_flow)
@ -101,20 +143,31 @@ def create_flows(*, session: Session = Depends(get_session), flow_list: FlowList
@router.post("/upload/", response_model=List[FlowRead], status_code=201) @router.post("/upload/", response_model=List[FlowRead], status_code=201)
async def upload_file( async def upload_file(
*, session: Session = Depends(get_session), file: UploadFile = File(...) *,
session: Session = Depends(get_session),
file: UploadFile = File(...),
current_user: User = Depends(get_current_active_user),
): ):
"""Upload flows from a file.""" """Upload flows from a file."""
contents = await file.read() contents = await file.read()
data = json.loads(contents) data = orjson.loads(contents)
if "flows" in data: if "flows" in data:
flow_list = FlowListCreate(**data) flow_list = FlowListCreate(**data)
else: else:
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data]) flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data])
return create_flows(session=session, flow_list=flow_list) # Now we set the user_id for all flows
for flow in flow_list.flows:
flow.user_id = current_user.id
return create_flows(session=session, flow_list=flow_list, current_user=current_user)
@router.get("/download/", response_model=FlowListRead, status_code=200) @router.get("/download/", response_model=FlowListRead, status_code=200)
async def download_file(*, session: Session = Depends(get_session)): async def download_file(
*,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user),
):
"""Download all flows as a file.""" """Download all flows as a file."""
flows = read_flows(session=session) flows = read_flows(session=session, current_user=current_user)
return FlowListRead(flows=flows) return FlowListRead(flows=flows)

View file

@ -0,0 +1,63 @@
from sqlmodel import Session
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from langflow.services.utils import get_session
from langflow.api.v1.schemas import Token
from langflow.services.auth.utils import (
authenticate_user,
create_user_tokens,
create_refresh_token,
create_user_longterm_token,
get_current_active_user,
)
from langflow.services.utils import get_settings_manager
router = APIRouter(tags=["Login"])
@router.post("/login", response_model=Token)
async def login_to_get_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_session),
# _: Session = Depends(get_current_active_user)
):
if user := authenticate_user(form_data.username, form_data.password, db):
return create_user_tokens(user_id=user.id, db=db, update_last_login=True)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get("/auto_login")
async def auto_login(
db: Session = Depends(get_session), settings_manager=Depends(get_settings_manager)
):
if settings_manager.auth_settings.AUTO_LOGIN:
return create_user_longterm_token(db)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"message": "Auto login is disabled. Please enable it in the settings",
"auto_login": False,
},
)
@router.post("/refresh")
async def refresh_token(
token: str, current_user: Session = Depends(get_current_active_user)
):
if token:
return create_refresh_token(token)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
headers={"WWW-Authenticate": "Bearer"},
)

View file

@ -1,9 +1,13 @@
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from langflow.database.models.flow import FlowCreate, FlowRead from uuid import UUID
from langflow.services.database.models.api_key.api_key import ApiKeyRead
from langflow.services.database.models.flow import FlowCreate, FlowRead
from langflow.services.database.models.user import UserRead
from langflow.services.database.models.base import orjson_dumps
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
import json
class BuildStatus(Enum): class BuildStatus(Enum):
@ -47,6 +51,7 @@ class ProcessResponse(BaseModel):
"""Process response schema.""" """Process response schema."""
result: dict result: dict
session_id: Optional[str] = None
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
@ -115,7 +120,9 @@ class StreamData(BaseModel):
data: dict data: dict
def __str__(self) -> str: def __str__(self) -> str:
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n" return (
f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
)
class CustomComponentCode(BaseModel): class CustomComponentCode(BaseModel):
@ -133,3 +140,32 @@ class ComponentListCreate(BaseModel):
class ComponentListRead(BaseModel): class ComponentListRead(BaseModel):
flows: List[FlowRead] flows: List[FlowRead]
class UsersResponse(BaseModel):
total_count: int
users: List[UserRead]
class ApiKeyResponse(BaseModel):
id: str
api_key: str
name: str
created_at: str
last_used_at: str
class ApiKeysResponse(BaseModel):
total_count: int
user_id: UUID
api_keys: List[ApiKeyRead]
class CreateApiKeyRequest(BaseModel):
name: str
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str

View file

@ -0,0 +1,194 @@
from uuid import UUID
from langflow.api.v1.schemas import UsersResponse
from langflow.services.database.models.user import (
User,
UserCreate,
UserRead,
UserUpdate,
)
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from fastapi import APIRouter, Depends, HTTPException
from langflow.services.utils import get_session
from langflow.services.auth.utils import (
get_current_active_superuser,
get_current_active_user,
get_password_hash,
verify_password,
)
from langflow.services.database.models.user.crud import (
get_user_by_id,
update_user,
)
router = APIRouter(tags=["Users"], prefix="/users")
@router.post("/", response_model=UserRead, status_code=201)
def add_user(
user: UserCreate,
session: Session = Depends(get_session),
) -> User:
"""
Add a new user to the database.
"""
new_user = User.from_orm(user)
try:
new_user.password = get_password_hash(user.password)
session.add(new_user)
session.commit()
session.refresh(new_user)
except IntegrityError as e:
session.rollback()
raise HTTPException(
status_code=400, detail="This username is unavailable."
) from e
return new_user
@router.get("/whoami", response_model=UserRead)
def read_current_user(
current_user: User = Depends(get_current_active_user),
) -> User:
"""
Retrieve the current user's data.
"""
return current_user
@router.get("/", response_model=UsersResponse)
def read_all_users(
skip: int = 0,
limit: int = 10,
current_user: Session = Depends(get_current_active_superuser),
session: Session = Depends(get_session),
) -> UsersResponse:
"""
Retrieve a list of users from the database with pagination.
"""
query = select(User).offset(skip).limit(limit)
users = session.execute(query).fetchall()
count_query = select(func.count()).select_from(User) # type: ignore
total_count = session.execute(count_query).scalar()
return UsersResponse(
total_count=total_count, # type: ignore
users=[UserRead(**dict(user.User)) for user in users],
)
@router.patch("/{user_id}", response_model=UserRead)
def patch_user(
user_id: UUID,
user_update: UserUpdate,
user: User = Depends(get_current_active_user),
session: Session = Depends(get_session),
) -> User:
"""
Update an existing user's data.
"""
if not user.is_superuser and user.id != user_id:
raise HTTPException(
status_code=403, detail="You don't have the permission to update this user"
)
if user_update.password:
raise HTTPException(
status_code=400, detail="You can't change your password here"
)
if user_db := get_user_by_id(session, user_id):
return update_user(user_db, user_update, session)
else:
raise HTTPException(status_code=404, detail="User not found")
@router.patch("/{user_id}/reset-password", response_model=UserRead)
def reset_password(
user_id: UUID,
user_update: UserUpdate,
user: User = Depends(get_current_active_user),
session: Session = Depends(get_session),
) -> User:
"""
Reset a user's password.
"""
if user_id != user.id:
raise HTTPException(
status_code=400, detail="You can't change another user's password"
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if verify_password(user_update.password, user.password):
raise HTTPException(
status_code=400, detail="You can't use your current password"
)
new_password = get_password_hash(user_update.password)
user.password = new_password
session.commit()
session.refresh(user)
return user
@router.delete("/{user_id}", response_model=dict)
def delete_user(
user_id: UUID,
current_user: User = Depends(get_current_active_superuser),
session: Session = Depends(get_session),
) -> dict:
"""
Delete a user from the database.
"""
if current_user.id == user_id:
raise HTTPException(
status_code=400, detail="You can't delete your own user account"
)
elif not current_user.is_superuser:
raise HTTPException(
status_code=403, detail="You don't have the permission to delete this user"
)
user_db = session.query(User).filter(User.id == user_id).first()
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
session.delete(user_db)
session.commit()
return {"detail": "User deleted"}
# TODO: REMOVE - Just for testing purposes
@router.post("/super_user", response_model=User)
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
session: Session = Depends(get_session),
) -> User:
"""
Add a superuser for testing purposes.
(This should be removed in production)
"""
new_user = User(
username="superuser",
password=get_password_hash("12345"),
is_active=True,
is_superuser=True,
last_login_at=None,
)
try:
session.add(new_user)
session.commit()
session.refresh(new_user)
except IntegrityError as e:
session.rollback()
raise HTTPException(status_code=400, detail="User exists") from e
return new_user

View file

@ -8,7 +8,7 @@ from langflow.api.v1.base import (
validate_prompt, validate_prompt,
) )
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.utils.logger import logger from loguru import logger
from langflow.utils.validate import validate_code from langflow.utils.validate import validate_code
# build router # build router
@ -31,7 +31,12 @@ def post_validate_code(code: Code):
def post_validate_prompt(prompt_request: ValidatePromptRequest): def post_validate_prompt(prompt_request: ValidatePromptRequest):
try: try:
input_variables = validate_prompt(prompt_request.template) input_variables = validate_prompt(prompt_request.template)
# Check if frontend_node is None before proceeding to avoid attempting to update a non-existent node.
if prompt_request.frontend_node is None:
return PromptValidationResponse(
input_variables=input_variables,
frontend_node=None,
)
old_custom_fields = get_old_custom_fields(prompt_request) old_custom_fields = get_old_custom_fields(prompt_request)
add_new_variables_to_template(input_variables, prompt_request) add_new_variables_to_template(input_variables, prompt_request)
@ -53,6 +58,16 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
def get_old_custom_fields(prompt_request): def get_old_custom_fields(prompt_request):
try: try:
if (
len(prompt_request.frontend_node.custom_fields) == 1
and prompt_request.name == ""
):
# If there is only one custom field and the name is empty string
# then we are dealing with the first prompt request after the node was created
prompt_request.name = list(
prompt_request.frontend_node.custom_fields.keys()
)[0]
old_custom_fields = prompt_request.frontend_node.custom_fields[ old_custom_fields = prompt_request.frontend_node.custom_fields[
prompt_request.name prompt_request.name
].copy() ].copy()

View file

@ -1,7 +0,0 @@
from langflow.cache.manager import cache_manager
from langflow.cache.flow import InMemoryCache
__all__ = [
"cache_manager",
"InMemoryCache",
]

View file

@ -0,0 +1,82 @@
from langflow import CustomComponent
from typing import Optional
from langchain.prompts import SystemMessagePromptTemplate
from langchain.tools import Tool
from langchain.schema.memory import BaseMemory
from langchain.chat_models import ChatOpenAI
from langchain.agents.agent import AgentExecutor
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
from langchain.memory.token_buffer import ConversationTokenBufferMemory
from langchain.prompts.chat import MessagesPlaceholder
from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import (
_get_default_system_message,
)
class ConversationalAgent(CustomComponent):
display_name: str = "OpenAI Conversational Agent"
description: str = "Conversational Agent that can use OpenAI's function calling API"
def build_config(self):
openai_function_models = [
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0613",
"gpt-4-32k-0613",
]
return {
"tools": {"is_list": True, "display_name": "Tools"},
"memory": {"display_name": "Memory"},
"system_message": {"display_name": "System Message"},
"max_token_limit": {"display_name": "Max Token Limit"},
"model_name": {
"display_name": "Model Name",
"options": openai_function_models,
"value": openai_function_models[0],
},
"code": {"show": False},
}
def build(
self,
model_name: str,
openai_api_key: str,
tools: Tool,
openai_api_base: Optional[str] = None,
memory: Optional[BaseMemory] = None,
system_message: Optional[SystemMessagePromptTemplate] = None,
max_token_limit: int = 2000,
) -> AgentExecutor:
llm = ChatOpenAI(
model=model_name,
openai_api_key=openai_api_key,
openai_api_base=openai_api_base,
)
if not memory:
memory_key = "chat_history"
memory = ConversationTokenBufferMemory(
memory_key=memory_key,
return_messages=True,
output_key="output",
llm=llm,
max_token_limit=max_token_limit,
)
else:
memory_key = memory.memory_key # type: ignore
_system_message = system_message or _get_default_system_message()
prompt = OpenAIFunctionsAgent.create_prompt(
system_message=_system_message, # type: ignore
extra_prompt_messages=[MessagesPlaceholder(variable_name=memory_key)],
)
agent = OpenAIFunctionsAgent(
llm=llm, tools=tools, prompt=prompt # type: ignore
)
return AgentExecutor(
agent=agent,
tools=tools, # type: ignore
memory=memory,
verbose=True,
return_intermediate_steps=True,
)

View file

@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
"info": "Make sure the prompt has all variables filled.", "info": "Make sure the prompt has all variables filled.",
}, },
"code": {"show": False}, "code": {"show": False},
"inputs": {"field_type": "code"},
} }
def build( def build(
self, self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
llm: BaseLLM,
prompt: PromptTemplate,
) -> Document: ) -> Document:
chain = prompt | llm chain = prompt | llm
# The input is an empty dict because the prompt is already filled # The input is an empty dict because the prompt is already filled
result = chain.invoke({}) result = chain.invoke(input=inputs)
if hasattr(result, "content"): if hasattr(result, "content"):
result = result.content result = result.content
self.repr_value = result self.repr_value = result

View file

@ -0,0 +1,42 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms import HuggingFaceEndpoint
from langchain.llms.base import BaseLLM
class HuggingFaceEndpointsComponent(CustomComponent):
display_name: str = "Hugging Face Inference API"
description: str = "LLM model from Hugging Face Inference API."
def build_config(self):
return {
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
"task": {
"display_name": "Task",
"type": "select",
"options": ["text2text-generation", "text-generation", "summarization"],
},
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
"model_kwargs": {
"display_name": "Model Keyword Arguments",
"field_type": "code",
},
"code": {"show": False},
}
def build(
self,
endpoint_url: str,
task="text2text-generation",
huggingfacehub_api_token: Optional[str] = None,
model_kwargs: Optional[dict] = None,
) -> BaseLLM:
try:
output = HuggingFaceEndpoint(
endpoint_url=endpoint_url,
task=task,
huggingfacehub_api_token=huggingfacehub_api_token,
)
except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
return output

View file

@ -0,0 +1,28 @@
from typing import Optional
from langflow import CustomComponent
from langchain.retrievers import MetalRetriever
from langchain.schema import BaseRetriever
from metal_sdk.metal import Metal # type: ignore
class MetalRetrieverComponent(CustomComponent):
display_name: str = "Metal Retriever"
description: str = "Retriever that uses the Metal API."
def build_config(self):
return {
"api_key": {"display_name": "API Key", "password": True},
"client_id": {"display_name": "Client ID", "password": True},
"index_id": {"display_name": "Index ID"},
"params": {"display_name": "Parameters"},
"code": {"show": False},
}
def build(
self, api_key: str, client_id: str, index_id: str, params: Optional[dict] = None
) -> BaseRetriever:
try:
metal = Metal(api_key=api_key, client_id=client_id, index_id=index_id)
except Exception as e:
raise ValueError("Could not connect to Metal API.") from e
return MetalRetriever(client=metal, params=params or {})

View file

@ -0,0 +1,80 @@
from typing import Optional
from langflow import CustomComponent
from langchain.text_splitter import Language
from langchain.schema import Document
class LanguageRecursiveTextSplitterComponent(CustomComponent):
display_name: str = "Language Recursive Text Splitter"
description: str = "Split text into chunks of a specified length based on language."
documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter"
def build_config(self):
options = [x.value for x in Language]
return {
"documents": {
"display_name": "Documents",
"info": "The documents to split.",
},
"separator_type": {
"display_name": "Separator Type",
"info": "The type of separator to use.",
"field_type": "str",
"options": options,
"value": "Python",
},
"separators": {
"display_name": "Separators",
"info": "The characters to split on.",
"is_list": True,
},
"chunk_size": {
"display_name": "Chunk Size",
"info": "The maximum length of each chunk.",
"field_type": "int",
"value": 1000,
},
"chunk_overlap": {
"display_name": "Chunk Overlap",
"info": "The amount of overlap between chunks.",
"field_type": "int",
"value": 200,
},
"code": {"show": False},
}
def build(
self,
documents: list[Document],
chunk_size: Optional[int] = 1000,
chunk_overlap: Optional[int] = 200,
separator_type: Optional[str] = "Python",
) -> list[Document]:
"""
Split text into chunks of a specified length.
Args:
separators (list[str]): The characters to split on.
chunk_size (int): The maximum length of each chunk.
chunk_overlap (int): The amount of overlap between chunks.
length_function (function): The function to use to calculate the length of the text.
Returns:
list[str]: The chunks of text.
"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Make sure chunk_size and chunk_overlap are ints
if isinstance(chunk_size, str):
chunk_size = int(chunk_size)
if isinstance(chunk_overlap, str):
chunk_overlap = int(chunk_overlap)
splitter = RecursiveCharacterTextSplitter.from_language(
language=Language(separator_type),
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
docs = splitter.split_documents(documents)
return docs

View file

@ -0,0 +1,79 @@
from typing import Optional
from langflow import CustomComponent
from langchain.schema import Document
from langflow.utils.util import build_loader_repr_from_documents
class RecursiveCharacterTextSplitterComponent(CustomComponent):
display_name: str = "Recursive Character Text Splitter"
description: str = "Split text into chunks of a specified length."
documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter"
def build_config(self):
return {
"documents": {
"display_name": "Documents",
"info": "The documents to split.",
},
"separators": {
"display_name": "Separators",
"info": 'The characters to split on.\nIf left empty defaults to ["\\n\\n", "\\n", " ", ""].',
"is_list": True,
},
"chunk_size": {
"display_name": "Chunk Size",
"info": "The maximum length of each chunk.",
"field_type": "int",
"value": 1000,
},
"chunk_overlap": {
"display_name": "Chunk Overlap",
"info": "The amount of overlap between chunks.",
"field_type": "int",
"value": 200,
},
"code": {"show": False},
}
def build(
self,
documents: list[Document],
separators: Optional[list[str]] = None,
chunk_size: Optional[int] = 1000,
chunk_overlap: Optional[int] = 200,
) -> list[Document]:
"""
Split text into chunks of a specified length.
Args:
separators (list[str]): The characters to split on.
chunk_size (int): The maximum length of each chunk.
chunk_overlap (int): The amount of overlap between chunks.
length_function (function): The function to use to calculate the length of the text.
Returns:
list[str]: The chunks of text.
"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
if separators == "":
separators = None
elif separators:
# check if the separators list has escaped characters
# if there are escaped characters, unescape them
separators = [x.encode().decode("unicode-escape") for x in separators]
# Make sure chunk_size and chunk_overlap are ints
if isinstance(chunk_size, str):
chunk_size = int(chunk_size)
if isinstance(chunk_overlap, str):
chunk_overlap = int(chunk_overlap)
splitter = RecursiveCharacterTextSplitter(
separators=separators,
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
docs = splitter.split_documents(documents)
self.repr_value = build_loader_repr_from_documents(docs)
return docs

View file

@ -0,0 +1,56 @@
from typing import List, Union
from langflow import CustomComponent
from metaphor_python import Metaphor # type: ignore
from langchain.tools import Tool
from langchain.agents import tool
from langchain.agents.agent_toolkits.base import BaseToolkit
class MetaphorToolkit(CustomComponent):
display_name: str = "Metaphor"
description: str = "Metaphor Toolkit"
documentation = (
"https://python.langchain.com/docs/integrations/tools/metaphor_search"
)
beta = True
# api key should be password = True
field_config = {
"metaphor_api_key": {"display_name": "Metaphor API Key", "password": True},
"code": {"advanced": True},
}
def build(
self,
metaphor_api_key: str,
use_autoprompt: bool = True,
search_num_results: int = 5,
similar_num_results: int = 5,
) -> Union[Tool, BaseToolkit]:
# If documents, then we need to create a Vectara instance using .from_documents
client = Metaphor(api_key=metaphor_api_key)
@tool
def search(query: str):
"""Call search engine with a query."""
return client.search(
query, use_autoprompt=use_autoprompt, num_results=search_num_results
)
@tool
def get_contents(ids: List[str]):
"""Get contents of a webpage.
The ids passed in should be a list of ids as fetched from `search`.
"""
return client.get_contents(ids)
@tool
def find_similar(url: str):
"""Get search results similar to a given URL.
The url passed in should be a URL returned from `search`
"""
return client.find_similar(url, num_results=similar_num_results)
return [search, get_contents, find_similar] # type: ignore

View file

@ -0,0 +1,75 @@
from langflow import CustomComponent
from langchain.schema import Document
from langflow.services.database.models.base import orjson_dumps
import requests
from typing import Optional
class GetRequest(CustomComponent):
display_name: str = "GET Request"
description: str = "Make a GET request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#get-request"
beta = True
field_config = {
"url": {
"display_name": "URL",
"info": "The URL to make the request to",
"is_list": True,
},
"headers": {
"display_name": "Headers",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"timeout": {
"display_name": "Timeout",
"field_type": "int",
"info": "The timeout to use for the request.",
"value": 5,
},
}
def get_document(
self, session: requests.Session, url: str, headers: Optional[dict], timeout: int
) -> Document:
try:
response = session.get(url, headers=headers, timeout=int(timeout))
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response.status_code,
},
)
except requests.Timeout:
return Document(
page_content="Request Timed Out",
metadata={"source": url, "headers": headers, "status_code": 408},
)
except Exception as exc:
return Document(
page_content=str(exc),
metadata={"source": url, "headers": headers, "status_code": 500},
)
def build(
self,
url: str,
headers: Optional[dict] = None,
timeout: int = 5,
) -> list[Document]:
if headers is None:
headers = {}
urls = url if isinstance(url, list) else [url]
with requests.Session() as session:
documents = [self.get_document(session, u, headers, timeout) for u in urls]
self.repr_value = documents
return documents

View file

@ -0,0 +1,55 @@
### JSON Document Builder
# Build a Document containing a JSON object using a key and another Document page content.
# **Params**
# - **Key:** The key to use for the JSON object.
# - **Document:** The Document page to use for the JSON object.
# **Output**
# - **Document:** The Document containing the JSON object.
from langflow import CustomComponent
from langchain.schema import Document
from langflow.services.database.models.base import orjson_dumps
class JSONDocumentBuilder(CustomComponent):
display_name: str = "JSON Document Builder"
description: str = "Build a Document containing a JSON object using a key and another Document page content."
output_types: list[str] = ["Document"]
beta = True
documentation: str = (
"https://docs.langflow.org/components/utilities#json-document-builder"
)
field_config = {
"key": {"display_name": "Key"},
"document": {"display_name": "Document"},
}
def build(
self,
key: str,
document: Document,
) -> Document:
documents = None
if isinstance(document, list):
documents = [
Document(
page_content=orjson_dumps({key: doc.page_content}, indent_2=False)
)
for doc in document
]
elif isinstance(document, Document):
documents = Document(
page_content=orjson_dumps({key: document.page_content}, indent_2=False)
)
else:
raise TypeError(
f"Expected Document or list of Documents, got {type(document)}"
)
self.repr_value = documents
return documents

View file

@ -0,0 +1,80 @@
from langflow import CustomComponent
from langchain.schema import Document
from langflow.services.database.models.base import orjson_dumps
import requests
from typing import Optional
class PostRequest(CustomComponent):
display_name: str = "POST Request"
description: str = "Make a POST request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#post-request"
beta = True
field_config = {
"url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": {
"display_name": "Headers",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"document": {"display_name": "Document"},
}
def post_document(
self,
session: requests.Session,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> Document:
try:
response = session.post(url, headers=headers, data=document.page_content)
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response,
},
)
except Exception as exc:
return Document(
page_content=str(exc),
metadata={
"source": url,
"headers": headers,
"status_code": 500,
},
)
def build(
self,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> list[Document]:
if headers is None:
headers = {}
if not isinstance(document, list) and isinstance(document, Document):
documents: list[Document] = [document]
elif isinstance(document, list) and all(
isinstance(doc, Document) for doc in document
):
documents = document
else:
raise ValueError("document must be a Document or a list of Documents")
with requests.Session() as session:
documents = [
self.post_document(session, doc, url, headers) for doc in documents
]
self.repr_value = documents
return documents

View file

@ -0,0 +1,94 @@
from typing import List, Optional
import requests
from langflow import CustomComponent
from langchain.schema import Document
from langflow.services.database.models.base import orjson_dumps
class UpdateRequest(CustomComponent):
display_name: str = "Update Request"
description: str = "Make a PATCH request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#update-request"
beta = True
field_config = {
"url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": {
"display_name": "Headers",
"field_type": "NestedDict",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"document": {"display_name": "Document"},
"method": {
"display_name": "Method",
"field_type": "str",
"info": "The HTTP method to use.",
"options": ["PATCH", "PUT"],
"value": "PATCH",
},
}
def update_document(
self,
session: requests.Session,
document: Document,
url: str,
headers: Optional[dict] = None,
method: str = "PATCH",
) -> Document:
try:
if method == "PATCH":
response = session.patch(
url, headers=headers, data=document.page_content
)
elif method == "PUT":
response = session.put(url, headers=headers, data=document.page_content)
else:
raise ValueError(f"Unsupported method: {method}")
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response.status_code,
},
)
except Exception as exc:
return Document(
page_content=str(exc),
metadata={"source": url, "headers": headers, "status_code": 500},
)
def build(
self,
method: str,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> List[Document]:
if headers is None:
headers = {}
if not isinstance(document, list) and isinstance(document, Document):
documents: list[Document] = [document]
elif isinstance(document, list) and all(
isinstance(doc, Document) for doc in document
):
documents = document
else:
raise ValueError("document must be a Document or a list of Documents")
with requests.Session() as session:
documents = [
self.update_document(session, doc, url, headers, method)
for doc in documents
]
self.repr_value = documents
return documents

View file

@ -0,0 +1,109 @@
from typing import Optional, Union
from langflow import CustomComponent
from langchain.vectorstores import Chroma
from langchain.schema import Document
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseRetriever
from langchain.embeddings.base import Embeddings
import chromadb # type: ignore
class ChromaComponent(CustomComponent):
"""
A custom component for implementing a Vector Store using Chroma.
"""
display_name: str = "Chroma (Custom Component)"
description: str = "Implementation of Vector Store using Chroma"
documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma"
beta = True
def build_config(self):
"""
Builds the configuration for the component.
Returns:
- dict: A dictionary containing the configuration options for the component.
"""
return {
"collection_name": {"display_name": "Collection Name", "value": "langflow"},
"persist": {"display_name": "Persist"},
"persist_directory": {"display_name": "Persist Directory"},
"code": {"show": False, "display_name": "Code"},
"documents": {"display_name": "Documents", "is_list": True},
"embedding": {"display_name": "Embedding"},
"chroma_server_cors_allow_origins": {
"display_name": "Server CORS Allow Origins",
"advanced": True,
},
"chroma_server_host": {"display_name": "Server Host", "advanced": True},
"chroma_server_port": {"display_name": "Server Port", "advanced": True},
"chroma_server_grpc_port": {
"display_name": "Server gRPC Port",
"advanced": True,
},
"chroma_server_ssl_enabled": {
"display_name": "Server SSL Enabled",
"advanced": True,
},
}
def build(
self,
collection_name: str,
persist: bool,
chroma_server_ssl_enabled: bool,
persist_directory: Optional[str] = None,
embedding: Optional[Embeddings] = None,
documents: Optional[Document] = None,
chroma_server_cors_allow_origins: Optional[str] = None,
chroma_server_host: Optional[str] = None,
chroma_server_port: Optional[int] = None,
chroma_server_grpc_port: Optional[int] = None,
) -> Union[VectorStore, BaseRetriever]:
"""
Builds the Vector Store or BaseRetriever object.
Args:
- collection_name (str): The name of the collection.
- persist_directory (Optional[str]): The directory to persist the Vector Store to.
- chroma_server_ssl_enabled (bool): Whether to enable SSL for the Chroma server.
- persist (bool): Whether to persist the Vector Store or not.
- embedding (Optional[Embeddings]): The embeddings to use for the Vector Store.
- documents (Optional[Document]): The documents to use for the Vector Store.
- chroma_server_cors_allow_origins (Optional[str]): The CORS allow origins for the Chroma server.
- chroma_server_host (Optional[str]): The host for the Chroma server.
- chroma_server_port (Optional[int]): The port for the Chroma server.
- chroma_server_grpc_port (Optional[int]): The gRPC port for the Chroma server.
Returns:
- Union[VectorStore, BaseRetriever]: The Vector Store or BaseRetriever object.
"""
# Chroma settings
chroma_settings = None
if chroma_server_host is not None:
chroma_settings = chromadb.config.Settings(
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
or None,
chroma_server_host=chroma_server_host,
chroma_server_port=chroma_server_port or None,
chroma_server_grpc_port=chroma_server_grpc_port or None,
chroma_server_ssl_enabled=chroma_server_ssl_enabled,
)
# If documents, then we need to create a Chroma instance using .from_documents
if documents is not None and embedding is not None:
return Chroma.from_documents(
documents=documents, # type: ignore
persist_directory=persist_directory if persist else None,
collection_name=collection_name,
embedding=embedding,
client_settings=chroma_settings,
)
return Chroma(
persist_directory=persist_directory, client_settings=chroma_settings
)

View file

@ -0,0 +1,50 @@
from typing import Optional, Union
from langflow import CustomComponent
from langchain.vectorstores import Vectara
from langchain.schema import Document
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseRetriever
from langchain.embeddings.base import Embeddings
class VectaraComponent(CustomComponent):
display_name: str = "Vectara"
description: str = "Implementation of Vector Store using Vectara"
documentation = (
"https://python.langchain.com/docs/integrations/vectorstores/vectara"
)
beta = True
# api key should be password = True
field_config = {
"vectara_customer_id": {"display_name": "Vectara Customer ID"},
"vectara_corpus_id": {"display_name": "Vectara Corpus ID"},
"vectara_api_key": {"display_name": "Vectara API Key", "password": True},
"code": {"show": False},
"documents": {"display_name": "Documents"},
"embedding": {"display_name": "Embedding"},
}
def build(
self,
vectara_customer_id: str,
vectara_corpus_id: str,
vectara_api_key: str,
embedding: Optional[Embeddings] = None,
documents: Optional[Document] = None,
) -> Union[VectorStore, BaseRetriever]:
# If documents, then we need to create a Vectara instance using .from_documents
if documents is not None and embedding is not None:
return Vectara.from_documents(
documents=documents, # type: ignore
vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key,
embedding=embedding,
)
return Vectara(
vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key,
)

View file

@ -104,6 +104,8 @@ embeddings:
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/sentence_transformers" documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/sentence_transformers"
CohereEmbeddings: CohereEmbeddings:
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/cohere" documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/cohere"
VertexAIEmbeddings:
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/google_vertex_ai_palm"
llms: llms:
OpenAI: OpenAI:
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai" documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai"
@ -127,8 +129,8 @@ llms:
# There's a bug in this component deactivating until we get it sorted: _language_models.py", line 804, in send_message # There's a bug in this component deactivating until we get it sorted: _language_models.py", line 804, in send_message
# is_blocked=safety_attributes.get("blocked", False), # is_blocked=safety_attributes.get("blocked", False),
# AttributeError: 'list' object has no attribute 'get' # AttributeError: 'list' object has no attribute 'get'
# ChatVertexAI: ChatVertexAI:
# documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/google_vertex_ai_palm" documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/google_vertex_ai_palm"
### ###
memories: memories:
# https://github.com/supabase-community/supabase-py/issues/482 # https://github.com/supabase-community/supabase-py/issues/482
@ -169,8 +171,6 @@ prompts:
textsplitters: textsplitters:
CharacterTextSplitter: CharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter" documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"
RecursiveCharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter"
toolkits: toolkits:
OpenAPIToolkit: OpenAPIToolkit:
documentation: "" documentation: ""

View file

@ -1,78 +0,0 @@
from contextlib import contextmanager
import os
from sqlmodel import SQLModel, Session, create_engine
from langflow.utils.logger import logger
class Engine:
_instance = None
@classmethod
def get(cls):
logger.debug("Getting database engine")
if cls._instance is None:
cls.create()
return cls._instance
@classmethod
def create(cls):
logger.debug("Creating database engine")
from langflow.settings import settings
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
settings.DATABASE_URL = langflow_database_url
logger.debug("Using LANGFLOW_DATABASE_URL")
if settings.DATABASE_URL and settings.DATABASE_URL.startswith("sqlite"):
connect_args = {"check_same_thread": False}
else:
connect_args = {}
if not settings.DATABASE_URL:
raise RuntimeError("No database_url provided")
cls._instance = create_engine(settings.DATABASE_URL, connect_args=connect_args)
@classmethod
def update(cls):
logger.debug("Updating database engine")
cls._instance = None
cls.create()
def create_db_and_tables():
logger.debug("Creating database and tables")
try:
SQLModel.metadata.create_all(Engine.get())
except Exception as exc:
logger.error(f"Error creating database and tables: {exc}")
raise RuntimeError("Error creating database and tables") from exc
# Now check if the table Flow exists, if not, something went wrong
# and we need to create the tables again.
from sqlalchemy import inspect
inspector = inspect(Engine.get())
if "flow" not in inspector.get_table_names():
logger.error("Something went wrong creating the database and tables.")
logger.error("Please check your database settings.")
raise RuntimeError("Something went wrong creating the database and tables.")
else:
logger.debug("Database and tables created successfully")
@contextmanager
def session_getter():
try:
session = Session(Engine.get())
yield session
except Exception as e:
print("Session rollback because of exception:", e)
session.rollback()
raise
finally:
session.close()
def get_session():
with session_getter() as session:
yield session

View file

@ -1,14 +0,0 @@
from sqlmodel import SQLModel
import orjson
def orjson_dumps(v, *, default):
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
return orjson.dumps(v, default=default).decode()
class SQLModelSerializable(SQLModel):
class Config:
orm_mode = True
json_loads = orjson.loads
json_dumps = orjson_dumps

View file

@ -1,33 +0,0 @@
# Path: src/backend/langflow/database/models/flowstyle.py
from langflow.database.models.base import SQLModelSerializable
from sqlmodel import Field, Relationship
from uuid import UUID, uuid4
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from langflow.database.models.flow import Flow
class FlowStyleBase(SQLModelSerializable):
color: str
emoji: str
flow_id: UUID = Field(default=None, foreign_key="flow.id")
class FlowStyle(FlowStyleBase, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
flow: "Flow" = Relationship(back_populates="style")
class FlowStyleUpdate(SQLModelSerializable):
color: Optional[str] = None
emoji: Optional[str] = None
class FlowStyleCreate(FlowStyleBase):
pass
class FlowStyleRead(FlowStyleBase):
id: UUID

View file

@ -0,0 +1,3 @@
from .base import NestedDict
__all__ = ["NestedDict"]

View file

@ -0,0 +1,4 @@
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]

View file

@ -1,4 +1,4 @@
from langflow.utils.logger import logger from loguru import logger
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -40,7 +40,6 @@ class Edge:
if no_matched_type: if no_matched_type:
logger.debug(self.source_types) logger.debug(self.source_types)
logger.debug(self.target_reqs) logger.debug(self.target_reqs)
if no_matched_type:
raise ValueError( raise ValueError(
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} " f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
f"has no matched type" f"has no matched type"

View file

@ -1,7 +1,7 @@
from typing import Dict, Generator, List, Type, Union from typing import Dict, Generator, List, Type, Union
from langflow.graph.edge.base import Edge from langflow.graph.edge.base import Edge
from langflow.graph.graph.constants import VERTEX_TYPE_MAP from langflow.graph.graph.constants import lazy_load_vertex_dict
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.types import ( from langflow.graph.vertex.types import (
FileToolVertex, FileToolVertex,
@ -10,7 +10,7 @@ from langflow.graph.vertex.types import (
) )
from langflow.interface.tools.constants import FILE_TOOLS from langflow.interface.tools.constants import FILE_TOOLS
from langflow.utils import payload from langflow.utils import payload
from langflow.utils.logger import logger from loguru import logger
from langchain.chains.base import Chain from langchain.chains.base import Chain
@ -144,7 +144,7 @@ class Graph:
return list(reversed(sorted_vertices)) return list(reversed(sorted_vertices))
def generator_build(self) -> Generator: def generator_build(self) -> Generator[Vertex, None, None]:
"""Builds each vertex in the graph and yields it.""" """Builds each vertex in the graph and yields it."""
sorted_vertices = self.topological_sort() sorted_vertices = self.topological_sort()
logger.debug("Sorted vertices: %s", sorted_vertices) logger.debug("Sorted vertices: %s", sorted_vertices)
@ -187,10 +187,12 @@ class Graph:
"""Returns the node class based on the node type.""" """Returns the node class based on the node type."""
if node_type in FILE_TOOLS: if node_type in FILE_TOOLS:
return FileToolVertex return FileToolVertex
if node_type in VERTEX_TYPE_MAP: if node_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
return VERTEX_TYPE_MAP[node_type] return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_type]
return ( return (
VERTEX_TYPE_MAP[node_lc_type] if node_lc_type in VERTEX_TYPE_MAP else Vertex lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_lc_type]
if node_lc_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP
else Vertex
) )
def _build_vertices(self) -> List[Vertex]: def _build_vertices(self) -> List[Vertex]:

View file

@ -1,4 +1,3 @@
from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex import types from langflow.graph.vertex import types
from langflow.interface.agents.base import agent_creator from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator from langflow.interface.chains.base import chain_creator
@ -15,23 +14,45 @@ from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.output_parsers.base import output_parser_creator from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.custom.base import custom_component_creator from langflow.interface.custom.base import custom_component_creator
from typing import Dict, Type from langflow.utils.lazy_load import LazyLoadDictBase
VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = { class VertexTypesDict(LazyLoadDictBase):
**{t: types.PromptVertex for t in prompt_creator.to_list()}, def __init__(self):
**{t: types.AgentVertex for t in agent_creator.to_list()}, self._all_types_dict = None
**{t: types.ChainVertex for t in chain_creator.to_list()},
**{t: types.ToolVertex for t in tool_creator.to_list()}, @property
**{t: types.ToolkitVertex for t in toolkits_creator.to_list()}, def VERTEX_TYPE_MAP(self):
**{t: types.WrapperVertex for t in wrapper_creator.to_list()}, return self.all_types_dict
**{t: types.LLMVertex for t in llm_creator.to_list()},
**{t: types.MemoryVertex for t in memory_creator.to_list()}, def _build_dict(self):
**{t: types.EmbeddingVertex for t in embedding_creator.to_list()}, langchain_types_dict = self.get_type_dict()
**{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()}, return {
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, **langchain_types_dict,
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, "Custom": ["Custom Tool", "Python Function"],
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, }
**{t: types.CustomComponentVertex for t in custom_component_creator.to_list()},
**{t: types.RetrieverVertex for t in retriever_creator.to_list()}, def get_type_dict(self):
} return {
**{t: types.PromptVertex for t in prompt_creator.to_list()},
**{t: types.AgentVertex for t in agent_creator.to_list()},
**{t: types.ChainVertex for t in chain_creator.to_list()},
**{t: types.ToolVertex for t in tool_creator.to_list()},
**{t: types.ToolkitVertex for t in toolkits_creator.to_list()},
**{t: types.WrapperVertex for t in wrapper_creator.to_list()},
**{t: types.LLMVertex for t in llm_creator.to_list()},
**{t: types.MemoryVertex for t in memory_creator.to_list()},
**{t: types.EmbeddingVertex for t in embedding_creator.to_list()},
**{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()},
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
**{
t: types.CustomComponentVertex
for t in custom_component_creator.to_list()
},
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
}
lazy_load_vertex_dict = VertexTypesDict()

View file

@ -3,6 +3,10 @@ from typing import Any, Union
from langflow.interface.utils import extract_input_variables_from_prompt from langflow.interface.utils import extract_input_variables_from_prompt
class UnbuiltObject:
pass
def validate_prompt(prompt: str): def validate_prompt(prompt: str):
"""Validate prompt.""" """Validate prompt."""
if extract_input_variables_from_prompt(prompt): if extract_input_variables_from_prompt(prompt):

View file

@ -1,8 +1,9 @@
import ast import ast
from langflow.graph.utils import UnbuiltObject
from langflow.interface.initialize import loading from langflow.interface.initialize import loading
from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface.listing import lazy_load_dict
from langflow.utils.constants import DIRECT_TYPES from langflow.utils.constants import DIRECT_TYPES
from langflow.utils.logger import logger from loguru import logger
from langflow.utils.util import sync_to_async from langflow.utils.util import sync_to_async
@ -22,7 +23,7 @@ class Vertex:
self.edges: List["Edge"] = [] self.edges: List["Edge"] = []
self.base_type: Optional[str] = base_type self.base_type: Optional[str] = base_type
self._parse_data() self._parse_data()
self._built_object = None self._built_object = UnbuiltObject()
self._built = False self._built = False
self.artifacts: Dict[str, Any] = {} self.artifacts: Dict[str, Any] = {}
@ -62,7 +63,7 @@ class Vertex:
) )
if self.base_type is None: if self.base_type is None:
for base_type, value in ALL_TYPES_DICT.items(): for base_type, value in lazy_load_dict.ALL_TYPES_DICT.items():
if self.vertex_type in value: if self.vertex_type in value:
self.base_type = base_type self.base_type = base_type
break break
@ -121,6 +122,19 @@ class Vertex:
except Exception as exc: except Exception as exc:
logger.debug(f"Error parsing code: {exc}") logger.debug(f"Error parsing code: {exc}")
params[key] = value.get("value") params[key] = value.get("value")
elif value.get("type") in ["dict", "NestedDict"]:
# When dict comes from the frontend it comes as a
# list of dicts, so we need to convert it to a dict
# before passing it to the build method
_value = value.get("value")
if isinstance(_value, list):
params[key] = {
k: v
for item in value.get("value", [])
for k, v in item.items()
}
elif isinstance(_value, dict):
params[key] = _value
else: else:
params[key] = value.get("value") params[key] = value.get("value")
@ -132,13 +146,13 @@ class Vertex:
# Add _type to params # Add _type to params
self.params = params self.params = params
def _build(self): def _build(self, user_id=None):
""" """
Initiate the build process. Initiate the build process.
""" """
logger.debug(f"Building {self.vertex_type}") logger.debug(f"Building {self.vertex_type}")
self._build_each_node_in_params_dict() self._build_each_node_in_params_dict()
self._get_and_instantiate_class() self._get_and_instantiate_class(user_id)
self._validate_built_object() self._validate_built_object()
self._built = True self._built = True
@ -168,23 +182,25 @@ class Vertex:
""" """
return all(self._is_node(node) for node in value) return all(self._is_node(node) for node in value)
def _build_node_and_update_params(self, key, node): def _build_node_and_update_params(self, key, node, user_id=None):
""" """
Builds a given node and updates the params dictionary accordingly. Builds a given node and updates the params dictionary accordingly.
""" """
result = node.build() result = node.build(user_id)
self._handle_func(key, result) self._handle_func(key, result)
if isinstance(result, list): if isinstance(result, list):
self._extend_params_list_with_result(key, result) self._extend_params_list_with_result(key, result)
self.params[key] = result self.params[key] = result
def _build_list_of_nodes_and_update_params(self, key, nodes): def _build_list_of_nodes_and_update_params(
self, key, nodes: List["Vertex"], user_id=None
):
""" """
Iterates over a list of nodes, builds each and updates the params dictionary. Iterates over a list of nodes, builds each and updates the params dictionary.
""" """
self.params[key] = [] self.params[key] = []
for node in nodes: for node in nodes:
built = node.build() built = node.build(user_id)
if isinstance(built, list): if isinstance(built, list):
if key not in self.params: if key not in self.params:
self.params[key] = [] self.params[key] = []
@ -214,7 +230,7 @@ class Vertex:
if isinstance(self.params[key], list): if isinstance(self.params[key], list):
self.params[key].extend(result) self.params[key].extend(result)
def _get_and_instantiate_class(self): def _get_and_instantiate_class(self, user_id=None):
""" """
Gets the class from a dictionary and instantiates it with the params. Gets the class from a dictionary and instantiates it with the params.
""" """
@ -225,6 +241,7 @@ class Vertex:
node_type=self.vertex_type, node_type=self.vertex_type,
base_type=self.base_type, base_type=self.base_type,
params=self.params, params=self.params,
user_id=user_id,
) )
self._update_built_object_and_artifacts(result) self._update_built_object_and_artifacts(result)
except Exception as exc: except Exception as exc:
@ -245,12 +262,18 @@ class Vertex:
""" """
Checks if the built object is None and raises a ValueError if so. Checks if the built object is None and raises a ValueError if so.
""" """
if self._built_object is None: if isinstance(self._built_object, UnbuiltObject):
raise ValueError(f"Node type {self.vertex_type} not found") raise ValueError(f"{self.vertex_type}: {self._built_object_repr()}")
elif self._built_object is None:
message = f"{self.vertex_type} returned None."
if self.base_type == "custom_components":
message += " Make sure your build method returns a component."
def build(self, force: bool = False) -> Any: raise ValueError(message)
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
if not self._built or force: if not self._built or force:
self._build() self._build(user_id, *args, **kwargs)
return self._built_object return self._built_object

Some files were not shown because too many files have changed in this diff Show more