This commit is contained in:
Cristhian Zanforlin Lousa 2023-09-26 11:21:38 -03:00
commit f56dcc03b0
147 changed files with 14203 additions and 1842 deletions

View file

@ -46,10 +46,20 @@ LANGFLOW_OPEN_BROWSER=
# Example: LANGFLOW_REMOVE_API_KEYS=false
LANGFLOW_REMOVE_API_KEYS=
# Whether to use RedisCache or InMemoryCache
# Values: memory, redis
# Example: LANGFLOW_CACHE_TYPE=memory
# If you want to use redis then the following environment variables must be set:
# LANGFLOW_REDIS_HOST (default: localhost)
# LANGFLOW_REDIS_PORT (default: 6379)
# LANGFLOW_REDIS_DB (default: 0)
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE=
# Superuser username
# Example: LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER=
# Superuser password
# Example: LANGFLOW_SUPERUSER_PASSWORD=123456
LANGFLOW_SUPERUSER_PASSWORD=
LANGFLOW_SUPERUSER_PASSWORD=

44
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: "Async API tests"
on:
push:
branches:
- dev
pull_request:
branches:
- dev
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Set up Docker
run: docker --version && docker-compose --version
- name: "Create env file"
working-directory: ./deploy
run: |
echo "${{ secrets.ENV_FILE }}" > .env
- name: Build and start services
working-directory: ./deploy
run: docker compose up --exit-code-from tests tests result_backend broker celeryworker db --build
continue-on-error: true
- name: Stop services
run: docker compose down

View file

@ -7,7 +7,7 @@ on:
branches: [dev]
env:
POETRY_VERSION: "1.4.0"
POETRY_VERSION: "1.5.0"
jobs:
build:
@ -16,6 +16,8 @@ jobs:
matrix:
python-version:
- "3.10"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- uses: actions/checkout@v3
- name: Install poetry

1
.gitignore vendored
View file

@ -254,3 +254,4 @@ langflow.db
/tmp/*
src/backend/langflow/frontend/
.docker

View file

@ -61,7 +61,7 @@ frontendc:
make run_frontend
install_backend:
poetry install
poetry install --extras deploy
backend:
make install_backend

97
base.Dockerfile Normal file
View file

@ -0,0 +1,97 @@
# syntax=docker/dockerfile:1
# Keep this syntax directive! It's used to enable Docker BuildKit
# Based on https://github.com/python-poetry/poetry/discussions/1879?sort=top#discussioncomment-216865
# but I try to keep it updated (see history)
################################
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.10-slim as python-base
# python
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
\
# pip
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
\
# poetry
# https://python-poetry.org/docs/configuration/#using-environment-variables
POETRY_VERSION=1.5.1 \
# make poetry install to this location
POETRY_HOME="/opt/poetry" \
# make poetry create the virtual environment in the project's root
# it gets named `.venv`
POETRY_VIRTUALENVS_IN_PROJECT=true \
# do not ask any interactive question
POETRY_NO_INTERACTION=1 \
\
# paths
# this is where our requirements + virtual environment will live
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
################################
# BUILDER-BASE
# Used to build deps + create our virtual environment
################################
FROM python-base as builder-base
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
curl \
# deps for building python deps
build-essential
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
# The --mount will mount the buildx cache directory to where
# Poetry and Pip store their cache so that they can re-use it
RUN --mount=type=cache,target=/root/.cache \
curl -sSL https://install.python-poetry.org | python3 -
# copy project requirement files here to ensure they will be cached.
WORKDIR $PYSETUP_PATH
COPY poetry.lock pyproject.toml ./
COPY ./src/backend/langflow/main.py ./src/backend/langflow/main.py
# Copy README.md to the build context
COPY README.md .
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
RUN --mount=type=cache,target=/root/.cache \
poetry install --without dev --extras deploy
################################
# DEVELOPMENT
# Image used during development / testing
################################
FROM python-base as development
WORKDIR $PYSETUP_PATH
# copy in our built poetry + venv
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
# Copy just one file to avoid rebuilding the whole image
COPY ./src/backend/langflow/__init__.py ./src/backend/langflow/__init__.py
# quicker install as runtime deps are already installed
RUN --mount=type=cache,target=/root/.cache \
poetry install --with=dev --extras deploy
# copy in our app code
COPY ./src/backend ./src/backend
RUN --mount=type=cache,target=/root/.cache \
poetry install --with=dev --extras deploy
COPY ./tests ./tests=

57
deploy/.env.example Normal file
View file

@ -0,0 +1,57 @@
DOMAIN=localhost
STACK_NAME=langflow-stack
ENVIRONMENT=development
TRAEFIK_PUBLIC_NETWORK=traefik-public
TRAEFIK_TAG=langflow-traefik
TRAEFIK_PUBLIC_TAG=traefik-public
# RabbitMQ configuration
RABBITMQ_DEFAULT_USER=langflow
RABBITMQ_DEFAULT_PASS=langflow
# Database configuration
DB_USER=langflow
DB_PASSWORD=langflow
DB_HOST=db
DB_PORT=5432
DB_NAME=langflow
# Logging configuration
LOG_LEVEL=debug
# DB configuration
POSTGRES_USER=langflow
POSTGRES_PASSWORD=langflow
POSTGRES_DB=langflow
POSTGRES_PORT=5432
# Flower configuration
LANGFLOW_CACHE_TYPE=redis
LANGFLOW_REDIS_HOST=result_backend
LANGFLOW_REDIS_PORT=6379
LANGFLOW_REDIS_DB=0
LANGFLOW_REDIS_EXPIRE=3600
LANGFLOW_REDIS_PASSWORD=
FLOWER_UNAUTHENTICATED_API=True
BROKER_URL=amqp://langflow:langflow@broker:5672
RESULT_BACKEND=redis://result_backend:6379/0
C_FORCE_ROOT="true"
# Frontend configuration
VITE_PROXY_TARGET=http://backend:7860/api/
BACKEND_URL=http://backend:7860
# PGAdmin configuration
PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=admin
# OpenAI configuration (for testing purposes)
OPENAI_API_KEY=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
# Superuser configuration
LANGFLOW_SUPERUSER=superuser
LANGFLOW_SUPERUSER_PASSWORD=superuser
# New user configuration
LANGFLOW_NEW_USER_IS_ACTIVE=False

1
deploy/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
pgadmin

92
deploy/base.Dockerfile Normal file
View file

@ -0,0 +1,92 @@
# syntax=docker/dockerfile:1
# Keep this syntax directive! It's used to enable Docker BuildKit
# Based on https://github.com/python-poetry/poetry/discussions/1879?sort=top#discussioncomment-216865
# but I try to keep it updated (see history)
################################
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.10-slim as python-base
# python
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
\
# pip
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
\
# poetry
# https://python-poetry.org/docs/configuration/#using-environment-variables
POETRY_VERSION=1.5.1 \
# make poetry install to this location
POETRY_HOME="/opt/poetry" \
# make poetry create the virtual environment in the project's root
# it gets named `.venv`
POETRY_VIRTUALENVS_IN_PROJECT=true \
# do not ask any interactive question
POETRY_NO_INTERACTION=1 \
\
# paths
# this is where our requirements + virtual environment will live
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
################################
# BUILDER-BASE
# Used to build deps + create our virtual environment
################################
FROM python-base as builder-base
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
curl \
# deps for building python deps
build-essential
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
# The --mount will mount the buildx cache directory to where
# Poetry and Pip store their cache so that they can re-use it
RUN --mount=type=cache,target=/root/.cache \
curl -sSL https://install.python-poetry.org | python3 -
# copy project requirement files here to ensure they will be cached.
WORKDIR $PYSETUP_PATH
COPY ./poetry.lock ./pyproject.toml ./
# Copy README.md to the build context
COPY ./README.md ./
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
RUN --mount=type=cache,target=/root/.cache \
poetry install --without dev --extras deploy
################################
# DEVELOPMENT
# Image used during development / testing
################################
FROM python-base as development
WORKDIR $PYSETUP_PATH
# copy in our built poetry + venv
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
# Copy just one file to avoid rebuilding the whole image
COPY ./src/backend/langflow/__init__.py ./src/backend/langflow/__init__.py
# quicker install as runtime deps are already installed
RUN --mount=type=cache,target=/root/.cache \
poetry install --with=dev --extras deploy
# copy in our app code
COPY ./src/backend ./src/backend
COPY ./tests ./tests

View file

@ -0,0 +1,66 @@
version: "3.8"
services:
proxy:
ports:
- "80:80"
- "8090:8080"
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Add a constraint to only use services with the label for this stack
# from the env var TRAEFIK_TAG
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`, `${TRAEFIK_TAG?Variable not set}`)
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Disable Docker Swarm mode for local development
# - --providers.docker.swarmmode
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
# Enable the Dashboard and API in insecure mode for local development
- --api.insecure=true
labels:
- traefik.enable=true
- traefik.http.routers.${STACK_NAME?Variable not set}-traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.services.${STACK_NAME?Variable not set}-traefik-public.loadbalancer.server.port=80
result_backend:
ports:
- "6379:6379"
pgadmin:
ports:
- "5050:5050"
flower:
ports:
- "5555:5555"
backend:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=7860
frontend:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
celeryworker:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-celeryworker-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-celeryworker.loadbalancer.server.port=7860
networks:
traefik-public:
# For local dev, don't expect an external Traefik network
external: false

View file

@ -0,0 +1,85 @@
version: "3.8"
services:
proxy:
ports:
- "80:80"
- "8090:8080"
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Add a constraint to only use services with the label for this stack
# from the env var TRAEFIK_TAG
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`, `${TRAEFIK_TAG?Variable not set}`)
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Disable Docker Swarm mode for local development
# - --providers.docker.swarmmode
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
# Enable the Dashboard and API in insecure mode for local development
- --api.insecure=true
labels:
- traefik.enable=true
- traefik.http.routers.${STACK_NAME?Variable not set}-traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.services.${STACK_NAME?Variable not set}-traefik-public.loadbalancer.server.port=80
result_backend:
ports:
- "6379:6379"
pgadmin:
ports:
- "5050:5050"
flower:
ports:
- "5555:5555"
backend:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=7860
frontend:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
celeryworker:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-celeryworker-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-celeryworker.loadbalancer.server.port=7860
tests:
extends:
file: docker-compose.yml
service: backend
env_file:
- .env
build:
context: ../
dockerfile: base.Dockerfile
command: pytest -vv
healthcheck:
test: "exit 0"
# override deploy labels to avoid conflicts with the backend service
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-tests-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-tests.loadbalancer.server.port=7861
networks:
traefik-public:
# For local dev, don't expect an external Traefik network
external: false

258
deploy/docker-compose.yml Normal file
View file

@ -0,0 +1,258 @@
version: "3.8"
services:
proxy:
image: traefik:v3.0
env_file:
- .env
networks:
- ${TRAEFIK_PUBLIC_NETWORK?Variable not set}
- default
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Add a constraint to only use services with the label for this stack
# from the env var TRAEFIK_TAG
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`, `${TRAEFIK_TAG?Variable not set}`)
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
deploy:
placement:
constraints:
- node.role == manager
labels:
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
# Use the traefik-public network (declared below)
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set}
# Use the custom label "traefik.constraint-label=traefik-public"
# This public Traefik will only use services with this label
- traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set}
# traefik-http set up only to use the middleware to redirect to https
- traefik.http.middlewares.${STACK_NAME?Variable not set}-https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.${STACK_NAME?Variable not set}-https-redirect.redirectscheme.permanent=true
# Handle host with and without "www" to redirect to only one of them
# Uses environment variable DOMAIN
# To disable www redirection remove the Host() you want to discard, here and
# below for HTTPS
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.entrypoints=http
# traefik-https the actual router using HTTPS
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.tls=true
# Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.tls.certresolver=le
# Define the port inside of the Docker service to use
- traefik.http.services.${STACK_NAME?Variable not set}-proxy.loadbalancer.server.port=80
# Handle domain with and without "www" to redirect to only one
# To disable www redirection remove the next line
- traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.regex=^https?://(www.)?(${DOMAIN?Variable not set})/(.*)
# Redirect a domain with www to non-www
# To disable it remove the next line
- traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.replacement=https://${DOMAIN?Variable not set}/$${3}
# Redirect a domain without www to www
# To enable it remove the previous line and uncomment the next
# - traefik.http.middlewares.${STACK_NAME}-www-redirect.redirectregex.replacement=https://www.${DOMAIN}/$${3}
# Middleware to redirect www, to disable it remove the next line
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.middlewares=${STACK_NAME?Variable not set}-www-redirect
# Middleware to redirect www, and redirect HTTP to HTTPS
# to disable www redirection remove the section: ${STACK_NAME?Variable not set}-www-redirect,
- traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.middlewares=${STACK_NAME?Variable not set}-www-redirect,${STACK_NAME?Variable not set}-https-redirect
backend: &backend
image: "ogabrielluiz/langflow:latest"
build:
context: ../
dockerfile: base.Dockerfile
depends_on:
- db
- broker
- result_backend
env_file:
- .env
volumes:
- ../:/app
- ./startup-backend.sh:/startup-backend.sh # Ensure the paths match
command: /startup-backend.sh # Fixed the path
healthcheck:
test: "exit 0"
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=PathPrefix(`/api/v1`) || PathPrefix(`/docs`) || PathPrefix(`/health`)
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=7860
db:
image: postgres:15.4
volumes:
- app-db-data:/var/lib/postgresql/data/pgdata
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
deploy:
placement:
constraints:
- node.labels.app-db-data == true
healthcheck:
test: "exit 0"
env_file:
- .env
pgadmin:
image: dpage/pgadmin4
networks:
- ${TRAEFIK_PUBLIC_NETWORK?Variable not set}
- default
volumes:
- pgadmin-data:/var/lib/pgadmin
env_file:
- .env
deploy:
labels:
- traefik.enable=true
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set}
- traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.rule=Host(`pgadmin.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.middlewares=${STACK_NAME?Variable not set}-https-redirect
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.rule=Host(`pgadmin.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.tls.certresolver=le
- traefik.http.services.${STACK_NAME?Variable not set}-pgadmin.loadbalancer.server.port=5050
result_backend:
image: redis:6.2.5
env_file:
- .env
# ports:
# - 6379:6379
healthcheck:
test: "exit 0"
celeryworker:
<<: *backend
env_file:
- .env
build:
context: ../
dockerfile: base.Dockerfile
command: celery -A langflow.worker.celery_app worker --loglevel=INFO --concurrency=1 -n lf-worker@%h
healthcheck:
test: "exit 0"
deploy:
replicas: 1
flower:
<<: *backend
env_file:
- .env
networks:
- default
build:
context: ../
dockerfile: base.Dockerfile
environment:
- FLOWER_PORT=5555
command: /bin/sh -c "celery -A langflow.worker.celery_app --broker=${BROKER_URL?Variable not set} flower --port=5555"
deploy:
labels:
- traefik.enable=true
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set}
- traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.rule=Host(`flower.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.middlewares=${STACK_NAME?Variable not set}-https-redirect
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.rule=Host(`flower.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.tls.certresolver=le
- traefik.http.services.${STACK_NAME?Variable not set}-flower.loadbalancer.server.port=5555
frontend:
image: "ogabrielluiz/langflow_frontend:latest"
env_file:
- .env
# user: your-non-root-user
build:
context: ../src/frontend
dockerfile: Dockerfile
args:
- BACKEND_URL=http://backend:7860
restart: on-failure
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
broker:
# RabbitMQ management console
image: rabbitmq:3-management
environment:
- RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-admin}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS:-admin}
volumes:
- rabbitmq_data:/etc/rabbitmq/
- rabbitmq_data:/var/lib/rabbitmq/
- rabbitmq_log:/var/log/rabbitmq/
ports:
- 5672:5672
- 15672:15672
prometheus:
image: prom/prometheus:v2.37.9
env_file:
- .env
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- "--config.file=/etc/prometheus/prometheus.yml"
# ports:
# - 9090:9090
healthcheck:
test: "exit 0"
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-prometheus-http.rule=PathPrefix(`/metrics`)
- traefik.http.services.${STACK_NAME?Variable not set}-prometheus.loadbalancer.server.port=9090
grafana:
image: grafana/grafana:8.2.6
env_file:
- .env
# ports:
# - 3000:3000
volumes:
- grafana_data:/var/lib/grafana
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-grafana-http.rule=PathPrefix(`/grafana`)
- traefik.http.services.${STACK_NAME?Variable not set}-grafana.loadbalancer.server.port=3000
volumes:
grafana_data:
app-db-data:
rabbitmq_data:
rabbitmq_log:
pgadmin-data:
networks:
traefik-public:
# Allow setting it to false for testing
external: false # ${TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL-true}

11
deploy/prometheus.yml Normal file
View file

@ -0,0 +1,11 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["prometheus:9090"]
- job_name: flower
static_configs:
- targets: ["flower:5555"]

5
deploy/scripts/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
# terraform
*.tfstate
*.tfstate.backup
*.tfvars
.terraform/

11
deploy/scripts/build-push.sh Executable file
View file

@ -0,0 +1,11 @@
#! /usr/bin/env sh
# Exit in case of error
set -e
TAG=${TAG?Variable not set} \
FRONTEND_ENV=${FRONTEND_ENV-production} \
sh ./scripts/build.sh
docker-compose -f docker-compose.yml push

10
deploy/scripts/build.sh Executable file
View file

@ -0,0 +1,10 @@
#! /usr/bin/env sh
# Exit in case of error
set -e
TAG=${TAG?Variable not set} \
FRONTEND_ENV=${FRONTEND_ENV-production} \
docker-compose \
-f docker-compose.yml \
build

View file

@ -0,0 +1,24 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.13.1"
hashes = [
"h1:T/p3Gp8uAvRk3BmBZI/B7JIUpxF+2DygvBsv2UvJK4w=",
"zh:0d107e410ecfbd5d2fb5ff9793f88e2ce03ae5b3bda4e3b772b5d146cdd859d8",
"zh:1080cf6a402939ec4ad393380f2ab2dfdc0e175903e08ed796aa22eb95868847",
"zh:300420d642c3ada48cfe633444eafa7bcd410cd6a8503de2384f14ac54dc3ce3",
"zh:4e0121014a8d6ef0b1ab4634877545737bb54e951340f1b67ffea8cd22b2d252",
"zh:59b401bbf95dc8c6bea58085ff286543380f176271251193eac09cb7fcf619b7",
"zh:5dfaf51e979131710ce8e1572e6012564e68c7c842e3d9caaaeb0fe6af15c351",
"zh:84bb75dafca056d7c3783be5185187fdd3294f902e9d72f7655f2efb5e066650",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:aa4e2b9f699d497041679bc05ca34ac21a5184298eb1813f35455b1883977910",
"zh:b51a4f08d84b071128df68a95cfa5114301a50bf8ab8e655dcf7e384e5bc6458",
"zh:bce284ac6ebb65053d9b703048e3157bf4175782ea9dbaec9f64dc2b6fcefb0c",
"zh:c748f78b79b354794098b06b18a02aefdb49be144dff8876c9204083980f7de0",
"zh:ee69d9aef5ca532392cdc26499096f3fa6e55f8622387f78194ebfaadb796aef",
"zh:ef561bee58e4976474bc056649095737fa3b2bcb74602032415d770bfc620c1f",
"zh:f696d8416c57c31f144d432779ba34764560a95937db3bb3dd2892a791a6d5a7",
]
}

View file

@ -0,0 +1,114 @@
terraform {
backend "s3" {
bucket = "terraform-5fso81t4tn8z"
key = "backend/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = var.region # Choose the region as needed
}
module "docker-swarm" {
source = "./modules/docker-swarm"
key_name = aws_key_pair.swarm-key.key_name
vpc_id = aws_vpc.swarm-vpc.id
subnet_id = aws_subnet.swarm-public-subnet.id
security_group = aws_security_group.swarm-sg.id
instance_type = var.instance_type # Choose the instance type as needed
manager_count = var.manager_count
worker_count = var.worker_count # This is the number of services in the docker-compose.yml file
}
resource "aws_key_pair" "swarm-key" {
key_name = "swarm-key"
public_key = file("~/.ssh/id_rsa.pub")
}
resource "aws_vpc" "swarm-vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}
resource "aws_subnet" "swarm-private-subnet" {
vpc_id = aws_vpc.swarm-vpc.id
cidr_block = "10.0.1.0/24"
}
resource "aws_subnet" "swarm-public-subnet" {
vpc_id = aws_vpc.swarm-vpc.id
cidr_block = "10.0.2.0/24"
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.swarm-vpc.id
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.swarm-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
resource "aws_route_table_association" "public_subnet_asso" {
subnet_id = aws_subnet.swarm-public-subnet.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_security_group" "swarm-sg" {
vpc_id = aws_vpc.swarm-vpc.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 2376
to_port = 2377
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 7946
to_port = 7946
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 4789
to_port = 4789
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = [aws_subnet.swarm-public-subnet.cidr_block]
}
egress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = [aws_subnet.swarm-public-subnet.cidr_block]
}
}

View file

@ -0,0 +1,94 @@
variable "key_name" {}
variable "vpc_id" {}
variable "subnet_id" {}
variable "security_group" {}
variable "instance_type" {}
variable "manager_count" {}
variable "worker_count" {}
resource "aws_instance" "manager" {
count = var.manager_count
ami = "ami-08a52ddb321b32a8c" # Amazon Linux 2 LTS
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [var.security_group]
subnet_id = var.subnet_id
associate_public_ip_address = true
user_data = <<-EOT
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo yum install -y git
sudo yum install -y nc
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo chkconfig docker on
# Fetch instance metadata with token
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
IP_ADDR=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/local-ipv4)
docker swarm init --advertise-addr $IP_ADDR
# Create a script to get the join token
echo 'docker swarm join-token worker -q' > get_token.sh
chmod +x get_token.sh
timeout 5m bash -c "while true; do { echo -e 'HTTP/1.1 200 OK\r\n'; ./get_token.sh; } | nc -l 8080; done" &
sleep 10
# Clone the repo and start the stack
git clone https://github.com/logspace-ai/langflow.git
cd /langflow
git checkout celery
cd /langflow/deploy
sudo cp .env.example .env
# Add the label to random worker node to ensure that the data volume is created on the same node
docker node update --label-add app-db-data=true $(docker node ls --format '{{.Hostname}}' --filter role=worker | head -n 1)
env $(cat .env | grep ^[A-Z] | xargs) docker stack deploy --compose-file docker-compose.yml langflow_stack
EOT
tags = {
Name = "manager-${count.index}"
}
}
resource "aws_instance" "worker" {
count = var.worker_count
ami = "ami-08a52ddb321b32a8c" # Amazon Linux 2 LTS
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [var.security_group]
subnet_id = var.subnet_id
associate_public_ip_address = true
user_data = <<-EOT
#!/bin/bash
MANAGER_IP="${aws_instance.manager.0.private_ip}"
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo chkconfig docker on
docker swarm join --token $(curl -s http://$MANAGER_IP:8080/token) $MANAGER_IP:2377
EOT
tags = {
Name = "worker-${count.index}"
}
}
output "manager_public_ips" {
value = aws_instance.manager[*].public_ip
}
output "worker_public_ips" {
value = aws_instance.worker[*].public_ip
}

View file

@ -0,0 +1,8 @@
output "manager_public_ips" {
value = module.docker-swarm.manager_public_ips
}
output "worker_public_ips" {
value = module.docker-swarm.worker_public_ips
}

View file

@ -0,0 +1,19 @@
variable "region" {
description = "The AWS region"
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type"
default = "t2.micro"
}
variable "manager_count" {
description = "Number of manager nodes"
default = 1
}
variable "worker_count" {
description = "Number of worker nodes"
default = 3
}

17
deploy/startup-backend.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
export LANGFLOW_DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
# Your command to start the backend
# If the ENVIRONMENT variable is set to "development", then start the backend in development mode
# else start the backend in production mode with guvicorn
if [ "$ENVIRONMENT" = "development" ]; then
echo "Starting backend in development mode"
exec python -m uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --log-level ${LOG_LEVEL:-info} --workers 2 --reload
else
echo "Starting backend in production mode"
exec langflow run --host 0.0.0.0 --port 7860 --log-level ${LOG_LEVEL:-info} --workers -1 --backend-only
fi

View file

@ -0,0 +1,147 @@
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
# API Keys
## Introduction
Langflow offers an API Key functionality that allows users to access their individual components and flows without going through traditional login authentication. The API Key is a user-specific token that can be included in the request's header or query parameter to authenticate API calls. The following documentation outlines how to generate, use, and manage these API Keys in Langflow.
## Generating an API Key
### Through Langflow UI
{/* add image img/api-key.png */}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/api-key.png"),
}}
style={{ width: "50%", maxWidth: "600px", margin: "0 auto" }}
/>
1. Click on the "API Key" icon.
2. Click on "Create new secret key".
3. Give it an optional name.
4. Click on "Create secret key".
5. Copy the API key and store it in a secure location.
## Using the API Key
### Using the `x-api-key` Header
Include the `x-api-key` in the HTTP header when making API requests:
```bash
curl -X POST \
http://localhost:3000/api/v1/process/<your_flow_id> \
-H 'Content-Type: application/json'\
-H 'x-api-key: <your api key>'\
-d '{"inputs": {"text":""}, "tweaks": {}}'
```
With Python using `requests`:
```python
import requests
from typing import Optional
BASE_API_URL = "http://localhost:3001/api/v1/process"
FLOW_ID = "4441b773-0724-434e-9cee-19d995d8f2df"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = {}
def run_flow(inputs: dict,
flow_id: str,
tweaks: Optional[dict] = None,
apiKey: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {"inputs": inputs}
headers = {}
if tweaks:
payload["tweaks"] = tweaks
if apiKey:
headers = {"x-api-key": apiKey}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
inputs = {"text":""}
api_key = "<your api key>"
print(run_flow(inputs, flow_id=FLOW_ID, tweaks=TWEAKS, apiKey=api_key))
```
### Using the Query Parameter
Alternatively, you can include the API key as a query parameter in the URL:
```bash
curl -X POST \
http://localhost:3000/api/v1/process/<your_flow_id>?x-api-key=<your_api_key> \
-H 'Content-Type: application/json'\
-d '{"inputs": {"text":""}, "tweaks": {}}'
```
Or with Python:
```python
import requests
BASE_API_URL = "http://localhost:3001/api/v1/process"
FLOW_ID = "4441b773-0724-434e-9cee-19d995d8f2df"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = {}
def run_flow(inputs: dict,
flow_id: str,
tweaks: Optional[dict] = None,
apiKey: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {"inputs": inputs}
headers = {}
if tweaks:
payload["tweaks"] = tweaks
if apiKey:
api_url += f"?x-api-key={apiKey}"
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
inputs = {"text":""}
api_key = "<your api key>"
print(run_flow(inputs, flow_id=FLOW_ID, tweaks=TWEAKS, apiKey=api_key))
```
## Security Considerations
- **Visibility**: The API key won't be retrievable again through the UI for security reasons.
- **Scope**: The key only allows access to the flows and components of the specific user to whom it was issued.
## Revoking an API Key
To revoke an API key, simply delete it from the UI. This will immediately invalidate the key and prevent it from being used again.

