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

This commit is contained in:
anovazzi1 2023-08-15 19:34:53 -03:00
commit ec79cc43bd
170 changed files with 3755 additions and 2142 deletions

1
.dockerignore Normal file
View file

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

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, 'Release') }}
${{ github.event.pull_request.merged == true }}
&& ${{ contains(github.event.pull_request.labels.*.name, 'Release') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

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

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

@ -33,7 +33,7 @@
- [HuggingFace Spaces](#huggingface-spaces) - [HuggingFace Spaces](#huggingface-spaces)
- [🖥️ Command Line Interface (CLI)](#️-command-line-interface-cli) - [🖥️ Command Line Interface (CLI)](#️-command-line-interface-cli)
- [Usage](#usage) - [Usage](#usage)
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [Deployment](#deployment) - [Deployment](#deployment)
- [Deploy Langflow on Google Cloud Platform](#deploy-langflow-on-google-cloud-platform) - [Deploy Langflow on Google Cloud Platform](#deploy-langflow-on-google-cloud-platform)
- [Deploy Langflow on Jina AI Cloud](#deploy-langflow-on-jina-ai-cloud) - [Deploy Langflow on Jina AI Cloud](#deploy-langflow-on-jina-ai-cloud)
@ -112,7 +112,6 @@ Each option is detailed below:
- `--cache`: Selects the type of cache to use. Options are `InMemoryCache` and `SQLiteCache`. Can be set using the `LANGFLOW_LANGCHAIN_CACHE` environment variable. The default is `SQLiteCache`. - `--cache`: Selects the type of cache to use. Options are `InMemoryCache` and `SQLiteCache`. Can be set using the `LANGFLOW_LANGCHAIN_CACHE` environment variable. The default is `SQLiteCache`.
- `--jcloud/--no-jcloud`: Toggles the option to deploy on Jina AI Cloud. The default is `no-jcloud`. - `--jcloud/--no-jcloud`: Toggles the option to deploy on Jina AI Cloud. The default is `no-jcloud`.
- `--dev/--no-dev`: Toggles the development mode. The default is `no-dev`. - `--dev/--no-dev`: Toggles the development mode. The default is `no-dev`.
- `--database-url`: Sets the database URL to connect to. If not provided, a local SQLite database will be used. Can be set using the `LANGFLOW_DATABASE_URL` environment variable.
- `--path`: Specifies the path to the frontend directory containing build files. This option is for development purposes only. Can be set using the `LANGFLOW_FRONTEND_PATH` environment variable. - `--path`: Specifies the path to the frontend directory containing build files. This option is for development purposes only. Can be set using the `LANGFLOW_FRONTEND_PATH` environment variable.
- `--open-browser/--no-open-browser`: Toggles the option to open the browser after starting the server. Can be set using the `LANGFLOW_OPEN_BROWSER` environment variable. The default is `open-browser`. - `--open-browser/--no-open-browser`: Toggles the option to open the browser after starting the server. Can be set using the `LANGFLOW_OPEN_BROWSER` environment variable. The default is `open-browser`.
- `--remove-api-keys/--no-remove-api-keys`: Toggles the option to remove API keys from the projects saved in the database. Can be set using the `LANGFLOW_REMOVE_API_KEYS` environment variable. The default is `no-remove-api-keys`. - `--remove-api-keys/--no-remove-api-keys`: Toggles the option to remove API keys from the projects saved in the database. Can be set using the `LANGFLOW_REMOVE_API_KEYS` environment variable. The default is `no-remove-api-keys`.
@ -276,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", "langflow.main: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

@ -1,4 +1,4 @@
version: '3.4' version: "3.4"
services: services:
backend: backend:
@ -7,7 +7,12 @@ services:
build: build:
context: ./ context: ./
dockerfile: ./dev.Dockerfile dockerfile: ./dev.Dockerfile
command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn langflow.main:app --host 0.0.0.0 --port 7860 --reload"] command:
[
"sh",
"-c",
"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
- 5678:5678 - 5678:5678
@ -22,7 +27,7 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
volumes: volumes:
- ./src/frontend/public:/home/node/app/public - ./src/frontend/public:/home/node/app/public
- ./src/frontend/src:/home/node/app/src - ./src/frontend/src:/home/node/app/src
- ./src/frontend/package.json:/home/node/app/package.json - ./src/frontend/package.json:/home/node/app/package.json
restart: on-failure restart: on-failure

View file

@ -1,4 +1,4 @@
version: '3' version: "3"
services: services:
backend: backend:
@ -9,7 +9,7 @@ services:
- "7860:7860" - "7860:7860"
volumes: volumes:
- ./:/app - ./:/app
command: bash -c "uvicorn langflow.main: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:
@ -22,7 +22,7 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
volumes: volumes:
- ./src/frontend/public:/home/node/app/public - ./src/frontend/public:/home/node/app/public
- ./src/frontend/src:/home/node/app/src - ./src/frontend/src:/home/node/app/src
- ./src/frontend/package.json:/home/node/app/package.json - ./src/frontend/package.json:/home/node/app/package.json
restart: on-failure restart: on-failure

View file

@ -6,5 +6,5 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "5003:5003" - "7860:7860"
command: langflow --host 0.0.0.0 command: langflow --host 0.0.0.0

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

@ -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",
@ -385,19 +384,19 @@ Your structure should look something like this:
### Loading Custom Components ### Loading Custom Components
You can specify the path to your custom components using the _`--components-path`_ argument when running the Langflow CLI, as shown below: 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
langflow --components-path /path/to/components
```
Alternatively, you can set the `LANGFLOW_COMPONENTS_PATH` environment variable:
```bash ```bash
export LANGFLOW_COMPONENTS_PATH=/path/to/components export LANGFLOW_COMPONENTS_PATH=/path/to/components
langflow langflow
``` ```
Alternatively, you can specify the path to your custom components using the _`--components-path`_ argument when running the Langflow CLI, as shown below:
```bash
langflow --components-path /path/to/components
```
Langflow will attempt to load all of the components found in the specified directory. If a component fails to load due to errors in the component's code, Langflow will print an error message to the console but will continue loading the rest of the components. Langflow will attempt to load all of the components found in the specified directory. If a component fails to load due to errors in the component's code, Langflow will print an error message to the console but will continue loading the rest of the components.
### Interacting with Custom Components ### Interacting with Custom Components

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

2
docs/static/CNAME vendored
View file

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

1160
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.0" version = "0.4.7"
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"
@ -33,7 +33,7 @@ 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.1.0"
langchain = "^0.0.249" langchain = "^0.0.256"
openai = "^0.27.8" openai = "^0.27.8"
pandas = "^2.0.0" pandas = "^2.0.0"
chromadb = "^0.3.21" chromadb = "^0.3.21"
@ -45,7 +45,7 @@ unstructured = "^0.7.0"
pypdf = "^3.11.0" pypdf = "^3.11.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"
@ -63,7 +63,7 @@ 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"
@ -76,6 +76,9 @@ google-cloud-aiplatform = "^1.26.1"
psycopg = "^3.1.9" 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"
alembic = "^1.11.2"
metaphor-python = "^0.1.11"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^23.1.0" black = "^23.1.0"

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,7 +1,7 @@
import os
import sys import sys
import time import time
import httpx import httpx
from langflow.services.utils import get_settings_manager
from langflow.utils.util import get_number_of_workers from langflow.utils.util import get_number_of_workers
from multiprocess import Process # type: ignore from multiprocess import Process # type: ignore
import platform import platform
@ -13,7 +13,6 @@ from rich import box
from rich import print as rprint from rich import print as rprint
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
@ -25,49 +24,25 @@ def update_settings(
config: str, config: str,
cache: str, cache: str,
dev: bool = False, dev: bool = False,
database_url: Optional[str] = None,
remove_api_keys: bool = False, remove_api_keys: bool = False,
components_path: Optional[Path] = None, components_path: Optional[Path] = None,
): ):
"""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
database_url = database_url or os.getenv("langflow_database_url") 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 database_url:
settings.update_settings(database_url=database_url)
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 load_params():
"""
Load the parameters from the environment variables.
"""
global_vars = globals()
for key, value in global_vars.items():
env_key = f"LANGFLOW_{key.upper()}"
if env_key in os.environ:
if isinstance(value, bool):
# Handle booleans
global_vars[key] = os.getenv(env_key, str(value)).lower() == "true"
elif isinstance(value, int):
# Handle integers
global_vars[key] = int(os.getenv(env_key, str(value)))
elif isinstance(value, str) or value is None:
# Handle strings and None values
global_vars[key] = os.getenv(env_key, str(value))
def serve_on_jcloud(): def serve_on_jcloud():
@ -131,10 +106,12 @@ 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(
".env", help="Path to the .env file containing environment variables." None, help="Path to the .env file containing environment variables."
), ),
log_level: str = typer.Option( log_level: str = typer.Option(
"critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL" "critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"
@ -149,11 +126,13 @@ def serve(
), ),
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)"),
database_url: str = typer.Option( # This variable does not work but is set by the .env file
None, # and works with Pydantic
help="Database URL to connect to. If not provided, a local SQLite database will be used.", # database_url: str = typer.Option(
envvar="LANGFLOW_DATABASE_URL", # None,
), # help="Database URL to connect to. If not provided, a local SQLite database will be used.",
# envvar="LANGFLOW_DATABASE_URL",
# ),
path: str = typer.Option( path: str = typer.Option(
None, None,
help="Path to the frontend directory containing build files. This is for development purposes only.", help="Path to the frontend directory containing build files. This is for development purposes only.",
@ -169,6 +148,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.
@ -176,7 +160,6 @@ def serve(
# override env variables with .env file # override env variables with .env file
if env_file: if env_file:
load_dotenv(env_file, override=True) load_dotenv(env_file, override=True)
load_params()
if jcloud: if jcloud:
return serve_on_jcloud() return serve_on_jcloud()
@ -185,14 +168,13 @@ def serve(
update_settings( update_settings(
config, config,
dev=dev, dev=dev,
database_url=database_url,
remove_api_keys=remove_api_keys, remove_api_keys=remove_api_keys,
cache=cache, cache=cache,
components_path=components_path, components_path=components_path,
) )
# 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)
@ -204,6 +186,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

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,78 @@
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"},
)
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)
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,42 @@
"""Remove FlowStyles table
Revision ID: 0a534bdfd84b
Revises: 4814b6f4abfd
Create Date: 2023-08-07 14:09:06.844104
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0a534bdfd84b"
down_revision: Union[str, None] = "4814b6f4abfd"
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! ###
op.drop_table("flowstyle")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"flowstyle",
sa.Column("color", sa.VARCHAR(), nullable=False),
sa.Column("emoji", sa.VARCHAR(), nullable=False),
sa.Column("flow_id", sa.CHAR(length=32), nullable=True),
sa.Column("id", sa.CHAR(length=32), nullable=False),
sa.ForeignKeyConstraint(
["flow_id"],
["flow.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
# ### end Alembic commands ###

View file

@ -0,0 +1,65 @@
"""Add Flow table
Revision ID: 4814b6f4abfd
Revises:
Create Date: 2023-08-05 17:47:42.879824
"""
import contextlib
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = "4814b6f4abfd"
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! ###
# This suppress is used to not break the migration if the table already exists.
with contextlib.suppress(sa.exc.OperationalError):
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.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_flow_description"), "flow", ["description"], unique=False
)
op.create_index(op.f("ix_flow_name"), "flow", ["name"], unique=False)
with contextlib.suppress(sa.exc.OperationalError):
op.create_table(
"flowstyle",
sa.Column("color", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("emoji", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.ForeignKeyConstraint(
["flow_id"],
["flow.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("flowstyle")
op.drop_index(op.f("ix_flow_name"), table_name="flow")
op.drop_index(op.f("ix_flow_description"), table_name="flow")
op.drop_table("flow")
# ### end Alembic commands ###

View file

@ -5,7 +5,6 @@ from langflow.api.v1 import (
endpoints_router, endpoints_router,
validate_router, validate_router,
flows_router, flows_router,
flow_styles_router,
component_router, component_router,
) )
@ -17,4 +16,3 @@ 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)

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,7 +2,6 @@ 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
__all__ = [ __all__ = [
@ -11,5 +10,4 @@ __all__ = [
"component_router", "component_router",
"validate_router", "validate_router",
"flows_router", "flows_router",
"flow_styles_router",
] ]

View file

@ -3,13 +3,13 @@ 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.services import service_manager, ServiceType
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger
from cachetools import LRUCache from cachetools import LRUCache
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)
@ -17,6 +17,7 @@ flow_data_store: LRUCache = LRUCache(maxsize=10)
async def chat(client_id: str, websocket: WebSocket): async def chat(client_id: str, websocket: WebSocket):
"""Websocket endpoint for chat.""" """Websocket endpoint for chat."""
try: try:
chat_manager = service_manager.get(ServiceType.CHAT_MANAGER)
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:
@ -45,6 +46,7 @@ async def init_build(graph_data: dict, flow_id: str):
return InitResponse(flowId=flow_id) return InitResponse(flowId=flow_id)
# Delete from cache if already exists # Delete from cache if already exists
chat_manager = service_manager.get(ServiceType.CHAT_MANAGER)
if flow_id in chat_manager.in_memory_cache: if flow_id in chat_manager.in_memory_cache:
with chat_manager.in_memory_cache._lock: with chat_manager.in_memory_cache._lock:
chat_manager.in_memory_cache.delete(flow_id) chat_manager.in_memory_cache.delete(flow_id)
@ -125,9 +127,8 @@ async def stream_build(flow_id: str):
vertex.build() vertex.build()
params = vertex._built_object_repr() params = vertex._built_object_repr()
valid = True valid = True
logger.debug( logger.debug(f"Building node {str(vertex.vertex_type)}")
f"Building node {str(params)[:50]}{'...' if len(str(params)) > 50 else ''}" logger.debug(f"Output: {params}")
)
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
@ -156,12 +157,12 @@ 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 = service_manager.get(ServiceType.CHAT_MANAGER)
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,19 +1,15 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Annotated, Optional from typing import Annotated, Optional
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.services.utils import get_settings_manager
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.settings import settings
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body
from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.custom_component import CustomComponent
from langflow.interface.custom.directory_reader import (
CustomComponentPathValueError,
)
from langflow.api.v1.schemas import ( from langflow.api.v1.schemas import (
ProcessResponse, ProcessResponse,
@ -21,7 +17,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,
@ -29,7 +25,7 @@ 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
@ -38,48 +34,38 @@ router = APIRouter(tags=["Base"])
@router.get("/all") @router.get("/all")
def get_all(): def get_all():
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 = {}
if settings.components_path: settings_manager = get_settings_manager()
if settings_manager.settings.COMPONENTS_PATH:
logger.info(
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
)
custom_component_dicts = [ custom_component_dicts = [
build_langchain_custom_component_list_from_path(str(path)) build_langchain_custom_component_list_from_path(str(path))
for path in settings.components_path for path in settings_manager.settings.COMPONENTS_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}"
)
logger.debug(custom_component_dict)
custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict custom_components_from_file, custom_component_dict
) )
return merge_nested_dicts(native_components, custom_components_from_file)
return merge_nested_dicts_with_renaming(
@router.get("/load_custom_component_from_path") native_components, custom_components_from_file
def get_load_custom_component_from_path(path: str):
try:
data = build_langchain_custom_component_list_from_path(path)
except CustomComponentPathValueError as err:
raise HTTPException(
status_code=400,
detail={"error": type(err).__name__, "traceback": str(err)},
) from err
return data
@router.get("/load_custom_component_from_path_TEST")
def get_load_custom_component_from_path_test(path: str):
from langflow.interface.custom.directory_reader import (
DirectoryReader,
) )
reader = DirectoryReader(path, False)
file_list = reader.get_files()
data = reader.build_component_menu_list(file_list)
return reader.filter_loaded_components(data, True)
# 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("/predict/{flow_id}", response_model=ProcessResponse)

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,16 +1,15 @@
from typing import List from typing import List
from uuid import UUID from uuid import UUID
from langflow.settings import settings
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.database.models.flow import (
Flow, Flow,
FlowCreate, FlowCreate,
FlowRead, FlowRead,
FlowReadWithStyle,
FlowUpdate, FlowUpdate,
) )
from langflow.database.base import get_session from langflow.services.utils import get_session
from langflow.services.utils import get_settings_manager
from sqlmodel import Session, select from sqlmodel import Session, select
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
@ -32,7 +31,7 @@ def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
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)):
"""Read all flows.""" """Read all flows."""
try: try:
@ -42,7 +41,7 @@ def read_flows(*, session: Session = Depends(get_session)):
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):
"""Read a flow.""" """Read a flow."""
if flow := session.get(Flow, flow_id): if flow := session.get(Flow, flow_id):
@ -61,7 +60,8 @@ def update_flow(
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: settings_manager = get_settings_manager()
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) setattr(db_flow, key, value)

View file

@ -1,7 +1,7 @@
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 langflow.services.database.models.flow import FlowCreate, FlowRead
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
import json import json

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,4 @@
from langflow.interface.custom.custom_component import CustomComponent
__all__ = ["CustomComponent"]

View file

@ -0,0 +1,33 @@
from langflow import CustomComponent
from langchain.llms.base import BaseLLM
from langchain import PromptTemplate
from langchain.schema import Document
class PromptRunner(CustomComponent):
display_name: str = "Prompt Runner"
description: str = "Run a Chain with the given PromptTemplate"
beta = True
field_config = {
"llm": {"display_name": "LLM"},
"prompt": {
"display_name": "Prompt Template",
"info": "Make sure the prompt has all variables filled.",
},
"code": {"show": False},
"inputs": {"field_type": "code"},
}
def build(
self,
llm: BaseLLM,
prompt: PromptTemplate,
) -> Document:
chain = prompt | llm
# The input is an empty dict because the prompt is already filled
result = chain.invoke({})
if hasattr(result, "content"):
result = result.content
self.repr_value = result
return Document(page_content=str(result))

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

View file

@ -1,51 +0,0 @@
from contextlib import contextmanager
from langflow.settings import settings
from sqlmodel import SQLModel, Session, create_engine
from langflow.utils.logger import logger
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")
engine = create_engine(settings.database_url, connect_args=connect_args)
def create_db_and_tables():
logger.debug("Creating database and tables")
try:
SQLModel.metadata.create_all(engine)
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)
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)
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,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

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

@ -1,5 +1,6 @@
import ast
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 langflow.utils.logger import logger
from langflow.utils.util import sync_to_async from langflow.utils.util import sync_to_async
@ -61,7 +62,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
@ -100,7 +101,9 @@ class Vertex:
params[param_key] = edge.source params[param_key] = edge.source
for key, value in template_dict.items(): for key, value in template_dict.items():
if key == "_type" or not value.get("show"): # Skip _type and any value that has show == False and is not code
# If we don't want to show code but we want to use it
if key == "_type" or (not value.get("show") and key != "code"):
continue continue
# If the type is not transformable to a python base class # If the type is not transformable to a python base class
# then we need to get the edge that connects to this node # then we need to get the edge that connects to this node
@ -112,7 +115,14 @@ class Vertex:
params[key] = file_path params[key] = file_path
elif value.get("type") in DIRECT_TYPES and params.get(key) is None: elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
params[key] = value.get("value") if value.get("type") == "code":
try:
params[key] = ast.literal_eval(value.get("value"))
except Exception as exc:
logger.debug(f"Error parsing code: {exc}")
params[key] = value.get("value")
else:
params[key] = value.get("value")
if not value.get("required") and params.get(key) is None: if not value.get("required") and params.get(key) is None:
if value.get("default"): if value.get("default"):
@ -259,4 +269,8 @@ class Vertex:
def _built_object_repr(self): def _built_object_repr(self):
# Add a message with an emoji, stars for sucess, # Add a message with an emoji, stars for sucess,
return "Built sucessfully ✨" if self._built_object else "Failed to build 😵‍💫" return (
"Built sucessfully ✨"
if self._built_object is not None
else "Failed to build 😵‍💫"
)

View file

@ -226,7 +226,11 @@ class PromptVertex(Vertex):
# so the prompt format doesn't break # so the prompt format doesn't break
artifacts.pop("handle_keys", None) artifacts.pop("handle_keys", None)
try: try:
template = self._built_object.format(**artifacts) template = self._built_object.template
for key, value in artifacts.items():
if value:
replace_key = "{" + key + "}"
template = template.replace(replace_key, value)
return ( return (
template template
if isinstance(template, str) if isinstance(template, str)

View file

@ -5,7 +5,8 @@ from langchain.agents import types
from langflow.custom.customs import get_custom_nodes from langflow.custom.customs import get_custom_nodes
from langflow.interface.agents.custom import CUSTOM_AGENTS from langflow.interface.agents.custom import CUSTOM_AGENTS
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.agents import AgentFrontendNode from langflow.template.frontend_node.agents import AgentFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class, build_template_from_method from langflow.utils.util import build_template_from_class, build_template_from_method
@ -53,13 +54,17 @@ class AgentCreator(LangChainTypeCreator):
# Now this is a generator # Now this is a generator
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
names = [] names = []
settings_manager = get_settings_manager()
for _, agent in self.type_to_loader_dict.items(): for _, agent in self.type_to_loader_dict.items():
agent_name = ( agent_name = (
agent.function_name() agent.function_name()
if hasattr(agent, "function_name") if hasattr(agent, "function_name")
else agent.__name__ else agent.__name__
) )
if agent_name in settings.agents or settings.dev: if (
agent_name in settings_manager.settings.AGENTS
or settings_manager.settings.DEV
):
names.append(agent_name) names.append(agent_name)
return names return names

View file

@ -2,13 +2,14 @@ from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Type, Union from typing import Any, Dict, List, Optional, Type, Union
from langchain.chains.base import Chain from langchain.chains.base import Chain
from langchain.agents import AgentExecutor from langchain.agents import AgentExecutor
from langflow.services.utils import get_settings_manager
from pydantic import BaseModel from pydantic import BaseModel
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template from langflow.template.template.base import Template
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.settings import settings
# Assuming necessary imports for Field, Template, and FrontendNode classes # Assuming necessary imports for Field, Template, and FrontendNode classes
@ -26,9 +27,12 @@ class LangChainTypeCreator(BaseModel, ABC):
@property @property
def docs_map(self) -> Dict[str, str]: def docs_map(self) -> Dict[str, str]:
"""A dict with the name of the component as key and the documentation link as value.""" """A dict with the name of the component as key and the documentation link as value."""
settings_manager = get_settings_manager()
if self.name_docs_dict is None: if self.name_docs_dict is None:
try: try:
type_settings = getattr(settings, self.type_name) type_settings = getattr(
settings_manager.settings, self.type_name.upper()
)
self.name_docs_dict = { self.name_docs_dict = {
name: value_dict["documentation"] name: value_dict["documentation"]
for name, value_dict in type_settings.items() for name, value_dict in type_settings.items()

View file

@ -3,11 +3,13 @@ from typing import Any, Dict, List, Optional, Type
from langflow.custom.customs import get_custom_nodes from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.chains import ChainFrontendNode from langflow.template.frontend_node.chains import ChainFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class, build_template_from_method from langflow.utils.util import build_template_from_class, build_template_from_method
from langchain import chains from langchain import chains
from langchain_experimental.sql import SQLDatabaseChain # type: ignore
# Assuming necessary imports for Field, Template, and FrontendNode classes # Assuming necessary imports for Field, Template, and FrontendNode classes
@ -29,18 +31,22 @@ class ChainCreator(LangChainTypeCreator):
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
if self.type_dict is None: if self.type_dict is None:
settings_manager = get_settings_manager()
self.type_dict: dict[str, Any] = { self.type_dict: dict[str, Any] = {
chain_name: import_class(f"langchain.chains.{chain_name}") chain_name: import_class(f"langchain.chains.{chain_name}")
for chain_name in chains.__all__ for chain_name in chains.__all__
} }
from langflow.interface.chains.custom import CUSTOM_CHAINS from langflow.interface.chains.custom import CUSTOM_CHAINS
self.type_dict["SQLDatabaseChain"] = SQLDatabaseChain
self.type_dict.update(CUSTOM_CHAINS) self.type_dict.update(CUSTOM_CHAINS)
# Filter according to settings.chains # Filter according to settings.chains
self.type_dict = { self.type_dict = {
name: chain name: chain
for name, chain in self.type_dict.items() for name, chain in self.type_dict.items()
if name in settings.chains or settings.dev if name in settings_manager.settings.CHAINS
or settings_manager.settings.DEV
} }
return self.type_dict return self.type_dict

View file

@ -1,5 +1,5 @@
import ast import ast
from typing import Optional from typing import Any, Optional
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import HTTPException from fastapi import HTTPException
@ -63,10 +63,13 @@ class Component(BaseModel):
elif "description" in item_name: elif "description" in item_name:
template_config["description"] = ast.literal_eval(item_value) template_config["description"] = ast.literal_eval(item_value)
elif "field_config" in item_name: elif "beta" in item_name:
template_config["field_config"] = ast.literal_eval(item_value) template_config["beta"] = ast.literal_eval(item_value)
elif "documentation" in item_name:
template_config["documentation"] = ast.literal_eval(item_value)
return template_config return template_config
def build(self): def build(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError raise NotImplementedError

View file

@ -7,6 +7,7 @@ from langchain.schema import BaseRetriever, Document
from langchain.text_splitter import TextSplitter from langchain.text_splitter import TextSplitter
from langchain.tools import Tool from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseOutputParser
LANGCHAIN_BASE_TYPES = { LANGCHAIN_BASE_TYPES = {
@ -20,6 +21,7 @@ LANGCHAIN_BASE_TYPES = {
"VectorStore": VectorStore, "VectorStore": VectorStore,
"Embeddings": Embeddings, "Embeddings": Embeddings,
"BaseRetriever": BaseRetriever, "BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
} }
# Langchain base types plus Python base types # Langchain base types plus Python base types

View file

@ -3,12 +3,14 @@ from fastapi import HTTPException
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component from langflow.interface.custom.component import Component
from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.services.utils import get_db_manager
from langflow.utils import validate from langflow.utils import validate
from langflow.database.base import session_getter from langflow.services.database.utils import session_getter
from langflow.database.models.flow import Flow from langflow.services.database.models.flow import Flow
from pydantic import Extra from pydantic import Extra
import yaml
class CustomComponent(Component, extra=Extra.allow): class CustomComponent(Component, extra=Extra.allow):
@ -24,6 +26,10 @@ class CustomComponent(Component, extra=Extra.allow):
super().__init__(**data) super().__init__(**data)
def custom_repr(self): def custom_repr(self):
if isinstance(self.repr_value, dict):
return yaml.dump(self.repr_value)
if isinstance(self.repr_value, str):
return self.repr_value
return str(self.repr_value) return str(self.repr_value)
def build_config(self): def build_config(self):
@ -44,7 +50,9 @@ class CustomComponent(Component, extra=Extra.allow):
reader = DirectoryReader("", False) reader = DirectoryReader("", False)
for type_hint in TYPE_HINT_LIST: for type_hint in TYPE_HINT_LIST:
if reader.is_type_hint_used_but_not_imported(type_hint, code): if reader._is_type_hint_used_in_args(
"Optional", code
) and not reader._is_type_hint_imported("Optional", code):
error_detail = { error_detail = {
"error": "Type hint Error", "error": "Type hint Error",
"traceback": f"Type hint '{type_hint}' is used but not imported in the code.", "traceback": f"Type hint '{type_hint}' is used but not imported in the code.",
@ -87,9 +95,9 @@ class CustomComponent(Component, extra=Extra.allow):
return build_method["args"] return build_method["args"]
@property @property
def get_function_entrypoint_return_type(self) -> str: def get_function_entrypoint_return_type(self) -> List[str]:
if not self.code: if not self.code:
return "" return []
tree = self.get_code_tree(self.code) tree = self.get_code_tree(self.code)
component_classes = [ component_classes = [
@ -98,7 +106,7 @@ class CustomComponent(Component, extra=Extra.allow):
if self.code_class_base_inheritance in cls["bases"] if self.code_class_base_inheritance in cls["bases"]
] ]
if not component_classes: if not component_classes:
return "" return []
# Assume the first Component class is the one we're interested in # Assume the first Component class is the one we're interested in
component_class = component_classes[0] component_class = component_classes[0]
@ -109,11 +117,21 @@ class CustomComponent(Component, extra=Extra.allow):
] ]
if not build_methods: if not build_methods:
return "" return []
build_method = build_methods[0] build_method = build_methods[0]
return_type = build_method["return_type"]
if not return_type:
return []
# If the return type is not a Union, then we just return it as a list
if "Union" not in return_type:
return [return_type] if return_type in self.return_type_valid_list else []
return build_method["return_type"] # If the return type is a Union, then we need to parse it
return_type = return_type.replace("Union", "").replace("[", "").replace("]", "")
return_type = return_type.split(",")
return_type = [item.strip() for item in return_type]
return [item for item in return_type if item in self.return_type_valid_list]
@property @property
def get_main_class_name(self): def get_main_class_name(self):
@ -154,7 +172,8 @@ class CustomComponent(Component, extra=Extra.allow):
from langflow.processing.process import build_sorted_vertices_with_caching from langflow.processing.process import build_sorted_vertices_with_caching
from langflow.processing.process import process_tweaks from langflow.processing.process import process_tweaks
with session_getter() as session: db_manager = get_db_manager()
with session_getter(db_manager) as session:
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
if not graph_data: if not graph_data:
raise ValueError(f"Flow {flow_id} not found") raise ValueError(f"Flow {flow_id} not found")
@ -164,7 +183,8 @@ class CustomComponent(Component, extra=Extra.allow):
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]: def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
get_session = get_session or session_getter get_session = get_session or session_getter
with get_session() as session: db_manager = get_db_manager()
with get_session(db_manager) as session:
flows = session.query(Flow).all() flows = session.query(Flow).all()
return flows return flows
@ -177,8 +197,8 @@ class CustomComponent(Component, extra=Extra.allow):
get_session: Optional[Callable] = None, get_session: Optional[Callable] = None,
) -> Flow: ) -> Flow:
get_session = get_session or session_getter get_session = get_session or session_getter
db_manager = get_db_manager()
with get_session() as session: with get_session(db_manager) as session:
if flow_id: if flow_id:
flow = session.query(Flow).get(flow_id) flow = session.query(Flow).get(flow_id)
elif flow_name: elif flow_name:
@ -190,5 +210,5 @@ class CustomComponent(Component, extra=Extra.allow):
raise ValueError(f"Flow {flow_name or flow_id} not found") raise ValueError(f"Flow {flow_name or flow_id} not found")
return self.load_flow(flow.id, tweaks) return self.load_flow(flow.id, tweaks)
def build(self): def build(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError raise NotImplementedError

View file

@ -1,6 +1,7 @@
import os import os
import ast import ast
import zlib import zlib
from langflow.utils.logger import logger
class CustomComponentPathValueError(ValueError): class CustomComponentPathValueError(ValueError):
@ -74,8 +75,11 @@ class DirectoryReader:
} }
for menu in data["menu"] for menu in data["menu"]
] ]
filtred = [menu for menu in items if menu["components"]] filtered = [menu for menu in items if menu["components"]]
return {"menu": filtred} logger.debug(
f'Filtered components {"with errors" if with_errors else ""}: {filtered}'
)
return {"menu": filtered}
def validate_code(self, file_content): def validate_code(self, file_content):
""" """
@ -116,7 +120,7 @@ class DirectoryReader:
file_list.extend( file_list.extend(
os.path.join(root, filename) os.path.join(root, filename)
for filename in files for filename in files
if filename.endswith(".py") if filename.endswith(".py") and not filename.startswith("__")
) )
return file_list return file_list
@ -148,15 +152,19 @@ class DirectoryReader:
Check if a specific type hint is used in the Check if a specific type hint is used in the
function definitions within the given code. function definitions within the given code.
""" """
module = ast.parse(code) try:
module = ast.parse(code)
for node in ast.walk(module): for node in ast.walk(module):
if isinstance(node, ast.FunctionDef): if isinstance(node, ast.FunctionDef):
for arg in node.args.args: for arg in node.args.args:
if self._is_type_hint_in_arg_annotation( if self._is_type_hint_in_arg_annotation(
arg.annotation, type_hint_name arg.annotation, type_hint_name
): ):
return True return True
except SyntaxError:
# Returns False if the code is not valid Python
return False
return False return False
def _is_type_hint_in_arg_annotation(self, annotation, type_hint_name: str) -> bool: def _is_type_hint_in_arg_annotation(self, annotation, type_hint_name: str) -> bool:
@ -200,8 +208,13 @@ class DirectoryReader:
return False, "Syntax error" return False, "Syntax error"
elif not self.validate_build(file_content): elif not self.validate_build(file_content):
return False, "Missing build function" return False, "Missing build function"
elif self.is_type_hint_used_but_not_imported("Optional", file_content): elif self._is_type_hint_used_in_args(
return False, "Type hint 'Optional' is used but not imported in the code." "Optional", file_content
) and not self._is_type_hint_imported("Optional", file_content):
return (
False,
"Type hint 'Optional' is used but not imported in the code.",
)
else: else:
if self.compress_code_field: if self.compress_code_field:
file_content = str(StringCompressor(file_content).compress_string()) file_content = str(StringCompressor(file_content).compress_string())
@ -213,27 +226,47 @@ class DirectoryReader:
from the .py files in the directory. from the .py files in the directory.
""" """
response = {"menu": []} response = {"menu": []}
logger.debug(
"-------------------- Building component menu list --------------------"
)
for file_path in file_paths: for file_path in file_paths:
menu_name = os.path.basename(os.path.dirname(file_path)) menu_name = os.path.basename(os.path.dirname(file_path))
logger.debug(f"Menu name: {menu_name}")
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
validation_result, result_content = self.process_file(file_path) validation_result, result_content = self.process_file(file_path)
logger.debug(f"Validation result: {validation_result}")
menu_result = self.find_menu(response, menu_name) or { menu_result = self.find_menu(response, menu_name) or {
"name": menu_name, "name": menu_name,
"path": os.path.dirname(file_path), "path": os.path.dirname(file_path),
"components": [], "components": [],
} }
component_name = filename.split(".")[0]
# This is the name of the file which will be displayed in the UI
# We need to change it from snake_case to CamelCase
# first check if it's already CamelCase
if "_" in component_name:
component_name_camelcase = " ".join(
word.title() for word in component_name.split("_")
)
else:
component_name_camelcase = component_name
component_info = { component_info = {
"name": filename.split(".")[0], "name": "CustomComponent",
"output_types": [component_name_camelcase],
"file": filename, "file": filename,
"code": result_content if validation_result else "", "code": result_content if validation_result else "",
"error": "" if validation_result else result_content, "error": "" if validation_result else result_content,
} }
menu_result["components"].append(component_info) menu_result["components"].append(component_info)
logger.debug(f"Component info: {component_info}")
if menu_result not in response["menu"]: if menu_result not in response["menu"]:
response["menu"].append(menu_result) response["menu"].append(menu_result)
logger.debug(
"-------------------- Component menu list built --------------------"
)
return response return response

View file

@ -1,9 +1,10 @@
from typing import Dict, List, Optional, Type from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode
from langflow.interface.custom_lists import documentloaders_type_to_cls_dict from langflow.interface.custom_lists import documentloaders_type_to_cls_dict
from langflow.settings import settings
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -30,10 +31,12 @@ class DocumentLoaderCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
documentloader.__name__ documentloader.__name__
for documentloader in self.type_to_loader_dict.values() for documentloader in self.type_to_loader_dict.values()
if documentloader.__name__ in settings.documentloaders or settings.dev if documentloader.__name__ in settings_manager.settings.DOCUMENTLOADERS
or settings_manager.settings.DEV
] ]

View file

@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import embedding_type_to_cls_dict from langflow.interface.custom_lists import embedding_type_to_cls_dict
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
@ -32,10 +33,12 @@ class EmbeddingCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
embedding.__name__ embedding.__name__
for embedding in self.type_to_loader_dict.values() for embedding in self.type_to_loader_dict.values()
if embedding.__name__ in settings.embeddings or settings.dev if embedding.__name__ in settings_manager.settings.EMBEDDINGS
or settings_manager.settings.DEV
] ]

View file

@ -61,9 +61,7 @@ def import_by_type(_type: str, name: str) -> Any:
def import_custom_component(custom_component: str) -> CustomComponent: def import_custom_component(custom_component: str) -> CustomComponent:
"""Import custom component from custom component name""" """Import custom component from custom component name"""
return import_class( return import_class("langflow.interface.custom.custom_component.CustomComponent")
f"langflow.interface.custom.custom_component.{custom_component}"
)
def import_output_parser(output_parser: str) -> Any: def import_output_parser(output_parser: str) -> Any:

View file

@ -6,7 +6,11 @@ from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.base import BaseToolkit from langchain.agents.agent_toolkits.base import BaseToolkit
from langchain.agents.tools import BaseTool from langchain.agents.tools import BaseTool
from langflow.interface.initialize.llm import initialize_vertexai from langflow.interface.initialize.llm import initialize_vertexai
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type from langflow.interface.initialize.utils import (
handle_format_kwargs,
handle_node_type,
handle_partial_variables,
)
from langflow.interface.initialize.vector_store import vecstore_initializer from langflow.interface.initialize.vector_store import vecstore_initializer
@ -29,6 +33,7 @@ from langflow.utils import validate
from langchain.chains.base import Chain from langchain.chains.base import Chain
from langchain.vectorstores.base import VectorStore from langchain.vectorstores.base import VectorStore
from langchain.document_loaders.base import BaseLoader from langchain.document_loaders.base import BaseLoader
from langflow.utils.logger import logger
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
@ -40,7 +45,7 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
if hasattr(custom_node, "initialize"): if hasattr(custom_node, "initialize"):
return custom_node.initialize(**params) return custom_node.initialize(**params)
return custom_node(**params) return custom_node(**params)
logger.debug(f"Instantiating {node_type} of type {base_type}")
class_object = import_by_type(_type=base_type, name=node_type) class_object = import_by_type(_type=base_type, name=node_type)
return instantiate_based_on_type(class_object, base_type, node_type, params) return instantiate_based_on_type(class_object, base_type, node_type, params)
@ -60,7 +65,12 @@ def convert_kwargs(params):
kwargs_keys = [key for key in params.keys() if "kwargs" in key or "config" in key] kwargs_keys = [key for key in params.keys() if "kwargs" in key or "config" in key]
for key in kwargs_keys: for key in kwargs_keys:
if isinstance(params[key], str): if isinstance(params[key], str):
params[key] = json.loads(params[key]) try:
params[key] = json.loads(params[key])
except json.JSONDecodeError:
# if the string is not a valid json string, we will
# remove the key from the params
params.pop(key, None)
return params return params
@ -78,7 +88,7 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
elif base_type == "toolkits": elif base_type == "toolkits":
return instantiate_toolkit(node_type, class_object, params) return instantiate_toolkit(node_type, class_object, params)
elif base_type == "embeddings": elif base_type == "embeddings":
return instantiate_embedding(class_object, params) return instantiate_embedding(node_type, class_object, params)
elif base_type == "vectorstores": elif base_type == "vectorstores":
return instantiate_vectorstore(class_object, params) return instantiate_vectorstore(class_object, params)
elif base_type == "documentloaders": elif base_type == "documentloaders":
@ -106,9 +116,12 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
def instantiate_custom_component(node_type, class_object, params): def instantiate_custom_component(node_type, class_object, params):
class_object = get_function_custom(params.pop("code")) # we need to make a copy of the params because we will be
# modifying it
params_copy = params.copy()
class_object = get_function_custom(params_copy.pop("code"))
custom_component = class_object() custom_component = class_object()
built_object = custom_component.build(**params) built_object = custom_component.build(**params_copy)
return built_object, {"repr": custom_component.custom_repr()} return built_object, {"repr": custom_component.custom_repr()}
@ -134,7 +147,7 @@ def instantiate_llm(node_type, class_object, params: Dict):
# This is a workaround so JinaChat works until streaming is implemented # This is a workaround so JinaChat works until streaming is implemented
# if "openai_api_base" in params and "jina" in params["openai_api_base"]: # if "openai_api_base" in params and "jina" in params["openai_api_base"]:
# False if condition is True # False if condition is True
if node_type == "VertexAI": if "VertexAI" in node_type:
return initialize_vertexai(class_object=class_object, params=params) return initialize_vertexai(class_object=class_object, params=params)
# max_tokens sometimes is a string and should be an int # max_tokens sometimes is a string and should be an int
if "max_tokens" in params: if "max_tokens" in params:
@ -212,6 +225,9 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params:
def instantiate_prompt(node_type, class_object, params: Dict): def instantiate_prompt(node_type, class_object, params: Dict):
params, prompt = handle_node_type(node_type, class_object, params) params, prompt = handle_node_type(node_type, class_object, params)
format_kwargs = handle_format_kwargs(prompt, params) format_kwargs = handle_format_kwargs(prompt, params)
# Now we'll use partial_format to format the prompt
if format_kwargs:
prompt = handle_partial_variables(prompt, format_kwargs)
return prompt, format_kwargs return prompt, format_kwargs
@ -245,9 +261,13 @@ def instantiate_toolkit(node_type, class_object: Type[BaseToolkit], params: Dict
return loaded_toolkit return loaded_toolkit
def instantiate_embedding(class_object, params: Dict): def instantiate_embedding(node_type, class_object, params: Dict):
params.pop("model", None) params.pop("model", None)
params.pop("headers", None) params.pop("headers", None)
if "VertexAI" in node_type:
return initialize_vertexai(class_object=class_object, params=params)
try: try:
return class_object(**params) return class_object(**params)
except ValidationError: except ValidationError:

View file

@ -44,6 +44,16 @@ def handle_format_kwargs(prompt, params: Dict):
return format_kwargs return format_kwargs
def handle_partial_variables(prompt, format_kwargs: Dict):
partial_variables = format_kwargs.copy()
partial_variables = {
key: value for key, value in partial_variables.items() if value
}
# Remove handle_keys otherwise LangChain raises an error
partial_variables.pop("handle_keys", None)
return prompt.partial(**partial_variables)
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict): def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):
variable = params[input_variable] variable = params[input_variable]
if isinstance(variable, str): if isinstance(variable, str):

View file

@ -130,8 +130,8 @@ def initialize_pinecone(class_object: Type[Pinecone], params: dict):
import pinecone # type: ignore import pinecone # type: ignore
pinecone_api_key = params.get("pinecone_api_key") pinecone_api_key = params.pop("pinecone_api_key")
pinecone_env = params.get("pinecone_env") pinecone_env = params.pop("pinecone_env")
if pinecone_api_key is None or pinecone_env is None: if pinecone_api_key is None or pinecone_env is None:
if os.getenv("PINECONE_API_KEY") is not None: if os.getenv("PINECONE_API_KEY") is not None:
@ -170,6 +170,26 @@ def initialize_pinecone(class_object: Type[Pinecone], params: dict):
def initialize_chroma(class_object: Type[Chroma], params: dict): def initialize_chroma(class_object: Type[Chroma], params: dict):
"""Initialize a ChromaDB object from the params""" """Initialize a ChromaDB object from the params"""
if ( # type: ignore
"chroma_server_host" in params or "chroma_server_http_port" in params
):
import chromadb # type: ignore
settings_params = {
key: params[key]
for key, value_ in params.items()
if key.startswith("chroma_server_") and value_
}
chroma_settings = chromadb.config.Settings(**settings_params)
params["client_settings"] = chroma_settings
else:
# remove all chroma_server_ keys from params
params = {
key: value
for key, value in params.items()
if not key.startswith("chroma_server_")
}
persist = params.pop("persist", False) persist = params.pop("persist", False)
if not docs_in_params(params): if not docs_in_params(params):
params.pop("documents", None) params.pop("documents", None)

View file

@ -14,34 +14,43 @@ 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 langflow.utils.lazy_load import LazyLoadDictBase
def get_type_dict(): class AllTypesDict(LazyLoadDictBase):
return { def __init__(self):
"agents": agent_creator.to_list(), self._all_types_dict = None
"prompts": prompt_creator.to_list(),
"llms": llm_creator.to_list(), @property
"tools": tool_creator.to_list(), def ALL_TYPES_DICT(self):
"chains": chain_creator.to_list(), return self.all_types_dict
"memory": memory_creator.to_list(),
"toolkits": toolkits_creator.to_list(), def _build_dict(self):
"wrappers": wrapper_creator.to_list(), langchain_types_dict = self.get_type_dict()
"documentLoaders": documentloader_creator.to_list(), return {
"vectorStore": vectorstore_creator.to_list(), **langchain_types_dict,
"embeddings": embedding_creator.to_list(), "Custom": ["Custom Tool", "Python Function"],
"textSplitters": textsplitter_creator.to_list(), }
"utilities": utility_creator.to_list(),
"outputParsers": output_parser_creator.to_list(), def get_type_dict(self):
"retrievers": retriever_creator.to_list(), return {
"custom_components": custom_component_creator.to_list(), "agents": agent_creator.to_list(),
} "prompts": prompt_creator.to_list(),
"llms": llm_creator.to_list(),
"tools": tool_creator.to_list(),
"chains": chain_creator.to_list(),
"memory": memory_creator.to_list(),
"toolkits": toolkits_creator.to_list(),
"wrappers": wrapper_creator.to_list(),
"documentLoaders": documentloader_creator.to_list(),
"vectorStore": vectorstore_creator.to_list(),
"embeddings": embedding_creator.to_list(),
"textSplitters": textsplitter_creator.to_list(),
"utilities": utility_creator.to_list(),
"outputParsers": output_parser_creator.to_list(),
"retrievers": retriever_creator.to_list(),
"custom_components": custom_component_creator.to_list(),
}
LANGCHAIN_TYPES_DICT = get_type_dict() lazy_load_dict = AllTypesDict()
# Now we'll build a dict with Langchain types and ours
ALL_TYPES_DICT = {
**LANGCHAIN_TYPES_DICT,
"Custom": ["Custom Tool", "Python Function"],
}

View file

@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import llm_type_to_cls_dict from langflow.interface.custom_lists import llm_type_to_cls_dict
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.llms import LLMFrontendNode from langflow.template.frontend_node.llms import LLMFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -33,10 +34,12 @@ class LLMCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
llm.__name__ llm.__name__
for llm in self.type_to_loader_dict.values() for llm in self.type_to_loader_dict.values()
if llm.__name__ in settings.llms or settings.dev if llm.__name__ in settings_manager.settings.LLMS
or settings_manager.settings.DEV
] ]

View file

@ -2,7 +2,8 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import memory_type_to_cls_dict from langflow.interface.custom_lists import memory_type_to_cls_dict
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.frontend_node.memories import MemoryFrontendNode from langflow.template.frontend_node.memories import MemoryFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
@ -48,10 +49,12 @@ class MemoryCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
memory.__name__ memory.__name__
for memory in self.type_to_loader_dict.values() for memory in self.type_to_loader_dict.values()
if memory.__name__ in settings.memories or settings.dev if memory.__name__ in settings_manager.settings.MEMORIES
or settings_manager.settings.DEV
] ]

View file

@ -4,7 +4,8 @@ from langchain import output_parsers
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class, build_template_from_method from langflow.utils.util import build_template_from_class, build_template_from_method
@ -23,6 +24,7 @@ class OutputParserCreator(LangChainTypeCreator):
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
if self.type_dict is None: if self.type_dict is None:
settings_manager = get_settings_manager()
self.type_dict = { self.type_dict = {
output_parser_name: import_class( output_parser_name: import_class(
f"langchain.output_parsers.{output_parser_name}" f"langchain.output_parsers.{output_parser_name}"
@ -33,7 +35,8 @@ class OutputParserCreator(LangChainTypeCreator):
self.type_dict = { self.type_dict = {
name: output_parser name: output_parser
for name, output_parser in self.type_dict.items() for name, output_parser in self.type_dict.items()
if name in settings.output_parsers or settings.dev if name in settings_manager.settings.OUTPUT_PARSERS
or settings_manager.settings.DEV
} }
return self.type_dict return self.type_dict

View file

@ -5,7 +5,8 @@ from langchain import prompts
from langflow.custom.customs import get_custom_nodes from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.prompts import PromptFrontendNode from langflow.template.frontend_node.prompts import PromptFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -20,6 +21,7 @@ class PromptCreator(LangChainTypeCreator):
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
settings_manager = get_settings_manager()
if self.type_dict is None: if self.type_dict is None:
self.type_dict = { self.type_dict = {
prompt_name: import_class(f"langchain.prompts.{prompt_name}") prompt_name: import_class(f"langchain.prompts.{prompt_name}")
@ -34,7 +36,8 @@ class PromptCreator(LangChainTypeCreator):
self.type_dict = { self.type_dict = {
name: prompt name: prompt
for name, prompt in self.type_dict.items() for name, prompt in self.type_dict.items()
if name in settings.prompts or settings.dev if name in settings_manager.settings.PROMPTS
or settings_manager.settings.DEV
} }
return self.type_dict return self.type_dict

View file

@ -4,7 +4,8 @@ from langchain import retrievers
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.retrievers import RetrieverFrontendNode from langflow.template.frontend_node.retrievers import RetrieverFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_method, build_template_from_class from langflow.utils.util import build_template_from_method, build_template_from_class
@ -48,10 +49,12 @@ class RetrieverCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
retriever retriever
for retriever in self.type_to_loader_dict.keys() for retriever in self.type_to_loader_dict.keys()
if retriever in settings.retrievers or settings.dev if retriever in settings_manager.settings.RETRIEVERS
or settings_manager.settings.DEV
] ]

View file

@ -1,4 +1,4 @@
from langflow.cache.utils import memoize_dict from langflow.services.cache.utils import memoize_dict
from langflow.graph import Graph from langflow.graph import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger

View file

@ -1,9 +1,10 @@
from typing import Dict, List, Optional, Type from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode
from langflow.interface.custom_lists import textsplitter_type_to_cls_dict from langflow.interface.custom_lists import textsplitter_type_to_cls_dict
from langflow.settings import settings
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -30,10 +31,12 @@ class TextSplitterCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
textsplitter.__name__ textsplitter.__name__
for textsplitter in self.type_to_loader_dict.values() for textsplitter in self.type_to_loader_dict.values()
if textsplitter.__name__ in settings.textsplitters or settings.dev if textsplitter.__name__ in settings_manager.settings.TEXTSPLITTERS
or settings_manager.settings.DEV
] ]

View file

@ -4,7 +4,8 @@ from langchain.agents import agent_toolkits
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class, import_module from langflow.interface.importing.utils import import_class, import_module
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -29,13 +30,15 @@ class ToolkitCreator(LangChainTypeCreator):
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
if self.type_dict is None: if self.type_dict is None:
settings_manager = get_settings_manager()
self.type_dict = { self.type_dict = {
toolkit_name: import_class( toolkit_name: import_class(
f"langchain.agents.agent_toolkits.{toolkit_name}" f"langchain.agents.agent_toolkits.{toolkit_name}"
) )
# if toolkit_name is not lower case it is a class # if toolkit_name is not lower case it is a class
for toolkit_name in agent_toolkits.__all__ for toolkit_name in agent_toolkits.__all__
if not toolkit_name.islower() and toolkit_name in settings.toolkits if not toolkit_name.islower()
and toolkit_name in settings_manager.settings.TOOLKITS
} }
return self.type_dict return self.type_dict

View file

@ -15,7 +15,8 @@ from langflow.interface.tools.constants import (
OTHER_TOOLS, OTHER_TOOLS,
) )
from langflow.interface.tools.util import get_tool_params from langflow.interface.tools.util import get_tool_params
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.template.template.base import Template from langflow.template.template.base import Template
from langflow.utils import util from langflow.utils import util
@ -66,6 +67,7 @@ class ToolCreator(LangChainTypeCreator):
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
settings_manager = get_settings_manager()
if self.tools_dict is None: if self.tools_dict is None:
all_tools = {} all_tools = {}
@ -74,7 +76,10 @@ class ToolCreator(LangChainTypeCreator):
tool_name = tool_params.get("name") or tool tool_name = tool_params.get("name") or tool
if tool_name in settings.tools or settings.dev: if (
tool_name in settings_manager.settings.TOOLS
or settings_manager.settings.DEV
):
if tool_name == "JsonSpec": if tool_name == "JsonSpec":
tool_params["path"] = tool_params.pop("dict_") # type: ignore tool_params["path"] = tool_params.pop("dict_") # type: ignore
all_tools[tool_name] = { all_tools[tool_name] = {

View file

@ -1,4 +1,7 @@
from typing import Any import ast
import contextlib
from typing import Any, List
from langflow.api.utils import merge_nested_dicts_with_renaming
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
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
@ -28,7 +31,6 @@ from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import get_base_classes from langflow.utils.util import get_base_classes
from langflow.api.utils import merge_nested_dicts
import re import re
import warnings import warnings
@ -145,7 +147,7 @@ def add_code_field(template, raw_code, field_config):
"dynamic": True, "dynamic": True,
"required": True, "required": True,
"placeholder": "", "placeholder": "",
"show": True, "show": field_config.pop("show", True),
"multiline": True, "multiline": True,
"value": raw_code, "value": raw_code,
"password": False, "password": False,
@ -186,7 +188,7 @@ def build_frontend_node(custom_component: CustomComponent):
return None return None
def update_display_name_and_description(frontend_node, template_config): def update_attributes(frontend_node, template_config):
"""Update the display name and description of a frontend node""" """Update the display name and description of a frontend node"""
if "display_name" in template_config: if "display_name" in template_config:
frontend_node["display_name"] = template_config["display_name"] frontend_node["display_name"] = template_config["display_name"]
@ -194,6 +196,12 @@ def update_display_name_and_description(frontend_node, template_config):
if "description" in template_config: if "description" in template_config:
frontend_node["description"] = template_config["description"] frontend_node["description"] = template_config["description"]
if "beta" in template_config:
frontend_node["beta"] = template_config["beta"]
if "documentation" in template_config:
frontend_node["documentation"] = template_config["documentation"]
def build_field_config(custom_component: CustomComponent): def build_field_config(custom_component: CustomComponent):
"""Build the field configuration for a custom component""" """Build the field configuration for a custom component"""
@ -247,55 +255,60 @@ def get_field_properties(extra_field):
if not field_required: if not field_required:
field_type = extract_type_from_optional(field_type) field_type = extract_type_from_optional(field_type)
with contextlib.suppress(Exception):
field_value = ast.literal_eval(field_value)
return field_name, field_type, field_value, field_required return field_name, field_type, field_value, field_required
def add_base_classes(frontend_node, return_type): def add_base_classes(frontend_node, return_types: List[str]):
"""Add base classes to the frontend node""" """Add base classes to the frontend node"""
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None: for return_type in return_types:
raise HTTPException( if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
status_code=400, raise HTTPException(
detail={ status_code=400,
"error": ( detail={
"Invalid return type should be one of: " "error": (
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}" "Invalid return type should be one of: "
), f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
"traceback": traceback.format_exc(), ),
}, "traceback": traceback.format_exc(),
) },
)
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type) return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
base_classes = get_base_classes(return_type_instance) base_classes = get_base_classes(return_type_instance)
for base_class in base_classes: for base_class in base_classes:
if base_class not in CLASSES_TO_REMOVE: if base_class not in CLASSES_TO_REMOVE:
frontend_node.get("base_classes").append(base_class) frontend_node.get("base_classes").append(base_class)
def build_langchain_template_custom_component(custom_component: CustomComponent): def build_langchain_template_custom_component(custom_component: CustomComponent):
"""Build a custom component template for the langchain""" """Build a custom component template for the langchain"""
logger.debug("Building custom component template")
frontend_node = build_frontend_node(custom_component) frontend_node = build_frontend_node(custom_component)
if frontend_node is None: if frontend_node is None:
return None return None
logger.debug("Built base frontend node")
template_config = custom_component.build_template_config template_config = custom_component.build_template_config
update_display_name_and_description(frontend_node, template_config) update_attributes(frontend_node, template_config)
logger.debug("Updated attributes")
field_config = build_field_config(custom_component) field_config = build_field_config(custom_component)
logger.debug("Built field config")
add_extra_fields( add_extra_fields(
frontend_node, field_config, custom_component.get_function_entrypoint_args frontend_node, field_config, custom_component.get_function_entrypoint_args
) )
logger.debug("Added extra fields")
frontend_node = add_code_field( frontend_node = add_code_field(
frontend_node, custom_component.code, field_config.get("code", {}) frontend_node, custom_component.code, field_config.get("code", {})
) )
logger.debug("Added code field")
add_base_classes( add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type frontend_node, custom_component.get_function_entrypoint_return_type
) )
logger.debug("Added base classes")
return frontend_node return frontend_node
@ -306,7 +319,7 @@ def load_files_from_path(path: str):
return reader.get_files() return reader.get_files()
def build_and_validate_all_files(reader, file_list): def build_and_validate_all_files(reader: DirectoryReader, file_list):
"""Build and validate all files""" """Build and validate all files"""
data = reader.build_component_menu_list(file_list) data = reader.build_component_menu_list(file_list)
@ -319,31 +332,53 @@ def build_and_validate_all_files(reader, file_list):
def build_valid_menu(valid_components): def build_valid_menu(valid_components):
"""Build the valid menu""" """Build the valid menu"""
valid_menu = {} valid_menu = {}
logger.debug("------------------- VALID COMPONENTS -------------------")
for menu_item in valid_components["menu"]: for menu_item in valid_components["menu"]:
menu_name = menu_item["name"] menu_name = menu_item["name"]
valid_menu[menu_name] = {} valid_menu[menu_name] = {}
for component in menu_item["components"]: for component in menu_item["components"]:
logger.debug(f"Building component: {component}")
try: try:
component_name = component["name"] component_name = component["name"]
component_code = component["code"] component_code = component["code"]
component_output_types = component["output_types"]
component_extractor = CustomComponent(code=component_code) component_extractor = CustomComponent(code=component_code)
component_extractor.is_check_valid() component_extractor.is_check_valid()
component_template = build_langchain_template_custom_component( component_template = build_langchain_template_custom_component(
component_extractor component_extractor
) )
component_template["output_types"] = component_output_types
if len(component_output_types) == 1:
component_name = component_output_types[0]
else:
file_name = component.get("file").split(".")[0]
if "_" in file_name:
# turn .py file into camelcase
component_name = "".join(
[word.capitalize() for word in file_name.split("_")]
)
else:
component_name = file_name
valid_menu[menu_name][component_name] = component_template valid_menu[menu_name][component_name] = component_template
logger.debug(f"Added {component_name} to valid menu to {menu_name}")
except Exception as exc: except Exception as exc:
logger.error(f"Error while building custom component: {exc}") logger.error(f"Error loading Component: {component['output_types']}")
logger.exception(
f"Error while building custom component {component_output_types}: {exc}"
)
return valid_menu return valid_menu
def build_invalid_menu(invalid_components): def build_invalid_menu(invalid_components):
"""Build the invalid menu""" """Build the invalid menu"""
if invalid_components.get("menu"):
logger.debug("------------------- INVALID COMPONENTS -------------------")
invalid_menu = {} invalid_menu = {}
for menu_item in invalid_components["menu"]: for menu_item in invalid_components["menu"]:
menu_name = menu_item["name"] menu_name = menu_item["name"]
@ -364,12 +399,16 @@ def build_invalid_menu(invalid_components):
) )
component_template["error"] = component.get("error", None) component_template["error"] = component.get("error", None)
logger.debug(component)
logger.debug(f"Component Path: {component.get('path', None)}")
logger.debug(f"Component Error: {component.get('error', None)}")
component_template.get("template").get("code")["value"] = component_code component_template.get("template").get("code")["value"] = component_code
invalid_menu[menu_name][component_name] = component_template invalid_menu[menu_name][component_name] = component_template
logger.debug(f"Added {component_name} to invalid menu to {menu_name}")
except Exception as exc: except Exception as exc:
logger.error( logger.exception(
f"Error while creating custom component [{component_name}]: {str(exc)}" f"Error while creating custom component [{component_name}]: {str(exc)}"
) )
@ -388,4 +427,4 @@ def build_langchain_custom_component_list_from_path(path: str):
valid_menu = build_valid_menu(valid_components) valid_menu = build_valid_menu(valid_components)
invalid_menu = build_invalid_menu(invalid_components) invalid_menu = build_invalid_menu(invalid_components)
return merge_nested_dicts(valid_menu, invalid_menu) return merge_nested_dicts_with_renaming(valid_menu, invalid_menu)

View file

@ -5,7 +5,8 @@ from langchain import SQLDatabase, utilities
from langflow.custom.customs import get_custom_nodes from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.utilities import UtilitiesFrontendNode from langflow.template.frontend_node.utilities import UtilitiesFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -26,6 +27,7 @@ class UtilityCreator(LangChainTypeCreator):
from the langchain.chains module and filtering them according to the settings.utilities list. from the langchain.chains module and filtering them according to the settings.utilities list.
""" """
if self.type_dict is None: if self.type_dict is None:
settings_manager = get_settings_manager()
self.type_dict = { self.type_dict = {
utility_name: import_class(f"langchain.utilities.{utility_name}") utility_name: import_class(f"langchain.utilities.{utility_name}")
for utility_name in utilities.__all__ for utility_name in utilities.__all__
@ -35,7 +37,8 @@ class UtilityCreator(LangChainTypeCreator):
self.type_dict = { self.type_dict = {
name: utility name: utility
for name, utility in self.type_dict.items() for name, utility in self.type_dict.items()
if name in settings.utilities or settings.dev if name in settings_manager.settings.UTILITIES
or settings_manager.settings.DEV
} }
return self.type_dict return self.type_dict

View file

@ -9,7 +9,8 @@ import yaml
from langchain.base_language import BaseLanguageModel from langchain.base_language import BaseLanguageModel
from PIL.Image import Image from PIL.Image import Image
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.chat.config import ChatConfig from langflow.services.chat.config import ChatConfig
from langflow.services.utils import get_settings_manager
def load_file_into_dict(file_path: str) -> dict: def load_file_into_dict(file_path: str) -> dict:
@ -63,24 +64,21 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]:
def setup_llm_caching(): def setup_llm_caching():
"""Setup LLM caching.""" """Setup LLM caching."""
settings_manager = get_settings_manager()
from langflow.settings import settings
try: try:
set_langchain_cache(settings) set_langchain_cache(settings_manager.settings)
except ImportError: except ImportError:
logger.warning(f"Could not import {settings.cache}. ") logger.warning(f"Could not import {settings_manager.settings.CACHE}. ")
except Exception as exc: except Exception as exc:
logger.warning(f"Could not setup LLM caching. Error: {exc}") logger.warning(f"Could not setup LLM caching. Error: {exc}")
# TODO Rename this here and in `setup_llm_caching`
def set_langchain_cache(settings): def set_langchain_cache(settings):
import langchain import langchain
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
cache_type = os.getenv("LANGFLOW_LANGCHAIN_CACHE") cache_type = os.getenv("LANGFLOW_LANGCHAIN_CACHE")
cache_class = import_class(f"langchain.cache.{cache_type or settings.cache}") cache_class = import_class(f"langchain.cache.{cache_type or settings.CACHE}")
logger.debug(f"Setting up LLM caching with {cache_class.__name__}") logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
langchain.llm_cache = cache_class() langchain.llm_cache = cache_class()

View file

@ -4,7 +4,8 @@ from langchain import vectorstores
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.services.utils import get_settings_manager
from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_method from langflow.utils.util import build_template_from_method
@ -43,10 +44,12 @@ class VectorstoreCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
settings_manager = get_settings_manager()
return [ return [
vectorstore vectorstore
for vectorstore in self.type_to_loader_dict.keys() for vectorstore in self.type_to_loader_dict.keys()
if vectorstore in settings.vectorstores or settings.dev if vectorstore in settings_manager.settings.VECTORSTORES
or settings_manager.settings.DEV
] ]

View file

@ -6,13 +6,15 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from langflow.api import router from langflow.api import router
from langflow.database.base import create_db_and_tables
from langflow.interface.utils import setup_llm_caching from langflow.interface.utils import setup_llm_caching
from langflow.services.database.utils import initialize_database
from langflow.services.manager import initialize_services
from langflow.utils.logger import configure from langflow.utils.logger import configure
def create_app(): def create_app():
"""Create the FastAPI app and include the router.""" """Create the FastAPI app and include the router."""
configure() configure()
app = FastAPI() app = FastAPI()
@ -32,9 +34,10 @@ def create_app():
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(router) app.include_router(router)
app.on_event("startup")(create_db_and_tables)
app.on_event("startup")(initialize_services)
app.on_event("startup")(initialize_database)
app.on_event("startup")(setup_llm_caching) app.on_event("startup")(setup_llm_caching)
return app return app
@ -67,16 +70,19 @@ def get_static_files_dir():
return frontend_path / "frontend" return frontend_path / "frontend"
def setup_app(static_files_dir: Optional[Path] = None) -> FastAPI: def setup_app(
static_files_dir: Optional[Path] = None, backend_only: bool = False
) -> FastAPI:
"""Setup the FastAPI app.""" """Setup the FastAPI app."""
# get the directory of the current file # get the directory of the current file
if not static_files_dir: if not static_files_dir:
static_files_dir = get_static_files_dir() static_files_dir = get_static_files_dir()
if not static_files_dir or not static_files_dir.exists(): if not backend_only and (not static_files_dir or not static_files_dir.exists()):
raise RuntimeError(f"Static files directory {static_files_dir} does not exist.") raise RuntimeError(f"Static files directory {static_files_dir} does not exist.")
app = create_app() app = create_app()
setup_static_files(app, static_files_dir) if not backend_only and static_files_dir is not None:
setup_static_files(app, static_files_dir)
return app return app

View file

@ -0,0 +1,4 @@
from .manager import service_manager
from .schema import ServiceType
__all__ = ["service_manager", "ServiceType"]

View file

@ -0,0 +1,2 @@
class Service:
name: str

View file

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

View file

@ -0,0 +1,11 @@
from langflow.services.cache.manager import CacheManager
from langflow.services.factory import ServiceFactory
class CacheManagerFactory(ServiceFactory):
def __init__(self):
super().__init__(CacheManager)
def create(self, settings_service):
# Here you would have logic to create and configure a CacheManager
return CacheManager()

View file

@ -2,7 +2,7 @@ import threading
import time import time
from collections import OrderedDict from collections import OrderedDict
from langflow.cache.base import BaseCache from langflow.services.cache.base import BaseCache
class InMemoryCache(BaseCache): class InMemoryCache(BaseCache):

View file

@ -1,5 +1,6 @@
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Awaitable, Callable, List, Optional from typing import Any, Awaitable, Callable, List, Optional
from langflow.services.base import Service
import pandas as pd import pandas as pd
from PIL import Image from PIL import Image
@ -49,9 +50,11 @@ class AsyncSubject:
await observer() await observer()
class CacheManager(Subject): class CacheManager(Subject, Service):
"""Manages cache for different clients and notifies observers on changes.""" """Manages cache for different clients and notifies observers on changes."""
name = "cache_manager"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._cache = {} self._cache = {}

View file

@ -0,0 +1,11 @@
from langflow.services.chat.manager import ChatManager
from langflow.services.factory import ServiceFactory
class ChatManagerFactory(ServiceFactory):
def __init__(self):
super().__init__(ChatManager)
def create(self, settings_service):
# Here you would have logic to create and configure a ChatManager
return ChatManager()

View file

@ -1,10 +1,12 @@
from collections import defaultdict from collections import defaultdict
from fastapi import WebSocket, status from fastapi import WebSocket, status
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.cache import cache_manager from langflow.services.base import Service
from langflow.cache.manager import Subject from langflow.services import service_manager
from langflow.chat.utils import process_graph from langflow.services.cache.manager import Subject
from langflow.services.chat.utils import process_graph
from langflow.interface.utils import pil_to_base64 from langflow.interface.utils import pil_to_base64
from langflow.services.schema import ServiceType
from langflow.utils.logger import logger from langflow.utils.logger import logger
@ -12,7 +14,7 @@ import asyncio
import json import json
from typing import Any, Dict, List from typing import Any, Dict, List
from langflow.cache.flow import InMemoryCache from langflow.services.cache.flow import InMemoryCache
class ChatHistory(Subject): class ChatHistory(Subject):
@ -42,11 +44,13 @@ class ChatHistory(Subject):
self.history[client_id] = [] self.history[client_id] = []
class ChatManager: class ChatManager(Service):
name = "chat_manager"
def __init__(self): def __init__(self):
self.active_connections: Dict[str, WebSocket] = {} self.active_connections: Dict[str, WebSocket] = {}
self.chat_history = ChatHistory() self.chat_history = ChatHistory()
self.cache_manager = cache_manager self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER)
self.cache_manager.attach(self.update) self.cache_manager.attach(self.update)
self.in_memory_cache = InMemoryCache() self.in_memory_cache = InMemoryCache()
@ -117,7 +121,7 @@ class ChatManager:
self, client_id: str, payload: Dict, langchain_object: Any self, client_id: str, payload: Dict, langchain_object: Any
): ):
# Process the graph data and chat message # Process the graph data and chat message
chat_inputs = payload.pop("inputs", "") chat_inputs = payload.pop("inputs", {})
chat_inputs = ChatMessage(message=chat_inputs) chat_inputs = ChatMessage(message=chat_inputs)
self.chat_history.add_message(client_id, chat_inputs) self.chat_history.add_message(client_id, chat_inputs)

View file

@ -21,9 +21,9 @@ async def process_graph(
# Generate result and thought # Generate result and thought
try: try:
if not chat_inputs.message: if chat_inputs.message is None:
logger.debug("No message provided") logger.debug("No message provided")
raise ValueError("No message provided") chat_inputs.message = {}
logger.debug("Generating result and thought") logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps( result, intermediate_steps = await get_result_and_steps(

View file

@ -0,0 +1,17 @@
from typing import TYPE_CHECKING
from langflow.services.database.manager import DatabaseManager
from langflow.services.factory import ServiceFactory
if TYPE_CHECKING:
from langflow.services.settings.manager import SettingsManager
class DatabaseManagerFactory(ServiceFactory):
def __init__(self):
super().__init__(DatabaseManager)
def create(self, settings_service: "SettingsManager"):
# Here you would have logic to create and configure a DatabaseManager
if not settings_service.settings.DATABASE_URL:
raise ValueError("No database URL provided")
return DatabaseManager(settings_service.settings.DATABASE_URL)

View file

@ -0,0 +1,67 @@
from pathlib import Path
from langflow.services.base import Service
from sqlmodel import SQLModel, Session, create_engine
from langflow.utils.logger import logger
from alembic.config import Config
from alembic import command
from langflow.services.database import models # noqa
class DatabaseManager(Service):
name = "database_manager"
def __init__(self, database_url: str):
self.database_url = database_url
# This file is in langflow.services.database.manager.py
# the ini is in langflow
langflow_dir = Path(__file__).parent.parent.parent
self.script_location = langflow_dir / "alembic"
self.alembic_cfg_path = langflow_dir / "alembic.ini"
self.engine = create_engine(database_url)
def __enter__(self):
self._session = Session(self.engine)
return self._session
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None: # If an exception has been raised
logger.error(
f"Session rollback because of exception: {exc_type.__name__} {exc_value}"
)
self._session.rollback()
else:
self._session.commit()
self._session.close()
def get_session(self):
with Session(self.engine) as session:
yield session
def run_migrations(self):
logger.info(
f"Running DB migrations in {self.script_location} on {self.database_url}"
)
alembic_cfg = Config()
alembic_cfg.set_main_option("script_location", str(self.script_location))
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
command.upgrade(alembic_cfg, "head")
def create_db_and_tables(self):
logger.debug("Creating database and tables")
try:
SQLModel.metadata.create_all(self.engine)
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(self.engine)
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")

View file

@ -0,0 +1,4 @@
from .flow import Flow
__all__ = ["Flow"]

View file

@ -1,4 +1,4 @@
from langflow.database.models.base import SQLModelSerializable, SQLModel from langflow.services.database.models.base import SQLModelSerializable, SQLModel
from sqlmodel import Field from sqlmodel import Field
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime

View file

@ -1,13 +1,12 @@
# Path: src/backend/langflow/database/models/flow.py # Path: src/backend/langflow/database/models/flow.py
from langflow.database.models.base import SQLModelSerializable from langflow.services.database.models.base import SQLModelSerializable
from pydantic import validator from pydantic import validator
from sqlmodel import Field, Relationship, JSON, Column from sqlmodel import Field, JSON, Column
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from typing import Dict, Optional from typing import Dict, Optional
# if TYPE_CHECKING: # if TYPE_CHECKING:
from langflow.database.models.flow_style import FlowStyle, FlowStyleRead
class FlowBase(SQLModelSerializable): class FlowBase(SQLModelSerializable):
@ -35,11 +34,6 @@ class FlowBase(SQLModelSerializable):
class Flow(FlowBase, table=True): class Flow(FlowBase, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) data: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
style: Optional["FlowStyle"] = Relationship(
back_populates="flow",
# use "uselist=False" to make it a one-to-one relationship
sa_relationship_kwargs={"uselist": False},
)
class FlowCreate(FlowBase): class FlowCreate(FlowBase):
@ -50,10 +44,6 @@ class FlowRead(FlowBase):
id: UUID id: UUID
class FlowReadWithStyle(FlowRead):
style: Optional["FlowStyleRead"] = None
class FlowUpdate(SQLModelSerializable): class FlowUpdate(SQLModelSerializable):
name: Optional[str] = None name: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None

View file

@ -0,0 +1,47 @@
from typing import TYPE_CHECKING
from langflow.utils.logger import logger
from contextlib import contextmanager
from alembic.util.exc import CommandError
from sqlmodel import Session
if TYPE_CHECKING:
from langflow.services.database.manager import DatabaseManager
def initialize_database():
logger.debug("Initializing database")
from langflow.services import service_manager, ServiceType
database_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
try:
database_manager.run_migrations()
except CommandError as exc:
if "Can't locate revision identified by" not in str(exc):
raise exc
# This means there's wrong revision in the DB
# We need to delete the alembic_version table
# and run the migrations again
logger.warning(
"Wrong revision in DB, deleting alembic_version table and running migrations again"
)
with session_getter(database_manager) as session:
session.execute("DROP TABLE alembic_version")
database_manager.run_migrations()
except Exception as exc:
logger.error(f"Error running migrations: {exc}")
raise RuntimeError("Error running migrations") from exc
database_manager.create_db_and_tables()
logger.debug("Database initialized")
@contextmanager
def session_getter(db_manager: "DatabaseManager"):
try:
session = Session(db_manager.engine)
yield session
except Exception as e:
print("Session rollback because of exception:", e)
session.rollback()
raise
finally:
session.close()

View file

@ -0,0 +1,6 @@
class ServiceFactory:
def __init__(self, service_class):
self.service_class = service_class
def create(self, *args, **kwargs):
raise NotImplementedError

View file

@ -0,0 +1,87 @@
from langflow.services.schema import ServiceType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from langflow.services.factory import ServiceFactory
class ServiceManager:
"""
Manages the creation of different services.
"""
def __init__(self):
self.services = {}
self.factories = {}
def register_factory(self, service_factory: "ServiceFactory"):
"""
Registers a new factory.
"""
self.factories[service_factory.service_class.name] = service_factory
def get(self, service_name: ServiceType):
"""
Get (or create) a service by its name.
"""
if service_name not in self.services:
self._create_service(service_name)
return self.services[service_name]
def _create_service(self, service_name: ServiceType):
"""
Create a new service given its name.
"""
self._validate_service_creation(service_name)
if service_name == ServiceType.SETTINGS_MANAGER:
self.services[service_name] = self.factories[service_name].create()
else:
settings_service = self.get(ServiceType.SETTINGS_MANAGER)
self.services[service_name] = self.factories[service_name].create(
settings_service
)
def _validate_service_creation(self, service_name: ServiceType):
"""
Validate whether the service can be created.
"""
if service_name not in self.factories:
raise ValueError(
f"No factory registered for the service class '{service_name.name}'"
)
if (
ServiceType.SETTINGS_MANAGER not in self.factories
and service_name != ServiceType.SETTINGS_MANAGER
):
raise ValueError(
f"Cannot create service '{service_name.name}' before the settings service"
)
def update(self, service_name: ServiceType):
"""
Update a service by its name.
"""
if service_name in self.services:
self.services.pop(service_name, None)
self.get(service_name)
service_manager = ServiceManager()
def initialize_services():
"""
Initialize all the services needed.
"""
from langflow.services.database import factory as database_factory
from langflow.services.cache import factory as cache_factory
from langflow.services.chat import factory as chat_factory
from langflow.services.settings import factory as settings_factory
service_manager.register_factory(settings_factory.SettingsManagerFactory())
service_manager.register_factory(database_factory.DatabaseManagerFactory())
service_manager.register_factory(cache_factory.CacheManagerFactory())
service_manager.register_factory(chat_factory.ChatManagerFactory())

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