View file

@ -0,0 +1,128 @@
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
import ReactPlayer from "react-player";
import Admonition from "@theme/Admonition";
# Sign up and Sign in
## Introduction
The login functionality in Langflow serves to authenticate users and protect sensitive routes in the application. Starting from version 0.5, Langflow introduces an enhanced login mechanism that is governed by a few environment variables. This allows new secure features.
## Environment Variables
The following environment variables are crucial in configuring the login settings:
- _`LANGFLOW_AUTO_LOGIN`_: Determines whether Langflow should automatically log users in. Default is `True`.
- _`LANGFLOW_SUPERUSER`_: The username of the superuser.
- _`LANGFLOW_SUPERUSER_PASSWORD`_: The password for the superuser.
- _`LANGFLOW_SECRET_KEY`_: A key used for encrypting the superuser's password.
- _`LANGFLOW_NEW_USER_IS_ACTIVE`_: Determines whether new users are automatically activated. Default is `False`.
All of these variables can be passed to the CLI command _`langflow run`_ through the _`--env-file`_ option. For example:
```bash
langflow run --env-file .env
```
<Admonition type="info">
It is critical not to expose these environment variables in your code
repository. Always set them securely in your deployment environment, for
example, using Docker secrets, Kubernetes ConfigMaps/Secrets, or dedicated
secure environment configuration systems like AWS Secrets Manager.
</Admonition>
### _`LANGFLOW_AUTO_LOGIN`_
By default, this variable is set to `True`. When enabled (`True`), Langflow operates as it did in versions prior to 0.5—automatic login without requiring explicit user authentication.
To disable automatic login and enforce user authentication:
```bash
export LANGFLOW_AUTO_LOGIN=False
```
### _`LANGFLOW_SUPERUSER`_ and _`LANGFLOW_SUPERUSER_PASSWORD`_
These environment variables are only relevant when `LANGFLOW_AUTO_LOGIN` is set to `False`. They specify the username and password for the superuser, which is essential for administrative tasks.
To create a superuser manually:
```bash
export LANGFLOW_SUPERUSER=admin
export LANGFLOW_SUPERUSER_PASSWORD=securepassword
```
You can also use the CLI command `langflow superuser` to set up a superuser interactively.
### _`LANGFLOW_SECRET_KEY`_
This environment variable holds a secret key used for encrypting the superuser's password. Make sure to set this to a secure, randomly generated string.
```bash
export LANGFLOW_SECRET_KEY=randomly_generated_secure_key
```
### _`LANGFLOW_NEW_USER_IS_ACTIVE`_
By default, this variable is set to `False`. When enabled (`True`), new users are automatically activated and can log in without requiring explicit activation by the superuser.
## Command-Line Interface
Langflow provides a command-line utility for managing superusers:
```bash
langflow superuser
```
This command prompts you to enter the username and password for the superuser, unless they are already set using environment variables.
## Sign-up
With _`LANGFLOW_AUTO_LOGIN`_ set to _`False`_, Langflow requires users to sign up before they can log in. The sign-up page is the default landing page when a user visits Langflow for the first time.
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/sign-up.png"),
}}
style={{ width: "50%", maxWidth: "600px", margin: "0 auto" }}
/>
## Profile settings
Users can change their profile settings by clicking on the profile icon in the top right corner of the application. This opens a dropdown menu with the following options:
- **Admin Page**: Opens the admin page, which is only accessible to the superuser.
- **Profile Settings**: Opens the profile settings page.
- **Sign Out**: Logs the user out.
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/my-account.png"),
}}
style={{ width: "50%", maxWidth: "600px", margin: "0 auto" }}
/>
By clicking on **Profile Settings**, the user is taken to the profile settings page, where they can change their password and their profile picture.
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/profile-settings.png"),
}}
style={{ maxWidth: "600px", margin: "0 auto" }}
/>
By clicking on **Admin Page**, the superuser is taken to the admin page, where they can manage users and groups.
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/admin-page.png"),
}}
style={{ maxWidth: "600px", margin: "0 auto" }}
/>

View file

@ -0,0 +1,7 @@
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
import ReactPlayer from "react-player";
Now, we need to explain what are the permissions the superuser gets. Once logged in, they can activate new users,
edit them,

View file

@ -16,6 +16,8 @@ module.exports = {
label: "Guidelines",
collapsed: false,
items: [
"guidelines/login",
"guidelines/api",
"guidelines/components",
"guidelines/features",
"guidelines/collection",

BIN
docs/static/img/admin-page.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
docs/static/img/api-key.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
docs/static/img/my-account.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/static/img/profile-settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

BIN
docs/static/img/sign-up.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

1377
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,6 @@ documentation = "https://docs.langflow.org"
langflow = "langflow.__main__:main"
[tool.poetry.dependencies]
pandas = "^2.0.0"
python = ">=3.9,<3.11"
fastapi = "^0.100.0"
uvicorn = "^0.22.0"
@ -36,7 +35,8 @@ typer = "^0.9.0"
gunicorn = "^21.2.0"
langchain = "^0.0.274"
openai = "^0.27.8"
chromadb = "^0.3.0"
pandas = "2.0.3"
chromadb = "^0.3.21"
huggingface-hub = { version = "^0.16.0", extras = ["inference"] }
rich = "^13.5.0"
llama-cpp-python = { version = "~0.1.0", optional = true }
@ -77,6 +77,9 @@ psycopg = "^3.1.9"
psycopg-binary = "^3.1.9"
fastavro = "^1.8.0"
langchain-experimental = "^0.0.8"
celery = { extras = ["redis"], version = "^5.3.1", optional = true }
redis = { version = "^4.6.0", optional = true }
flower = { version = "^2.0.0", optional = true }
alembic = "^1.11.2"
passlib = "^1.7.4"
bcrypt = "^4.0.1"
@ -89,7 +92,9 @@ pillow = "^10.0.0"
metal-sdk = "^2.0.2"
markupsafe = "^2.1.3"
[tool.poetry.group.dev.dependencies]
types-redis = "^4.6.0.5"
black = "^23.1.0"
ipykernel = "^6.21.2"
mypy = "^1.1.1"
@ -105,6 +110,7 @@ types-appdirs = "^1.4.3.5"
types-pyyaml = "^6.0.12.8"
types-python-jose = "^3.3.4.8"
types-passlib = "^1.7.7.13"
locust = "^2.16.1"
pytest-mock = "^3.11.1"
pytest-xdist = "^3.3.1"
types-pywin32 = "^306.0.0.4"
@ -113,7 +119,7 @@ pytest-sugar = "^0.9.7"
[tool.poetry.extras]
deploy = ["langchain-serve"]
deploy = ["langchain-serve", "celery", "redis", "flower"]
local = ["llama-cpp-python", "sentence-transformers", "ctransformers"]
all = ["deploy", "local"]
@ -125,6 +131,7 @@ testpaths = ["tests", "integration"]
console_output_style = "progress"
filterwarnings = ["ignore::DeprecationWarning"]
log_cli = true
markers = ["async_test"]
[tool.ruff]

View file

@ -1,7 +1,7 @@
from importlib import metadata
# Deactivate cache manager for now
# from langflow.services.cache import cache_manager
# from langflow.services.cache import cache_service
from langflow.processing.process import load_flow_from_json
from langflow.interface.custom.custom_component import CustomComponent
@ -12,4 +12,4 @@ except metadata.PackageNotFoundError:
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = ["load_flow_from_json", "cache_manager", "CustomComponent"]
__all__ = ["load_flow_from_json", "cache_service", "CustomComponent"]

View file

@ -2,9 +2,8 @@ import sys
import time
import httpx
from langflow.services.database.utils import session_getter
from langflow.services.utils import initialize_services
from langflow.services.getters import get_db_manager, get_settings_manager
from langflow.services.utils import initialize_settings_manager
from langflow.services.manager import initialize_services, initialize_settings_service
from langflow.services.getters import get_db_service, get_settings_service
from multiprocess import Process, cpu_count # type: ignore
import platform
@ -64,20 +63,20 @@ def update_settings(
"""Update the settings from a config file."""
# Check for database_url in the environment variables
initialize_settings_manager()
settings_manager = get_settings_manager()
initialize_settings_service()
settings_service = get_settings_service()
if config:
logger.debug(f"Loading settings from {config}")
settings_manager.settings.update_from_yaml(config, dev=dev)
settings_service.settings.update_from_yaml(config, dev=dev)
if remove_api_keys:
logger.debug(f"Setting remove_api_keys to {remove_api_keys}")
settings_manager.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
settings_service.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
if cache:
logger.debug(f"Setting cache to {cache}")
settings_manager.settings.update_settings(CACHE=cache)
settings_service.settings.update_settings(CACHE=cache)
if components_path:
logger.debug(f"Adding component path {components_path}")
settings_manager.settings.update_settings(COMPONENTS_PATH=components_path)
settings_service.settings.update_settings(COMPONENTS_PATH=components_path)
def serve_on_jcloud():
@ -353,8 +352,8 @@ def superuser(
),
):
initialize_services()
db_manager = get_db_manager()
with session_getter(db_manager) as session:
db_service = get_db_service()
with session_getter(db_service) as session:
from langflow.services.auth.utils import create_super_user
if create_super_user(db=session, username=username, password=password):
@ -375,10 +374,10 @@ def superuser(
@app.command()
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
initialize_services()
db_manager = get_db_manager()
db_service = get_db_service()
if not test:
db_manager.run_migrations()
results = db_manager.run_migrations_test()
db_service.run_migrations()
results = db_service.run_migrations_test()
display_results(results)

View file

@ -59,33 +59,6 @@ def build_input_keys_response(langchain_object, artifacts):
return input_keys_response
def merge_nested_dicts(dict1, dict2):
for key, value in dict2.items():
if isinstance(value, dict) and isinstance(dict1.get(key), dict):
dict1[key] = merge_nested_dicts(dict1[key], value)
else:
dict1[key] = value
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) + ")"

View file

@ -14,16 +14,14 @@ from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, St
from langflow.graph.graph.base import Graph
from langflow.services.auth.utils import get_current_active_user, get_current_user
from loguru import logger
from langflow.services.getters import get_chat_manager, get_session
from cachetools import LRUCache
from langflow.services.getters import get_chat_service, get_session, get_cache_service
from sqlmodel import Session
from langflow.services.chat.manager import ChatManager
from langflow.services.chat.manager import ChatService
from langflow.services.cache.manager import BaseCacheService
router = APIRouter(tags=["Chat"])
flow_data_store: LRUCache = LRUCache(maxsize=10)
@router.websocket("/chat/{client_id}")
async def chat(
@ -31,7 +29,7 @@ async def chat(
websocket: WebSocket,
token: str = Query(...),
db: Session = Depends(get_session),
chat_manager: "ChatManager" = Depends(get_chat_manager),
chat_service: "ChatService" = Depends(get_chat_service),
):
"""Websocket endpoint for chat."""
try:
@ -46,15 +44,15 @@ async def chat(
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
)
if client_id in chat_manager.in_memory_cache:
await chat_manager.handle_websocket(client_id, websocket)
if client_id in chat_service.cache_service:
await chat_service.handle_websocket(client_id, websocket)
else:
# We accept the connection but close it immediately
# if the flow is not built yet
message = "Please, build the flow before sending messages"
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message)
except WebSocketException as exc:
logger.error(f"Websocket error: {exc}")
logger.error(f"Websocket exrror: {exc}")
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
except Exception as exc:
logger.error(f"Error in chat websocket: {exc}")
@ -72,26 +70,26 @@ async def init_build(
graph_data: dict,
flow_id: str,
current_user=Depends(get_current_active_user),
chat_manager: "ChatManager" = Depends(get_chat_manager),
chat_service: "ChatService" = Depends(get_chat_service),
cache_service: "BaseCacheService" = Depends(get_cache_service),
):
"""Initialize the build by storing graph data and returning a unique session ID."""
try:
if flow_id is None:
raise ValueError("No ID provided")
# Check if already building
if (
flow_id in flow_data_store
and flow_data_store[flow_id]["status"] == BuildStatus.IN_PROGRESS
flow_id in cache_service
and isinstance(cache_service[flow_id], dict)
and cache_service[flow_id].get("status") == BuildStatus.IN_PROGRESS
):
return InitResponse(flowId=flow_id)
# Delete from cache if already exists
if flow_id in chat_manager.in_memory_cache:
with chat_manager.in_memory_cache._lock:
chat_manager.in_memory_cache.delete(flow_id)
logger.debug(f"Deleted flow {flow_id} from cache")
flow_data_store[flow_id] = {
if flow_id in chat_service.cache_service:
chat_service.cache_service.delete(flow_id)
logger.debug(f"Deleted flow {flow_id} from cache")
cache_service[flow_id] = {
"graph_data": graph_data,
"status": BuildStatus.STARTED,
"user_id": current_user.id,
@ -104,12 +102,14 @@ async def init_build(
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
async def build_status(flow_id: str):
"""Check the flow_id is in the flow_data_store."""
async def build_status(
flow_id: str, cache_service: "BaseCacheService" = Depends(get_cache_service)
):
"""Check the flow_id is in the cache_service."""
try:
built = (
flow_id in flow_data_store
and flow_data_store[flow_id]["status"] == BuildStatus.SUCCESS
flow_id in cache_service
and cache_service[flow_id]["status"] == BuildStatus.SUCCESS
)
return BuiltResponse(
@ -123,7 +123,9 @@ async def build_status(flow_id: str):
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
async def stream_build(
flow_id: str, chat_manager: "ChatManager" = Depends(get_chat_manager)
flow_id: str,
chat_service: "ChatService" = Depends(get_chat_service),
cache_service: "BaseCacheService" = Depends(get_cache_service),
):
"""Stream the build process based on stored flow data."""
@ -131,18 +133,18 @@ async def stream_build(
final_response = {"end_of_stream": True}
artifacts = {}
try:
if flow_id not in flow_data_store:
if flow_id not in cache_service:
error_message = "Invalid session ID"
yield str(StreamData(event="error", data={"error": error_message}))
return
if flow_data_store[flow_id].get("status") == BuildStatus.IN_PROGRESS:
if cache_service[flow_id].get("status") == BuildStatus.IN_PROGRESS:
error_message = "Already building"
yield str(StreamData(event="error", data={"error": error_message}))
return
graph_data = flow_data_store[flow_id].get("graph_data")
user_id = flow_data_store[flow_id]["user_id"]
graph_data = cache_service[flow_id].get("graph_data")
cache_service[flow_id]["user_id"]
if not graph_data:
error_message = "No data provided"
@ -155,7 +157,7 @@ async def stream_build(
graph = Graph.from_payload(graph_data)
number_of_nodes = len(graph.nodes)
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
cache_service[flow_id]["status"] = BuildStatus.IN_PROGRESS
for i, vertex in enumerate(graph.generator_build(), 1):
try:
@ -163,7 +165,10 @@ async def stream_build(
"log": f"Building node {vertex.vertex_type}",
}
yield str(StreamData(event="log", data=log_dict))
vertex.build(user_id)
if vertex.is_task:
vertex = try_running_celery_task(vertex)
else:
vertex.build()
params = vertex._built_object_repr()
valid = True
logger.debug(f"Building node {str(vertex.vertex_type)}")
@ -179,7 +184,7 @@ async def stream_build(
logger.exception(exc)
params = str(exc)
valid = False
flow_data_store[flow_id]["status"] = BuildStatus.FAILURE
cache_service[flow_id]["status"] = BuildStatus.FAILURE
response = {
"valid": valid,
@ -203,14 +208,14 @@ async def stream_build(
"handle_keys": [],
}
yield str(StreamData(event="message", data=input_keys_response))
chat_manager.set_cache(flow_id, langchain_object)
chat_service.set_cache(flow_id, langchain_object)
# We need to reset the chat history
chat_manager.chat_history.empty_history(flow_id)
flow_data_store[flow_id]["status"] = BuildStatus.SUCCESS
chat_service.chat_history.empty_history(flow_id)
cache_service[flow_id]["status"] = BuildStatus.SUCCESS
except Exception as exc:
logger.exception(exc)
logger.error("Error while building the flow: %s", exc)
flow_data_store[flow_id]["status"] = BuildStatus.FAILURE
cache_service[flow_id]["status"] = BuildStatus.FAILURE
yield str(StreamData(event="error", data={"error": str(exc)}))
finally:
yield str(StreamData(event="message", data=final_response))
@ -220,3 +225,20 @@ async def stream_build(
except Exception as exc:
logger.error(f"Error streaming build: {exc}")
raise HTTPException(status_code=500, detail=str(exc))
def try_running_celery_task(vertex):
# Try running the task in celery
# and set the task_id to the local vertex
# if it fails, run the task locally
try:
from langflow.worker import build_vertex
task = build_vertex.delay(vertex)
vertex.task_id = task.id
except Exception as exc:
logger.exception(exc)
logger.error("Error running task in celery, running locally")
vertex.task_id = None
vertex.build()
return vertex

View file

@ -1,12 +1,17 @@
from http import HTTPStatus
from typing import Annotated, Any, Optional, Union
from typing import Annotated, Optional, Union
from langflow.services.auth.utils import api_key_security, get_current_active_user
from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
from langflow.processing.process import process_graph_cached, process_tweaks
from langflow.services.database.models.user.user import User
from langflow.services.getters import get_settings_manager
from langflow.services.getters import (
get_session_service,
get_settings_service,
get_task_service,
)
from loguru import logger
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status
import sqlalchemy as sa
@ -15,66 +20,42 @@ from langflow.interface.custom.custom_component import CustomComponent
from langflow.api.v1.schemas import (
ProcessResponse,
TaskStatusResponse,
UploadFileResponse,
CustomComponentCode,
)
from langflow.api.utils import merge_nested_dicts_with_renaming
from langflow.interface.types import (
build_langchain_types_dict,
build_langchain_template_custom_component,
build_langchain_custom_component_list_from_path,
)
from langflow.services.getters import get_session
try:
from langflow.worker import process_graph_cached_task
except ImportError:
def process_graph_cached_task(*args, **kwargs):
raise NotImplementedError("Celery is not installed")
from sqlmodel import Session
from langflow.services.task.manager import TaskService
# build router
router = APIRouter(tags=["Base"])
@router.get("/all", dependencies=[Depends(get_current_active_user)])
def get_all(
settings_manager=Depends(get_settings_manager),
settings_service=Depends(get_settings_service),
):
from langflow.interface.types import get_all_types_dict
logger.debug("Building langchain types dict")
native_components = build_langchain_types_dict()
# custom_components is a list of dicts
# need to merge all the keys into one dict
custom_components_from_file: dict[str, Any] = {}
if settings_manager.settings.COMPONENTS_PATH:
logger.info(
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
)
custom_component_dicts = []
processed_paths = []
for path in settings_manager.settings.COMPONENTS_PATH:
if str(path) in processed_paths:
continue
custom_component_dict = build_langchain_custom_component_list_from_path(
str(path)
)
custom_component_dicts.append(custom_component_dict)
processed_paths.append(str(path))
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
for custom_component_dict in custom_component_dicts:
# custom_component_dict is a dict of dicts
if not custom_component_dict:
continue
category = list(custom_component_dict.keys())[0]
logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict
)
return merge_nested_dicts_with_renaming(
native_components, custom_components_from_file
)
try:
return get_all_types_dict(settings_service)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
# For backwards compatibility we will keep the old endpoint
@ -94,7 +75,9 @@ async def process_flow(
tweaks: Optional[dict] = None,
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
task_service: "TaskService" = Depends(get_task_service),
api_key_user: User = Depends(api_key_security),
sync: Annotated[bool, Body(embed=True)] = True, # noqa: F821
):
"""
Endpoint to process an input with a given flow_id.
@ -125,10 +108,49 @@ async def process_flow(
graph_data = process_tweaks(graph_data, tweaks)
except Exception as exc:
logger.error(f"Error processing tweaks: {exc}")
response, session_id = process_graph_cached(
graph_data, inputs, clear_cache, session_id
if sync:
task_id, result = await task_service.launch_and_await_task(
process_graph_cached_task
if task_service.use_celery
else process_graph_cached,
graph_data,
inputs,
clear_cache,
session_id,
)
if isinstance(result, dict) and "result" in result:
task_result = result["result"]
session_id = result["session_id"]
elif hasattr(result, "result") and hasattr(result, "session_id"):
task_result = result.result
session_id = result.session_id
else:
logger.warning(
"This is an experimental feature and may not work as expected."
"Please report any issues to our GitHub repository."
)
if session_id is None:
# Generate a session ID
session_id = get_session_service().generate_key(
session_id=session_id, data_graph=graph_data
)
task_id, task = await task_service.launch_task(
process_graph_cached_task
if task_service.use_celery
else process_graph_cached,
graph_data,
inputs,
clear_cache,
session_id,
)
task_result = task.status
return ProcessResponse(
result=task_result,
id=task_id,
session_id=session_id,
backend=str(type(task_service.backend)),
)
return ProcessResponse(result=response, session_id=session_id)
except sa.exc.StatementError as exc:
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc):
@ -151,6 +173,23 @@ async def process_flow(
raise HTTPException(status_code=500, detail=str(e)) from e
@router.get("/task/{task_id}/status", response_model=TaskStatusResponse)
async def get_task_status(task_id: str):
task_service = get_task_service()
task = task_service.get_task(task_id)
result = None
if task.ready():
result = task.result
if isinstance(result, dict) and "result" in result:
result = result["result"]
elif hasattr(result, "result"):
result = result.result
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return TaskStatusResponse(status=task.status, result=result)
@router.post(
"/upload/{flow_id}",
response_model=UploadFileResponse,
@ -182,6 +221,10 @@ def get_version():
async def custom_component(
raw_code: CustomComponentCode,
):
from langflow.interface.types import (
build_langchain_template_custom_component,
)
extractor = CustomComponent(code=raw_code.code)
extractor.is_check_valid()

View file

@ -13,7 +13,7 @@ from langflow.services.database.models.flow import (
)
from langflow.services.database.models.user.user import User
from langflow.services.getters import get_session
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
import orjson
from sqlmodel import Session
from fastapi import APIRouter, Depends, HTTPException
@ -83,7 +83,7 @@ def update_flow(
flow_id: UUID,
flow: FlowUpdate,
current_user: User = Depends(get_current_active_user),
settings_manager=Depends(get_settings_manager),
settings_service=Depends(get_settings_service),
):
"""Update a flow."""
@ -91,7 +91,7 @@ def update_flow(
if not db_flow:
raise HTTPException(status_code=404, detail="Flow not found")
flow_data = flow.dict(exclude_unset=True)
if settings_manager.settings.REMOVE_API_KEYS:
if settings_service.settings.REMOVE_API_KEYS:
flow_data = remove_api_keys(flow_data)
for key, value in flow_data.items():
if value is not None:

View file

@ -12,7 +12,7 @@ from langflow.services.auth.utils import (
get_current_active_user,
)
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
router = APIRouter(tags=["Login"])
@ -23,7 +23,17 @@ async def login_to_get_access_token(
db: Session = Depends(get_session),
# _: Session = Depends(get_current_active_user)
):
if user := authenticate_user(form_data.username, form_data.password, db):
try:
user = authenticate_user(form_data.username, form_data.password, db)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(exc),
) from exc
if user:
return create_user_tokens(user_id=user.id, db=db, update_last_login=True)
else:
raise HTTPException(
@ -35,9 +45,9 @@ async def login_to_get_access_token(
@router.get("/auto_login")
async def auto_login(
db: Session = Depends(get_session), settings_manager=Depends(get_settings_manager)
db: Session = Depends(get_session), settings_service=Depends(get_settings_service)
):
if settings_manager.auth_settings.AUTO_LOGIN:
if settings_service.auth_settings.AUTO_LOGIN:
return create_user_longterm_token(db)
raise HTTPException(

View file

@ -50,8 +50,20 @@ class UpdateTemplateRequest(BaseModel):
class ProcessResponse(BaseModel):
"""Process response schema."""
result: dict
result: Any
id: Optional[str] = None
session_id: Optional[str] = None
backend: Optional[str] = None
# TaskStatusResponse(
# status=task.status, result=task.result if task.ready() else None
# )
class TaskStatusResponse(BaseModel):
"""Task status response schema."""
status: str
result: Optional[Any] = None
class ChatMessage(BaseModel):

View file

@ -13,7 +13,7 @@ from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from fastapi import APIRouter, Depends, HTTPException
from langflow.services.getters import get_session
from langflow.services.getters import get_session, get_settings_service
from langflow.services.auth.utils import (
get_current_active_superuser,
get_current_active_user,
@ -32,6 +32,7 @@ router = APIRouter(tags=["Users"], prefix="/users")
def add_user(
user: UserCreate,
session: Session = Depends(get_session),
settings_service=Depends(get_settings_service),
) -> User:
"""
Add a new user to the database.
@ -39,7 +40,7 @@ def add_user(
new_user = User.from_orm(user)
try:
new_user.password = get_password_hash(user.password)
new_user.is_active = settings_service.auth_settings.NEW_USER_IS_ACTIVE
session.add(new_user)
session.commit()
session.refresh(new_user)
@ -66,7 +67,7 @@ def read_current_user(
def read_all_users(
skip: int = 0,
limit: int = 10,
current_user: Session = Depends(get_current_active_superuser),
_: Session = Depends(get_current_active_superuser),
session: Session = Depends(get_session),
) -> UsersResponse:
"""
@ -99,9 +100,11 @@ def patch_user(
status_code=403, detail="You don't have the permission to update this user"
)
if user_update.password:
raise HTTPException(
status_code=400, detail="You can't change your password here"
)
if not user.is_superuser:
raise HTTPException(
status_code=400, detail="You can't change your password here"
)
user_update.password = get_password_hash(user_update.password)
if user_db := get_user_by_id(session, user_id):
return update_user(user_db, user_update, session)
@ -164,31 +167,3 @@ def delete_user(
session.commit()
return {"detail": "User deleted"}
# TODO: REMOVE - Just for testing purposes
@router.post("/super_user", response_model=User)
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
session: Session = Depends(get_session),
) -> User:
"""
Add a superuser for testing purposes.
(This should be removed in production)
"""
new_user = User(
username="superuser",
password=get_password_hash("12345"),
is_active=True,
is_superuser=True,
last_login_at=None,
)
try:
session.add(new_user)
session.commit()
session.refresh(new_user)
except IntegrityError as e:
session.rollback()
raise HTTPException(status_code=400, detail="User exists") from e
return new_user

View file

View file

@ -0,0 +1,11 @@
from celery import Celery # type: ignore
def make_celery(app_name: str, config: str) -> Celery:
celery_app = Celery(app_name)
celery_app.config_from_object(config)
celery_app.conf.task_routes = {"langflow.worker.tasks.*": {"queue": "langflow"}}
return celery_app
celery_app = make_celery("langflow", "langflow.core.celeryconfig")

View file

@ -0,0 +1,14 @@
# celeryconfig.py
import os
langflow_redis_host = os.environ.get("LANGFLOW_REDIS_HOST")
langflow_redis_port = os.environ.get("LANGFLOW_REDIS_PORT")
if "BROKER_URL" in os.environ and "RESULT_BACKEND" in os.environ:
# RabbitMQ
broker_url = os.environ.get("BROKER_URL", "amqp://localhost")
result_backend = os.environ.get("RESULT_BACKEND", "redis://localhost:6379/0")
elif langflow_redis_host and langflow_redis_port:
broker_url = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
result_backend = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
# tasks should be json or pickle
accept_content = ["json", "pickle"]

View file

@ -17,6 +17,17 @@ class Edge:
self.validate_edge()
def __setstate__(self, state):
self.source = state["source"]
self.target = state["target"]
self.target_param = state["target_param"]
self.source_handle = state["source_handle"]
self.target_handle = state["target_handle"]
def reset(self) -> None:
self.source._build_params()
self.target._build_params()
def validate_edge(self) -> None:
# Validate that the outputs of the source node are valid inputs
# for the target node

View file

@ -26,6 +26,12 @@ class Graph:
self._edges = edges
self._build_graph()
def __setstate__(self, state):
self.__dict__.update(state)
for edge in self.edges:
edge.reset()
edge.validate_edge()
@classmethod
def from_payload(cls, payload: Dict) -> "Graph":
"""
@ -48,6 +54,11 @@ class Graph:
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
) from exc
def __eq__(self, other: object) -> bool:
if not isinstance(other, Graph):
return False
return self.__repr__() == other.__repr__()
def _build_graph(self) -> None:
"""Builds the graph from the nodes and edges."""
self.nodes = self._build_vertices()
@ -147,7 +158,7 @@ class Graph:
def generator_build(self) -> Generator[Vertex, None, None]:
"""Builds each vertex in the graph and yields it."""
sorted_vertices = self.topological_sort()
logger.debug("Sorted vertices: %s", sorted_vertices)
logger.debug("There are %s vertices in the graph", len(sorted_vertices))
yield from sorted_vertices
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:

View file

@ -1,5 +1,7 @@
import ast
import pickle
from langflow.graph.utils import UnbuiltObject
from langflow.graph.vertex.utils import is_basic_type
from langflow.interface.initialize import loading
from langflow.interface.listing import lazy_load_dict
from langflow.utils.constants import DIRECT_TYPES
@ -12,12 +14,19 @@ import types
from typing import Any, Dict, List, Optional
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from langflow.graph.edge.base import Edge
class Vertex:
def __init__(self, data: Dict, base_type: Optional[str] = None) -> None:
def __init__(
self,
data: Dict,
base_type: Optional[str] = None,
is_task: bool = False,
params: Optional[Dict] = None,
) -> None:
self.id: str = data["id"]
self._data = data
self.edges: List["Edge"] = []
@ -26,6 +35,59 @@ class Vertex:
self._built_object = UnbuiltObject()
self._built = False
self.artifacts: Dict[str, Any] = {}
self.task_id: Optional[str] = None
self.is_task = is_task
self.params = params or {}
def reset_params(self):
for edge in self.edges:
if edge.source != self:
target_param = edge.target_param
if target_param in ["document", "texts"]:
# this means they got data and have already ingested it
# so we continue after removing the param
self.params.pop(target_param, None)
continue
if target_param in self.params and not is_basic_type(
self.params[target_param]
):
# edge.source.params = {}
edge.source._build_params()
edge.source._built_object = UnbuiltObject()
edge.source._built = False
self.params[target_param] = edge.source
def __getstate__(self):
state_dict = self.__dict__.copy()
try:
# try pickling the built object
# if it fails, then we need to delete it
# and build it again
pickle.dumps(state_dict["_built_object"])
except Exception:
self.reset_params()
del state_dict["_built_object"]
del state_dict["_built"]
return state_dict
def __setstate__(self, state):
self._data = state["_data"]
self.params = state["params"]
self.base_type = state["base_type"]
self.is_task = state["is_task"]
self.edges = state["edges"]
self.id = state["id"]
self._parse_data()
if "_built_object" in state:
self._built_object = state["_built_object"]
self._built = state["_built"]
else:
self._built_object = UnbuiltObject()
self._built = False
self.artifacts: Dict[str, Any] = {}
self.task_id: Optional[str] = None
def _parse_data(self) -> None:
self.data = self._data["data"]
@ -68,6 +130,13 @@ class Vertex:
self.base_type = base_type
break
def get_task(self):
# using the task_id, get the task from celery
# and return it
from celery.result import AsyncResult # type: ignore
return AsyncResult(self.task_id)
def _build_params(self):
# sourcery skip: merge-list-append, remove-redundant-if
# Some params are required, some are optional
@ -89,9 +158,11 @@ class Vertex:
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
params = {}
params = self.params.copy() if self.params else {}
for edge in self.edges:
if not hasattr(edge, "target_param"):
continue
param_key = edge.target_param
if param_key in template_dict:
if template_dict[param_key]["list"]:
@ -102,6 +173,8 @@ class Vertex:
params[param_key] = edge.source
for key, value in template_dict.items():
if key in params:
continue
# 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"):
@ -144,6 +217,7 @@ class Vertex:
else:
params.pop(key, None)
# Add _type to params
self._raw_params = params
self.params = params
def _build(self, user_id=None):
@ -151,13 +225,13 @@ class Vertex:
Initiate the build process.
"""
logger.debug(f"Building {self.vertex_type}")
self._build_each_node_in_params_dict()
self._build_each_node_in_params_dict(user_id)
self._get_and_instantiate_class(user_id)
self._validate_built_object()
self._built = True
def _build_each_node_in_params_dict(self):
def _build_each_node_in_params_dict(self, user_id=None):
"""
Iterates over each node in the params dictionary and builds it.
"""
@ -166,9 +240,9 @@ class Vertex:
if value == self:
del self.params[key]
continue
self._build_node_and_update_params(key, value)
self._build_node_and_update_params(key, value, user_id)
elif isinstance(value, list) and self._is_list_of_nodes(value):
self._build_list_of_nodes_and_update_params(key, value)
self._build_list_of_nodes_and_update_params(key, value, user_id)
def _is_node(self, value):
"""
@ -182,11 +256,31 @@ class Vertex:
"""
return all(self._is_node(node) for node in value)
def get_result(self, user_id=None, timeout=None) -> Any:
# Check if the Vertex was built already
if self._built:
return self._built_object
if self.is_task and self.task_id is not None:
task = self.get_task()
result = task.get(timeout=timeout)
if result is not None: # If result is ready
self._update_built_object_and_artifacts(result)
return self._built_object
else:
# Handle the case when the result is not ready (retry, throw exception, etc.)
pass
# If there's no task_id, build the vertex locally
self.build(user_id)
return self._built_object
def _build_node_and_update_params(self, key, node, user_id=None):
"""
Builds a given node and updates the params dictionary accordingly.
"""
result = node.build(user_id)
result = node.get_result(user_id)
self._handle_func(key, result)
if isinstance(result, list):
self._extend_params_list_with_result(key, result)
@ -200,7 +294,7 @@ class Vertex:
"""
self.params[key] = []
for node in nodes:
built = node.build(user_id)
built = node.get_result(user_id)
if isinstance(built, list):
if key not in self.params:
self.params[key] = []
@ -245,6 +339,7 @@ class Vertex:
)
self._update_built_object_and_artifacts(result)
except Exception as exc:
logger.exception(exc)
raise ValueError(
f"Error building node {self.vertex_type}: {str(exc)}"
) from exc
@ -285,7 +380,10 @@ class Vertex:
return f"Vertex(id={self.id}, data={self.data})"
def __eq__(self, __o: object) -> bool:
return self.id == __o.id if isinstance(__o, Vertex) else False
try:
return self.id == __o.id if isinstance(__o, Vertex) else False
except AttributeError:
return False
def __hash__(self) -> int:
return id(self)

View file

@ -7,14 +7,27 @@ from langflow.interface.utils import extract_input_variables_from_prompt
class AgentVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="agents")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="agents", params=params)
self.tools: List[Union[ToolkitVertex, ToolVertex]] = []
self.chains: List[ChainVertex] = []
def __getstate__(self):
state = super().__getstate__()
state["tools"] = self.tools
state["chains"] = self.chains
return state
def __setstate__(self, state):
self.tools = state["tools"]
self.chains = state["chains"]
super().__setstate__(state)
def _set_tools_and_chains(self) -> None:
for edge in self.edges:
if not hasattr(edge, "source"):
continue
source_node = edge.source
if isinstance(source_node, (ToolVertex, ToolkitVertex)):
self.tools.append(source_node)
@ -38,16 +51,16 @@ class AgentVertex(Vertex):
class ToolVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="tools")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="tools", params=params)
class LLMVertex(Vertex):
built_node_type = None
class_built_object = None
def __init__(self, data: Dict):
super().__init__(data, base_type="llms")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="llms", params=params)
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
# LLM is different because some models might take up too much memory
@ -64,13 +77,13 @@ class LLMVertex(Vertex):
class ToolkitVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="toolkits")
def __init__(self, data: Dict, params=None):
super().__init__(data, base_type="toolkits", params=params)
class FileToolVertex(ToolVertex):
def __init__(self, data: Dict):
super().__init__(data)
def __init__(self, data: Dict, params=None):
super().__init__(data, params=params)
class WrapperVertex(Vertex):
@ -86,17 +99,19 @@ class WrapperVertex(Vertex):
class DocumentLoaderVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="documentloaders")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="documentloaders", params=params)
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
# show how many documents are in the list?
if self._built_object:
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
self._built_object
)
avg_length = sum(
len(doc.page_content)
for doc in self._built_object
if hasattr(doc, "page_content")
) / len(self._built_object)
return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {int(avg_length)}
Documents: {self._built_object[:3]}..."""
@ -104,14 +119,51 @@ class DocumentLoaderVertex(Vertex):
class EmbeddingVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="embeddings")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="embeddings", params=params)
class VectorStoreVertex(Vertex):
def __init__(self, data: Dict):
def __init__(self, data: Dict, params=None):
super().__init__(data, base_type="vectorstores")
self.params = params or {}
# VectorStores may contain databse connections
# so we need to define the __reduce__ method and the __setstate__ method
# to avoid pickling errors
def clean_edges_for_pickling(self):
# for each edge that has self as source
# we need to clear the _built_object of the target
# so that we don't try to pickle a database connection
for edge in self.edges:
if edge.source == self:
edge.target._built_object = None
edge.target._built = False
edge.target.params[edge.target_param] = self
def remove_docs_and_texts_from_params(self):
# remove documents and texts from params
# so that we don't try to pickle a database connection
self.params.pop("documents", None)
self.params.pop("texts", None)
def __getstate__(self):
# We want to save the params attribute
# and if "documents" or "texts" are in the params
# we want to remove them because they have already
# been processed.
params = self.params.copy()
params.pop("documents", None)
params.pop("texts", None)
self.clean_edges_for_pickling()
return super().__getstate__()
def __setstate__(self, state):
super().__setstate__(state)
self.remove_docs_and_texts_from_params()
class MemoryVertex(Vertex):
def __init__(self, data: Dict):
@ -124,8 +176,8 @@ class RetrieverVertex(Vertex):
class TextSplitterVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="textsplitters")
def __init__(self, data: Dict, params: Optional[Dict] = None):
super().__init__(data, base_type="textsplitters", params=params)
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
@ -211,7 +263,7 @@ class PromptVertex(Vertex):
self.params["input_variables"] = list(
set(self.params["input_variables"])
)
else:
elif isinstance(self.params, dict):
self.params.pop("input_variables", None)
self._build(user_id=user_id)
@ -258,8 +310,13 @@ class OutputParserVertex(Vertex):
class CustomComponentVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="custom_components")
super().__init__(data, base_type="custom_components", is_task=True)
def _built_object_repr(self):
if self.task_id and self.is_task:
if task := self.get_task():
return str(task.info)
else:
return f"Task {self.task_id} is not running"
if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr()

View file

@ -0,0 +1,5 @@
from langflow.utils.constants import PYTHON_BASIC_TYPES
def is_basic_type(obj):
return type(obj) in PYTHON_BASIC_TYPES

View file

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

View file

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Type, Union
from langchain.chains.base import Chain
from langchain.agents import AgentExecutor
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
from pydantic import BaseModel
from langflow.template.field.base import TemplateField
@ -27,11 +27,11 @@ class LangChainTypeCreator(BaseModel, ABC):
@property
def docs_map(self) -> Dict[str, str]:
"""A dict with the name of the component as key and the documentation link as value."""
settings_manager = get_settings_manager()
settings_service = get_settings_service()
if self.name_docs_dict is None:
try:
type_settings = getattr(
settings_manager.settings, self.type_name.upper()
settings_service.settings, self.type_name.upper()
)
self.name_docs_dict = {
name: value_dict["documentation"]

View file

@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Type
from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
from langflow.template.frontend_node.chains import ChainFrontendNode
from loguru import logger
@ -31,7 +31,7 @@ class ChainCreator(LangChainTypeCreator):
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
settings_manager = get_settings_manager()
settings_service = get_settings_service()
self.type_dict: dict[str, Any] = {
chain_name: import_class(f"langchain.chains.{chain_name}")
for chain_name in chains.__all__
@ -45,8 +45,8 @@ class ChainCreator(LangChainTypeCreator):
self.type_dict = {
name: chain
for name, chain in self.type_dict.items()
if name in settings_manager.settings.CHAINS
or settings_manager.settings.DEV
if name in settings_service.settings.CHAINS
or settings_service.settings.DEV
}
return self.type_dict

View file

@ -4,7 +4,7 @@ from fastapi import HTTPException
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component
from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.services.getters import get_db_manager
from langflow.services.getters import get_db_service
from langflow.interface.custom.utils import extract_inner_type
from langflow.utils import validate
@ -176,25 +176,25 @@ class CustomComponent(Component, extra=Extra.allow):
return validate.create_function(self.code, self.function_entrypoint_name)
def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
from langflow.processing.process import build_sorted_vertices_with_caching
from langflow.processing.process import build_sorted_vertices
from langflow.processing.process import process_tweaks
db_manager = get_db_manager()
with session_getter(db_manager) as session:
db_service = get_db_service()
with session_getter(db_service) as session:
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
if not graph_data:
raise ValueError(f"Flow {flow_id} not found")
if tweaks:
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
return build_sorted_vertices_with_caching(graph_data)
return build_sorted_vertices(graph_data)
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
if not self.user_id:
raise ValueError("Session is invalid")
try:
get_session = get_session or session_getter
db_manager = get_db_manager()
with get_session(db_manager) as session:
db_service = get_db_service()
with get_session(db_service) as session:
flows = session.query(Flow).filter(Flow.user_id == self.user_id).all()
return flows
except Exception as e:
@ -209,8 +209,8 @@ class CustomComponent(Component, extra=Extra.allow):
get_session: Optional[Callable] = None,
) -> Flow:
get_session = get_session or session_getter
db_manager = get_db_manager()
with get_session(db_manager) as session:
db_service = get_db_service()
with get_session(db_service) as session:
if flow_id:
flow = session.query(Flow).get(flow_id)
elif flow_name:

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import json
import orjson
from typing import Any, Callable, Dict, Sequence, Type, TYPE_CHECKING
from langchain.schema import Document
from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.base import BaseToolkit
@ -40,12 +40,23 @@ if TYPE_CHECKING:
from langflow import CustomComponent
def build_vertex_in_params(params: Dict) -> Dict:
from langflow.graph.vertex.base import Vertex
# If any of the values in params is a Vertex, we will build it
return {
key: value.build() if isinstance(value, Vertex) else value
for key, value in params.items()
}
def instantiate_class(
node_type: str, base_type: str, params: Dict, user_id=None
) -> Any:
"""Instantiate class from module type and key, and params"""
params = convert_params_to_sets(params)
params = convert_kwargs(params)
if node_type in CUSTOM_NODES:
if custom_node := CUSTOM_NODES.get(node_type):
if hasattr(custom_node, "initialize"):
@ -289,6 +300,13 @@ def instantiate_embedding(node_type, class_object, params: Dict):
def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict):
search_kwargs = params.pop("search_kwargs", {})
# clean up docs or texts to have only documents
if "texts" in params:
params["documents"] = params.pop("texts")
if "documents" in params:
params["documents"] = [
doc for doc in params["documents"] if isinstance(doc, Document)
]
if initializer := vecstore_initializer.get(class_object.__name__):
vecstore = initializer(class_object, params)
else:

View file

@ -8,7 +8,7 @@ from langchain.vectorstores import (
SupabaseVectorStore,
MongoDBAtlasVectorSearch,
)
from langchain.schema import Document
import os
import orjson
@ -201,11 +201,16 @@ def initialize_chroma(class_object: Type[Chroma], params: dict):
if "texts" in params:
params["documents"] = params.pop("texts")
for doc in params["documents"]:
if not isinstance(doc, Document):
# remove any non-Document objects from the list
params["documents"].remove(doc)
continue
if doc.metadata is None:
doc.metadata = {}
for key, value in doc.metadata.items():
if value is None:
doc.metadata[key] = ""
chromadb = class_object.from_documents(**params)
if persist:
chromadb.persist()

View file

@ -1,19 +1,4 @@
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.llms.base import llm_creator
from langflow.interface.memories.base import memory_creator
from langflow.interface.prompts.base import prompt_creator
from langflow.interface.text_splitters.base import textsplitter_creator
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.tools.base import tool_creator
from langflow.interface.utilities.base import utility_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.custom.base import custom_component_creator
from langflow.services.getters import get_settings_service
from langflow.utils.lazy_load import LazyLoadDictBase
@ -33,24 +18,10 @@ class AllTypesDict(LazyLoadDictBase):
}
def get_type_dict(self):
return {
"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(),
}
from langflow.interface.types import get_all_types_dict
settings_service = get_settings_service()
return get_all_types_dict(settings_service=settings_service)
lazy_load_dict = AllTypesDict()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,22 +1,9 @@
from typing import Any, Dict, Tuple
from langflow.services.cache.utils import memoize_dict
from typing import Dict, Tuple
from langflow.graph import Graph
from loguru import logger
@memoize_dict(maxsize=10)
def build_langchain_object_with_caching(data_graph):
"""
Build langchain object from data_graph.
"""
logger.debug("Building langchain object")
graph = Graph.from_payload(data_graph)
return graph.build()
@memoize_dict(maxsize=10)
def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
def build_sorted_vertices(data_graph) -> Tuple[Graph, Dict]:
"""
Build langchain object from data_graph.
"""
@ -29,7 +16,7 @@ def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
vertex.build()
if vertex.artifacts:
artifacts.update(vertex.artifacts)
return graph.build(), artifacts
return graph, artifacts
def build_langchain_object(data_graph):

View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import textwrap
from typing import Dict, Union
from langchain.agents.tools import Tool
from loguru import logger
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:

View file

@ -1,7 +1,7 @@
import ast
import contextlib
from typing import Any, List
from langflow.api.utils import merge_nested_dicts_with_renaming
from langflow.api.utils import get_new_key
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
@ -433,6 +433,24 @@ def build_invalid_menu(invalid_components):
return invalid_menu
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 build_langchain_custom_component_list_from_path(path: str):
"""Build a list of custom components for the langchain from a given path"""
file_list = load_files_from_path(path)
@ -446,3 +464,51 @@ def build_langchain_custom_component_list_from_path(path: str):
invalid_menu = build_invalid_menu(invalid_components)
return merge_nested_dicts_with_renaming(valid_menu, invalid_menu)
def get_all_types_dict(settings_service):
native_components = build_langchain_types_dict()
# custom_components is a list of dicts
# need to merge all the keys into one dict
custom_components_from_file: dict[str, Any] = {}
if settings_service.settings.COMPONENTS_PATH:
logger.info(
f"Building custom components from {settings_service.settings.COMPONENTS_PATH}"
)
custom_component_dicts = []
processed_paths = []
for path in settings_service.settings.COMPONENTS_PATH:
if str(path) in processed_paths:
continue
custom_component_dict = build_langchain_custom_component_list_from_path(
str(path)
)
custom_component_dicts.append(custom_component_dict)
processed_paths.append(str(path))
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
for custom_component_dict in custom_component_dicts:
# custom_component_dict is a dict of dicts
if not custom_component_dict:
continue
category = list(custom_component_dict.keys())[0]
logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict
)
return merge_nested_dicts_with_renaming(
native_components, custom_components_from_file
)
def merge_nested_dicts(dict1, dict2):
for key, value in dict2.items():
if isinstance(value, dict) and isinstance(dict1.get(key), dict):
dict1[key] = merge_nested_dicts(dict1[key], value)
else:
dict1[key] = value
return dict1

View file

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

View file

@ -10,7 +10,7 @@ from langchain.base_language import BaseLanguageModel
from PIL.Image import Image
from loguru import logger
from langflow.services.chat.config import ChatConfig
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
def load_file_into_dict(file_path: str) -> dict:
@ -64,11 +64,11 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]:
def setup_llm_caching():
"""Setup LLM caching."""
settings_manager = get_settings_manager()
settings_service = get_settings_service()
try:
set_langchain_cache(settings_manager.settings)
set_langchain_cache(settings_service.settings)
except ImportError:
logger.warning(f"Could not import {settings_manager.settings.CACHE}. ")
logger.warning(f"Could not import {settings_service.settings.CACHE_TYPE}. ")
except Exception as exc:
logger.warning(f"Could not setup LLM caching. Error: {exc}")
@ -80,7 +80,7 @@ def set_langchain_cache(settings):
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
try:
cache_class = import_class(
f"langchain.cache.{cache_type or settings.CACHE}"
f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}"
)
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")

View file

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

View file

@ -2,10 +2,11 @@ import json
from pathlib import Path
from langchain.schema import AgentAction
from langflow.interface.run import (
build_sorted_vertices_with_caching,
build_sorted_vertices,
get_memory_key,
update_memory_keys,
)
from langflow.services.getters import get_session_service
from loguru import logger
from langflow.graph import Graph
from langchain.chains.base import Chain
@ -13,6 +14,8 @@ from langchain.vectorstores.base import VectorStore
from typing import Any, Dict, List, Optional, Tuple, Union
from langchain.schema import Document
from pydantic import BaseModel
def fix_memory_inputs(langchain_object):
"""
@ -65,7 +68,7 @@ def get_result_and_thought(langchain_object: Any, inputs: dict):
langchain_object.verbose = True
if hasattr(langchain_object, "return_intermediate_steps"):
langchain_object.return_intermediate_steps = True
langchain_object.return_intermediate_steps = False
fix_memory_inputs(langchain_object)
@ -93,26 +96,19 @@ def get_build_result(data_graph, session_id):
# otherwise, build the graph and return the result
if session_id:
logger.debug(f"Loading LangChain object from session {session_id}")
result = build_sorted_vertices_with_caching.get_result_by_session_id(session_id)
result = build_sorted_vertices(data_graph=data_graph)
if result is not None:
logger.debug("Loaded LangChain object")
return result
logger.debug("Building langchain object")
return build_sorted_vertices_with_caching(data_graph)
def clear_caches_if_needed(clear_cache: bool):
if clear_cache:
build_sorted_vertices_with_caching.clear_cache()
logger.debug("Cleared cache")
return build_sorted_vertices(data_graph)
def load_langchain_object(
data_graph: Dict[str, Any], session_id: str
) -> Tuple[Union[Chain, VectorStore], Dict[str, Any], str]:
langchain_object, artifacts = get_build_result(data_graph, session_id)
session_id = build_sorted_vertices_with_caching.hash
logger.debug("Loaded LangChain object")
if langchain_object is None:
@ -140,6 +136,7 @@ def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
raise ValueError("Inputs must be provided for a Chain")
logger.debug("Generating result and thought")
result = get_result_and_thought(langchain_object, inputs)
logger.debug("Generated result and thought")
elif isinstance(langchain_object, VectorStore):
result = langchain_object.search(**inputs)
@ -152,22 +149,34 @@ def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
return result
def process_graph_cached(
class Result(BaseModel):
result: Any
session_id: str
async def process_graph_cached(
data_graph: Dict[str, Any],
inputs: Optional[dict] = None,
clear_cache=False,
session_id=None,
) -> Tuple[Any, str]:
clear_caches_if_needed(clear_cache)
# If session_id is provided, load the langchain_object from the session
# else build the graph and return the result and the new session_id
langchain_object, artifacts, session_id = load_langchain_object(
data_graph, session_id
)
) -> Result:
session_service = get_session_service()
if clear_cache:
session_service.clear_session(session_id)
if session_id is None:
session_id = session_service.generate_key(
session_id=session_id, data_graph=data_graph
)
# Load the graph using SessionService
graph, artifacts = session_service.load_session(session_id, data_graph)
built_object = graph.build()
processed_inputs = process_inputs(inputs, artifacts)
result = generate_result(langchain_object, processed_inputs)
result = generate_result(built_object, processed_inputs)
# langchain_object is now updated with the new memory
# we need to update the cache with the updated langchain_object
session_service.update_session(session_id, (graph, artifacts))
return result, session_id
return Result(result=result, session_id=session_id)
def load_flow_from_json(

View file

@ -1,12 +1,12 @@
from langflow.services.factory import ServiceFactory
from langflow.services.auth.service import AuthManager
from langflow.services.auth.service import AuthService
class AuthManagerFactory(ServiceFactory):
name = "auth_manager"
class AuthServiceFactory(ServiceFactory):
name = "auth_service"
def __init__(self):
super().__init__(AuthManager)
super().__init__(AuthService)
def create(self, settings_manager):
return AuthManager(settings_manager)
def create(self, settings_service):
return AuthService(settings_service)

View file

@ -2,11 +2,11 @@ from langflow.services.base import Service
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from langflow.services.settings.manager import SettingsManager
from langflow.services.settings.manager import SettingsService
class AuthManager(Service):
name = "auth_manager"
class AuthService(Service):
name = "auth_service"
def __init__(self, settings_manager: "SettingsManager"):
self.settings_manager = settings_manager
def __init__(self, settings_service: "SettingsService"):
self.settings_service = settings_service

View file

@ -12,12 +12,12 @@ from langflow.services.database.models.user.crud import (
get_user_by_username,
update_user_last_login_at,
)
from langflow.services.getters import get_session, get_settings_manager
from langflow.services.getters import get_session, get_settings_service
from sqlmodel import Session
oauth2_login = OAuth2PasswordBearer(tokenUrl="api/v1/login")
API_KEY_NAME = "api-key"
API_KEY_NAME = "x-api-key"
api_key_query = APIKeyQuery(
name=API_KEY_NAME, scheme_name="API key query", auto_error=False
@ -33,17 +33,17 @@ async def api_key_security(
header_param: str = Security(api_key_header),
db: Session = Depends(get_session),
) -> Optional[User]:
settings_manager = get_settings_manager()
settings_service = get_settings_service()
result: Optional[Union[ApiKey, User]] = None
if settings_manager.auth_settings.AUTO_LOGIN:
if settings_service.auth_settings.AUTO_LOGIN:
# Get the first user
if not settings_manager.auth_settings.SUPERUSER:
if not settings_service.auth_settings.SUPERUSER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing first superuser credentials",
)
result = get_user_by_username(db, settings_manager.auth_settings.SUPERUSER)
result = get_user_by_username(db, settings_service.auth_settings.SUPERUSER)
elif not query_param and not header_param:
raise HTTPException(
@ -72,7 +72,7 @@ async def get_current_user(
token: Annotated[str, Depends(oauth2_login)],
db: Session = Depends(get_session),
) -> User:
settings_manager = get_settings_manager()
settings_service = get_settings_service()
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -83,14 +83,14 @@ async def get_current_user(
if isinstance(token, Coroutine):
token = await token
if settings_manager.auth_settings.SECRET_KEY is None:
if settings_service.auth_settings.SECRET_KEY is None:
raise credentials_exception
try:
payload = jwt.decode(
token,
settings_manager.auth_settings.SECRET_KEY,
algorithms=[settings_manager.auth_settings.ALGORITHM],
settings_service.auth_settings.SECRET_KEY,
algorithms=[settings_service.auth_settings.ALGORITHM],
)
user_id: UUID = payload.get("sub") # type: ignore
token_type: str = payload.get("type") # type: ignore
@ -130,19 +130,19 @@ def get_current_active_superuser(
def verify_password(plain_password, hashed_password):
settings_manager = get_settings_manager()
return settings_manager.auth_settings.pwd_context.verify(
settings_service = get_settings_service()
return settings_service.auth_settings.pwd_context.verify(
plain_password, hashed_password
)
def get_password_hash(password):
settings_manager = get_settings_manager()
return settings_manager.auth_settings.pwd_context.hash(password)
settings_service = get_settings_service()
return settings_service.auth_settings.pwd_context.hash(password)
def create_token(data: dict, expires_delta: timedelta):
settings_manager = get_settings_manager()
settings_service = get_settings_service()
to_encode = data.copy()
expire = datetime.now(timezone.utc) + expires_delta
@ -150,8 +150,8 @@ def create_token(data: dict, expires_delta: timedelta):
return jwt.encode(
to_encode,
settings_manager.auth_settings.SECRET_KEY,
algorithm=settings_manager.auth_settings.ALGORITHM,
settings_service.auth_settings.SECRET_KEY,
algorithm=settings_service.auth_settings.ALGORITHM,
)
@ -179,9 +179,9 @@ def create_super_user(
def create_user_longterm_token(db: Session = Depends(get_session)) -> dict:
settings_manager = get_settings_manager()
username = settings_manager.auth_settings.SUPERUSER
password = settings_manager.auth_settings.SUPERUSER_PASSWORD
settings_service = get_settings_service()
username = settings_service.auth_settings.SUPERUSER
password = settings_service.auth_settings.SUPERUSER_PASSWORD
if not username or not password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -225,10 +225,10 @@ def get_user_id_from_token(token: str) -> UUID:
def create_user_tokens(
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
) -> dict:
settings_manager = get_settings_manager()
settings_service = get_settings_service()
access_token_expires = timedelta(
minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
minutes=settings_service.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
access_token = create_token(
data={"sub": str(user_id)},
@ -236,7 +236,7 @@ def create_user_tokens(
)
refresh_token_expires = timedelta(
minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
minutes=settings_service.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
)
refresh_token = create_token(
data={"sub": str(user_id), "type": "rf"},
@ -255,13 +255,13 @@ def create_user_tokens(
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
settings_manager = get_settings_manager()
settings_service = get_settings_service()
try:
payload = jwt.decode(
refresh_token,
settings_manager.auth_settings.SECRET_KEY,
algorithms=[settings_manager.auth_settings.ALGORITHM],
settings_service.auth_settings.SECRET_KEY,
algorithms=[settings_service.auth_settings.ALGORITHM],
)
user_id: UUID = payload.get("sub") # type: ignore
token_type: str = payload.get("type") # type: ignore

View file

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

View file

@ -1,11 +1,13 @@
import abc
class BaseCache(abc.ABC):
class BaseCacheService(abc.ABC):
"""
Abstract base class for a cache.
"""
name = "cache_service"
@abc.abstractmethod
def get(self, key):
"""
@ -28,6 +30,16 @@ class BaseCache(abc.ABC):
value: The value to cache.
"""
@abc.abstractmethod
def upsert(self, key, value):
"""
Add an item to the cache if it doesn't exist, or update it if it does.
Args:
key: The key of the item.
value: The value to cache.
"""
@abc.abstractmethod
def delete(self, key):
"""

View file

@ -1,11 +1,35 @@
from langflow.services.cache.manager import CacheManager
from langflow.services.cache.manager import InMemoryCache, RedisCache, BaseCacheService
from langflow.services.factory import ServiceFactory
from langflow.utils.logger import logger
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from langflow.services.settings.manager import SettingsService
class CacheManagerFactory(ServiceFactory):
class CacheServiceFactory(ServiceFactory):
def __init__(self):
super().__init__(CacheManager)
super().__init__(BaseCacheService)
def create(self):
# Here you would have logic to create and configure a CacheManager
return CacheManager()
def create(self, settings_service: "SettingsService"):
# Here you would have logic to create and configure a CacheService
# based on the settings_service
if settings_service.settings.CACHE_TYPE == "redis":
logger.debug("Creating Redis cache")
redis_cache = RedisCache(
host=settings_service.settings.REDIS_HOST,
port=settings_service.settings.REDIS_PORT,
db=settings_service.settings.REDIS_DB,
expiration_time=settings_service.settings.REDIS_CACHE_EXPIRE,
)
if redis_cache.is_connected():
logger.debug("Redis cache is connected")
return redis_cache
logger.warning(
"Redis cache is not connected, falling back to in-memory cache"
)
return InMemoryCache()
elif settings_service.settings.CACHE_TYPE == "memory":
return InMemoryCache()

View file

@ -1,146 +0,0 @@
import threading
import time
from collections import OrderedDict
from langflow.services.cache.base import BaseCache
class InMemoryCache(BaseCache):
"""
A simple in-memory cache using an OrderedDict.
This cache supports setting a maximum size and expiration time for cached items.
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
Thread-safe using a threading Lock.
Attributes:
max_size (int, optional): Maximum number of items to store in the cache.
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
Example:
cache = InMemoryCache(max_size=3, expiration_time=5)
# setting cache values
cache.set("a", 1)
cache.set("b", 2)
cache["c"] = 3
# getting cache values
a = cache.get("a")
b = cache["b"]
"""
def __init__(self, max_size=None, expiration_time=60 * 60):
"""
Initialize a new InMemoryCache instance.
Args:
max_size (int, optional): Maximum number of items to store in the cache.
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
"""
self._cache = OrderedDict()
self._lock = threading.Lock()
self.max_size = max_size
self.expiration_time = expiration_time
def get(self, key):
"""
Retrieve an item from the cache.
Args:
key: The key of the item to retrieve.
Returns:
The value associated with the key, or None if the key is not found or the item has expired.
"""
with self._lock:
if key in self._cache:
item = self._cache.pop(key)
if (
self.expiration_time is None
or time.time() - item["time"] < self.expiration_time
):
# Move the key to the end to make it recently used
self._cache[key] = item
return item["value"]
else:
self.delete(key)
return None
def set(self, key, value):
"""
Add an item to the cache.
If the cache is full, the least recently used item is evicted.
Args:
key: The key of the item.
value: The value to cache.
"""
with self._lock:
if key in self._cache:
# Remove existing key before re-inserting to update order
self.delete(key)
elif self.max_size and len(self._cache) >= self.max_size:
# Remove least recently used item
self._cache.popitem(last=False)
self._cache[key] = {"value": value, "time": time.time()}
def get_or_set(self, key, value):
"""
Retrieve an item from the cache. If the item does not exist, set it with the provided value.
Args:
key: The key of the item.
value: The value to cache if the item doesn't exist.
Returns:
The cached value associated with the key.
"""
with self._lock:
if key in self._cache:
return self.get(key)
self.set(key, value)
return value
def delete(self, key):
"""
Remove an item from the cache.
Args:
key: The key of the item to remove.
"""
# with self._lock:
self._cache.pop(key, None)
def clear(self):
"""
Clear all items from the cache.
"""
with self._lock:
self._cache.clear()
def __contains__(self, key):
"""Check if the key is in the cache."""
return key in self._cache
def __getitem__(self, key):
"""Retrieve an item from the cache using the square bracket notation."""
return self.get(key)
def __setitem__(self, key, value):
"""Add an item to the cache using the square bracket notation."""
self.set(key, value)
def __delitem__(self, key):
"""Remove an item from the cache using the square bracket notation."""
self.delete(key)
def __len__(self):
"""Return the number of items in the cache."""
return len(self._cache)
def __repr__(self):
"""Return a string representation of the InMemoryCache instance."""
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"

View file

@ -1,153 +1,330 @@
from contextlib import contextmanager
from typing import Any, Awaitable, Callable, List, Optional
import threading
import time
from collections import OrderedDict
from langflow.services.base import Service
import pandas as pd
from PIL import Image
from langflow.services.cache.base import BaseCacheService
import pickle
from loguru import logger
class Subject:
"""Base class for implementing the observer pattern."""
class InMemoryCache(BaseCacheService, Service):
def __init__(self):
self.observers: List[Callable[[], None]] = []
"""
A simple in-memory cache using an OrderedDict.
def attach(self, observer: Callable[[], None]):
"""Attach an observer to the subject."""
self.observers.append(observer)
This cache supports setting a maximum size and expiration time for cached items.
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
Thread-safe using a threading Lock.
def detach(self, observer: Callable[[], None]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
Attributes:
max_size (int, optional): Maximum number of items to store in the cache.
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
observer()
Example:
cache = InMemoryCache(max_size=3, expiration_time=5)
class AsyncSubject:
"""Base class for implementing the async observer pattern."""
# setting cache values
cache.set("a", 1)
cache.set("b", 2)
cache["c"] = 3
def __init__(self):
self.observers: List[Callable[[], Awaitable]] = []
# getting cache values
a = cache.get("a")
b = cache["b"]
"""
def attach(self, observer: Callable[[], Awaitable]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], Awaitable]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
async def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
await observer()
class CacheManager(Subject, Service):
"""Manages cache for different clients and notifies observers on changes."""
name = "cache_manager"
def __init__(self):
super().__init__()
self._cache = {}
self.current_client_id = None
self.current_cache = {}
@contextmanager
def set_client_id(self, client_id: str):
def __init__(self, max_size=None, expiration_time=60 * 60):
"""
Context manager to set the current client_id and associated cache.
Initialize a new InMemoryCache instance.
Args:
client_id (str): The client identifier.
max_size (int, optional): Maximum number of items to store in the cache.
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
"""
self._cache = OrderedDict()
self._lock = threading.RLock()
self.max_size = max_size
self.expiration_time = expiration_time
def get(self, key):
"""
Retrieve an item from the cache.
Args:
key: The key of the item to retrieve.
Returns:
The value associated with the key, or None if the key is not found or the item has expired.
"""
with self._lock:
return self._get_without_lock(key)
def _get_without_lock(self, key):
"""
Retrieve an item from the cache without acquiring the lock.
"""
if item := self._cache.get(key):
if (
self.expiration_time is None
or time.time() - item["time"] < self.expiration_time
):
# Move the key to the end to make it recently used
self._cache.move_to_end(key)
unpickled = pickle.loads(item["value"])
return unpickled
else:
self.delete(key)
return None
def set(self, key, value):
"""
Add an item to the cache.
If the cache is full, the least recently used item is evicted.
Args:
key: The key of the item.
value: The value to cache.
"""
with self._lock:
if key in self._cache:
# Remove existing key before re-inserting to update order
self.delete(key)
elif self.max_size and len(self._cache) >= self.max_size:
# Remove least recently used item
self._cache.popitem(last=False)
# pickle locally to mimic Redis
pickled = pickle.dumps(value)
self._cache[key] = {"value": pickled, "time": time.time()}
def upsert(self, key, value):
"""
Inserts or updates a value in the cache.
If the existing value and the new value are both dictionaries, they are merged.
Args:
key: The key of the item.
value: The value to insert or update.
"""
with self._lock:
existing_value = self._get_without_lock(key)
if (
existing_value is not None
and isinstance(existing_value, dict)
and isinstance(value, dict)
):
existing_value.update(value)
value = existing_value
self.set(key, value)
def get_or_set(self, key, value):
"""
Retrieve an item from the cache. If the item does not exist,
set it with the provided value.
Args:
key: The key of the item.
value: The value to cache if the item doesn't exist.
Returns:
The cached value associated with the key.
"""
with self._lock:
if key in self._cache:
return self.get(key)
self.set(key, value)
return value
def delete(self, key):
"""
Remove an item from the cache.
Args:
key: The key of the item to remove.
"""
with self._lock:
self._cache.pop(key, None)
def clear(self):
"""
Clear all items from the cache.
"""
with self._lock:
self._cache.clear()
def __contains__(self, key):
"""Check if the key is in the cache."""
return key in self._cache
def __getitem__(self, key):
"""Retrieve an item from the cache using the square bracket notation."""
return self.get(key)
def __setitem__(self, key, value):
"""Add an item to the cache using the square bracket notation."""
self.set(key, value)
def __delitem__(self, key):
"""Remove an item from the cache using the square bracket notation."""
self.delete(key)
def __len__(self):
"""Return the number of items in the cache."""
return len(self._cache)
def __repr__(self):
"""Return a string representation of the InMemoryCache instance."""
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"
class RedisCache(BaseCacheService, Service):
"""
A Redis-based cache implementation.
This cache supports setting an expiration time for cached items.
Attributes:
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
Example:
cache = RedisCache(expiration_time=5)
# setting cache values
cache.set("a", 1)
cache.set("b", 2)
cache["c"] = 3
# getting cache values
a = cache.get("a")
b = cache["b"]
"""
def __init__(self, host="localhost", port=6379, db=0, expiration_time=60 * 60):
"""
Initialize a new RedisCache instance.
Args:
host (str, optional): Redis host.
port (int, optional): Redis port.
db (int, optional): Redis DB.
expiration_time (int, optional): Time in seconds after which a
ached item expires. Default is 1 hour.
"""
previous_client_id = self.current_client_id
self.current_client_id = client_id
self.current_cache = self._cache.setdefault(client_id, {})
try:
yield
finally:
self.current_client_id = previous_client_id
self.current_cache = self._cache.get(self.current_client_id, {})
import redis
except ImportError as exc:
raise ImportError(
"RedisCache requires the redis-py package."
" Please install Langflow with the deploy extra: pip install langflow[deploy]"
) from exc
logger.warning(
"RedisCache is an experimental feature and may not work as expected."
" Please report any issues to our GitHub repository."
)
self._client = redis.StrictRedis(host=host, port=port, db=db)
self.expiration_time = expiration_time
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
# check connection
def is_connected(self):
"""
Add an object to the current client's cache.
Check if the Redis client is connected.
"""
import redis
try:
self._client.ping()
return True
except redis.exceptions.ConnectionError:
return False
def get(self, key):
"""
Retrieve an item from the cache.
Args:
name (str): The cache key.
obj (Any): The object to cache.
obj_type (str): The type of the object.
"""
object_extensions = {
"image": "png",
"pandas": "csv",
}
if obj_type in object_extensions:
_extension = object_extensions[obj_type]
else:
_extension = type(obj).__name__.lower()
self.current_cache[name] = {
"obj": obj,
"type": obj_type,
"extension": extension or _extension,
}
self.notify()
def add_pandas(self, name: str, obj: Any):
"""
Add a pandas DataFrame or Series to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The pandas DataFrame or Series object.
"""
if isinstance(obj, (pd.DataFrame, pd.Series)):
self.add(name, obj.to_csv(), "pandas", extension="csv")
else:
raise ValueError("Object is not a pandas DataFrame or Series")
def add_image(self, name: str, obj: Any, extension: str = "png"):
"""
Add a PIL Image to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The PIL Image object.
"""
if isinstance(obj, Image.Image):
self.add(name, obj, "image", extension=extension)
else:
raise ValueError("Object is not a PIL Image")
def get(self, name: str):
"""
Get an object from the current client's cache.
Args:
name (str): The cache key.
key: The key of the item to retrieve.
Returns:
The cached object associated with the given cache key.
The value associated with the key, or None if the key is not found.
"""
return self.current_cache[name]
value = self._client.get(key)
return pickle.loads(value) if value else None
def get_last(self):
def set(self, key, value):
"""
Get the last added item in the current client's cache.
Add an item to the cache.
Returns:
The last added item in the cache.
Args:
key: The key of the item.
value: The value to cache.
"""
return list(self.current_cache.values())[-1]
try:
if pickled := pickle.dumps(value):
result = self._client.setex(key, self.expiration_time, pickled)
if not result:
raise ValueError("RedisCache could not set the value.")
except TypeError as exc:
raise TypeError(
"RedisCache only accepts values that can be pickled. "
) from exc
def upsert(self, key, value):
"""
Inserts or updates a value in the cache.
If the existing value and the new value are both dictionaries, they are merged.
cache_manager = CacheManager()
Args:
key: The key of the item.
value: The value to insert or update.
"""
existing_value = self.get(key)
if (
existing_value is not None
and isinstance(existing_value, dict)
and isinstance(value, dict)
):
existing_value.update(value)
value = existing_value
self.set(key, value)
def delete(self, key):
"""
Remove an item from the cache.
Args:
key: The key of the item to remove.
"""
self._client.delete(key)
def clear(self):
"""
Clear all items from the cache.
"""
self._client.flushdb()
def __contains__(self, key):
"""Check if the key is in the cache."""
return False if key is None else self._client.exists(key)
def __getitem__(self, key):
"""Retrieve an item from the cache using the square bracket notation."""
return self.get(key)
def __setitem__(self, key, value):
"""Add an item to the cache using the square bracket notation."""
self.set(key, value)
def __delitem__(self, key):
"""Remove an item from the cache using the square bracket notation."""
self.delete(key)
def __repr__(self):
"""Return a string representation of the RedisCache instance."""
return f"RedisCache(expiration_time={self.expiration_time})"

View file

@ -6,11 +6,14 @@ import os
import tempfile
from collections import OrderedDict
from pathlib import Path
from typing import Any, Dict
from typing import TYPE_CHECKING, Any, Dict
from appdirs import user_cache_dir
from fastapi import UploadFile
from langflow.services.database.models.base import orjson_dumps
if TYPE_CHECKING:
pass
CACHE: Dict[str, Any] = {}
CACHE_DIR = user_cache_dir("langflow", "langflow")
@ -166,7 +169,11 @@ def save_uploaded_file(file: UploadFile, folder_name):
"""
cache_path = Path(CACHE_DIR)
folder_path = cache_path / folder_name
file_extension = Path(file.filename).suffix
filename = file.filename
if isinstance(filename, str) or isinstance(filename, Path):
file_extension = Path(filename).suffix
else:
file_extension = ""
file_object = file.file
# Create the folder if it doesn't exist

View file

@ -0,0 +1,153 @@
from contextlib import contextmanager
from typing import Any, Awaitable, Callable, List, Optional
from langflow.services.base import Service
import pandas as pd
from PIL import Image
class Subject:
"""Base class for implementing the observer pattern."""
def __init__(self):
self.observers: List[Callable[[], None]] = []
def attach(self, observer: Callable[[], None]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], None]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
observer()
class AsyncSubject:
"""Base class for implementing the async observer pattern."""
def __init__(self):
self.observers: List[Callable[[], Awaitable]] = []
def attach(self, observer: Callable[[], Awaitable]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], Awaitable]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
async def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
await observer()
class CacheService(Subject, Service):
"""Manages cache for different clients and notifies observers on changes."""
name = "cache_service"
def __init__(self):
super().__init__()
self._cache = {}
self.current_client_id = None
self.current_cache = {}
@contextmanager
def set_client_id(self, client_id: str):
"""
Context manager to set the current client_id and associated cache.
Args:
client_id (str): The client identifier.
"""
previous_client_id = self.current_client_id
self.current_client_id = client_id
self.current_cache = self._cache.setdefault(client_id, {})
try:
yield
finally:
self.current_client_id = previous_client_id
self.current_cache = self._cache.get(self.current_client_id, {})
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
"""
Add an object to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The object to cache.
obj_type (str): The type of the object.
"""
object_extensions = {
"image": "png",
"pandas": "csv",
}
if obj_type in object_extensions:
_extension = object_extensions[obj_type]
else:
_extension = type(obj).__name__.lower()
self.current_cache[name] = {
"obj": obj,
"type": obj_type,
"extension": extension or _extension,
}
self.notify()
def add_pandas(self, name: str, obj: Any):
"""
Add a pandas DataFrame or Series to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The pandas DataFrame or Series object.
"""
if isinstance(obj, (pd.DataFrame, pd.Series)):
self.add(name, obj.to_csv(), "pandas", extension="csv")
else:
raise ValueError("Object is not a pandas DataFrame or Series")
def add_image(self, name: str, obj: Any, extension: str = "png"):
"""
Add a PIL Image to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The PIL Image object.
"""
if isinstance(obj, Image.Image):
self.add(name, obj, "image", extension=extension)
else:
raise ValueError("Object is not a PIL Image")
def get(self, name: str):
"""
Get an object from the current client's cache.
Args:
name (str): The cache key.
Returns:
The cached object associated with the given cache key.
"""
return self.current_cache[name]
def get_last(self):
"""
Get the last added item in the current client's cache.
Returns:
The last added item in the cache.
"""
return list(self.current_cache.values())[-1]
cache_service = CacheService()

View file

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

View file

@ -2,19 +2,17 @@ from collections import defaultdict
import uuid
from fastapi import WebSocket, status
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.services.base import Service
from langflow.services import service_manager
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.services.schema import ServiceType
from langflow.services.base import Service
from langflow.services.chat.cache import Subject
from langflow.services.chat.utils import process_graph
from loguru import logger
from .cache import cache_service
import asyncio
from typing import Any, Dict, List
from langflow.services.cache.flow import InMemoryCache
from langflow.services import service_manager, ServiceType
import orjson
@ -45,20 +43,20 @@ class ChatHistory(Subject):
self.history[client_id] = []
class ChatManager(Service):
name = "chat_manager"
class ChatService(Service):
name = "chat_service"
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
self.connection_ids: Dict[str, str] = {}
self.chat_history = ChatHistory()
self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER)
self.cache_manager.attach(self.update)
self.in_memory_cache = InMemoryCache()
self.chat_cache = cache_service
self.chat_cache.attach(self.update)
self.cache_service = service_manager.get(ServiceType.CACHE_SERVICE)
def on_chat_history_update(self):
"""Send the last chat message to the client."""
client_id = self.cache_manager.current_client_id
client_id = self.chat_cache.current_client_id
if client_id in self.active_connections:
chat_response = self.chat_history.get_history(
client_id, filter_messages=False
@ -79,8 +77,8 @@ class ChatManager(Service):
asyncio.run_coroutine_threadsafe(coroutine, loop)
def update(self):
if self.cache_manager.current_client_id in self.active_connections:
self.last_cached_object_dict = self.cache_manager.get_last()
if self.chat_cache.current_client_id in self.active_connections:
self.last_cached_object_dict = self.chat_cache.get_last()
# Add a new ChatResponse with the data
chat_response = FileResponse(
message=None,
@ -90,7 +88,7 @@ class ChatManager(Service):
)
self.chat_history.add_message(
self.cache_manager.current_client_id, chat_response
self.chat_cache.current_client_id, chat_response
)
async def connect(self, client_id: str, websocket: WebSocket):
@ -145,6 +143,7 @@ class ChatManager(Service):
websocket=self.active_connections[client_id],
session_id=self.connection_ids[client_id],
)
self.set_cache(client_id, langchain_object)
except Exception as e:
# Log stack trace
logger.exception(e)
@ -180,9 +179,15 @@ class ChatManager(Service):
"""
Set the cache for a client.
"""
# client_id is the flow id but that already exists in the cache
# so we need to change it to something else
self.in_memory_cache.set(client_id, langchain_object)
return client_id in self.in_memory_cache
result_dict = {
"result": langchain_object,
"type": type(langchain_object),
}
self.cache_service.upsert(client_id, result_dict)
return client_id in self.cache_service
async def handle_websocket(self, client_id: str, websocket: WebSocket):
await self.connect(client_id, websocket)
@ -203,10 +208,16 @@ class ChatManager(Service):
self.chat_history.history[client_id] = []
continue
with self.cache_manager.set_client_id(client_id):
langchain_object = self.in_memory_cache.get(client_id)
await self.process_message(client_id, payload, langchain_object)
with self.chat_cache.set_client_id(client_id):
if langchain_object := self.cache_service.get(client_id).get(
"result"
):
await self.process_message(client_id, payload, langchain_object)
else:
raise RuntimeError(
f"Could not find a LangChain object for client_id {client_id}"
)
except Exception as exc:
# Handle any exceptions that might occur
logger.error(f"Error handling websocket: {exc}")

View file

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

View file

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
from langflow.services.base import Service
from langflow.services.database.models.user.crud import get_user_by_username
from langflow.services.database.utils import Result, TableResults
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
from sqlalchemy import inspect
import sqlalchemy as sa
from sqlmodel import SQLModel, Session, create_engine
@ -16,8 +16,8 @@ if TYPE_CHECKING:
from sqlalchemy.engine import Engine
class DatabaseManager(Service):
name = "database_manager"
class DatabaseService(Service):
name = "database_service"
def __init__(self, database_url: str):
self.database_url = database_url
@ -30,10 +30,10 @@ class DatabaseManager(Service):
def _create_engine(self) -> "Engine":
"""Create the engine for the database."""
settings_manager = get_settings_manager()
settings_service = get_settings_service()
if (
settings_manager.settings.DATABASE_URL
and settings_manager.settings.DATABASE_URL.startswith("sqlite")
settings_service.settings.DATABASE_URL
and settings_service.settings.DATABASE_URL.startswith("sqlite")
):
connect_args = {"check_same_thread": False}
else:
@ -107,12 +107,10 @@ class DatabaseManager(Service):
# We will check that all models are in the database
# and that the database is up to date with all columns
sql_models = [models.Flow, models.User, models.ApiKey]
results = []
for sql_model in sql_models:
results.append(
TableResults(sql_model.__tablename__, self.check_table(sql_model))
)
return results
return [
TableResults(sql_model.__tablename__, self.check_table(sql_model))
for sql_model in sql_models
]
def check_table(self, model):
results = []
@ -164,12 +162,12 @@ class DatabaseManager(Service):
def teardown(self):
logger.debug("Tearing down database")
try:
settings_manager = get_settings_manager()
settings_service = get_settings_service()
# remove the default superuser if auto_login is enabled
# using the SUPERUSER to get the user
if settings_manager.auth_settings.AUTO_LOGIN:
if settings_service.auth_settings.AUTO_LOGIN:
logger.debug("Removing default superuser")
username = settings_manager.auth_settings.SUPERUSER
username = settings_service.auth_settings.SUPERUSER
with Session(self.engine) as session:
user = get_user_by_username(session, username)
session.delete(user)

View file

@ -22,8 +22,11 @@ class ApiKey(ApiKeyBase, table=True):
api_key: str = Field(index=True, unique=True)
# User relationship
# Delete API keys when user is deleted
user_id: UUID = Field(index=True, foreign_key="user.id")
user: "User" = Relationship(back_populates="api_keys")
user: "User" = Relationship(
back_populates="api_keys",
)
class ApiKeyCreate(ApiKeyBase):

View file

@ -63,9 +63,15 @@ def check_key(session: Session, api_key: str) -> Optional[ApiKey]:
def update_total_uses(session, api_key: ApiKey):
"""Update the total uses and last used at."""
api_key.total_uses += 1
api_key.last_used_at = datetime.datetime.now(datetime.timezone.utc)
session.add(api_key)
session.commit()
session.refresh(api_key)
return api_key
# This is running in a separate thread to avoid slowing down the request
# but session is not thread safe so we need to create a new session
with Session(session.get_bind()) as new_session:
new_api_key = new_session.get(ApiKey, api_key.id)
if new_api_key is None:
raise ValueError("API Key not found")
new_api_key.total_uses += 1
new_api_key.last_used_at = datetime.datetime.now(datetime.timezone.utc)
new_session.add(new_api_key)
new_session.commit()
return new_api_key

View file

@ -21,7 +21,10 @@ class User(SQLModelSerializable, table=True):
create_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
last_login_at: Optional[datetime] = Field()
api_keys: list["ApiKey"] = Relationship(back_populates="user")
api_keys: list["ApiKey"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"cascade": "delete"},
)
flows: list["Flow"] = Relationship(back_populates="user")
@ -42,6 +45,7 @@ class UserRead(SQLModel):
class UserUpdate(SQLModel):
username: Optional[str] = Field()
profile_image: Optional[str] = Field()
password: Optional[str] = Field()
is_active: Optional[bool] = Field()

View file

@ -6,21 +6,21 @@ from alembic.util.exc import CommandError
from sqlmodel import Session
if TYPE_CHECKING:
from langflow.services.database.manager import DatabaseManager
from langflow.services.database.manager import DatabaseService
def initialize_database():
logger.debug("Initializing database")
from langflow.services import service_manager, ServiceType
database_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
database_service = service_manager.get(ServiceType.DATABASE_SERVICE)
try:
database_manager.check_schema_health()
database_service.check_schema_health()
except Exception as exc:
logger.error(f"Error checking schema health: {exc}")
raise RuntimeError("Error checking schema health") from exc
try:
database_manager.run_migrations()
database_service.run_migrations()
except CommandError as exc:
if "Can't locate revision identified by" not in str(exc):
raise exc
@ -30,23 +30,23 @@ def initialize_database():
logger.warning(
"Wrong revision in DB, deleting alembic_version table and running migrations again"
)
with session_getter(database_manager) as session:
with session_getter(database_service) as session:
session.execute("DROP TABLE alembic_version")
database_manager.run_migrations()
database_service.run_migrations()
except Exception as exc:
# if the exception involves tables already existing
# we can ignore it
if "already exists" not in str(exc):
logger.error(f"Error running migrations: {exc}")
raise RuntimeError("Error running migrations") from exc
database_manager.create_db_and_tables()
database_service.create_db_and_tables()
logger.debug("Database initialized")
@contextmanager
def session_getter(db_manager: "DatabaseManager"):
def session_getter(db_service: "DatabaseService"):
try:
session = Session(db_manager.engine)
session = Session(db_service.engine)
yield session
except Exception as e:
print("Session rollback because of exception:", e)

View file

@ -3,24 +3,46 @@ from typing import TYPE_CHECKING, Generator
if TYPE_CHECKING:
from langflow.services.database.manager import DatabaseManager
from langflow.services.settings.manager import SettingsManager
from langflow.services.chat.manager import ChatManager
from langflow.services.database.manager import DatabaseService
from langflow.services.settings.manager import SettingsService
from langflow.services.cache.manager import BaseCacheService
from langflow.services.session.manager import SessionService
from langflow.services.task.manager import TaskService
from langflow.services.chat.manager import ChatService
from sqlmodel import Session
def get_settings_manager() -> "SettingsManager":
return service_manager.get(ServiceType.SETTINGS_MANAGER)
def get_settings_service() -> "SettingsService":
try:
return service_manager.get(ServiceType.SETTINGS_SERVICE)
except ValueError:
# initialize settings service
from langflow.services.manager import initialize_settings_service
initialize_settings_service()
return service_manager.get(ServiceType.SETTINGS_SERVICE)
def get_db_manager() -> "DatabaseManager":
return service_manager.get(ServiceType.DATABASE_MANAGER)
def get_db_service() -> "DatabaseService":
return service_manager.get(ServiceType.DATABASE_SERVICE)
def get_session() -> Generator["Session", None, None]:
db_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
yield from db_manager.get_session()
db_service = service_manager.get(ServiceType.DATABASE_SERVICE)
yield from db_service.get_session()
def get_chat_manager() -> "ChatManager":
return service_manager.get(ServiceType.CHAT_MANAGER)
def get_cache_service() -> "BaseCacheService":
return service_manager.get(ServiceType.CACHE_SERVICE)
def get_session_service() -> "SessionService":
return service_manager.get(ServiceType.SESSION_SERVICE)
def get_task_service() -> "TaskService":
return service_manager.get(ServiceType.TASK_SERVICE)
def get_chat_service() -> "ChatService":
return service_manager.get(ServiceType.CHAT_SERVICE)

View file

@ -1,9 +1,10 @@
from langflow.services.schema import ServiceType
from typing import TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, Dict, List, Optional
from loguru import logger
if TYPE_CHECKING:
from langflow.services.factory import ServiceFactory
from langflow.services.base import Service
class ServiceManager:
@ -12,7 +13,7 @@ class ServiceManager:
"""
def __init__(self):
self.services = {}
self.services: Dict[str, "Service"] = {}
self.factories = {}
self.dependencies = {}
@ -86,6 +87,8 @@ class ServiceManager:
Teardown all the services.
"""
for service in self.services.values():
if service is None:
continue
logger.debug(f"Teardown service {service.name}")
service.teardown()
self.services = {}
@ -94,3 +97,107 @@ class ServiceManager:
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
from langflow.services.session import factory as session_service_factory
from langflow.services.auth import factory as auth_factory
from langflow.services.task import factory as task_factory
service_manager.register_factory(settings_factory.SettingsServiceFactory())
service_manager.register_factory(
database_factory.DatabaseServiceFactory(),
dependencies=[ServiceType.SETTINGS_SERVICE],
)
service_manager.register_factory(
cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
)
service_manager.register_factory(
auth_factory.AuthServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
)
service_manager.register_factory(chat_factory.ChatServiceFactory())
service_manager.register_factory(
session_service_factory.SessionServiceFactory(),
dependencies=[ServiceType.CACHE_SERVICE],
)
service_manager.register_factory(
task_factory.TaskServiceFactory(),
)
# Test cache connection
service_manager.get(ServiceType.CACHE_SERVICE)
# Test database connection
service_manager.get(ServiceType.DATABASE_SERVICE)
# Test cache connection
service_manager.get(ServiceType.CACHE_SERVICE)
# Test database connection
service_manager.get(ServiceType.DATABASE_SERVICE)
def reinitialize_services():
"""
Reinitialize all the services needed.
"""
service_manager.update(ServiceType.SETTINGS_SERVICE)
service_manager.update(ServiceType.DATABASE_SERVICE)
service_manager.update(ServiceType.CACHE_SERVICE)
service_manager.update(ServiceType.CHAT_SERVICE)
service_manager.update(ServiceType.SESSION_SERVICE)
service_manager.update(ServiceType.AUTH_SERVICE)
service_manager.update(ServiceType.TASK_SERVICE)
# Test cache connection
service_manager.get(ServiceType.CACHE_SERVICE)
# Test database connection
service_manager.get(ServiceType.DATABASE_SERVICE)
# Test cache connection
service_manager.get(ServiceType.CACHE_SERVICE)
# Test database connection
service_manager.get(ServiceType.DATABASE_SERVICE)
def initialize_settings_service():
"""
Initialize the settings manager.
"""
from langflow.services.settings import factory as settings_factory
service_manager.register_factory(settings_factory.SettingsServiceFactory())
def initialize_session_service():
"""
Initialize the session manager.
"""
from langflow.services.session import factory as session_service_factory # type: ignore
from langflow.services.cache import factory as cache_factory
initialize_settings_service()
service_manager.register_factory(
cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
)
service_manager.register_factory(
session_service_factory.SessionServiceFactory(),
dependencies=[ServiceType.CACHE_SERVICE],
)
def teardown_services():
"""
Teardown all the services.
"""
service_manager.teardown()

View file

@ -1,4 +1,4 @@
from langflow.services.getters import get_settings_manager
from langflow.services.getters import get_settings_service
from langflow.utils.logger import logger
### Temporary implementation
@ -20,7 +20,7 @@ class LangfuseInstance:
logger.debug("Creating Langfuse instance")
from langfuse import Langfuse # type: ignore
settings_manager = get_settings_manager()
settings_manager = get_settings_service()
if (
settings_manager.settings.LANGFUSE_PUBLIC_KEY

View file

@ -7,8 +7,10 @@ class ServiceType(str, Enum):
registered with the service manager.
"""
AUTH_MANAGER = "auth_manager"
CACHE_MANAGER = "cache_manager"
SETTINGS_MANAGER = "settings_manager"
DATABASE_MANAGER = "database_manager"
CHAT_MANAGER = "chat_manager"
AUTH_SERVICE = "auth_service"
CACHE_SERVICE = "cache_service"
SETTINGS_SERVICE = "settings_service"
DATABASE_SERVICE = "database_service"
CHAT_SERVICE = "chat_service"
SESSION_SERVICE = "session_service"
TASK_SERVICE = "task_service"

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